The FjordStaking
contract allows users to bypass the early claim penalty by creating a claim receipt and waiting for the claim cycle to complete, undermining the intended penalty mechanism.
The FjordStaking
contract implements a staking system where users can stake FJORD tokens and earn rewards over time. To discourage early withdrawal of rewards, the contract includes an early claim penalty mechanism. However, the current implementation allows users to circumvent this penalty.
The contract provides two main functions for claiming rewards:
claimReward()
: This function allows users to claim rewards immediately with a penalty if _isClaimEarly
is set to true, or create a claim receipt if _isClaimEarly
is false.
completeClaimRequest()
: This function allows users to claim their rewards after the claim cycle has completed, without incurring any penalty.
The vulnerability lies in the interaction between these two functions. Users can exploit this by:
Staking their tokens and accumulating rewards.
Unstaking their tokens early.
Instead of claiming rewards immediately (which would incur a penalty), they create a claim receipt using claimReward(false)
.
Waiting for the claim cycle to complete.
Using completeClaimRequest()
to claim their full rewards without any penalty.
This process effectively bypasses the early claim penalty, as the contract does not track whether the user has unstaked early when processing the completed claim request.
Here's the relevant code from the claimReward()
function:
And from the completeClaimRequest()
function:
The completeClaimRequest()
function does not check whether the user has unstaked early, allowing them to claim their full rewards without any penalty.
Users can bypass the early claim penalty by creating a claim receipt and waiting for the claim cycle to complete. This undermines the intended penalty mechanism and allows users to receive their full rewards without any deductions, potentially leading to an uneven distribution of rewards among users.
Here's a step-by-step scenario demonstrating the exploit:
Alice stakes 1000 FJORD tokens using the stake()
function.
Over time, Alice accumulates 100 FJORD tokens as rewards.
Alice decides to unstake her tokens early using the unstake()
function.
Instead of claiming rewards immediately (which would incur a 50% penalty), Alice calls claimReward(false)
to create a claim receipt.
Alice waits for the claim cycle (e.g., 3 epochs) to complete.
After the claim cycle, Alice calls completeClaimRequest()
.
Alice receives the full 100 FJORD tokens as rewards, bypassing the early claim penalty.
Manual review
To address this issue, It's recommend to implement a mechanism to track whether a user has unstaked early and apply the penalty accordingly, even when completing a claim request. Here's a suggested fix:
This modification ensures that the early unstaking penalty is applied even when users complete their claim requests after the claim cycle, maintaining the integrity of the penalty mechanism. The changes include:
Adding an unstakedEarly
flag to the UserData
struct to track early unstaking.
Setting this flag in the unstake()
function when a user unstakes early.
Including the unstakedEarly
status in the ClaimReceipt
struct.
Storing the unstaking status when creating a claim receipt in claimReward()
.
Checking the unstaking status and applying the penalty if necessary in completeClaimRequest()
.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.