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

Users can manipulate vested token accounting through `FjordStaking::stakeVested()` and `FjordStaking::_unstakeVested()`

Summary

The FjordStaking contract fails to accurately track the dynamic nature of Sablier stream vesting, potentially allowing users to manipulate token accounting and gain unfair advantages in staking and rewards.

Vulnerability Details

The FjordStaking contract interacts with Sablier streams to allow users to stake vested tokens. However, the contract's implementation does not account for the continuous vesting nature of Sablier streams, leading to potential issues.

The stakeVested() function captures the vested amount at a single point in time:

function stakeVested(uint256 _streamID) external checkEpochRollover redeemPendingRewards {
// ... (checks omitted for brevity)
uint128 depositedAmount = sablier.getDepositedAmount(_streamID);
uint128 withdrawnAmount = sablier.getWithdrawnAmount(_streamID);
uint128 refundedAmount = sablier.getRefundedAmount(_streamID);
uint256 _amount = depositedAmount - (withdrawnAmount + refundedAmount);
// ... (storage updates omitted)
}

This static capture fails to account for tokens that vest after the initial stake. The _unstakeVested() function similarly relies on this static amount:

function _unstakeVested(address streamOwner, uint256 _streamID, uint256 amount) internal {
NFTData storage data = _streamIDs[streamOwner][_streamID];
if (amount > data.amount) revert InvalidAmount();
// ... (unstaking logic omitted)
}

The root cause of the issue is the failure to update the vested amount over time. This static approach leads to several critical issues:

  1. Inaccurate vested amounts: The contract does not reflect the true vested balance as it changes over time.

  2. Potential for double-spending: Users could stake more tokens than they actually have vested by manipulating the timing of their actions.

  3. Loss of rewards: Tokens vesting after the initial stake are not accounted for in reward calculations.

  4. Inconsistency with Sablier: The contract's view of vested tokens becomes out of sync with the actual state in the Sablier contract.

Impact

Users can manipulate the staking process to gain unfair advantages, leading to an imbalance in reward distribution and potential economic losses for the protocol and its users.

Proof of Concept

  1. Alice has a Sablier stream of 1000 FJORD tokens vesting over 10 days.

  2. On Day 1, 100 tokens have vested. Alice calls stakeVested() with her stream ID.

  3. The contract records that Alice has staked 100 tokens.

  4. By Day 5, an additional 400 tokens have vested (total 500 vested).

  5. Alice calls _unstakeVested() to unstake 100 tokens.

  6. Alice immediately calls stakeVested() again.

  7. The contract now records that Alice has staked 500 tokens, even though she only unstaked 100.

  8. Alice has effectively "created" 400 tokens in the contract's accounting.

Tools Used

Manual review

Recommendation

To address this issue, the contract should implement a dynamic tracking system for vested tokens. Here's a high-level approach:

  1. Instead of storing a static amount, store the stream ID and the last update time.

  2. Implement a function to calculate the current vested amount based on the Sablier stream's current state.

  3. Update the vested amount calculation in all relevant functions (staking, unstaking, reward calculations).

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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