Checkpoint 3:MetaMask Mint 交易耗尽 gas

挑战地址:https://speedrunethereum.com/challenge/tokenization

本文是 Speedrun Ethereum Tokenization 系列第二篇,记录 Checkpoint 3 中 Mint NFT 失败的完整排错过程。

隐私说明:本文不包含真实钱包地址、交易哈希、部署 URL、IPFS 哈希或本地绝对路径。

系列目录

问题:MetaMask EIP-7702 智能账户 Mint 时耗尽 gas

当前 Challenge 步骤:Checkpoint 3 - Mint NFTs

合约已经部署到 Sepolia,前端也已经连接到新合约。现在正在通过 MetaMask 调用 mintItem 铸造 NFT。

1. 现象

合约已经在 Sepolia 部署成功:

1
2
YourCollectible:
<DEPLOYED_CONTRACT_ADDRESS>

前端连接 MetaMask 后点击 Mint,MetaMask 显示交易已经确认,但链上最终失败。

交易哈希:

1
<FAILED_MINT_TX_HASH>

交易信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
From:
<YOUR_WALLET_ADDRESS>

To:
<DELEGATION_MANAGER_ADDRESS>

Type:
4 (EIP-7702)

Method:
redeemDelegations(bytes[],bytes32[],bytes[])

Gas Limit:
1,448,890

Gas Used:
1,124,400

Status:
failed

表面上看,这笔交易像是在调用 MetaMask Delegation Framework,而不是直接调用 NFT 合约。

但解码 calldata 后可以看到,内层调用目标是:

1
<DEPLOYED_CONTRACT_ADDRESS>

函数选择器是:

1
0x110bcd45

对应函数:

1
mintItem(address,string)

所以 MetaMask 的 EIP-7702 智能账户把真正的 mintItem 调用包装在:

1
redeemDelegations(...)

里面执行。

2. 先排除 ERC-721 Receiver 问题

mintItem 内部调用的是:

1
_safeMint(to, tokenId);

如果 to 是合约,ERC-721 会调用:

1
onERC721Received(...)

最初我怀疑 MetaMask EIP-7702 账户有代码,但没有实现 ERC-721 Receiver,导致 safe mint 失败。

链上查询后可以排除这个原因:

1
2
supportsInterface(0x150b7a02) -> true
onERC721Received(...) -> 0x150b7a02

这说明 MetaMask 智能账户已经正确实现了 ERC-721 Receiver。

因此失败不是收款账户不支持 NFT safe mint。

3. 重放交易后找到真实失败点

使用 Foundry 重放失败交易:

1
2
3
4
cast run \
<FAILED_MINT_TX_HASH> \
--rpc-url sepolia \
-vvvv

关键 trace 简化后如下:

1
2
3
4
5
6
7
8
9
10
DelegationManager::redeemDelegations(...)
-> NativeBalanceChangeEnforcer...
-> ERC721BalanceChangeEnforcer...
-> <YOUR_WALLET_ADDRESS>::executeFromExecutor(...)
-> YourCollectible::mintItem(...)
-> emit Transfer(...)
-> onERC721Received(...)
-> [OutOfGas]
-> [Revert]
-> [Revert]

这说明:

  1. mintItem 已经被成功调用。
  2. Transfer 事件已经发出。
  3. MetaMask 智能账户的 onERC721Received 已经返回正确结果。
  4. 后续执行 token URI 相关逻辑时,内层调用耗尽 gas。
  5. 最外层交易回滚,所以前面的 Mint 状态也全部撤销。

链上最终状态也能证明回滚:

1
2
3
tokenIdCounter() = 0
balanceOf(<YOUR_WALLET_ADDRESS>) = 0
ownerOf(1) -> ERC721NonexistentToken(1)

4. Gas 数据对比

失败交易中,内层 mintItem 大约只获得:

1
961,602 gas

但直接在 Sepolia 上估算同样的 mintItem 调用,需要:

1
1,032,313 gas

验证结果:

1
2
961,602 gas  -> out of gas
2,000,000 gas -> 成功

外层 EIP-7702 交易虽然设置了:

1
1,448,890 gas

但其中相当一部分被以下逻辑消耗:

  • redeemDelegations
  • delegation 签名验证
  • balance enforcer
  • ERC-721 balance enforcer
  • executeFromExecutor

留给真正 mintItem 的 gas 只有约 961,602,不足以完成需要约 1,032,313 gas 的 Mint。

