Vyper Vested Claims

First Flight #34
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: low
Invalid

Integer Underflow in _calculate_vested_amount When current_time < start_time

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) // 100
linear_vesting: uint256 = (total_amount * 69) // 100
vested = instant_release + (linear_vesting * elapsed) // vesting_duration
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:

  1. Direct time check: assert block.timestamp >= self.vesting_start_time

  2. Total amount invariant: assert current_amount + claimable <= total_amount

These safeguards ensure that:

  • Users cannot claim before vesting starts

  • Users cannot claim more than their total allocation

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
"""
# Simulate contract parameters
total_amount = 1000000 # 1 million tokens
# Scenario 1: Normal vesting (current_time > start_time)
start_time = 1000
end_time = 2000
current_time = 1200 # 20% through vesting period
# Scenario 2: Exploit (current_time < start_time)
exploit_current_time = 900 # Before vesting starts
# Simulate the vulnerable function
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
# Vulnerable calculation
vesting_duration = end_time - start_time
elapsed = current_time - start_time # Underflows when current_time < start_time
# In Vyper, this would underflow and wrap around to a huge number
# In Python, we need to simulate this behavior
if elapsed < 0:
# Simulate Vyper's uint256 underflow
MAX_UINT256 = 2**256 - 1
elapsed = MAX_UINT256 + elapsed + 1
instant_release = (total_amount * 31) // 100
linear_vesting = (total_amount * 69) // 100
# This calculation will now use the massive underflowed value
vested = instant_release + (linear_vesting * elapsed) // vesting_duration
# In Vyper, if vested > total_amount, it would still return vested
# But for our demonstration, let's cap it at total_amount to show what would happen
# in a real contract where the balance check would limit the withdrawal
return min(vested, total_amount)
# Normal case
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 case
exploit_vested = calculate_vested_amount(total_amount, exploit_current_time, start_time, end_time)
# Print results
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)")
# Calculate what would happen with the underflow
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")
# Check if exploit gives more tokens than should be available at this time
intended_amount = (total_amount * 31) // 100 # Only instant release should be available
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:

  1. In the normal scenario (20% through vesting), the function correctly returns 448,000 tokens (31% instant + portion of linear vesting)

  2. In the exploit scenario (before vesting starts), the function returns the full allocation of 1,000,000 tokens

  3. 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) // 100
# 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) // 100
vested = instant_release + (linear_vesting * elapsed) // vesting_duration
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.

Updates

Appeal created

bube Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

[Invalid] Underflow in `_calculate_vested_amount`

The `_calculate_vested_amount` function is called in ` claim` and `claimable_amount` functions. There is a check that ensures the `block.timestamp` is greater or equal to the `vesting_start_time` in the both functions. Also, the admin sets the start and end time of the vesting. This means it will be always correct. Therefore, there is no risk from underflow or division by zero in `_calculate_vested_amount` function.

Support

FAQs

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