ERC-4494: 用于 ERC-721 NFT 的 Permit
用于 ERC-721 NFT 的 ERC-712 签名授权
Authors | Simon Fremaux (@dievardump), William Schwab (@wschwab) |
---|---|
Created | 2021-11-25 |
Discussion Link | https://ethereum-magicians.org/t/eip-extending-erc2612-style-permits-to-erc721-nfts/7519/2 |
Requires | EIP-165, EIP-712, EIP-721 |
摘要
ERC-2612 中概述的 “Permit” 授权流程已被证明是 UX 方面非常有价值的进步,它为 ERC20 代币创建了无 gas 授权。此 EIP 将该模式扩展到 ERC-721 NFT。此 EIP 大量借鉴了 ERC-2612。
由于 ERC-20 和 ERC-721 代币之间的结构差异,这需要一个单独的 EIP。虽然 ERC-20 许可使用价值(被批准的 ERC-20 代币的数量)和基于所有者地址的 nonce,但 ERC-721 许可侧重于 NFT 的 tokenId
,并根据 NFT 的转移递增 nonce。
动机
ERC-2612 中概述的 permit 结构允许使用签名消息(结构如 ERC-712 中所述)来创建授权。正常的基于授权的拉取流程通常涉及两个交易,一个用于授权合约,第二个用于合约拉取资产,这是一种糟糕的 UX,并且经常使新用户感到困惑,而 permit 样式的流程只需要签署消息和一个交易。ERC-2612 中可以找到更多信息。
ERC-2612 仅概述了 ERC-20 代币的 permit 架构。此 ERC 提出了 ERC-721 NFT 的架构,其中还包含一个 approve 架构,该架构将受益于基于签名消息的授权流程。
规范
本文档中“MUST”,“MUST NOT”,“REQUIRED”,“SHALL”,“SHALL NOT”,“SHOULD”,“SHOULD NOT”,“RECOMMENDED”,“MAY”和“OPTIONAL”等关键词应按照 RFC 2119 中的描述进行解释。
必须将三个新函数添加到 ERC-721:
pragma solidity 0.8.10;
import "./IERC165.sol";
///
/// @dev Interface for token permits for ERC-721
///
interface IERC4494 is IERC165 {
/// ERC165 bytes to add to interface array - set in parent contract
///
/// _INTERFACE_ID_ERC4494 = 0x5604e225
/// @notice Function to approve by way of owner signature
// / @notice 通过所有者签名进行批准的函数
/// @param spender the address to approve
/// @param tokenId the index of the NFT to approve the spender on
/// @param deadline a timestamp expiry for the permit
/// @param sig a traditional or EIP-2098 signature
function permit(address spender, uint256 tokenId, uint256 deadline, bytes memory sig) external;
/// @notice Returns the nonce of an NFT - useful for creating permits
// / @notice 返回 NFT 的 nonce - 用于创建 permit
/// @param tokenId the index of the NFT to get the nonce of
/// @return the uint256 representation of the nonce
function nonces(uint256 tokenId) external view returns(uint256);
/// @notice Returns the domain separator used in the encoding of the signature for permits, as defined by EIP-712
// / @notice 返回用于 permit 签名编码的域分隔符,如 EIP-712 所定义
/// @return the bytes32 domain separator
function DOMAIN_SEPARATOR() external view returns(bytes32);
}
其语义如下:
对于所有地址 spender
、uint256
tokenId
、deadline
和 nonce
,以及 bytes
sig
,只要 tokenId
的所有者仍然拥有它,调用 permit(spender, tokenId, deadline, sig)
就 MUST 将 spender
设置为对 tokenId
的已批准,并且 MUST 发出相应的 Approval
事件,仅当满足以下条件时:
- 当前区块时间小于或等于
deadline
tokenId
的所有者不是零地址nonces[tokenId]
等于nonce
sig
是tokenId
所有者的有效secp256k1
或 EIP-2098 签名:keccak256(abi.encodePacked( hex"1901", DOMAIN_SEPARATOR, keccak256(abi.encode( keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"), spender, tokenId, nonce, deadline)) ));
其中
DOMAIN_SEPARATOR
必须按照 EIP-712 进行定义。DOMAIN_SEPARATOR
应该是合约和链独有的,以防止来自其他域的重放攻击,并且满足 EIP-712 的要求,但不受其他约束。DOMAIN_SEPARATOR
的常见选择是:DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), keccak256(bytes(name)), keccak256(bytes(version)), chainid, address(this) ));
换句话说,消息是以下 ERC-712 类型结构:
{ "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "Permit": [ { "name": "spender", "type": "address" }, { "name": "tokenId", "type": "uint256" }, { "name": "nonce", "type": "uint256" }, { "name": "deadline", "type": "uint256" } ], "primaryType": "Permit", "domain": { "name": erc721name, "version": version, "chainId": chainid, "verifyingContract": tokenAddress }, "message": { "spender": spender, "value": value, "nonce": nonce, "deadline": deadline } }}
此外:
- 特定
tokenId
的nonce
(nonces[tokenId]
) 必须在每次tokenId
转移时递增 permit
函数必须检查签名者是否不是零地址
请注意,在此定义中的任何地方,我们都没有提到 msg.sender
。permit
函数的调用者可以是任何地址。
此 EIP 需要 EIP-165。ERC-721 中已经需要 EIP165,但这里还需要它才能注册此 EIP 的接口。这样做将允许轻松验证 NFT 合约是否已实现此 EIP,从而使它们能够相应地进行交互。此 EIP 的接口(如 EIP-165 中定义)为 0x5604e225
。实现此 EIP 的合约在被调用 0x5604e225
时,必须使 supportsInterface
函数返回 true
。
原理
permit
函数足以在不需要额外交易的情况下启用 safeTransferFrom
交易。
该格式避免了对未知代码的任何调用。
给出 nonces
映射是为了防止重放攻击。
permit 的常见用例是中继器代表所有者提交 permit。在这种情况下,中继方实际上被赋予了提交或拒绝 permit 的免费选择权。如果这是一个引起关注的原因,所有者可以通过将 deadline 设置为不久的将来来限制 permit 的有效时间。可以将 deadline 参数设置为 uint(-1) 以创建实际上永不过期的 permit。
包括 ERC-712 类型消息是因为它在 ERC-2612 中的使用,而后者又提到了许多钱包提供商的广泛采用。
虽然 ERC-2612 侧重于被批准的值,但此 EIP 侧重于通过 permit
批准的 NFT 的 tokenId
。这实现了一种 ERC-20(甚至 ERC-1155)代币无法实现的灵活性,从而使单个所有者可以在同一 NFT 上授予多个 permit。这是可能的,因为每个 ERC-721 代币都是离散的(通常被称为非同质化),这允许断言该代币仍然由 owner
简单而确凿地拥有。
虽然 ERC-2612 将签名拆分为其 v,r,s
组件,但此 EIP 选择采用可变长度的 bytes
数组,以便支持 EIP-2098 签名(64 字节),这不能轻易地从 r,s,v
组件(65 字节)中分离或重建。
向后兼容性
已经有一些 ERC-721 合约实现了 permit
样式的架构,最著名的是 Uniswap v3。
它们的实现与此处的规范不同,因为:
permit
架构基于owner
nonce
在创建permit
时递增permit
函数必须由 NFT 所有者调用,该所有者设置为owner
- 签名拆分为
r,s,v
而不是bytes
上面详细介绍了在设计决策上的差异的基本原理。
测试用例
参考实现的基本测试用例可以在此处找到。
一般来说,测试套件应至少断言有关此 EIP 的任何实现的以下内容:
- 每次转移后,nonce 都会递增
permit
在正确的tokenId
上批准spender
- 在 NFT 转移后,无法使用该 permit
- 过期的 permit 不能使用
参考实现
参考实现已在此处设置。
安全注意事项
在创建转移函数时应格外小心,在该函数中,可以在一个函数中使用 permit
和转移函数,以确保无效的 permit 不能以任何方式使用。这对于自动化 NFT 平台尤其重要,在这些平台上,粗心的实现可能会导致大量用户资产的compromise。
其余注意事项已从 ERC-2612 复制并进行了少量修改,并且在此同样相关:
尽管 Permit
的签名者可能心中有一个特定的方来提交他们的交易,但另一方始终可以抢先该交易并在预期方之前调用 permit
。但是,对于 Permit
签名者来说,最终结果是相同的。
由于 ecrecover 预编译会悄无声息地失败,并且在给定格式错误的消息时仅返回零地址作为 signer
,因此务必确保 ownerOf(tokenId) != address(0)
,以避免 permit
创建对任何未设置授权的 tokenId
的授权。
签名 Permit
消息是可审查的。中继方始终可以选择在收到 Permit
后不提交它,从而拒绝提交它的选项。deadline
参数是对此的一种缓解措施。如果签名方持有 ETH,他们也可以自己提交 Permit
,这可能会使先前签署的 Permit
无效。
对于 permit
,标准 ERC-20 授权竞争条件 也适用。
如果 DOMAIN_SEPARATOR
包含 chainId
并且在合约部署时定义而不是为每个签名重建,则在未来链拆分时存在链之间可能发生重放攻击的风险。
版权
通过 CC0 放弃版权及相关权利。
Citation
Please cite this document as:
Simon Fremaux (@dievardump), William Schwab (@wschwab), "ERC-4494: 用于 ERC-721 NFT 的 Permit [DRAFT]," Ethereum Improvement Proposals, no. 4494, November 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4494.