Summary
The contract does not explicitly prevent duplicate claims for the same total_amount
and proof
. If a user submits the same proof multiple times, they could potentially claim more tokens than intended.
@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
Vulnerability Details
The issue of duplicate claims will arise when a user can submit the same proof multiple times to claim tokens, potentially allowing them to claim more tokens than they are entitled to. This can happen if the contract does not properly track or validate whether a claim has already been made for a specific user
and total_amount
.
Impact
Users could exploit this to claim more tokens than they are entitled to.
Tools Used
Manual Review
Recommendations
To prevent duplicate claims in the claim
function, I added a check to ensure that the same user
and total_amount
combination cannot be claimed more than once. I added a mapping to track whether a specific user
and total_amount
combination has already been claimed.
# Add the new mapping to track claims
+ claimed_amount: public(HashMap[address, uint256])
@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
"""
# Generate a unique claim ID for the user and total_amount combination
+ claim_id: bytes32 = keccak256(concat(convert(user, bytes20), convert(total_amount, bytes32)))
# Check if the claim has already been processed
+ assert not self.claimed[claim_id], "Claim already processed"
# 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
# Mark the claim as processed
+ self.claimed[claim_id] = True
# 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