Company Simulator

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

M02. Missing Slippage and Price Protection in Investment Logic

Root + Impact

Description

  • Under normal behavior, the fund_investor() function lets users invest ETH in exchange for shares, using a computed share_price that depends on company balance and debt.

  • The issue is that this calculation happens entirely on-chain at transaction execution time, with no guarantee that the price matches user expectations. Between the time a user submits the transaction and the time it is mined, the company_balance, issued_shares, or holding_debt can change, resulting in a different share_price.

  • This allows MEV bots or privileged actors to front-run investors, manipulate company balance, and cause users to receive fewer shares than expected.

@payable
@internal
def fund_investor():
...
share_price: uint256 = (
net_worth // max(self.issued_shares, 1)
if self.issued_shares > 0
else INITIAL_SHARE_PRICE
)
new_shares: uint256 = msg.value // share_price @> No check on expected vs actual share_price
...
self.shares[msg.sender] += new_shares
self.issued_shares += new_shares
self.company_balance += msg.value

Risk

Likelihood:

  • This occurs during normal operation whenever multiple investors interact in short time windows, or when the owner changes company balance.

  • It can be deliberately exploited by MEV actors monitoring the mempool for pending fund_investor() calls.

Impact:

  • Users can lose value by receiving fewer shares than they anticipated.

  • MEV actors or insiders can profit by momentarily manipulating company_balance before others’ transactions are executed.

Proof of Concept

1. Victim sends 10 ETH to fund_investor(), expecting 10,000 shares at current price.
2. Attacker front-runs by injecting 100 ETH via fund_owner(), increasing company_balance.
3. This increases `net_worth` and thus the computed `share_price`.
4. Victim’s transaction executes with a higher price, only receiving ~5,000 shares.
5. Attacker then withdraws their ETH (via owner withdrawal or refund path), restoring original balance but keeping the victim undercompensated.

Recommended Mitigation

Add slippage protection by allowing the investor to specify the minimum number of shares expected in the transaction.
If the actual number of shares to be issued falls below that threshold, the transaction should revert.

@payable
@external
-def fund_cyfrin(action: uint256):
+def fund_cyfrin(action: uint256, min_expected_shares: uint256 = 0):
if action == 0:
self.fund_owner()
elif action == 1:
- self.fund_investor()
+ self.fund_investor(min_expected_shares)
else:
raise "Input MUST be between 0 and 1!!!"
@payable
@internal
-def fund_investor():
+def fund_investor(min_expected_shares: uint256):
assert msg.value > 0, "Must send ETH!!!"
assert self.issued_shares <= self.public_shares_cap, "Share cap reached!!!"
assert self.company_balance > self.holding_debt, "Company is insolvent!!!"
net_worth: uint256 = 0
if self.company_balance > self.holding_debt:
net_worth = self.company_balance - self.holding_debt
share_price: uint256 = (
net_worth // max(self.issued_shares, 1)
if self.issued_shares > 0
else INITIAL_SHARE_PRICE
)
new_shares: uint256 = msg.value // share_price
+ # Enforce slippage protection
+ assert new_shares >= min_expected_shares, "Slippage exceeded or price moved"
available: uint256 = self.public_shares_cap - self.issued_shares
if new_shares > available:
new_shares = available
self.shares[msg.sender] += new_shares
self.issued_shares += new_shares
self.company_balance += msg.value
if self.share_received_time[msg.sender] == 0:
self.share_received_time[msg.sender] = block.timestamp
log SharesIssued(investor=msg.sender, amount=new_shares)

This mitigation gives users deterministic control over their investment outcome, ensuring the transaction fails if the on-chain price moves unfavorably between submission and execution.

Updates

Lead Judging Commences

0xshaedyw Lead Judge
10 days ago
0xshaedyw Lead Judge 8 days ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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