Company Simulator

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

The "Divide by Zero" Brick Attack

Author Revealed upon completion

Root + Impact

Description


  • Normal Behavior: The fund_investor function allows users to invest ETH to receive company shares. It calculates the current share_price by dividing the company's net worth (company_balance - holding_debt) by the total issued_shares.


  • Specific Issue: If the company's holding_debt grows larger than its company_balance, the net_worth can become extremely low. When this low net_worth is divided by a large issued_shares number, integer division can cause the share_price to become 0.


  • Root Cause:

    Code snippet

    // Root cause in the Cyfrin_Hub.vy contract:
    // If `share_price` is calculated as 0, this line
    // will cause a "division by zero" error,
    // reverting the transaction.
    @> new_shares: uint256 = msg.value // share_price@>


Risk

Likelihood: Medium


  • Reason 1: This scenario occurs when the _apply_holding_cost function is not called for an extended period, which happens when no one triggers sell_to_customer.



  • Reason 2: When sell_to_customer is finally called, the large, accumulated holding_debt is applied all at once, which can drop the company's net_worth to a value less than issued_shares.


Impact:

  • Impact 1: This creates a Denial of Service (DoS) vulnerability, as any new call to fund_investor will permanently fail with a "division by zero" error.

  • Impact 2: The contract's primary method for raising capital is "bricked," preventing any new investors from funding the company and effectively freezing its growth.



Proof of Concept


  1. A few investors call fund_investor to buy shares, so issued_shares is greater than zero.


  2. All users (or an attacker) wait for a long period (e.g., several days) without calling trigger_demand on the CustomerEngine contract.

  3. During this time, a large holding_debt accumulates in the Cyfrin_Hub contract.


  4. A user finally calls trigger_demand, which executes sell_to_customer.


  5. This triggers _apply_holding_cost, applying the massive debt and causing the company's net_worth to become very low (e.g., net_worth = 500 wei, while issued_shares = 1,000,000).

  6. A new, legitimate investor calls fund_investor with 1 ETH.

  7. The contract calculates share_price = 500 // 1_000_000, which results in share_price = 0.


  8. The contract then attempts to execute new_shares = (1 ETH) // 0, which reverts the transaction.

  9. All future attempts to call fund_investor will also revert, as the share_price will remain 0.



Recommended Mitigation


Code snippet

@payable
@internal
def fund_investor():
"""
@notice Allows public users to invest ETH in exchange for shares.
@dev Share amount is calculated based on current net worth per share.
If no shares have been issued, uses INITIAL_SHARE_PRICE.
@dev Investor receives shares proportional to contribution.
Excess shares beyond cap are trimmed.
@dev Emits SharesIssued event.
"""
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!!!"
# Calculate shares based on contribution
net_worth: uint256 = 0
if self.company_balance > self.holding_debt:
net_worth = self.company_balance - self.holding_debt
share_price: uint256 = INITIAL_SHARE_PRICE
if self.issued_shares > 0:
calculated_price: uint256 = net_worth // max(self.issued_shares, 1)
# --- MITIGATION ---
# Enforce a minimum price of 1 wei to prevent division by zero.
share_price = max(calculated_price, 1)
# --- END MITIGATION ---
# The original "else" is no longer needed if we default to INITIAL_SHARE_PRICE
# else:
# share_price = INITIAL_SHARE_PRICE
new_shares: uint256 = msg.value // share_price
# Cap shares if exceeding visible limit
available: uint256 = self.public_shares_cap - self.issued_shares
if new_shares > available:
new_shares = available
self.shares[msg.sender] += new_shares
self.issued_shares += new_shares
self.company_balance += msg.value
if self.share_received_time[msg.sender] == 0:
self.share_received_time[msg.sender] = block.timestamp
log SharesIssued(investor=msg.sender, amount=new_shares)
- remove this code
+ add this code

Support

FAQs

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