Перейти к основному содержанию
Работает на: публичных сетях (mainnet и testnet). Чтобы верифицировать контракты в Virtual Environment, см. Деплой и верификация контрактов и Верификация прокси-контрактов с помощью Foundry.
Tenderly верифицирует смарт-контракты, развёрнутые через команды Foundry forge create, forge script и forge verify-contract, посредством своего API верификации, совместимого с Etherscan. Верификация бывает либо приватной (исходный код виден только внутри вашего проекта в Tenderly), либо публичной (виден всем, у кого есть ссылка).

Прежде чем начать

Вам понадобятся slug вашего аккаунта и проекта Tenderly, ключ доступа Tenderly (Dashboard → Account Settings → Authorization) и профинансированный аккаунт в целевой сети. Верификатор Tenderly сопоставляет хеш метаданных, который компилятор Solidity добавляет к развёрнутому байткоду, с исходным кодом, который вы отправляете. Сохраняйте метаданные в результатах компиляции и фиксируйте настройки компилятора, чтобы они не изменились между моментом деплоя и моментом верификации:
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 и bytecode_hash = "ipfs" — это значения по умолчанию в Foundry, но некоторые шаблоны их убирают; bytecode_hash = "none" ломает верификацию. Расхождение в настройках компилятора между деплоем и верификацией приводит к ошибке Bytecode does not match deployed contract. В примерах ниже используется Base Sepolia (chain ID 84532). Настройка окружения:
.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

URL верификатора

Каждая команда верификации Foundry принимает --verifier-url, указывающий на API верификации Tenderly, и --etherscan-api-key $TENDERLY_ACCESS_KEY для аутентификации:
https://api.tenderly.co/api/v1/account/$TENDERLY_ACCOUNT/project/$TENDERLY_PROJECT/etherscan/verify/network/$NETWORK_ID
Каждый сегмент URL соответствует значению, которое можно посмотреть в Dashboard:
СегментЧто это
account/$TENDERLY_ACCOUNTSlug вашего аккаунта Tenderly (пользователя или организации). Регистрозависимый.
project/$TENDERLY_PROJECTПроект, к которому будет привязан верифицированный контракт.
network/$NETWORK_IDСеть, в которой развёрнут контракт, в виде десятичного chain ID (1 для Ethereum Mainnet, 8453 для Base, 84532 для Base Sepolia).
без суффиксаПриватная верификация: контракт виден только внутри вашего проекта.
суффикс /publicПубличная верификация: исходный код контракта виден всем, у кого есть ссылка, без входа в Tenderly.

Приватная верификация

Приватно верифицированные контракты видны только участникам вашего проекта в разделе Contracts в 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 ожидает, что аргументы конструктора уже закодированы в ABI; используйте cast abi-encode, чтобы получить их. --watch опрашивает верификатор, пока верификация не завершится, и печатает результат.
При успехе Foundry печатает строку URL: https://etherscan.io/address/.... Это особенность отображения потока, совместимого с Etherscan; контракт был верифицирован в Tenderly, а не в Etherscan. Подтвердите это в Dashboard в разделе Contracts.

Публичная верификация

Замените URL верификатора на вариант с суффиксом /public; больше ничего менять не нужно. Страница с верифицированным исходным кодом станет доступна всем, у кого есть ссылка, без входа в Tenderly.
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
Публичная верификация необратима. Публично верифицированный контракт остаётся публичным. Приватная и публичная верификация — независимые записи: чтобы сделать приватно верифицированный контракт публичным, повторно верифицируйте его через URL /public.

Деплой и верификация одной командой

forge create и forge script принимают те же флаги встроенно, поэтому деплой и верификацию можно запустить одной командой. В отличие от forge verify-contract, обе они принимают аргументы конструктора как сырые значения и сами кодируют их в ABI:
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
Ставьте --constructor-args в конце. Он жадно поглощает остаток командной строки, поэтому любой флаг, поставленный после него, будет воспринят как ещё один аргумент конструктора.
Для деплоев нескольких контрактов используйте forge script с флагом --slow; каждый контракт, который развёртывает скрипт, верифицируется автоматически, а аргументы конструктора берутся из broadcast-лога. Без --slow пакетирование broadcast может отправить транзакцию до подтверждения предыдущей, из-за чего может произойти гонка с шагом верификации:
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

