Liquid Staking

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

Account Index and Merkle Tree Size Mismatch Prevents Valid Token Unqueuing

Summary

Inconsistent handling of account indexing and Merkle tree size updates can cause valid token unqueue attempts to fail. This occurs when users deposit tokens between distribution updates, as their account indexes are recorded but they aren't yet included in the Merkle tree verification process.

Vulnerability Details

unqueueTokens performs the following check to determine whether the caller's Merkle proof should be verified:

if (accountIndexes[account] < merkleTreeSize) {
// Verify Merkle proof
bytes32 node = keccak256(
bytes.concat(keccak256(abi.encode(account, _amount, _sharesAmount)))
);
if (!MerkleProofUpgradeable.verify(_merkleProof, merkleRoot, node))
revert InvalidProof();
}

This condition assumes that merkleTreeSize is in sync with the number of accounts and their respective accountIndexes. However, merkleTreeSize is only updated when a new distribution is made, not during each individual deposit. As a result, after a deposit, the accountIndexes mapping may include more accounts than the merkleTreeSize, but these new accounts will not be included in the Merkle tree until the next distribution.

When new tokens are deposited and queued, the account's index is updated in the _deposit Function:

if (accountIndexes[_account] == 0) {
accounts.push(_account);
accountIndexes[_account] = accounts.length - 1;
}

This adds the new account to the accountIndexes mapping, but merkleTreeSize is not updated at this point. This means new accounts may be queued, but they won’t be reflected in the Merkle tree until the next distribution is made via updateDistribution.

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

However, there's a gap between when an account is added in the _deposit function and when merkleTreeSize is updated in updateDistribution.

If a new account queues tokens and later tries to unqueue them before the next distribution update, the comparison if (accountIndexes[account] < merkleTreeSize) in the unqueueTokens function will trigger a false revert. This is because the accountIndexes will be less than the (still outdated) merkleTreeSize, but the new account is not part of the Merkle tree yet, and no proof is available.

Impact

Users who deposit tokens after the last distribution will fail to unqueue tokens, as their accountIndexes will be updated, but they won't be included in the Merkle proof verification process until the next distribution.

Tools Used

Manual Review

Recommendations

Consider adding a more explicit distinction between accounts that need proof and those that don't, or require new accounts to wait for the next distribution cycle to unqueue tokens.

Updates

Lead Judging Commences

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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