Closed sherlock-admin2 closed 5 months ago
@MxAxM, please share the rejection response.
@MxAxM, please share the rejection response.
user should be able to claim full amount after end-time otherwise calculate vested amount, what's wrong with it ?
Hi @MxAxM ,
Basically the Issue here is that the code claim that the last vested Fraction should be calculated on basis of fractionDenominator
. However in the code it is not done like it and the last tranche data is not validated.
if last vestedFraction=8000
and fractionDenominator=10000
the formula :
uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator;
Will not calculated the correct amount.
we have only one tranche with following data :
tranches:{
time : block.timestamp+2;
vestedFraction: 8000
}
getVestedFraction will return 8000 event if it is the last vested , lets assume total amount is record.total=1e18
$1e18 * 8000 / 10000 =800000000000000000(0.8 ether)$ So here the user was suppose to claim the total=1e18(1 ether);
Hi @MxAxM , Basically the Issue here is that the code claim that the last vested Fraction should be calculated on basis of
fractionDenominator
. However in the code it is not done like it and the last tranche data is not validated. if lastvestedFraction=8000
andfractionDenominator=10000
the formula :uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator;
Will not calculated the correct amount. we have only one tranche with following data :tranches:{ time : block.timestamp+2; vestedFraction: 8000 }
getVestedFraction will return 8000 event if it is the last vested , lets assume total amount is
record.total=1e18
1e18∗8000/10000=800000000000000000(0.8ether) So here the user was suppose to claim the total=1e18(1 ether);
Can you provide a permalink to comment you're mentioning ?
please have a look at here : https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update-amankakar/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol#L31-L53.
It says : After the last tranche time, the vested fraction will be the fraction denominator, it means It returns denominator if last tranche end time is passed not during that period
please have a look at here : https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update-amankakar/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol#L31-L53.
It says : After the last tranche time, the vested fraction will be the fraction denominator, it means It returns denominator if last tranche end time is passed not during that period
Exactly that is my point : How can we identify that the last tranche time has passed at first place.
lets understand this with example data:
Alice create Tranche Vesting with following data for Bob as a beneficiary:
record.total= 1 ether; with the following vesting time:
{ time: block.timesstamp+1; vestedFraction:8000; }
Alice after block.timesstamp+100
calls the claim function(At this timestamp Alice would be able to claim all the tokens). The claim calls the getVestedFraction
to identify how much token can be claimed at this point.
The code Snippet which is used to calculate the vestedFraction:
for (uint256 i = tranches.length; i != 0; ) {
unchecked {
--i;
}
// * After the last tranche time, the vested fraction will be the fraction denominator.
// it
if (time - delay > tranches[i].time ) { // Here the Time for last tranche has already passed , but it still return the vestedFraction
return tranches[i].vestedFraction;
}
}
This function which calculates the claimable amount is getClaimableAmount
:
uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator;
Now put our values and vestedFraction
into above calcualtion:
$claimable= 1e18*8000/10000=> 0.8$ . it return 0.8 ethers but it should have returned 1 ether for which the bob is entitled to receive.
Now the state Variable record.claimed=0.8 ether
. The Bob will never be able to claim the 0.2 ether
because the claim function will always return 0.8 ether
which he has already claimed.
return record.claimed >= claimable //
? 0 // no more tokens to claim
: claimable - record.claimed; // claim all available tokens
Not as a proof but to let you know that the sponsor has already conformed it in discord private chat. you can share it with them.
please have a look at here : https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update-amankakar/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol#L31-L53.
It says : After the last tranche time, the vested fraction will be the fraction denominator, it means It returns denominator if last tranche end time is passed not during that period
Exactly that is my point : How can we identify that the last tranche time has passed at first place. lets understand this with example data: Alice create Tranche Vesting with following data for Bob as a beneficiary: record.total= 1 ether; with the following vesting time:
{ time: block.timesstamp+1; vestedFraction:8000; }
Alice after
block.timesstamp+100
calls the claim function(At this timestamp Alice would be able to claim all the tokens). The claim calls thegetVestedFraction
to identify how much token can be claimed at this point. The code Snippet which is used to calculate the vestedFraction:for (uint256 i = tranches.length; i != 0; ) { unchecked { --i; } // * After the last tranche time, the vested fraction will be the fraction denominator. // it if (time - delay > tranches[i].time ) { // Here the Time for last tranche has already passed , but it still return the vestedFraction return tranches[i].vestedFraction; } }
This function which calculates the claimable amount is
getClaimableAmount
:uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator;
Now put our values and
vestedFraction
into above calcualtion: claimable=1e18∗8000/10000=>0.8 . it return 0.8 ethers but it should have returned 1 ether for which the bob is entitled to receive. Now the state Variablerecord.claimed=0.8 ether
. The Bob will never be able to claim the0.2 ether
because the claim function will always return0.8 ether
which he has already claimed.return record.claimed >= claimable // ? 0 // no more tokens to claim : claimable - record.claimed; // claim all available tokens
Not as a proof but to let you know that the sponsor has already conformed it in discord private chat. you can share it with them.
In the scenario you described after end time user vested fraction would be denominator, since he claimed 0.8 ether so he would receive 0.2 ether, am I right or misunderstood something ?
After the vested end time the user would be able to claim 1 ether which is total amount for vesting. Due to issue which I have described he will only be able to claim 0.8 ether. The remaining 0.2 ether will be locked in the contract.
On Sun, Jun 9, 2024, 4:43 PM xMxAxMx @.***> wrote:
please have a look at here : https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update-amankakar/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol#L31-L53 .
It says : After the last tranche time, the vested fraction will be the fraction denominator, it means It returns denominator if last tranche end time is passed not during that period
Exactly that is my point : How can we identify that the last tranche time has passed at first place. lets understand this with example data: Alice create Tranche Vesting with following data for Bob as a beneficiary: record.total= 1 ether; with the following vesting time: { time: block.timesstamp+1; vestedFraction:8000; }
Alice after block.timesstamp+100 calls the claim function(At this timestamp Alice would be able to claim all the tokens). The claim calls the getVestedFraction to identify how much token can be claimed at this point. The code Snippet which is used to calculate the vestedFraction:
for (uint256 i = tranches.length; i != 0; ) { unchecked { --i; }// * After the last tranche time, the vested fraction will be the fraction denominator.// it if (time - delay > tranches[i].time ) { // Here the Time for last tranche has already passed , but it still return the vestedFraction return tranches[i].vestedFraction; } }
This function which calculates the claimable amount is getClaimableAmount :
uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator;
Now put our values and vestedFraction into above calcualtion: claimable=1e18∗8000/10000=>0.8 . it return 0.8 ethers but it should have returned 1 ether for which the bob is entitled to receive. Now the state Variable record.claimed=0.8 ether. The Bob will never be able to claim the 0.2 ether because the claim function will always return 0.8 ether which he has already claimed.
return record.claimed >= claimable // ? 0 // no more tokens to claim : claimable - record.claimed; // claim all available tokens
Not as a proof but to let you know that the sponsor has already conformed it in discord private chat. you can share it with them.
In the scenario you described after end time user vested fraction would be denominator, since he claimed 0.8 ether so he would receive 0.2 ether, am I right or misunderstood something ?
— Reply to this email directly, view it on GitHub https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update-judging/issues/46#issuecomment-2156458266, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJD5ESN7IFYMSIT4JYRRHVTZGQ5VPAVCNFSM6AAAAABI26YVZCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJWGQ2TQMRWGY . You are receiving this because you commented.Message ID: <sherlock-audit/2024-05-tokensoft-distributor-contracts-update-judging/issues/46/2156458266 @github.com>
Hi @MxAxM , Thanks for your support and patience all the way in this debate. I have written a code POC which will help you to understand what i am trying to explain. please add this test case file to project and test it.
import "forge-std/Test.sol";
contract TestClaimable is Test {
struct Tranche {
uint128 time;
uint128 vestedFraction;
}
uint256 totalVested = 1 ether; // total amount a user can claim
uint256 claimed = 0;
uint256 fractionDenominator = 10000; // Fee denominator
function setUp() external {}
function testClaimableAmount() external {
bytes memory tranches = encodeTranches();
vm.warp(block.timestamp); // before the time it will return 0.
uint256 amountBeforeTime = getClaimableAmount(address(0), tranches);
assertEq(amountBeforeTime, 0);
vm.warp(block.timestamp + 2 days); // the time has passed
// @audit : At this point the user must be able to claim All the vested token in this tranche
uint256 amountAfterEndTime = getClaimableAmount(address(0), tranches);
// but he only receive 0.8 ether
assertEq(amountAfterEndTime, 0.8 ether);
// Assume here user has claim 0.8 ether so we store this value in claim state veraible
claimed += amountAfterEndTime;
vm.warp(block.timestamp + 2 days); // here again tries to claim the 0.2 ether left from 1 ether
uint256 triesToClaimLeftEther = getClaimableAmount(
address(0),
tranches
);
// will revert because the amount is not correct
vm.expectRevert();
assertEq(triesToClaimLeftEther, 0.2 ether);
assertEq(triesToClaimLeftEther, 0);
}
function getClaimableAmount(
address beneficiary,
bytes memory data
) public view returns (uint256) {
uint256 claimable = (totalVested *
getVestedFraction(beneficiary, block.timestamp, data)) /
fractionDenominator;
return
claimed >= claimable
? 0 // no more tokens to claim
: claimable - claimed; // claim all available tokens
}
function getVestedFraction(
address beneficiary,
uint256 time,
bytes memory data
) public view returns (uint256) {
Tranche[] memory tranches = decodeTranches(data);
uint256 delay = 0;
for (uint256 i = tranches.length; i != 0; ) {
unchecked {
--i;
}
if (time - delay > tranches[i].time) {
return tranches[i].vestedFraction;
}
}
return 0; // will return 0 here
}
function encodeTranches() public view returns (bytes memory) {
Tranche[] memory tranchesArray = new Tranche[](1);
tranchesArray[0] = Tranche({
time: uint128(block.timestamp + 10),
vestedFraction: 8000
});
return abi.encode(tranchesArray);
}
function decodeTranches(
bytes memory data
) public pure returns (Tranche[] memory) {
Tranche[] memory tranches = abi.decode(data, (Tranche[]));
return tranches;
}
}
After the vested end time the user would be able to claim 1 ether which is total amount for vesting. Due to issue which I have described he will only be able to claim 0.8 ether. The remaining 0.2 ether will be locked in the contract. … On Sun, Jun 9, 2024, 4:43 PM xMxAxMx @.**> wrote: please have a look at here : https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update-amankakar/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol#L31-L53 . It says : After the last tranche time, the vested fraction will be the fraction denominator, it means It returns denominator if last tranche end time is passed not during that period Exactly that is my point : How can we identify that the last tranche time has passed at first place. lets understand this with example data: Alice create Tranche Vesting with following data for Bob as a beneficiary: record.total= 1 ether; with the following vesting time: { time: block.timesstamp+1; vestedFraction:8000; } Alice after block.timesstamp+100 calls the claim function(At this timestamp Alice would be able to claim all the tokens). The claim calls the getVestedFraction to identify how much token can be claimed at this point. The code Snippet which is used to calculate the vestedFraction: for (uint256 i = tranches.length; i != 0; ) { unchecked { --i; }// After the last tranche time, the vested fraction will be the fraction denominator.// it if (time - delay > tranches[i].time ) { // Here the Time for last tranche has already passed , but it still return the vestedFraction return tranches[i].vestedFraction; } } This function which calculates the claimable amount is getClaimableAmount : uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator; Now put our values and vestedFraction into above calcualtion: claimable=1e18∗8000/10000=>0.8 . it return 0.8 ethers but it should have returned 1 ether for which the bob is entitled to receive. Now the state Variable record.claimed=0.8 ether. The Bob will never be able to claim the 0.2 ether because the claim function will always return 0.8 ether which he has already claimed. return record.claimed >= claimable // ? 0 // no more tokens to claim : claimable - record.claimed; // claim all available tokens Not as a proof but to let you know that the sponsor has already conformed it in discord private chat. you can share it with them. In the scenario you described after end time user vested fraction would be denominator, since he claimed 0.8 ether so he would receive 0.2 ether, am I right or misunderstood something ? — Reply to this email directly, view it on GitHub <#46 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJD5ESN7IFYMSIT4JYRRHVTZGQ5VPAVCNFSM6AAAAABI26YVZCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJWGQ2TQMRWGY . You are receiving this because you commented.Message ID: <sherlock-audit/2024-05-tokensoft-distributor-contracts-update-judging/issues/46/2156458266 @github.com>
vestedFraction means percentage of tokens that is vesting and fraction denominator means 100% percentage of vested amount
After the vested end time the user would be able to claim 1 ether which is total amount for vesting. Due to issue which I have described he will only be able to claim 0.8 ether. The remaining 0.2 ether will be locked in the contract. … On Sun, Jun 9, 2024, 4:43 PM xMxAxMx @.**> wrote: please have a look at here : https://github.com/sherlock-audit/2024-05-tokensoft-distributor-contracts-update-amankakar/blob/main/contracts/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol#L31-L53 . It says : After the last tranche time, the vested fraction will be the fraction denominator, it means It returns denominator if last tranche end time is passed not during that period Exactly that is my point : How can we identify that the last tranche time has passed at first place. lets understand this with example data: Alice create Tranche Vesting with following data for Bob as a beneficiary: record.total= 1 ether; with the following vesting time: { time: block.timesstamp+1; vestedFraction:8000; } Alice after block.timesstamp+100 calls the claim function(At this timestamp Alice would be able to claim all the tokens). The claim calls the getVestedFraction to identify how much token can be claimed at this point. The code Snippet which is used to calculate the vestedFraction: for (uint256 i = tranches.length; i != 0; ) { unchecked { --i; }// After the last tranche time, the vested fraction will be the fraction denominator.// it if (time - delay > tranches[i].time ) { // Here the Time for last tranche has already passed , but it still return the vestedFraction return tranches[i].vestedFraction; } } This function which calculates the claimable amount is getClaimableAmount : uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, data)) / fractionDenominator; Now put our values and vestedFraction into above calcualtion: claimable=1e18∗8000/10000=>0.8 . it return 0.8 ethers but it should have returned 1 ether for which the bob is entitled to receive. Now the state Variable record.claimed=0.8 ether. The Bob will never be able to claim the 0.2 ether because the claim function will always return 0.8 ether which he has already claimed. return record.claimed >= claimable // ? 0 // no more tokens to claim : claimable - record.claimed; // claim all available tokens Not as a proof but to let you know that the sponsor has already conformed it in discord private chat. you can share it with them. In the scenario you described after end time user vested fraction would be denominator, since he claimed 0.8 ether so he would receive 0.2 ether, am I right or misunderstood something ? — Reply to this email directly, view it on GitHub <#46 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJD5ESN7IFYMSIT4JYRRHVTZGQ5VPAVCNFSM6AAAAABI26YVZCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJWGQ2TQMRWGY . You are receiving this because you commented.Message ID: </issues/46/2156458266 @github.com>
I think you misunderstood difference between vestedFraction and fraction denominator, vestedFraction means total amount of tokens that is vesting and fraction denominator means 100% percentage of vested amount
Have you checked the POC?.
The vestedFraction
by definition is not totalAmount
it is the fraction of totalAmount that is available to be claimed at given time.
For Total Amount that would be vested we have this struct
struct DistributionRecord {
bool initialized; // has the claim record been initialized
uint120 total; // total token quantity claimable
uint120 claimed; // token quantity already claimed
}
/it value initializes when we create new Vesting inside : _initializeDistributionRecord().
records[beneficiary] = DistributionRecord(true, totalAmount, records[beneficiary].claimed);
So vestedFraction should be equal to fraction denominator.
Owner sets vestedFraction at setTranches function, we consider admin would set this value correctly
setTranches
could never be called because it is not there for reference please run this test case .
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.21;
import "forge-std/Test.sol";
import "../../contracts/claim/factory/TrancheVestingMerkleDistributorFactory.sol";
import "../../contracts/claim/factory/TrancheVestingMerkleDistributor.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../../contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol";
import "../../contracts/claim/factory/PerAddressTrancheVestingMerkleDistributorFactory.sol";
import "../../contracts/utilities/Registry.sol";
contract TrancheVestingMerkleDistributorFactoryTest is Test {
TrancheVestingMerkleDistributor implementation;
TrancheVestingMerkleDistributor clone;
TrancheVestingMerkleDistributorFactory factory;
PerAddressTrancheVestingMerkleDistributorFactory factory1;
Tranche[] tranches = [Tranche(1, 8000)];
PerAddressTrancheVestingMerkleDistributor implementation1;
function setUp() public {
implementation = new TrancheVestingMerkleDistributor();
factory = new TrancheVestingMerkleDistributorFactory(
address(implementation)
);
}
function test_SetUp() public {
assertEq(address(factory.getImplementation()), address(implementation));
}
function test_DeployDistributor() public {
clone = factory.deployDistributor(
IERC20(token),
1000,
"uri",
tranches,
bytes32(0),
0,
address(this),
0
);
assertEq(clone.owner(), address(this));
assertEq(clone.getSweepRecipient(), address(this));
}
function test_PredictDistributorAddress() public {
uint256 total = 1000;
string memory uri = "uri";
bytes32 merkleRoot = bytes32(0);
uint160 maxDelayTime = 0;
address owner = address(this);
uint256 nonce = 1;
address nextCloneAddress = factory.predictDistributorAddress(
token,
total,
uri,
tranches,
merkleRoot,
maxDelayTime,
owner,
nonce
);
TrancheVestingMerkleDistributor nextClone = factory.deployDistributor(
token,
total,
uri,
tranches,
merkleRoot,
maxDelayTime,
owner,
nonce
);
assertEq(nextCloneAddress, address(nextClone));
}
function testClaimRevert() external {
PerAddressTrancheVestingMerkleDistributor distributor = new PerAddressTrancheVestingMerkleDistributor();
distributor.initialize(
IERC20(token),
1000,
"uri",
bytes32(
0xf32ce147ef6c8e7a07fbe3ce1ae1aef2405a59b50db2a8ede14f4e75bfe7d949
),
10,
address(this)
);
factory1 = new PerAddressTrancheVestingMerkleDistributorFactory(
address(distributor)
);
implementation1 = factory1.deployDistributor(
IERC20(token),
1000,
"uri",
bytes32(
0xf32ce147ef6c8e7a07fbe3ce1ae1aef2405a59b50db2a8ede14f4e75bfe7d949
),
10,
address(this),
1
);
implementation1.setToken(IERC20(token));
bytes32[] memory proof = new bytes32[](1);
token.transfer(address(implementation1), 10e18);
proof[0] = bytes32(
0xa4170e52dc35c1127b67e1c2f5466fce9673e61b44e077b7023f7c994d55c5dd
);
try implementation1.claim(
1,
address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8),
5000000000000000000000,
proof
){} catch{}
// Tranche[] memory _tranches = new
uint256 frc = implementation1.setTranches(tranches);
}
}
Please add this test file inside foundry test cases and run with command : forge test --mt testClaimRevert
. it will gives you error : Member "setTranches" not found or not visible after argument-dependent lookup in contract PerAddressTrancheVestingMerkleDistributor.
setTranches
could never be called because it is not there for reference please run this test case .// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.21; import "forge-std/Test.sol"; import "../../contracts/claim/factory/TrancheVestingMerkleDistributorFactory.sol"; import "../../contracts/claim/factory/TrancheVestingMerkleDistributor.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../../contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol"; import "../../contracts/claim/factory/PerAddressTrancheVestingMerkleDistributorFactory.sol"; import "../../contracts/utilities/Registry.sol"; contract TrancheVestingMerkleDistributorFactoryTest is Test { TrancheVestingMerkleDistributor implementation; TrancheVestingMerkleDistributor clone; TrancheVestingMerkleDistributorFactory factory; PerAddressTrancheVestingMerkleDistributorFactory factory1; Tranche[] tranches = [Tranche(1, 8000)]; PerAddressTrancheVestingMerkleDistributor implementation1; function setUp() public { implementation = new TrancheVestingMerkleDistributor(); factory = new TrancheVestingMerkleDistributorFactory( address(implementation) ); } function test_SetUp() public { assertEq(address(factory.getImplementation()), address(implementation)); } function test_DeployDistributor() public { clone = factory.deployDistributor( IERC20(token), 1000, "uri", tranches, bytes32(0), 0, address(this), 0 ); assertEq(clone.owner(), address(this)); assertEq(clone.getSweepRecipient(), address(this)); } function test_PredictDistributorAddress() public { uint256 total = 1000; string memory uri = "uri"; bytes32 merkleRoot = bytes32(0); uint160 maxDelayTime = 0; address owner = address(this); uint256 nonce = 1; address nextCloneAddress = factory.predictDistributorAddress( token, total, uri, tranches, merkleRoot, maxDelayTime, owner, nonce ); TrancheVestingMerkleDistributor nextClone = factory.deployDistributor( token, total, uri, tranches, merkleRoot, maxDelayTime, owner, nonce ); assertEq(nextCloneAddress, address(nextClone)); } function testClaimRevert() external { PerAddressTrancheVestingMerkleDistributor distributor = new PerAddressTrancheVestingMerkleDistributor(); distributor.initialize( IERC20(token), 1000, "uri", bytes32( 0xf32ce147ef6c8e7a07fbe3ce1ae1aef2405a59b50db2a8ede14f4e75bfe7d949 ), 10, address(this) ); factory1 = new PerAddressTrancheVestingMerkleDistributorFactory( address(distributor) ); implementation1 = factory1.deployDistributor( IERC20(token), 1000, "uri", bytes32( 0xf32ce147ef6c8e7a07fbe3ce1ae1aef2405a59b50db2a8ede14f4e75bfe7d949 ), 10, address(this), 1 ); implementation1.setToken(IERC20(token)); bytes32[] memory proof = new bytes32[](1); token.transfer(address(implementation1), 10e18); proof[0] = bytes32( 0xa4170e52dc35c1127b67e1c2f5466fce9673e61b44e077b7023f7c994d55c5dd ); try implementation1.claim( 1, address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8), 5000000000000000000000, proof ){} catch{} // Tranche[] memory _tranches = new uint256 frc = implementation1.setTranches(tranches); } }
Please add this test file inside foundry test cases and run with command :
forge test --mt testClaimRevert
. it will gives you error :Member "setTranches" not found or not visible after argument-dependent lookup in contract PerAddressTrancheVestingMerkleDistributor.
Do you have any idea why this test reverts ? I think there is a problem with your PoC since it doesn't recognize setTranches
setTranches
could never be called because it is not there for reference please run this test case .// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.21; import "forge-std/Test.sol"; import "../../contracts/claim/factory/TrancheVestingMerkleDistributorFactory.sol"; import "../../contracts/claim/factory/TrancheVestingMerkleDistributor.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../../contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol"; import "../../contracts/claim/factory/PerAddressTrancheVestingMerkleDistributorFactory.sol"; import "../../contracts/utilities/Registry.sol"; contract TrancheVestingMerkleDistributorFactoryTest is Test { TrancheVestingMerkleDistributor implementation; TrancheVestingMerkleDistributor clone; TrancheVestingMerkleDistributorFactory factory; PerAddressTrancheVestingMerkleDistributorFactory factory1; Tranche[] tranches = [Tranche(1, 8000)]; PerAddressTrancheVestingMerkleDistributor implementation1; function setUp() public { implementation = new TrancheVestingMerkleDistributor(); factory = new TrancheVestingMerkleDistributorFactory( address(implementation) ); } function test_SetUp() public { assertEq(address(factory.getImplementation()), address(implementation)); } function test_DeployDistributor() public { clone = factory.deployDistributor( IERC20(token), 1000, "uri", tranches, bytes32(0), 0, address(this), 0 ); assertEq(clone.owner(), address(this)); assertEq(clone.getSweepRecipient(), address(this)); } function test_PredictDistributorAddress() public { uint256 total = 1000; string memory uri = "uri"; bytes32 merkleRoot = bytes32(0); uint160 maxDelayTime = 0; address owner = address(this); uint256 nonce = 1; address nextCloneAddress = factory.predictDistributorAddress( token, total, uri, tranches, merkleRoot, maxDelayTime, owner, nonce ); TrancheVestingMerkleDistributor nextClone = factory.deployDistributor( token, total, uri, tranches, merkleRoot, maxDelayTime, owner, nonce ); assertEq(nextCloneAddress, address(nextClone)); } function testClaimRevert() external { PerAddressTrancheVestingMerkleDistributor distributor = new PerAddressTrancheVestingMerkleDistributor(); distributor.initialize( IERC20(token), 1000, "uri", bytes32( 0xf32ce147ef6c8e7a07fbe3ce1ae1aef2405a59b50db2a8ede14f4e75bfe7d949 ), 10, address(this) ); factory1 = new PerAddressTrancheVestingMerkleDistributorFactory( address(distributor) ); implementation1 = factory1.deployDistributor( IERC20(token), 1000, "uri", bytes32( 0xf32ce147ef6c8e7a07fbe3ce1ae1aef2405a59b50db2a8ede14f4e75bfe7d949 ), 10, address(this), 1 ); implementation1.setToken(IERC20(token)); bytes32[] memory proof = new bytes32[](1); token.transfer(address(implementation1), 10e18); proof[0] = bytes32( 0xa4170e52dc35c1127b67e1c2f5466fce9673e61b44e077b7023f7c994d55c5dd ); try implementation1.claim( 1, address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8), 5000000000000000000000, proof ){} catch{} // Tranche[] memory _tranches = new uint256 frc = implementation1.setTranches(tranches); } }
Please add this test file inside foundry test cases and run with command :
forge test --mt testClaimRevert
. it will gives you error :Member "setTranches" not found or not visible after argument-dependent lookup in contract PerAddressTrancheVestingMerkleDistributor.
Do you have any idea why this test reverts ? I think there is a problem with your PoC since it recognize setTranches
I don't think that becuase the claim function is working which is inside PerAddressTrancheVestingMerkle
and the set Tranche function is in not there lets have look at contract link with other contracts:
PerAddressTrancheVestingMerkle -> PerAddressTrancheVesting -> AdvancedDistributor -> Distributor . these contract does not have any link with the TrancheVestingMerkleDistributor:TrancheVestingInitializable:setTranches. In contract TrancheVestingInitializable
inside _setTranche
function they also have this check require(lastVestedFraction == fractionDenominator, "last tranche must vest all tokens");
which is exactly my point in this finding . if PerAddressTrancheVestingMerkle
some how have access to this _setTranche
function then i would not have submitted this finding.
That why it says that setTranche
is not found . my test case is correct. i can also share the diagram if you require.
Escalate
On basis of above points discussed with lead judge.
The escalation could not be created because you are not exceeding the escalation threshold.
You can view the required number of additional valid issues/judging contest payouts in your Profile page, in the Sherlock webapp.
PerAddressTrancheVestingMerkle works different that other TrancheVestings, tranche array is passed as parameter to claim => _executeClaim > super._executeClaim => getClaimableAmount => getVestedFraction, then getVestedFraction decodes data and use tranche array which contains vestedFraction so it doesn't need to setTranches manually
In TrancheVesting there is a Tranche[] array that can be set globally by setTranches function but in PerAddressTrancheVestingMerkle it's passed as argument
PerAddressTrancheVestingMerkle works different that other TrancheVestings, tranche array is passed as parameter to claim => _executeClaim > super.executeClaim => getClaimableAmount => getVestedFraction, then getVestedFraction decodes data and use tranche array which contains vestedFraction
So vestedFraction should be equal to fraction denominator.
Owner sets vestedFraction at setTranches function, we consider admin would set this value correctly
The above quoted point was shared by you.
it doesn't need to setTranches manually this was not my point you shared it that if tranche is wrong then owner can set it via setTranche
function. and my points is that the tranche last vestedFraction is not checked that why we need to check if it is last tranche than instead of relaying on passed tranche data we must return fractionDenominator
. please at least diacuss it with the sponsor Because he has agreed with me on this one in our chat . Because the admin is only trusted for sweep the funds and also add / remove tokens as well
as per readMe we can relay on passed tranche data via sender.
PerAddressTrancheVestingMerkle works different that other TrancheVestings, tranche array is passed as parameter to claim => _executeClaim > super.executeClaim => getClaimableAmount => getVestedFraction, then getVestedFraction decodes data and use tranche array which contains vestedFraction
So vestedFraction should be equal to fraction denominator.
Owner sets vestedFraction at setTranches function, we consider admin would set this value correctly
The above quoted point was shared by you.
it doesn't need to setTranches manually this was not my point you shared it that if tranche is wrong then owner can set it via
setTranche
function. and my points is that the tranche last vestedFraction is not checked that why we need to check if it is last tranche than instead of relaying on passed tranche data we must returnfractionDenominator
. please at least diacuss it with the sponsor Because he has agreed with me on this one in our chat . Because the admin is only trusted forsweep the funds and also add / remove tokens as well
as per readMe we can relay on passed tranche data via sender.
Tranches data is used to validate Merkle proof, see validMerkleProof at claim function, so if there is anything wrong with passed data it won't verified and transaction would revert ( admin sets Merkle Root )
The admin is only trusted for sweep the funds and also add / remove tokens as well As per contest readme. So we can't trust admin for right trance data and also the fraction denominator is used in smart contract. Either verify the last tranche vested fraction or return the fraction denominator as I suggested in my finding. Have you discussed it with the sponsor??
So I've been asked to comment.
It's pretty clear that the data in the Merkle tree needs to pass the same validations as would be done on-chain for the corresponding set function. Any Merkle root that contains improper tranche data, e.g..: tranches whose last tranche is not fractionDenominator, would be invalid input. Admin input validation errors are not valid issues.
I don't think admin input validation error argument is applicable for this issue. The report clearly highlights the logical flaw in tokensoft contract, which incorrectly calculates the last vesting of user. It would be better if @MxAxM can take clarification of this issue from the sponsors.
How wouldn't it be? This issue requires that the admin initialize the contract with a Merkle tree containing invalid tranche data.
The logic is perfectly correct, and is identical to that in TrancheVestingMerkle. It just assumes valid tranche data.
Also, this issue is not even escalated -- not sure why so much ink is being spilled on it.
Also, this issue is not even escalated -- not sure why so much ink is being spilled on it.
So if some one is not able to escalate his issue , he can't ask the judge and add further clarification. I am not sure why you say it.
The best solution is to discuss it with the sponsor as @Hash01011122 he has said.
aman
high
The last index should be checked for fraction denominator
Summary
The comments on
getVestedFraction
states that theif last tranche the vested fraction will be fraction denominator
. However it is not done in case of PerAddressVulnerability Detail
It is not enforce in code that the last vesting must be the fraction denominator. and in other cases like with the continuous vesting we explictly check that if end of vesting has passed return the fraction denominator.
but in case of Tranche Vesting we are not returning the fraction denominator but instead returns vested fraction :
To Identify the Claimable amount at specific time we use following formula :
if
getVestedFraction
returnfractionDenominator
then the user will be able to claim all token.Impact
The claimable amount will not be correct as there is a high chance of that last
vestedFraction != fractionDenominator
. Because we allow full claim after vesting end time but the amount return will not be full amount.Code Snippet
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
change the code to following :