Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: medium
Valid

Investor ETH accepted but no shares issued when public cap reached

Root + Impact

Description

  • Normal behavior:
    Users send ETH to fund_investor() (via fund_cyfrin(1)) and should receive shares proportional to their contribution. If the requested number of shares would exceed the public cap, the investor should either receive only the allowable shares and any ETH excess refunded, or the transaction should revert.


  • Specific issue:
    When issued_shares == public_shares_cap, the function accepts the investor’s ETH, computes new_shares, clamps new_shares to available = public_shares_cap - issued_shares (which becomes 0), credits company_balance with the investor’s ETH and emits SharesIssued with amount 0. The investor ends up paying ETH and receiving zero shares with no refund or revert — effectively losing funds (or unintentionally funding the company without stake).

// Root cause in the codebase with @> marks to highlight the relevant section
# compute shares
new_shares: uint256 = msg.value // share_price
# Cap shares if exceeding visible limit
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 # <-- investor ETH accepted even when new_shares == 0

Risk

Likelihood:

  • This will occur whenever the public share cap is fully allocated (i.e., issued_shares == public_shares_cap), which is a normal state after public fundraising completes.

  • This will also occur during race/ordering scenarios where the cap is reached by other transactions in the same block or shortly before the investor’s transaction is processed.

Impact:

  • Investors may lose funds (their ETH is accepted but they receive zero shares).

  • Trust and reputation damage — investors who see money accepted but no shares will consider the system broken or malicious.

  • Economic & accounting inconsistency: company balance increases without corresponding ownership changes, which can be abused or create legal/auditing issues.

Proof of Concept

Explanation:

The contract accepted investor ETH and increased companyBalance even though newShares was trimmed to 0 because the cap was reached. The investor paid but received no shares.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @notice PoC: fundInvestor accepts ETH but issues 0 shares when cap reached
contract CyfrinPoC_Cap {
uint256 public companyBalance;
uint256 public publicSharesCap;
uint256 public issuedShares;
mapping(address => uint256) public shares;
uint256 public constant INITIAL_SHARE_PRICE = 1e15;
constructor(uint256 _cap) {
publicSharesCap = _cap;
}
// helper to set state for PoC
function setIssuedShares(uint256 _s) external { issuedShares = _s; }
// vulnerable mimic of fund_investor
function fundInvestor() external payable {
require(msg.value > 0, "Send ETH");
uint256 sharePrice = INITIAL_SHARE_PRICE; // simplified price for PoC
uint256 newShares = msg.value / sharePrice;
uint256 available = publicSharesCap - issuedShares;
if (newShares > available) {
newShares = available;
}
// update state — NOTE: companyBalance increases even when newShares == 0
shares[msg.sender] += newShares;
issuedShares += newShares;
companyBalance += msg.value;
}
}

Recommended Mitigation

Explanation: (issue available shares + refund excess) gives best UX: partial issuance with refund of leftover ETH, but requires careful math and a safe refund mechanism. Always perform state updates before external calls and use non-reentrant patterns.

- remove this code
+ add this code
+ available: uint256 = self.public_shares_cap - self.issued_shares
+ if available == 0:
+ # revert to avoid accepting ETH when no shares are available
+ raise "No public shares available"
+
+ if new_shares > available:
+ # calculate ETH cost for available shares and refund the rest
+ cost_for_available: uint256 = available * share_price
+ refund: uint256 = msg.value - cost_for_available
+
+ # issue the available shares, accept only cost_for_available
+ self.shares[msg.sender] += available
+ self.issued_shares += available
+ self.company_balance += cost_for_available
+
+ if refund > 0:
+ # send refund back to investor
+ raw_call(msg.sender, b"", value=refund, revert_on_failure=True)
+ else:
+ # normal flow
+ self.shares[msg.sender] += new_shares
+ self.issued_shares += new_shares
+ self.company_balance += msg.value
Updates

Lead Judging Commences

0xshaedyw Lead Judge
6 days ago
0xshaedyw Lead Judge 4 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Medium – Excess Contribution Not Refunded

Investor ETH above share cap is accepted without refund or shares, breaking fairness.

Support

FAQs

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