> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tenderly.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Верификация прокси-контрактов с Foundry

> Разверните и верифицируйте UUPS-имплементацию и её ERC1967-прокси в Virtual Environment через forge script, затем повторно верифицируйте после апгрейда.

Развёртывание прокси состоит из двух контрактов: прокси, который делегирует каждый вызов, и имплементации, содержащей логику. Оба должны быть верифицированы отдельно. Для любого прокси, следующего [ERC-1967](https://eips.ethereum.org/EIPS/eip-1967), обозреватель Tenderly считывает стандартный слот хранилища имплементации, так что после верификации обоих контрактов вызовы к адресу прокси декодируются с помощью ABI имплементации.

Это руководство деплоит UUPS-имплементацию (`CounterV1`) за `ERC1967Proxy` в [Virtual Environment](/virtual-environments/overview), верифицирует обе за один запуск `forge script`, обновляет имплементацию до `CounterV2` и рассказывает, как повторно верифицировать прокси, задеплоенный без верификации. Для эквивалента в Hardhat см. [Verifying Proxy Contracts](/contract-verification/hardhat-proxy).

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

* Выполните настройку из [Деплой и верификация контрактов](/virtual-environments/develop/deploy-contracts): Virtual Environment, [пополненный адрес деплойера](/virtual-environments/unlimited-faucet) и `foundry.toml` с `cbor_metadata = true` и `bytecode_hash = "ipfs"`.
* Установите переменные окружения:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
export TENDERLY_VIRTUAL_TESTNET_RPC=...   # your Virtual Environment RPC URL
export TENDERLY_VERIFIER_URL=$TENDERLY_VIRTUAL_TESTNET_RPC/verify
export PRIVATE_KEY=...                    # deployer private key
```

* Установите upgradeable-контракты OpenZeppelin рядом с базовой библиотекой:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge install OpenZeppelin/openzeppelin-contracts --no-git --shallow
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-git --shallow
```

* Добавьте оба remapping в `remappings.txt`:

```bash title="remappings.txt" theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
forge-std/=lib/forge-std/src/
```

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

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

```solidity title="src/CounterV1.sol" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract CounterV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public number;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address owner_, uint256 initialNumber) external initializer {
        __Ownable_init(owner_);
        number = initialNumber;
    }

    function increment() external {
        number += 1;
    }

    function setNumber(uint256 newNumber) external {
        number = newNumber;
    }

    function version() external pure virtual returns (string memory) {
        return "v1";
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
```

Три части этого контракта важны для паттерна прокси:

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

## Deploy-скрипт

```solidity title="script/DeployProxy.s.sol" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {CounterV1} from "../src/CounterV1.sol";

contract DeployProxyScript is Script {
    function run() public {
        address owner = vm.addr(vm.envUint("PRIVATE_KEY"));
        uint256 initialNumber = 42;

        vm.startBroadcast();

        // 1. Deploy the implementation.
        CounterV1 implementation = new CounterV1();

        // 2. Deploy the proxy, calling initialize(owner, 42) in the same tx.
        bytes memory initData = abi.encodeCall(CounterV1.initialize, (owner, initialNumber));
        ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);

        vm.stopBroadcast();

        console.log("Implementation:", address(implementation));
        console.log("Proxy         :", address(proxy));
        console.log("Owner         :", owner);
    }
}
```

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

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

Прокси и имплементация — это два контракта в одном скрипте, поэтому команда имеет ту же форму `forge script`, что и любое развёртывание с несколькими контрактами:

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge script script/DeployProxy.s.sol:DeployProxyScript \
  --rpc-url        $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key    $PRIVATE_KEY \
  --broadcast --slow \
  --verify \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL
```

Ожидаемый хвост вывода:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
##
Start verification for (2) contracts
Submitting verification for [src/CounterV1.sol:CounterV1] 0xeDa7...
	Response: `OK`
	Details: `Pass - Verified`
Submitting verification for [lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy] 0xb22e...
	Response: `OK`
	Details: `Pass - Verified`
All (2) contracts were verified!
```

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

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

Обозреватель декодирует вызовы через прокси, когда и прокси, и имплементация верифицированы. Чтобы подтвердить, что прокси действительно делегирует, прочитайте слот имплементации ERC-1967 напрямую:

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
PROXY=<your-proxy-address>
IMPL_SLOT=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

cast storage $PROXY $IMPL_SLOT --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 0x000000000000000000000000<implementation address, 20 bytes>
```

Затем вызовите функцию имплементации через прокси:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
cast call $PROXY "number()(uint256)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 42
```

В Tenderly Dashboard страница адреса прокси показывает верифицированный исходник `ERC1967Proxy`, тег прокси со связанным адресом имплементации и представление ABI, включающее функции имплементации. Транзакции, вызывающие `proxy.increment()` или `proxy.setNumber(...)`, декодируются в call trace с именами функций и меток параметров имплементации.

<Note>
  Если обозреватель показывает прокси, но не декодирует вызовы с помощью ABI имплементации, самая частая причина — что верифицирован только один из двух контрактов. Перезапустите `forge verify-contract` на том, который отсутствует.
</Note>

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

Апгрейды UUPS выполняются через `upgradeToAndCall(newImpl, callData)`, унаследованный от `UUPSUpgradeable`. Добавьте V2:

```solidity title="src/CounterV2.sol" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {CounterV1} from "./CounterV1.sol";

contract CounterV2 is CounterV1 {
    function decrement() external {
        number -= 1;
    }

    function version() external pure override returns (string memory) {
        return "v2";
    }
}
```

Ограничения на схему хранилища при написании V2:

* Наследуйтесь от V1 или копируйте схему хранилища V1 буквально. Слоты хранилища позиционные; добавление новой переменной состояния перед существующей повреждает состояние прокси.
* Новые переменные состояния идут после существующих переменных V1, никогда между ними.
* Не изменяйте типы существующих переменных и не переупорядочивайте объявления.

```solidity title="script/UpgradeProxy.s.sol" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {CounterV1} from "../src/CounterV1.sol";
import {CounterV2} from "../src/CounterV2.sol";

contract UpgradeProxyScript is Script {
    function run() public {
        address proxy = vm.envAddress("PROXY");

        vm.startBroadcast();

        CounterV2 newImplementation = new CounterV2();
        CounterV1(proxy).upgradeToAndCall(address(newImplementation), bytes(""));

        vm.stopBroadcast();

        console.log("New implementation:", address(newImplementation));
        console.log("Proxy upgraded:    ", proxy);
    }
}
```

Запустите его с теми же флагами верификации:

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
PROXY=<your-proxy-address> \
forge script script/UpgradeProxy.s.sol:UpgradeProxyScript \
  --rpc-url        $TENDERLY_VIRTUAL_TESTNET_RPC \
  --private-key    $PRIVATE_KEY \
  --broadcast --slow \
  --verify \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL
```

Деплоится и верифицируется только `CounterV2` (в выводе указано `(1) contracts`). Адрес прокси не меняется; меняется только его слот ERC-1967. После возврата скрипта:

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
cast storage $PROXY $IMPL_SLOT --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 0x000000000000000000000000<CounterV2 address>

cast call $PROXY "version()(string)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# "v2"

cast call $PROXY "number()(uint256)" --rpc-url $TENDERLY_VIRTUAL_TESTNET_RPC
# 42   <-- state preserved; only the logic changed
```

Адрес прокси в обозревателе теперь декодируется по ABI `CounterV2`. Старая имплементация `CounterV1` остаётся верифицированной по своему оригинальному адресу, но больше не является активной имплементацией.

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

Если вы задеплоили без `--verify`, запустите `forge verify-contract` для каждого контракта отдельно.

Имплементация была задеплоена через `new CounterV1()` без аргументов конструктора, поэтому флаг `--constructor-args` не нужен:

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
forge verify-contract <IMPL_ADDRESS> src/CounterV1.sol:CounterV1 \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL \
  --watch
```

Прокси требует точные аргументы конструктора, использованные во время деплоя, в ABI-кодированной форме:

```bash showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
# ABI-encode: constructor(address implementation, bytes initData)
INIT_DATA=$(cast calldata "initialize(address,uint256)" <OWNER_ADDRESS> 42)

forge verify-contract <PROXY_ADDRESS> \
  lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
  --verifier       custom \
  --verifier-url   $TENDERLY_VERIFIER_URL \
  --constructor-args $(cast abi-encode "constructor(address,bytes)" <IMPL_ADDRESS> $INIT_DATA) \
  --watch
```

Три детали, которые важно сделать правильно:

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

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

* [Деплой и верификация контрактов](/virtual-environments/develop/deploy-contracts): базовый процесс верификации Foundry и Hardhat в Virtual Environment.
* [Верификация смарт-контрактов с помощью Foundry](/contract-verification/foundry): верификация контрактов в публичных сетях через API верификации Tenderly.
* [Верификация прокси-контрактов](/contract-verification/hardhat-proxy): процесс с плагином Hardhat для UUPS-, Transparent- и Beacon-прокси.
