Description
Lines of code
https://github.com/code-423n4/2023-04-eigenlayer/blob/5e4872358cd2bda1936c29f460ece2308af4def6/src/contracts/core/StrategyManager.sol#L523
https://github.com/code-423n4/2023-04-eigenlayer/blob/5e4872358cd2bda1936c29f460ece2308af4def6/src/contracts/core/StrategyManager.sol#L567-L573
Vulnerability details
Description
According to the documentation, the EigenLayer protocol allows for partial slashes. If an operator is slashed, all stakers who have delegated to the operator will be subject to the same percentage slash. This means that if a delegate is slashed by
Impact
Despite intending to implement a partial slash, the current issue has resulted in the complete cancellation of queued withdrawals and the loss of funds for the user, therefore we evaluate the impact of this issue as HIGH.
Proof of Concept
The scenario below explains the issue:
Context:
- Alice is a staker who has delegated their shares
- Bob is the operator
- Alice and Bob have staked 100 shares each
Scenario:
- Alice initiates a withdrawal of 100 shares using the
queueWithdrawal
function, which sets her shares in thestakerStrategyShares
mapping to 0. - A day later, Bob is slashed by governance for 30 shares due to misbehavior, using the
StategyManager::slashShares
function. - To slash Alice's shares that do not correspond to the queued withdrawal, calling the
StategyManager::slashShares
function does not make sense, as thestakerStrategyShares
element in the mapping was previously set to 0. Doing so would cause theStategyManager::slashShares
transaction to revert. - Instead, the
StategyManager
owner calls theStategyManager::slashQueuedWithdrawals
function to slash Alice's queued withdrawal. Alice should only be slashed for 30% of her shares in the strategy. BUT, the amount to withdraw from the strategy corresponds to all the shares in thequeuedWithdrawal.share[i]
element, which results in Alice being fully slashed. - As a result, Alice loses all her shares, instead of just 30.
Recommended Mitigation steps
The issue stems from the failure to account for the percentage of shares of the operator that are cut during a slash.
We propose two options to mitigate this issue:
Option A
Disallow partial slashes and only allow full slashes.
Option B
When an operator is slashed, take into account the percentage that is slashed and slash the queued withdrawals accordingly.
This can be done by including the amount of shares to slash in the function StrategyManager::slashQueuedWithdrawal
.
function slashQueuedWithdrawal(
address recipient,
QueuedWithdrawal calldata queuedWithdrawal,
IERC20[] calldata tokens,
uint256[] calldata indicesToSkip
+ uint256[] calldata sharesToSlash,
)
external
onlyOwner
onlyFrozen(queuedWithdrawal.delegatedAddress)
nonReentrant
{
require(tokens.length == queuedWithdrawal.strategies.length, "StrategyManager.slashQueuedWithdrawal: input length mismatch");
+ require(sharesToSlash.length == queuedWithdrawal.strategies.length, "StrategyManager.slashQueuedWithdrawal: input length mismatch");
// find the withdrawalRoot
bytes32 withdrawalRoot = calculateWithdrawalRoot(queuedWithdrawal);
// verify that the queued withdrawal is pending
require(
withdrawalRootPending[withdrawalRoot],
"StrategyManager.slashQueuedWithdrawal: withdrawal is not pending"
);
// reset the storage slot in mapping of queued withdrawals
withdrawalRootPending[withdrawalRoot] = false;
// keeps track of the index in the `indicesToSkip` array
uint256 indicesToSkipIndex = 0;
uint256 strategiesLength = queuedWithdrawal.strategies.length;
for (uint256 i = 0; i < strategiesLength;) {
// check if the index i matches one of the indices specified in the `indicesToSkip` array
if (indicesToSkipIndex < indicesToSkip.length && indicesToSkip[indicesToSkipIndex] == i) {
unchecked {
++indicesToSkipIndex;
}
} else {
if (queuedWithdrawal.strategies[i] == beaconChainETHStrategy){
//withdraw the beaconChainETH to the recipient
- _withdrawBeaconChainETH(queuedWithdrawal.depositor, recipient, queuedWithdrawal.shares[i]);
+ _withdrawBeaconChainETH(queuedWithdrawal.depositor, recipient,sharesToSlash[i]);
} else {
// tell the strategy to send the appropriate amount of funds to the recipient
- queuedWithdrawal.strategies[i].withdraw(recipient, tokens[i], queuedWithdrawal.shares[i]);
+ queuedWithdrawal.strategies[i].withdraw(recipient, tokens[i], sharesToSlash[i]);
}
unchecked {
++i;
}
}
}
Assessed type
Other