Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

Accounting mismatch: revenue not tied to msg.value in sell_to_customer()

Description

  • With the intended CustomerEngine caller, trigger_demand() enforces payment (msg.value >= total_cost), refunds any excess, and forwards exactly total_cost to CompanyGame.sell_to_customer(requested).

  • CompanyGame.sell_to_customer() credits self.company_balance += requested * SALE_PRICE without asserting the actual ETH received (msg.value). If CUSTOMER_ENGINE is set to a wrong address (EOA or a malicious/buggy contract) or is later replaced/compromised, it can call sell_to_customer() with less or zero ETH and still cause internal revenue and reputation to increase—creating accounting drift and false solvency.

// Root cause in the codebase with @> marks to highlight the relevant section
// File: Cyfrin_Hub.vy (CompanyGame)
@external
@payable
def sell_to_customer(requested: uint256):
assert msg.sender == self.CUSTOMER_ENGINE, "Not the customer engine!!!"
assert not self._is_bankrupt(), "Company is bankrupt!!!"
self._apply_holding_cost()
if self.inventory >= requested:
self.inventory -= requested
revenue: uint256 = requested * SALE_PRICE
// @> BUG: credits internal balance using computed revenue
// @> without checking the actual ETH sent (msg.value)
self.company_balance += revenue
if self.reputation < 100:
self.reputation = min(self.reputation + REPUTATION_REWARD, 100)
else:
self.reputation = 100
log Sold(amount=requested, revenue=revenue)
else:
self.reputation = min(max(self.reputation - REPUTATION_PENALTY, 0), 100)
log ReputationChanged(new_reputation=self.reputation)

Risk

Likelihood (under misconfiguration/compromise): Low

  • Occurs when CUSTOMER_ENGINE is set to a non‑conforming caller (e.g., EOA) or a contract that forwards the wrong value; the current set_customer_engine does not verify code size and allows any address once.

  • Occurs when the trusted engine later changes behavior (upgrade proxy, bug, or malicious actor) and calls sell_to_customer() with underpayment.

Impact: Medium

  • False solvency / accounting driftcompany_balance inflates without matching ETH, causing downstream reverts (withdrawals, production) and misleading “net worth” calculations.

  • Reputation pumping — “Successful” sales increase reputation even if no ETH was actually paid, weakening demand gating in the engine.

POC

This PoC does not use the provided CustomerEngine. It demonstrates misconfiguration by setting CUSTOMER_ENGINE to an EOA and calling with zero value.

# scripts/poc_accounting_mismatch_misconfig.py
# Run: mox run scripts/poc_accounting_mismatch_misconfig.py
import boa
COMPANY_PATH = "src/Cyfrin_Hub.vy"
SALE_PRICE = 2 * 10**16
REQUESTED = 3
def main():
owner = boa.env.generate_address()
engine = boa.env.generate_address() # EOA misconfigured as CUSTOMER_ENGINE
for a in (owner, engine):
boa.env.set_balance(a, 10**21)
# Deploy company and mis-set the engine to an EOA
with boa.env.prank(owner):
company = boa.load(COMPANY_PATH)
company.set_customer_engine(engine)
company.fund_cyfrin(0, value=10**19)
company.produce(100)
base_internal = company.get_balance()
base_real = boa.env.get_balance(company.address)
# Engine EOA calls with zero ETH
with boa.env.prank(engine):
company.sell_to_customer(REQUESTED, value=0)
after_internal = company.get_balance()
after_real = boa.env.get_balance(company.address)
expected_add = SALE_PRICE * REQUESTED
assert after_internal == base_internal + expected_add
assert after_real == base_real
print(f"[✓] Misconfig PoC: internal +{expected_add} without ETH received")
print(f" internal: {base_internal} -> {after_internal}")
print(f" real ETH: {base_real} -> {after_real}")
if __name__ == "__main__":
main()

Recommended Mitigation

Even if the Author keeps the current engine design, it's recommended to make sell_to_customer() payment-aware and tighten configuration:

@external
@payable
def sell_to_customer(requested: uint256):
assert msg.sender == self.CUSTOMER_ENGINE, "Not the customer engine!!!"
assert not self._is_bankrupt(), "Company is bankrupt!!!"
self._apply_holding_cost()
if self.inventory >= requested:
revenue: uint256 = requested * SALE_PRICE
+ # Enforce exact payment to prevent accounting drift
+ assert msg.value == revenue, "Incorrect payment for requested items"
- self.company_balance += revenue
+ self.company_balance += msg.value
self.inventory -= requested
# reputation update unchanged...
Updates

Lead Judging Commences

0xshaedyw Lead Judge
4 days ago
0xshaedyw Lead Judge 2 days ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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