Company Simulator

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

Investor lockup time is not reset

Author Revealed upon completion

Root + Impact

The timestamp stored in share_received_time (the moment an investor receives shares) is never reset when the investor withdraws, and it is only set the first time the investor makes an investment. As a result, any subsequent investments are not subject to the lock‑up period.


Description

  • Expected behavior: Every time an investor makes an investment, the newly acquired shares must remain locked for at least LOCKUP_PERIOD (30 days in the current version).

  • Actual behavior: After an investor’s initial investment, the timestamp for that investment is never cleared or updated. Consequently, later investments can be withdrawn without incurring the early‑withdrawal penalty, even if the shares have been held for only a single day.

# Root cause in the codebase – “@>” marks highlight the relevant section
@payable
@internal
def fund_investor():
# … snip …
# Below you can see the check whether an investor has made an investment before
if self.share_received_time[msg.sender] == 0:
self.share_received_time[msg.sender] = block.timestamp

Risk

Likelihood: High – this condition will affect every investor after their first investment.

Impact: Subsequent investments bypass the lock‑up period, allowing investors to withdraw before the lock‑up expires without paying the penalty.


Proof of Concept

(No code provided – placeholder left intentionally.) Normal use by an investor will trigger the bug.

# No Code Required

Recommended Mitigation

Explanation:
When an investor withdraws their shares, the contract should also clear the stored share_received_time. Resetting this timestamp ensures that any future investment starts a fresh lock‑up period, preventing the bug where subsequent deposits can be withdrawn instantly without the early‑withdrawal penalty.

@external
def withdraw_shares():
"""
@notice Allows investors to redeem their shares for ETH.
@dev Payout is based on the current share price (net worth per share).
@dev If shares are withdrawn before LOCKUP_PERIOD, a 10 % penalty is applied.
@dev Total payout is capped at MAX_PAYOUT_PER_SHARE per share to prevent fund draining.
@dev Investor's share count is reset to zero.
@dev Emits Withdrawn_Shares event.
"""
shares_owned: uint256 = self.shares[msg.sender]
assert shares_owned > 0, "Not an investor!"
share_price: uint256 = self.get_share_price()
payout: uint256 = shares_owned * share_price
# Check lockup
time_held: uint256 = block.timestamp - self.share_received_time[msg.sender]
if time_held < LOCKUP_PERIOD:
penalty: uint256 = payout * EARLY_WITHDRAWAL_PENALTY // 100
payout -= penalty
max_payout: uint256 = shares_owned * MAX_PAYOUT_PER_SHARE
if payout > max_payout:
payout = max_payout
self.shares[msg.sender] = 0
+ self.share_received_time[msg.sender] = 0 # Reset timestamp on withdrawal
self.issued_shares -= shares_owned
assert self.company_balance >= payout, "Insufficient company funds!"
self.company_balance -= payout
raw_call(
msg.sender,
b"",
value=payout,
revert_on_failure=True,
)
log Withdrawn_Shares(investor=msg.sender, shares=shares_owned)

Support

FAQs

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