Skip to main content
Deploy Solidity contracts to a Virtual Environment using Foundry or Hardhat. Contracts deployed on a Virtual Environment are called virtual contracts and appear in the Contracts section under the Virtual Contracts tab. Verify contracts as you deploy them. Verified contracts unlock the Debugger, Gas Profiler, Simulator, and richer transaction views in the explorer.

Before you begin

Verification on a Virtual Environment needs no separate access key: the RPC URL itself authenticates, and the verifier endpoint is that URL with /verify appended.
By default, contracts verified on a Virtual Environment are private. Depending on the visibility setting of the public explorer, the code may be visible externally. Check the public explorer toggle and verification visibility before verifying anything you want to keep private.

Deploy and verify

Configure foundry.toml

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 by setting cbor_metadata and bytecode_hash explicitly:
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. Pin solc_version and the optimizer settings so re-verification compiles the same bytecode.If your project uses libraries such as OpenZeppelin, declare the remappings explicitly in remappings.txt:
remappings.txt
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
forge-std/=lib/forge-std/src/
With auto-detected remappings, the verification payload can contain different remapping strings than the build used, which causes bytecode-mismatch failures.

Set environment variables

showLineNumbers
export TENDERLY_VIRTUAL_TESTNET_RPC=...   # paste your RPC URL
export TENDERLY_VERIFIER_URL=$TENDERLY_VIRTUAL_TESTNET_RPC/verify
export PRIVATE_KEY=...                    # deployer private key
Sanity-check the connection before deploying. cast chain-id should print the Virtual Environment’s chain ID, and the deployer balance should be non-zero:
cast chain-id --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
cast balance <DEPLOYER_ADDRESS> --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC

Deploy with forge create

Run forge create with the verify flags. Constructor arguments are passed as raw values; Foundry ABI-encodes them and submits the encoded form to the verifier:
showLineNumbers
forge create src/Counter.sol:Counter \
  --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --verify \
  --verifier custom \
  --verifier-url $TENDERLY_VERIFIER_URL \
  --constructor-args 42
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.

Deploy with a Foundry script

Use forge script to deploy multiple contracts in one run. Every new Foo(...) in the script is submitted for verification automatically, with the correct constructor arguments taken from the broadcast log. Imports are handled transparently: the verifier receives a standard-JSON payload containing every source file the compiler touched, so local imports and library imports (such as OpenZeppelin) verify without extra steps.Use --slow so transactions are sent one at a time. Without it, broadcast batching can submit a transaction before the previous one is confirmed, which is flaky against a hosted RPC and can race the verification step.
showLineNumbers
forge script script/Counter.s.sol:CounterScript \
  --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --slow \
  --verify \
  --verifier custom \
  --verifier-url $TENDERLY_VERIFIER_URL

Verify an existing contract

Already-deployed contracts can be verified with forge verify-contract. Unlike forge create, this command expects constructor arguments already ABI-encoded; use cast abi-encode to produce them:
showLineNumbers
forge verify-contract $COUNTER_ADDRESS src/Counter.sol:Counter \
  --verifier custom \
  --verifier-url $TENDERLY_VERIFIER_URL \
  --constructor-args $(cast abi-encode "constructor(uint256)" 42) \
  --watch
--watch polls the verifier until verification finishes and prints the result. For multi-contract deployments, run forge verify-contract once per deployed address.

Check the dashboard

In the Tenderly Dashboard, open Contracts and select the Virtual Contracts tab. Your newly deployed contracts appear there, with verification status visible per contract.

Troubleshooting

SymptomLikely causeFix
unauthorized / -32004 response from the verifierWrong verifier URL. A common mistake is appending /verify/etherscan instead of /verify.Make sure the verifier URL ends in exactly /verify.
Bytecode does not match deployed contractThe submitted source compiled differently than what’s deployed: an optimizer_runs mismatch, a different solc_version, or a stripped metadata hash.Pin solc_version, optimizer, optimizer_runs, and bytecode_hash in foundry.toml. 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.
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.
Verifier reports OK but the explorer still shows unverifiedUI caching.Hard-refresh the explorer page. The status in the API response is authoritative.

Next steps