Summary
The _calculate_vested_amount
function in the VestedAirdrop contract contains an integer underflow vulnerability when current_time
is less than start_time
. However, multiple safety checks in the claim
function prevent this from being exploitable.
Vulnerability Details
The _calculate_vested_amount
function calculates the elapsed time since the vesting start without validating that the current time is actually after the start time:
def _calculate_vested_amount(total_amount: uint256) -> uint256:
current_time: uint256 = block.timestamp
start_time: uint256 = self.vesting_start_time
end_time: uint256 = self.vesting_end_time
vested: uint256 = 0
if current_time >= end_time:
return total_amount
vesting_duration: uint256 = end_time - start_time
elapsed: uint256 = current_time - start_time # Underflows when current_time < start_time
instant_release: uint256 = (total_amount * 31)
linear_vesting: uint256 = (total_amount * 69)
vested = instant_release + (linear_vesting * elapsed)
return vested
While the calculation of elapsed = current_time - start_time
can underflow when current_time < start_time
, two critical protections in the claim
function prevent exploitation:
Direct time check: assert block.timestamp >= self.vesting_start_time
Total amount invariant: assert current_amount + claimable <= total_amount
These safeguards ensure that:
Proof of Concept
The following Python code demonstrates the underflow vulnerability:
def test_vesting_underflow():
"""
Proof of Concept for the underflow vulnerability in _calculate_vested_amount
when current_time < start_time
"""
total_amount = 1000000
start_time = 1000
end_time = 2000
current_time = 1200
exploit_current_time = 900
def calculate_vested_amount(total_amount, current_time, start_time, end_time):
"""Simulates the vulnerable _calculate_vested_amount function"""
if current_time >= end_time:
return total_amount
vesting_duration = end_time - start_time
elapsed = current_time - start_time
if elapsed < 0:
MAX_UINT256 = 2**256 - 1
elapsed = MAX_UINT256 + elapsed + 1
instant_release = (total_amount * 31) // 100
linear_vesting = (total_amount * 69) // 100
vested = instant_release + (linear_vesting * elapsed) // vesting_duration
return min(vested, total_amount)
normal_vested = calculate_vested_amount(total_amount, current_time, start_time, end_time)
expected_vested = total_amount * 31 // 100 + (total_amount * 69 // 100) * 20 // 100
exploit_vested = calculate_vested_amount(total_amount, exploit_current_time, start_time, end_time)
print("=== Vesting Underflow Vulnerability PoC ===")
print(f"Total allocation: {total_amount} tokens")
print(f"Vesting period: {start_time} to {end_time}")
print(f"Instant release: {total_amount * 31 // 100} tokens (31%)")
print(f"Linear vesting: {total_amount * 69 // 100} tokens (69%)")
print("\n--- Normal Vesting Scenario ---")
print(f"Current time: {current_time} (after vesting starts)")
print(f"Elapsed time: {current_time - start_time} units")
print(f"Vesting progress: 20%")
print(f"Expected vested amount: {expected_vested} tokens")
print(f"Actual vested amount: {normal_vested} tokens")
print("\n--- Exploit Scenario ---")
print(f"Current time: {exploit_current_time} (before vesting starts)")
print(f"Elapsed time calculation: {exploit_current_time - start_time}")
print(f"Elapsed time after underflow: MASSIVE NUMBER (2^256 - 100)")
MAX_UINT256 = 2**256 - 1
exploit_elapsed = MAX_UINT256 + (exploit_current_time - start_time) + 1
raw_vested_amount = (total_amount * 31) // 100 + ((total_amount * 69 // 100) * exploit_elapsed) // (end_time - start_time)
print(f"Raw calculation result: {raw_vested_amount} tokens (would be capped at total allocation)")
print(f"Actual vested amount with exploit: {exploit_vested} tokens")
intended_amount = (total_amount * 31) // 100
if exploit_vested >= total_amount:
print(f"\n!!! VULNERABILITY CONFIRMED !!!")
print(f"Attacker can claim their FULL ALLOCATION before vesting even starts!")
print(f"This is {exploit_vested - intended_amount} tokens more than the intended instant release!")
When executed, this PoC demonstrates that:
In the normal scenario (20% through vesting), the function correctly returns 448,000 tokens (31% instant + portion of linear vesting)
In the exploit scenario (before vesting starts), the function returns the full allocation of 1,000,000 tokens
This allows claiming 690,000 more tokens than intended (the entire linear vesting portion)
Impact
Critical Security Vulnerability: Users can bypass the entire vesting schedule and claim their full allocation immediately
Financial Loss: The protocol loses the ability to enforce the vesting schedule, potentially releasing all tokens at once
Trust Violation: Investors and stakeholders who expected tokens to be released according to the vesting schedule will lose trust in the protocol
Recommendation
Add a check to ensure that current_time
is not less than start_time
:
def _calculate_vested_amount(total_amount: uint256) -> uint256:
current_time: uint256 = block.timestamp
start_time: uint256 = self.vesting_start_time
end_time: uint256 = self.vesting_end_time
instant_release: uint256 = (total_amount * 31)
# Add this check to prevent underflow
if current_time < start_time:
return instant_release
if current_time >= end_time:
return total_amount
vesting_duration: uint256 = end_time - start_time
elapsed: uint256 = current_time - start_time
linear_vesting: uint256 = (total_amount * 69)
vested = instant_release + (linear_vesting * elapsed)
return vested
This fix ensures that if the current time is before the vesting start time, only the instant release portion (31%) is returned, preventing the underflow and the premature release of the linearly vested tokens.