xChar
·a year ago

什么是ERC-2612

ERC-2612: Permit Extension for EIP-20 Signed Approvals

ERC-2612是针对erc20中的approve的优化,传统的approve必须由EOA发起,对于EOA来说approve+后续操作就是至少两笔交易,有一些额外的gas开销。erc2612是对erc20的拓展,引入新的方法来实现approve

erc-2612需要实现三个新方法:

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
function nonces(address owner) external view returns (uint)
function DOMAIN_SEPARATOR() external view returns (bytes32)

image

重点就是permit方法,这个方法同样也会修改erc20中的approval结构,释放event

permit方法需要传入7个参数:

  • owner:当前token的owner
  • spender:授权人
  • value:approve的额度
  • deadline:交易执行的截止日期
  • v:交易签名数据
  • r:交易签名数据
  • s:交易签名数据
function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");

        // Unchecked because the only math done is incrementing
        // the owner's nonce which cannot realistically overflow.
        unchecked {
            address recoveredAddress = ecrecover(
                keccak256(
                    abi.encodePacked(
                        "\x19\x01",
                        DOMAIN_SEPARATOR(),
                        keccak256(
                            abi.encode(
                                keccak256(
                                    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                                ),
                                owner,
                                spender,
                                value,
                                nonces[owner]++,
                                deadline
                            )
                        )
                    )
                ),
                v,
                r,
                s
            );

            require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");

            allowance[recoveredAddress][spender] = value;
        }

        emit Approval(owner, spender, value);
    }

ERC-2612的使用流程

permit

permit的主要流程包括:

  • 校验当前的交易deadline
  • 通过ecrecover解析出recoveredAddress地址
  • 校验recoveredAddress是否与传入的owner地址一致
  • 执行approve逻辑,修改allowance并释放Approval事件

重要的流程就是第二步,根据传入的参数解析出recoveredAddress,并校验与传入的owner一致,再执行类似:token.approve(spender, value) 的逻辑完成approve

首先通过uncheck 包装内部的逻辑,使用uncheck有几个用处:

  • 解决solidity 0.8.0之后针对溢出默认revert与之前代码冲突的问题,比如我能确保自己的这段运算不会溢出,就可以在外面用uncheck包装,不让solidity来默认帮我做溢出的判断,这会引入额外的gas开销
  • 在0.8.0之后不再需要safemath

其次又用了一个关键词ecrecover 这个方法的输入参数是:

  • bytes32:签名后的消息
  • uint8: v
  • bytes32: r
  • bytes32: s

返回的参数是签名的地址recoveredAddress

因为rvs都是传入的参数,继续看签名的消息是啥

keccak256(
    abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR(),
        keccak256(
            abi.encode(
                keccak256(
                    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                ),
                owner,
                spender,
                value,
                nonces[owner]++,
                deadline
            )
        )
    )
)

keccak256 是在solidity中用来计算hash的内置方法, abi.encodePacked 是对传入的参数进行紧密的编码(省去0的填充,节省空间不与合约交互时使用)

DOMAIN_SEPARATOR

其中还有一个方法:DOMAIN_SEPARATOR() 这也是erc-2612中需要实现的方法,这个字段的目的是把每个链上的每个合约都做唯一性标识,并满足EIP-712的要求(一个标准的结构化签名协议,保证链下签名,链上校验的安全性,其中的domain separator是一个唯一标识符防止重放攻击)

EIP-712

function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
        return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
  }

function computeDomainSeparator() internal view virtual returns (bytes32) {
    return
        keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256(bytes(name)),
                keccak256("1"),
                block.chainid,
                address(this)
            )
        );
}

这个示例的DOMAIN_SEPARATOR() 方法使用keccak256对合约名称,chainID,版本号,当前合约地址等信息进行哈希计算得出的唯一标识

结束计算DOMAIN_SEPARATOR后,又通过abi.encode对这一段进行编码:

abi.encode(
    keccak256(
        "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
    ),
    owner,
    spender,
    value,
    nonces[owner]++,
    deadline
)

这里的参数都是permit传入的,需要注意的是nonce 这个字段,使用的是nonces[owner]++ ,通过自定义的数组来维护自增的用户nonce,避免用户同一笔签名的交易被重复利用

由此流程,便从哈希后的签名信息中解析出了recoveredAddress地址,那么为什么解析出的recoveredAddress地址就和owner一定一致呢?

如何使用ERC-2612

前面提到,通过调用permit方法即可完成approve的代理调用,省去EOA前置approve的操作,在调用permit时有非常重要的三个参数r s v,那么这三个参数是需要EOA前置签名好提供过来的

通过forge的下面一段代码为例:

address alice = vm.addr(1);
bytes32 hash = keccak256("Signed by Alice");
(uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash);
address signer = ecrecover(hash, v, r, s);
assertEq(alice, signer); // [PASS]

alice对hash进行签名,即返回vrs三个参数,再通过ecrecover即得到signer即为alice

那么为了通过permit中的校验逻辑,我们也需要构造一致的签名内容让EOA完成签名,我们再拿着rsv去调用permit即可

下面一段代码就是对permit签名方法的封装,定义了Permit结构,与erc-2612中的计算方式一致

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract SigUtils {
    bytes32 internal DOMAIN_SEPARATOR;

    constructor(bytes32 _DOMAIN_SEPARATOR) {
        DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR;
    }

    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 public constant PERMIT_TYPEHASH =
        0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

    struct Permit {
        address owner;
        address spender;
        uint256 value;
        uint256 nonce;
        uint256 deadline;
    }

    // computes the hash of a permit
    function getStructHash(Permit memory _permit)
        internal
        pure
        returns (bytes32)
    {
        return
            keccak256(
                abi.encode(
                    PERMIT_TYPEHASH,
                    _permit.owner,
                    _permit.spender,
                    _permit.value,
                    _permit.nonce,
                    _permit.deadline
                )
            );
    }

    // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer
    function getTypedDataHash(Permit memory _permit)
        public
        view
        returns (bytes32)
    {
        return
            keccak256(
                abi.encodePacked(
                    "\x19\x01",
                    DOMAIN_SEPARATOR,
                    getStructHash(_permit)
                )
            );
    }
}

ERC-2612 安全问题

ERC-2612本质上就是在approve之上添加了一组校验逻辑,允许用户在链下提前生成交易的签名,再将交易签名的rsv作为参数传入调用permit

我可以想到可能存在安全漏洞:

重放攻击:在计算DOMAIN_SEPARATOR 时会使用chainID作为标识符,如果链发生分叉,且chainID是在构造函数时即设定,那么在两条链上会有同一个chainID的合约,可以在链A上签名,去链B上重放

Loading comments...