Claim Function Fails in PerAddressTrancheVestingMerkleDistributor Due to Empty Data
Summary
When users attempt to claim their tokens by calling the PerAddressTrancheVestingMerkleDistributor::claim function, the function internally calls _executeClaim with an empty byte array new bytes(0). This causes the getVestedFraction function to always return zero, leading to the claim always reverting.
Vulnerability Detail
When users attempt to claim their tokens by calling the PerAddressTrancheVestingMerkleDistributor::claim function, the function internally calls _executeClaim with an empty byte array (new bytes(0)).
function getClaimableAmount(address beneficiary, bytes memory data) public view virtual returns (uint256) {
require(records[beneficiary].initialized, "Distributor: claim not initialized");
DistributionRecord memory record = records[beneficiary];
// @audit getVestedFraction always return zero because of data
@>> uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator;
return record.claimed >= claimable
? 0 // no more tokens to claim
: claimable - record.claimed; // claim all available tokens
}
Based on the implementation of PerAddressTrancheVestingInitializable::getVestedFraction function is always returning zero. This issue arises because the data parameter passed to the function has a zero length. As a result, the function fails to decode any tranches and consequently never finds a valid vestedFraction and return 0.
getVestedFraction tries to decode vesting tranches from the data, but since the data is empty, the length of the tranches array is zero. Therefore, the loop does not execute, and the function returns zero.
function getVestedFraction(address beneficiary, uint256 time, bytes memory data) public view override returns (uint256) {
@>> Tranche[] memory tranches = abi.decode(data, (Tranche[]));
uint256 delay = getFairDelayTime(beneficiary);
for (uint256 i = tranches.length; i != 0;) {
unchecked {
--i;
}
if (time - delay > tranches[i].time) {
return tranches[i].vestedFraction;
}
}
return 0;
}
So, the value of getClaimableAmount is zero, then _executeClaim checks if the claimable amount is greater than zero. Since it is not, the function reverts."
function _executeClaim(address beneficiary, uint256 _totalAmount, bytes memory data) internal virtual returns (uint256) {
uint120 totalAmount = uint120(_totalAmount);
// effects
if (records[beneficiary].total != totalAmount) {
// re-initialize if the total has been updated
_initializeDistributionRecord(beneficiary, totalAmount);
}
uint120 claimableAmount = uint120(getClaimableAmount(beneficiary, data));
// @audit claimableAmount is always zero revert
require(claimableAmount > 0, "Distributor: no more tokens claimable right now");
records[beneficiary].claimed += claimableAmount;
claimed += claimableAmount;
return claimableAmount;
}
Simple Scenario
The owner deploys the PerAddressTrancheVestingMerkleDistributor Distributor contract and initializes it for token distribution, setting up various tranches for vesting.
Users are informed that they can call the claim function to receive their vested tokens.
Alice Initializes Her Distribution Record.
Alice follows the instructions and calls the claim function, expecting to receive her vested tokens.
Internally, the claim function calls _executeClaim with an empty byte array as the data parameter.
The _executeClaim function attempts to determine the claimable amount by calling getClaimableAmount, which in turn calls getVestedFraction.
Since getVestedFraction is called with an empty data parameter, it decodes no tranches and returns a vested fraction of zero.
As a result, getClaimableAmount returns zero, indicating no tokens are claimable.
The require statement in _executeClaim fails because the claimable amount is zero, causing the transaction to revert.
Alice's attempt to claim her vested tokens fails, and she receives no tokens.
Impact
This issue affects all users attempting to claim their tokens, rendering the token distribution system non-functional.
To fix this issue, the _executeClaim function should pass the correct data parameter containing the vesting tranches to the getClaimableAmount function.
hunter_w3b
high
Claim Function Fails in
PerAddressTrancheVestingMerkleDistributor
Due to Empty DataSummary
When users attempt to claim their tokens by calling the
PerAddressTrancheVestingMerkleDistributor::claim
function, the function internally calls_executeClaim
with an empty byte arraynew bytes(0)
. This causes thegetVestedFraction
function to always return zero, leading to the claim always reverting.Vulnerability Detail
When users attempt to claim their tokens by calling the
PerAddressTrancheVestingMerkleDistributor::claim
function, the function internally calls_executeClaim
with an empty byte array (new bytes(0)
).AdvancedDistributorInitializable ---> DistributorInitializable inside
DistributorInitializable
the_executeClaim
function callsgetClaimableAmount
with the empty byte array.Based on the implementation of
PerAddressTrancheVestingInitializable::getVestedFraction
function is always returning zero. This issue arises because thedata
parameter passed to the function has a zero length. As a result, the function fails to decode any tranches and consequently never finds a valid vestedFraction andreturn 0
.getVestedFraction
tries to decode vesting tranches from the data, but since the data is empty, the length of the tranches array is zero. Therefore, the loop does not execute, and the function returns zero.So, the value of
getClaimableAmount
is zero, then_executeClaim
checks if the claimable amount is greater than zero. Since it is not, the function reverts."Simple Scenario
PerAddressTrancheVestingMerkleDistributor
Distributor contract and initializes it for token distribution, setting up various tranches for vesting.claim
function to receive their vested tokens.claim
function, expecting to receive her vested tokens.claim
function calls_executeClaim
with an empty byte array as thedata
parameter._executeClaim
function attempts to determine the claimable amount by callinggetClaimableAmount
, which in turn callsgetVestedFraction
.getVestedFraction
is called with an emptydata
parameter, it decodes no tranches and returns a vested fraction of zero.getClaimableAmount
returns zero, indicating no tokens are claimable.require
statement in_executeClaim
fails because the claimable amount is zero, causing the transaction to revert.Impact
This issue affects all users attempting to claim their tokens, rendering the token distribution system non-functional.
Code Snippet
https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol#L51-L65
https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update/blob/main/contracts/packages/hardhat/contracts/claim/factory/AdvancedDistributorInitializable.sol#L106-L114
https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update/blob/main/contracts/packages/hardhat/contracts/claim/factory/DistributorInitializable.sol#L66-L82
https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol#L37-L52
Tool used
Manual Review
Recommendation
To fix this issue, the
_executeClaim
function should pass the correct data parameter containing the vesting tranches to the getClaimableAmount function.Duplicate of #11