Skip to main content
Works on: public networks (mainnets and testnets). To verify contracts on a Virtual Environment, see Deploy and verify contracts and Verify proxy contracts with Foundry.
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, a Tenderly access key (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:
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 = 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:
.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

Verifier URL

Every Foundry verification command takes --verifier-url pointing at Tenderly’s verification API, and --etherscan-api-key $TENDERLY_ACCESS_KEY for authentication:
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:
SegmentWhat it is
account/$TENDERLY_ACCOUNTYour Tenderly account (user or organization) slug. Case-sensitive.
project/$TENDERLY_PROJECTThe project the verified contract is filed under.
network/$NETWORK_IDThe chain the contract is deployed on, as a decimal chain ID (1 for Ethereum Mainnet, 8453 for Base, 84532 for Base Sepolia).
no suffixPrivate verification: the contract is visible only inside your project.
/public suffixPublic 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.
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 expects constructor arguments already ABI-encoded; use cast abi-encode to produce them. --watch polls the verifier until verification finishes and prints the result.
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.

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

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:
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
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.
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, broadcast batching can submit a transaction before the previous one is confirmed, which can race the verification step:
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

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 and re-verify it through Tenderly’s API:
showLineNumbers
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:
showLineNumbers
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:
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

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

SymptomLikely causeFix
unauthorizedMissing or wrong --etherscan-api-key, or the access key was revoked.Generate a new access key and re-set TENDERLY_ACCESS_KEY.
not foundWrong 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 contractThe 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 contentThe 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.
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 scriptRPC race: the second transaction was submitted before the first receipt.Add --slow.
Verification succeeds but the contract doesn’t appear in the projectWrong project slug in the verifier URL.Re-verify with the correct slug. The contract is filed under whichever project the URL pointed at.