Verifying Proxy Contracts on Tenderly

The @tenderly/hardhat-tenderly plugin enables you to verify contracts in Tenderly, enabling full transaction tracing and other features. The plugin verifies the contracts whether they're deployed on a public network (both mainnets and testnets) a Tenderly fork or a Tenderly DevNet.

When working with proxy contracts, you need to verify the following:

  • The proxy contract (e.g. OpenZepplin's proxies)
  • Implementation behind the proxy
  • Any dependencies the implementation has
  • New implementation instances deployed with upgrades.

The verification process varies depending on the proxy contract type and approach to the implementation.

Tenderly Docs

Overview

In this guide, we'll use an example Hardhat project and the @tenderly/tenderly-hardhat plugin to demonstrate the verification of OpenZeppelin's UUPSUpgradeable, TransparentUpgradeableProxy, and BeaconProxy alternatives.

⚠️

Proxy contracts need to be verified manually. Turn off automatic verification like so:

example.ts
import * as tenderly from '@tenderly/hardhat-tenderly';
// use manual verification
tenderly.setup({ automaticVerifications: false });

To obtain the address of the deployed implementation, use the @openzeppelin/upgrades-core package and getImplementationAddress function.

Verifying the proxy implementation is usually straightforward; verify it just like any other contract:

example.ts
await tenderly.verify({
  // the new implementation contract
  name: 'VaultV2',
  // the address where implementation is deployed
  address: await getImplementationAddress(ethers.provider, await proxy.getAddress()),
});

To verify the proxy instance, you need to complete these two preliminary steps:

Once these steps are completed, you can proceed to verify the proxy just as you would with any other contract:

example.ts
await tenderly.verify({
  name: 'ERC1967Proxy', // or TransparentUpgradeableProxy or BeaconProxy
  address: await proxy.getAddress(),
});

Running the sample

Step 1: Clone the repo and install dependencies:

git clone git@github.com:Tenderly/tenderly-examples.git
cd contract-verifications
npm i

Step 2: Set up the Tenderly CLI:

brew tap tenderly/tenderly && brew install tenderly
tenderly login

Step 3: Modify your hardhat.config.ts file and update the tenderly.username and tenderly.project with your Tenderly username and project slug.

Step 4: The fastest way to deploy and verify contracts is to use Tenderly DevNets. Log into the Tenderly Dashboard and create a new DevNet template.

Tenderly Docs

Project and DevNet slug

Step 5: Spawn a DevNet from the template you created and use the generated RPC URL. You can spawn a DevNet and obtain the RPC in three ways:

  • Option 1: Click "Spawn DevNet" from the Dashboard and add the RPC link to networks.tenderly.url in the hardhat.config.ts file.
  • Option 2: Run the DevNet spawning command you copied from the Dashboard and paste the RPC link to networks.tenderly.url in hardhat.config.ts file.
  • Option 3: Run the spawn-devnet-to-hardhat script from the example project, which will spin up a fresh DevNet and update your Hardhat project automatically. Replace PROJECT_SLUG and DEVNET_TEMPLATE_SLUG with appropriate values. (see the screenshot)
example
npm run spawn-devnet-to-hardhat <PROJECT_SLUG> <DEVNET_TEMPLATE_SLUG>

Step 6: Finally, run the tests:

rm -rf .openzeppelin &&  npx hardhat test --network tenderly

Loading proxy contracts

To verify the proxy contract, create a DummyProxy.sol file and import the OpenZepplin proxy contracts you're working with. In doing so, these contracts are loaded and have passed through the compiler, enabling you to reference the exact contract source of the proxy during verification.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

abstract contract ERC1967ProxyAccess is ERC1967Proxy {}
abstract contract UpgradableBeaconAccess is UpgradeableBeacon {}
abstract contract BeaconProxyAccess is BeaconProxy {}
abstract contract TransparentUpgradeableProxyAccess is TransparentUpgradeableProxy {}

Configuring Solidity Compiler Overrides

The following overrides map was derived for @openzeppelin/contracts-upgradeable version 4.9.1. They may differ for other versions of the package.

After compiling Openzepplin's proxy contracts, you also need to specify the following:

  • Version of the Solidity compiler that was used to compile the contracts
  • Optimization settings used by Openzepplin's upgrades plugin when performing proxy deployment/upgrades
⚠️

The hardhat-tenderly plugin uses both the source code of smart contracts and compiler settings for verification. If either of these settings is incorrect, the verification will fail.

Add the following overrides map to the config.solidity section of your Hardhat User config object:

example.ts
const config: HardhatUserConfig = {
  solidity: {
    compilers: [{ version: '0.8.18' } /* OTHER COMPILER VERSIONS*/],
    overrides: {
      '@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol': {
        version: '0.8.9',
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
      '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol': {
        version: '0.8.9',
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
 
      '@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol': {
        version: '0.8.9',
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
      '@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol': {
        version: '0.8.9',
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
      'contracts/proxy.sol': {
        version: '0.8.9',
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
    },
  },
  /* OTHER CONFIG */
};

Example: Proxied Vault

Below are code samples showing the verification of a proxied Vault contract. The Vault references an ERC-20 token (TToken). We'll demonstrate how to verify both the implementation and the proxy using OpenZeppelin's UUPS, Transparent, and Beacon proxying methods.

Verifying a UUPS proxy

To verify the UUPS proxy and the underlying information, call the hardhat-tenderly plugin twice:

  1. To verify the implementation, you need to provide the following:
    • name of your proxied contract (in our case Vault)
    • Address where the contract was deployed using the getImplementationAddress method from @openzeppelin/upgrades-core.
  2. To verify the proxy, provide the following:
    • ERC1967Proxy as the proxy contract name
    • Address of the proxy proxy.address
example.ts
await tenderly.verify(
  {
    name: 'Vault',
    address: await getImplementationAddress(ethers.provider, await proxy.getAddress()),
  },
  {
    name: 'ERC1967Proxy',
    address: await proxy.getAddress(),
  },
);

Complete Code Sample

Here's a complete Hardhat test that does the following:

  • Deploys the TToken (needed for the vault)
  • Deploys Vault as a proxy, initialized with the TToken contract
  • Verifies the proxy (ERC1967Proxy) instance deployed at await proxy.getAddress()
  • Verifies the implementation instance Vault, deployed at getImplementationAddress(ethers.provider, await proxy.getAddress())
  • Upgrades the proxy to VaultV2
example.ts
import { getImplementationAddress } from '@openzeppelin/upgrades-core';
import { ethers, tenderly, upgrades } from 'hardhat';
import { Vault } from '../typechain-types';
 
describe('Vault', () => {
  it('uups proxy deployment and verification', async () => {
    const VaultFactory = await ethers.getContractFactory('Vault');
    const TokenFactory = await ethers.getContractFactory('TToken');
 
    let token = await ethers.deployContract('TToken');
    token = await token.waitForDeployment();
    const tokenAddress = await token.getAddress();
 
    await tenderly.verify({
      name: 'TToken',
      address: tokenAddress,
    });
 
    let proxy = await upgrades.deployProxy(VaultFactory, [tokenAddress], {
      kind: 'uups',
    });
    await proxy.waitForDeployment();
    const proxyAddress = await proxy.getAddress();
 
    console.log('Deployed UUPS ', {
      proxy: proxyAddress,
      implementation: await getImplementationAddress(ethers.provider, proxyAddress),
    });
 
    await tenderly.verify(
      {
        name: 'Vault',
        address: await getImplementationAddress(ethers.provider, proxyAddress),
      },
      {
        name: 'ERC1967Proxy',
        address: proxyAddress,
      },
    );
 
    // upgrade
    const vaultV2Factory = await ethers.getContractFactory('VaultV2');
    proxy = (await upgrades.upgradeProxy(proxy, vaultV2Factory, {
      kind: 'uups',
    })) as Vault;
 
    await proxy.waitForDeployment();
 
    console.log('Upgraded UUPS ', {
      proxy: proxyAddress,
      implementation: await getImplementationAddress(ethers.provider, proxyAddress),
    });
 
    await tenderly.verify({
      name: 'VaultV2',
      address: await getImplementationAddress(ethers.provider, proxyAddress),
    });
  });
});

Verifying a TransparentUpgradable Proxy

To verify the UUPS proxy and the underlying information, call hardhat-tenderly while passing two contracts: Vault for the implementation, and TransparentUpgradeableProxy for the proxy itself:

example.ts
await tenderly.verify(
  {
    name: 'Vault',
    address: await getImplementationAddress(ethers.provider, await proxy.getAddress()),
  },
  {
    name: 'TransparentUpgradeableProxy',
    address: await proxy.getAddress(),
  },
);
  1. To verify the implementation, provide the following:
    • name of your proxied contract (in our case Vault)
    • Address where the contract was deployed, using the getImplementationAddress method from @openzeppelin/upgrades-core.
  2. To verify the proxy, provide the following:
    • TransparentUpgradeableProxy as the proxy contract name
    • Address of the proxy await proxy.getAddress()

Complete Code Sample

example.ts
import { getImplementationAddress } from '@openzeppelin/upgrades-core';
import { ethers, tenderly, upgrades } from 'hardhat';
import { Vault } from '../typechain-types';
 
describe('Vault', () => {
  it('transparent upgradable proxy deployment and verification', async () => {
    const VaultFactory = await ethers.getContractFactory('Vault');
    const TokenFactory = await ethers.getContractFactory('TToken');
 
    let token = await ethers.deployContract('TToken');
    token = await token.waitForDeployment();
    const tokenAddress = await token.getAddress();
 
    await tenderly.verify({
      name: 'TToken',
      address: tokenAddress,
    });
 
    let proxy = await upgrades.deployProxy(VaultFactory, [tokenAddress], {
      kind: 'transparent',
    });
    await proxy.waitForDeployment();
    const proxyAddress = await proxy.getAddress();
 
    console.log('Deployed transparent', {
      proxy: proxyAddress,
      implementation: await getImplementationAddress(ethers.provider, proxyAddress),
    });
 
    await tenderly.verify(
      {
        name: 'Vault',
        address: await getImplementationAddress(ethers.provider, proxyAddress),
      },
      {
        name: 'TransparentUpgradeableProxy',
        address: proxyAddress,
      },
    );
 
    // upgrade
    const vaultV2Factory = await ethers.getContractFactory('VaultV2');
 
    proxy = (await upgrades.upgradeProxy(proxy, vaultV2Factory, {
      kind: 'transparent',
    })) as Vault;
 
    await proxy.waitForDeployment();
 
    console.log('Upgraded transparent ', {
      proxy: proxyAddress,
      implementation: await getImplementationAddress(ethers.provider, proxyAddress),
    });
 
    await tenderly.verify({
      name: 'VaultV2',
      address: await getImplementationAddress(ethers.provider, proxyAddress),
    });
  });
});

Verifying a Beacon Proxy

To verify the Beacon proxy and the underlying information, you have to verify two contracts: the Vault (implementation) and OpenZepplin's UpgradableBeacon:

example.ts
await tenderly.verify(
  {
    name: 'Vault',
    address: await getImplementationAddressFromBeacon(ethers.provider, await beacon.getAddress()),
  },
  {
    name: 'UpgradeableBeacon',
    address: await beacon.getAddress(),
  },
);

Complete Code Sample

example.ts
import { getImplementationAddressFromBeacon } from '@openzeppelin/upgrades-core';
import { ethers, tenderly, upgrades } from 'hardhat';
import { BeaconProxy, UpgradeableBeacon } from '../typechain-types';
 
describe('Vault', () => {
  it('beacon proxy deployment and verification', async () => {
    const VaultFactory = await ethers.getContractFactory('Vault');
    const TokenFactory = await ethers.getContractFactory('TToken');
 
    let token = await ethers.deployContract('TToken');
    token = await token.waitForDeployment();
    const tokenAddress = await token.getAddress();
 
    await tenderly.verify({
      name: 'TToken',
      address: tokenAddress,
    });
 
    let beacon = (await upgrades.deployBeacon(VaultFactory)) as UpgradeableBeacon;
 
    await beacon.waitForDeployment();
    const beaconAddress = await beacon.getAddress();
 
    let vault = await upgrades.deployBeaconProxy(beacon, VaultFactory, [tokenAddress], {
      initializer: 'initialize',
    });
    await vault.waitForDeployment();
 
    console.log('Deployed beacon ', {
      proxy: beaconAddress,
      implementation: await getImplementationAddressFromBeacon(ethers.provider, beaconAddress),
      beacon: beaconAddress,
    });
 
    await tenderly.verify(
      {
        name: 'Vault',
        address: await getImplementationAddressFromBeacon(ethers.provider, beaconAddress),
      },
      {
        name: 'UpgradeableBeacon',
        address: beaconAddress,
      },
    );
 
    const vaultV2Factory = await ethers.getContractFactory('VaultV2');
 
    // upgrade
    vault = await upgrades.deployBeaconProxy(beacon, vaultV2Factory, [tokenAddress]);
 
    await upgrades.upgradeBeacon(beaconAddress, vaultV2Factory, {});
 
    console.log('Upgraded beacon ', {
      proxy: beaconAddress,
      implementation: await getImplementationAddressFromBeacon(ethers.provider, beaconAddress),
      beacon: beaconAddress,
    });
 
    await tenderly.verify({
      name: 'VaultV2',
      address: await getImplementationAddressFromBeacon(ethers.provider, beaconAddress),
    });
  });
});