Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: medium
Valid

Weak Randomness Allows Manipulation of Customer Demand

[H-01] Weak Randomness Allows Manipulation of Customer Demand

Description

The trigger_demand() function in CustomerEngine.vy derives a pseudo-random seed from block.timestamp and msg.sender:

@external
@payable
def trigger_demand():
...
# @audit Use weak randomness which can be manipulated.
seed: uint256 = convert(
keccak256(
concat(
convert(block.timestamp, bytes32),
convert(msg.sender, bytes32)
)
),
uint256,
)

This is insecure because both inputs are predictable and partially controllable:

  • block.timestamp can be nudged by miners/validators within protocol allowances.

  • msg.sender is attacker-controlled; an attacker can try many EOAs or contract addresses to bias the outcome.

Because this seed directly influences economic behaviour (requested items and extra-item chance), attackers can bias demand to inflate profit/reputation or otherwise distort simulation results.

Risk

Likelihood:

  • High — the random seed can be manipulated through multiple calls or timestamp control.

Impact:

  • High — allows attackers to bias item purchase outcomes and distort company performance.

Proof of Concept

Measure inventory reduction after multiple trigger_demand() calls from different accounts while advancing block time past cooldown. If reductions vary, randomness is manipulable.

# tests/test_weak_randomness.py
# PoC: Weak randomness in CustomerEngine.trigger_demand() — inventory-delta version
import boa
import pytest
from eth_utils import to_wei
CALL_VALUE = to_wei(0.1, "ether")
FUND_VALUE = to_wei(10, "ether")
PRODUCE_AMOUNT = 200 # ensure plenty of inventory
def _make_attacker_accounts(n=8, balance=to_wei(1, "ether")):
addrs = []
for i in range(n):
name = f"attacker_{i}"
addr = boa.env.generate_address(name)
boa.env.set_balance(addr, balance)
addrs.append(addr)
return addrs
def _get_cooldown_seconds(contract):
try:
cooldown = contract.COOLDOWN()
return int(cooldown)
except Exception:
return 300
def test_poc_weak_randomness_inventory(customer_engine_contract, industry_contract, OWNER):
# ARRANGE: fund & produce
boa.env.set_balance(OWNER, FUND_VALUE)
with boa.env.prank(OWNER):
industry_contract.fund_cyfrin(0, value=FUND_VALUE)
industry_contract.produce(PRODUCE_AMOUNT)
attackers = _make_attacker_accounts(n=8, balance=to_wei(5, "ether"))
cooldown_seconds = _get_cooldown_seconds(customer_engine_contract)
step = max(cooldown_seconds + 1, 400) # safe time-travel step
last_inventory = int(industry_contract.inventory())
observed_deltas = []
for idx, attacker in enumerate(attackers):
# advance time to bypass per-address cooldown
boa.env.time_travel(step * (idx + 1))
with boa.env.prank(attacker):
customer_engine_contract.trigger_demand(value=CALL_VALUE)
new_inventory = int(industry_contract.inventory())
delta = last_inventory - new_inventory
last_inventory = new_inventory
observed_deltas.append(delta)
print(f"[poc] attacker={attacker} consumed={delta} items (inventory={new_inventory})")
unique_deltas = set(observed_deltas)
assert len(observed_deltas) >= 2
assert len(unique_deltas) >= 2, (
"PoC failed: all observed inventory reductions were identical. "
"Randomness may not be influenced by msg.sender/timestamp in current behavior."
)

Run with:

mox test tests/test_weak_randomness.py -vv
  • What this shows?
    If unique_deltas contains two or more different values, varying msg.sender and timestamp produced different demand sizes — demonstrating manipulable randomness.

Recommended Mitigation

  • Replace the insecure seed derivation with a secure randomness source. Preferably use Chainlink VRF for economic flows. If VRF is not feasible, a commit–reveal scheme can be used. Avoid using block.timestamp or msg.sender alone. As a temporary mitigation, blockhash(block.number - 1) can reduce timestamp manipulation, but it is not fully secure.

- seed: uint256 = convert(
- keccak256(
- concat(
- convert(block.timestamp, bytes32),
- convert(msg.sender, bytes32)
- )
- ),
- uint256,
- )
+ prev_hash: bytes32 = blockhash(block.number - 1)
+ seed: uint256 = convert(
+ keccak256(
+ concat(
+ prev_hash,
+ convert(msg.sender, bytes32),
+ )
+ ),
+ uint256,
+ )
Updates

Lead Judging Commences

0xshaedyw Lead Judge
about 1 month ago
0xshaedyw Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Medium – Predictable Seed

Demand randomness is grindable via timestamp and sender, enabling biased outcomes and reputation manipulation.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!