> ## 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.

# Verify proxy contracts with Foundry

> Deploy and verify a UUPS implementation and its ERC1967 proxy on a Virtual Environment with forge script, then re-verify after an upgrade.

A proxy deployment consists of two contracts: the proxy, which delegates every call, and the implementation, which holds the logic. Both must be verified separately. For any proxy that follows [ERC-1967](https://eips.ethereum.org/EIPS/eip-1967), the Tenderly explorer reads the standard implementation storage slot, so once both contracts are verified, calls to the proxy address are decoded with the implementation's ABI.

This guide deploys a UUPS implementation (`CounterV1`) behind an `ERC1967Proxy` on a [Virtual Environment](/virtual-environments/overview), verifies both in one `forge script` run, upgrades the implementation to `CounterV2`, and covers re-verifying a proxy that was deployed without verification. For the Hardhat equivalent, see [Verifying Proxy Contracts](/contract-verification/hardhat-proxy).

## Before you begin

* Complete the setup from [Deploy and verify contracts](/virtual-environments/develop/deploy-contracts): a Virtual Environment, a [funded deployer address](/virtual-environments/unlimited-faucet), and a `foundry.toml` with `cbor_metadata = true` and `bytecode_hash = "ipfs"`.
* Set the environment variables:

```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
```

* Install OpenZeppelin's upgradeable contracts alongside the base library:

```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
```

* Add both remappings to `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/
```

The two libraries serve different roles: `openzeppelin-contracts` provides the proxy (`ERC1967Proxy`), and `openzeppelin-contracts-upgradeable` provides the implementation building blocks (`Initializable`, `UUPSUpgradeable`, `OwnableUpgradeable`). The upgradeable versions replace constructors with initializer functions, because constructors don't run when state lives behind a proxy.

## The implementation contract

```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 {}
}
```

Three parts of this contract matter for the proxy pattern:

1. `constructor() { _disableInitializers(); }` locks the implementation. Only the proxy is supposed to hold state; this constructor makes a direct `initialize` call on the implementation address revert.
2. `initialize(...)` replaces the constructor. The proxy delegatecalls it once, in the same transaction that deploys the proxy.
3. `_authorizeUpgrade(...)` is the required UUPS permissioning hook. It runs on every `upgradeToAndCall`; if it reverts, the upgrade is blocked.

## The deploy script

```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);
    }
}
```

The script deploys the logic contract, builds the `initialize` calldata with `abi.encodeCall` (which type-checks the arguments at compile time, unlike `abi.encodeWithSignature`), and deploys the proxy. The proxy's constructor stores the implementation address in the ERC-1967 implementation slot and delegatecalls `initData` against it, atomically initializing the proxy's storage.

## Deploy and verify both contracts

The proxy and the implementation are two contracts in one script, so the command is the same `forge script` shape as any multi-contract deployment:

```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
```

Expected tail of the output:

```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!
```

Note the second submission path: the proxy is verified as `lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy`, the OpenZeppelin contract from your `lib/` tree, not a contract in `src/`. Foundry resolves this automatically because the script's broadcast log records every `new` and its bytecode.

## Confirm the proxy linkage

The explorer decodes calls through the proxy when both the proxy and the implementation are verified. To confirm the proxy is delegating, read the ERC-1967 implementation slot directly:

```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>
```

Then call an implementation function through the proxy:

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

In the Tenderly Dashboard, the proxy's address page shows the verified `ERC1967Proxy` source, a proxy tag with the linked implementation address, and an ABI view that includes the implementation's functions. Transactions calling `proxy.increment()` or `proxy.setNumber(...)` are decoded with the implementation's function names and parameter labels in the call trace.

<Note>
  If the explorer shows the proxy but doesn't decode calls with the implementation's ABI, the most common cause is that only one of the two contracts is verified. Re-run `forge verify-contract` on whichever is missing.
</Note>

## Upgrade the implementation

UUPS upgrades are issued through `upgradeToAndCall(newImpl, callData)`, inherited from `UUPSUpgradeable`. Add a 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";
    }
}
```

Storage layout constraints when writing a V2:

* Inherit from V1, or copy V1's storage layout verbatim. Storage slots are positional; adding a new state variable before an existing one corrupts the proxy's state.
* New state variables go after V1's existing variables, never between them.
* Don't change existing variable types or reorder declarations.

```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);
    }
}
```

Run it with the same verify flags:

```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
```

Only `CounterV2` is deployed and verified (the output reports `(1) contracts`). The proxy address doesn't change; only its ERC-1967 slot does. After the script returns:

```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
```

The proxy address in the explorer now decodes against `CounterV2`'s ABI. The old `CounterV1` implementation remains verified at its original address but is no longer the active implementation.

## Verify a proxy that's already deployed

If you deployed without `--verify`, run `forge verify-contract` for each contract separately.

The implementation was deployed via `new CounterV1()` with no constructor arguments, so no `--constructor-args` flag is needed:

```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
```

The proxy requires the exact constructor arguments used at deploy time, ABI-encoded:

```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
```

Three details to get right:

1. **The contract path uses the `lib/` location**, not `src/`. The proxy is OpenZeppelin's code, so the source the verifier matches against lives in `lib/openzeppelin-contracts/...`. `forge script --verify` resolves this automatically; `forge verify-contract` doesn't.
2. **`initData` must match the deploy-time `initData` exactly.** Even if state changed after deployment, the constructor argument is the original `initialize(owner, 42)` calldata the proxy bytecode was constructed with.
3. **The proxy's constructor signature is `(address, bytes)`**, and `forge verify-contract` expects the already-ABI-encoded form, which is what `cast abi-encode` produces.

## Next steps

* [Deploy and verify contracts](/virtual-environments/develop/deploy-contracts): the base Foundry and Hardhat verification flow on a Virtual Environment.
* [Smart Contract Verification Using Foundry](/contract-verification/foundry): verifying contracts on public networks through Tenderly's verification API.
* [Verifying Proxy Contracts](/contract-verification/hardhat-proxy): the Hardhat plugin flow for UUPS, Transparent, and Beacon proxies.
