Company Simulator

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

Failed Sales Capture Funds

Author Revealed upon completion

Root + Impact

Description

sell_to_customer accepts payment from the CustomerEngine even when inventory cannot satisfy the order. Rather than reverting or refunding msg.value, the function simply lowers reputation and exits successfully. The transferred ETH stays in the contract, but the ledger is never updated.

if self.inventory >= requested:
...
self.company_balance += revenue
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: Stockouts are inevitable once demand exceeds production; no attacker coordination is required.

Impact:

  • Customers permanently lose their payment despite receiving no goods.

  • The unaccounted ETH is trapped: company_balance never increases, so withdrawals and net-worth metrics exclude the funds.

  • Attackers can grief buyers by emptying inventory and letting them burn ETH while reputation merely ticks down.

Proof of Concept

  1. Set inventory = 0, but leave CustomerEngine active.

  2. Call trigger_demand so it forwards total_cost to sell_to_customer.

  3. The call succeeds; the buyer loses ETH, the company has no inventory change, and company_balance is unchanged.

  4. Shareholders cannot withdraw the stuck ETH because the ledger thinks no cash is available.

Simplified pytest outline:

def test_failed_sale_keeps_payment(hub, engine, buyer):
hub.set_customer_engine(engine.address, sender=hub.owner())
engine.trigger_demand(value=5 * engine.ITEM_PRICE(), sender=buyer)
assert hub.inventory() == 0
assert hub.company_balance() == 0 # payment not recorded

Recommended Mitigation

  1. Revert the sale when inventory is insufficient so CustomerEngine refunds the caller automatically.

  2. Alternatively, explicitly refund msg.value inside sell_to_customer before returning.

  3. If business logic dictates keeping the payment, add it to company_balance so accounting remains accurate.

Patch example (revert approach):

if self.inventory >= requested:
...
log Sold(amount=requested, revenue=revenue)
else:
- self.reputation = min(max(self.reputation - REPUTATION_PENALTY, 0), 100)
- log ReputationChanged(new_reputation=self.reputation)
+ self.reputation = min(max(self.reputation - REPUTATION_PENALTY, 0), 100)
+ log ReputationChanged(new_reputation=self.reputation)
+ raise "Insufficient inventory!!!"

Support

FAQs

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