CSM Gate 与 Extension

lido__ 发布于 2025-02-11 阅读 12

本文是Lido社区质押模块(CSM)v2的设计草案,针对现有CSM在节点运营商注册上的灵活性不足问题,引入了Gate和Extension概念。Gate是允许用户加入CSM的智能合约,分为Permissionless(无需许可)和Permissioned(需许可)两类,可以为新运营商指定债券曲线;Extension则允许第三方合约作为节点运营商的代理人,实现自定义的管理逻辑,例如DVT扩展可以管理集群参与者并共享奖励。文章详细说明了这两类合约的设计思路,并给出了Solidity代码示例,以及它们对CSM现有交互接口(如addValidatorKeys方法增加from参数)的影响。

本文档已关闭!

请评论并参阅 https://hackmd.io/@lido/csm-v2-internal

我们都知道,定制裁剪的衣服往往比大众市场的成衣更舒适。对于使用 LST 协议的 Ethereum 验证也是如此。Community Staking Module (CSM) 已被证明是面向各种 Ethereum 质押者的市场领先解决方案,从家庭质押者到专业 Node Operators 都适用。如此多样化的参与者群体,自然需要一些定制化的解决方案。

目标

  • 为 CSM 中不同类型的质押者提供独立的入口,例如:
    • 未识别的无许可参与者;
    • 已识别的 solo、家庭及社区质押者;
    • 已识别的专业 Node Operators;
    • 使用特定技术(例如 DVT)运行验证器的质押者;
    • 其他可区分的类型;
  • 允许 CSM 与愿意吸引其用户成为 CSM Node Operators 的第三方进行无缝集成;

总体描述

CSModule.sol 目前有若干创建 Node Operator 的方法。在 Early Adoption (EA) 期间,这些方法仅允许 EA List 的成员加入。一旦 EA 期结束,这些方法就变得无需许可。这种方法在定制化方面极为有限。因此,我们提出了 Gates 和 Extensions 的概念。

为实现这一概念,CSModule.sol 中的 Node Operator 创建方法应设为有权限的。只有相应角色(CREATE_NODE_OPERATOR_ROLE)的成员才能调用这些方法。这些角色成员就是我们所说的 Gates 和 Extensions。

Gates

image

Gates 是允许用户加入 CSM 的智能合约。有两种类型的 gates:

  • Permissionless(无许可)
  • Permissioned(有许可)

Gate 可以为其创建的 operators 分配自定义的 Node Operator 类型(由 bondCurveId 定义)。Permissioned gate 可以允许现有的 Node Operators 证明他们有资格获得相应的 Node Operator 类型,并申领一个(对应的 bondCurveId)。

Gates 示例和代码片段

Permissionless gate

这是一个无状态合约,代理 addNodeOperator* 调用,不附带额外功能。它通过强制上传 keys 来维持不变量。

PermissionlessGate.sol

import { ICSAccounting } from "./interfaces/ICSAccounting.sol";
import { ICSModule, NodeOperatorManagementProperties } from "./interfaces/ICSModule.sol";

contract PermissionlessGate {
    /// @dev 这是 accounting 合约中的默认 bond curve ID
    ///      无需显式设置
    uint256 public immutable CURVE_ID;

    ICSModule public immutable CSM;

    constructor(address csm) {
        CSM = ICSModule(csm);
        CURVE_ID = CSM.accounting().DEFAULT_BOND_CURVE_ID();
    }

    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        address referrer
    ) external payable returns (uint256 nodeOperatorId) {
        nodeOperatorId = CSM.createNodeOperator(
            msg.sender,
            managementProperties,
            referrer
        );

        CSM.addValidatorKeysETH{ value: msg.value }({
            from: msg.sender,
            nodeOperatorId: nodeOperatorId,
            keysCount: keysCount,
            publicKeys: publicKeys,
            signatures: signatures
        });
    }

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }
}
Vetted Gate

一个仅允许通过 Merkle Tree 审核通过的地址加入的 Gate。此外,它还为 Node Operator 设置特殊的 bond curve。

VettedGate.sol

import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import { ICSModule, NodeOperatorManagementProperties, NodeOperator } from "./interfaces/ICSModule.sol";
import { ICSAccounting } from "./interfaces/ICSAccounting.sol";

