> ## 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 验证代理合约

> 使用 forge script 在 Virtual Environment 上部署并验证 UUPS 实现及其 ERC1967 代理，然后在升级后重新验证。

代理部署由两个合约组成：代理，负责委托每次调用；以及实现，包含逻辑。两者都必须分别验证。对于任何遵循 [ERC-1967](https://eips.ethereum.org/EIPS/eip-1967) 的代理，Tenderly 浏览器都会读取标准的实现存储槽，因此一旦两个合约都被验证，对代理地址的调用就会使用实现的 ABI 进行解码。

本指南在 [Virtual Environment](/virtual-environments/overview) 上将 UUPS 实现（`CounterV1`）部署在 `ERC1967Proxy` 之后，在一次 `forge script` 运行中验证两者，将实现升级到 `CounterV2`，并涵盖对未验证部署的代理进行重新验证。对于 Hardhat 的等效方法，请参见[验证代理合约](/contract-verification/hardhat-proxy)。

## 开始之前

* 完成[部署和验证合约](/virtual-environments/develop/deploy-contracts)中的设置：一个 Virtual Environment、一个[已注资的部署者地址](/virtual-environments/unlimited-faucet)以及一个包含 `cbor_metadata = true` 和 `bytecode_hash = "ipfs"` 的 `foundry.toml`。
* 设置环境变量：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
export TENDERLY_VIRTUAL_TESTNET_RPC=...   # your Virtual Environment RPC URL
export TENDERLY_VERIFIER_URL=$TENDERLY_VIRTUAL_TESTNET_RPC/verify
export PRIVATE_KEY=...                    # deployer private key
```

* 与基础库一起安装 OpenZeppelin 的可升级合约：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge install OpenZeppelin/openzeppelin-contracts --no-git --shallow
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-git --shallow
```

* 将两个 remappings 添加到 `remappings.txt`：

```bash title="remappings.txt" theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
forge-std/=lib/forge-std/src/
```

这两个库担任不同的角色：`openzeppelin-contracts` 提供代理（`ERC1967Proxy`），而 `openzeppelin-contracts-upgradeable` 提供实现的构建块（`Initializable`、`UUPSUpgradeable`、`OwnableUpgradeable`）。可升级版本使用初始化函数替代构造函数，因为当状态位于代理之后时，构造函数不会运行。

## 实现合约

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

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract CounterV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public number;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address owner_, uint256 initialNumber) external initializer {
        __Ownable_init(owner_);
        number = initialNumber;
    }

    function increment() external {
        number += 1;
    }

    function setNumber(uint256 newNumber) external {
        number = newNumber;
    }

    function version() external pure virtual returns (string memory) {
        return "v1";
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
```

此合约中对代理模式重要的三个部分：

1. `constructor() { _disableInitializers(); }` 锁定实现。只有代理应该持有状态；此构造函数使得对实现地址直接调用 `initialize` 会 revert。
2. `initialize(...)` 替代构造函数。代理在部署代理的同一笔交易中对它进行一次 delegatecall。
3. `_authorizeUpgrade(...)` 是必需的 UUPS 权限钩子。每次 `upgradeToAndCall` 时都会运行；如果它 revert，升级就会被阻止。

## 部署脚本

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

import {Script, console} from "forge-std/Script.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {CounterV1} from "../src/CounterV1.sol";

contract DeployProxyScript is Script {
    function run() public {
        address owner = vm.addr(vm.envUint("PRIVATE_KEY"));
        uint256 initialNumber = 42;

        vm.startBroadcast();

        // 1. Deploy the implementation.
        CounterV1 implementation = new CounterV1();

        // 2. Deploy the proxy, calling initialize(owner, 42) in the same tx.
        bytes memory initData = abi.encodeCall(CounterV1.initialize, (owner, initialNumber));
        ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);

        vm.stopBroadcast();

        console.log("Implementation:", address(implementation));
        console.log("Proxy         :", address(proxy));
        console.log("Owner         :", owner);
    }
}
```

该脚本部署逻辑合约，使用 `abi.encodeCall` 构造 `initialize` calldata（它在编译时对参数进行类型检查，与 `abi.encodeWithSignature` 不同），然后部署代理。代理的构造函数将实现地址存储在 ERC-1967 实现槽中，并对其进行 delegatecall `initData`，原子性地初始化代理的存储。

## 部署并验证两个合约

代理和实现是一个脚本中的两个合约，因此命令与任何多合约部署使用的 `forge script` 形式相同：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge script script/DeployProxy.s.sol:DeployProxyScript \
  --rpc-url        $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key    $PRIVATE_KEY \
  --broadcast --slow \
  --verify \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL
```

预期的输出末尾：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
##
Start verification for (2) contracts
Submitting verification for [src/CounterV1.sol:CounterV1] 0xeDa7...
	Response: `OK`
	Details: `Pass - Verified`
Submitting verification for [lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy] 0xb22e...
	Response: `OK`
	Details: `Pass - Verified`
All (2) contracts were verified!
```

注意第二次提交的路径：代理被验证为 `lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy`，即来自您 `lib/` 树中的 OpenZeppelin 合约，而不是 `src/` 中的合约。Foundry 会自动解析此路径，因为脚本的广播日志记录了每一个 `new` 及其字节码。

## 确认代理链接

当代理和实现都通过验证时，浏览器会通过代理解码调用。要确认代理正在委托，请直接读取 ERC-1967 实现槽：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
PROXY=<your-proxy-address>
IMPL_SLOT=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

cast storage $PROXY $IMPL_SLOT --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 0x000000000000000000000000<implementation address, 20 bytes>
```

然后通过代理调用实现的一个函数：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
cast call $PROXY "number()(uint256)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 42
```

在 Tenderly Dashboard 中，代理的地址页面会显示已验证的 `ERC1967Proxy` 源代码、带有已链接实现地址的代理标签，以及包含实现函数的 ABI 视图。调用 `proxy.increment()` 或 `proxy.setNumber(...)` 的交易在调用跟踪中会使用实现的函数名和参数标签进行解码。

<Note>
  如果浏览器显示代理，但没有使用实现的 ABI 解码调用，最常见的原因是两个合约中只有一个被验证。请对缺失的那个重新运行 `forge verify-contract`。
</Note>

## 升级实现

UUPS 升级通过继承自 `UUPSUpgradeable` 的 `upgradeToAndCall(newImpl, callData)` 发起。添加一个 V2：

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

import {CounterV1} from "./CounterV1.sol";

contract CounterV2 is CounterV1 {
    function decrement() external {
        number -= 1;
    }

    function version() external pure override returns (string memory) {
        return "v2";
    }
}
```

编写 V2 时的存储布局约束：

* 继承自 V1，或原样复制 V1 的存储布局。存储槽是位置相关的；在现有变量之前添加新的状态变量会破坏代理的状态。
* 新的状态变量放在 V1 现有变量之后，永远不要放在它们之间。
* 不要更改现有变量的类型或重新排序声明。

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

import {Script, console} from "forge-std/Script.sol";
import {CounterV1} from "../src/CounterV1.sol";
import {CounterV2} from "../src/CounterV2.sol";

contract UpgradeProxyScript is Script {
    function run() public {
        address proxy = vm.envAddress("PROXY");

        vm.startBroadcast();

        CounterV2 newImplementation = new CounterV2();
        CounterV1(proxy).upgradeToAndCall(address(newImplementation), bytes(""));

        vm.stopBroadcast();

        console.log("New implementation:", address(newImplementation));
        console.log("Proxy upgraded:    ", proxy);
    }
}
```

使用相同的 verify 标志运行它：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
PROXY=<your-proxy-address> \
forge script script/UpgradeProxy.s.sol:UpgradeProxyScript \
  --rpc-url        $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key    $PRIVATE_KEY \
  --broadcast --slow \
  --verify \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL
```

只有 `CounterV2` 被部署和验证（输出报告 `(1) contracts`）。代理地址不变；只有它的 ERC-1967 槽发生变化。脚本返回后：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
cast storage $PROXY $IMPL_SLOT --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 0x000000000000000000000000<CounterV2 address>

cast call $PROXY "version()(string)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# "v2"

cast call $PROXY "number()(uint256)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 42   <-- state preserved; only the logic changed
```

浏览器中的代理地址现在按照 `CounterV2` 的 ABI 进行解码。旧的 `CounterV1` 实现在其原地址上仍然处于已验证状态，但不再是活动的实现。

## 验证已经部署的代理

如果您在部署时没有使用 `--verify`，请为每个合约分别运行 `forge verify-contract`。

实现是通过 `new CounterV1()` 部署的，没有构造函数参数，因此不需要 `--constructor-args` 标志：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge verify-contract <IMPL_ADDRESS> src/CounterV1.sol:CounterV1 \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL \
  --watch
```

代理需要部署时使用的完全相同的构造函数参数，且经过 ABI 编码：

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
# ABI-encode: constructor(address implementation, bytes initData)
INIT_DATA=$(cast calldata "initialize(address,uint256)" <OWNER_ADDRESS> 42)

forge verify-contract <PROXY_ADDRESS> \
  lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL \
  --constructor-args $(cast abi-encode "constructor(address,bytes)" <IMPL_ADDRESS> $INIT_DATA) \
  --watch
```

需要正确处理的三个细节：

1. **合约路径使用 `lib/` 位置**，而不是 `src/`。代理是 OpenZeppelin 的代码，因此验证器要匹配的源代码位于 `lib/openzeppelin-contracts/...` 中。`forge script --verify` 会自动解析此路径；`forge verify-contract` 不会。
2. **`initData` 必须与部署时的 `initData` 完全匹配。** 即使部署后状态发生变化，构造函数参数依然是代理字节码构造时使用的原始 `initialize(owner, 42)` calldata。
3. **代理的构造函数签名是 `(address, bytes)`**，`forge verify-contract` 期望已经过 ABI 编码的形式，这正是 `cast abi-encode` 生成的。

## 后续步骤

* [部署和验证合约](/virtual-environments/develop/deploy-contracts)：在 Virtual Environment 上的基础 Foundry 与 Hardhat 验证流程。
* [使用 Foundry 验证智能合约](/contract-verification/foundry)：通过 Tenderly 的验证 API 在公共网络上验证合约。
* [验证代理合约](/contract-verification/hardhat-proxy)：针对 UUPS、Transparent 和 Beacon 代理的 Hardhat 插件流程。
