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

# Smart Contract Verification Using Foundry

> Verify Foundry-deployed contracts on public networks through Tenderly's verification API, privately or publicly, with forge verify-contract.

<Info>
  **Works on:** public networks (mainnets and testnets). To verify contracts on a Virtual Environment, see [Deploy and verify contracts](/virtual-environments/develop/deploy-contracts) and [Verify proxy contracts with Foundry](/virtual-environments/develop/verify-proxy-contracts).
</Info>

Tenderly verifies smart contracts deployed with Foundry's `forge create`, `forge script`, and `forge verify-contract` commands through its Etherscan-compatible verification API. Verification is either private (the source is visible only inside your Tenderly project) or public (visible to anyone with the link).

## Before you begin

You need your Tenderly account and project [slugs](/platform/account/projects/slug), a [Tenderly access key](/platform/account/projects/api-tokens) (Dashboard → Account Settings → Authorization), and a funded account on the target network.

Tenderly's verifier matches the metadata hash the Solidity compiler appends to deployed bytecode against the source you submit. Keep the metadata in the compiled output, and pin the compiler settings so they can't drift between deploy time and verify time:

```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` and `bytecode_hash = "ipfs"` are Foundry's defaults, but some templates strip them; `bytecode_hash = "none"` breaks verification. Compiler-setting drift between deploy and verify causes `Bytecode does not match deployed contract` failures.

The examples below use Base Sepolia (chain ID `84532`). Set up the environment:

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

## Verifier URL

Every Foundry verification command takes `--verifier-url` pointing at Tenderly's verification API, and `--etherscan-api-key $TENDERLY_ACCESS_KEY` for authentication:

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

Each URL segment maps to a value you can read off the Dashboard:

| Segment                     | What it is                                                                                                                          |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `account/$TENDERLY_ACCOUNT` | Your Tenderly account (user or organization) [slug](/platform/account/projects/slug). Case-sensitive.                               |
| `project/$TENDERLY_PROJECT` | The project the verified contract is filed under.                                                                                   |
| `network/$NETWORK_ID`       | The chain the contract is deployed on, as a decimal chain ID (`1` for Ethereum Mainnet, `8453` for Base, `84532` for Base Sepolia). |
| no suffix                   | Private verification: the contract is visible only inside your project.                                                             |
| `/public` suffix            | Public verification: the contract source is visible to anyone with the link, no Tenderly login required.                            |

## Private verification

Privately verified contracts are visible only to your project's members, under **Contracts** in the Dashboard.

```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` expects constructor arguments **already ABI-encoded**; use `cast abi-encode` to produce them. `--watch` polls the verifier until verification finishes and prints the result.

<Note>
  Foundry's output prints a `URL: https://etherscan.io/address/...` line on success. This is a display quirk of the Etherscan-compatible flow; the contract was verified at Tenderly, not Etherscan. Confirm in the Dashboard under **Contracts**.
</Note>

## Public verification

Swap the verifier URL for the `/public`-suffixed variant; nothing else changes. The verified source page becomes reachable by anyone with the link, without a Tenderly login.

```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 verification is irreversible. A contract verified publicly stays public. Private and public verifications are independent records: to make a privately verified contract public, re-verify it against the `/public` URL.
</Warning>

## Deploy and verify in one step

`forge create` and `forge script` accept the same flags inline, so deployment and verification run as one command. Unlike `forge verify-contract`, both take constructor arguments as raw values and ABI-encode them for you:

```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>
  Put `--constructor-args` last. It greedily consumes the rest of the command line, so any flag placed after it is treated as another constructor argument.
</Warning>

