Overview
Background
EigenLayer’s idea is based on POS(proof of stake). Before the POS, there is POW(proof of work) used by Bitcoin.
POW and POS are both consensus methods used to improve the safety of some system. Here the “safety” means that the system works by the defined rule. For example, in the Bitcoin system, there is a shared ledger between nodes which uses POW to reach consensus about the ledger. As long as most nodes are honest(the computation power of honest nodes is greater than 50%), then the system will be safe in theory. The reason why the POW works is that it decentralizes the vote authority of the ledger according to the computation power, so that there won’t be a single party controls the vote, which improves safety.
But POW is energy-intensive due to the hard mathematical problem nodes need to solve. So there is POS, rather than distribute vote power by computation power, POS distibutes it by stake.
Node which stakes recognized valuable asset can participate in the vote process, and the state has received enough votes will be considered valid. If some nodes break the rules, then the staked asset will be slashed according to defined rules which disencourage the misbehavior. As long as most nodes are honest(different POS system may have different threshold), the system is safe in theory.
We can see that both POW and POS let multi parties to stake some valuable asset to participate a vote process, and let the state voted by most of the nodes be valid, which prevent the system being attacked by single party. In the POW, the valuable asset is computation power, in the POS, its some token, like ether in Ethereum.
POS is more efficient than POW system, but it needs enough stake to ensure the system’s safety. The more stake, the more safety. If the stake is not valuable enough to cover the potential benefit from attacking the system, then the attacker will have incentive to do the attack.
So this is where problems occur, current POS based blockchains want more stake, but the valuable asset is in fact limited, to attract more stake, blockchains need to improve the yield of the stakers which will lead to the difficulty to maintain the blockchain and the cost to build new decentralized infrastructure.
So there is EigenLayer wants to solve the above problems, the general purpose of EigenLayer is to make building infrastructure easy.
What EigenLayer provides?
- Shared Security Model: EigenLayer creates a shared security model that allows different blockchains and applications to benefit from pooled security resources. This approach can help smaller chains or applications achieve higher security without needing to establish their own, separate validators or security mechanisms.
- Enhanced Decentralization: By allowing multiple blockchains and applications to share security resources, EigenLayer aims to enhance the decentralization of these networks. This can reduce the risk of attacks and increase the robustness of the ecosystem as a whole.
- Economic Efficiency: By pooling security and allowing multiple participants to leverage shared infrastructure, EigenLayer allows for cost-sharing among different entities, reducing the overall expenses related to maintaining high security and operational efficiency.
The above illustration might be abstract, let me use a detailed example to illustrate how EigenLayer works. In the current Ethereum design, account can deposit 32 ETH(stake) to become a validator to participate the ledger consensus in the consensus layer, which helps ensure the safety of Ethereum. But the staked ETHs are locked, and can’t be used again. Luckily, there is some LST(liquid staking token) project, like Lido, which allows users to deposit their ETH to the Lido’s contract and issues corresponding shares(stETH) to users. Those shares can be seen as the claim authority of underlying ETH in Lido. Shares are just like normal tokens can be tranferred, swapped, etc. Because LST has value, it can theoretically be used as stake to secure POS based system, like another blockchain or oracle which need decentralization to ensure system safety. For example, if users stake their stETH to secure another blockchain like , they help consolidate the POS system of both Ethereum and and also get reward from both. In this case, doesn’t needs to pay extreme high yield to attract users to unstake their stETH and rather stake them on , or launch a new unstable token which may incur new risks. And users’ yield increases due to the double stake of single ETH.
This is essentially what EigenLayer does!
What EigenLayer does is basically building a layer where users can deposit arbitrary valuable assets(whitelisted by EigenLayer) to secure multiple POS based systems.
In the above picture, there are two roles: operators and AVS(Actively Validated Services).
Many users may lack necessary resource or knowledge to run hardware or software to participate the POS validation system. EigenLayer builds a delegation layer which allows users to delegate their stake to certain operator which will behave on behalf of them to fulfill the duty to run clients to secure POS system and get reward. If operator misbehaves, users’ stake will be slashed.
Actively Validated Services is Any system that requires its own distributed validation semantics for verification, such as sidechains, data availability layers, new virtual machines, keeper networks, oracle networks, bridges, threshold cryptography schemes, and trusted execution environments. —— EigenLayer doc.
We can simply see AVS as the client run by operators to secure certain POS system, like the execution and consensus clients of Ethereum, sequencer of layer 2, data provider of oracle, etc.
The overall process is:
- Users deposit asset to EigenLaer
- Node registers as opeartor and run one or multiple AVS.
- User delegates assets to certain operator to behave on behalf them.
- Operator runs AVS to secure other decentralized system and earn rewards.
- User issues a transaction to request the withdraw of her staked asset, after some delay, user issues another transaction to withdraw assets back with reward or penalty.
Contracts
StrategyManager
- Register whitelisted strategy.
- Entry for user to stake into strategies.
Strategy
- Handle shares mint and burn logic when stake is deposited into and withdrawed from.
DelegationManager
- Register delegation.
- Store operator’s balance.
- Entry for user to delegate to operator.
- Entry for user to issue withdraw stake request.
- Entry for user to issue tx to complete withdraw.
EigenPodManager
- Entry for user to create
EigenPod
.
- Stores user’s shares of native ether stake.
EigenPod
- Receiver of user’s validator balance.
- Implement methods to sync validator balance between execution layer and consensus layer.
Slasher
- Slash user’s staked asset according to corresponding AVS’s rule.
Code analysis
Deposit
LST
EigenLayer uses
StrategyManager
to handle deposit. User can call
StrategyManager.depositIntoStrategy
to deposit whitelisted ERC20 standard token to specified strategy. Each strategy handles single kind of token.// eigenlayer-contracts/src/contracts/core/StrategyManager.sol /** * @notice Deposits `amount` of `token` into the specified `strategy`, with the resultant shares credited to `msg.sender` * @param strategy is the specified strategy where deposit is to be made, * @param token is the denomination in which the deposit is to be made, * @param amount is the amount of token to be deposited in the strategy by the staker * @return shares The amount of new shares in the `strategy` created as part of the action. * @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf. * * WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors * where the token balance and corresponding strategy shares are not in sync upon reentrancy. */ function depositIntoStrategy( IStrategy strategy, IERC20 token, uint256 amount ) external onlyWhenNotPaused(PAUSED_DEPOSITS) nonReentrant returns (uint256 shares) { shares = _depositIntoStrategy(msg.sender, strategy, token, amount); }
Inside the
_depositIntoStrategy
:- transfer token from
msg.sender
tostrategy
- call
Strategy.deposit
to handle deposit logic inside theStrategy
- update the strategy shares of the user inside
StrategyManager
- call
DelegationManager.
increaseDelegatedShares
to add themsg.sender
’s delegated operator’s shares(if the user has delegated certain operator)
// eigenlayer-contracts/src/contracts/core/StrategyManager.sol /** * @notice Internal function in which `amount` of ERC20 `token` is transferred from `msg.sender` to the Strategy-type contract * `strategy`, with the resulting shares credited to `staker`. * @param staker The address that will be credited with the new shares. * @param strategy The Strategy contract to deposit into. * @param token The ERC20 token to deposit. * @param amount The amount of `token` to deposit. * @return shares The amount of *new* shares in `strategy` that have been credited to the `staker`. */ function _depositIntoStrategy( address staker, IStrategy strategy, IERC20 token, uint256 amount ) internal onlyStrategiesWhitelistedForDeposit(strategy) returns (uint256 shares) { // transfer tokens from the sender to the strategy token.safeTransferFrom(msg.sender, address(strategy), amount); // deposit the assets into the specified strategy and get the equivalent amount of shares in that strategy shares = strategy.deposit(token, amount); // add the returned shares to the staker's existing shares for this strategy _addShares(staker, strategy, shares); // Increase shares delegated to operator, if needed delegation.increaseDelegatedShares(staker, strategy, shares); emit Deposit(staker, token, strategy, shares); return shares; }
Inside the strategy’s
deposit
:- call hook to check deposit before
- check the to-deposit token is the registered token of the
Strategy
- calculate share ratio and mint corresponding shares to the user according to the deposited token.
- return the minted shares to
StrategyManager
to record,
//eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol /** * @notice Used to deposit tokens into this Strategy * @param token is the ERC20 token being deposited * @param amount is the amount of token being deposited * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's * `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well. * @dev Note that the assumption is made that `amount` of `token` has already been transferred directly to this contract * (as performed in the StrategyManager's deposit functions). In particular, setting the `underlyingToken` of this contract * to be a fee-on-transfer token will break the assumption that the amount this contract *received* of the token is equal to * the amount that was input when the transfer was performed (i.e. the amount transferred 'out' of the depositor's balance). * @return newShares is the number of new shares issued at the current exchange ratio. */ function deposit( IERC20 token, uint256 amount ) external virtual override onlyWhenNotPaused(PAUSED_DEPOSITS) onlyStrategyManager returns (uint256 newShares) { // call hook to allow for any pre-deposit logic _beforeDeposit(token, amount); require(token == underlyingToken, "StrategyBase.deposit: Can only deposit underlyingToken"); // copy `totalShares` value to memory, prior to any change uint256 priorTotalShares = totalShares; /** * @notice calculation of newShares *mirrors* `underlyingToShares(amount)`, but is different since the balance of `underlyingToken` * has already been increased due to the `strategyManager` transferring tokens to this strategy prior to calling this function */ // account for virtual shares and balance uint256 virtualShareAmount = priorTotalShares + SHARES_OFFSET; uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET; // calculate the prior virtual balance to account for the tokens that were already transferred to this contract uint256 virtualPriorTokenBalance = virtualTokenBalance - amount; newShares = (amount * virtualShareAmount) / virtualPriorTokenBalance; // extra check for correctness / against edge case where share rate can be massively inflated as a 'griefing' sort of attack require(newShares != 0, "StrategyBase.deposit: newShares cannot be zero"); // update total share amount to account for deposit totalShares = (priorTotalShares + newShares); return newShares; }
Inside the
StrategyManager._addShares
:- update staker’s staked strategy array
stakerStrategyList
if user stakes in a new strategy.
- update staker’s stake in the strategy
// eigenlayer-contracts/src/contracts/core/StrategyManager.sol /** * @notice This function adds `shares` for a given `strategy` to the `staker` and runs through the necessary update logic. * @param staker The address to add shares to * @param strategy The Strategy in which the `staker` is receiving shares * @param shares The amount of shares to grant to the `staker` * @dev In particular, this function calls `delegation.increaseDelegatedShares(staker, strategy, shares)` to ensure that all * delegated shares are tracked, increases the stored share amount in `stakerStrategyShares[staker][strategy]`, and adds `strategy` * to the `staker`'s list of strategies, if it is not in the list already. */ function _addShares(address staker, IStrategy strategy, uint256 shares) internal { // sanity checks on inputs require(staker != address(0), "StrategyManager._addShares: staker cannot be zero address"); require(shares != 0, "StrategyManager._addShares: shares should not be zero!"); // if they dont have existing shares of this strategy, add it to their strats if (stakerStrategyShares[staker][strategy] == 0) { require( stakerStrategyList[staker].length < MAX_STAKER_STRATEGY_LIST_LENGTH, "StrategyManager._addShares: deposit would exceed MAX_STAKER_STRATEGY_LIST_LENGTH" ); stakerStrategyList[staker].push(strategy); } // add the returned shares to their existing shares for this strategy stakerStrategyShares[staker][strategy] += shares; }
Inside the
DelegationManager.increaseDelegatedShares
:- check whether the staker has delegated to certain operator, if does, then increase the operator’s stake of the corresponding strategy.
// eigenlayer-contracts/src/contracts/core/DelegationManager.sol /** * @notice Increases a staker's delegated share balance in a strategy. * @param staker The address to increase the delegated shares for their operator. * @param strategy The strategy in which to increase the delegated shares. * @param shares The number of shares to increase. * * @dev *If the staker is actively delegated*, then increases the `staker`'s delegated shares in `strategy` by `shares`. Otherwise does nothing. * @dev Callable only by the StrategyManager or EigenPodManager. */ function increaseDelegatedShares( address staker, IStrategy strategy, uint256 shares ) external onlyStrategyManagerOrEigenPodManager { // if the staker is delegated to an operator if (isDelegated(staker)) { address operator = delegatedTo[staker]; // add strategy shares to delegate's shares _increaseOperatorShares({operator: operator, staker: staker, strategy: strategy, shares: shares}); } } mapping(address => address) public delegatedTo; /** * @notice Returns 'true' if `staker` *is* actively delegated, and 'false' otherwise. */ function isDelegated(address staker) public view returns (bool) { return (delegatedTo[staker] != address(0)); } /** * @notice returns the total number of shares in `strategy` that are delegated to `operator`. * @notice Mapping: operator => strategy => total number of shares in the strategy delegated to the operator. * @dev By design, the following invariant should hold for each Strategy: * (operator's shares in delegation manager) = sum (shares above zero of all stakers delegated to operator) * = sum (delegateable shares of all stakers delegated to the operator) */ mapping(address => mapping(IStrategy => uint256)) public operatorShares; // @notice Increases `operator`s delegated shares in `strategy` by `shares` and emits an `OperatorSharesIncreased` event function _increaseOperatorShares(address operator, address staker, IStrategy strategy, uint256 shares) internal { operatorShares[operator][strategy] += shares; emit OperatorSharesIncreased(operator, staker, strategy, shares); }
Native Ether
User can also deposit native ethers and run validators themselves, through assigning the withdraw credential to
EigenPod
, they can also restake on other systems. Each address can create one single EigenPod
, which syncs and receives owner’s multiple validators’s balance on consensus layers.Users have several ways to restake native ether:
- call
EigenPodManager.stake
to createEigenPod
and deposit to beacon chain in a single tx
- call
EigenPodManager.createPod
first, then callEigenPodManager.stake
- call
EigenPodManager.createPod
, deposit through beacon chain deposit contract and assign the withdrawal credentials to theEigenPod
address.
After deposit and the validator having been activated in the consensus layer, user should call
EigenPodManager.verifyBalanceUpdates
to sync the validator balance on consensus layer balance to update shares used to restake.Inside the
stake
:- check whether the user has registered
EigenPod
, if not, then deployEigenPod
first
- call
EigenPod.stake
to deposit ethers to beacon chain and set the withdraw credential to theEigenPod
// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /// @notice Pod owner to deployed EigenPod address mapping(address => IEigenPod) public ownerToPod; /** * @notice Stakes for a new beacon chain validator on the sender's EigenPod. * Also creates an EigenPod for the sender if they don't have one already. * @param pubkey The 48 bytes public key of the beacon chain validator. * @param signature The validator's signature of the deposit data. * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. */ function stake( bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { IEigenPod pod = ownerToPod[msg.sender]; if (address(pod) == address(0)) { //deploy a pod if the sender doesn't have one already pod = _deployPod(); } pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); }
Inside the
_deployPod
:EigenPodManager
use Beacon proxy to deployEigenPod
which faciliates the future upgrade ofEigenPod
.
EigenPodManager
register theEigenPod
of the owner which ensure that one address can only deploy oneEigenPod
.
- initialize the
EigenPod
to register thepodOwner
.
// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /// @notice Beacon proxy to which the EigenPods point IBeacon public immutable eigenPodBeacon; /// @notice Pod owner to deployed EigenPod address mapping(address => IEigenPod) public ownerToPod; function _deployPod() internal returns (IEigenPod) { // check that the limit of EigenPods has not been hit, and increment the EigenPod count require(numPods + 1 <= maxPods, "EigenPodManager._deployPod: pod limit reached"); ++numPods; // create the pod IEigenPod pod = IEigenPod( Create2.deploy( 0, bytes32(uint256(uint160(msg.sender))), // set the beacon address to the eigenPodBeacon and initialize it abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) ) ); pod.initialize(msg.sender); // store the pod in the mapping ownerToPod[msg.sender] = pod; emit PodDeployed(address(pod), msg.sender); return pod; }
// eigenlayer-contracts/src/contracts/pods/EigenPod.sol /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. function initialize(address _podOwner) external initializer { require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); podOwner = _podOwner; /** * From the M2 deployment onwards, we are requiring that pods deployed are by default enabled with restaking * In prior deployments without proofs, EigenPods could be deployed with restaking disabled so as to allow * simple (proof-free) withdrawals. However, this is no longer the case. Thus going forward, all pods are * initialized with hasRestaked set to true. */ hasRestaked = true; emit RestakingActivated(podOwner); }
inside the
stake
, it calls beacon chain deposit contract to deposit and set the withdrawal address to EigenPod
.// eigenlayer-contracts/src/contracts/pods/EigenPod.sol /// @notice This is the beacon chain deposit contract IETHPOSDeposit public immutable ethPOS; /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. function stake( bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot ) external payable onlyEigenPodManager { // stake on ethpos require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); emit EigenPodStaked(pubkey); } function _podWithdrawalCredentials() internal view returns (bytes memory) { return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); }
After the validator having been activated on the beacon chain, user can call
EigenPod.verifyBalanceUpdates
to sync the balance of validator and get shares to restake.Currently, Ethereum execution layer can’t access information on consensus layer directly, to sync balance of validators, EigenLayer uses an oracle to publish beacon chain’s block root regularly. Because beacon chain block root is calculated using merkle tree which includes detailed information of all validators, so it’s efficient to provide proof to prove and sync information like validator’s balance and exit status.
User passes the validator’s status and proof to
verifyBalanceUpdates
, which verify them against the beacon chain block root, then update the shares of the user.Inside the
verifyBalanceUpdates
:- check
validatorIndices
,validatorFieldsProofs
, andvalidatorFields
have same length.
- check
oracleTimestamp
is new enough.
- check
beaconStateRoot
is valid, using beacon chain block root published by oracle and the merkle tree proof provided by user.
- check each validator’s validatorFields is correct, and accumulate the delta shares(delta validator eth balance)
- call
eigenPodManager.recordBeaconChainETHBalanceUpdate
to update user’s native ether stake shares.
/** * @notice This function records an update (either increase or decrease) in a validator's balance. * @param oracleTimestamp The oracleTimestamp whose state root the proof will be proven against. * Must be within `VERIFY_BALANCE_UPDATE_WINDOW_SECONDS` of the current block. * @param validatorIndices is the list of indices of the validators being proven, refer to consensus specs * @param stateRootProof proves a `beaconStateRoot` against a block root fetched from the oracle * @param validatorFieldsProofs proofs against the `beaconStateRoot` for each validator in `validatorFields` * @param validatorFields are the fields of the "Validator Container", refer to consensus specs * @dev For more details on the Beacon Chain spec, see: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator */ function verifyBalanceUpdates( uint64 oracleTimestamp, uint40[] calldata validatorIndices, BeaconChainProofs.StateRootProof calldata stateRootProof, bytes[] calldata validatorFieldsProofs, bytes32[][] calldata validatorFields ) external onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_BALANCE_UPDATE) { // check validatorIndices, validatorFieldsProofs, and validatorFields have same length. require( (validatorIndices.length == validatorFieldsProofs.length) && (validatorFieldsProofs.length == validatorFields.length), "EigenPod.verifyBalanceUpdates: validatorIndices and proofs must be same length" ); // Balance updates should not be "stale" (older than VERIFY_BALANCE_UPDATE_WINDOW_SECONDS) require( oracleTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS >= block.timestamp, "EigenPod.verifyBalanceUpdates: specified timestamp is too far in past" ); // Verify passed-in beaconStateRoot against oracle-provided block root: BeaconChainProofs.verifyStateRootAgainstLatestBlockRoot({ latestBlockRoot: eigenPodManager.getBlockRootAtTimestamp(oracleTimestamp), beaconStateRoot: stateRootProof.beaconStateRoot, stateRootProof: stateRootProof.proof }); // verify validatorFields and calculate delta shares(eth balance) int256 sharesDeltaGwei; for (uint256 i = 0; i < validatorIndices.length; i++) { sharesDeltaGwei += _verifyBalanceUpdate( oracleTimestamp, validatorIndices[i], stateRootProof.beaconStateRoot, validatorFieldsProofs[i], // Use validator fields proof because contains the effective balance validatorFields[i] ); } // update beacon chain eth balance record in eigenPodManager(shares record) eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, sharesDeltaGwei * int256(GWEI_TO_WEI)); }
EigenPodManager.getBlockRootAtTimestamp
calls beaconChainOracle
to fetch state root on certain timestamp.// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /// @notice Returns the Beacon block root at `timestamp`. Reverts if the Beacon block root at `timestamp` has not yet been finalized. function getBlockRootAtTimestamp(uint64 timestamp) external view returns (bytes32) { bytes32 stateRoot = beaconChainOracle.timestampToBlockRoot(timestamp); require( stateRoot != bytes32(0), "EigenPodManager.getBlockRootAtTimestamp: state root at timestamp not yet finalized" ); return stateRoot; }
Inside the
EigenPod._verifyBalanceUpdate
:- get current effective validator balance from provided
validatorFields
- get recorded
validatorInfo
according tovalidatorPubkeyHash
from providedvalidatorFields
- require the oracle report timestamp is newer than the last validator information update time
- require the validator is active in the last time
- Check whether the validator is fully withdrawable, if it is, then require balance not being zero. Otherwise user should use
verifyAndProcessWithdrawals
to withdraw. This is becauseverifyBalanceUpdates
is used to update validator balance resulted from reward and slash on consensus, if used to update the balance change which is resulted from withdraw, then the ether can’t be withdrawed fromEigenPod
normally.
- verify
validatorFields
is valid againstbeaconStateRoot
,validatorFieldsProof
andvalidatorIndex
.
- update validator information
- calculate share delta(validator eth balance delta) and return
// eigenlayer-contracts/src/contracts/pods/EigenPod.sol struct ValidatorInfo { // index of the validator in the beacon chain uint64 validatorIndex; // amount of beacon chain ETH restaked on EigenLayer in gwei uint64 restakedBalanceGwei; //timestamp of the validator's most recent balance update uint64 mostRecentBalanceUpdateTimestamp; // status of the validator VALIDATOR_STATUS status; } /// @notice This is a mapping that tracks a validator's information by their pubkey hash mapping(bytes32 => ValidatorInfo) internal _validatorPubkeyHashToInfo; function _verifyBalanceUpdate( uint64 oracleTimestamp, uint40 validatorIndex, bytes32 beaconStateRoot, bytes calldata validatorFieldsProof, bytes32[] calldata validatorFields ) internal returns (int256 sharesDeltaGwei) { uint64 validatorEffectiveBalanceGwei = validatorFields.getEffectiveBalanceGwei(); bytes32 validatorPubkeyHash = validatorFields.getPubkeyHash(); ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkeyHash]; // 1. Balance updates should be more recent than the most recent update require( validatorInfo.mostRecentBalanceUpdateTimestamp < oracleTimestamp, "EigenPod.verifyBalanceUpdate: Validators balance has already been updated for this timestamp" ); // 2. Balance updates should only be performed on "ACTIVE" validators require(validatorInfo.status == VALIDATOR_STATUS.ACTIVE, "EigenPod.verifyBalanceUpdate: Validator not active"); // 3. Balance updates should only be made before a validator is fully withdrawn. // -- A withdrawable validator may not have withdrawn yet, so we require their balance is nonzero // -- A fully withdrawn validator should withdraw via verifyAndProcessWithdrawals if (validatorFields.getWithdrawableEpoch() <= _timestampToEpoch(oracleTimestamp)) { require( validatorEffectiveBalanceGwei > 0, "EigenPod.verifyBalanceUpdate: validator is withdrawable but has not withdrawn" ); } // Verify passed-in validatorFields against verified beaconStateRoot: BeaconChainProofs.verifyValidatorFields({ beaconStateRoot: beaconStateRoot, validatorFields: validatorFields, validatorFieldsProof: validatorFieldsProof, validatorIndex: validatorIndex }); // Done with proofs! Now update the validator's balance and send to the EigenPodManager if needed uint64 currentRestakedBalanceGwei = validatorInfo.restakedBalanceGwei; uint64 newRestakedBalanceGwei; if (validatorEffectiveBalanceGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { newRestakedBalanceGwei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; } else { newRestakedBalanceGwei = validatorEffectiveBalanceGwei; } // Update validator balance and timestamp, and save to state: validatorInfo.restakedBalanceGwei = newRestakedBalanceGwei; validatorInfo.mostRecentBalanceUpdateTimestamp = oracleTimestamp; _validatorPubkeyHashToInfo[validatorPubkeyHash] = validatorInfo; // If our new and old balances differ, calculate the delta and send to the EigenPodManager if (newRestakedBalanceGwei != currentRestakedBalanceGwei) { emit ValidatorBalanceUpdated(validatorIndex, oracleTimestamp, newRestakedBalanceGwei); sharesDeltaGwei = _calculateSharesDelta({ newAmountGwei: newRestakedBalanceGwei, previousAmountGwei: currentRestakedBalanceGwei }); } } /** * Calculates delta between two share amounts and returns as an int256 */ function _calculateSharesDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int256) { return int256(uint256(newAmountGwei)) - int256(uint256(previousAmountGwei)); }
Inside the
EigenPodManager.recordBeaconChainETHBalanceUpdate
:- update
podOwner
's share according tosharesDelta
- calculate
changeInDelegatableShares
using_calculateChangeInDelegatableShares
.
- update the delegated operator’s shares.
// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /** * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager * to ensure that delegated shares are also tracked correctly * @param podOwner is the pod owner whose balance is being updated. * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares * @dev Callable only by the podOwner's EigenPod contract. * @dev Reverts if `sharesDelta` is not a whole Gwei amount */ function recordBeaconChainETHBalanceUpdate( address podOwner, int256 sharesDelta ) external onlyEigenPod(podOwner) nonReentrant { require(podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address"); require(sharesDelta % int256(GWEI_TO_WEI) == 0, "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount"); int256 currentPodOwnerShares = podOwnerShares[podOwner]; int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; podOwnerShares[podOwner] = updatedPodOwnerShares; // inform the DelegationManager of the change in delegateable shares int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ sharesBefore: currentPodOwnerShares, sharesAfter: updatedPodOwnerShares }); // skip making a call to the DelegationManager if there is no change in delegateable shares if (changeInDelegatableShares != 0) { if (changeInDelegatableShares < 0) { delegationManager.decreaseDelegatedShares({ staker: podOwner, strategy: beaconChainETHStrategy, shares: uint256(-changeInDelegatableShares) }); } else { delegationManager.increaseDelegatedShares({ staker: podOwner, strategy: beaconChainETHStrategy, shares: uint256(changeInDelegatableShares) }); } } }
The
_calculateChangeInDelegatableShares
is a little confusing at the first glanace. It is used to handle the validator balance inconsistence between operator-undelegation request and withdrawl-completion request to ensure the delegated operator’s shares is accurate.One example is:
- Pod Owner calls
DelegationManager.undelegate
, which queues a withdrawal in theDelegationManager
. The Pod Owner's shares and delegated operator’s shares are set to 0 while the withdrawal is in the queue.
- Pod Owner's beacon chain ETH balance decreases (maybe due to slashing), and someone provides a proof of this to
EigenPod.verifyBalanceUpdates
. In this case, the Pod Owner will have negative shares in theEigenPodManager
.
- Inside the
_calculateChangeInDelegatableShares
, because thesharesBefore
is 0 andsharesAfter
is negative, so thechangeInDelegatableShares
is 0 which is reasonable, because operator’s delegated stake is already zero, no need to update again. On the otherhand, if eth balance increases in step 2, then operator will havesharesAfter
eth delegated.
If user only undelegates part of the ethers, then the
sharesBefore
is positive. Later when the balance of validator decreases significantly due to slash, then operator’s share should be decreased to counteract this situation. Due to operator’s minimal share is zero, the maximal decrease would be sharesBefore
. If operator’s share is not enough to counteract the validator’s balance decrease, in the withdraw completion tx(withdrawSharesAsTokens
), user’s withdrawable ethers will also decrease to counteract the slash.// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /** * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. */ function _calculateChangeInDelegatableShares(int256 sharesBefore, int256 sharesAfter) internal pure returns (int256) { if (sharesBefore <= 0) { // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares if (sharesAfter <= 0) { return 0; // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount } else { return sharesAfter; } } else { // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount if (sharesAfter <= 0) { return (-sharesBefore); // if the shares started positive and stayed positive, then the change in delegateable shares // is the difference between starting and ending amounts } else { return (sharesAfter - sharesBefore); } } }
We can see that
withdrawSharesAsTokens
and addShares
in EigenPodManager
both consider this situation.// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /** * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address * @dev Prioritizes decreasing the podOwner's share deficit, if they have one * @dev Reverts if `shares` is not a whole Gwei amount * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive */ function withdrawSharesAsTokens( address podOwner, address destination, uint256 shares ) external onlyDelegationManager { require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); int256 currentPodOwnerShares = podOwnerShares[podOwner]; // if there is an existing shares deficit, prioritize decreasing the deficit first if (currentPodOwnerShares < 0) { uint256 currentShareDeficit = uint256(-currentPodOwnerShares); // get rid of the whole deficit if possible, and pass any remaining shares onto destination if (shares > currentShareDeficit) { podOwnerShares[podOwner] = 0; shares -= currentShareDeficit; // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on } else { podOwnerShares[podOwner] += int256(shares); return; } } // Actually withdraw to the destination ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); }
// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /** * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) * @dev Reverts if `shares` is not a whole Gwei amount */ function addShares( address podOwner, uint256 shares ) external onlyDelegationManager returns (uint256) { require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); int256 currentPodOwnerShares = podOwnerShares[podOwner]; int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); podOwnerShares[podOwner] = updatedPodOwnerShares; return uint256(_calculateChangeInDelegatableShares({sharesBefore: currentPodOwnerShares, sharesAfter: updatedPodOwnerShares})); }
Register as operator
Address can call
DelegationManager.registerAsOperator
to register as an operator.operator’s details include:
earningsReceiver
: address to receive the rewards that the operator earns via serving AVS built on EigenLayer.
delegationApprover
: used by operator to whitelist stakers delegates to them. If operator has set thedelegationApprover
, then only staker with the signature ofdelegationApprover
can delegate to the operator.
stakerOptOutWindowBlocks
: used to control the time for a staker to exit the operator, allowing enough time for slashes to be discovered and executed. It is the max time among differnet AVS the operator participated in.
// eigenlayer-contracts/src/contracts/core/DelegationManager.sol // @notice Struct used for storing information about a single operator who has registered with EigenLayer struct OperatorDetails { // @notice address to receive the rewards that the operator earns via serving applications built on EigenLayer. address earningsReceiver; /** * @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations". * @dev Signature verification follows these rules: * 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed. * 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator. * 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value". */ address delegationApprover; /** * @notice A minimum delay -- measured in blocks -- enforced between: * 1) the operator signalling their intent to register for a service, via calling `Slasher.optIntoSlashing` * and * 2) the operator completing registration for the service, via the service ultimately calling `Slasher.recordFirstStakeUpdate` * @dev note that for a specific operator, this value *cannot decrease*, i.e. if the operator wishes to modify their OperatorDetails, * then they are only allowed to either increase this value or keep it the same. */ uint32 stakerOptOutWindowBlocks; } /** * @notice Registers the caller as an operator in EigenLayer. * @param registeringOperatorDetails is the `OperatorDetails` for the operator. * @param metadataURI is a URI for the operator's metadata, i.e. a link providing more details on the operator. * * @dev Once an operator is registered, they cannot 'deregister' as an operator, and they will forever be considered "delegated to themself". * @dev This function will revert if the caller attempts to set their `earningsReceiver` to address(0). * @dev Note that the `metadataURI` is *never stored * and is only emitted in the `OperatorMetadataURIUpdated` event */ function registerAsOperator( OperatorDetails calldata registeringOperatorDetails, string calldata metadataURI ) external { require( _operatorDetails[msg.sender].earningsReceiver == address(0), "DelegationManager.registerAsOperator: operator has already registered" ); _setOperatorDetails(msg.sender, registeringOperatorDetails); SignatureWithExpiry memory emptySignatureAndExpiry; // delegate from the operator to themselves _delegate(msg.sender, msg.sender, emptySignatureAndExpiry, bytes32(0)); // emit events emit OperatorRegistered(msg.sender, registeringOperatorDetails); emit OperatorMetadataURIUpdated(msg.sender, metadataURI); } // @dev Maximum Value for `stakerOptOutWindowBlocks`. Approximately equivalent to 6 months in blocks. uint256 public constant MAX_STAKER_OPT_OUT_WINDOW_BLOCKS = (180 days) / 12; /** * @notice Sets operator parameters in the `_operatorDetails` mapping. * @param operator The account registered as an operator updating their operatorDetails * @param newOperatorDetails The new parameters for the operator * * @dev This function will revert if the operator attempts to set their `earningsReceiver` to address(0). */ function _setOperatorDetails(address operator, OperatorDetails calldata newOperatorDetails) internal { require( newOperatorDetails.earningsReceiver != address(0), "DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address" ); require( newOperatorDetails.stakerOptOutWindowBlocks <= MAX_STAKER_OPT_OUT_WINDOW_BLOCKS, "DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS" ); require( newOperatorDetails.stakerOptOutWindowBlocks >= _operatorDetails[operator].stakerOptOutWindowBlocks, "DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be decreased" ); _operatorDetails[operator] = newOperatorDetails; emit OperatorDetailsModified(msg.sender, newOperatorDetails); }
After the details of operator have been registered, the operator delegates to itself. (Operator can also restake, same as other stakers)
Delegate
User calls
DelegationManager.delegateTo
to delegate to certain operator, let the operator use their stake to restake to other system and earn rewards.// eigenlayer-contracts/src/contracts/core/DelegationManager.sol /** * @notice Caller delegates their stake to an operator. * @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on EigenLayer. * @param approverSignatureAndExpiry Verifies the operator approves of this delegation * @param approverSalt A unique single use value tied to an individual signature. * @dev The approverSignatureAndExpiry is used in the event that: * 1) the operator's `delegationApprover` address is set to a non-zero value. * AND * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator * or their delegationApprover is the `msg.sender`, then approval is assumed. * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input * in this case to save on complexity + gas costs */ function delegateTo( address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt ) external { // go through the internal delegation flow, checking the `approverSignatureAndExpiry` if applicable _delegate(msg.sender, operator, approverSignatureAndExpiry, approverSalt); }
Inside the
_delegate
:- check the staker hasn’t delegated to any operator
- check the
operator
has registered onDelegationManager
- if the operator has registered
delegationApprover
, then check theapproverSignatureAndExpiry
is signed by thedelegationApprover
. It usesapproverSalt
to prevent double usage of same signature.
- record the operator of the user in the
delegatedTo
- get all strategies and corresponding shares of the user, add them on the operator.
// eigenlayer-contracts/src/contracts/core/DelegationManager.sol /** * @notice Mapping: staker => operator whom the staker is currently delegated to. * @dev Note that returning address(0) indicates that the staker is not actively delegated to any operator. */ mapping(address => address) public delegatedTo; /** * @notice Delegates *from* a `staker` *to* an `operator`. * @param staker The address to delegate *from* -- this address is delegating control of its own assets. * @param operator The address to delegate *to* -- this address is being given power to place the `staker`'s assets at risk on services * @param approverSignatureAndExpiry Verifies the operator approves of this delegation * @param approverSalt Is a salt used to help guarantee signature uniqueness. Each salt can only be used once by a given approver. * @dev Ensures that: * 1) the `staker` is not already delegated to an operator * 2) the `operator` has indeed registered as an operator in EigenLayer * 3) if applicable, that the approver signature is valid and non-expired */ function _delegate( address staker, address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt ) internal onlyWhenNotPaused(PAUSED_NEW_DELEGATION) { require(!isDelegated(staker), "DelegationManager._delegate: staker is already actively delegated"); require(isOperator(operator), "DelegationManager._delegate: operator is not registered in EigenLayer"); // fetch the operator's `delegationApprover` address and store it in memory in case we need to use it multiple times address _delegationApprover = _operatorDetails[operator].delegationApprover; /** * Check the `_delegationApprover`'s signature, if applicable. * If the `_delegationApprover` is the zero address, then the operator allows all stakers to delegate to them and this verification is skipped. * If the `_delegationApprover` or the `operator` themselves is the caller, then approval is assumed and signature verification is skipped as well. */ if (_delegationApprover != address(0) && msg.sender != _delegationApprover && msg.sender != operator) { // check the signature expiry require( approverSignatureAndExpiry.expiry >= block.timestamp, "DelegationManager._delegate: approver signature expired" ); // check that the salt hasn't been used previously, then mark the salt as spent require( !delegationApproverSaltIsSpent[_delegationApprover][approverSalt], "DelegationManager._delegate: approverSalt already spent" ); delegationApproverSaltIsSpent[_delegationApprover][approverSalt] = true; // calculate the digest hash bytes32 approverDigestHash = calculateDelegationApprovalDigestHash( staker, operator, _delegationApprover, approverSalt, approverSignatureAndExpiry.expiry ); // actually check that the signature is valid EIP1271SignatureUtils.checkSignature_EIP1271( _delegationApprover, approverDigestHash, approverSignatureAndExpiry.signature ); } // record the delegation relation between the staker and operator, and emit an event delegatedTo[staker] = operator; emit StakerDelegated(staker, operator); (IStrategy[] memory strategies, uint256[] memory shares) = getDelegatableShares(staker); for (uint256 i = 0; i < strategies.length;) { _increaseOperatorShares({ operator: operator, staker: staker, strategy: strategies[i], shares: shares[i] }); unchecked { ++i; } } }
getDelegatableShares
gets delegatable shares of native ether stake and other assets of the user./** * @notice Returns the number of actively-delegatable shares a staker has across all strategies. * @dev Returns two empty arrays in the case that the Staker has no actively-delegateable shares. */ function getDelegatableShares(address staker) public view returns (IStrategy[] memory, uint256[] memory) { // Get currently active shares and strategies for `staker` int256 podShares = eigenPodManager.podOwnerShares(staker); (IStrategy[] memory strategyManagerStrats, uint256[] memory strategyManagerShares) = strategyManager.getDeposits(staker); // Has no shares in EigenPodManager, but potentially some in StrategyManager if (podShares <= 0) { return (strategyManagerStrats, strategyManagerShares); } IStrategy[] memory strategies; uint256[] memory shares; if (strategyManagerStrats.length == 0) { // Has shares in EigenPodManager, but not in StrategyManager strategies = new IStrategy[](1); shares = new uint256[](1); strategies[0] = beaconChainETHStrategy; shares[0] = uint256(podShares); } else { // Has shares in both // 1. Allocate return arrays strategies = new IStrategy[](strategyManagerStrats.length + 1); shares = new uint256[](strategies.length); // 2. Place StrategyManager strats/shares in return arrays for (uint256 i = 0; i < strategyManagerStrats.length; ) { strategies[i] = strategyManagerStrats[i]; shares[i] = strategyManagerShares[i]; unchecked { ++i; } } // 3. Place EigenPodManager strat/shares in return arrays strategies[strategies.length - 1] = beaconChainETHStrategy; shares[strategies.length - 1] = uint256(podShares); } return (strategies, shares); }
_increaseOperatorShares
updates operator’s certain strategy’s shares./** * @notice returns the total number of shares in `strategy` that are delegated to `operator`. * @notice Mapping: operator => strategy => total number of shares in the strategy delegated to the operator. * @dev By design, the following invariant should hold for each Strategy: * (operator's shares in delegation manager) = sum (shares above zero of all stakers delegated to operator) * = sum (delegateable shares of all stakers delegated to the operator) */ mapping(address => mapping(IStrategy => uint256)) public operatorShares; // @notice Increases `operator`s delegated shares in `strategy` by `shares` and emits an `OperatorSharesIncreased` event function _increaseOperatorShares(address operator, address staker, IStrategy strategy, uint256 shares) internal { operatorShares[operator][strategy] += shares; emit OperatorSharesIncreased(operator, staker, strategy, shares); }
Withdraw
To improve the safety, for users to withdraw stake, they need to first issue a tx to queue their withdraw request, after certain delay, they can issue withdraw-completion tx to get their restaked asset back. The delay is for the AVS to check whether slash occurs.
Queue withdraw request
User can call
DelegationManager.queueWithdrawals
to queue their withdraw requests. User can specify which strategies and how much shares they want to withdraw.Inside the
queueWithdrawals
:- check the to-move strategies and shares array’s lengths match
- remove staker and operator’s shares, calculate withdraw request’s root for later withdraw-completion tx to use.
Note the
withdrawer
inside the QueuedWithdrawalParams
is the specified address who can issue tx to complete the withdrawl.// eigenlayer-contracts/src/contracts/core/DelegationManager.sol struct QueuedWithdrawalParams { // Array of strategies that the QueuedWithdrawal contains IStrategy[] strategies; // Array containing the amount of shares in each Strategy in the `strategies` array uint256[] shares; // The address of the withdrawer address withdrawer; } /** * Allows a staker to withdraw some shares. Withdrawn shares/strategies are immediately removed * from the staker. If the staker is delegated, withdrawn shares/strategies are also removed from * their operator. * * All withdrawn shares/strategies are placed in a queue and can be fully withdrawn after a delay. */ function queueWithdrawals( QueuedWithdrawalParams[] calldata queuedWithdrawalParams ) external onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) returns (bytes32[] memory) { bytes32[] memory withdrawalRoots = new bytes32[](queuedWithdrawalParams.length); for (uint256 i = 0; i < queuedWithdrawalParams.length; i++) { require(queuedWithdrawalParams[i].strategies.length == queuedWithdrawalParams[i].shares.length, "DelegationManager.queueWithdrawal: input length mismatch"); require(queuedWithdrawalParams[i].withdrawer != address(0), "DelegationManager.queueWithdrawal: must provide valid withdrawal address"); address operator = delegatedTo[msg.sender]; // Remove shares from staker's strategies and place strategies/shares in queue. // If the staker is delegated to an operator, the operator's delegated shares are also reduced // NOTE: This will fail if the staker doesn't have the shares implied by the input parameters withdrawalRoots[i] = _removeSharesAndQueueWithdrawal({ staker: msg.sender, operator: operator, withdrawer: queuedWithdrawalParams[i].withdrawer, strategies: queuedWithdrawalParams[i].strategies, shares: queuedWithdrawalParams[i].shares }); } return withdrawalRoots; }
Inside the
_removeSharesAndQueueWithdrawal
:- decrease operator’s shares
- remove staker’s shares
- calculate the root of the withdraw request struct and register it in
pendingWithdrawals
// eigenlayer-contracts/src/contracts/core/DelegationManager.sol /// @notice Mapping: hash of withdrawal inputs, aka 'withdrawalRoot' => whether the withdrawal is pending mapping(bytes32 => bool) public pendingWithdrawals; /** * @notice Removes `shares` in `strategies` from `staker` who is currently delegated to `operator` and queues a withdrawal to the `withdrawer`. * @dev If the `operator` is indeed an operator, then the operator's delegated shares in the `strategies` are also decreased appropriately. */ function _removeSharesAndQueueWithdrawal( address staker, address operator, address withdrawer, IStrategy[] memory strategies, uint256[] memory shares ) internal returns (bytes32) { require(staker != address(0), "DelegationManager._removeSharesAndQueueWithdrawal: staker cannot be zero address"); require(strategies.length != 0, "DelegationManager._removeSharesAndQueueWithdrawal: strategies cannot be empty"); // Remove shares from staker and operator // Each of these operations fail if we attempt to remove more shares than exist for (uint256 i = 0; i < strategies.length;) { // Similar to `isDelegated` logic if (operator != address(0)) { _decreaseOperatorShares({ operator: operator, staker: staker, strategy: strategies[i], shares: shares[i] }); } // Remove active shares from EigenPodManager/StrategyManager if (strategies[i] == beaconChainETHStrategy) { /** * This call will revert if it would reduce the Staker's virtual beacon chain ETH shares below zero. * This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive * shares from the operator to whom the staker is delegated. * It will also revert if the share amount being withdrawn is not a whole Gwei amount. */ eigenPodManager.removeShares(staker, shares[i]); } else { // this call will revert if `shares[i]` exceeds the Staker's current shares in `strategies[i]` strategyManager.removeShares(staker, strategies[i], shares[i]); } unchecked { ++i; } } // Create queue entry and increment withdrawal nonce uint256 nonce = cumulativeWithdrawalsQueued[staker]; cumulativeWithdrawalsQueued[staker]++; Withdrawal memory withdrawal = Withdrawal({ staker: staker, delegatedTo: operator, withdrawer: withdrawer, nonce: nonce, startBlock: uint32(block.number), strategies: strategies, shares: shares }); bytes32 withdrawalRoot = calculateWithdrawalRoot(withdrawal); // Place withdrawal in queue pendingWithdrawals[withdrawalRoot] = true; emit WithdrawalQueued(withdrawalRoot, withdrawal); return withdrawalRoot; } // @notice Decreases `operator`s delegated shares in `strategy` by `shares` and emits an `OperatorSharesDecreased` event function _decreaseOperatorShares(address operator, address staker, IStrategy strategy, uint256 shares) internal { // This will revert on underflow, so no check needed operatorShares[operator][strategy] -= shares; emit OperatorSharesDecreased(operator, staker, strategy, shares); } /// @notice Returns the keccak256 hash of `withdrawal`. function calculateWithdrawalRoot(Withdrawal memory withdrawal) public pure returns (bytes32) { return keccak256(abi.encode(withdrawal)); }
// eigenlayer-contracts/src/contracts/core/StrategyManager.sol /// @notice Used by the DelegationManager to remove a Staker's shares from a particular strategy when entering the withdrawal queue function removeShares( address staker, IStrategy strategy, uint256 shares ) external onlyDelegationManager { _removeShares(staker, strategy, shares); } /** * @notice Decreases the shares that `staker` holds in `strategy` by `shareAmount`. * @param staker The address to decrement shares from * @param strategy The strategy for which the `staker`'s shares are being decremented * @param shareAmount The amount of shares to decrement * @dev If the amount of shares represents all of the staker`s shares in said strategy, * then the strategy is removed from stakerStrategyList[staker] and 'true' is returned. Otherwise 'false' is returned. */ function _removeShares( address staker, IStrategy strategy, uint256 shareAmount ) internal returns (bool) { // sanity checks on inputs require(shareAmount != 0, "StrategyManager._removeShares: shareAmount should not be zero!"); //check that the user has sufficient shares uint256 userShares = stakerStrategyShares[staker][strategy]; require(shareAmount <= userShares, "StrategyManager._removeShares: shareAmount too high"); //unchecked arithmetic since we just checked this above unchecked { userShares = userShares - shareAmount; } // subtract the shares from the staker's existing shares for this strategy stakerStrategyShares[staker][strategy] = userShares; // if no existing shares, remove the strategy from the staker's dynamic array of strategies if (userShares == 0) { _removeStrategyFromStakerStrategyList(staker, strategy); // return true in the event that the strategy was removed from stakerStrategyList[staker] return true; } // return false in the event that the strategy was *not* removed from stakerStrategyList[staker] return false; }
// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /** * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive * shares from the operator to whom the staker is delegated. * @dev Reverts if `shares` is not a whole Gwei amount * @dev The delegation manager validates that the podOwner is not address(0) */ function removeShares( address podOwner, uint256 shares ) external onlyDelegationManager { require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); require(updatedPodOwnerShares >= 0, "EigenPodManager.removeShares: cannot result in pod owner having negative shares"); podOwnerShares[podOwner] = updatedPodOwnerShares; }
Complete withdraw
User calls
DelegationManager.completeQueuedWithdrawal
to complete the withdraw.// eigenlayer-contracts/src/contracts/core/DelegationManager.sol /** * @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer` * @param withdrawal The Withdrawal to complete. * @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `withdrawal.strategies` array. * This input can be provided with zero length if `receiveAsTokens` is set to 'false' (since in that case, this input will be unused) * @param middlewareTimesIndex is the index in the operator that the staker who triggered the withdrawal was delegated to's middleware times array * @param receiveAsTokens If true, the shares specified in the withdrawal will be withdrawn from the specified strategies themselves * and sent to the caller, through calls to `withdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies * will simply be transferred to the caller directly. * @dev middlewareTimesIndex is unused, but will be used in the Slasher eventually * @dev beaconChainETHStrategy shares are non-transferrable, so if `receiveAsTokens = false` and `withdrawal.withdrawer != withdrawal.staker`, note that * any beaconChainETHStrategy shares in the `withdrawal` will be _returned to the staker_, rather than transferred to the withdrawer, unlike shares in * any other strategies, which will be transferred to the withdrawer. */ function completeQueuedWithdrawal( Withdrawal calldata withdrawal, IERC20[] calldata tokens, uint256 middlewareTimesIndex, bool receiveAsTokens ) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant { _completeQueuedWithdrawal(withdrawal, tokens, middlewareTimesIndex, receiveAsTokens); }
Inside the
_completeQueuedWithdrawal
:- verify the withdrawl is pending
- verify the withdrawl has waiting for a long enough time(
withdrawalDelayBlocks
).
- check the
msg.sender
is thewithdrawal.withdrawer
receiveAsTokens
is to express whether the withdrawer wants to burn shares and get underlying asset back, or wants to recover shares.- If
receiveAsTokens
is true, then it callsEigenPodManager
orStrategyManager
to burn the shares and transfer underlying asset to the recipient. - if
receiverAsTokens
is false, then it recover the shares of the staker and operator.
// eigenlayer-contracts/src/contracts/core/DelegationManager.sol /** * @dev commented-out param (middlewareTimesIndex) is the index in the operator that the staker who triggered the withdrawal was delegated to's middleware times array * This param is intended to be passed on to the Slasher contract, but is unused in the M2 release of these contracts, and is thus commented-out. */ function _completeQueuedWithdrawal( Withdrawal calldata withdrawal, IERC20[] calldata tokens, uint256 /*middlewareTimesIndex*/, bool receiveAsTokens ) internal { bytes32 withdrawalRoot = calculateWithdrawalRoot(withdrawal); require( pendingWithdrawals[withdrawalRoot], "DelegationManager.completeQueuedAction: action is not in queue" ); require( withdrawal.startBlock + withdrawalDelayBlocks <= block.number, "DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed" ); require( msg.sender == withdrawal.withdrawer, "DelegationManager.completeQueuedAction: only withdrawer can complete action" ); if (receiveAsTokens) { require( tokens.length == withdrawal.strategies.length, "DelegationManager.completeQueuedAction: input length mismatch" ); } // Remove `withdrawalRoot` from pending roots delete pendingWithdrawals[withdrawalRoot]; // Finalize action by converting shares to tokens for each strategy, or // by re-awarding shares in each strategy. if (receiveAsTokens) { for (uint256 i = 0; i < withdrawal.strategies.length; ) { _withdrawSharesAsTokens({ staker: withdrawal.staker, withdrawer: msg.sender, strategy: withdrawal.strategies[i], shares: withdrawal.shares[i], token: tokens[i] }); unchecked { ++i; } } // Award shares back in StrategyManager/EigenPodManager. If withdrawer is delegated, increase the shares delegated to the operator } else { address currentOperator = delegatedTo[msg.sender]; for (uint256 i = 0; i < withdrawal.strategies.length; ) { /** When awarding podOwnerShares in EigenPodManager, we need to be sure to only give them back to the original podOwner. * Other strategy shares can + will be awarded to the withdrawer. */ if (withdrawal.strategies[i] == beaconChainETHStrategy) { address staker = withdrawal.staker; /** * Update shares amount depending upon the returned value. * The return value will be lower than the input value in the case where the staker has an existing share deficit */ uint256 increaseInDelegateableShares = eigenPodManager.addShares({ podOwner: staker, shares: withdrawal.shares[i] }); address podOwnerOperator = delegatedTo[staker]; // Similar to `isDelegated` logic if (podOwnerOperator != address(0)) { _increaseOperatorShares({ operator: podOwnerOperator, // the 'staker' here is the address receiving new shares staker: staker, strategy: withdrawal.strategies[i], shares: increaseInDelegateableShares }); } } else { strategyManager.addShares(msg.sender, withdrawal.strategies[i], withdrawal.shares[i]); // Similar to `isDelegated` logic if (currentOperator != address(0)) { _increaseOperatorShares({ operator: currentOperator, // the 'staker' here is the address receiving new shares staker: msg.sender, strategy: withdrawal.strategies[i], shares: withdrawal.shares[i] }); } } unchecked { ++i; } } } emit WithdrawalCompleted(withdrawalRoot); }
Inside the
_withdrawSharesAsTokens
, it calls EigenPodManager
or StrategyManager
to handle withdrawl logic according to the strategy
.// eigenlayer-contracts/src/contracts/core/DelegationManager.sol /** * @notice Withdraws `shares` in `strategy` to `withdrawer`. If the shares are virtual beaconChainETH shares, then a call is ultimately forwarded to the * `staker`s EigenPod; otherwise a call is ultimately forwarded to the `strategy` with info on the `token`. */ function _withdrawSharesAsTokens(address staker, address withdrawer, IStrategy strategy, uint256 shares, IERC20 token) internal { if (strategy == beaconChainETHStrategy) { eigenPodManager.withdrawSharesAsTokens({ podOwner: staker, destination: withdrawer, shares: shares }); } else { strategyManager.withdrawSharesAsTokens(withdrawer, strategy, shares, token); } }
Note in the
EigenPodManager.withdrawSharesAsTokens
, it considers the decifit situation. If there is update of the validator’s balance between withdrawl request and completion request, then in the completion request, it should consider the potential change of the validator balance, ajdust the withdrawable shares or the podOwnerShares
to counteract the change.// eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol /** * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address * @dev Prioritizes decreasing the podOwner's share deficit, if they have one * @dev Reverts if `shares` is not a whole Gwei amount * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive */ function withdrawSharesAsTokens( address podOwner, address destination, uint256 shares ) external onlyDelegationManager { require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); int256 currentPodOwnerShares = podOwnerShares[podOwner]; // if there is an existing shares deficit, prioritize decreasing the deficit first if (currentPodOwnerShares < 0) { uint256 currentShareDeficit = uint256(-currentPodOwnerShares); // get rid of the whole deficit if possible, and pass any remaining shares onto destination if (shares > currentShareDeficit) { podOwnerShares[podOwner] = 0; shares -= currentShareDeficit; // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on } else { podOwnerShares[podOwner] += int256(shares); return; } } // Actually withdraw to the destination ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); }
// eigenlayer-contracts/src/contracts/pods/EigenPod.sol /** * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the * `amountWei` input (when converted to GWEI). * @dev Reverts if `amountWei` is not a whole Gwei amount */ function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { require( amountWei % GWEI_TO_WEI == 0, "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" ); uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); require( amountGwei <= withdrawableRestakedExecutionLayerGwei, "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" ); withdrawableRestakedExecutionLayerGwei -= amountGwei; emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); // transfer ETH from pod to `recipient` directly _sendETH(recipient, amountWei); }
StrategyManager.withdrawSharesAsTokens
calls corresponding strategy to burn the shares and transfer underlying asset to the recipient.// eigenlayer/contract/eigenlayer-contracts/src/contracts/core/StrategyManager.sol /// @notice Used by the DelegationManager to convert withdrawn shares to tokens and send them to a recipient function withdrawSharesAsTokens( address recipient, IStrategy strategy, uint256 shares, IERC20 token ) external onlyDelegationManager { strategy.withdraw(recipient, token, shares); }
// eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol /** * @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address * @param recipient is the address to receive the withdrawn funds * @param token is the ERC20 token being transferred out * @param amountShares is the amount of shares being withdrawn * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's * other functions, and individual share balances are recorded in the strategyManager as well. */ function withdraw( address recipient, IERC20 token, uint256 amountShares ) external virtual override onlyWhenNotPaused(PAUSED_WITHDRAWALS) onlyStrategyManager { // call hook to allow for any pre-withdrawal logic _beforeWithdrawal(recipient, token, amountShares); require(token == underlyingToken, "StrategyBase.withdraw: Can only withdraw the strategy token"); // copy `totalShares` value to memory, prior to any change uint256 priorTotalShares = totalShares; require( amountShares <= priorTotalShares, "StrategyBase.withdraw: amountShares must be less than or equal to totalShares" ); /** * @notice calculation of amountToSend *mirrors* `sharesToUnderlying(amountShares)`, but is different since the `totalShares` has already * been decremented. Specifically, notice how we use `priorTotalShares` here instead of `totalShares`. */ // account for virtual shares and balance uint256 virtualPriorTotalShares = priorTotalShares + SHARES_OFFSET; uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET; // calculate ratio based on virtual shares and balance, being careful to multiply before dividing uint256 amountToSend = (virtualTokenBalance * amountShares) / virtualPriorTotalShares; // Decrease the `totalShares` value to reflect the withdrawal totalShares = priorTotalShares - amountShares; underlyingToken.safeTransfer(recipient, amountToSend); }
Slaher
EigenLayer hasn’t implemeted this part currently.
Conclusion
By building a shared security model, EigenLayer saves cost to launch new infrastructure.
But there is some potential risks should be considered carefully.
The most significant risk may be the restake improves the benefit of attack. Because restaked asset can be used to participate multiple system, if attacker simultaneously attacks multiple systems, the cost will be the sum of the attacks on each system, but the cost is the same underlying restaked asset. So there should be proper scheme to balance the benefit and the cost of attacks considering the new restake pattern.