Liquid Staking

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

First Deposit will receive less shares due to wrong calculation

Summary

when first user deposit into stakingPool, the user will receive 10**3 less share due to wrong calculation in _mintShares function.

Vulnerability Details

To deposit in stakingPool the user will call deposit function of PriorityPool . The PriorityPool will call the deposit function of stakingPool. The stakingPool than mint the share and deposit the assets in startegy vault to stake the assets on chainlink staking contract.

However the issue here lies in _mintShares function where the user will receive the share which represent his stake in the vault.

function _mintShares(address _recipient, uint256 _amount) internal {
require(_recipient != address(0), "Mint to the zero address");
if (totalShares == 0) {
shares[address(0)] = DEAD_SHARES;
totalShares = DEAD_SHARES;
_amount -= DEAD_SHARES; // @audit : share sub
}
totalShares += _amount;
shares[_recipient] += _amount;
}

POC

Add the following code inside the new test file : mintingBug.test.ts

import { assert, expect } from 'chai'
import {
toEther,
deploy,
fromEther,
deployUpgradeable,
getAccounts,
setupToken,
} from '../../utils/helpers'
import {
ERC677,
SDLPoolMock,
StakingPool,
PriorityPool,
StrategyMock,
WithdrawalPool,
} from '../../../typechain-types'
import { ethers } from 'hardhat'
import { StandardMerkleTree } from '@openzeppelin/merkle-tree'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
describe('PriorityPool', () => {
async function deployFixture() {
const { accounts, signers } = await getAccounts()
const adrs: any = {}
const token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [
'Chainlink',
'LINK',
1000000000,
])) as ERC677
adrs.token = await token.getAddress()
await setupToken(token, accounts, true)
const stakingPool = (await deployUpgradeable('StakingPool', [
adrs.token,
'Staked LINK',
'stLINK',
[],
toEther(10000),
])) as StakingPool
adrs.stakingPool = await stakingPool.getAddress()
const strategy = (await deployUpgradeable('StrategyMock', [
adrs.token,
adrs.stakingPool,
toEther(1000),
toEther(100),
])) as StrategyMock
adrs.strategy = await strategy.getAddress()
const sdlPool = (await deploy('SDLPoolMock')) as SDLPoolMock
adrs.sdlPool = await sdlPool.getAddress()
const pp = (await deployUpgradeable('PriorityPool', [
adrs.token,
adrs.stakingPool,
adrs.sdlPool,
toEther(100),
toEther(1000),
])) as PriorityPool
adrs.pp = await pp.getAddress()
const withdrawalPool = (await deployUpgradeable('WithdrawalPool', [
adrs.token,
adrs.stakingPool,
adrs.pp,
toEther(10),
0,
])) as WithdrawalPool
adrs.withdrawalPool = await withdrawalPool.getAddress()
await stakingPool.addStrategy(adrs.strategy)
await stakingPool.setPriorityPool(adrs.pp)
await stakingPool.setRebaseController(accounts[0])
await pp.setDistributionOracle(accounts[0])
await pp.setWithdrawalPool(adrs.withdrawalPool)
for (let i = 0; i < signers.length; i++) {
await token.connect(signers[i]).approve(adrs.pp, ethers.MaxUint256)
}
return { signers, accounts, adrs, token, stakingPool, strategy, sdlPool, pp, withdrawalPool }
}
it('deposit works correctly but shares are not minted correctly', async () => {
const { signers, accounts, adrs, pp, token, strategy, stakingPool } = await loadFixture(
deployFixture
)
// if user deposit less then 100 it will revert ,
await expect(pp.deposit(100, false, ['0x'])).to.be.reverted;
await pp.deposit(1000, false, ['0x'])
assert.equal(fromEther(await stakingPool.balanceOf(accounts[0])), 0) // user will recevice 0 share
})
})

Run with :
npx hardhat test test/core/priorityPool/mintingBug.test.ts
Logs :

shares 0n
✔ deposit work correctly but shares are not minted correctly (2475ms)

Impact

The impact is only limited to first users in case of assets lost.

Tools Used

ManualReview , hardhat

Recommendations

The One potential fix will be to make totalShare and totalStake equal to DEAD_SHARES and remove the subtraction of _amount :

diff --git a/contracts/core/StakingPool.sol b/contracts/core/StakingPool.sol
index bb00ace..b0ee799 100644
--- a/contracts/core/StakingPool.sol
+++ b/contracts/core/StakingPool.sol
@@ -5,7 +5,6 @@ import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeab
import "./base/StakingRewardsPool.sol";
import "./interfaces/IStrategy.sol";
-
/**
* @title Staking Pool
* @notice Allows users to stake asset tokens and receive liquid staking tokens 1:1, then deposits staked
@@ -121,6 +120,9 @@ contract StakingPool is StakingRewardsPool {
token.safeTransferFrom(msg.sender, address(this), _amount);
_depositLiquidity(_data);
_mint(_account, _amount);
+ if(totalStaked ==0) {
+ totalStaked = DEAD_SHARES;
+ }
totalStaked += _amount;
} else {
_depositLiquidity(_data);
--------------------------------------------------
diff --git a/contracts/core/base/StakingRewardsPool.sol b/contracts/core/base/StakingRewardsPool.sol
index a58d308..28acb7b 100644
--- a/contracts/core/base/StakingRewardsPool.sol
+++ b/contracts/core/base/StakingRewardsPool.sol
@@ -13,7 +13,7 @@ import "../tokens/base/ERC677Upgradeable.sol";
*/
abstract contract StakingRewardsPool is ERC677Upgradeable, UUPSUpgradeable, OwnableUpgradeable {
// used to prevent vault inflation attack
- uint256 private constant DEAD_SHARES = 10 ** 3;
+ uint256 internal constant DEAD_SHARES = 10 ** 3;
// address of staking asset token
IERC20Upgradeable public token;
@@ -191,7 +191,6 @@ abstract contract StakingRewardsPool is ERC677Upgradeable, UUPSUpgradeable, Owna
if (totalShares == 0) {
shares[address(0)] = DEAD_SHARES;
totalShares = DEAD_SHARES;
- _amount -= DEAD_SHARES;
}
totalShares += _amount;
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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