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;
}
工作原理
函数调用流程
- 用户调用函数
用户 → Diamond.transfer(to, amount)
- Fallback 函数拦截
Diamond 的 fallback 函数被触发
提取 function selector: 0xa9059cbb (transfer 的签名)
- 查找对应的 Facet
从 selectorToFacet 映射中查找
找到 TokenFacet 地址
- Delegatecall 执行
Diamond → delegatecall → TokenFacet.transfer()
在 Diamond 的存储上下文中执行
- 返回结果
执行结果返回给用户
升级操作
添加新功能
// 部署新的 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 基础设施 去中心化组织的合约:
- 提案系统
- 投票机制
- 财库管理
- 成员管理
- 模块化扩展
- 铸造逻辑
- 市场交易
- 版税分配
- 元数据管理
- 灵活扩展
知名实现
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 协议采用
- 工具生态不断完善
- 最佳实践逐渐形成
- 审计标准化