Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Users currently in soulmate waiting list are able to claim LoveToken from AIrdrop

Summary

The functions Soulmate::mintSoulmateToken and Airdrop::claim contains an inconsistency where users who are in waiting list and have not been matched with a soulmate can still claim tokens because of the ownerToId mapping returning a token ID that is set to nextID when a user first calls mintSoulmateToken, leading to claiming of Airdrop without having a soulmate while still being in the waitlist.

Vulnerability Details

The vulnerability occurs due to insufficient checks inside Airdrop::claim function as a result of which it considers users still in waiting list for soulmate are able to claim LoveToken from Airdrop.

A user calling Soulmate::mintSoulmateToken when there is no user assigned to nextID, then that user will be assigned the nextID in the mapping ownerToId, but notice that the user currently has no soulmate and is waiting for them to call it.

But before the next user calling mintSoulmateToken, the first user will be eligible to claim Airdrop as Aidrop::claim function doesn't implement proper check whether the user has a soulmate or not.

uint256 numberOfDaysInCouple = (block.timestamp -
soulmateContract.idToCreationTimestamp(
soulmateContract.ownerToId(msg.sender)
)) / daysInSecond;

Here the ownerToId for the user will be the nextID and corresponding to that nextID, no nft is created therefore idToCreationTimestamp for that token id will be 0 and the user will be able to claim a large number of LoveToken.

Impact

Users currently in waiting list for other users are able to claim Aidrop by a bigger amount.

PoC

Add the test in the file: test/unit/AirdropTest.t.sol and also import console2 from forge-std/Test.sol

Run the test:

forge test --mt test_UsersInSoulmateWaitingList_Are_Still_AbleToClaimAirdrop -vv
function test_UsersInSoulmateWaitingList_Are_Still_AbleToClaimAirdrop() public {
// the current timestamp of testing this
vm.warp(1707932269);
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
// now the user soulmate1 is in waiting list
// balance before claim
uint256 balanceBeforeClaim = loveToken.balanceOf(soulmate1);
// soulmate1 calls the Airdrop::claim function
vm.prank(soulmate1);
airdropContract.claim();
uint256 claimedBalance = loveToken.balanceOf(soulmate1) - balanceBeforeClaim;
// even though the user was currently in waiting list
// but is still able to claim a large amount of LoveToken
// because of insufficient checks inside claim function
assert(claimedBalance > 0);
console2.log("Claimed Balance:", claimedBalance);
}

Tools Used

Manual Review, Unit Test in Foundry

Recommendations

Add a check in the Aidrop::claim function that if a user is in waiting list then they should not be able to claim Aidrop.

+ error Airdrop__UserInWaitlist();
function claim() public {
// No LoveToken for people who don't love their soulmates anymore.
if (soulmateContract.isDivorced()) revert Airdrop__CoupleIsDivorced();
+ if (soulmateContract.ownerToId(msg.sender) == soulmateContract.totalSupply()) {
+ revert Airdrop__UserInWaitlist();
+ }
// 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
);
}
Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-claim-airdrop-without-owning-NFT

High severity, This issue is separated from the flawed `isDivorced()` check presented in issue #168 as even if that is fixed, if ownership is not checked, isDivorced would still default to false and allow bypass to claim airdrops by posing as tokenId 0 in turn resulting in this [important check for token claim is bypassed.](https://github.com/Cyfrin/2024-02-soulmate/blob/b3f9227942ffd5c443ce6bccaa980fea0304c38f/src/Airdrop.sol#L61-L66). #220 is the most comprehensive issue as it correctly recognizes both issues existing within the same function.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.