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

The `Airdrop::claim` public (Shouldn't be external?) function permits individuals(singles) to claim one `love token per day`, severely compromising and abusing the Protocol and its funds.

Summary

Firstly, the Airdrop::claim function possesses a public visibility specifier. Given that the function isn't invoked from any other dependent contract, it ought to have an external visibility specifier.

The concerning aspect of the Airdrop::claim function is its allowance for anyone to claim One Love Token Per Day, irrespective of their relationship status. This behavior is severely detrimental and deviates from the protocol's intended functionality.

// ----------------------
// ----------- ||
// ----- \/
@> function claim() public {
// No LoveToken for people who don't love their soulmates anymore.
if (soulmateContract.isDivorced()) 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);
}

Vulnerability Details

Please expand to see details by clicking on it.

Anyone Can Claim Love Token
  1. Place the following test code snippet into the test/unit/soulmateTest.t.sol file. Put it at the very bottom but before the last closing semicolon }.

// replace imports at the top
import {console2} from "forge-std/Test.sol";
import {BaseTest} from "./BaseTest.t.sol";
import {Soulmate} from "../../src/Soulmate.sol";
import {ERC721} from "@solmate/tokens/ERC721.sol";
import {IVault} from "../../src/interface/IVault.sol";
import {ISoulmate} from "../../src/interface/ISoulmate.sol";
import {ILoveToken} from "../../src/interface/ILoveToken.sol";
import {IStaking} from "../../src/interface/IStaking.sol";
import {Vault} from "../../src/Vault.sol";
import {LoveToken} from "../../src/LoveToken.sol";
import {Airdrop} from "../../src/Airdrop.sol";
import {Staking} from "../../src/Staking.sol";
function testAirdropAnyoneCanClaim() public {
Vault airdropVault_tst = new Vault();
Vault stakingVault_tst = new Vault();
Soulmate soulmateContract_tst = new Soulmate();
LoveToken loveToken_tst = new LoveToken(
ISoulmate(address(soulmateContract_tst)), address(airdropVault_tst), address(stakingVault_tst)
);
Airdrop airdropContract_tst = new Airdrop(
ILoveToken(address(loveToken_tst)),
ISoulmate(address(soulmateContract_tst)),
IVault(address(airdropVault_tst))
);
airdropVault_tst.initVault(ILoveToken(address(loveToken_tst)), address(airdropContract_tst));
vm.expectRevert();
airdropVault_tst.initVault(ILoveToken(address(loveToken_tst)), address(airdropContract_tst));
uint256 totalSupply_one = loveToken_tst.totalSupply();
uint256 airdropVaultBalance = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
// 500_000_000_000000000000000000
console2.log("total supply : ", totalSupply_one);
console2.log("airdropVaultBalance : ", airdropVaultBalance);
console2.log("airdropAsManagerSpendAllowance: ", airdropAsManagerSpendAllowance);
address alice = makeAddr("ALICE");
uint256 aliceId = soulmateContract_tst.ownerToId(alice);
console2.log("Alice minted with ID : ", aliceId);
console2.log("Soulmate of Alice : ", soulmateContract_tst.soulmateOf(alice));
uint256 oneDaysInSecond = airdropContract_tst.daysInSecond();
vm.warp(oneDaysInSecond);
console2.log("seconds in one day: ", oneDaysInSecond);
vm.startPrank(alice);
airdropContract_tst.claim();
vm.stopPrank();
// Alice is waiting for her soulmate to be assigned but still she can claim the `Love Token`.
// This severly breaks the Protocol and can claim 1 Love token per day.
uint256 aliceLoveTokenBalance = loveToken_tst.balanceOf(alice);
uint256 totalSupply_updated = loveToken_tst.totalSupply();
uint256 airdropVaultBalance_updated = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance_updated =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
console2.log("total supply Updated : ", totalSupply_updated);
console2.log("aliceLoveTokenBalance : ", aliceLoveTokenBalance);
console2.log("airdropVaultBalance Updated : ", airdropVaultBalance_updated);
console2.log("airdropAsManagerSpendAllowance updated: ", airdropAsManagerSpendAllowance_updated);
assertEq(aliceLoveTokenBalance, 1e18);
assertEq(airdropVaultBalance_updated, airdropVaultBalance - aliceLoveTokenBalance);
assertEq(airdropAsManagerSpendAllowance_updated, airdropAsManagerSpendAllowance - aliceLoveTokenBalance);
}
  1. Open Your Bash Terminal and execute the following command...

forge test --mt "testAirdropAnyoneCanClaim" --via-ir -vv
  1. Some output might appear upon executing the above command. Take a look at that output.

  2. Now it's clear that anyone no matter of their relationship status, they can claim One Love Token Per Day.

Impact

Individuals with knowledge of this vulnerability can exploit it to steal One Love Token per day. They may be willing to wait for a day to claim tokens, even without having a soulmate. This vulnerability compromises the entire functionality of the Protocol, facilitates fund theft, and undermines the integrity of the Protocol.

Tools Used

Foundry Framework (Solidity, Rust)

Recommendations

Mitigation is simple, We can employ an if check to check whether a person has a soulmate.

Update src/Airdrop.sol file like below...

...
...
...
error Airdrop__CoupleIsDivorced();
error Airdrop__PreviousTokenAlreadyClaimed();
+ error Airdrop__dontHaveSoulmate(address soulmate);
...
...
...
function claim() public {
// No LoveToken for people who don't love their soulmates anymore.
if (soulmateContract.isDivorced()) revert Airdrop__CoupleIsDivorced();
+ address soulmate = soulmateContract.soulmateOf(msg.sender);
+ if (soulmate == address(0)) revert Airdrop__dontHaveSoulmate(soulmate);
// 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.