Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: medium
Invalid

Unused deposits logic: if there is a significant delay between deposits and strategies being deployed, this limit might revert transactions unnecessarily in `StakingPool.sol`

Summary

In a staking pool smart contract, there may exist a flaw in how unused deposits are handled, leading to unclaimed or incorrectly processed deposits. Specifically, when users deposit tokens into the pool, there may be situations where these deposits are either not allocated to strategies correctly or remain unprocessed due to gaps in the logic. This vulnerability may lead to the staking pool being inefficient, with deposits lying idle, or even a loss of funds due to improper handling of those deposits.

Vulnerability Details

The unusedDepositLimit mechanism seems intended to prevent large amounts of tokens from sitting idle in the pool. However, if there is a significant delay between deposits and strategies being deployed, this limit might revert transactions unnecessarily, causing operational issues.

This vulnerability arises when deposits are sent to the staking pool but are not correctly assigned to active strategies or are left in limbo within the contract without being staked. The unused deposits problem is particularly concerning in scenarios where:

  • The deposit function does not immediately allocate tokens to a strategy.

  • The logic assumes that all deposits will be distributed among strategies but fails to do so under certain conditions.

  • There is no mechanism to handle or track these unallocated deposits.

This can result in a significant portion of the user's funds being left idle within the contract, preventing them from earning staking rewards. In some cases, if withdrawals are based on strategy balances alone, these unused deposits may not be retrievable by users.

The following PoC demonstrates how deposits may go unprocessed and lie idle within the staking pool contract.

  1. Realistic attack scenario:

    • A user deposits tokens into the staking pool.

    • Due to a flaw in the logic (e.g., a strategy is full, removed, or temporarily unavailable), the tokens remain in the staking pool contract but are not transferred to any active strategy.

    • The user expects to receive rewards for the deposited tokens but does not, as the tokens were never actually staked.

    • If the user attempts to withdraw the deposited tokens, the contract may not reflect the correct balance, or in worst cases, these tokens may become unrecoverable.

  2. PoC on Hardhat:

// Solidity code snippets in Hardhat test setup
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Unused Deposits Logic PoC", function () {
let stakingPool, strategy1, strategy2, token, owner, user1;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MockERC20");
token = await Token.deploy();
await token.deployed();
const Strategy = await ethers.getContractFactory("MockStrategy");
strategy1 = await Strategy.deploy(token.address);
strategy2 = await Strategy.deploy(token.address);
const StakingPool = await ethers.getContractFactory("StakingPool");
stakingPool = await StakingPool.deploy();
await stakingPool.initialize(token.address, "StakeToken", "STK", [], 1000);
await stakingPool.addStrategy(strategy1.address);
await stakingPool.addStrategy(strategy2.address);
// Mint tokens and approve the staking pool to use them
await token.mint(user1.address, 1000);
await token.connect(user1).approve(stakingPool.address, 1000);
});
it("Deposit remains unused and idle within the contract", async function () {
// User deposits tokens
await stakingPool.connect(user1).deposit(user1.address, 500, []);
// Now, simulate a scenario where the deposit is not properly assigned to a strategy
// For example, the logic could skip assignment, or all strategies could be full
// We force this by calling no strategy actions in this simplified test
// Check that the balance in the staking pool is not assigned to any strategy
const stakingPoolBalance = await token.balanceOf(stakingPool.address);
expect(stakingPoolBalance).to.equal(500); // Tokens are stuck in the contract
// Verify strategies do not have the tokens (idle funds)
const strategy1Balance = await strategy1.totalStaked();
const strategy2Balance = await strategy2.totalStaked();
expect(strategy1Balance).to.equal(0);
expect(strategy2Balance).to.equal(0);
// Further test: trying to withdraw when tokens are unassigned
await stakingPool.connect(user1).withdraw(user1.address, 500, []);
// Ensure the tokens can be withdrawn
const userBalanceAfterWithdraw = await token.balanceOf(user1.address);
expect(userBalanceAfterWithdraw).to.equal(1000); // Initial 1000 minted tokens are back to the user
});
});

Steps:

  1. Deploy contract: Set up the Staking Pool and two strategies (strategy1 and strategy2).

  2. Deposit: Simulate a user depositing tokens into the pool.

  3. Failure to Assign to Strategy: The pool fails to assign the deposit to any strategy.

  4. Verify Idle Deposit: Check that the deposited tokens remain within the contract but are not staked, leading to unproductive funds.

Impact

  1. Tokens that remain unused within the contract do not earn any staking rewards, affecting the user’s expected returns.

  2. If tokens are not correctly tracked, the contract’s internal balances may become inconsistent, leading to potential errors during withdrawals.

  3. Users may become frustrated with the platform due to the lack of rewards on their deposits, leading to a loss of confidence.

  4. In edge cases, if the contract logic does not account for idle deposits correctly, users may lose access to their funds, leading to irreversible losses.

Tools Used

Manual review.

Recommendations

  1. Ensure that deposits are immediately allocated to active strategies. If all strategies are full or unavailable, the transaction should revert, or the contract should clearly handle these edge cases.

  2. Implement a mechanism to track and process idle deposits. Any unassigned funds should either be returned to the user or processed within a reasonable timeframe.

  3. Before allowing a deposit, ensure that there is sufficient capacity in at least one strategy to accommodate the user's tokens.

  4. If strategies are unavailable, the contract should trigger a fallback mechanism, such as holding the tokens in a temporary reserve and notifying the user.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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