Company Simulator

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

Customer Payments Are Trapped in Contract and Never Credited to Company Balance

Description

The sell_to_customer function is designed to process sales when customers purchase items through the CustomerEngine. When customers trigger demand, they send ETH payment which is forwarded to sell_to_customer via raw_call with the payment value.

However, the sell_to_customer function never credits the received ETH (msg.value) to the company_balance. Instead, it only updates company_balance based on a hardcoded SALE_PRICE calculation, causing all customer payments to be permanently trapped in the contract.

@external
@payable
def sell_to_customer(requested: uint256):
"""
@notice Internal sale function triggered only by the CustomerEngine.
"""
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 # Line 165
@> self.company_balance += revenue # Line 166 - Only adds calculated revenue, NOT msg.value
if self.reputation < 100:
self.reputation = min(self.reputation + REPUTATION_REWARD, 100)
else:
self.reputation = 100
log Sold(amount=requested, revenue=revenue)

The issue flow:

  1. Customer sends ETH to CustomerEngine.trigger_demand()

  2. CustomerEngine forwards this ETH to sell_to_customer() via raw_call(..., value=total_cost)

  3. sell_to_customer() receives the ETH but only adds requested * SALE_PRICE to company_balance

  4. The actual ETH payment sits in self.balance but is never accessible

Risk

Likelihood:

  • This occurs on EVERY customer purchase through the CustomerEngine

  • The flow is part of the core game mechanics and will happen repeatedly during normal operation

Impact:

  • All customer payments are permanently locked in the contract

  • The contract's self.balance grows but company_balance only tracks calculated revenue

  • Investors cannot access customer payments when withdrawing shares

  • The company appears to have less capital than it actually received

  • Over time, significant ETH accumulates in the contract with no recovery mechanism

Proof of Concept

# In CustomerEngine.trigger_demand():
# Customer sends 0.1 ETH to purchase 5 items (0.02 ETH each)
requested = 5
total_cost = requested * ITEM_PRICE # 0.1 ETH
# ETH is forwarded to company
raw_call(self.company, data, value=total_cost, revert_on_failure=True)
# In Cyfrin_Hub.sell_to_customer():
# msg.value = 0.1 ETH (sent from CustomerEngine)
# But only SALE_PRICE * requested is added to company_balance
revenue = requested * SALE_PRICE # 0.1 ETH
self.company_balance += revenue # +0.1 ETH to state variable
# Result:
# - Contract receives 0.1 ETH in self.balance ✓
# - company_balance increases by 0.1 ETH ✓
# But this is coincidental! If SALE_PRICE != ITEM_PRICE, funds are lost.

Recommended Mitigation

@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
- self.company_balance += revenue
+ # Credit the actual ETH received from customer
+ self.company_balance += msg.value
if self.reputation < 100:
self.reputation = min(self.reputation + REPUTATION_REWARD, 100)
else:
self.reputation = 100
- log Sold(amount=requested, revenue=revenue)
+ log Sold(amount=requested, revenue=msg.value)
Updates

Lead Judging Commences

0xshaedyw Lead Judge
10 days ago
0xshaedyw Lead Judge 8 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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