contract VettedGate {
    uint256 public immutable CURVE_ID;

    ICSModule public immutable CSM;

    /// @dev 符合条件的成员 Merkle Tree 的根
    bytes32 public treeRoot;

    mapping(address => bool) internal _consumedAddresses;

    constructor(
        bytes32 _treeRoot,
        uint256 curveId,
        address csm,
        address admin
    ) {
        CSM = ICSModule(csm);
        CURVE_ID = curveId;
        _setTreeRoot(_treeRoot);
    }

    /// @dev 首先检查 msg.sender 的资格
    ///      设置 bond curve。这要求在 CSM 侧授予 `SET_BOND_CURVE` 角色
    ///      然后,按照给定 curve 的 bond 要求上传 keys
    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        bytes32[] calldata proof,
        address referrer
    ) external payable returns (uint256 nodeOperatorId) {
        _consume(proof);

        nodeOperatorId = CSM.createNodeOperator(
            msg.sender,
            managementProperties,
            referrer
        );
        CSM.setBondCurve(nodeOperatorId, CURVE_ID);
        CSM.addValidatorKeysETH{ value: msg.value }({
            from: msg.sender,
            nodeOperatorId: nodeOperatorId,
            keysCount: keysCount,
            publicKeys: publicKeys,
            signatures: signatures
        });
    }

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) {  ...  }

    /// @dev 为符合条件的 Node Operator 申领 bond curve
    ///      检查 msg.sender 是否是 Node Operator 的安全地址
    function claimBondCurve(
        uint256 nodeOperatorId,
        bytes32[] calldata proof
    ) external {
        NodeOperator memory nodeOperator = CSM.getNodeOperator(nodeOperatorId);
        address nodeOperatorAddress = nodeOperator.extendedManagerPermissions
            ? nodeOperator.managerAddress
            : nodeOperator.rewardAddress;
        if (nodeOperatorAddress != msg.sender) revert NotAllowedToClaim();
        _consume(proof);

        CSM.setBondCurve(nodeOperatorId, CURVE_ID);
    }

    function isConsumed(address member) public view returns (bool) {
        return _consumedAddresses[member];
    }

    function verifyProof(
        address member,
        bytes32[] calldata proof
    ) public view returns (bool) {
        return MerkleProof.verifyCalldata(proof, treeRoot, hashLeaf(member));
    }

    function hashLeaf(address member) public pure returns (bytes32) {
        return keccak256(bytes.concat(keccak256(abi.encode(member))));
    }

    function _consume(bytes32[] calldata proof) internal {
        if (isConsumed(msg.sender)) revert AlreadyConsumed();
        if (!verifyProof(msg.sender, proof)) revert InvalidProof();
        _consumedAddresses[msg.sender] = true;
        emit Consumed(msg.sender);
    }

    function _setTreeRoot(bytes32 _treeRoot) internal {
        treeRoot = _treeRoot;
        emit TreeRootSet(_treeRoot);
    }
}

Extensions

image

与 gates 类似,extensions 也是允许 Node Operators 加入 CSM 的智能合约。Extensions 也以某种形式抽象了 Node Operator 管理。为此,extension 可以将自己设置为 CSM Node Operator 的 manager 和/或 reward address。这使 extension 可以实现自定义的 Node Operator 管理原则,例如从 Node Operator 奖励中抽取奖励份额,或者只允许上传经过验证的验证者密钥。

一个很好但不详尽的 extension 示例是 DVT 驱动的 extension。由于 DVT 假设集群参与者共同运营单个验证者,因此 DVT 驱动的 extension 可以管理各个集群参与者并分享奖励。DVT 驱动的 extension 的另一个可能方面是链上密钥验证,确保只有通过 DKG 创建的密钥才能通过此 extension 上传到 CSM。

Extensions 示例和代码片段

ExtensionExample.sol

