Summary
Claim function allows divorced soulmates to claim airdrop
Vulnerability Details
Airdrop:: claim
function is meant to reward soulmates for staying along, more time they will be along, more tokens they will be earning.
Here is the function -
function claim() public {
@> if (soulmateContract.isDivorced()) revert Airdrop__CoupleIsDivorced();
uint256 numberOfDaysInCouple = (block.timestamp -
soulmateContract.idToCreationTimestamp(
soulmateContract.ownerToId(msg.sender)
)) / daysInSecond;
uint256 amountAlreadyClaimed = _claimedBy[msg.sender];
if (
amountAlreadyClaimed >=
numberOfDaysInCouple * 10 ** loveToken.decimals()
) revert Airdrop__PreviousTokenAlreadyClaimed();
uint256 tokenAmountToDistribute = (numberOfDaysInCouple *
10 ** loveToken.decimals()) - amountAlreadyClaimed;
if (
tokenAmountToDistribute >=
loveToken.balanceOf(address(airdropVault))
) {
tokenAmountToDistribute = loveToken.balanceOf(
address(airdropVault)
);
}
_claimedBy[msg.sender] += tokenAmountToDistribute;
emit TokenClaimed(msg.sender, tokenAmountToDistribute);
loveToken.transferFrom(
address(airdropVault),
msg.sender,
tokenAmountToDistribute
);
}
If we notice highlighted line, it call the function isDivorced
on soulmate contract to check if caller is divorced or not. But this approach has flaw.
When the function is called on soulmate contract
, msg.sender is the airdrop contract. so it will return false everytime, no matter if a couple is divorced or not. This will make divorced couples to claim the airdrop, which they was not supposed to.
POC
In existing AirdropTest.t.sol
file, import following line
+ import {console2} from "forge-std/Test.sol";
then add the following test -
function testClaimWhenSoulmatesAreDivorced() public {
_mintOneTokenForBothSoulmates();
vm.startPrank(soulmate1);
soulmateContract.getDivorced();
console2.log("soulmate1 got divorce:", soulmateContract.isDivorced() );
vm.stopPrank();
vm.prank(soulmate2);
console2.log("soulmate2 got divorce:", soulmateContract.isDivorced() );
vm.stopPrank();
vm.startPrank(soulmate1);
vm.warp(block.timestamp + 7 days);
uint256 balanceBefore = loveToken.balanceOf(soulmate1);
airdropContract.claim();
uint256 balanceAfter = loveToken.balanceOf(soulmate1);
console2.log("love token claimed by soulmate1:", balanceAfter - balanceBefore);
vm.stopPrank();
vm.startPrank(soulmate2);
uint256 balanceBefore1 = loveToken.balanceOf(soulmate2);
airdropContract.claim();
uint256 balanceAfter1 = loveToken.balanceOf(soulmate2);
console2.log("love token claim by soulmate2:", balanceAfter1 - balanceBefore1);
vm.stopPrank();
}
Now, run the forge test --mt testClaimWhenSoulmatesAreDivorced -vv
in your terminal. You'll get following output -
[⠢] Compiling...
[⠒] Compiling 1 files with 0.8.23
[⠢] Solc 0.8.23 finished in 1.89s
Running 1 test for test/unit/AirdropTest.t.sol:AirdropTest
[PASS] testClaimWhenSoulmatesAreDivorced() (gas: 401617)
Logs:
soulmate1 got divorce: true
soulmate2 got divorce: true
love token claimed by soulmate1: 7000000000000000000
love token claim by soulmate2: 7000000000000000000
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.08ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
Loss of love tokens, that could have been distributed to more soulmates for longer run.
Tools Used
Manual Review, Foundry
Recommendations
Here is recomendation to fix it -
In Soulmate
contract update the isDivorced
function as follows -
function isDivorced(address user) public view returns (bool) {
return divorced[user];
}
then update the claim function like below:
function claim() public {
// No LoveToken for people who don't love their soulmates anymore.
- if (soulmateContract.isDivorced()) revert Airdrop__CoupleIsDivorced();
+ if (soulmateContract.isDivorced(msg.sender)) revert Airdrop__CoupleIsDivorced();
// Calculating since how long soulmates are reunited
uint256 numberOfDaysInCouple = (block.timestamp -
soulmateContract.idToCreationTimestamp(
soulmateContract.ownerToId(msg.sender)
)) / daysInSecond;
uint256 amountAlreadyClaimed = _claimedBy[msg.sender];
if (
amountAlreadyClaimed >=
numberOfDaysInCouple * 10 ** loveToken.decimals()
) revert Airdrop__PreviousTokenAlreadyClaimed();
uint256 tokenAmountToDistribute = (numberOfDaysInCouple *
10 ** loveToken.decimals()) - amountAlreadyClaimed;
// Dust collector
if (
tokenAmountToDistribute >=
loveToken.balanceOf(address(airdropVault))
) {
tokenAmountToDistribute = loveToken.balanceOf(
address(airdropVault)
);
}
_claimedBy[msg.sender] += tokenAmountToDistribute;
emit TokenClaimed(msg.sender, tokenAmountToDistribute);
loveToken.transferFrom(
address(airdropVault),
msg.sender,
tokenAmountToDistribute
);
}