Description
The fund_investor function allows users to invest ETH in exchange for company shares. The function calculates how many shares the investment can purchase, then caps the shares at the available amount if it exceeds the public share cap.
However, the function adds the ENTIRE msg.value to company_balance regardless of how many shares were actually issued, meaning investors who send more ETH than needed will overpay without receiving any refund.
@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
if self.issued_shares > 0
else INITIAL_SHARE_PRICE
)
new_shares: uint256 = msg.value
# Cap shares if exceeding visible limit
available: uint256 = self.public_shares_cap - self.issued_shares
@> if new_shares > available: # Line 324
@> new_shares = available # Line 325 - Shares are capped
self.shares[msg.sender] += new_shares
self.issued_shares += new_shares
@> self.company_balance += msg.value # Line 329 - Full payment is kept!
Risk
Likelihood:
This occurs whenever an investor sends more ETH than the remaining share capacity allows
As the share cap approaches the limit, this becomes increasingly likely
Malicious investors could exploit this by front-running and intentionally overpaying
Impact:
Investors lose excess ETH without receiving equivalent value in shares
The company receives free capital from overpayment
This creates an unfair wealth transfer from investors to existing shareholders
Later investors are especially vulnerable when approaching the share cap
Proof of Concept
available_shares = 1000
share_price = 0.001 ETH
investor_payment = 10 ETH
expected_shares = 10 ETH // 0.001 ETH = 10,000 shares
new_shares = min(expected_shares, available_shares) = 1000 shares
actual_value = 1000 * 0.001 ETH = 1 ETH
company_balance += 10 ETH
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
)
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
+ # Calculate actual cost and refund excess
+ actual_cost: uint256 = new_shares * share_price
+ excess: uint256 = msg.value - actual_cost
+ if excess > 0:
+ send(msg.sender, excess)
self.shares[msg.sender] += new_shares
self.issued_shares += new_shares
- self.company_balance += msg.value
+ self.company_balance += actual_cost
if self.share_received_time[msg.sender] == 0:
self.share_received_time[msg.sender] = block.timestamp
log SharesIssued(investor=msg.sender, amount=new_shares)