Summary
FjordStaking::claimReward
allows user to claim reward in 2 different ways depending on _isClaimEarly
param. If set to true
user will be able to claim rewards immediately but will incur penalty (50%). Otherwise, user will be able to claim full amount after cooldown period of 3 epochs. Furthermore, function is supposed to return rewardAmount
and penaltyAmount
. However, when _isClaimEarly
param is set to false, it does not return the accurate value for rewardAmount
.
Vulnerability Details
FjordStaking.sol#L641
In the case when user sets the _isClaimEarly
param to false when calling claimReward
function , the following function will return (0, 0), even tough the ud.unclaimedRewards
is ≠ 0.
function claimReward(bool _isClaimEarly)
external
checkEpochRollover
redeemPendingRewards
returns (uint256 rewardAmount, uint256 penaltyAmount)
{
(...)
if (!_isClaimEarly) {
claimReceipts[msg.sender] =
ClaimReceipt({ requestEpoch: currentEpoch, amount: ud.unclaimedRewards });
emit ClaimReceiptCreated(msg.sender, currentEpoch);
@> return (0, 0);
}
(...)
}
PoC
Add following test in the test/unit/unstake.t.sol:
function testClaimRewardReturnsWrongValue() public {
address user=makeAddr("user");
deal(address(token), user, 2 ether);
vm.startPrank(user);
token.approve(address(fjordStaking), 1 ether);
fjordStaking.stake(1 ether);
vm.stopPrank();
vm.startPrank(minter);
for (uint256 i = 0; i < 7; i++) {
vm.warp(vm.getBlockTimestamp() + fjordStaking.epochDuration());
fjordStaking.addReward(1 ether);
}
vm.stopPrank();
vm.startPrank(user);
(uint256 rewardAmount, uint256 penaltyAmount)=fjordStaking.claimReward(false);
vm.stopPrank();
(uint256 totalStaked, uint256 unclaimedRewards, uint16 unredeemedEpoch, uint16 lastClaimedEpoch) = fjordStaking.userData(user);
console.log("-----------------------------------------");
console.log("totalStaked", totalStaked);
console.log("unclaimedRewards", unclaimedRewards);
console.log("unredeemedEpoch", unredeemedEpoch);
console.log("lastClaimedEpoch", lastClaimedEpoch);
console.log("-----------------------------------------");
assert(0 == rewardAmount);
assert(0 == penaltyAmount);
}
Impact
The incorrect return values from the claimReward
function can mislead users about the amount of rewards they are entitled to. Additionally, this issue poses a greater risk if the function is used in other contracts or integrated systems that depend on its return values. For instance, if an external protocol or automated system relies on the correct rewardAmount
value to execute further logic or distribute assets, the incorrect data could impact their operation and as result their users.
Tools Used
Manual code review / Foundry tests
Recommendations
Consider making following changes:
function claimReward(bool _isClaimEarly)
external
checkEpochRollover
redeemPendingRewards
returns (uint256 rewardAmount, uint256 penaltyAmount)
{
(...)
if (ud.unclaimedRewards == 0) revert NothingToClaim();
if (!_isClaimEarly) {
claimReceipts[msg.sender] =
ClaimReceipt({ requestEpoch: currentEpoch, amount: ud.unclaimedRewards });
emit ClaimReceiptCreated(msg.sender, currentEpoch);
- return (0, 0);
+ return (ud.unclaimedRewards, 0);
}
(...)
}