Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

Weak Randomness Allows Manipulation of Customer Demand

Author Revealed upon completion

[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,
+ )

Support

FAQs

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