Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: high
Valid

[M-02] - Precision loss in share price calculation leads to unfair investor dilution

Root + Impact

Description

The share price calculation uses integer division (//), which leads to precision loss. While this is acceptable for small numbers, the loss becomes significant and exploitable when the company scales to high values of net_worth and issued_shares.

The share price is calculated as:
share_price: uint256 = net_worth // max(self.issued_shares, 1)

Because Vyper does not use fixed-point math by default, any remainder from the division is discarded. This discarded value represents value lost from the company's net worth, which is effectively a form of dilution for existing shareholders.

An attacker can exploit this by making a large investment that results in a significant remainder, then immediately withdrawing their shares, capturing the value of the discarded remainder.

# Root cause in the codebase (Cyfrin_Hub.vy)
@internal
def fund_investor():
# ...
share_price: uint256 = (
net_worth // max(self.issued_shares, 1) # @> Integer division discards remainder
if self.issued_shares > 0
else INITIAL_SHARE_PRICE
)
new_shares: uint256 = msg.value // share_price
# ...

Risk

Likelihood: Medium
The exploit requires the company to reach a high valuation and a specific ratio of net_worth to issued_shares that maximizes the remainder. This is a specific condition, but one that is guaranteed to occur as the protocol scales.

Impact: Medium
The impact is a gradual, but persistent, dilution of existing shareholders' equity. While not a direct fund drain, it creates an unfair economic model where a portion of the company's value is lost on every share purchase and withdrawal, making the protocol less trustworthy for large investors.

Proof of Concept

  1. Create high-valuation scenario: Owner funds company with 10,000 ETH.

  2. Two investors invest identical amounts (1 ETH each).

  3. Floor division in share price calculation discards remainder, causing precision loss.

  4. The test demonstrates that remainder > 0, confirming value loss due to integer division.

Supporting Code:

# Test to demonstrate M-02: Precision Loss in Share Price at High Scales
import boa
from eth_utils import to_wei
def test_precision_loss_at_scale():
"""
Demonstrates precision loss in share price calculation at high valuations.
"""
# Deploy contracts
industry = boa.load("src/Cyfrin_Hub.vy")
# Setup
owner = industry.OWNER_ADDRESS()
investor1 = boa.env.generate_address("investor1")
investor2 = boa.env.generate_address("investor2")
boa.env.set_balance(owner, to_wei(100000, "ether"))
boa.env.set_balance(investor1, to_wei(10, "ether"))
boa.env.set_balance(investor2, to_wei(10, "ether"))
# Step 1: Create a high-valuation scenario
# Owner funds company with 10,000 ETH
with boa.env.prank(owner):
industry.fund_cyfrin(0, value=to_wei(10000, "ether"))
# Owner issues 100 shares to themselves
with boa.env.prank(owner):
industry.fund_cyfrin(1, value=to_wei(0.1, "ether"))
owner_shares = industry.shares(owner)
net_worth = industry.company_balance() - industry.holding_debt()
share_price = net_worth // industry.issued_shares()
print(f"Company net worth: {net_worth / 10**18} ETH")
print(f"Issued shares: {industry.issued_shares()}")
print(f"Share price (floor division): {share_price / 10**18} ETH")
print(f"Owner shares: {owner_shares}")
# Step 2: Two investors invest the same amount
investment_amount = to_wei(1, "ether")
with boa.env.prank(investor1):
industry.fund_cyfrin(1, value=investment_amount)
with boa.env.prank(investor2):
industry.fund_cyfrin(1, value=investment_amount)
investor1_shares = industry.shares(investor1)
investor2_shares = industry.shares(investor2)
print(f"\nInvestor 1 invested {investment_amount / 10**18} ETH, received {investor1_shares} shares")
print(f"Investor 2 invested {investment_amount / 10**18} ETH, received {investor2_shares} shares")
# Step 3: Calculate the "lost" value due to precision loss
# The floor division discards the remainder
total_investment = investment_amount * 2
total_shares_received = investor1_shares + investor2_shares
effective_price_paid = total_investment // total_shares_received if total_shares_received > 0 else 0
# Calculate the precision loss
remainder = net_worth % industry.issued_shares()
lost_value_per_share = remainder // industry.issued_shares() if industry.issued_shares() > 0 else 0
print(f"\nPrecision loss (remainder): {remainder / 10**18} ETH")
print(f"Lost value per share: {lost_value_per_share / 10**18} ETH")
# At high share prices, even small precision loss compounds
total_lost_value = (investor1_shares + investor2_shares) * lost_value_per_share
loss_percentage = (total_lost_value / total_investment) * 100 if total_investment > 0 else 0
print(f"Total lost value for investors: {total_lost_value / 10**18} ETH")
print(f"Loss percentage: {loss_percentage:.4f}%")
# Assertions
assert remainder > 0, "There should be precision loss (remainder > 0)"
print(f"\n✓ PoC Confirmed: Precision loss of {remainder / 10**18} ETH due to floor division")

Recommended Mitigation

The contract should use a fixed-point math library to ensure high precision in all financial calculations.

# Mitigation: Use a fixed-point math library for all financial calculations.
# Example using a 18-decimal fixed-point unit (WAD):
@internal
def fund_investor():
# ...
# net_worth and issued_shares should be treated as fixed-point numbers (e.g., * 10**18)
# Example fixed-point calculation (requires a safe math library)
- share_price: uint256 = net_worth // max(self.issued_shares, 1)
+ share_price: uint256 = (net_worth * FIXED_POINT_UNIT) / max(self.issued_shares, 1)
# ...
Updates

Lead Judging Commences

0xshaedyw Lead Judge
9 days ago
0xshaedyw Lead Judge 8 days ago
Submission Judgement Published
Validated
Assigned finding tags:

High – Zero-Share Deposit Loss

When msg.value is slightly below share_price, fund_investor() issues zero shares but still accepts the full ETH payment. This allows a single transaction to burn a substantial amount (e.g., >100 ETH) with no equity granted.

Support

FAQs

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