Company Simulator

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

withdraw_shares() reverts for all investors when company balance < payout (DoS condition)

Root + Impact

Description

  • Normal behavior: withdraw_shares() lets investors redeem their shares for ETH based on the calculated share_price, subject to caps and penalties.

  • Specific issue: If one investor withdraws most of the company’s funds (or the company spends balance elsewhere), then company_balance can fall below the payout required for the next investor. The next withdrawal attempt will hit:@> assert self.company_balance >= payout, "Insufficient company funds!!!"

    causing a revert, locking all remaining investors from withdrawing any ETH, even though they still hold shares. There’s no queuing or proportional payout fallback — the function simply fails.

// Root cause in the codebase with @> marks to highlight the relevant@external
def withdraw_shares():
...
@> assert self.company_balance >= payout, "Insufficient company funds!!!"
self.company_balance -= payout
raw_call(
msg.sender,
b"",
value=payout,
revert_on_failure=True,
)

Risk

Likelihood:

1.Happens whenever total redemption requests exceed available balance — likely after large withdrawals or production expenses.

2.Medium likelihood in real operation because balance fluctuations are common in on-chain companies or DeFi-style contracts.

Impact:

1.High — a single depleted balance locks every investor’s funds until the owner manually re-funds.

2.Reputation and trust impact — investors’ capital becomes illiquid.

3.Creates a denial-of-service for legitimate withdrawals and may break tokenomics or fairness assumptions.

Proof of Concept

Explanation:

After producing items or paying debt, company_balance drops.

When an investor calls withdraw_shares(), the assert fails, reverting the whole transaction and preventing any withdrawal.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/*
PoC: Demonstrate withdrawal denial when company_balance < payout.
Simplified pseudo-interface for the vulnerable contract.
*/
interface IVulnerable {
function fund_cyfrin(uint256 action) external payable;
function withdraw_shares() external;
}
contract WithdrawDoS {
IVulnerable target;
constructor(address _target) {
target = IVulnerable(_target);
}
function exploit() external payable {
// Step 1: Investor A funds company as investor
target.fund_cyfrin{value: 1 ether}(1);
// Step 2: Owner (or another call) drains company_balance manually
// For PoC, simulate this off-chain or by calling produce() to consume funds.
// Step 3: Attempt withdrawal after balance drop — this reverts:
target.withdraw_shares(); // <-- triggers "Insufficient company funds!!!"
}
}

Recommended Mitigation

Explanation (brief):

Instead of reverting when funds are insufficient, implement partial withdrawals or a queued payout system.

Investors should be able to withdraw up to the available balance, with the remainder owed later.

- remove this code
+ add this code
- assert self.company_balance >= payout, "Insufficient company funds!!!"
- self.company_balance -= payout
- raw_call(msg.sender, b"", value=payout, revert_on_failure=True)
+ if self.company_balance < payout:
+ # Pay what is available; record unpaid remainder for future claim
+ owed: uint256 = payout - self.company_balance
+ payout = self.company_balance
+ self.unpaid_withdrawals[msg.sender] = owed
+
+ self.company_balance -= payout
+ raw_call(msg.sender, b"", value=payout, revert_on_failure=True)
Updates

Lead Judging Commences

0xshaedyw Lead Judge
6 days ago
0xshaedyw Lead Judge 5 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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