Company Simulator

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

Investor{dust} accepted--->0 share minted- ETH retained

Author Revealed upon completion

User ETH can be permanently lost when sending a small {dust} investment, funds increase company balance while no shares are issued.

##Summary:
This finding explains how extremely small ETH deposits can be accepted without minting shares, causing silent user losses. The issue occurs due to a missing guard condition in the
fund_investor() function.

##Description:
The
fund_investor() function accepts extremely small {dust}ETH investments that are too small to mint even one share. These transactions still increase the company’s balance but yield zero shares for the user, effectively donating ETH to the protocol without benefit. This results in permanent user value loss and breaks the expected share-minting logic.

###Normal behavior

fund_cyfrin(1) should mint shares for every ETH invested or revert/refund if insufficient to purchase a share.

Problem behavior

Tiny (“dust”) ETH amounts are accepted but mint 0 shares, resulting in silent donations to the protocol.



// Root cause in the codebase with @> marks to highlight the relevant sectionsrc/Cyfrin_Hub.vy
310 # Calculate shares based on contribution
315 share_price: uint256 = (
316 net_worth // max(self.issued_shares, 1)
317 if self.issued_shares > 0
318 else INITIAL_SHARE_PRICE
319 )
@>320 new_shares: uint256 = msg.value // share_price # floor division → may be 0
@>321 # No check for new_shares > 0, no refund path

Risk

Investors can lose funds with no shares received.

  • Malicious UIs can exploit this to siphon dust.

  • Violates protocol’s expected guarantee that every contribution yields proportional equity.

Likelihood:

Occurs whenever a user invests less than one share’s price (common in testnets, faucets, or mis-calculated inputs).

Malicious UIs can exploit this to siphon dust.

  • Violates protocol’s expected guarantee that every contribution yields proportional equity.

  • No guard conditions prevent it; happens deterministically with integer division.

  • Permanent user value loss (ETH transferred but 0 shares).

  • No guard conditions prevent it; happens deterministically with integer division.

Proof of Concept

PoC explanation:
This proof of concept demonstrates how a user can send a very small (“dust”) investment that is accepted by the contract yet mints zero shares. The test uses a simulated attacker account that contributes 1e12 wei (0.000001 ETH). After the transaction, the company balance increases while the attacker’s share count remains zero—proving ETH loss without ownership gain.

# Deploy baseline contracts
mox run deploy
# Run PoC
mox test tests/poc/test_investor_dust_rounding.py -q
# or sweep
mox test tests/poc/test_investor_dust_sweep.py -q
AssertionError: Investor can donate dust: 0 shares minted but ETH accepted

Recommended Mitigation

Mitigation explanation:
The mitigation ensures that every investor contribution either results in at least one share or reverts. By asserting that the calculated share amount (desired) is greater than zero, the function prevents acceptance of dust deposits. Adding an optional refund path ensures any leftover ETH is returned, maintaining user fairness and protocol integrity.

@@ fund_investor():
+ desired: uint256 = msg.value // share_price
+ assert desired > 0, "Investment too small for 1 share"
+ minted: uint256 = min(desired, self.public_shares_cap - self.issued_shares)
+ cost: uint256 = minted * share_price
+ self.company_balance += cost
+ self.issued_shares += minted
+ self.shares[msg.sender] += minted
+ log SharesIssued(investor=msg.sender, amount=minted)
+ refund: uint256 = msg.value - cost
+ if refund > 0:
+ send(msg.sender, refund)

Support

FAQs

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