Description
-
Public investment flows through fund_cyfrin(1) → fund_investor(). The function computes a share price from the company’s net worth per outstanding share, and then mints new_shares = msg.value // share_price. If no shares exist, it falls back to INITIAL_SHARE_PRICE.
-
When some shares already exist (issued_shares > 0) but the company’s net worth is smaller than the outstanding share count (e.g., net_worth < issued_shares), the integer division
share_price = net_worth // issued_shares evaluates to 0. The next line
new_shares = msg.value // share_price then attempts division by zero, reverting every investment and creating a DoS for new investors until state changes (e.g., owner funding or debt reduction).
// Root cause in the codebase with @> marks to highlight the relevant lines
// File: Cyfrin_Hub.vy (CompanyGame)
@payable
@internal
def fund_investor():
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
Risk
Likelihood: Medium
-
Occurs when net worth per issued share becomes < 1 wei, for example, after large issuance at high valuations followed by net worth shrinkage, or when debt nearly equals balance leaving a very small positive net_worth while issued_shares remains large.
-
Occurs when operational flows (holding-cost accrual, debt payments, revenue mismatches) drive net_worth // issued_shares == 0 while issued_shares > 0.
Impact: Medium
-
Investor DoS. All new investments revert due to division by zero, blocking recapitalization exactly when the company may need it most.
-
Economic instability. Prevents fresh capital inflow; can strand the company at the insolvency boundary, worsening other issues (e.g., rising holding debt, inability to produce).
Proof of Concept
import boa
COMPANY_PATH = "src/Cyfrin_Hub.vy"
def test_share_price_division_by_zero_dos():
owner = boa.env.generate_address()
investor = boa.env.generate_address()
boa.env.set_balance(owner, 10**21)
boa.env.set_balance(investor, 10**21)
with boa.env.prank(owner):
Company = boa.load(COMPANY_PATH)
company = Company.deploy()
company.fund_cyfrin(0, value=10**18)
company.holding_debt = (company.get_balance() - 1)
company.issued_shares = 1_000_000
net_worth = max(company.get_balance() - company.holding_debt(), 0)
assert net_worth == 1
assert company.issued_shares() == 1_000_000
with boa.env.prank(investor):
reverted = False
try:
company.fund_cyfrin(1, value=10**16)
except boa.BoaError as e:
reverted = True
print(f"[PoC] Investment reverted as expected: {e}")
assert reverted, "Expected division-by-zero revert when share_price == 0"
Recommended Mitigation
Enforce a strictly positive share price whenever issued_shares > 0
@payable
@internal
def fund_investor():
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
- )
+ if self.issued_shares > 0:
+ # Require a positive per-share value; otherwise, reject investment until recapitalized.
+ assert net_worth >= self.issued_shares, "Share price too low"
+ share_price: uint256 = net_worth // self.issued_shares
+ assert share_price > 0, "Share price too low"
+ else:
+ share_price: uint256 = INITIAL_SHARE_PRICE