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 {
@>
uint256 soulmateId = soulmateContract.ownerToId(msg.sender);
if (lastClaim[msg.sender] == 0) {
lastClaim[msg.sender] = soulmateContract.idToCreationTimestamp(soulmateId);
}
uint256 timeInWeeksSinceLastClaim = ((block.timestamp - lastClaim[msg.sender]) / 1 weeks);
if (timeInWeeksSinceLastClaim < 1) {
revert Staking__StakingPeriodTooShort();
}
lastClaim[msg.sender] = block.timestamp;
uint256 amountToClaim = userStakes[msg.sender] * timeInWeeksSinceLastClaim;
loveToken.transferFrom(address(stakingVault), msg.sender, amountToClaim);
emit RewardsClaimed(msg.sender, amountToClaim);
}
Vulnerability Details
Zero Rewards:
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();
}
Open Your Bash Terminal
and execute the following command...
forge test --mt "test_zeroStakingReward" -vv --via-ir
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);
}
...
...
...