Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: medium
Invalid

The `Airdrop::claim` function does not revert if the Protocol runs out of funds. Users may Leave the `Soulmate` Protocol or DAPP.

Summary

The Airdrop::claim public function fails to notify users about rewards shortages or the inability to make further reward claims if the love token amount depletes due to claimers exceeding 500,000,000,000.

Upon careful examination of the functionality, it becomes apparent that there's a lock in src/Vault.sol that restricts and prevents the minting and approvals from occurring more than once. Therefore, it's clear that there's no way to mint and approve the AirDrop and its vault again if the Protocol runs out of funds (Love Tokens).

  1. Airdrop::claim:

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);
// not so safe transfer.
loveToken.transferFrom(address(airdropVault), msg.sender, tokenAmountToDistribute);
}
  1. Vault::initVault

function initVault(ILoveToken loveToken, address managerContract) public {
// -------
// --- ||
// - \/
@> if (vaultInitialize) revert Vault__AlreadyInitialized();
loveToken.initVault(managerContract);
vaultInitialize = true;
}

Vulnerability Details

Out of funds:
  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 testAirdropOutOfFunds() 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");
address bob = makeAddr("BOB");
address charlie = makeAddr("CHARLIE");
address darci = makeAddr("DARCI");
vm.startPrank(alice);
soulmateContract_tst.mintSoulmateToken();
vm.stopPrank();
vm.startPrank(bob);
soulmateContract_tst.mintSoulmateToken();
vm.stopPrank();
vm.startPrank(charlie);
soulmateContract_tst.mintSoulmateToken();
vm.stopPrank();
vm.startPrank(darci);
soulmateContract_tst.mintSoulmateToken();
vm.stopPrank();
uint256 aliceId = soulmateContract_tst.ownerToId(alice);
uint256 bobId = soulmateContract_tst.ownerToId(bob);
uint256 charlieId = soulmateContract_tst.ownerToId(charlie);
uint256 darciId = soulmateContract_tst.ownerToId(darci);
console2.log("Alice minted with ID : ", aliceId);
console2.log("Bob minted with ID : ", bobId);
console2.log("Charlie minted with ID : ", charlieId);
console2.log("Darci minted with ID : ", darciId);
console2.log("Soulmate of Alice : ", soulmateContract_tst.soulmateOf(alice));
console2.log("Soulmate of Bob : ", soulmateContract_tst.soulmateOf(bob));
console2.log("Soulmate of charlie : ", soulmateContract_tst.soulmateOf(charlie));
console2.log("Soulmate of darci : ", soulmateContract_tst.soulmateOf(darci));
uint256 oneDaysInSecond = airdropContract_tst.daysInSecond();
console2.log("---------------------------1 Day---------------------------");
// vm.warp(oneDaysInSecond * 500_000_000_000);
vm.warp(block.timestamp + 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("seconds in one day : ", oneDaysInSecond);
console2.log("total supply Updated : ", totalSupply_updated);
console2.log("aliceLoveTokenBalance : ", aliceLoveTokenBalance);
console2.log("airdropVaultBalance Updated : ", airdropVaultBalance_updated);
console2.log("airdropAsManagerSpendAllowance updated: ", airdropAsManagerSpendAllowance_updated);
console2.log("---------------------------2 Days---------------------------");
vm.warp(block.timestamp + oneDaysInSecond);
vm.startPrank(bob);
airdropContract_tst.claim();
vm.stopPrank();
vm.startPrank(alice);
airdropContract_tst.claim();
vm.stopPrank();
uint256 aliceLoveTokenBalanceNew = loveToken_tst.balanceOf(alice);
uint256 bobLoveTokenBalance = loveToken_tst.balanceOf(bob);
uint256 airdropVaultBalance_updatedNew = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance_updatedNew =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
console2.log("aliceLoveTokenBalanceNew : ", aliceLoveTokenBalanceNew);
console2.log("bobLoveTokenBalance : ", bobLoveTokenBalance);
console2.log("airdropVaultBalance Updated : ", airdropVaultBalance_updatedNew);
console2.log("airdropAsManagerSpendAllowance updated: ", airdropAsManagerSpendAllowance_updatedNew);
console2.log("---------------------------After Million Years---------------------------");
console2.log("Approx 1.40 Billion Years!");
vm.warp((oneDaysInSecond * 499_999_997) + 1);
vm.startPrank(alice);
airdropContract_tst.claim();
vm.stopPrank();
// uint256 bobLoveTokenBalance = loveToken_tst.balanceOf(bob);
uint256 aliceLoveTokenBalanceAfterMillionYears = loveToken_tst.balanceOf(alice);
uint256 bobLoveTokenBalanceAfterMillionYears = loveToken_tst.balanceOf(bob);
uint256 airdropVaultBalance_updatedAfterMillionYears = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance_updatedAfterMillionYears =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
console2.log("Bob haven't Claim yet!");
console2.log("aliceLoveTokenBalanceAfterMillionYears : ", aliceLoveTokenBalanceAfterMillionYears);
console2.log("bobLoveTokenBalanceAfterMillionYears : ", bobLoveTokenBalanceAfterMillionYears);
console2.log(
"airdropVaultBalance Updated : ", airdropVaultBalance_updatedAfterMillionYears
);
console2.log(
"airdropAsManagerSpendAllowance updated : ",
airdropAsManagerSpendAllowance_updatedAfterMillionYears
);
console2.log("-------------Now Bob claims-----------");
vm.startPrank(bob);
airdropContract_tst.claim();
vm.stopPrank();
uint256 numDays = 499_999_997 - 2;
uint256 bobLoveTokenBalanceAfterMillionYearsClaimed = loveToken_tst.balanceOf(bob);
console2.log("Bob's expected claim : ", numDays * 10 ** 18);
console2.log(
"BobLoveTokenBalanceAfterMillionYears actual : ", bobLoveTokenBalanceAfterMillionYearsClaimed
);
console2.log("-------------------Now charlie and darci claims-------------------");
vm.startPrank(charlie);
airdropContract_tst.claim();
vm.stopPrank();
vm.startPrank(charlie);
airdropContract_tst.claim();
vm.stopPrank();
uint256 charlieLoveTokenBalanceAfterMillionYears = loveToken_tst.balanceOf(charlie);
uint256 darciLoveTokenBalanceAfterMillionYears = loveToken_tst.balanceOf(darci);
console2.log(
"charlieLoveTokenBalanceAfterMillionYears : ", charlieLoveTokenBalanceAfterMillionYears
);
console2.log("darciLoveTokenBalanceAfterMillionYears : ", darciLoveTokenBalanceAfterMillionYears);
}
  1. Open Your Bash Terminal and execute the following command...

