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 到达时失效。
- 检查
deadline >= now,或使用最大 uint256 表示许可提交不设时间上限。 - 取
_permitNonces[owner]的旧值参与摘要,并递增存储值。 - 将结构体哈希与
_domainSeparator()组合为 EIP-712 摘要。 - 使用该快照的
SignatureChecker验证 Owner 的当前签名策略。 _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);
}
}