首页>>资讯>>学院

离线授权 NFT EIP-4494:ERC721 -Permit

2024-01-25 14:07:57 280

ERC721-Permit(EIP-4494)让我们避免授权+转账 两步进行转账。


为了真正理解这是如何工作的,我建议你先看以下教程:


ERC721[4]

ERC20-Permit[5]

Solidity中的ecrecover的应用[6]


但我们也会尝试在这里覆盖基础知识。你可能已经熟悉 ERC20-Permit(EIP-2612[7])。它添加了一个新的permit函数。用户可以在链下签署 ERC20 approve交易,生成任何人都可以使用并提交给permit函数的签名。当执行permit时,它将执行approve函数。这允许对 ERC20 转账进行元交易支持,但也简单地摆脱了需要两笔交易的麻烦:授权(approve)和转账。现在,你可以将签名提交给智能合约,该智能合约将在同一笔交易中调用permit,然后调用transferFrom。


ERC721-Permit:防止滥用和重放


我们面临的主要问题是,有效的签名可能会被多次使用,或者在不打算使用的其他地方使用。为了防止这种情况,我们正在添加几个参数。在幕后,我们正在使用已经存在且广泛使用的 EIP-712[8] 标准。


好了,我知道这变得令人困惑,EIP-712 和 EIP-721 在一个标准中,但请跟上我。我们现在将始终将 EIP-721 称为 ERC-721,以使其更容易理解。


1. EIP-712 域哈希


使用 EIP-712,我们为我们的 ERC-721 定义了一个域分隔符。


bytes32 eip712DomainHash = keccak256(

    abi.encode(

        keccak256(

            "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"

        ),

        keccak256(bytes(name())), // ERC-721 Name

        keccak256(bytes("1")),    // Version

        block.chainid,

        address(this)

    )

);


这确保了签名仅用于我们给定的代币合约地址和正确的链 ID。chain ID 是在以太坊经典分叉后引入的,该分叉继续使用网络 ID 1。现有链 ID 的列表可以在此处[9]查看。


2. Permit 哈希结构


现在,我们可以创建一个特定的 Permit 签名:


bytes32 hashStruct = keccak256(

    abi.encode(

        keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"),

        spender,

        tokenId,

        nonces[tokenId],

        deadline

    )

);


这个哈希结构将确保签名仅用于


permit函数

为spender授权

授权给定的tokenId

仅在给定的deadline之前有效

仅对给定的nonce有效


nonce 确保某人无法重放签名,即在同一合约上多次使用它。现在,它基于tokenId的基础工作,而不是 ERC721 所有者地址,正如你所看到的,这是与 ERC20-Permit 的第一个真正的区别。而且nonce 仅在转移 ERC721 时递增,而不是在调用 permit 时递增。为什么呢?


因为使用 Permit 和 NFT,你实际上有一个与 ERC20 不可能的独特功能机会。你可以允许用户为同一tokenId创建多个 spender 地址的许可签名。所有 spender 都可以使用相同的 nonce 执行 permit 函数,因为我们在 permit 内部不递增 nonce。只有当 NFT 实际转移时,nonce 才会递增,使旧的签名无效。因此,请确保在你的 ERC721 合约中为每次转移增加 nonce:


function _transfer(address from, address to, uint256 tokenId) internal override {

    _nonces[tokenId]++;

    super._transfer(from, to, tokenId);

}


3. 最终哈希


现在,我们可以构建以 0x1901 开头的最终签名,用于 EIP-191[10] 兼容的 712 哈希:


bytes32 hash = keccak256(

    abi.encodePacked("\x19\x01", eip712DomainHash, hashStruct)

);


4. 验证签名


使用此哈希,我们可以使用 ecrecover[11] 来检索函数的签名者。但现在我们来到了与 ERC20-Permit 的下一个重大区别。这里的签名是一个哈希,而不是通常使用的 v、r、s 签名值。我们将在一秒钟内了解原因,但首先在正常情况下,你需要使用汇编来恢复 v、r、s 值,然后将它们用于 ecrecover:


bytes32 r;

