The FjordStaking contract allows users to stake their Fjord tokens in order to accrue rewards over time. In addition to these rewards, staking allows users to earn points distributed by the FjordPoints contract.
When a user stakes his tokens using FjordStaking::stake(), a call to FjordPoints::onStaked() is made and updates the staked amount in FjordPoints which will be used to account for the points owed to the user.
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordStaking.sol#L388
Moreover, the modifier checkDistribution on FjordPoints::onStaked() is triggered which can update the points distribution mecanism.
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordPoints.sol#L197-L207
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordPoints.sol#L232-L248
The points owed to users is calculated once every week by the distributePoints() function which updates pointsPerToken.
Once pointsPerToken is updated, users can use claimPoints() to mint their points.
The amount of points earned depends on the amount of tokens staked. These tokens can be unstaked immediately but only if the stake and unstake occur during the same epoch cycle. The epoch cycle in FjordStaking increments every 1 week, starting at 1 when the contract is deployed.
This means, in theory, users have at most 1 week to unstake their tokens.
Both FjordStaking and FjordPoints are intrinsically inter-dependant but they maintain their own 1 week cycles respectively for their accounting operations.
In case they fail to update their respective cycles and end up out-of-sync, points can be minted for free.
Assume the following scenario :
FjordPoints is deployed at block.timestamp == 100_000
FjordStaking is deployed at block.timestamp == 100_050 (50 seconds after FjordPoints)
the attacker FjordStaking::stake() his tokens at block.timestamp == 100_000 + 1 weeks which will internally trigger the points distribution in FjordPoints with the checkDistribution modifier and accrue his points with the updatePendingPoints modifier
at this point, the attacker has staked his tokens at FjordStaking::epochDuration == 1 and can FjordPoints::claimPoints()
since the FjordStaking is still at epoch 1, the attacker can unstake his tokens at no cost
The attacker has the ability to conduct the attack infinitely within the same block by transferring his tokens to another address he controls.
This means an attacker has the ability to mint an infinite amount of points.
The following PoC demonstrates the scenario described above and shows the attacker is able to mint points at no cost
First, modify the setUp() function in test\FjordStakingBase.t.sol to skip 50 seconds between the FjordPoints and FjordStaking deployments so the contracts are out-of-sync.
Then add the following in test\integration\points.t.sol
By minting an infinite amount of points, an attacker is able to bid them in every FjordAuction contract to claim the majority of the auction tokens for himself. This leads to an unfair distribution the auction tokens.
Manual review
Implement a mecanism responsible for both FjordPoints and FjordStaking contracts to be synchronized regarding their respective 1 week cycles.
An example would consist in adding a function in FjordPoints to allow the staking contract to initialize lastDistribution and another in FjordStaking that will set FjordStaking::startTime to the same value
Impact: High - Users are getting an unreasonable amount of points through exploiting a vulnerability Likelihood: Low - Most of the times, when using the script, all deployment tx will get processed in the same block. But, there is a small chance for them to be processed in different blocks.
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.