contract ExtensionExample {
    /// @dev 扩展需要保存真实的 Node Operator 地址以便与其交互
    ///      扩展应有相应的方法来更改这些地址。它可以使用与 CSM 自身相同的系统,也可以使用特定的系统
    struct ExtensionNodeOperator {
        uint64 nodeOperatorId;
        address managerAddress;
        address rewardAddress;
    }

    mapping(uint64 => ExtensionNodeOperator) public extensionNodeOperators;

    /// @dev 为符合条件的成员分配的 bond curve 的 Id
    uint256 public immutable CURVE_ID;

    /// @dev Community Staking Module 的地址
    ICSModule public immutable CSM;

    constructor(
        uint256 curveId,
        address csm,
        uint256 extension_share_bp
    ) {
        CSM = ICSModule(csm);
        CURVE_ID = curveId;
    }

    /// @dev 主要区别在于合约本身充当 node operator
    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        bytes32[] calldata proof,
        address referrer
    ) external payable returns (uint256 nodeOperatorId) {
        nodeOperatorId = CSM.createNodeOperator(
            address(this),
            managementProperties,
            referrer
        );

        extensionNodeOperators[nodeOperatorId] = ExtensionNodeOperator({
            nodeOperatorId: uint64(nodeOperatorId),
            managerAddress: msg.sender,
            rewardAddress: msg.sender
        });

        CSM.setBondCurve(nodeOperatorId, CURVE_ID);
        CSM.addValidatorKeysETH{ value: msg.value }({
            from: address(this),
            nodeOperatorId: nodeOperatorId,
            keysCount: keysCount,
            publicKeys: publicKeys,
            signatures: signatures
        });
    }

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) { ... }

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId) { ... }

    function claimRewardsStETH(uint64 nodeOperatorId,
        uint256 stETHAmount,
        uint256 cumulativeFeeShares,
        bytes32[] memory rewardsProof
    ) external {
        ExtensionNodeOperator storage extensionNodeOperator = extensionNodeOperators[nodeOperatorId];
        if (extensionNodeOperator.managerAddress != msg.sender) revert NotAuthorized();

        /// 拉取奖励以计算给定份额的准确数量
        uint256 distributedShares = CSM.accounting().pullFeeRewards(
            nodeOperatorId,
            cumulativeFeeShares,
            rewardsProof
        );

        uint256 claimedShares = CSM.claimRewardsStETH(
            nodeOperatorId,
            stETHAmount,
            0,
            new bytes32[](0)
        );

        // 扩展可以对已分配的份额做任何想做的事,例如在合约余额中保留一部分
        // 然后将剩余的份额转移到 node operator 的 reward address
    }

    /// @dev 所有其他仅允许 NO 地址调用的 CSM 方法
    ///      也应在此处暴露,例如:
    ///      - 上传更多 keys
    ///      - removeKeys
    ///      - compensateELRewardsStealingPenalty
}

对 CSM 交互的影响

Node Operator 创建将拥有与当前接口类似的接口。然而,被调用的合约将有所不同。默认情况下,CSM 将有两个原生 gates:

  • Permissionless gate;
  • Identified Home stakers gate;

Permissionless gate 将拥有一个简化的 Node Operator 创建接口(不带 proof 参数):

    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        address referrer
    ) external payable returns (uint256 nodeOperatorId);

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId);

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        address referrer
    ) external returns (uint256 nodeOperatorId);

Identified Home stakers gate 将继承现有的 CSM Node Operator 创建接口:

    function addNodeOperatorETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        bytes32[] calldata proof,
        address referrer
    ) external payable returns (uint256 nodeOperatorId);

    function addNodeOperatorStETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId);

    function addNodeOperatorWstETH(
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        NodeOperatorManagementProperties calldata managementProperties,
        ICSAccounting.PermitInput calldata permit,
        bytes32[] calldata proof,
        address referrer
    ) external returns (uint256 nodeOperatorId);

除了 Node Operator 创建之外,Identified Home stakers gate 还将为现有的 CSM Node Operators 提供一个方法,以申领有利的 bond curve:

    function claimBondCurve(
        uint256 nodeOperatorId,
        bytes32[] calldata proof
    ) external;

与 CSM 的所有其他交互将保持不变,除了 addKeys 方法接口的细微更改(新增 from 参数):

    function addValidatorKeysETH(
        address from,
        uint256 nodeOperatorId,
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures
    ) external payable;

    function addValidatorKeysStETH(
        address from,
        uint256 nodeOperatorId,
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        ICSAccounting.PermitInput calldata permit
    ) external;

    function addValidatorKeysWstETH(
        address from,
        uint256 nodeOperatorId,
        uint256 keysCount,
        bytes calldata publicKeys,
        bytes calldata signatures,
        ICSAccounting.PermitInput calldata permit
    ) external;
  • 原文链接: hackmd.io/@lido/HyMTWnER...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论