Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

USDC 源码:permit 的 nonce、截止时间与合约签名

标准接口见 EIP-2612。这里分析 Circle 的具体实现,不把它的扩展直接当成标准要求。

1. 源码范围与版本

本文代码主体已对照 Circle 官方 stablecoin-evm 提交 574b947dfcfcb5fa05b46d8961ece29e06b14129,文件为 contracts/v2/EIP2612.sol,编译器声明是 Solidity 0.6.12。正文保留原先的许可注释;其许可头与该提交不同,pragma 之后的主体一致。固定源码

这固定的是仓库快照,不是任一网络的当前 USDC 部署认证。代理地址、实现地址、升级时点、库链接和链 ID 需要另查链上记录。本轮没有编译或部署完整 USDC 继承链。

2. 从消息到 allowance

Owner 签入自己、Spender、Value、当前 permit nonce 与 Deadline。_permit 验证后调用 _approve 覆盖额度;它不转账、不决定之后的收款人,也不让额度自动在 Deadline 到达时失效。

  1. 检查 deadline >= now,或使用最大 uint256 表示许可提交不设时间上限。
  2. 取 _permitNonces[owner] 的旧值参与摘要,并递增存储值。
  3. 将结构体哈希与 _domainSeparator() 组合为 EIP-712 摘要。
  4. 使用该快照的 SignatureChecker 验证 Owner 的当前签名策略。
  5. _approve(owner, spender, value) 建立授权额度。

nonce 的自增虽在验签前发生,验签失败时整个调用回滚,nonce 不会因此被永久消费。域来自虚函数 _domainSeparator(),实际行为还要沿最终实现的覆盖关系分析,不能只看本文件就断定缓存永不变化。

3. deadline 不是授权的有效期

deadline 限制这份签名何时还能提交。如果它在有效期内已经成功建立 allowance,后来超过 deadline,剩余额度仍可代扣。最大值表示签名提交不设通常的过期边界,不表示一种额外的“永久额度”类型。

若业务将 permit 与消费放进同一交易,后续转账回滚且异常没有被吞掉,当前交易的许可与额度变化也回滚。单独已经确认的 permit 不会因后来另一笔业务交易失败而撤销。

4. 此实现的合约签名扩展

这里同时有 (v,r,s) 和变长 signature 两个内部重载;前者按 r,s,v 拼成字节,再走统一逻辑。变长签名支持钱包策略,不意味着标准 EIP-2612 的所有实现都有该重载。

同一快照的 contracts/util/SignatureChecker.sol 根据代码大小选择 ECDSA 恢复或 ERC-1271 静态查询。它是 Circle 的库实现,包含自己的恢复路径,并不是本博客教学工程使用的 OpenZeppelin 5.2.0 文件。固定库源码

外部用户能调用哪个重载,还应查看最终 FiatToken 实现暴露的函数。合约签名可能因钱包策略变更而失效;code.length 分支也要结合 EIP-7702 委托账户说明,不能理解成“有代码就没有私钥”。

5. 与教学测试的对应关系

独立示例工程 使用 Solidity 0.8.24 与 OpenZeppelin 5.2.0,验证 nonce、deadline、业务代扣与回滚边界。这些结果说明教学模型,不证明上述 0.6.12 USDC 源码或某个线上部署已经通过测试。

源码材料

/**
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright (c) 2023, Circle Internet Financial, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

pragma solidity 0.6.12;

import { AbstractFiatTokenV2 } from "./AbstractFiatTokenV2.sol";
import { EIP712Domain } from "./EIP712Domain.sol";
import { MessageHashUtils } from "../util/MessageHashUtils.sol";
import { SignatureChecker } from "../util/SignatureChecker.sol";

/**
 * @title EIP-2612
 * @notice Provide internal implementation for gas-abstracted approvals
 */
abstract contract EIP2612 is AbstractFiatTokenV2, EIP712Domain {
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
    bytes32
        public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

    mapping(address => uint256) private _permitNonces;

    /**
     * @notice Nonces for permit
     * @param owner Token owner's address (Authorizer)
     * @return Next nonce
     */
    function nonces(address owner) external view returns (uint256) {
        return _permitNonces[owner];
    }

    /**
     * @notice Verify a signed approval permit and execute if valid
     * @param owner     Token owner's address (Authorizer)
     * @param spender   Spender's address
     * @param value     Amount of allowance
     * @param deadline  The time at which the signature expires (unix time), or max uint256 value to signal no expiration
     * @param v         v of the signature
     * @param r         r of the signature
     * @param s         s of the signature
     */
    function _permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        _permit(owner, spender, value, deadline, abi.encodePacked(r, s, v));
    }

    /**
     * @notice Verify a signed approval permit and execute if valid
     * @dev EOA wallet signatures should be packed in the order of r, s, v.
     * @param owner      Token owner's address (Authorizer)
     * @param spender    Spender's address
     * @param value      Amount of allowance
     * @param deadline   The time at which the signature expires (unix time), or max uint256 value to signal no expiration
     * @param signature  Signature byte array signed by an EOA wallet or a contract wallet
     */
    function _permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        bytes memory signature
    ) internal {
        require(
            deadline == type(uint256).max || deadline >= now,
            "FiatTokenV2: permit is expired"
        );

        bytes32 typedDataHash = MessageHashUtils.toTypedDataHash(
            _domainSeparator(),
            keccak256(
                abi.encode(
                    PERMIT_TYPEHASH,
                    owner,
                    spender,
                    value,
                    _permitNonces[owner]++,
                    deadline
                )
            )
        );
        require(
            SignatureChecker.isValidSignatureNow(
                owner,
                typedDataHash,
                signature
            ),
            "EIP2612: invalid signature"
        );

        _approve(owner, spender, value);
    }
}