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.
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
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
Verifier URL
Every Foundry verification command takes--verifier-url pointing at Tenderly’s verification API, and --etherscan-api-key $TENDERLY_ACCESS_KEY for authentication:
| Segment | What it is |
|---|---|
account/$TENDERLY_ACCOUNT | Your Tenderly account (user or organization) 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.showLineNumbers
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
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 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
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 withforge clone and re-verify it through Tenderly’s API:
showLineNumbers
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
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
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:
src/Imports.sol that imports the contract, then rebuild:
src/Imports.sol
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
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 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. |
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. |