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;
}
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
)
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)
})
})
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 :
@@ -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);
@@ -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;