Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: low
Invalid

`Staking::claimRewards`, Anyone can clsim `0` Rewards(Love Tokens). Ambiguous Protocol Behavior.

Summary

People can claim 0 LoveTokens as a Staking Reward without staking any LoveToken. This behavior is endless and limitless within the Protocol. As weeks pass, a user can continue to claim rewards without staking, resulting in receiving 0 or nothing in return.

Anyone, regardless of whether they have minted the Soulmate NFT or staked any LoveToken, can execute Staking::claimRewards even for 0 rewards.

function claimRewards() public {
@> // Anyone can calim 0 LoveTokens without any constraints because here is no require to check that.
uint256 soulmateId = soulmateContract.ownerToId(msg.sender);
// first claim
if (lastClaim[msg.sender] == 0) {
lastClaim[msg.sender] = soulmateContract.idToCreationTimestamp(soulmateId);
}
// 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) {
revert Staking__StakingPeriodTooShort();
}
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);
}

Vulnerability Details

Zero Rewards:
  1. Place the following test code snippet into the test/unit/soulmateTest.t.sol file. Put it at the very bottom but before the last closing semicolon }.

function test_zeroStakingReward() public {
Vault airdropVault_tst = new Vault();
Vault stakingVault_tst = new Vault();
Soulmate soulmateContract_tst = new Soulmate();
LoveToken loveToken_tst = new LoveToken(
ISoulmate(address(soulmateContract_tst)), address(airdropVault_tst), address(stakingVault_tst)
);
Airdrop airdropContract_tst = new Airdrop(
ILoveToken(address(loveToken_tst)),
ISoulmate(address(soulmateContract_tst)),
IVault(address(airdropVault_tst))
);
airdropVault_tst.initVault(ILoveToken(address(loveToken_tst)), address(airdropContract_tst));
Staking stakingContract_tst = new Staking(
ILoveToken(address(loveToken_tst)),
ISoulmate(address(soulmateContract_tst)),
IVault(address(stakingVault_tst))
);
stakingVault_tst.initVault(ILoveToken(address(loveToken_tst)), address(stakingContract_tst));
address bob = makeAddr("BOB");
vm.warp(block.timestamp + 1 weeks);
vm.startPrank(bob);
stakingContract_tst.claimRewards();
vm.stopPrank();
}
  1. Open Your Bash Terminal and execute the following command...

forge test --mt "test_zeroStakingReward" -vv --via-ir
  1. Ouput should indicate that test Passed Successfully and not reverted anywhere. So, It's ambiguous that Anyone can claim zero rewards.

Impact

It doesn't harm the Protocol, but it's also something that the Protocol, aka Soulmate, doesn't expect to happen with its users. Therefore, if they haven't staked any LoveToken, they shouldn't be able to claim any staking reward. This behavior potentially allows users to waste their GAS. Although the Protocol still remains harmless.

Tools Used

Foundry Framework (Solidity, Rust)

Recommendations

There should be an if statement with a require check to verify whether a user has ever staked LoveToken(s). Although we can leave the Protocol as it is, it would be more proficient to implement this validation.

One recommended mitigation code....

Update The src/Staking.sol like below...

...
...
...
error Staking__NoMoreRewards();
error Staking__StakingPeriodTooShort();
+ error Staking__NotStakedAnyLoveToken();
...
...
...
function claimRewards() public {
+ if (userStakes[msg.sender] == 0) {
+ revert Staking__NotStakedAnyLoveToken();
+ }
uint256 soulmateId = soulmateContract.ownerToId(msg.sender);
// first claim
if (lastClaim[msg.sender] == 0) {
lastClaim[msg.sender] = soulmateContract.idToCreationTimestamp(soulmateId);
}
// 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) {
revert Staking__StakingPeriodTooShort();
}
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
Invalidated
Reason: Other

Support

FAQs

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