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;
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);
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