以“代理模式”构建可升级合约(代理合约将调用转发给实施合约)现在已成为行业标准。
ERC-2535 Diamonds等标准引入了使用多项实现智能合约的代理合约概念,其中代理从多项实现合约中选择一个来委托调用。
本提案标准化了如何编写“具有多种实现的代理”或“一对多”代理合约,同时考虑到客户端友好性和采用性。具体而言,本提案标准化了一对多代理应如何公开其真实 ABI,以便通过客户端直接进行合约交互。
展望未来,我们将这种一对多的代理合约称为动态合约。
动机
本提案并未引入任何编写智能合约的新技术。动态合约或“一对多”代理合约在之前的 EIP 标准或自定义实现中已经引入。
该提案旨在通过使动态合约易于掌握且易于通过客户端直接交互来推动动态合约的采用。
ERC-2535 详细记录了构建动态合约的优势。我们在此重申其中的一些优势:
合约代码大小限制(EIP-170)不再是构建功能齐全的智能合约的限制。
对智能合约进行每个功能级别的升级。
重新使用已部署的合约作为实现
显然,这种编写智能合约的模式有其优势。本提案的动机远不止这些优势。
1. 可理解性
作为一种软件,智能合约已成为面向公众的后端。在区块扫描仪上发现、理解或验证智能合约代码对于开发人员,甚至是高级区块链应用程序的用户来说都是第二天性。
该提案涵盖了智能合约的这一方面,并引入了一种易于理解的编写动态合约的模式,没有多余的术语或复杂性。
2. 客户友好度
智能合约的生命周期包括有人构建客户端来与之交互。构建客户端(图形或程序)来与智能合约交互需要 ABI,以及对 ABI 进行明确定义的解释来指导交互。
该提案重点关注客户端友好性。具体来说,这意味着应该可以直接构建客户端来与动态合约进行交互。
规格
本文档中的关键词“必须”、“不得”、“要求”、“应”、“不应”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。
路由器
每个符合ERC-7504 的合约都必须实现以下Router
接口:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;/** * @title ERC-7504 Dynamic Contracts. * @dev Fallback function delegateCalls `getImplementationForFunction(msg.sig)` for a given incoming call. * NOTE: The ERC-165 identifier for this interface is 0xce0b6013. */interface Router { /** * @notice delegateCalls the appropriate implementation address for the given incoming function call. * @dev The implementation address to delegateCall MUST be retrieved from calling `getImplementationForFunction` with the * incoming call's function selector. */ fallback() external payable; /// @dev Returns the implementation address to delegateCall for the given function selector. function getImplementationForFunction(bytes4 _functionSelector) external view returns (address);}
回退函数必须使用“msg.sig”(即calldata的前四个字节)进行调用getImplementationForFunction
,并对返回值执行delegateCall。
以下是Router的参考实现:
// SPDX-License-Identifier: MIT// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts)pragma solidity ^0.8.0;abstract contract DynamicContract is Router { /// @dev delegate calls the appropriate implementation smart contract for a given function. fallback() external payable virtual { address implementation = getImplementationForFunction(msg.sig); _delegate(implementation); } /// @dev delegateCalls an `implementation` smart contract. function _delegate(address implementation) internal virtual { assembly { // Copy msg.data. We take full control of memory in this inline assembly // block because it will not return to Solidity code. We overwrite the // Solidity scratch pad at memory position 0. calldatacopy(0, 0, calldatasize()) // Call the implementation. // out and outsize are 0 because we don't know the size yet. let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) // Copy the returned data. returndatacopy(0, 0, returndatasize()) switch result // delegatecall returns 0 on error. case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } /// @dev Unimplemented. Returns the implementation contract address for a given function selector. function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address);}
路由器状态
每个符合ERC-7504 的合约都必须实现以下RouterState
接口:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;/** * @title ERC-7504 Dynamic Contracts. * NOTE: The ERC-165 identifier for this interface is 0x4a00cc48. */interface RouterState /* is Router */ { /*/////////////////////////////////////////////////////////////// Structs //////////////////////////////////////////////////////////////*/ /** * @notice An extension's metadata. * * @param name The unique name of the extension. * @param metadataURI The URI where the metadata for the extension lives. * @param implementation The implementation smart contract address of the extension. */ struct ExtensionMetadata { string name; string metadataURI; address implementation; } /** * @notice An interface to describe an extension's function. * * @param functionSelector The 4 byte selector of the function. * @param functionSignature Function signature as a string. E.g. "transfer(address,address,uint256)" */ struct ExtensionFunction { bytes4 functionSelector; string functionSignature; } /** * @notice An interface to describe an extension. * * @param metadata The extension's metadata; it's name, metadata URI and implementation contract address. * @param functions The functions that belong to the extension. */ struct Extension { ExtensionMetadata metadata; ExtensionFunction[] functions; } /*/////////////////////////////////////////////////////////////// View Functions //////////////////////////////////////////////////////////////*/ /// @dev Returns all extensions of the Router. function getAllExtensions() external view returns (Extension[] memory allExtensions);}
返回值getAllExtensions
包括合约上所有可调用函数,按实现它们的合约分组。
对于契约上给定的可调用函数,getImplementationForFunction
其函数选择器的返回值必须与为其表达的实现契约的返回值相匹配getAllExtensions
。
概念
“可升级智能合约”实际上是将两种智能合约合并为一个系统:
代理智能合约:我们关注其状态/存储的智能合约。
实施智能合约:无状态智能合约,定义代理智能合约状态如何变异的逻辑。
代理合约的作用是将其收到的所有调用通过 转发给实施合约delegateCall
。简而言之,代理合约存储状态,并始终询问实施合约如何改变其状态(在收到调用时)。
该提案引入了Router
智能合约。

delegateCall 并非总是委托相同的实现契约,而是Router
为其接收的特定函数调用委托特定的实现契约(即“扩展”)。
路由器存储从函数选择器到实现给定函数的实现合约的映射。“升级合约”现在仅意味着更新给定函数或函数映射到的实现合约。
基本原理
路由器
所谓“路由器”,是指实现接口的契约Router
。
我们将这样的合约称为“路由器”,因为它的主要且唯一的工作是将传入的函数调用路由到正确的实现合约。
调用非固定函数时会触发 fallback 函数(请参阅“原理”部分下的“固定函数”)。fallback 函数使用 calldata ( msg.sig
) 的前四个字节以及getImplementationForFunction
函数来确定应在哪个实现合约上执行委托调用。
路由器是一种代理合约。
代理合约是一种委托调用实现合约来指示其自身状态应如何改变的合约。这就是路由器的作用。
与代理契约的常见关联是委托调用单一实施契约的最小代理契约。
路由器合约可以在有或没有这种最小代理合约的情况下使用。也就是说,路由器合约可以直接部署和使用,而无需将其置于最小代理之后。此外,路由器合约也可以通过将其置于最小代理之后来使用。
本提案不对此提出任何建议。无论哪种使用形式,均保持标准的实用性。
固定函数
动态合约可能包含固定功能。
严格来说,动态合约中的“固定函数”是指在调用时不会触发回退函数的函数。
固定函数是顶级路由器合约本身实现的函数,而不是任何必须委托调用的实现合约中的函数。固定函数无法升级或删除。
该提案要求动态合约中有2个固定函数:Router.getImplementationForFunction
和RouterState.getAllExtensions
。
这保证了合约公开其 ABI 的系统不会改变。
RouterState、扩展和构建 ABI
界面RouterState
是使动态合约客户友好的核心。
构建一个客户端(图形或程序)来与智能合约交互,需要 ABI,以及对 ABI 的明确定义的解释来指导交互。
动态合同在这方面带来两个直接的挑战:
动态合约的可调用函数可能会发生变化。这对客户来说是一个挑战,因为他们无法总是预期相同的 ABI 适用于动态合约。
动态合约的可调用函数分布在许多实现合约中。
接口RouterState
创建了一个我们称之为的自然分组Extension
。在任何给定点,动态契约都知道一组固定的扩展。
Extension
是动态合约所使用的实施合约的抽象。
该Extension
结构的设计是为了让智能合约和客户端能够互相帮助。具体来说,该设计旨在为智能合约和客户端之间的协调创造空间。
命名扩展允许双方创建一个连贯的概念分组,以确定哪些功能在哪个实现智能合约中共存。
扩展的元数据 URI 允许智能合约和客户端之间进行特定协调,以了解如何理解、交互或呈现路由器给定扩展的 UI。
通过存储扩展实现的每个函数的函数选择器和函数签名,可以为动态契约构建精确的联合 ABI。
通过扩展对函数进行分组,RouterState.getAllExtensions
函数可以充当返回动态合约 ABI 的固定函数。
返回的扩展列表getAllExtensions
与的返回值相符getImplementationForFunction
。因此,这两个函数结合在一起创建了一个可靠的来源来获取和构建动态合约的 ABI。
函数选择器冲突
当同一个动态合约使用的两个单独的实现合约实现相同的函数时会发生什么?(相同的函数签名,即使函数主体不同)
该提案要求 fallback 函数必须始终对 的返回值执行 delegateCall getImplementationForFunction(msg.sig)
。
即使在上述情况下,也必须满足这一要求。本提案不要求处理该情况的具体方式。
也就是说,对于给定的实施合约,该列表没有必要Extension.functions
是实施合约所有可调用函数的详尽列表。
因此,如果两个实施合约实现了相同的功能,那么Extension.functions
其中一个实施合约的列表可以简单地省略冲突的功能。
我们在“意外升级”下的安全注意事项部分中进一步扩展了函数选择器冲突。
适配器
该提案的重点是动态合同的可理解性和客户友好性。
以 ERC-2535 或ERC-6900等标准编写的合约(同样侧重于动态合约)或“一对任意”代理合约可以支持该标准。
例如,可以编写一个实现RouterState
接口的“适配器”智能合约,然后将其作为一个方面添加到 ERC-2535 Diamond 中。
向后兼容性
未发现向后兼容性问题。
参考实现
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;/// NOTE: This is a naive reference implementation. It is not intended to be used in production.import "./RouterState.sol"; // interface RouterState is Router { /* ... */}contract DynamicContract is RouterState { string[] private names; mapping(string => Extension) private allExtensions; mapping(bytes4 => ExtensionMetadata) private extensionMetadataForFunction; constructor(Extension[] memory _extensions) { for (uint256 i = 0; i < _extensions.length; i++) { Extension memory extension = _extensions[i]; names.push(extension.metadata.name); allExtensions[extension.metadata.name] = extension; for (uint256 j = 0; j < extension.functions.length; j++) { ExtensionFunction memory extFunction = extension.functions[j]; extensionMetadataForFunction[extFunction.functionSelector] = extension.metadata; } } } fallback() external payable virtual { /// @dev delegate calls the appropriate implementation smart contract for a given function. address implementation = getImplementationForFunction(msg.sig); _delegate(implementation); } /// @dev delegateCalls an `implementation` smart contract. function _delegate(address implementation) internal virtual { assembly { // Copy msg.data. We take full control of memory in this inline assembly // block because it will not return to Solidity code. We overwrite the // Solidity scratch pad at memory position 0. calldatacopy(0, 0, calldatasize()) // Call the implementation. // out and outsize are 0 because we don't know the size yet. let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) // Copy the returned data. returndatacopy(0, 0, returndatasize()) switch result // delegatecall returns 0 on error. case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } /// @dev Returns the implementation contract address for a given function signature. function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address) {} /// @dev Returns all extensions of the Router. function getAllExtensions() external view returns (Extension[] memory extensions) { extensions = new Extension[](names.length); for (uint256 i = 0; i < names.length; i++) { extensions[i] = allExtensions[names[i]]; } }}
安全注意事项
意外升级
动态合约的“升级”意味着改变给定函数选择器映射到的实现地址。
不鼓励直接更新/覆盖函数映射到的实现地址,因为这可能会导致意外升级。
本提案建议如下:
动态合约应该只允许将函数选择器映射到实现地址一次/当函数选择器未映射到不同的实现地址(零地址除外)时。
例子:
假设ERC-721 NFT 合约是一个动态合约,其所有可调用的 ERC-721 函数都映射到Extension_A.metadata.implementation
。
如果我们要升级approve
要映射到的函数Extension_B
,那么动态合约应该只允许在该approve
函数不再映射到时进行此升级Extension_A.metadata.implementation
。
具体来说,这意味着approve
函数必须首先在一次调用中映射到零地址,然后Extension_B.metadata.implementation
在后续调用中映射到零地址。这些调用可以是多调用的一部分,因此可以在同一事务中处理。但是,鼓励维护这两个单独的调用,以确保升级是有意为之。
在这个例子中,不需要改变其余 ERC-721 函数的映射方式Extension_A.metadata.implementation
。
函数实现包括selfdestruct
当使用指向路由器的最小代理契约作为其单一实现契约时,路由器中的扩展函数必须包含 selfdestruct。对此类扩展函数的 delegateCall 会破坏实现契约(即路由器),并导致委托给它的任何代理失去所有功能。
直接使用路由器时(即不将其置于最小代理合约之后),路由器中的扩展函数中不应包含 selfdestruct,这一点也很重要。对此类扩展函数的 delegateCall 会破坏路由器合约。
权限
动态合约的“升级”意味着改变给定函数选择器映射到的实现地址。