Company Simulator

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

Share Price Division by Zero Near Insolvency

Author Revealed upon completion

Root + Impact

Description

  • _current_share_price divides net worth by issued_shares without enforcing a floor, so when debt approaches the treasury balance the share price becomes zero and fund_investor reverts on every deposit, blocking capital inflow.

```vyper
@> net_worth: uint256 = max(self.company_balance - self.holding_debt, 0)
@> return net_worth // self.issued_shares # Division by zero when net_worth < issued_shares
```

Risk

Likelihood: High

  • Realistic inventory (1,000 items) accumulates 24 ETH/day in holding costs. Normal operational lags (slow sales for 40-50 days) trigger insolvency without attacker intervention.

Impact: Medium

  • Investors cannot enter positions until capital is restored, halting fundraising and starving the treasury.

Proof of Concept

  • Overview: tests/unit/test_poc_003_division_by_zero.py recreates the insolvency edge, proves share_price == 0, and demonstrates fund_cyfrin() reverts (division by zero in share price calculation). Note: PoC uses minimal inventory (1 item) for test determinism. With realistic inventory levels (100-1,000 items), this scenario occurs in days to weeks, not years. Holding costs are 0.001 ETH per item per hour, so 1,000 items accumulate 24 ETH/day in costs.

  • Step-by-step:

    1. Setup: Deploy fresh contracts, seed 1 ETH, and mint 1,000,000 shares to a primary investor.

    2. Attack Vector: Produce 1 item (minimal inventory for test), fast-forward time to accumulate holding costs until company_balance reaches zero and debt accrues.

    3. Execution Flow: Owner tops up with holding_debt + 1, leaving net worth at exactly 1 wei.

    4. Result: Next investor call to fund_cyfrin(1) reverts (division by zero when calculating share price).

"""
POC-003: Share Price Division by Zero Near Insolvency
Severity: MEDIUM | Likelihood: High | Impact: Medium
Demonstrates that when net worth shrinks to 1 wei while 1,000,000 shares
remain issued, the share price rounds to zero and `fund_investor()` reverts
with a zero-division error, blocking all new capital injections.
"""
# ============================================================================
# EXECUTION CONTEXT
# ============================================================================
# This PoC deploys contracts directly using script.deploy module:
# - deploy_industry(): Deploys Cyfrin_Hub.vy (vulnerable contract)
# - deploy_engine(): Deploys CustomerEngine.vy (demand simulator)
# - Test accounts generated via boa.env.generate_address()
#
# Framework: titanoboa (Python testing framework for Vyper smart contracts)
# Dependencies: pytest, titanoboa, eth_utils
# ============================================================================
import boa
from eth_utils import to_wei
from script.deploy import deploy_engine, deploy_industry
INITIAL_SHARE_PRICE_WEI = 10**15 # 0.001 ETH
PUBLIC_CAP = 1_000_000
HOLDING_TIME_SECONDS = 3_700_000_000 # ~117 years to accumulate debt with 1 item (1000 items = ~43 days)
def _share_price(contract) -> int:
issued = contract.issued_shares()
if issued == 0:
return INITIAL_SHARE_PRICE_WEI
net_worth = 0
balance = contract.company_balance()
debt = contract.holding_debt()
if balance > debt:
net_worth = balance - debt
return net_worth // issued
def test_poc_003_division_by_zero_near_insolvency():
"""
Setup:
- Mint full public share cap (1,000,000 shares) at initial price
- Generate large holding debt so company balance drops to zero
- Owner injects just `holding_debt + 1` wei, making net worth == 1 wei
- Share price becomes 0next investment reverts (division by zero)
"""
industry_contract = deploy_industry()
customer_engine_contract = deploy_engine(industry_contract)
owner = industry_contract.OWNER_ADDRESS()
# Fresh actors
primary_investor = boa.env.generate_address("primary_investor")
blocked_investor = boa.env.generate_address("blocked_investor")
faux_customer_engine = customer_engine_contract.address
# Provision generous balances for actors
boa.env.set_balance(owner, to_wei(2_000_000, "ether"))
boa.env.set_balance(primary_investor, to_wei(2_000_000, "ether"))
boa.env.set_balance(blocked_investor, to_wei(10, "ether"))
# 0. Owner seeds the treasury so solvency check passes
with boa.env.prank(owner):
industry_contract.fund_cyfrin(0, value=to_wei(1, "ether"))
# 1. Fill the entire public share cap at the initial price
with boa.env.prank(primary_investor):
industry_contract.fund_cyfrin(
1, value=INITIAL_SHARE_PRICE_WEI * PUBLIC_CAP
)
assert industry_contract.issued_shares() == PUBLIC_CAP
expected_balance = to_wei(1, "ether") + INITIAL_SHARE_PRICE_WEI * PUBLIC_CAP
assert industry_contract.company_balance() == expected_balance
# 2. Create a tiny inventory so holding costs accumulate over time
with boa.env.prank(owner):
industry_contract.produce(1) # costs 0.01 ETH
# 3. Fast forward into the future then apply holding cost
boa.env.time_travel(seconds=HOLDING_TIME_SECONDS)
with boa.env.prank(faux_customer_engine):
industry_contract.sell_to_customer(2, value=0)
assert industry_contract.company_balance() == 0
debt = industry_contract.holding_debt()
assert debt > 0
# 4. Owner injects only enough to leave net worth == 1 wei
boa.env.set_balance(owner, debt + 10) # ensure sufficient balance
with boa.env.prank(owner):
industry_contract.fund_cyfrin(0, value=debt + 1)
company_balance = industry_contract.company_balance()
holding_debt = industry_contract.holding_debt()
net_worth = company_balance - holding_debt
assert company_balance == debt + 1
assert net_worth == 1
assert industry_contract.issued_shares() == PUBLIC_CAP
assert _share_price(industry_contract) == 0
# 5. Next investor attempt triggers division by zero in fund_investor()
with boa.reverts():
with boa.env.prank(blocked_investor):
industry_contract.fund_cyfrin(1, value=to_wei(1, "ether"))
print("\n<<< POC-003 >>>")
print(f"issued_shares={industry_contract.issued_shares()}")
print(f"company_balance={company_balance}")
print(f"holding_debt={holding_debt}")
print(f"net_worth={net_worth}")
print(f"share_price={_share_price(industry_contract)}")
print(
"state_snapshot="
f"{{'company_balance': {company_balance}, "
f"'holding_debt': {holding_debt}, "
f"'net_worth': {net_worth}, "
f"'time_elapsed_seconds': {HOLDING_TIME_SECONDS}}}"
)
print("[fail] fund_investor division by zero; investment path DoS")

Recommended Mitigation

  • Clamp share price to at least 1 wei or revert early with a descriptive error when net_worth < issued_shares.

  • Consider writing idle holding debt off to treasury equity or diluting shares when debt spikes, preventing zero division.

  • Emit monitoring events when share price is within 1 wei of zero so ops can react.

- net_worth: uint256 = max(self.company_balance - self.holding_debt, 0)
- return net_worth // self.issued_shares
+ net_worth: uint256 = max(self.company_balance - self.holding_debt, 0)
+ price: uint256 = net_worth // self.issued_shares
+ if price == 0:
+ raise "share price floor hit"
+ return price
Updates

Lead Judging Commences

0xshaedyw Lead Judge
2 days ago
0xshaedyw Lead Judge about 17 hours ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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