bytes32 s;

uint8 v;

assembly {

    r := mload(add(signature, 0x20))

    s := mload(add(signature, 0x40))

    v := byte(0, mload(add(signature, 0x60)))

}


address signer = ecrecover(hash, v, r, s);

require(signer == owner, "ERC20Permit: invalid signature");

require(signer != address(0), "ECDSA: invalid signature");


无效的签名将产生一个空地址,这就是最后一个检查的目的。那么为什么我们将签名作为字符串传递,而不是作为 v、r、s 值呢?将签名作为字符串传递实际上允许最大的灵活性。如果签名只是一个字符串,那么将其验证扩展到不仅仅是常规的 ecrecover 检查是相当容易的。实际上,在 ERC721-Permit 中已经包括了两个额外的签名验证:


EIP-1271[12]

EIP-2098[13]


EIP-2098是签名的紧凑形式标准。它利用了数学技巧表示椭圆曲线上的签名。这里的细节并不重要,但基本上,与要求 65 字节的签名相比,你可以用 64 字节表示它。但它需要稍有不同的签名验证。但无论你使用 64 还是 65 字节,显然在这两种情况下,你都可以将签名表示为字符串。太好了。


然后EIP-1271,我们已经在这里[14]看过了用于元交易。但它本质上是智能合约创建签名的一种方式。在我们的情况下,想象一下智能合约是 NFT 的所有者。通常,智能合约无法创建签名,因为没有私钥。在验证签名并且它无效时,我们可以在第二步检查它是否是有效的智能合约签名:


我们使用staticcall调用 NFT 的所有者地址,这确保调用中不会发生进一步的状态修改。如果结果成功并且具有有效的 returnData 长度( 这非常关键,请参阅之前的 0x 漏洞[15] ),我们可以检查返回值是否与0x1626ba7e匹配,这是 EIP-1271 中的魔术值,意味着“这是有效的签名”。智能合约如何验证签名取决于合约决定。


最后,如果你阅读了我关于ecrecover的帖子这里[16] ,你将知道它存在一些问题。


完整的签名验证


因此,你应该正确处理这些问题。幸运的是,当使用 Openzeppelin 合约时,执行所有这些操作比听起来要容易。ERC721-Permit 的完整签名验证可能如下所示。


(address signer, ) = ECDSA.tryRecover(hash, signature);

bool isValidEOASignature = signer != address(0) &&

    _isApprovedOrOwner(signer, tokenId);


require(

    isValidEOASignature ||

    _isValidContractERC1271Signature(

        ownerOf(tokenId),

        hash, signature

    ) || _isValidContractERC1271Signature(

        getApproved(tokenId),

        hash,

        signature

    ),

    "ERC721Permit: invalid signature"

);


function _isValidContractERC1271Signature(

    address signer,

    bytes32 hash,

    bytes memory signature

) private view returns (bool) {

    (bool success, bytes memory result) = signer.staticcall(

        abi.encodeWithSelector(

        IERC1271.isValidSignature.selector,

        hash,

        signature

        )

    );

    return (success &&

        result.length == 32 &&

        abi.decode(result,(bytes4))

            == IERC1271.isValidSignature.selector

    );

}


首先,我们使用 ECDSA.tryRecover[17]。这将解决一些问题:


支持 EIP-2098 签名。

解决 ecrecover 可能存在的安全问题。


然后,如果签名验证恢复了一个地址,我们检查它是否是 NFT 的所有者,或者他是否已获授权。当然,你可以只允许所有者,但由于 ERC721 中获得授权的地址应该具有完全控制权,因此最好也为授权的地址授予控制权。


迄今为止验证是不是有效的 EOA,我们最后还可以检查它是否是有效的合约签名。由于合约可以直接是所有者或已获授权,因此我们需要使用 staticcall 功能来检查这两种情况。


请注意,如果合约是通过 ERC721.setApprovalForAll 获得授权的,我们将无法验证其签名并允许其使用 permit。但是,一个已获授权的合约可以首先使用 ERC721.approve 来授权自己。


5. 授权 NFT


