跳转到主要内容
适用于: 公共网络(主网和测试网)。若要在 Virtual Environment 上验证合约,请参阅部署并验证合约使用 Foundry 验证代理合约
Tenderly 通过其兼容 Etherscan 的验证 API,验证使用 Foundry 的 forge createforge scriptforge verify-contract 命令部署的智能合约。验证可以是私密的(源代码仅在您的 Tenderly 项目内可见)或公开的(任何拥有链接的人都可以查看)。

开始之前

您需要 Tenderly 账户和项目的 slug、一个 Tenderly 访问密钥(Dashboard → Account Settings → Authorization),以及目标网络上有资金的账户。 Tenderly 的验证器会将 Solidity 编译器附加到部署字节码的元数据哈希与您提交的源代码进行匹配。请保留编译输出中的元数据,并固定编译器设置,使部署时和验证时之间不会发生变化:
foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.24"
optimizer = true
optimizer_runs = 200

# Required for Tenderly contract verification:
# keep the CBOR metadata so the verifier can match the on-chain bytecode hash.
cbor_metadata = true
bytecode_hash = "ipfs"
cbor_metadata = truebytecode_hash = "ipfs" 是 Foundry 的默认值,但某些模板会剥离它们;bytecode_hash = "none" 会破坏验证。部署和验证之间的编译器设置漂移会导致 Bytecode does not match deployed contract 失败。 以下示例使用 Base Sepolia(链 ID 84532)。设置环境:
.env
BASE_SEPOLIA_RPC=https://sepolia.base.org
PRIVATE_KEY=...                           # a funded key on the target network

TENDERLY_ACCOUNT=...                      # your account slug
TENDERLY_PROJECT=...                      # your project slug
TENDERLY_ACCESS_KEY=...                   # from Account Settings -> Authorization

TENDERLY_PRIVATE_VERIFIER_URL=https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/${TENDERLY_PROJECT}/etherscan/verify/network/84532
TENDERLY_PUBLIC_VERIFIER_URL=${TENDERLY_PRIVATE_VERIFIER_URL}/public

验证器 URL

每个 Foundry 验证命令都需要指向 Tenderly 验证 API 的 --verifier-url,以及用于认证的 --etherscan-api-key $TENDERLY_ACCESS_KEY
https://api.tenderly.co/api/v1/account/$TENDERLY_ACCOUNT/project/$TENDERLY_PROJECT/etherscan/verify/network/$NETWORK_ID
每个 URL 段都映射到一个可以从 Dashboard 读取的值:
含义
account/$TENDERLY_ACCOUNT您的 Tenderly 账户(用户或组织)slug。区分大小写。
project/$TENDERLY_PROJECT已验证合约归属的项目。
network/$NETWORK_ID合约部署的链,以十进制链 ID 表示(1 表示 Ethereum Mainnet,8453 表示 Base,84532 表示 Base Sepolia)。
无后缀私密验证:合约仅在您的项目内可见。
/public 后缀公开验证:合约源代码对任何拥有链接的人可见,无需 Tenderly 登录。

私密验证

私密验证的合约仅对项目成员可见,位于 Dashboard 中的 Contracts 下。
showLineNumbers
COUNTER_ADDRESS=0x...   # the deployed contract address

forge verify-contract $COUNTER_ADDRESS src/Counter.sol:Counter \
  --verifier-url       $TENDERLY_PRIVATE_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY \
  --constructor-args   $(cast abi-encode "constructor(uint256)" 7) \
  --watch
forge verify-contract 期望构造函数参数已进行 ABI 编码;使用 cast abi-encode 生成它们。--watch 会轮询验证器直到验证完成并打印结果。
Foundry 在成功时会输出一行 URL: https://etherscan.io/address/...。这是 Etherscan 兼容流程的显示怪癖;合约实际上是在 Tenderly 验证的,而非 Etherscan。请在 Dashboard 的 Contracts 下确认。

公开验证

将验证器 URL 换成带 /public 后缀的变体;其他不变。已验证的源代码页面对任何拥有链接的人可访问,无需 Tenderly 登录。
showLineNumbers
forge verify-contract $COUNTER_ADDRESS src/Counter.sol:Counter \
  --verifier-url       $TENDERLY_PUBLIC_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY \
  --constructor-args   $(cast abi-encode "constructor(uint256)" 7) \
  --watch
公开验证不可逆。一旦公开验证,合约将始终保持公开状态。私密和公开验证是独立的记录:要将已私密验证的合约变为公开,请针对 /public URL 重新验证。

一步完成部署和验证

forge createforge script 内联接受相同的标志,因此部署和验证作为一条命令运行。与 forge verify-contract 不同,两者都以原始值形式接受构造函数参数,并为您进行 ABI 编码:
showLineNumbers
forge create src/Counter.sol:Counter \
  --rpc-url            $BASE_SEPOLIA_RPC \
  --private-key        $PRIVATE_KEY \
  --broadcast \
  --verify \
  --verifier-url       $TENDERLY_PRIVATE_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY \
  --constructor-args   7
--constructor-args 放在最后。它会贪婪地消耗命令行的其余部分,因此放在它之后的任何标志都会被视为另一个构造函数参数。
对于多合约部署,使用带 --slowforge script;脚本部署的每个合约都会自动验证,其构造函数参数从广播日志中获取。没有 --slow,广播批处理可能会在前一个交易确认之前就提交下一个交易,这可能与验证步骤竞争:
showLineNumbers
forge script script/Deploy.s.sol:DeployScript \
  --rpc-url            $BASE_SEPOLIA_RPC \
  --private-key        $PRIVATE_KEY \
  --broadcast --slow \
  --verify \
  --verifier-url       $TENDERLY_PRIVATE_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY

