Vyper Vested Claims

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

Reentrancy Risk Due to Logging Event Before Token Transfer

Summary

ROOT CAUSE:

The claim function in the Vested Airdrop Contract logs the Claimed event before transferring tokens to the user. This introduces a reentrancy risk, which can be exploited by malicious users, potentially allowing them to drain the contract's token supply. The issue arises because the contract allows external contract calls (such as token transfers) before finalizing the internal state changes. Logging the event prior to the transfer of tokens can trigger unexpected reentrancy behavior, allowing attackers to execute repeated claims before the claimed amount is updated.

Vulnerability Details

In the vulnerable claim function, the contract logs the Claimed event before transferring the tokens to the user. This opens up the possibility for a reentrancy attack, where a malicious user could exploit the logging mechanism to manipulate the state and claim more tokens.

Affected Line of code:

# Calculate how much the user can claim now
if vested > current_amount:
claimable = vested - current_amount
assert claimable > 0, "Nothing to claim"
# Update the claimed amount - Effects
self.claimed_amount[user] += claimable
# invariant: claimed amount should always be less than or equal to amount (better safe then sorry)
assert current_amount + claimable <= total_amount, "Claimed amount exceeds total amount"
==> log Claimed(user, claimable) # logging before transfer(@audit issue)
# Transfer the claimable amount to the user - Interactions
_success: bool = extcall IERC20(self.token).transfer(user, claimable)
assert _success, "Transfer failed"
return True

Reentrancy Attack Flow:

  1. The attacker initiates a claim.

  2. The contract logs the Claimed event (marking the claim as successful) before transferring the tokens.

  3. The attacker, with a malicious contract (which has a fallback or receive function), receives the tokens.

  4. The malicious contract triggers the claim function again before the internal state (the claimed_amount) is updated in the first claim transaction.

  5. This results in the attacker being able to claim more tokens than they are entitled to, potentially draining the contract of its tokens.

Impact

** Financial Loss:** Attackers can drain the contract’s token supply, preventing legitimate users from receiving their fair share.

  • Unfair Distribution: The reentrancy attack allows malicious actors to claim more tokens than they are entitled to, violating the fairness of the vesting system.

  • Compromise of Contract Integrity: The event is logged before the internal state changes, meaning the contract’s logs will not accurately represent the actual claimable amount or the correct state of the contract.

  • Reputational Risk: If exploited, the vulnerability can cause a loss of confidence in the contract, making it unsuitable for further use.

Tools Used

manual review

Recommendations

To prevent this reentrancy issue, it is essential to refactor the claim function to ensure that state changes (such as updating the claimed_amount and logging the event) are performed before any external interactions (such as token transfers). This will prevent attackers from exploiting the vulnerability and performing repeated claims.

Here’s the refactored claim function that follows the Checks-Effects-Interactions pattern:

# Update the claimed amount - Effects (before transferring tokens)
self.claimed_amount[user] += claimable
# invariant: claimed amount should always be less than or equal to amount (better safe than sorry)
assert current_amount + claimable <= total_amount, "Claimed amount exceeds total amount"
# Transfer the claimable amount to the user - Interactions
_success: bool = extcall IERC20(self.token).transfer(user, claimable)
assert _success, "Transfer failed"
# Log the Claimed event after the token transfer
log Claimed(user, claimable)
return True
Updates

Appeal created

bube Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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