For multi-contract deployments, use `forge script` with `--slow`; every contract the script deploys is verified automatically with its constructor arguments taken from the broadcast log. Without [`--slow`](https://www.getfoundry.sh/reference/forge/script?highlight=slow#forge-script), broadcast batching can submit a transaction before the previous one is confirmed, which can race the verification step:

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

## Verify a contract you didn't deploy

When a contract is verified on a public explorer (Etherscan, Basescan) but not in your Tenderly project, clone the verified source locally with [`forge clone`](https://www.getfoundry.sh/reference/forge/clone) and re-verify it through Tenderly's API:

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

`forge clone` downloads the verified source from the source chain's Etherscan-compatible API (use the matching explorer's key for each chain), reconstructs the project layout, and pins the compiler settings the original deployer used. Run `forge build` in the cloned directory to confirm it compiles; import-resolution errors usually trace back to `remappings.txt`.

Then verify against 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
```

If verification fails with `Bytecode does not match deployed contract`, pass the original compiler settings explicitly. All of them are listed on the explorer page where the contract is already verified, under the contract source code section:

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

### Contracts that live in the `lib` directory

`forge build` only compiles what's reachable from `src/`. A contract that exists solely inside a `lib/` dependency (a proxy, a standard ERC implementation) never enters the build cache, and `forge verify-contract` fails with:

```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/..."
```

Create a one-line `src/Imports.sol` that imports the contract, then rebuild:

```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` is never sent to the verifier; it only forces the compiler to cache the contract and its dependencies. Then verify using the contract's full `lib/` path, since that's where the source physically lives.

For a `TransparentUpgradeableProxy` you didn't deploy, the constructor is `(address _logic, address initialOwner, bytes _data)`, and most of it can be reconstructed from chain state:

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

Compiler version, optimizer runs, and EVM version come from the explorer page of the already-verified implementation. If the implementation isn't verified anywhere, fall back to the deploying project's `foundry.toml` or deployment scripts.

## Troubleshooting

| Symptom                                                              | Likely cause                                                                                                                                                          | Fix                                                                                                                                                                                                                     |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unauthorized`                                                       | Missing or wrong `--etherscan-api-key`, or the access key was revoked.                                                                                                | Generate a new [access key](/platform/account/projects/api-tokens) and re-set `TENDERLY_ACCESS_KEY`.                                                                                                                    |
| `not found`                                                          | Wrong account slug, project slug, or chain ID in the verifier URL.                                                                                                    | Re-check each URL segment. The account slug is case-sensitive.                                                                                                                                                          |
| `Bytecode does not match deployed contract`                          | The submitted source compiled differently than what's deployed: an `optimizer_runs` mismatch, a different `solc_version` or EVM version, or a stripped metadata hash. | Pin `solc_version`, `optimizer`, `optimizer_runs`, and `bytecode_hash` in `foundry.toml`, or pass `--compiler-version --optimizer-runs --evm-version` explicitly. Run `forge clean && forge build` before re-verifying. |
| `Failed to deserialize content`                                      | The verifier returned an error string Foundry can't parse, usually wrapping an upstream auth or path error.                                                           | Re-check the URL and re-run with `-vvvv` to see the raw response.                                                                                                                                                       |
| `Failed to get standard json input - cannot resolve file at lib/...` | The contract is never imported from `src/`, so it's missing from the build cache.                                                                                     | Create a `src/Imports.sol` that imports it, then `forge build --force`. See [Contracts that live in the `lib` directory](#contracts-that-live-in-the-lib-directory).                                                    |
| `Dry run enabled, not broadcasting transaction` on `forge create`    | `--broadcast` not passed, or swallowed by `--constructor-args`.                                                                                                       | Move `--constructor-args` to be the last flag.                                                                                                                                                                          |
| Only the first contract verifies on `forge script`                   | RPC race: the second transaction was submitted before the first receipt.                                                                                              | Add `--slow`.                                                                                                                                                                                                           |
| Verification succeeds but the contract doesn't appear in the project | Wrong project slug in the verifier URL.                                                                                                                               | Re-verify with the correct slug. The contract is filed under whichever project the URL pointed at.                                                                                                                      |
