Platform: Code4rena
Start Date: 04/11/2021
Pot Size: $50,000 USDC
Total HM: 20
Participants: 28
Period: 7 days
Judge: 0xean
Total Solo HM: 11
Id: 51
League: ETH
Rank: 8/28
Findings: 4
Award: $2,360.32
π Selected for report: 4
π Solo Findings: 2
π Selected for report: gpersoon
Also found by: elprofesor, fr0zn, pauliax
529.9131 USDC - $529.91
gpersoon
Suppose someone claims the last part of his airdrop via claimExact() of AirdropDistribution.sol Then airdrop[msg.sender].amount will be set to 0.
Suppose you then call validate() again. The check "airdrop[msg.sender].amount == 0" will allow you to continue, because amount has just be set to 0. In the next part of the function, airdrop[msg.sender] is overwritten with fresh values and airdrop[msg.sender].claimed will be reset to 0.
Now you can claim your airdrop again (as long as there are tokens present in the contract)
Note: The function claim() prevents this from happening via "assert(airdrop[msg.sender].amount - claimable != 0);", which has its own problems, see other reported issues.
function claimExact(uint256 _value) external nonReentrant { require(msg.sender != address(0)); require(airdrop[msg.sender].amount != 0);
uint256 avail = _available_supply(); uint256 claimable = avail * airdrop[msg.sender].fraction / 10**18; // if (airdrop[msg.sender].claimed != 0){ claimable -= airdrop[msg.sender].claimed; } require(airdrop[msg.sender].amount >= claimable); // amount can be equal to claimable require(_value <= claimable); // _value can be equal to claimable airdrop[msg.sender].amount -= _value; // amount will be set to 0 with the last claim
// https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/AirdropDistribution.sol#L504-L517 function validate() external nonReentrant { ... require(airdrop[msg.sender].amount == 0, "Already validated."); ... Airdrop memory newAirdrop = Airdrop(airdroppable, 0, airdroppable, 10**18 * airdroppable / airdrop_supply); airdrop[msg.sender] = newAirdrop; validated[msg.sender] = 1; // this is set, but isn't checked on entry of this function
Add the following to validate() : require(validated[msg.sender]== 0, "Already validated.");
#0 - chickenpie347
2021-11-10T02:20:20Z
Addressed in issue #101
#1 - CloudEllie
2022-01-05T02:21:46Z
Added labels to match duplicate issue #101
π Selected for report: gpersoon
872.285 USDC - $872.28
gpersoon
Suppose you are an investor and want to claim the last part of your claimable tokens (or your entire set of claimable tokens if you haven't claimed anything yet). Then you call the function claim() of InvestorDistribution.sol, which has the following statement: "require(investors[msg.sender].amount - claimable != 0);" This statement will prevent you from claiming your tokens because it will stop execution.
Note: with the function claimExact() it is possible to claim the last part.
function claim() external nonReentrant { ... require(investors[msg.sender].amount - claimable != 0); investors[msg.sender].amount -= claimable;
Remove the require statement.
#0 - chickenpie347
2021-11-10T02:25:14Z
Duplicate of issue #130
#1 - chickenpie347
2021-11-10T02:26:45Z
I just noticed it's different files. The AirdropDistrbution.sol and InvestorDistribution.sol contracts were built on the same base, with slight changes.
#2 - 0xean
2022-01-08T02:22:46Z
Downgrading to medium risk as an alternative path does exist for claiming the drop. Funds are not lost, but the availability of them is compromised. Per Docs:
2 β Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements. 3 β High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
π Selected for report: gpersoon
872.285 USDC - $872.28
gpersoon
Suppose you are eligible for the last part of your airdrop (or your entire airdrop if you haven't claimed anything yet). Then you call the function claim() of AirdropDistribution.sol, which has the following statement: "assert(airdrop[msg.sender].amount - claimable != 0);" This statement will prevent you from claiming your airdrop because it will stop execution.
Note: with the function claimExact() it is possible to claim the last part.
function claim() external nonReentrant { .. assert(airdrop[msg.sender].amount - claimable != 0); airdrop[msg.sender].amount -= claimable;
Remove the assert statement. Also add the following to validate() , to prevent claiming the airdrop again: require(validated[msg.sender]== 0, "Already validated.");
#0 - chickenpie347
2021-11-10T14:08:57Z
Patched it with assert(airdrop[msg.sender].amount - claimable >= 0);
the >=0 check is just to ensure the claimant does not end up claiming more than allocated due to any fringe case.
#1 - 0xean
2022-01-08T02:22:41Z
Downgrading to medium risk as an alternative path does exist for claiming the drop. Funds are not lost, but the availability of them is compromised. Per Docs:
2 β Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements. 3 β High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
85.8459 USDC - $85.85
gpersoon
Anyone can call the function vest() of Vesting.sol, for example with a smail "_amount" of tokens, for any _beneficiary.
The function overwrites the value of benRevocable[_beneficiary], effectively erasing any previous value.
So you can set any _beneficiary to Revocable. Although revoke() is only callable by the owner, this is circumventing the entire mechanism of benRevocable.
function vest(address _beneficiary, uint256 _amount, uint256 _isRevocable) external payable whenNotPaused { ... if(_isRevocable == 0){ benRevocable[_beneficiary] = [false,false]; // just overwrites the value } else if(_isRevocable == 1){ benRevocable[_beneficiary] = [true,false]; // just overwrites the value }
Whitelist the calling of vest() Or check if values for benRevocable are already set.