Root + Impact
Description
Normal behavior: fund_investor() accepts ETH from an investor and issues shares proportional to the contribution at the current share_price. The contract increases company_balance by the incoming ETH and records the issued shares for the investor.
Specific issue: When the computed new_shares exceeds the remaining public_shares_cap, the function truncates new_shares to the available amount but still credits the full msg.value to company_balance and does not refund the excess ETH. This causes investors to overpay for fewer shares. A MEV/front-runner can exploit timing and ordering to ensure an incoming investor is forced to accept a capped share allocation while the contract keeps the unused ETH.
@payable
@internal
def fund_investor():
...
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:
new_shares = available @> truncation without refund
self.shares[msg.sender] += new_shares
self.issued_shares += new_shares
self.company_balance += msg.value @> company keeps full msg.value even when fewer shares issued
...
Risk
Likelihood:
A high volume of near-simultaneous investors will occur during funding windows, increasing chances that an arriving transaction meets the cap partially and receives a truncated allocation.
MEV/front-running bots actively monitor mempool funding transactions and will target situations where a cap is near exhaustion to create profitable reorderings.
Impact:
Honest investors permanently lose ETH value because the contract retains excess ETH for which no shares were issued.
MEV actors or whales can profit economically by manipulating inclusion order (buying or canceling shares) so victims pay full msg.value but receive a reduced share allocation.
Proof of Concept
1. public_shares_cap = 1,000,000 ; issued_shares = 999,900 ; share_price = X wei
2. Victim submits a transaction sending msg.value = 200 * X wei intending to buy 200 shares (new_shares = 200).
3. MEV bot detects the pending tx and submits a higher-priority tx that purchases the remaining 100 shares (or otherwise consumes the available cap) so that issued_shares becomes 1,000,000 before the victim tx is mined.
4. When the victim tx is mined, new_shares computation yields 200, but available = 0 (or 100 depending on ordering) so contract truncates new_shares to the remaining available amount (e.g., 100 or 0).
5. Contract executes: issues truncated shares (100 or 0), increases issued_shares accordingly, and still executes self.company_balance += msg.value, keeping the full ETH the victim sent without providing shares or refund.
6. Result: Victim overpaid for fewer shares; MEV bot used ordering to reduce victim’s allocation and pocketed market advantange.
Recommended Mitigation
Add explicit refund logic for excess ETH when new_shares is capped, and introduce a slippage protection parameter so callers can require a minimum expected share allocation. Ensure company_balance is increased only by the actual amount charged for 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 new_shares > available:
+ new_shares = available
+
+ # compute cost for the shares actually being issued
+ cost_for_shares: uint256 = new_shares * share_price
+
+ # refund excess ETH to the investor when the cap truncates allocation
+ if msg.value > cost_for_shares:
+ refund: uint256 = msg.value - cost_for_shares
+ # guard against failed refund reverting entire funding flow
+ # attempt refund and require success to avoid taking excess funds
+ raw_call(msg.sender, b"", value=refund, revert_on_failure=True)
+
+ # increase company balance only by the amount actually charged
+ self.company_balance += cost_for_shares
+
+ # issue shares to investor
+ self.shares[msg.sender] += new_shares
+ self.issued_shares += new_shares
+
+ # optional: enforce caller-specified minimum acceptable allocation to protect against MEV slippage
+ # function signature must be changed to accept min_expected_shares parameter
+ assert new_shares >= min_expected_shares, "Slippage or cap truncation"