最后,我们只需要增加所有者的 nonce 并调用 approve 函数:


_approve(owner, spender, amount);


你可以在此处查看完整的实现示例[18]。


Uniswap ERC721-Permit


Uniswap 实现有一些不同之处,请查看实现[19]。在 Uniswap V3 中,仓位包装在 ERC721 接口中,并具有 permit 功能。


然而,值得注意的区别是:


签名通过 v、r、s 值传递

每次 permit 执行时 nonce 会递增,不允许同时执行多个 permit

1.jpg

ERC721-Permit 库


我创建了一个 ERC-721 Permit 库,你可以导入。你可以在 https://github.com/soliditylabs/ERC721-Permit 找到它。


使用参考实现[20]作为参考,以其原始用途使用。


你可以通过 npm 安装它:


$ npm install @soliditylabs/erc721-permit --save-dev


像这样将其导入到你的 ERC-721 合约中:


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;


import {ERC721, ERC721Permit} from "@soliditylabs/erc20-permit/contracts/ERC20Permit.sol";


contract MyNFTContract is ERC721Permit("MyNFT", "MNFT") {

  uint256 private _lastTokenId;


  function mint() public {

    _mint(msg.sender, ++_lastTokenId);

  }


  function safeTransferFromWithPermit(

    address from,

    address to,

    uint256 tokenId,

    bytes memory _data,

    uint256 deadline,

    bytes memory signature

  ) external {

    _permit(msg.sender, tokenId, deadline, signature);

    safeTransferFrom(from, to, tokenId, _data);

  }

}


我们还在这里添加了一个safeTransferFromWithPermit函数。这不是 ERC721-Permit 的标准函数,但它仍然可以是一个非常有用的补充。通过这样做,你可以在单个调用中授权和转账,节省一些额外的 gas,并且不需要任何辅助合约。


前端使用


通过 EIP-712 签署数据在许多钱包中得到直接支持。例如,在 MetaMask 中,可以查看 这里[21] 如何集成它。


始终谨慎使用


请注意,EIP4494 标准尚未最终确定。实际上,它处于相对早期的草案阶段。如果标准再次更改,我将保持库的更新。我的库代码也没有经过审计,请自行承担风险。


本翻译由 DeCert.me[22] 协助支持, 在 DeCert 构建可信履历,为自己码一个未来。


参考资料


[1]登链翻译计划: https://github.com/lbc-team/Pioneer


[2]翻译小组: https://learnblockchain.cn/people/412


[3]Tiny 熊: https://learnblockchain.cn/people/15


[4]ERC721: https://learnblockchain.cn/article/2077


[5]ERC20-Permit: https://learnblockchain.cn/article/1790


[6]Solidity中的ecrecover的应用: https://learnblockchain.cn/article/2701


[7]EIP-2612: https://eips.ethereum.org/EIPS/eip-2612


[8]EIP-712: https://eips.ethereum.org/EIPS/eip-712


[9]此处: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md


[10]EIP-191: https://eips.ethereum.org/EIPS/eip-191


[11]ecrecover: https://solidity.readthedocs.io/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions


[12]EIP-1271: https://eips.ethereum.org/EIPS/eip-1271


[13]EIP-2098: https://eips.ethereum.org/EIPS/eip-2098


[14]这里: https://learnblockchain.cn/article/2731


[15]这非常关键,请参阅之前的 0x 漏洞: https://samczsun.com/the-0x-vulnerability-explained/


[16]这里: https://learnblockchain.cn/article/2701


[17]

ECDSA.tryRecover: https://docs.openzeppelin.com/contracts/4.x/api/utils#ECDSA-tryRecover-bytes32-bytes-


[18]实现示例: https://github.com/soliditylabs/ERC721-Permit/blob/main/contracts/ERC721Permit.sol


[19]实现: https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/ERC721Permit.sol


[20]参考实现: https://github.com/dievardump/erc721-with-permits


[21]这里: https://docs.metamask.io/guide/signing-data.html#sign-typed-data-v4


[22]DeCert.me: https://decert.me/

声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!