ERC-1538: 透明合约标准
Authors | Nick Mudge <nick@perfectabstractions.com> |
---|---|
Created | 2018-10-31 |
Discussion Link | https://github.com/ethereum/EIPs/issues/1538 |
Table of Contents
已由 EIP-2535 Diamond Standard 替代。
简述
此标准提供了一种合约架构,使可升级合约具有灵活性、大小不受限制且透明。
一个透明合约公开记录对其所做的所有更改的完整历史。
对透明合约的所有更改都以标准格式报告。
摘要
透明合约是一种代理合约设计模式,提供以下功能:
- 一种原子地(同时)添加、替换和删除合约多个函数的方法。
- 标准事件,用于显示合约中添加、替换和删除的函数,以及进行更改的原因。
- 一种标准方式来查询合约,以发现和检索有关其公开的所有函数的信息。
- 解决了 24KB 的最大合约大小限制,使透明合约的最大合约大小实际上不受限制。 此标准使对合约大小的担忧成为过去。
- 使可升级合约在未来可以根据需要变为不可变。
动机
以太坊合约的一个根本好处是其代码是不可变的,因此通过无需信任来获得信任。 如果合约不可能被更改,人们就不必信任他人。
然而,无法更改的无需信任合约的一个根本问题是它们无法更改。
错误
错误和安全漏洞不知不觉地被写入不可变的合约中,从而毁了它们。
改进
不可变的、无需信任的合约无法改进,导致随着时间的推移合约越来越差。
合约标准不断发展,新的标准不断涌现。 随着时间的推移,人们、团体和组织会了解到人们想要什么、什么更好以及接下来应该构建什么。 无法改进的合约不仅阻碍了创建它们的作者,而且阻碍了所有使用它们的人。
可升级合约与中心化私有数据库
为什么要有可升级合约而不是中心化的、私有的、可变的数据库? 以下是一些原因:
- 由于存储数据的开放性和经过验证的代码,因此可以显示可证明的信任历史。
- 由于开放性,不良行为可以在发生时被发现和报告。
- 独立的安全性和领域专家可以审查合约的变更历史,并证明其可信赖的历史。
- 可升级合约有可能变得不可变且无需信任。
- 可升级合约可以具有不可升级的部分,因此是部分不可变且无需信任的。
不可变性
在某些情况下,不可变的、无需信任的合约是合适的。 当合约只需要短时间或者提前知道永远没有理由更改或改进它时,就是这种情况。
中间地带
透明合约在无法改进的不可变无需信任合约和无法信任的可升级合约之间提供了一个中间地带。
目的
- 创建可升级合约,通过显示可证明的信任历史来赢得信任。
- 记录合约的开发,使其开发和变更可证明地公开并可以理解。
- 创建可升级合约,如果需要,可以在未来变得不可变。
- 创建不受最大大小限制的合约。
益处和用例
此标准适用于以下情况:
- 能够原子地(同时)添加、替换或删除合约的多个函数。
- 每次添加、替换或删除函数时,都会通过事件进行记录。
- 通过显示对合约所做的所有更改来随着时间的推移建立信任。
- 无限的合约大小。
- 能够查询有关合约当前支持的函数的信息。
- 一个提供所有所需功能且永远不需要被另一个合约地址替换的合约地址。
- 合约可以在一段时间内可升级,然后变得不可变。
- 向具有“不可更改函数”的合约添加无需信任的保证。
新的软件可能性
此标准支持编写一种合约版本控制软件。
可以编写软件和用户界面来过滤合约地址的 FunctionUpdate
和 CommitMessage
事件。 此类软件可以显示任何实现此标准的合约的完整更改历史。
用户界面和软件还可以使用此标准来协助或自动化合约的更改。
规范
注意: solidity 的
delegatecall
操作码使合约能够执行来自另一个合约的函数,但执行时就好像该函数来自调用合约一样。 本质上,delegatecall
使合约能够“借用”另一个合约的函数。 使用delegatecall
执行的函数会影响调用合约的存储变量,而不是定义函数的合约。
概要
透明合约使用 delegatecode
将对其的函数调用委托或转发到其他合约。
透明合约具有 updateContract
函数,该函数使多个函数可以被添加、替换或删除。
对于每个被添加、替换或删除的函数,都会发出一个事件,以便可以以标准方式跟踪对合约的所有更改。
透明合约是实现并遵守以下设计要点的合约。
术语
- 在此标准中,委托合约是指透明合约回退函数使用
delegatecall
将函数调用转发到的合约。 - 在此标准中,不可更改函数是指直接在透明合约中定义的函数,因此无法被替换或删除。
设计要点
如果合约实现以下设计要点,则该合约是透明合约:
- 透明合约是一个包含回退函数、构造函数和零个或多个直接在其中定义的不可更改函数的合约。
- 透明合约的构造函数将
updateContract
函数与实现 ERC1538 接口的合约相关联。updateContract
函数可以是直接在透明合约中定义的“不可更改函数”,也可以在委托合约中定义。 其他函数也可以在构造函数中与合约关联。 - 在部署透明合约后,可以通过调用
updateContract
函数来添加、替换和删除函数。 updateContract
函数将函数与实现这些函数的合约相关联,并发出记录函数更改的CommitMessage
和FunctionUpdate
事件。- 对于添加、替换或删除的每个函数,都会发出
FunctionUpdate
事件。 对于每次调用updateContract
函数,都会发出一次CommitMessage
事件,并且在发出任何FunctionUpdate
事件之后发出。 updateContract
函数可以在其_functionSignatures
参数中采用多个函数签名的列表,以便同时添加/替换/删除多个函数。- 当在透明合约上调用函数时,如果它是“不可更改函数”,则会立即执行。 否则,将执行回退函数。 回退函数查找与该函数关联的委托合约,并使用
delegatecall
执行该函数。 如果该函数没有委托合约,则执行会恢复。 - 透明合约及其使用的所有委托合约的源代码都是公开可见且经过验证的。
透明合约地址是用户与之交互的地址。 透明合约地址永远不会改变。 只有委托地址可以通过使用 updateContracts
函数来更改。
通常,需要某种身份验证才能从透明合约添加/替换/删除函数,但是身份验证或所有权方案不是此标准的一部分。
示例
这是一个透明合约实现的示例。 请注意,以下示例仅是示例。 它不是标准。 当合约实现并遵守上面列出的设计要点时,它就是一个透明合约。
pragma solidity ^0.5.7;
contract ExampleTransparentContract {
// 合约的所有者
address internal contractOwner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
// 将函数映射到执行这些函数的委托合约
// funcId => 委托合约
mapping(bytes4 => address) internal delegates;
// 将每个函数签名映射到其在 funcSignatures 数组中的位置。
// signature => index+1
mapping(bytes => uint256) internal funcSignatureToIndex;
event CommitMessage(string message);
event FunctionUpdate(bytes4 indexed functionId, address indexed oldDelegate, address indexed newDelegate, string functionSignature);
// 这是一个“不可更改函数”的示例。
// 返回提供的函数签名的委托合约地址
function delegateAddress(string calldata _functionSignature) external view returns(address) {
require(funcSignatureToIndex[bytes(_functionSignature)] != 0, "Function signature not found.");
return delegates[bytes4(keccak256(bytes(_functionSignature)))];
}
// 使用 updateContract 函数添加函数
// 这是一个内部帮助函数
function addFunction(address _erc1538Delegate, address contractAddress, string memory _functionSignatures, string memory _commitMessage) internal {
// 0x03A9BCCF == bytes4(keccak256("updateContract(address,string,string)"))
bytes memory funcdata = abi.encodeWithSelector(0x03A9BCCF, contractAddress, _functionSignatures, _commitMessage);
bool success;
assembly {
success := delegatecall(gas, _erc1538Delegate, add(funcdata, 0x20), mload(funcdata), funcdata, 0)
}
require(success, "Adding a function failed");
}
constructor(address _erc1538Delegate) public {
contractOwner = msg.sender;
emit OwnershipTransferred(address(0), msg.sender);
// 添加 ERC1538 updateContract 函数
bytes memory signature = "updateContract(address,string,string)";
bytes4 funcId = bytes4(keccak256(signature));
delegates[funcId] = _erc1538Delegate;
emit FunctionUpdate(funcId, address(0), _erc1538Delegate, string(signature));
emit CommitMessage("Added ERC1538 updateContract function at contract creation");
// 将“不可更改函数”与此透明合约地址关联
// 避免与委托合约函数发生函数选择器冲突
// 使用 updateContract 函数
string memory functions = "delegateAddress(string)";
addFunction(_erc1538Delegate, address(this), functions, "Associating unchangeable functions");
// 添加 ERC1538Query 接口函数
functions = "functionByIndex(uint256)functionExists(string)delegateAddresses()delegateFunctionSignatures(address)functionById(bytes4)functionBySignature(string)functionSignatures()totalFunctions()";
// "0x01234567891011121314" 是 ERC1538Query 委托合约的示例地址
addFunction(_erc1538Delegate, 0x01234567891011121314, functions, "Adding ERC1538Query functions");
// 此时可以添加其他函数
}
// 使回退函数可支付,使其适用于可支付和不可支付的委托合约函数。
function() external payable {
// 将每个函数调用委托给委托合约
address delegate = delegates[msg.sig];
require(delegate != address(0), "Function does not exist.");
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, delegate, ptr, calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)
switch result
case 0 {revert(ptr, size)}
default {return (ptr, size)}
}
}
}
如上例所示,除非函数直接在透明合约中定义(使其成为不可更改的函数),否则每个函数调用都会委托给委托合约。
构造函数将 updateContract
函数添加到透明合约,然后用于将其他函数添加到透明合约。
每次将函数添加到透明合约时,都会发出事件 CommitMessage
和 FunctionUpdate
,以准确记录添加或替换了哪些函数以及原因。
实现 updateContract
函数的委托合约实现以下接口:
ERC1538 接口
pragma solidity ^0.5.7;
/// @title ERC1538 透明合约标准
/// @dev 必需接口
/// 注意:此接口的 ERC-165 标识符为 0x61455567
interface ERC1538 {
/// @dev 当透明合约中更新一个或一组函数时,会发出此事件。
/// 消息字符串应该简要描述更改以及进行更改的原因。
event CommitMessage(string message);
/// @dev 对于透明合约中更新的每个函数,都会发出此事件。
/// functionId 是函数签名 keccak256 的 bytes4。
/// oldDelegate 是旧委托合约的委托合约地址(如果函数正在被替换或删除)。
/// 如果首次添加函数,则 oldDelegate 为零值 address(0)。
/// newDelegate 是新委托合约的委托合约地址(如果函数正在被首次添加,或者函数正在被替换)。
/// 如果函数正在被删除,则 newDelegate 为零值 address(0)。
event FunctionUpdate(
bytes4 indexed functionId,
address indexed oldDelegate,
address indexed newDelegate,
string functionSignature
);
/// @notice 更新透明合约中的函数。
/// @dev 如果 _delegate 的值为零,则删除 _functionSignatures 中指定的函数。
/// 如果 _delegate 的值为委托合约地址,则 _functionSignatures 中指定的函数将委托给该地址。
/// @param _delegate 要委托到的委托合约地址,或零以删除函数。
/// @param _functionSignatures 一个接一个列出的函数签名列表
/// @param _commitMessage 对更改的简短描述以及进行更改的原因
/// 此消息将传递到 CommitMessage 事件。
function updateContract(address _delegate, string calldata _functionSignatures, string calldata _commitMessage) external;
}
函数签名字符串格式
_functionSignatures
参数的文本格式只是一个函数签名字符串。 例如:"myFirstFunction()mySecondFunction(string)"
这种格式易于解析且简洁。
这是一个调用 updateContract
函数的示例,该函数将 ERC721 标准函数添加到透明合约:
functionSignatures = "approve(address,uint256)balanceOf(address)getApproved(uint256)isApprovedForAll(address,address)ownerOf(uint256)safeTransferFrom(address,address,uint256)safeTransferFrom(address,address,uint256,bytes)setApprovalForAll(address,bool)transferFrom(address,address,uint256)"
tx = await transparentContract.updateContract(erc721Delegate.address, functionSignatures, "Adding ERC721 functions");
删除函数
通过将 address(0)
作为第一个参数传递给 updateContract
函数来删除函数。 传入的函数列表将被删除。
源代码验证
透明合约源代码和委托合约的源代码应通过第三方来源(如 etherscan.io)以可证明的方式进行验证。
函数选择器冲突
当添加到合约的函数与现有函数的四字节哈希值相同时,会发生函数选择器冲突。 这种情况不太可能发生,但应在 updateContract
函数的实现中防止这种情况。 请参阅 ERC1538 的参考实现,以查看如何防止函数冲突的示例。
ERC1538Query
可选地,透明合约的函数签名可以存储在透明合约的数组中,并进行查询以获取透明合约支持哪些函数以及它们的委托合约地址是什么。
以下是用于从透明合约查询函数信息的可选接口:
pragma solidity ^0.5.7;
interface ERC1538Query {
/// @notice 获取透明合约拥有的函数总数。
/// @return 透明合约拥有的函数数量,
/// 不包括回退函数。
function totalFunctions() external view returns(uint256);
/// @notice 获取有关特定函数的信息
/// @dev 如果 `_index` >= `totalFunctions()`,则抛出
/// @param _index 存储在数组中的函数签名的索引位置
/// @return 函数签名、函数选择器和委托合约地址
function functionByIndex(uint256 _index)
external
view
returns(
string memory functionSignature,
bytes4 functionId,
address delegate
);
/// @notice 检查函数是否存在
/// @param 要检查的函数签名
/// @return 如果函数存在,则为 True,否则为 False
function functionExists(string calldata _functionSignature) external view returns(bool);
/// @notice 获取透明合约支持的函数的所有函数签名
/// @return 包含函数签名列表的字符串
function functionSignatures() external view returns(string memory);
/// @notice 获取特定委托合约支持的所有函数签名
/// @param _delegate 委托合约地址
/// @return 包含函数签名列表的字符串
function delegateFunctionSignatures(address _delegate) external view returns(string memory);
/// @notice 获取支持给定函数签名的委托合约地址
/// @param 函数签名
/// @return 委托合约地址
function delegateAddress(string calldata _functionSignature) external view returns(address);
/// @notice 获取有关函数的信息
/// @dev 如果未找到函数,则抛出
/// @param _functionId 要获取信息的函数的 ID
/// @return 函数签名和合约地址
function functionById(bytes4 _functionId)
external
view
returns(
string memory signature,
address delegate
);
/// @notice 获取透明合约使用的所有委托合约地址
/// @return 所有委托合约地址的数组
function delegateAddresses() external view returns(address[] memory);
}
请参阅 ERC1538 的参考实现 以了解其实现方式。
从 delegateFunctionSignatures
和 functionSignatures
函数返回的函数签名列表的文本格式只是一个函数签名字符串。 这是一个此类字符串的示例:"approve(address,uint256)balanceOf(address)getApproved(uint256)isApprovedForAll(address,address)ownerOf(uint256)safeTransferFrom(address,address,uint256)safeTransferFrom(address,address,uint256,bytes)setApprovalForAll(address,bool)transferFrom(address,address,uint256)"
如何部署透明合约
- 创建一个实现 ERC1538 接口的合约并将其部署到区块链。 如果已经有此类合约部署到区块链,则可以跳过此步骤。
- 使用上面给出的回退函数创建透明合约。 透明合约还需要一个添加
updateContract
函数的构造函数。 - 将透明合约部署到区块链。 如果需要,将 ERC1538 委托合约的地址传递给构造函数。
有关这些合约的示例,请参阅参考实现。
依赖于其他委托合约的委托合约的包装合约
在某些情况下,某些委托合约可能需要调用其他委托合约中的外部/公共函数。 解决此问题的一种便捷方法是创建一个包含所需函数的空实现的合约,并在从其他委托合约调用函数的委托合约中导入和扩展此合约。 这样可以使委托合约在无需提供其他委托合约中已给出的函数的实现的情况下进行编译。 这是一种节省 gas 费、防止达到最大合约大小限制以及防止代码重复的方法。 此策略由 @amiromayer 给出。有关更多信息,请参阅他的评论。 解决此问题的另一种方法是使用 assembly 调用其他委托合约提供的函数。
去中心化机构
可以扩展此标准以添加共识功能,例如审批功能,多人调用该功能以在通过 updateContract
函数提交更改之前批准更改。 只有在更改获得完全批准后,更改才会生效。 只有在更改生效时才应发出 CommitMessage
和 FunctionUpdate
事件。
安全注意事项
此标准将所有者称为有权添加/替换/删除可升级合约函数的一个或多个个人。
一般注意事项
可升级合约的所有者有权更改、添加或删除合约数据存储中的数据。 合约的所有者还可以代表任何地址在合约中执行任何任意代码。 所有者可以通过向合约添加他们调用以执行任意代码的函数来执行这些操作。 这通常是可升级合约的问题,并且不是透明合约特有的。
注意: 合约所有权的设计和实现不是此标准的一部分。 此标准和参考实现中给出的示例只是说明如何完成的示例。
不可更改函数
“不可更改函数”是在透明合约本身而不是在委托合约中定义的函数。 透明合约的所有者无法替换这些函数。 不可更改函数的使用是有限的,因为在某些情况下,如果它们读取或写入数据到透明合约的存储中,它们仍然可以被操纵。 从透明合约的存储中读取的数据可能已被合约的所有者更改。 写入透明合约存储的数据可以被合约的所有者撤消或更改。
在某些情况下,不可更改函数会为透明合约添加无需信任的保证。
透明度
实现此标准的合约会在每次添加、替换或删除函数时发出一个事件。 这使人们和软件可以监视对合约的更改。 如果将任何恶意函数添加到合约,都可以看到。 为了符合此标准,透明合约和委托合约的所有源代码必须公开可用并经过验证。
安全和领域专家可以审查任何透明合约的更改历史,以检测任何不当行为的历史。
原理
函数签名字符串而不是 bytes4[] 函数选择器数组
updateContract
函数使用函数签名的 string
列表作为参数,而不是 bytes4[]
函数选择器数组,原因有三个:
- 传入函数签名使
updateContract
的实现能够防止选择器冲突。 - 此标准的主要部分是通过更容易看到随着时间的推移发生了什么变化以及原因,从而使可升级合约更加透明。 当添加、替换或删除函数时,其函数签名包含在发出的 FunctionUpdate 事件中。 这使得编写软件来过滤合约的事件,以便向人们显示随着时间的推移添加/删除和更改了哪些函数,而无需访问合约的源代码或 ABI 相对容易。 如果仅提供四字节函数选择器,则这是不可能的。
- 通过查看透明合约的源代码,无法查看它支持的所有函数。 这就是为什么存在 ERC1538Query 接口的原因,以便人们和软件有一种查找和检查或显示透明合约当前支持的所有函数的方法。 使用函数签名,以便 ERC1538Query 函数可以显示它们。
Gas 消耗考虑
委托函数调用确实有一些 gas 消耗开销。 这可以通过两种方式缓解:
- 委托合约可以很小,从而降低 gas 成本。 因为调用具有许多函数的合约中的函数比具有少量函数的合约花费更多的 gas。
- 因为透明合约没有最大大小限制,所以可以添加 gas 优化函数以用于用例。 例如,有人可以使用透明合约来实现 ERC721 标准,并实现来自 ERC1412 标准 的批量传输函数,以帮助减少 gas(并使批量传输更方便)。
存储
该标准未指定透明合约如何存储或组织数据。 但以下是一些建议:
继承的存储
-
透明合约的存储变量由透明合约源代码和已添加的委托合约源代码中定义的存储变量组成。
-
委托合约可以使用透明合约中存在的任何存储变量,只要它在其中定义了所有存在的存储变量,按照存在的顺序,直到并包括正在使用的那些。
-
委托合约可以创建新的存储变量,只要它以相同的顺序定义了所有透明合约中存在的存储变量。
以下是实现继承存储的一种简单方法:
- 创建一个存储合约,其中包含透明合约和委托合约将使用的存储变量。
- 使委托合约继承存储合约。
- 如果要添加添加新存储变量的新委托合约,则创建一个新的存储合约,该合约添加新的存储变量并从旧的存储合约继承。 将新的存储合约与新的委托合约一起使用。
- 对于每个新的委托合约,重复步骤 2 或 3。
非结构化存储
Assembly 用于在特定存储位置存储和读取数据。 这种方法的一个优点是,如果以前使用的存储位置未被委托合约使用,则不必在其中定义或提及它们。
永久存储
可以使用基于数据类型的通用 API 存储数据。有关更多信息,请参阅 ERC930。
变得不可变
可以使透明合约变得不可变。 这是通过调用 updateContract
函数来删除 updateContract
函数来完成的。 这样就不再可能添加、替换和删除函数了。
函数的版本
软件或用户可以通过获取函数的委托合约地址来验证调用了哪个版本的函数。 如果实现了 ERC1538Query 接口,则可以通过从该接口调用 delegateAddress
函数来完成此操作。 此函数使用函数签名作为参数,并返回实现该函数的委托合约地址。
最佳实践、工具和更多信息
需要开发和发布更多有关透明合约及其使用的信息、工具、教程和最佳实践。
以下是有关透明合约及其使用的文章的不断增长的列表。 如果您有关于透明合约的文章想要分享,请向此问题提交评论以将其添加。
灵感
该标准的灵感来自于 ZeppelinOS 对 使用 vtable 的可升级性 的实现。
该标准还受到了 Mokens 项目 中 Mokens 合约 的设计和实现的启发。 Mokens 合约已 升级为实现此标准。
向后兼容性
此标准使合约与未来的标准和功能兼容,因为可以添加新函数,并且可以替换或删除现有函数。
此标准使合约具有面向未来的特性。
实现
此标准的参考实现在 transparent-contracts-erc1538 存储库中给出。
版权
版权和相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Nick Mudge <nick@perfectabstractions.com>, "ERC-1538: 透明合约标准 [DRAFT]," Ethereum Improvement Proposals, no. 1538, October 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1538.