ERC-7579: 最小模块化智能账户
模块化智能账户接口和行为,用于与账户和模块的最小限制实现互操作性
Authors | zeroknots (@zeroknots), Konrad Kopp (@kopy-kat), Taek Lee (@leekt), Fil Makarov (@filmakarov), Elim Poon (@yaonam), Lyu Min (@rockmin216) |
---|---|
Created | 2023-12-14 |
Discussion Link | https://ethereum-magicians.org/t/erc-7579-minimal-modular-smart-accounts/17336 |
Requires | EIP-165, EIP-1271, EIP-2771, EIP-4337 |
摘要
本提案概述了模块化智能账户和模块所需的最少接口和行为,以确保跨实现的互操作性。 对于账户,该标准规定了执行、配置和回退接口,以及对 ERC-165 和 ERC-1271 的合规性。 对于模块,该标准规定了核心接口、模块类型和特定于类型的接口。
动机
合约账户正在被广泛采用,许多账户都是使用模块化架构构建的。 这些模块化合约账户(以下简称智能账户)将功能转移到外部合约(模块)中,以提高创新速度和潜力、面向未来,并允许开发者和用户进行自定义。 然而,目前这些智能账户的构建方式差异很大,导致模块碎片化和供应商锁定。 将智能账户标准化对生态系统非常有益,原因如下:
- 模块在不同智能账户之间使用的互操作性
- 智能账户在不同钱包应用程序和 SDK 之间使用的互操作性
- 防止智能账户用户的严重供应商锁定
然而,非常重要的是,这种标准化对账户的实现逻辑影响最小,以便智能账户供应商可以继续创新,同时允许蓬勃发展的、多账户兼容的模块生态系统。 因此,本标准的目标是定义智能账户和模块接口和行为,使其尽可能最小化,同时确保账户和模块之间的互操作性。
规范
本文档中的关键词“必须 (MUST)”,“禁止 (MUST NOT)”,“需要 (REQUIRED)”,“应该 (SHALL)”,“不应该 (SHALL NOT)”,“推荐 (RECOMMENDED)”,“不推荐 (NOT RECOMMENDED)”,“可以 (MAY)”,和“可选 (OPTIONAL)”按照 RFC 2119 和 RFC 8174 中的描述进行解释。
定义
- 智能账户 - 具有模块化架构的智能合约账户。
- 模块 - 具有自包含智能账户功能的智能合约。
- 验证器 (Validator):一种在验证阶段使用的模块,用于确定交易是否有效,是否应该在账户上执行。
- 执行器 (Executor):一种可以通过回调代表智能账户执行交易的模块。
- 回退处理程序 (Fallback Handler):一种可以扩展智能账户回退功能的模块。
- EntryPoint - 根据 ERC-4337 规范的可信单例合约。
- 验证 (Validation) - 用于确定是否应该在账户上执行的任何功能。 当使用 ERC-4337 时,此函数将为
validateUserOp
。 - 执行 (Execution) - 用于执行来自或在用户账户上的操作的任何功能。 当使用 ERC-4337 时,这将由 EntryPoint 使用
userOp.callData
调用。
账户
验证
本标准不规定如何实现验证器选择。 然而,如果智能账户在传递给验证器的数据字段中编码验证器选择机制(例如,如果与 ERC-4337 一起使用,则在 userOp.signature
中),则智能账户必须在调用验证器之前清理受影响的值。
智能账户的验证函数应该返回验证器的返回值。
执行行为
为了符合本标准,智能账户必须实现以下执行接口:
interface IERC7579Execution {
/**
* @dev 代表账户执行交易。 可以是 payable 的。
* @param mode 交易的编码执行模式。
* @param executionCalldata 编码的执行调用数据。
*
* 必须确保足够的授权控制:例如,如果与 ERC-4337 一起使用,则 onlyEntryPointOrSelf
* 如果请求的模式不受账户支持,则必须 revert
*/
function execute(bytes32 mode, bytes calldata executionCalldata) external;
/**
* @dev 代表账户执行交易。 可以是 payable 的。
* 此函数旨在由执行器模块调用
* @param mode 交易的编码执行模式。
* @param executionCalldata 编码的执行调用数据。
*
* @return returnData 包含每个已执行子调用的返回数据的数组
*
* 必须确保足够的授权控制:即 onlyExecutorModule
* 如果请求的模式不受账户支持,则必须 revert
*/
function executeFromExecutor(bytes32 mode, bytes calldata executionCalldata)
external
returns (bytes[] memory returnData);
}
账户还可以根据 ERC-4337 实现以下函数:
/**
* @dev 根据 ERC-4337 v0.7 的 ERC-4337 executeUserOp
* 此函数旨在由 ERC-4337 EntryPoint.sol 调用
* @param userOp PackedUserOperation struct (参见 ERC-4337 v0.7+)
* @param userOpHash PackedUserOperation struct 的哈希值
*
* 必须确保足够的授权控制:即 onlyEntryPoint
*/
function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external;
如果账户选择实现 executeUserOp
,则此方法应该确保账户执行 userOp.calldata
,除了前 4 个最高有效字节,根据 ERC-4337,这些字节保留给 executeUserOp.selector
。 因此,userOp.callData[4:]
应该表示对账户的有效调用的 calldata。 推荐账户执行 delegatecall
,以便将原始 msg.sender
保留到账户。
示例:
(bool success, bytes memory innerCallRet) = address(this).delegatecall(userOp.callData[4:]);
执行模式是一个 bytes32
值,其结构如下:
- callType(1 字节):
0x00
表示单个call
,0x01
表示批量call
,0xfe
表示staticcall
,0xff
表示delegatecall
- execType(1 字节):
0x00
表示执行失败时 revert,0x01
表示执行失败时不 revert 但实现某种形式的错误处理 - unused(4 字节):此范围保留供将来标准化
- modeSelector(4 字节):一个额外的模式选择器,可用于创建更多执行模式
- modePayload(22 字节):要传递的额外数据
以下是执行模式的可视化表示:
CallType | ExecType | Unused | ModeSelector | ModePayload |
---|---|---|---|---|
1 byte | 1 byte | 4 bytes | 4 bytes | 22 bytes |
账户不需要实现所有执行模式。 账户必须在 supportsExecutionMode
中声明支持哪些模式(见下文),如果请求的模式不受账户支持,则账户必须 revert。
账户必须按以下方式编码执行数据:
- 对于单个调用,
target
、value
和callData
按此顺序打包(即 Solidity 中的abi.encodePacked
)。 - 对于 delegatecall,
target
和callData
按此顺序打包(即 Solidity 中的abi.encodePacked
)。 - 对于批量调用,
targets
、values
和callDatas
被放入一个Execution
结构数组中,该结构按此顺序包含这些字段(即Execution(address target, uint256 value, bytes memory callData)
)。 然后,使用填充对该数组进行编码(即 Solidity 中的abi.encode
)。
账户配置
为了符合本标准,智能账户必须实现以下账户配置接口:
interface IERC7579AccountConfig {
/**
* @dev 返回智能账户的账户 ID
* @return accountImplementationId 智能账户的账户 ID
*
* 必须返回一个非空字符串
* accountId 的结构应该如下所示:
* "vendorname.accountname.semver"
* 该 ID 应该在所有智能账户中都是唯一的
*/
function accountId() external view returns (string memory accountImplementationId);
/**
* @dev 检查账户是否支持某种执行模式的函数(见上文)
* @param encodedMode 编码的模式
*
* 如果账户支持该模式,则必须返回 true,否则返回 false
*/
function supportsExecutionMode(bytes32 encodedMode) external view returns (bool);
/**
* @dev 检查账户是否支持某种 moduleTypeId 的函数
* @param moduleTypeId 根据 ERC-7579 规范的模块类型 ID
*
* 如果账户支持该模块类型,则必须返回 true,否则返回 false
*/
function supportsModule(uint256 moduleTypeId) external view returns (bool);
}
模块配置
为了符合本标准,智能账户必须实现以下模块配置接口。
在存储已安装的模块时,智能账户必须确保有一种方法可以区分模块类型。 例如,智能账户应该能够实施访问控制,该控制仅允许已安装的执行器(而不是其他已安装的模块)调用 executeFromExecutor
函数。
interface IERC7579ModuleConfig {
event ModuleInstalled(uint256 moduleTypeId, address module);
event ModuleUninstalled(uint256 moduleTypeId, address module);
/**
* @dev 在智能账户上安装某种类型的模块
* @param moduleTypeId 根据 ERC-7579 规范的模块类型 ID
* @param module 模块地址
* @param initData 在 `onInstall` 初始化期间模块可能需要的任意数据。
*
* 必须实施授权控制
* 如果提供了 `initData` 参数,则必须使用该参数在模块上调用 `onInstall`
* 必须发出 ModuleInstalled 事件
* 如果模块已经安装或模块上的初始化失败,则必须 revert
*/
function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external;
/**
* @dev 卸载智能账户上某种类型的模块
* @param moduleTypeId 根据 ERC-7579 规范的模块类型 ID
* @param module 模块地址
* @param deInitData 在 `onInstall` 初始化期间模块可能需要的任意数据。
*
* 必须实施授权控制
* 必须使用 `deInitData` 参数在模块上调用 `onUninstall`
* 必须发出 ModuleUninstalled 事件
* 如果模块未安装或模块上的反初始化失败,则必须 revert
*/
function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external;
/**
* @dev 返回模块是否安装在智能账户上
* @param moduleTypeId 根据 ERC-7579 规范的模块类型 ID
* @param module 模块地址
* @param additionalContext 确定模块是否已安装可能需要的任意数据
*
* 如果模块已安装,则必须返回 true,否则返回 false
*/
function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) external view returns (bool);
}
钩子
钩子 (Hooks) 是本标准的可选扩展。 智能账户可以使用钩子在智能账户执行单个或批量执行之前和/或之后执行自定义逻辑和检查。 为了符合此可选扩展,智能账户:
- 必须在任何通过 execute 或 executeFromExecutor 的调用或批量调用之前调用一个或多个钩子的
preCheck
函数 - 必须在任何通过 execute 或 executeFromExecutor 的调用或批量调用之后调用一个或多个钩子的
postCheck
函数 - 推荐在执行对
installModule
或uninstallModule
的调用之前和之后调用preCheck
和postCheck
- 推荐在通过其他(自定义)函数调用的执行之前和之后调用
preCheck
和postCheck
ERC-1271 转发
智能账户必须实现 ERC-1271 接口。 isValidSignature
函数调用可以转发到验证器。 如果实现了 ERC-1271 转发,则必须使用 isValidSignatureWithSender(address sender, bytes32 hash, bytes signature)
调用验证器,其中 sender 是对智能账户的调用的 msg.sender
。 如果智能账户在 bytes signature
参数中实现任何验证器选择编码,则智能账户必须在将其转发到验证器之前清理该参数。
智能账户的 ERC-1271 isValidSignature
函数应该返回请求转发到的验证器的返回值。
回退
智能账户可以实现一个回退函数,该函数将调用转发到回退处理程序。
如果智能账户安装了回退处理程序,则:
- 必须使用
call
或staticcall
来调用回退处理程序 - 必须利用 ERC-2771 将原始
msg.sender
添加到发送给回退处理程序的calldata
- 必须根据 calldata 的函数选择器路由到回退处理程序
- 可以实现授权控制,这应该通过钩子完成
如果账户通过回退添加功能,则这些功能应被视为与账户本身实现这些功能相同。 ERC-165 支持(见下文)是这种方法的一个例子。 请注意,仅推荐通过回退实现 view 函数,因为这可以带来更大的可扩展性。 不推荐通过回退实现核心账户逻辑。
ERC-165
智能账户可以实现 ERC-165。 但是,对于每个 revert 而不是实现功能的接口函数,智能账户必须为相应的接口 ID 返回 false
。
模块
本标准将模块分为以下不同的类型,每种类型都有一个唯一的增量标识符,账户、模块和其他实体必须使用该标识符来标识模块类型:
- 验证 (Validation)(类型 ID:1)
- 执行 (Execution)(类型 ID:2)
- 回退 (Fallback)(类型 ID:3)
- 钩子 (Hooks)(类型 ID:4)
注意:单个模块可以是多种类型。
模块必须实现以下接口:
interface IERC7579Module {
/**
* @dev 此函数由智能账户在安装模块期间调用
* @param data 在 `onInstall` 初始化期间模块可能需要的任意数据
*
* 必须在出错时 revert(例如,如果模块已启用)
*/
function onInstall(bytes calldata data) external;
/**
* @dev 此函数由智能账户在卸载模块期间调用
* @param data 在 `onUninstall` 反初始化期间模块可能需要的任意数据
*
* 必须在出错时 revert
*/
function onUninstall(bytes calldata data) external;
/**
* @dev 如果模块是某种类型,则返回布尔值
* @param moduleTypeId 根据 ERC-7579 规范的模块类型 ID
*
* 如果模块是给定的类型,则必须返回 true,否则返回 false
*/
function isModuleType(uint256 moduleTypeId) external view returns(bool);
}
注意:作为多种类型的单个模块可以决定将 moduleTypeId
传递到 onInstall
和/或 onUninstall
方法中的 data
中,因此这些方法能够正确处理各种类型的安装/卸载。
示例:
// Module.sol
function onInstall(bytes calldata data) external {
// ...
(uint256 moduleTypeId, bytes memory otherData) = abi.decode(data, (uint256, bytes));
// ...
}
验证器
验证器必须实现 IERC7579Module
和 IERC7579Validator
接口,并具有模块类型 ID:1
。
interface IERC7579Validator is IERC7579Module {
/**
* @dev 验证 UserOperation
* @param userOp ERC-4337 PackedUserOperation
* @param userOpHash ERC-4337 PackedUserOperation 的哈希
*
* 必须验证签名是 userOpHash 的有效签名
* 如果签名不匹配,应该返回 ERC-4337 的 SIG_VALIDATION_FAILED(而不是 revert)
*/
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256);
/**
* @dev 使用 ERC-1271 验证签名
* @param sender 将 ERC-1271 请求发送到智能账户的地址
* @param hash ERC-1271 请求的哈希
* @param signature ERC-1271 请求的签名
*
* 如果签名有效,则必须返回 ERC-1271 `MAGIC_VALUE`
* 必须不修改状态
*/
function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) external view returns (bytes4);
}
执行器
执行器必须实现 IERC7579Module
接口,并具有模块类型 ID:2
。
回退处理程序
回退处理程序必须实现 IERC7579Module
接口,并具有模块类型 ID:3
。
回退处理程序可以实现授权控制。 实现授权控制的回退处理程序不得依赖 msg.sender
进行授权控制,而必须使用 ERC-2771 _msgSender()
代替。
钩子
钩子必须实现 IERC7579Module
和 IERC7579Hook
接口,并具有模块类型 ID:4
。
interface IERC7579Hook is IERC7579Module {
/**
* @dev 在执行之前由智能账户调用
* @param msgSender 调用智能账户的地址
* @param value 发送到智能账户的值
* @param msgData 发送到智能账户的数据
*
* 可以在 `hookData` 返回值中返回任意数据
*/
function preCheck(address msgSender, uint256 value, bytes calldata msgData) external returns (bytes memory hookData);
/**
* @dev 在执行之后由智能账户调用
* @param hookData `preCheck` 函数返回的数据
*
* 可以验证 `hookData` 以验证 `preCheck` 函数的交易上下文
*/
function postCheck(bytes calldata hookData) external;
}
原理
最小方法
智能账户是一个新概念,我们仍在学习构建它们的最佳方法。 因此,我们不应该对它们的构建方式过于固执己见。 相反,我们应该定义最少的接口,以允许智能账户和模块之间的互操作性,以便在不同的账户实现中使用。
我们的方法是双重的:
- 从已在生产中使用过的现有智能账户以及在它们之间构建互操作性层中吸取经验教训
- 确保接口尽可能最小且对替代架构开放
扩展
虽然我们希望保持最小化,但我们也希望允许创新和固执己见的功能。 其中一些功能可能还需要标准化(原因与核心接口类似),即使并非所有智能账户都会实现它们。 为了确保这是可能的,我们建议将未来的标准化工作作为本标准的扩展进行。 这意味着核心接口不会改变,但可以添加新接口作为扩展。 这些应该作为单独的 ERC 提出,例如标题为“[FEATURE] ERC-7579 的扩展”。
规范
执行模式
账户需要能够以不同的方式执行 calldata。 我们没有为每种执行类型组合定义一个单独的函数,而是决定将执行类型编码为一个 bytes32
值。 这允许更灵活和可扩展的方法,同时也使代码更容易编写、阅读、维护和审计。 如上所述,执行模式由两个字节组成,它们编码调用类型和执行类型。 调用类型涵盖了三种不同的调用方法,即单个调用、批量调用和 delegatecall
(请注意,您可以 delegatecall
到多调用合约以批量 delegatecalls
)。 执行类型涵盖了两种不同的执行类型,即执行失败时 revert 的执行和执行失败时不 revert 但实现某种形式的错误处理的执行。 这允许账户将不相关的执行批量在一起,这样如果一个执行失败,其他执行仍然可以执行。 这两个字节之后是 4 个未使用的字节,这些字节保留供将来标准化,如果需要的话。 紧随其后的是一个 4 字节的项目,它是一个自定义模式选择器,账户可以实现。 这允许账户实现标准未涵盖的自定义执行模式,并且不需要标准化。 此项目长 4 个字节,以确保不同账户供应商之间的冲突阻力,其保证与 Solidity 函数选择器相同。 最后,最后 22 个字节保留给可以传递到账户的自定义数据。 这允许账户传递任何最多 22 个字节的数据,例如一个 2 字节的标志,后跟一个地址,或者指向进一步打包到执行的 calldata 中的数据的指针。 例如,此有效负载可用于传递应在执行之前和/或之后执行的钩子地址。
区分模块类型
在强制执行授权控制时,不区分模块类型可能会出现安全问题。 例如,如果智能账户将验证器和执行器视为相同类型的模块,则它可能允许验证器代表智能账户执行任意交易。
账户 ID
账户配置接口包括一个函数 accountId
,可用于标识账户。 这对于需要确定正在使用的账户类型和版本的前端库尤其有用,以便为未标准化的账户行为实现正确的逻辑。 替代解决方案包括使用类似于 ERC-165 的接口来声明账户的确切差异和支持的功能,或者返回账户 ID 的 keccak 哈希。 但是,第一种解决方案不如账户 ID 灵活,并且需要就一组明确定义的要使用的功能达成一致,而第二种解决方案不如账户 ID 那样易于理解。
依赖 ERC-4337
本标准对验证流程有严格的 ERC-4337 依赖性。 但是,智能账户构建者很可能希望在未来构建不使用 ERC-4337 的模块化账户,例如,rollup 上的原生账户抽象实现。 一旦这种情况开始发生,本标准的拟议升级路径是将 ERC-4337 依赖性移至扩展(即单独的 ERC),并使智能账户可以选择实现它。 如果需要标准化不同账户抽象实现的验证流程,那么这些要求也可以移至单独的扩展中。
从一开始就没有这样做,因为目前正在构建的唯一模块化账户正在使用 ERC-4337。 因此,首先标准化这些账户的接口是有意义的,并且一旦需要,就可以将 ERC-4337 依赖性移至扩展中。 这是为了最大限度地了解当构建在不同的账户抽象实现上时,模块化账户会是什么样子。
向后兼容性
已经部署的智能账户
已经部署的智能账户很可能能够实现本标准。 如果它们作为代理部署,则可以升级到符合本标准的新账户实现。 如果它们作为不可升级的合约部署,则仍然有可能变得合规,例如,如果支持,可以通过添加合规适配器作为回退处理程序来实现。
参考实现
智能账户的完整接口可以在 IMSA.sol
中找到。
安全考虑
需要更多讨论。 一些初步考虑:
- 必须仔细考虑在智能账户上实现
delegatecall
执行。 请注意,实现delegatecall
的智能账户必须确保目标合约是安全的,否则可能会出现安全漏洞。 - 模块上的
onInstall
和onUninstall
函数可能会导致意外的回调(例如,重入)。 账户实现应通过实施足够的保护例程来考虑这一点。 此外,模块可能会恶意地在onUninstall
上 revert,以阻止账户卸载模块并将其从账户中删除。 - 对于一次只有一个模块处于活动状态的模块类型(例如,回退处理程序),除非正确实现,否则在新模块上调用
installModule
将无法正确卸载上一个模块。 如果旧模块然后再次添加剩余状态,这可能会导致意外行为。 - 回退处理程序中不充分的授权控制可能导致未经授权的执行。
- 恶意钩子可能会在
preCheck
或postCheck
上 revert,添加不受信任的钩子可能会导致账户的拒绝服务。 - 目前账户配置函数(例如,
installModule
)是为单次操作设计的。 账户可以允许从address(this)
调用这些函数,从而创建批量配置操作的可能性。 但是,如果账户对这些函数实施更大的授权控制,因为它们更敏感,那么可以通过嵌套对自身调用的配置选项来绕过这些措施。
版权
通过 CC0 放弃版权及相关权利。
Citation
Please cite this document as:
zeroknots (@zeroknots), Konrad Kopp (@kopy-kat), Taek Lee (@leekt), Fil Makarov (@filmakarov), Elim Poon (@yaonam), Lyu Min (@rockmin216), "ERC-7579: 最小模块化智能账户 [DRAFT]," Ethereum Improvement Proposals, no. 7579, December 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7579.