Верификация контракта, который вы не разворачивали

Когда контракт верифицирован в публичном обозревателе (Etherscan, Basescan), но не в вашем проекте Tenderly, клонируйте верифицированный исходный код локально с помощью forge clone и повторно верифицируйте его через API Tenderly:
showLineNumbers
forge clone $CONTRACT_ADDRESS -e $ETHERSCAN_API_KEY --chain 1
forge clone скачивает верифицированный исходный код из Etherscan-совместимого API исходной сети (используйте ключ соответствующего обозревателя для каждой сети), восстанавливает структуру проекта и фиксирует настройки компилятора, которые использовал исходный деплойер. Запустите forge build в клонированной директории, чтобы убедиться, что она собирается; ошибки разрешения импортов обычно связаны с remappings.txt. Затем верифицируйте в Tenderly:
showLineNumbers
forge verify-contract $CONTRACT_ADDRESS src/MyContract.sol:MyContract \
  --verifier-url       $TENDERLY_PRIVATE_VERIFIER_URL \
  --etherscan-api-key  $TENDERLY_ACCESS_KEY \
  --watch
Если верификация падает с ошибкой Bytecode does not match deployed contract, передайте исходные настройки компилятора явно. Все они перечислены на странице обозревателя, где контракт уже верифицирован, в разделе исходного кода контракта:
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

Контракты, живущие в директории lib

forge build компилирует только то, что достижимо из src/. Контракт, который существует только внутри зависимости в lib/ (прокси, стандартная реализация ERC), никогда не попадает в кеш сборки, и forge verify-contract падает с ошибкой:
Error: Failed to get standard json input
- cannot resolve file at "lib/openzeppelin-contracts-upgradeable/lib/..."
Создайте однострочный файл src/Imports.sol, который импортирует контракт, а затем пересоберите:
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 никогда не отправляется в верификатор; он лишь заставляет компилятор кешировать контракт и его зависимости. После этого верифицируйте, используя полный путь контракта в lib/, поскольку исходный код физически находится именно там. Для TransparentUpgradeableProxy, который вы не разворачивали, конструктор имеет вид (address _logic, address initialOwner, bytes _data), и большую его часть можно восстановить из состояния сети:
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
Версия компилятора, число прогонов оптимизатора и версия EVM берутся со страницы обозревателя уже верифицированной реализации. Если реализация нигде не верифицирована, обратитесь к foundry.toml или скриптам деплоя разворачивающего проекта.

Устранение неполадок

СимптомВероятная причинаРешение
unauthorizedОтсутствует или неверный --etherscan-api-key, либо ключ доступа был отозван.Сгенерируйте новый ключ доступа и заново задайте TENDERLY_ACCESS_KEY.
not foundНеверный slug аккаунта, slug проекта или chain ID в URL верификатора.Перепроверьте каждый сегмент URL. Slug аккаунта регистрозависимый.
Bytecode does not match deployed contractОтправленный исходный код компилируется иначе, чем то, что развёрнуто: несоответствие optimizer_runs, другая solc_version или версия EVM, либо срезанный хеш метаданных.Зафиксируйте solc_version, optimizer, optimizer_runs и bytecode_hash в foundry.toml или передайте --compiler-version --optimizer-runs --evm-version явно. Перед повторной верификацией выполните forge clean && forge build.
Failed to deserialize contentВерификатор вернул строку ошибки, которую Foundry не может распарсить, обычно оборачивающую ошибку авторизации или пути от вышестоящего сервиса.Перепроверьте URL и запустите повторно с -vvvv, чтобы увидеть сырой ответ.
Failed to get standard json input - cannot resolve file at lib/...Контракт нигде не импортируется из src/, поэтому отсутствует в кеше сборки.Создайте src/Imports.sol, который импортирует его, затем forge build --force. См. Контракты, живущие в директории lib.
Dry run enabled, not broadcasting transaction при forge createНе передан --broadcast, либо он проглочен флагом --constructor-args.Переместите --constructor-args в конец списка флагов.
Верифицируется только первый контракт при forge scriptГонка на RPC: вторая транзакция была отправлена до получения квитанции первой.Добавьте --slow.
Верификация успешна, но контракт не появляется в проектеНеверный slug проекта в URL верификатора.Повторно верифицируйте с правильным slug. Контракт попадёт в тот проект, на который указывал URL.