5. 与问题二的关系

问题二和问题三都属于 OutOfGas,但发生层级不同:

1
2
3
4
5
6
7
8
9
10
11
问题二:Sepolia 部署
Foundry 估算约 1.895M gas
Sepolia 实际需要约 10.11M gas
-> 部署交易 OutOfGas

问题三:MetaMask EIP-7702 Mint
外层交易有约 1.45M gas
包装逻辑消耗大量 gas
内层 mintItem 只剩约 961k gas
mintItem 实际需要约 1.032M gas
-> 内层调用 OutOfGas

两者的共同点是 gas 不足。

不同点是:

  • 问题二是 Foundry 对部署交易的估算偏低。
  • 问题三是 MetaMask EIP-7702 包装交易产生了额外 gas 开销。

6. 推荐解决方案一:使用普通 MetaMask EOA

这个挑战的标准流程默认使用普通 EOA。

如果 MetaMask 地址已经开启 Smart Account,并被升级为 EIP-7702 委托账户,Mint 交易就会经过 redeemDelegations,额外 gas 开销明显增加。

最简单、最稳定的解决方式是:

  1. 在 MetaMask 中切换到普通 EOA 账户。
  2. 确保账户连接到 Sepolia。
  3. 使用 Sepolia 测试 ETH。
  4. 在前端重新点击 Mint。

普通 EOA 直接调用 mintItem 时,不需要经过 delegation 包装,gas 开销会小很多。

7. 推荐解决方案二:为前端 Mint 请求提高 gas

前端 Mint 代码位于:

1
packages/nextjs/app/myNFTs/page.tsx

原来的调用:

1
2
3
4
await writeContractAsync({
functionName: "mintItem",
args: [connectedAddress, uploadedItem.path],
});

可以改为:

1
2
3
4
5
await writeContractAsync({
functionName: "mintItem",
args: [connectedAddress, uploadedItem.path],
gas: 3000000n,
});

增加的是:

1
gas: 3000000n,

直接 Mint 实际需要约 1,032,313 gas。设置 3,000,000 是为了给 EIP-7702 包装逻辑预留足够空间。

修改后需要:

  1. 保存文件。
  2. 等待 Next.js 编译完成。
  3. 强制刷新网页,例如 Command + Shift + R。
  4. 重新发起 Mint。
  5. 在 MetaMask 中确认 gas limit 确实提高。

如果 MetaMask 仍然强制使用自己的 gas 估算,或者交易依旧是低 gas 的 redeemDelegations,则应使用普通 EOA 或 CLI 方案。

8. 推荐解决方案三:使用 CLI 直接 Mint

mintItem 没有访问控制限制,任何账户都可以调用。

因此可以使用已经创建的:

1
sepolia-deployer

发起 Mint,并把 NFT 直接铸给 MetaMask 地址:

1
2
3
4
5
6
7
8
9
cd "/path/to/challenge-tokenization/packages/foundry"

cast send <DEPLOYED_CONTRACT_ADDRESS> \
"mintItem(address,string)" \
<YOUR_WALLET_ADDRESS> \
"<IPFS_HASH>" \
--account sepolia-deployer \
--rpc-url sepolia \
--gas-limit 2000000

输入 sepolia-deployer 的密码。

成功后:

  • NFT 会属于:
1
<YOUR_WALLET_ADDRESS>
  • tokenIdCounter 会变成 1
  • balanceOf(<YOUR_WALLET_ADDRESS>) 会变成 1
  • 前端刷新后应该能看到这个 NFT

这种方式绕过了 MetaMask EIP-7702 包装层,适合先完成挑战和验证合约。

9. Mint 问题复盘

遇到 MetaMask “已确认但前端报错”时,不要只看钱包确认页面,还要检查:

1
2
3
4
5
交易类型
外层调用的函数
链上 receipt status
实际 gasUsed
内层调用 trace

本次交易的类型和函数:

1
2
Type 4
redeemDelegations

说明它经过了 MetaMask EIP-7702 智能账户包装,而不是普通 ERC-721 调用。

最终 trace:

1
mintItem -> Transfer -> onERC721Received -> OutOfGas

说明不是合约不接受 NFT,也不是 Solidity 业务条件 revert,而是内层调用拿到的 gas 不够。


下一篇

Checkpoint 3 完成后,继续阅读:

Checkpoint 4 - Ship

返回:完整排错路线