Liquid Staking

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

Get account data function will revert due to high gas usage and generating a merkle tree in here will be hard

Summary

Get account data function will revert due to high gas usage and generating a merkle tree in here will be hard

Vulnerability Details

As we know in stake.link protocol, after reaching the max points of the staking pool. The deposited amount is sent through the queue in priority pool.

function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 toDeposit = _amount;
if (totalQueued == 0) {
uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
if (queuedWithdrawals != 0) {
uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
? toDeposit
: queuedWithdrawals;
withdrawalPool.deposit(toDepositIntoQueue);
toDeposit -= toDepositIntoQueue;
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue);
}
if (toDeposit != 0) {
uint256 canDeposit = stakingPool.canDeposit();
if (canDeposit != 0) {
uint256 toDepositIntoPool = toDeposit <= canDeposit ? toDeposit : canDeposit;
stakingPool.deposit(_account, toDepositIntoPool, _data);
toDeposit -= toDepositIntoPool;
}
}
}
if (toDeposit != 0) {
if (_shouldQueue) {
_requireNotPaused();
if (accountIndexes[_account] == 0) {
accounts.push(_account);
accountIndexes[_account] = accounts.length - 1;
}
accountQueuedTokens[_account] += toDeposit;
totalQueued += toDeposit;
} else {
token.safeTransfer(_account, toDeposit);
}
} function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 toDeposit = _amount;
if (totalQueued == 0) {
uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
if (queuedWithdrawals != 0) {
uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
? toDeposit
: queuedWithdrawals;
withdrawalPool.deposit(toDepositIntoQueue);
toDeposit -= toDepositIntoQueue;
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue);
}
if (toDeposit != 0) {
uint256 canDeposit = stakingPool.canDeposit();
if (canDeposit != 0) {
uint256 toDepositIntoPool = toDeposit <= canDeposit ? toDeposit : canDeposit;
stakingPool.deposit(_account, toDepositIntoPool, _data);
toDeposit -= toDepositIntoPool;
}
}
}
if (toDeposit != 0) {
if (_shouldQueue) {
_requireNotPaused();
if (accountIndexes[_account] == 0) {
accounts.push(_account);
accountIndexes[_account] = accounts.length - 1;
}
accountQueuedTokens[_account] += toDeposit;
&> totalQueued += toDeposit;
} else {
token.safeTransfer(_account, toDeposit);
}
}

Later, those accounts who have queued tokens in priority pool can withdraw their tokens thanks to merkle tree. It's updated in update distribution function and uploaded merkle tree root is used for validation of accounts withdrawals.

function updateDistribution(
bytes32 _merkleRoot,
bytes32 _ipfsHash,
uint256 _amountDistributed,
uint256 _sharesAmountDistributed
) external onlyDistributionOracle {
_unpause();
depositsSinceLastUpdate -= _amountDistributed;
sharesSinceLastUpdate -= _sharesAmountDistributed;
&> merkleRoot = _merkleRoot;
ipfsHash = _ipfsHash;
merkleTreeSize = accounts.length;
emit UpdateDistribution(
_merkleRoot,
_ipfsHash,
_amountDistributed,
_sharesAmountDistributed
);
}
function withdraw(
uint256 _amountToWithdraw,
uint256 _amount,
uint256 _sharesAmount,
bytes32[] calldata _merkleProof,
bool _shouldUnqueue,
bool _shouldQueueWithdrawal
) external {
if (_amountToWithdraw == 0) revert InvalidAmount();
uint256 toWithdraw = _amountToWithdraw;
address account = msg.sender;
// attempt to unqueue tokens before withdrawing if flag is set
if (_shouldUnqueue == true) {
_requireNotPaused();
if (_merkleProof.length != 0) {
bytes32 node = keccak256(
bytes.concat(keccak256(abi.encode(account, _amount, _sharesAmount)))
);
&> if (!MerkleProofUpgradeable.verify(_merkleProof, merkleRoot, node))
revert InvalidProof();
} else if (accountIndexes[account] < merkleTreeSize) {
revert InvalidProof();
}

But in order to create that merkle tree, protocol needs to fetch the account data with following function:

* @notice Returns account data used for calculating a new merkle tree
* @dev merkle tree is calculated based on users' reSDL balance and the number of tokens they have queued
* @dev accounts are returned in the same order as they are in the merkle tree
* @return accounts list of all accounts that have ever queued tokens
* @return sdlBalances list of SDL balances for each account
* @return queuedBalances list of queued token amounts for each account (ignores previously distributed
* liquid staking tokens)
*/
function getAccountData()
external
view
returns (address[] memory, uint256[] memory, uint256[] memory)
{
uint256[] memory reSDLBalances = new uint256[]();
uint256[] memory queuedBalances = new uint256[]();
for (uint256 i = 0; i < reSDLBalances.length; ++i) {
address account = accounts[i];
reSDLBalances[i] = sdlPool.effectiveBalanceOf(account);
queuedBalances[i] = accountQueuedTokens[account];
}
return (accounts, reSDLBalances, queuedBalances); // @audit it will revert 3695

It states that this function will be used for creation of merkle tree in the comment lines but this function will revert due to out of gas problem if it's have high enough users in the accounts variable.

It can also cause attack vectors. Attacker can send 1 wei after he/she sees the deposit limit is reached and queueing is started in the protocol. This situation can make this function revert.

Impact

Actually, protocol can still generate a merkle tree by reading the accounts data one by one. But the comment line states that protocol will use that function in order to build the tree. In conclusion it will revert at 3695 accounts points because it will exceed 30M gas limit of Ethereum mainnet.

Tools Used

Manual Review

Recommendations

Adding a parameter to that function and only fetching some part of the accounts will be beneficial

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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