forge test --mt "testAirdropOutOfFunds" --via-ir -vv
  1. Some output might appear upon executing the above command. Take a look at that output and Please read the Ouptut carefully.

  2. Now it's clear that there is no notification mechanism to inform users when Love tokens have been depleted due to insufficient funds.

Impact

This lack of communication is unjust to users who have entrusted their Love Token claims to the Protocol with the expectation of redeeming them at a later time. It's also unfair to users who receive a disproportionately small amount of Love Tokens compared to their actual claiming amount. Considering the immense timespan of billions, or perhaps approximately 1.40 billion years, if only one user claims a Love token per day, Users at such an advanced age may indeed feel disheartened or overwhelmed, which could lead them to leave the DAPP or Protocol due to feelings of unluckiness or being inundated with notifications.

Tools Used

Foundry Framework (Solidity, Rust)

Recommendations

There's a few Mitigations approach we can follow...

  1. Develop a more effective logic that can notify users when Love tokens run out of funds and also inform them about the availability of Love tokens after a certain period.

  2. Yes, it's rare to happen. However, as the number of users increases, the likelihood of it occurring also increases. Therefore, a proxified version of Soulmate may play a significant role in notifying users and adding more funds as needed.

One mitigation code to resolve this issue....

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

error Airdrop__CoupleIsDivorced();
error Airdrop__PreviousTokenAlreadyClaimed();
+ error Airdrop__SorryOutOfLoveTokensFunds_AvailableSoon();
...
...
...
mapping(address owner => uint256 alreadyClaimed) private _claimedBy;
+ mapping(address owner => uint256 claimremaining) public s_remainingClaim;
+ event TokenClaimed(address indexed user, uint256 indexed amount, uint256 indexed amountRemaining);
- event TokenClaimed(address indexed user, uint256 amount);
...
...
...
function claim() public {
// No LoveToken for people who don't love their soulmates anymore.
if (soulmateContract.isDivorced()) revert Airdrop__CoupleIsDivorced();
+ if (loveToken.balanceOf(address(airdropVault)) == 0) {
+ revert Airdrop__SorryOutOfLoveTokensFunds_AvailableSoon();
+ }
// Calculating since how long soulmates are reunited
uint256 numberOfDaysInCouple = (
block.timestamp - soulmateContract.idToCreationTimestamp(soulmateContract.ownerToId(msg.sender))
) / daysInSecond;
uint256 amountAlreadyClaimed = _claimedBy[msg.sender];
// info - if check contains a magic number.
if (amountAlreadyClaimed >= numberOfDaysInCouple * 10 ** loveToken.decimals()) {
+ // s_remaining[msg.sender] = 0; // an alternative but a post reset. However it's efficient.
revert Airdrop__PreviousTokenAlreadyClaimed();
}
// info - expression contains a magic number.
uint256 tokenAmountToDistribute = (numberOfDaysInCouple * 10 ** loveToken.decimals()) - amountAlreadyClaimed;
+ uint256 loveTokenVaultBalance = loveToken.balanceOf(address(airdropVault));
+ if (tokenAmountToDistribute > loveTokenVaultBalance) {
+ uint256 remainingClaim = tokenAmountToDistribute - loveTokenVaultBalance;
+ s_remainingClaim[msg.sender] = remainingClaim;
+ tokenAmountToDistribute = loveTokenVaultBalance;
+ } else {
+ s_remainingClaim[msg.sender] = 0;
}
- // Dust collector
- if (tokenAmountToDistribute >= loveToken.balanceOf(address(airdropVault))) {
- tokenAmountToDistribute = loveToken.balanceOf(address(airdropVault));
- }
_claimedBy[msg.sender] += tokenAmountToDistribute;
+ emit TokenClaimed(msg.sender, tokenAmountToDistribute, s_remainingClaim[msg.sender]);
- emit TokenClaimed(msg.sender, tokenAmountToDistribute);
// not so safe transfer.
loveToken.transferFrom(address(airdropVault), msg.sender, tokenAmountToDistribute);
}
...
...
...
  1. Update src/Vault.sol like below...

