Перейти к основному содержанию
Развёртывание прокси состоит из двух контрактов: прокси, который делегирует каждый вызов, и имплементации, содержащей логику. Оба должны быть верифицированы отдельно. Для любого прокси, следующего ERC-1967, обозреватель Tenderly считывает стандартный слот хранилища имплементации, так что после верификации обоих контрактов вызовы к адресу прокси декодируются с помощью ABI имплементации. Это руководство деплоит UUPS-имплементацию (CounterV1) за ERC1967Proxy в Virtual Environment, верифицирует обе за один запуск forge script, обновляет имплементацию до CounterV2 и рассказывает, как повторно верифицировать прокси, задеплоенный без верификации. Для эквивалента в Hardhat см. Verifying Proxy Contracts.

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

  • Установите upgradeable-контракты OpenZeppelin рядом с базовой библиотекой:
  • Добавьте оба remapping в remappings.txt:
remappings.txt
Две библиотеки выполняют разные роли: openzeppelin-contracts предоставляет прокси (ERC1967Proxy), а openzeppelin-contracts-upgradeable — строительные блоки имплементации (Initializable, UUPSUpgradeable, OwnableUpgradeable). Upgradeable-версии заменяют конструкторы функциями инициализации, потому что конструкторы не выполняются, когда состояние живёт за прокси.

Контракт имплементации

src/CounterV1.sol
Три части этого контракта важны для паттерна прокси:
  1. constructor() { _disableInitializers(); } блокирует имплементацию. Только прокси должен держать состояние; этот конструктор делает так, что прямой вызов initialize по адресу имплементации завершается revert.
  2. initialize(...) заменяет конструктор. Прокси делает по нему delegatecall один раз, в той же транзакции, что деплоит прокси.
  3. _authorizeUpgrade(...) — обязательный UUPS-хук для авторизации. Он выполняется при каждом upgradeToAndCall; если он делает revert, апгрейд блокируется.

Deploy-скрипт

script/DeployProxy.s.sol
Скрипт деплоит контракт логики, собирает calldata для initialize с помощью abi.encodeCall (который проверяет типы аргументов на этапе компиляции, в отличие от abi.encodeWithSignature) и деплоит прокси. Конструктор прокси сохраняет адрес имплементации в слоте имплементации ERC-1967 и делает delegatecall с initData против неё, атомарно инициализируя хранилище прокси.

Деплой и верификация обоих контрактов

Прокси и имплементация — это два контракта в одном скрипте, поэтому команда имеет ту же форму forge script, что и любое развёртывание с несколькими контрактами:
showLineNumbers
Ожидаемый хвост вывода:
Обратите внимание на путь второго сабмита: прокси верифицируется как lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy, контракт OpenZeppelin из вашего дерева lib/, а не контракт в src/. Foundry разрешает это автоматически, потому что broadcast log скрипта записывает каждое new и его байткод.

Подтвердите связь прокси

Обозреватель декодирует вызовы через прокси, когда и прокси, и имплементация верифицированы. Чтобы подтвердить, что прокси действительно делегирует, прочитайте слот имплементации ERC-1967 напрямую:
showLineNumbers
Затем вызовите функцию имплементации через прокси:
В Tenderly Dashboard страница адреса прокси показывает верифицированный исходник ERC1967Proxy, тег прокси со связанным адресом имплементации и представление ABI, включающее функции имплементации. Транзакции, вызывающие proxy.increment() или proxy.setNumber(...), декодируются в call trace с именами функций и меток параметров имплементации.
Если обозреватель показывает прокси, но не декодирует вызовы с помощью ABI имплементации, самая частая причина — что верифицирован только один из двух контрактов. Перезапустите forge verify-contract на том, который отсутствует.

Апгрейд имплементации

Апгрейды UUPS выполняются через upgradeToAndCall(newImpl, callData), унаследованный от UUPSUpgradeable. Добавьте V2:
src/CounterV2.sol
Ограничения на схему хранилища при написании V2:
  • Наследуйтесь от V1 или копируйте схему хранилища V1 буквально. Слоты хранилища позиционные; добавление новой переменной состояния перед существующей повреждает состояние прокси.
  • Новые переменные состояния идут после существующих переменных V1, никогда между ними.
  • Не изменяйте типы существующих переменных и не переупорядочивайте объявления.
script/UpgradeProxy.s.sol
Запустите его с теми же флагами верификации:
showLineNumbers
Деплоится и верифицируется только CounterV2 (в выводе указано (1) contracts). Адрес прокси не меняется; меняется только его слот ERC-1967. После возврата скрипта:
showLineNumbers
Адрес прокси в обозревателе теперь декодируется по ABI CounterV2. Старая имплементация CounterV1 остаётся верифицированной по своему оригинальному адресу, но больше не является активной имплементацией.

Верификация уже задеплоенного прокси

Если вы задеплоили без --verify, запустите forge verify-contract для каждого контракта отдельно. Имплементация была задеплоена через new CounterV1() без аргументов конструктора, поэтому флаг --constructor-args не нужен:
showLineNumbers
Прокси требует точные аргументы конструктора, использованные во время деплоя, в ABI-кодированной форме:
showLineNumbers
Три детали, которые важно сделать правильно:
  1. Путь контракта использует расположение lib/, а не src/. Прокси — это код OpenZeppelin, поэтому исходник, с которым сравнивает верификатор, лежит в lib/openzeppelin-contracts/.... forge script --verify разрешает это автоматически; forge verify-contract — нет.
  2. initData должен точно совпадать с initData во время деплоя. Даже если состояние изменилось после развёртывания, аргумент конструктора — это оригинальная calldata initialize(owner, 42), с которой был сконструирован байткод прокси.
  3. Сигнатура конструктора прокси — (address, bytes), а forge verify-contract ожидает уже ABI-кодированную форму, которую и производит cast abi-encode.

Дальнейшие шаги