验证您未部署的合约

当合约已在公共浏览器(Etherscan、Basescan)上验证但未在您的 Tenderly 项目中验证时,请使用 forge clone 将已验证的源代码克隆到本地,然后通过 Tenderly 的 API 重新验证:
showLineNumbers
forge clone $CONTRACT_ADDRESS -e $ETHERSCAN_API_KEY --chain 1
forge clone 从源链的兼容 Etherscan 的 API 下载已验证的源代码(每条链请使用相应浏览器的密钥),重建项目布局,并固定原始部署者使用的编译器设置。在克隆目录中运行 forge build 以确认它可以编译;导入解析错误通常追溯到 remappings.txt 然后针对 Tenderly 进行验证:
showLineNumbers
forge verify-contract $CONTRACT_ADDRESS src/MyContract.sol:MyContract \
  --verifier-url       $TENDERLY_PRIVATE_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY \
  --watch
如果验证失败并显示 Bytecode does not match deployed contract,请显式传递原始编译器设置。所有这些设置都列在合约已经验证的浏览器页面上,位于合约源代码部分下:
showLineNumbers
forge verify-contract $CONTRACT_ADDRESS src/MyContract.sol:MyContract \
  --verifier-url       $TENDERLY_PRIVATE_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY \
  --compiler-version   v0.8.27+commit.40a35a09 \
  --optimizer-runs     10000 \
  --evm-version        prague \
  --constructor-args   $ENCODED_ARGS \
  --watch

位于 lib 目录中的合约

forge build 只编译从 src/ 可达的内容。一个仅存在于 lib/ 依赖中的合约(一个代理、一个标准 ERC 实现)永远不会进入构建缓存,forge verify-contract 会失败并显示:
Error: Failed to get standard json input
- cannot resolve file at "lib/openzeppelin-contracts-upgradeable/lib/..."
创建一个单行 src/Imports.sol 来导入该合约,然后重新构建:
src/Imports.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
forge build --force
Imports.sol 永远不会被发送到验证器;它只强制编译器缓存该合约及其依赖项。然后使用合约的完整 lib/ 路径进行验证,因为源代码物理上位于那里。 对于您未部署的 TransparentUpgradeableProxy,构造函数是 (address _logic, address initialOwner, bytes _data),其中大部分可以从链状态重建:
showLineNumbers
# 1. _logic: the implementation address (ERC-1967 slot)
cast implementation $PROXY_ADDRESS --rpc-url $RPC_URL

# 2. initialOwner: owner of the ProxyAdmin contract
cast admin $PROXY_ADDRESS --rpc-url $RPC_URL                      # returns the ProxyAdmin
cast call $PROXY_ADMIN "owner()(address)" --rpc-url $RPC_URL      # returns initialOwner

# 3. _data: the initialize() calldata from the deployment transaction
#    (decode the factory calldata on the explorer; initialize(address) is selector 0xc4d66de8)

ENCODED_ARGS=$(cast abi-encode "constructor(address,address,bytes)" $LOGIC $INITIAL_OWNER $DATA)

forge verify-contract $PROXY_ADDRESS \
  lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy \
  --verifier-url       $TENDERLY_PRIVATE_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY \
  --compiler-version   v0.8.27+commit.40a35a09 \
  --optimizer-runs     200 \
  --evm-version        shanghai \
  --constructor-args   $ENCODED_ARGS \
  --watch
编译器版本、优化器运行次数和 EVM 版本来自已验证实现的浏览器页面。如果实现未在任何地方验证,则回退到部署项目的 foundry.toml 或部署脚本。

故障排查

症状可能原因修复方法
unauthorized缺少或错误的 --etherscan-api-key,或访问密钥已被撤销。生成新的访问密钥并重新设置 TENDERLY_ACCESS_KEY
not found验证器 URL 中的账户 slug、项目 slug 或链 ID 错误。重新检查每个 URL 段。账户 slug 区分大小写。
Bytecode does not match deployed contract提交的源代码与已部署的代码编译不同:optimizer_runs 不匹配、solc_version 或 EVM 版本不同,或元数据哈希被剥离。foundry.toml 中固定 solc_versionoptimizeroptimizer_runsbytecode_hash,或显式传递 --compiler-version --optimizer-runs --evm-version。重新验证前运行 forge clean && forge build
Failed to deserialize content验证器返回了 Foundry 无法解析的错误字符串,通常包装了上游认证或路径错误。重新检查 URL,并使用 -vvvv 重新运行以查看原始响应。
Failed to get standard json input - cannot resolve file at lib/...该合约从未从 src/ 导入,因此从构建缓存中缺失。创建 src/Imports.sol 导入它,然后运行 forge build --force。请参阅位于 lib 目录中的合约
forge create 上显示 Dry run enabled, not broadcasting transaction未传递 --broadcast,或被 --constructor-args 吞没。--constructor-args 移至最后一个标志。
forge script 上只有第一个合约被验证RPC 竞争:第二个交易在第一个收据之前提交。添加 --slow
验证成功但合约未出现在项目中验证器 URL 中的项目 slug 错误。使用正确的 slug 重新验证。合约会归档到 URL 指向的任何项目下。