EigenLayer Contract Introduction # 1 — Core
DISCLAIMER #1 : 本文基于公开仓库和文档进行编译,不涉及任何商业机密和投资建议,仅供学习交流使用
DISCLAIMER #2: 不保证语句通顺,纠错和反馈请联系 wechat: xiaohuo0x or telegram: @xiaohuo
EigenLayer Core
EigenLayer Core 的代码其实很简洁,但是在理解代码之前我们需要理清 EigenLayer 的顶层设计,这里归纳了几点希望可以帮助大家理解:
- EigenLayer 的生态大体上可以分成三类角色:
a) 拥有 LST 资产的终端用户
b) 拥有节点运行能力的节点商/矿池
c) 需要网络效应的 AVS 应用方 - 各个角色的关系如下:
a) AVS 应用方定义 PoS 网络规则,包括节点软件,健康检查和监控指标,以及网络支持的 strategy list 和其权重,最小 stake 数目,最大节点数目,踢出规则等等
b) 节点商/矿池通过注册为 operator 从而进一步 opt-in 到某些特定的 AVS 网络中,运行其节点以获得奖励
c) 终端用户只需将自己的 LST delegate 到 operator 即可
IStrategy and StrategyBase
每一个被 EigenLayer 生态支持的 LST 都被抽象成一个叫 Strategy 的合约,它在概念上和 ERC4624 https://eips.ethereum.org/EIPS/eip-4626 非常相似,只是不会发行新的 token 而已。
掌握 EigenLayer 的 Strategy 合约或者说 ERC4624 合约的重点其实不难,单纯的把它看成一种记账的方法就行:
- 每一个 Strategy 合约都会关联一个底层 ERC20 资产合约,比如 StrategyBase(stETH) https://etherscan.io/address/0x93c4b944D05dfe6df7645A86cd2206016c51564D#readProxyContract 的底层合约地址是 https://etherscan.io/address/0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 也就是 stETH 详见:https://lido.fi/
- 用户向 Strategy 合约 deposit 底层资产之后,会被分配一定量的 share
- 用户向 Strategy 合约 withdraw 一定量的 share 之后,会收到相对应的底层资产 ERC20
- 无论是 deposit 还是 withdraw share 的单价总是 total share/ total underlying asset 比如 deposit 如果想存入的 amount 是 X,那么用户得到的 share 将会是:
a) 先计算 share 的单价 x = total asset before depositing / total share before depositing
b) 再计算用户将获取的 share 数量 X / x - 用户想 withdraw Y share
a) 先计算 share 的单价 x = total asset before withdrawing / total share before withdrawing
b) 再计算用户将获取的 asset 数量 Y * x
StrategyManager
StrategyManager 管理着所有的 Strategy 合约,目前 EigenLayer 所支持的 strategy 是白名单机制,实现方式也很直接:
/// @notice Mapping: strategy => whether or not stakers are allowed to deposit into it
mapping(IStrategy => bool) public strategyIsWhitelistedForDeposit;
modifier onlyStrategiesWhitelistedForDeposit(IStrategy strategy) {
require(
strategyIsWhitelistedForDeposit[strategy],
"StrategyManager.onlyStrategiesWhitelistedForDeposit: strategy not whitelisted"
);
_;
}
/**
* @notice Owner-only function that adds the provided Strategies to the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToWhitelist Strategies that will be added to the `strategyIsWhitelistedForDeposit` mapping (if they aren't in it already)
* @param thirdPartyTransfersForbiddenValues bool values to set `thirdPartyTransfersForbidden` to for each strategy
*/
function addStrategiesToDepositWhitelist(
IStrategy[] calldata strategiesToWhitelist,
bool[] calldata thirdPartyTransfersForbiddenValues
) external;
/**
* @notice Owner-only function that removes the provided Strategies from the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToRemoveFromWhitelist Strategies that will be removed to the `strategyIsWhitelistedForDeposit` mapping (if they are in it)
*/
function removeStrategiesFromDepositWhitelist(IStrategy[] calldata strategiesToRemoveFromWhitelist) external;
同时 StrategyManager 也是用户进行 LST deposit 的入口:
/// @notice Mapping: staker => Strategy => number of shares which they currently hold
mapping(address => mapping(IStrategy => uint256)) public stakerStrategyShares;
/**
* @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);
}
除此之外 StrategyManager 也会全局的记录下每个用户在各个 strategy 合约里面拥有的 share 数目。
DelegationManager
如果说 StrategyManager 处理了用户如何将 LST/LSR 带入 EigenLayer 生态,那么 DelegationManager 则着重处理:
- 如何注册为 operator
- delegate/undelegate to/from operator
注册为 operator 的操作只需要 invoke 一个合约方法:
/**
* @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)
OperatorDetails 定义了 earning receiver,delegation approver (通常为 address(0)) 以及 opt-out 时间窗后(以后再讲)
metadataURI 则定义了 operator 的基本信息,会被 EigenLayer 的 indexer 索引到并在 UI 上显示,模版如下:
{
"name": "EigenLabs",
"website": "https://www.eigenlayer.xyz/",
"description": "Shared Security To Hyperscale Ethereum",
"logo": "https://goerli-operator-metadata.s3.amazonaws.com/eigenlayer.png",
"twitter": "https://twitter.com/eigenlayer"
}
相应的 StrategyManager 也提供两个方法更改上述信息:
function modifyOperatorDetails(OperatorDetails calldata newOperatorDetails) external {
require(isOperator(msg.sender), "DelegationManager.modifyOperatorDetails: caller must be an operator");
_setOperatorDetails(msg.sender, newOperatorDetails);
}
function updateOperatorMetadataURI(string calldata metadataURI) external {
require(isOperator(msg.sender), "DelegationManager.updateOperatorMetadataURI: caller must be an operator");
emit OperatorMetadataURIUpdated(msg.sender, metadataURI);
}
当节点商注册为 operator 之后,用户就可以通过 delegateTo 将自己的 LST delegate 到该节点:
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);
}
根据目前 EigenLayer 的代码,delegate 是 all or no 的设计,也就是说,一个 staker 地址只能 delegate 到一个 operator 上:
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");
delegate 在 EigenLayer 合约 StrategyManager 里面并没有实际的 token 转移,而只是记账,也充分和 AVS 的代码解耦,后面会讲到这种设计的好处。
// 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;
}
}
StrategyManager 还有很大一部分代码在阐述如果 undelegate 和 withdraw:
- 用户选择 undelegate share 会立马从 staker/operator 上被移除并转移到 pending queue
- 一定的 delay 期限过后,用户可以选择 completeQueuedWithdrawal,delay 算法如下:(都是由 owner 去设置)
max(minWithdrawalDelayBlocks, strategyWithdrawalDelayBlocks[strategy])
AVSDirectory
AVSDirectory 合约则专注于 AVS 的注册和信息处理,逻辑相对简单
- AVS 合约通过 invoke updateAVSMetadataURI 方法来发布自己的 metadata 信息,从而 EigenLayer indexer 可以索引到 AVS 的基础信息并在 UI 上展示(目前是白名单制)
- Operator 可以通过 invoke registerOperatorToAVS 或者 deregisterOperatorFromAVS 来 opt-in/opt-out 特定的 AVS,从而加入网络或者退出网络
EigenLayer Middleware
EigeLayer Middleware 是一套帮助 AVS 项目方进行研发的工具合约,类似于智能合约版本的 sdk,之后出篇幅单独讲。