Description
The share price calculation in fund_investor uses integer division which can round down to zero when the net worth is less than the number of issued shares. When share_price = 0, investors can acquire shares for free or at near-zero cost.
@payable
@internal
def fund_investor():
# 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 = (
@> net_worth
@> if self.issued_shares > 0
@> else INITIAL_SHARE_PRICE
@> )
@> new_shares: uint256 = msg.value
If net_worth < issued_shares, then share_price = net_worth // issued_shares = 0.
When share_price = 0, line 320 would cause division by zero: msg.value // 0.
While line 308 has an assertion assert (self.company_balance > self.holding_debt) which prevents net_worth = 0 in most cases, there's still a rounding issue where share_price can be very small (1 wei) when net_worth is barely above zero, allowing attackers to acquire massive amounts of shares for minimal cost.
Risk
Likelihood:
-
This occurs when the company's net worth is very low relative to issued shares
-
Can happen after the company pays most debts or has minimal balance
-
The assertion on line 308 prevents the worst case but not the rounding exploit
Impact:
-
Investors can acquire disproportionate shares for minimal ETH
-
Dilutes existing shareholders unfairly
-
Breaks the economic model of the company
-
Can be exploited repeatedly to drain value from the company
Proof of Concept
company_balance = 1000 wei
holding_debt = 999 wei
issued_shares = 1,000,000
net_worth = 1000 - 999 = 1 wei
share_price = 1 wei // 1,000,000 = 0 wei (rounds down!)
msg.value = 1 wei
new_shares = 1 wei // 0 wei = DIVISION BY ZERO!
share_price = 100 // 1,000,000 = 0 wei (still rounds to zero!)
Recommended Mitigation
@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!!!"
# 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 = (
net_worth // max(self.issued_shares, 1)
if self.issued_shares > 0
else INITIAL_SHARE_PRICE
)
+
+ # Prevent zero or near-zero share price
+ assert share_price >= INITIAL_SHARE_PRICE, "Share price too low!"
+
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)