ERC2535

微信扫码分享

ERC-2535

概念简介

ERC-2535(Diamond Standard,又称 Diamonds, Multi-Facet Proxy)是以太坊的钻石标准,定义了一种模块化智能合约系统,可以在部署后无限升级和扩展,突破了以太坊 24KB 的合约大小限制。

钻石标准就像一颗多面钻石,每个面(Facet)代表一个功能模块,所有面共同组成一个完整的合约系统。这种设计使得大型复杂的 DeFi 协议能够在单一地址下组织和管理无限的功能。

核心概念

什么是 Diamond Diamond 是一个代理合约,具有以下特点:

  • 单一合约地址
  • 无限的功能扩展能力
  • 模块化的代码组织
  • 灵活的升级机制

什么是 Facet Facet 是实现具体功能的合约:

  • 类似于实现合约或库
  • 每个 Facet 包含一组相关函数
  • 多个 Facet 可以组成完整的业务逻辑
  • 可以独立升级和替换

与传统代理模式的区别

特性 传统代理 Diamond 标准
实现合约数量 1 个 无限个
合约大小限制 24KB 无限制
升级粒度 整个合约 单个函数
代码组织 单体 模块化

核心组件

Diamond 合约 作为代理的主合约:

contract Diamond {
    // 存储 function selector 到 facet 地址的映射
    mapping(bytes4 => address) selectorToFacet;

    // Fallback 函数:路由所有调用
    fallback() external payable {
        address facet = selectorToFacet[msg.sig];
        require(facet != address(0), "Function does not exist");

        // Delegatecall 到对应的 facet
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

DiamondCut 升级功能的核心接口:

interface IDiamondCut {
    enum FacetCutAction { Add, Replace, Remove }

    struct FacetCut {
        address facetAddress;      // Facet 合约地址
        FacetCutAction action;     // 操作类型
        bytes4[] functionSelectors;// 函数选择器列表
    }

    function diamondCut(
        FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata
    ) external;

    event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}

DiamondLoupe 钻石检查接口,用于查询 Diamond 的结构:

interface IDiamondLoupe {
    struct Facet {
        address facetAddress;
        bytes4[] functionSelectors;
    }

    // 返回所有 facet 地址
    function facets() external view returns (Facet[] memory);

    // 返回某个函数选择器对应的 facet
    function facetAddress(bytes4 _functionSelector) external view returns (address);

    // 返回所有函数选择器
    function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory);

    // 返回所有 facet 地址
    function facetAddresses() external view returns (address[] memory);
}

存储模式

Diamond Storage 推荐的存储模式,避免存储冲突:

// 定义存储结构
struct DiamondStorage {
    mapping(address => uint256) balances;
    uint256 totalSupply;
    // ...其他状态变量
}

// 使用固定的存储位置
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");

function diamondStorage() internal pure returns (DiamondStorage storage ds) {
    bytes32 position = DIAMOND_STORAGE_POSITION;
    assembly {
        ds.slot := position
    }
}

// 在 Facet 中使用
contract TokenFacet {
    function transfer(address to, uint256 amount) external {
        DiamondStorage storage ds = diamondStorage();
        require(ds.balances[msg.sender] >= amount, "Insufficient balance");

        ds.balances[msg.sender] -= amount;
        ds.balances[to] += amount;
    }
}

AppStorage 另一种存储模式,所有状态变量在一个结构体中:

struct AppStorage {
    uint256 totalSupply;
    mapping(address => uint256) balances;
    mapping(address => mapping(address => uint256)) allowances;
    // ...所有状态变量
}

// 在所有 Facet 的第一个存储位置
contract BaseFacet {
    AppStorage internal s;
}

工作原理

函数调用流程

  1. 用户调用函数
用户 → Diamond.transfer(to, amount)
  1. Fallback 函数拦截
Diamond 的 fallback 函数被触发
提取 function selector: 0xa9059cbb (transfer 的签名)
  1. 查找对应的 Facet
从 selectorToFacet 映射中查找
找到 TokenFacet 地址
  1. Delegatecall 执行
Diamond → delegatecall → TokenFacet.transfer()
在 Diamond 的存储上下文中执行
  1. 返回结果
执行结果返回给用户

升级操作

添加新功能

// 部署新的 Facet
GovernanceFacet governanceFacet = new GovernanceFacet();

// 准备 DiamondCut
IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
cut[0] = IDiamondCut.FacetCut({
    facetAddress: address(governanceFacet),
    action: IDiamondCut.FacetCutAction.Add,
    functionSelectors: [
        GovernanceFacet.propose.selector,
        GovernanceFacet.vote.selector,
        GovernanceFacet.execute.selector
    ]
});

// 执行升级
diamond.diamondCut(cut, address(0), "");

替换现有功能

// 部署新版本的 Facet
TokenFacetV2 newTokenFacet = new TokenFacetV2();

cut[0] = IDiamondCut.FacetCut({
    facetAddress: address(newTokenFacet),
    action: IDiamondCut.FacetCutAction.Replace,
    functionSelectors: [TokenFacet.transfer.selector]
});

diamond.diamondCut(cut, address(0), "");

删除功能

cut[0] = IDiamondCut.FacetCut({
    facetAddress: address(0),
    action: IDiamondCut.FacetCutAction.Remove,
    functionSelectors: [OldFeature.deprecatedFunction.selector]
});

diamond.diamondCut(cut, address(0), "");

初始化数据

// 部署初始化合约
InitContract init = new InitContract();

// 升级时调用初始化
diamond.diamondCut(
    cut,
    address(init),
    abi.encodeWithSelector(InitContract.init.selector, constructorArgs)
);

关键优势

突破合约大小限制

  • 以太坊合约大小限制:24KB
  • Diamond 可以有无限个 Facet
  • 每个 Facet 最大 24KB
  • 总功能大小:无限制

细粒度升级

  • 只升级需要修改的函数
  • 不影响其他功能
  • 降低升级风险
  • 节省 Gas 费用

单一地址

  • 用户和集成者只需记住一个地址
  • 简化前端集成
  • 便于品牌识别
  • 简化授权管理

模块化架构

  • 代码组织清晰
  • 功能模块独立
  • 便于团队协作
  • 易于维护和测试

透明升级历史

  • 每次升级触发 DiamondCut 事件
  • 完整的升级日志
  • 可追溯的变更历史
  • 提高透明度

应用场景

大型 DeFi 协议 像 Aave、Compound 这样的复杂协议:

  • 借贷功能
  • 治理功能
  • 清算功能
  • 奖励分配
  • 每个模块都可以是独立的 Facet

游戏合约 链游需要大量功能:

  • 角色管理
  • 物品系统
  • 战斗逻辑
  • 交易市场
  • 每个系统独立升级

DAO 基础设施 去中心化组织的合约:

  • 提案系统
  • 投票机制
  • 财库管理
  • 成员管理
  • 模块化扩展

NFT 平台 复杂的 NFT 协议:

  • 铸造逻辑
  • 市场交易
  • 版税分配
  • 元数据管理
  • 灵活扩展

知名实现

Aavegotchi GameFi 项目,使用 Diamond 标准:

  • 游戏逻辑模块化
  • 持续功能迭代
  • 保持单一合约地址

Louper Diamond 浏览器和开发工具:

  • 可视化 Diamond 结构
  • 查看所有 Facet
  • 追踪升级历史

开发工具

diamondABI 自动生成 Diamond 的完整 ABI

const diamond = await ethers.getContractAt('DiamondFullABI', diamondAddress)
// 可以调用所有 Facet 的函数

hardhat-deploy 支持 Diamond 部署和升级:

await deploy('Diamond', {
    from: deployer,
    diamondCut: facetCuts,
})

louper.dev 在线 Diamond 浏览器:

  • 输入 Diamond 地址
  • 查看所有 Facet 和函数
  • 追踪升级历史

安全考虑

升级权限管理

// 使用 AccessControl
modifier onlyOwner() {
    require(msg.sender == owner, "Not authorized");
    _;
}

function diamondCut(...) external onlyOwner {
    // 升级逻辑
}

存储冲突

  • 使用 Diamond Storage 模式
  • 每个数据结构使用唯一的存储位置
  • 避免 Facet 之间的存储覆盖

函数选择器冲突

  • 确保不同 Facet 没有相同的函数签名
  • DiamondCut 应该检测冲突
  • 使用工具自动检查

初始化安全

bool private _initialized;

function init() external {
    require(!_initialized, "Already initialized");
    _initialized = true;
    // 初始化逻辑
}

最佳实践

模块划分

  • 按功能领域划分 Facet
  • 每个 Facet 职责单一
  • 避免 Facet 过大
  • 保持代码整洁

升级策略

  • 使用时间锁延迟升级
  • 多签控制升级权限
  • 提供紧急暂停机制
  • 充分测试升级流程

事件记录

event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);

// 记录所有重要操作
emit FunctionAdded(selector, facetAddress);

文档维护

  • 记录每个 Facet 的功能
  • 说明升级历史
  • 提供集成指南
  • 维护 ABI 文档

局限性

复杂性

  • 学习曲线陡峭
  • 开发和调试更复杂
  • 需要专业知识

Gas 成本

  • Delegatecall 有额外开销
  • 函数查找需要成本
  • 比直接调用稍贵

生态支持

  • 工具支持相对较少
  • 审计经验有限
  • 社区资源较少

审计挑战

  • 需要审计整个 Diamond 系统
  • 升级可能引入新漏洞
  • 持续安全监控

未来展望

Diamond 标准正在成为大型复杂协议的首选架构:

  • 更多 DeFi 协议采用
  • 工具生态不断完善
  • 最佳实践逐渐形成
  • 审计标准化

相关链接