Company Simulator

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

Lockup Bypass can leads to breaking the intended economic guarantees

Author Revealed upon completion

Root + Impact

Description

share_received_time[msg.sender] is written the first time anyone interacts with fund_investor. The timestamp is never cleared on exit and, because of VULN-01, it is even set when no shares are minted. Subsequent deposits reuse the stale value, letting attackers bypass the 30-day lockup.

self.shares[msg.sender] += new_shares
...
if self.share_received_time[msg.sender] == 0:
@> self.share_received_time[msg.sender] = block.timestamp

withdraw_shares resets the share balance but leaves the timestamp untouched:

self.shares[msg.sender] = 0
self.issued_shares -= shares_owned
...
@> time_held: uint256 = block.timestamp - self.share_received_time[msg.sender]

Risk

Likelihood: Any user can intentionally send a dust transaction to stamp a timestamp, making the bug broadly exploitable. No special permissions are required.

Impact:

  • The early withdrawal penalty is trivially bypassed, breaking the intended economic guarantees.

  • Attackers can pre-farm timestamps across many addresses and execute instant exit trades, disadvantaging honest investors.

  • Lockup metrics become meaningless for compliance or treasury management.

Proof of Concept

  1. Send < INITIAL_SHARE_PRICE to fund_cyfrin(1); zero shares mint but share_received_time records the current block.

  2. Wait until the timestamp ages past 30 days (or simply fork forward in tests).

  3. Deposit a real amount to mint shares.

  4. Immediately call withdraw_shares; time_held treats the old timestamp as current and skips the penalty.

Unit-test outline:

def test_lockup_bypass(hub, investor, chain):
dust = 5 * 10**14
hub.fund_cyfrin(action=1, value=dust, sender=investor)
chain.sleep(31 * 86400)
hub.fund_cyfrin(action=1, value=10**18, sender=investor)
pre = hub.company_balance()
hub.withdraw_shares(sender=investor)
# No penalty applied despite immediate exit
assert hub.company_balance() == pre - 10**18

Recommended Mitigation

  1. Set share_received_time only when new_shares > 0 (paired with VULN-01 mitigation).

  2. Clear share_received_time[msg.sender] inside withdraw_shares.

  3. When a holder adds to their position, refresh the timestamp or compute a weighted-average holding period so every share tranche observes the lockup.

Suggested adjustments:

if new_shares > 0:
- self.share_received_time[msg.sender] = block.timestamp
+ self.share_received_time[msg.sender] = block.timestamp
...
self.shares[msg.sender] = 0
self.issued_shares -= shares_owned
+self.share_received_time[msg.sender] = 0

Support

FAQs

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