> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tenderly.co/llms.txt
> Use this file to discover all available pages before exploring further.

# 使用 Foundry 进行智能合约验证

> 通过 Tenderly 的验证 API，使用 forge verify-contract 私密或公开地验证在公共网络上通过 Foundry 部署的合约。

<Info>
  **适用于：** 公共网络（主网和测试网）。若要在 Virtual Environment 上验证合约，请参阅[部署并验证合约](/virtual-environments/develop/deploy-contracts)和[使用 Foundry 验证代理合约](/virtual-environments/develop/verify-proxy-contracts)。
</Info>

Tenderly 通过其兼容 Etherscan 的验证 API，验证使用 Foundry 的 `forge create`、`forge script` 和 `forge verify-contract` 命令部署的智能合约。验证可以是私密的（源代码仅在您的 Tenderly 项目内可见）或公开的（任何拥有链接的人都可以查看）。

## 开始之前

您需要 Tenderly 账户和项目的 [slug](/platform/account/projects/slug)、一个 [Tenderly 访问密钥](/platform/account/projects/api-tokens)（Dashboard → Account Settings → Authorization），以及目标网络上有资金的账户。

Tenderly 的验证器会将 Solidity 编译器附加到部署字节码的元数据哈希与您提交的源代码进行匹配。请保留编译输出中的元数据，并固定编译器设置，使部署时和验证时之间不会发生变化：

```toml title="foundry.toml" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
[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 = true` 和 `bytecode_hash = "ipfs"` 是 Foundry 的默认值，但某些模板会剥离它们；`bytecode_hash = "none"` 会破坏验证。部署和验证之间的编译器设置漂移会导致 `Bytecode does not match deployed contract` 失败。

以下示例使用 Base Sepolia（链 ID `84532`）。设置环境：

```bash title=".env" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
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`：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
https://api.tenderly.co/api/v1/account/$TENDERLY_ACCOUNT/project/$TENDERLY_PROJECT/etherscan/verify/network/$NETWORK_ID
```

每个 URL 段都映射到一个可以从 Dashboard 读取的值：

| 段                           | 含义                                                                                  |
| --------------------------- | ----------------------------------------------------------------------------------- |
| `account/$TENDERLY_ACCOUNT` | 您的 Tenderly 账户（用户或组织）[slug](/platform/account/projects/slug)。区分大小写。                 |
| `project/$TENDERLY_PROJECT` | 已验证合约归属的项目。                                                                         |
| `network/$NETWORK_ID`       | 合约部署的链，以十进制链 ID 表示（`1` 表示 Ethereum Mainnet，`8453` 表示 Base，`84532` 表示 Base Sepolia）。 |
| 无后缀                         | 私密验证：合约仅在您的项目内可见。                                                                   |
| `/public` 后缀                | 公开验证：合约源代码对任何拥有链接的人可见，无需 Tenderly 登录。                                               |

## 私密验证

私密验证的合约仅对项目成员可见，位于 Dashboard 中的 **Contracts** 下。

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
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` 会轮询验证器直到验证完成并打印结果。

<Note>
  Foundry 在成功时会输出一行 `URL: https://etherscan.io/address/...`。这是 Etherscan 兼容流程的显示怪癖；合约实际上是在 Tenderly 验证的，而非 Etherscan。请在 Dashboard 的 **Contracts** 下确认。
</Note>

## 公开验证

将验证器 URL 换成带 `/public` 后缀的变体；其他不变。已验证的源代码页面对任何拥有链接的人可访问，无需 Tenderly 登录。

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
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
```

<Warning>
  公开验证不可逆。一旦公开验证，合约将始终保持公开状态。私密和公开验证是独立的记录：要将已私密验证的合约变为公开，请针对 `/public` URL 重新验证。
</Warning>

## 一步完成部署和验证

`forge create` 和 `forge script` 内联接受相同的标志，因此部署和验证作为一条命令运行。与 `forge verify-contract` 不同，两者都以原始值形式接受构造函数参数，并为您进行 ABI 编码：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
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
```

<Warning>
  将 `--constructor-args` 放在最后。它会贪婪地消耗命令行的其余部分，因此放在它之后的任何标志都会被视为另一个构造函数参数。
</Warning>

对于多合约部署，使用带 `--slow` 的 `forge script`；脚本部署的每个合约都会自动验证，其构造函数参数从广播日志中获取。没有 [`--slow`](https://www.getfoundry.sh/reference/forge/script?highlight=slow#forge-script)，广播批处理可能会在前一个交易确认之前就提交下一个交易，这可能与验证步骤竞争：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
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`](https://www.getfoundry.sh/reference/forge/clone) 将已验证的源代码克隆到本地，然后通过 Tenderly 的 API 重新验证：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge clone $CONTRACT_ADDRESS -e $ETHERSCAN_API_KEY --chain 1
```

`forge clone` 从源链的兼容 Etherscan 的 API 下载已验证的源代码（每条链请使用相应浏览器的密钥），重建项目布局，并固定原始部署者使用的编译器设置。在克隆目录中运行 `forge build` 以确认它可以编译；导入解析错误通常追溯到 `remappings.txt`。

然后针对 Tenderly 进行验证：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
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`，请显式传递原始编译器设置。所有这些设置都列在合约已经验证的浏览器页面上，位于合约源代码部分下：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
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` 会失败并显示：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
Error: Failed to get standard json input
- cannot resolve file at "lib/openzeppelin-contracts-upgradeable/lib/..."
```

创建一个单行 `src/Imports.sol` 来导入该合约，然后重新构建：

```solidity title="src/Imports.sol" theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
```

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge build --force
```

`Imports.sol` 永远不会被发送到验证器；它只强制编译器缓存该合约及其依赖项。然后使用合约的完整 `lib/` 路径进行验证，因为源代码物理上位于那里。

对于您未部署的 `TransparentUpgradeableProxy`，构造函数是 `(address _logic, address initialOwner, bytes _data)`，其中大部分可以从链状态重建：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
# 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`，或访问密钥已被撤销。                                     | 生成新的[访问密钥](/platform/account/projects/api-tokens)并重新设置 `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_version`、`optimizer`、`optimizer_runs` 和 `bytecode_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` 目录中的合约](#contracts-that-live-in-the-lib-directory)。                                                               |
| 在 `forge create` 上显示 `Dry run enabled, not broadcasting transaction` | 未传递 `--broadcast`，或被 `--constructor-args` 吞没。                               | 将 `--constructor-args` 移至最后一个标志。                                                                                                                                                   |
| 在 `forge script` 上只有第一个合约被验证                                         | RPC 竞争：第二个交易在第一个收据之前提交。                                                     | 添加 `--slow`。                                                                                                                                                                       |
| 验证成功但合约未出现在项目中                                                       | 验证器 URL 中的项目 slug 错误。                                                       | 使用正确的 slug 重新验证。合约会归档到 URL 指向的任何项目下。                                                                                                                                               |