contract Vault {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
- error Vault__AlreadyInitialized();
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
- bool public vaultInitialize;
/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Init vault with the loveToken.
/// @notice Vault will approve its corresponding management contract to handle tokens.
/// @notice vaultInitialize protect against multiple initialization.
function initVault(ILoveToken loveToken, address managerContract) public {
- if (vaultInitialize) revert Vault__AlreadyInitialized();
loveToken.initVault(managerContract);
- vaultInitialize = true;
}
}
  1. Test Mitigation...

Mitigation Test Proof:
  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 testAirdropOutOfFundsMitigation() 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));
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");
address bob = makeAddr("BOB");
vm.startPrank(alice);
soulmateContract_tst.mintSoulmateToken();
vm.stopPrank();
vm.startPrank(bob);
soulmateContract_tst.mintSoulmateToken();
vm.stopPrank();
uint256 aliceId = soulmateContract_tst.ownerToId(alice);
uint256 bobId = soulmateContract_tst.ownerToId(bob);
console2.log("Alice minted with ID : ", aliceId);
console2.log("Bob minted with ID : ", bobId);
console2.log("Soulmate of Alice : ", soulmateContract_tst.soulmateOf(alice));
console2.log("Soulmate of Bob : ", soulmateContract_tst.soulmateOf(bob));
uint256 oneDaysInSecond = airdropContract_tst.daysInSecond();
console2.log("---------------------------1 Day---------------------------");
vm.warp(block.timestamp + oneDaysInSecond);
vm.startPrank(alice);
airdropContract_tst.claim();
vm.stopPrank();
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("seconds in one day : ", oneDaysInSecond);
console2.log("total supply Updated : ", totalSupply_updated);
console2.log("aliceLoveTokenBalance : ", aliceLoveTokenBalance);
console2.log("airdropVaultBalance Updated : ", airdropVaultBalance_updated);
console2.log("airdropAsManagerSpendAllowance updated: ", airdropAsManagerSpendAllowance_updated);
console2.log("---------------------------2 Days---------------------------");
vm.warp(block.timestamp + oneDaysInSecond);
vm.startPrank(bob);
airdropContract_tst.claim();
vm.stopPrank();
vm.startPrank(alice);
airdropContract_tst.claim();
vm.stopPrank();
uint256 aliceLoveTokenBalanceNew = loveToken_tst.balanceOf(alice);
uint256 bobLoveTokenBalance = loveToken_tst.balanceOf(bob);
uint256 airdropVaultBalance_updatedNew = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance_updatedNew =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
console2.log("aliceLoveTokenBalanceNew : ", aliceLoveTokenBalanceNew);
console2.log("bobLoveTokenBalance : ", bobLoveTokenBalance);
console2.log("airdropVaultBalance Updated : ", airdropVaultBalance_updatedNew);
console2.log("airdropAsManagerSpendAllowance updated: ", airdropAsManagerSpendAllowance_updatedNew);
console2.log("---------------------------After Million Years---------------------------");
console2.log("Approx 1.40 Billion Years!");
vm.warp((oneDaysInSecond * 499_999_997) + 1);
vm.startPrank(alice);
airdropContract_tst.claim();
vm.stopPrank();
// uint256 bobLoveTokenBalance = loveToken_tst.balanceOf(bob);
uint256 aliceLoveTokenBalanceAfterMillionYears = loveToken_tst.balanceOf(alice);
uint256 bobLoveTokenBalanceAfterMillionYears = loveToken_tst.balanceOf(bob);
uint256 airdropVaultBalance_updatedAfterMillionYears = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance_updatedAfterMillionYears =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
console2.log("Bob haven't Claim yet!");
console2.log("aliceLoveTokenBalanceAfterMillionYears : ", aliceLoveTokenBalanceAfterMillionYears);
console2.log("bobLoveTokenBalanceAfterMillionYears : ", bobLoveTokenBalanceAfterMillionYears);
console2.log(
"airdropVaultBalance Updated : ", airdropVaultBalance_updatedAfterMillionYears
);
console2.log(
"airdropAsManagerSpendAllowance updated : ",
airdropAsManagerSpendAllowance_updatedAfterMillionYears
);
console2.log("-------------Now Bob claims-----------");
vm.startPrank(bob);
airdropContract_tst.claim();
vm.stopPrank();
uint256 bobRemainingClaim = airdropContract_tst.s_remainingClaim(bob);
uint256 numDays = 499_999_997 - 2;
uint256 bobLoveTokenBalanceAfterMillionYearsClaimed = loveToken_tst.balanceOf(bob);
console2.log("Bob's expected claim : ", numDays * 10 ** 18);
console2.log(
"BobLoveTokenBalanceAfterMillionYears actual : ", bobLoveTokenBalanceAfterMillionYearsClaimed
);
console2.log("Bob's remaining claim : ", bobRemainingClaim);
airdropVault_tst.initVault(ILoveToken(address(loveToken_tst)), address(airdropContract_tst));
console2.log("-------------------After Minting & Approving more LoveTokens-------------------");
vm.startPrank(bob);
airdropContract_tst.claim();
vm.stopPrank();
uint256 bobLoveTokenBalanceAfterMillionYearsClaimedNew = loveToken_tst.balanceOf(bob);
uint256 bobRemainingClaimNew = airdropContract_tst.s_remainingClaim(bob);
uint256 airdropVaultBalance_updatedAfterMillionYearsNew = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance_updatedAfterMillionYearsNew =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
console2.log(
"BobLoveTokenBalanceAfterMillionYears actual : ", bobLoveTokenBalanceAfterMillionYearsClaimedNew
);
console2.log("Bob's remaining claim : ", bobRemainingClaimNew);
console2.log(
"airdropVaultBalance Updated : ", airdropVaultBalance_updatedAfterMillionYearsNew
);
console2.log(
"airdropAsManagerSpendAllowance updated : ",
airdropAsManagerSpendAllowance_updatedAfterMillionYearsNew
);
}
  1. Open Your Bash Terminal and execute the following command...

forge test --mt "testAirdropOutOfFundsMitigation" --via-ir -vv
  1. Some output might appear upon executing the above command. Take a look at that output and Please read the Ouptut carefully.

  2. Now it Proofs that mitigation works.

Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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