Company Simulator

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

Investor DoS: division-by-zero risk in share pricing

Author Revealed upon completion

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 = company_balance - holding_debt (if positive)
net_worth: uint256 = 0
if self.company_balance > self.holding_debt:
net_worth = self.company_balance - self.holding_debt
share_price: uint256 = (
# @> If issued_shares > 0 and net_worth < issued_shares, this floors to 0
net_worth // max(self.issued_shares, 1)
if self.issued_shares > 0
else INITIAL_SHARE_PRICE
)
# @> Division by ZERO when share_price == 0 -> hard revert -> investor DoS
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

  • Construct a state where net_worth = 1 wei and issued_shares is large, forcing share_price = 0, and show that calling fund_cyfrin(1) reverts.

# tests/test_share_price_div_by_zero.py
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()
# Make company barely solvent: company_balance = holding_debt + 1
company.fund_cyfrin(0, value=10**18) # seed 1 ETH
company.holding_debt = (company.get_balance() - 1) # leave net_worth = 1 wei
# Inflate issued_shares so net_worth // issued_shares == 0
company.issued_shares = 1_000_000 # large number; 1 // 1e6 -> 0
# Sanity: engineered state
net_worth = max(company.get_balance() - company.holding_debt(), 0)
assert net_worth == 1
assert company.issued_shares() == 1_000_000
# Any investment now attempts msg.value // 0 and reverts
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

Support

FAQs

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