"""
POC-001: Customer Funds Lost on Failed Sales
Severity: HIGH | Likelihood: High | Impact: High
Deterministic reproduction that customers permanently lose ETH when
`Cyfrin_Hub.sell_to_customer()` is invoked with insufficient inventory.
The function is `@payable`, accepts funds forwarded by `CustomerEngine`,
but never refunds when the inventory check fails. The ETH remains in
contract balance while `company_balance` accounting stays unchanged.
"""
import boa
from eth_utils import keccak, to_wei
ITEM_PRICE = to_wei(0.02, "ether")
COOLDOWN = 60
def _addr_to_int(addr) -> int:
if isinstance(addr, int):
return addr
if hasattr(addr, "hex"):
addr_hex = addr.hex()
else:
addr_hex = str(addr)
if addr_hex.startswith("0x"):
addr_hex = addr_hex[2:]
return int(addr_hex, 16)
def _predicted_request(timestamp: int, customer, reputation: int) -> int:
"""Mirror CustomerEngine demand calculation for logging/debug."""
seed_bytes = keccak(
timestamp.to_bytes(32, "big")
+ _addr_to_int(customer).to_bytes(32, "big")
)
seed = int.from_bytes(seed_bytes, "big")
base = seed % 5
extra = 1 if (seed % 100) < (reputation - 50) else 0
requested = base + 1 + extra
return min(requested, 5)
def test_poc_001_customer_eth_stuck_on_failed_sale(
industry_contract,
customer_engine_contract,
OWNER,
PATRICK,
):
"""
Scenario: Company has zero inventory but reputation is high enough to
attract demand. CustomerEngine forwards ETH to Cyfrin_Hub. The sale
fails, yet Cyfrin_Hub keeps the ETH, never refunding and never crediting
`company_balance`.
"""
assert industry_contract.inventory() == 0
assert industry_contract.company_balance() == 0
assert industry_contract.reputation() == 100
boa.env.set_balance(PATRICK, to_wei(5, "ether"))
boa.env.time_travel(seconds=COOLDOWN + 5)
ts_before = boa.env.timestamp
predicted = _predicted_request(ts_before, PATRICK, industry_contract.reputation())
customer_balance_before = boa.env.get_balance(PATRICK)
contract_balance_before = boa.env.get_balance(industry_contract.address)
company_balance_before = industry_contract.company_balance()
reputation_before = industry_contract.reputation()
payment_ceiling = ITEM_PRICE * 5
with boa.env.prank(PATRICK):
customer_engine_contract.trigger_demand(value=payment_ceiling)
ts_after = boa.env.timestamp
customer_balance_after = boa.env.get_balance(PATRICK)
contract_balance_after = boa.env.get_balance(industry_contract.address)
company_balance_after = industry_contract.company_balance()
inventory_after = industry_contract.inventory()
reputation_after = industry_contract.reputation()
paid_by_customer = customer_balance_before - customer_balance_after
eth_stuck = (contract_balance_after - company_balance_after) - (
contract_balance_before - company_balance_before
)
assert paid_by_customer > 0, "Customer must lose ETH"
assert company_balance_after == company_balance_before == 0, (
"Accounting must ignore failed sale"
)
assert inventory_after == 0, "Inventory unchanged because sale failed"
assert reputation_after == reputation_before - 5, "Failure penalty applied"
assert contract_balance_after - contract_balance_before == paid_by_customer, (
"All ETH forwarded to hub contract"
)
assert eth_stuck == paid_by_customer, (
"Entire payment stuck (self.balance - company_balance)"
)
with boa.reverts():
with boa.env.prank(PATRICK):
industry_contract.withdraw_shares()
trapped_balance = contract_balance_after - company_balance_after
print("\n<<< POC-001 >>>")
print(f"timestamp_before={ts_before}")
print(f"timestamp_after={ts_after}")
print(f"predicted_request={predicted} items")
print(f"customer_paid={paid_by_customer} wei")
print(f"hub_contract_balance={contract_balance_after} wei")
print(f"company_accounting_balance={company_balance_after} wei")
print(f"reputation_drop={reputation_before - reputation_after}")
print(f"eth_stuck={eth_stuck} wei")
print(
f"state_snapshot="
f"{{'raw_balance': {contract_balance_after}, "
f"'accounting_balance': {company_balance_after}, "
f"'trapped_delta': {trapped_balance}}}"
)
print("[fail] sell_to_customer refunds=0; ETH trapped")