Vyper Vested Claims

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

No Token Balance Validation

Summary

The token vesting contract lacks a mechanism to validate whether it has sufficient token balance to fulfill user claims. This oversight can lead to failed transactions when users attempt to claim their tokens from an underfunded contract. Instead of receiving a clear, user-friendly error message explaining the issue, users will experience transaction failures with cryptic error messages, creating a poor user experience and potentially causing confusion.

Vulnerability Details

In the claim function, the contract attempts to transfer tokens to users without first checking if it has sufficient balance:

@external
def claim(user: address, total_amount: uint256, proof: DynArray[bytes32, 20]) -> bool:
"""
@notice This function is used to claim the tokens
@dev Anyone can claim for any user
@param user address, the address of the user
@param total_amount uint256, the total amount of tokens
@param proof DynArray[bytes32, 20], the merkle proof
@return bool True if the claim is successful
"""
# Checks
assert self.verify_proof(user, total_amount, proof), "Invalid proof"
assert block.timestamp >= self.vesting_start_time, "Claiming is not available yet"
claimable: uint256 = 0
current_amount: uint256 = self.claimed_amount[user]
vested: uint256 = self._calculate_vested_amount(total_amount)
# 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)
# Transfer the claimable amount to the user - Interactions
_success: bool = extcall IERC20(self.token).transfer(user, claimable)
assert _success, "Transfer failed"
return True

The function simply attempts the transfer and asserts that it was successful:

_success: bool = extcall IERC20(self.token).transfer(user, claimable)
assert _success, "Transfer failed"

If the contract has insufficient tokens, this will fail with the generic "Transfer failed" message, which doesn't clearly indicate the nature of the problem.

Impact

This vulnerability leads to several issues:

  1. Poor User Experience: Users attempting to claim tokens from an underfunded contract will receive cryptic error messages rather than clear explanations.

  2. Increased Support Burden: The lack of clear error messages will likely result in more support requests as users try to understand why their claims are failing.

  3. Wasted Gas: Users will spend gas on transactions that are destined to fail due to insufficient contract balance.

  4. Reduced Trust: Unexplained transaction failures can reduce user trust in the system.

  5. Operational Complexity: Contract administrators will have difficulty monitoring whether the contract has sufficient funds to meet upcoming vesting obligations.

Tools Used

Manual Review

Recommendations

Implement balance validation before attempting token transfers:

@external
def claim(user: address, total_amount: uint256, proof: DynArray[bytes32, 20]) -> bool:
"""
@notice This function is used to claim the tokens
@dev Anyone can claim for any user
@param user address, the address of the user
@param total_amount uint256, the total amount of tokens
@param proof DynArray[bytes32, 20], the merkle proof
@return bool True if the claim is successful
"""
# Checks
assert self.verify_proof(user, total_amount, proof), "Invalid proof"
assert block.timestamp >= self.vesting_start_time, "Claiming is not available yet"
claimable: uint256 = 0
current_amount: uint256 = self.claimed_amount[user]
vested: uint256 = self._calculate_vested_amount(total_amount)
# Calculate how much the user can claim now
if vested > current_amount:
claimable = vested - current_amount
assert claimable > 0, "Nothing to claim"
# Check contract has sufficient balance
contract_balance: uint256 = extcall IERC20(self.token).balanceOf(self)
assert contract_balance >= claimable, "Insufficient contract balance"
# 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)
# Transfer the claimable amount to the user - Interactions
_success: bool = extcall IERC20(self.token).transfer(user, claimable)
assert _success, "Transfer failed"
return True

Additional improvements could include:

  1. View Function for Balance Checking: Implement a view function that users or frontends can call to check if the contract has sufficient balance before attempting a claim.

  2. Funding Monitoring: Implement events or monitoring functions to alert administrators when the contract's balance drops below expected future claim requirements.

  3. Partial Claims: Consider implementing a mechanism for partial claims if the contract cannot fulfill the entire claim amount.

Updates

Appeal created

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

Support

FAQs

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