Company Simulator

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

Max Payout Cap Loss

Author Revealed upon completion

Root + Impact

Description

  • Withdrawals are expected to redeem shares at the contract’s current net-asset value (shares_owned * share_price).

  • withdraw_shares forcibly caps every redemption to MAX_PAYOUT_PER_SHARE (0.002 ETH) per share, so whenever the share price grows above that threshold, investors permanently lose the excess value they paid for. Affected Target: Cyfrin_Hub.vy::withdraw_shares.

max_payout: uint256 = shares_owned * MAX_PAYOUT_PER_SHARE
if payout > max_payout:
@> payout = max_payout

Risk

Likelihood:

  • Sales revenue and owner recapitalizations routinely push net asset value per share above 0.002 ETH while the number of issued shares remains modest.

  • Nothing in fund_investor or other flows prevents new users from buying at these higher NAVs, so affected deposits are common.

Impact:

  • Investors who buy when NAV > 0.002 ETH lose the majority of their principal at withdrawal (e.g., paying 10 ETH but receiving only 0.198 ETH back in the PoC).

  • The cap lets the company siphon away accumulated profits without compensating shareholders, breaking basic economic guarantees.

Proof of Concept

  1. Successful operations or owner infusions push per-share NAV far beyond MAX_PAYOUT_PER_SHARE.

  2. A user purchases shares at this elevated valuation, expecting to redeem the full amount later.

  3. When they withdraw, the payout is clamped to the cap, stripping most of the investment despite sufficient treasury funds.

# Owner seeds 10 ETH, early investor mints 1k shares, owner injects +100 ETH
with boa.env.prank(owner):
hub.fund_cyfrin(0, value=to_wei(10, "ether"))
with boa.env.prank(alice):
hub.fund_cyfrin(1, value=to_wei(1, "ether"))
with boa.env.prank(owner):
hub.fund_cyfrin(0, value=to_wei(100, "ether"))
# Bob buys at ~0.11 ETH/share, withdraws later, and receives only 0.198 ETH
with boa.env.prank(bob):
hub.fund_cyfrin(1, value=to_wei(10, "ether"))
boa.env.time_travel(31 * 86400)
with boa.env.prank(bob):
hub.withdraw_shares()

The execution shows bob shares 90, a final balance of 190.18 ETH (9.82 ETH loss on a 10 ETH investment), and the contract still holding 120.82 ETH, proving the cap confiscates investor value.【F:src/Cyfrin_Hub.vy†L180-L211】【e98dba†L1-L5】

Recommended Mitigation

Remove the hard cap and rely on NAV-based pricing so withdrawals honor full share value

- max_payout: uint256 = shares_owned * MAX_PAYOUT_PER_SHARE
- if payout > max_payout:
- payout = max_payout

Support

FAQs

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