or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

contract-interaction.mdcore-deployment.mddeployment-management.mddiamond-deployment.mdindex.mdnamed-accounts.mdproxy-deployment.mdtest-fixtures.mdverification.md
tile.json

verification.mddocs/

Contract Verification

Automated contract verification with Etherscan and Sourcify, including source submission and verification status management.

Capabilities

Etherscan Verification

Submit contract source code to Etherscan for automatic verification and public access.

/**
 * Submit contract sources to Etherscan for verification
 * @param deploymentName - Name of deployed contract to verify
 * @param options - Verification options
 * @returns Promise that resolves when verification is submitted
 */
submitSources(
  deploymentName: string,
  options?: {
    apiKey?: string;
    apiUrl?: string;
    license?: string;
    forceLicense?: boolean;
    solcInput?: boolean;
    sleep?: number;
  }
): Promise<void>;

/**
 * Verify all deployed contracts on Etherscan
 * @param options - Global verification options
 * @returns Promise that resolves when all verifications are submitted
 */
etherscanVerify(options?: {
  apiKey?: string;
  apiUrl?: string;
  license?: string;
  forceLicense?: boolean;
  solcInput?: boolean;
  sleep?: number;
}): Promise<void>;

Usage Examples:

// Verify single contract
await deployments.deploy("MyContract", {
  from: deployer,
  args: ["constructor", "args"],
  log: true,
});

// Submit to Etherscan for verification
await submitSources("MyContract", {
  apiKey: process.env.ETHERSCAN_API_KEY,
  license: "MIT",
  sleep: 1000, // Wait 1s between submissions
});

// Verify all contracts
await etherscanVerify({
  apiKey: process.env.ETHERSCAN_API_KEY,
  license: "MIT",
  solcInput: true, // Use full solc input if minimal sources fail
  sleep: 2000, // Wait 2s between verifications
});

// Network-specific verification
const networkName = deployments.getNetworkName();
const config = hre.config.networks[networkName];
const verifyConfig = config.verify?.etherscan;

if (verifyConfig) {
  await etherscanVerify({
    apiKey: verifyConfig.apiKey,
    apiUrl: verifyConfig.apiUrl,
    license: "SPDX-License-Identifier: MIT",
  });
}

// Conditional verification for production networks
const { deploy } = deployments;
const deployment = await deploy("ProductionContract", {
  from: deployer,
  args: [tokenAddress, admin],
  log: true,
});

if (deployment.newlyDeployed && hre.network.tags?.production) {
  console.log("Submitting contract for verification...");
  await submitSources("ProductionContract", {
    apiKey: process.env.ETHERSCAN_API_KEY,
    license: "Business Source License 1.1",
    forceLicense: true, // Override source license
  });
}

Sourcify Verification

Submit contract metadata to Sourcify for decentralized verification and IPFS storage.

/**
 * Submit contract sources to Sourcify for verification
 * @param deploymentName - Name of deployed contract to verify
 * @param options - Sourcify submission options
 * @returns Promise that resolves when sources are submitted
 */
submitSourcesToSourcify(
  deploymentName: string,
  options?: {
    endpoint?: string;
    writeFailingMetadata?: boolean;
  }
): Promise<void>;

/**
 * Verify all deployed contracts with Sourcify
 * @param options - Global Sourcify options
 * @returns Promise that resolves when all sources are submitted
 */
sourcifyVerify(options?: {
  endpoint?: string;
  writeFailingMetadata?: boolean;
}): Promise<void>;

Usage Examples:

// Submit single contract to Sourcify
await deployments.deploy("OpenSourceContract", {
  from: deployer,
  args: ["public", "contract"],
  log: true,
});

await submitSourcesToSourcify("OpenSourceContract", {
  endpoint: "https://sourcify.dev/server/",
  writeFailingMetadata: true, // Write metadata files on failure for debugging
});

// Verify all contracts with Sourcify
await sourcifyVerify({
  endpoint: "https://repo.sourcify.dev/",
  writeFailingMetadata: false,
});

// Custom Sourcify instance
await sourcifyVerify({
  endpoint: "https://custom-sourcify.example.com/",
});

// Error handling with metadata writing
try {
  await submitSourcesToSourcify("ComplexContract");
} catch (error) {
  console.error("Sourcify verification failed:", error);
  
  // Retry with metadata debugging
  await submitSourcesToSourcify("ComplexContract", {
    writeFailingMetadata: true, // This will write metadata files for inspection
  });
}

// Batch verification with error handling
const contractNames = ["Token", "Sale", "Governance"];
for (const contractName of contractNames) {
  try {
    await submitSourcesToSourcify(contractName);
    console.log(`✓ ${contractName} submitted to Sourcify`);
  } catch (error) {
    console.error(`✗ Failed to submit ${contractName}:`, error);
  }
}

Verification Configuration

Configure verification settings in hardhat.config.ts for different networks and providers.

Configuration Examples:

// hardhat.config.ts
export default {
  networks: {
    mainnet: {
      url: "https://mainnet.infura.io/v3/...",
      accounts: ["0x..."],
      verify: {
        etherscan: {
          apiKey: process.env.ETHERSCAN_API_KEY,
          apiUrl: "https://api.etherscan.io/api", // Default Ethereum
        },
      },
    },
    
    polygon: {
      url: "https://polygon-rpc.com",
      accounts: ["0x..."],
      verify: {
        etherscan: {
          apiKey: process.env.POLYGONSCAN_API_KEY,
          apiUrl: "https://api.polygonscan.com/api",
        },
      },
    },
    
    bsc: {
      url: "https://bsc-dataseed1.binance.org",
      accounts: ["0x..."],
      verify: {
        etherscan: {
          apiKey: process.env.BSCSCAN_API_KEY,
          apiUrl: "https://api.bscscan.com/api",
        },
      },
    },
    
    optimism: {
      url: "https://mainnet.optimism.io",
      accounts: ["0x..."],
      verify: {
        etherscan: {
          apiKey: process.env.OPTIMISM_API_KEY,
          apiUrl: "https://api-optimistic.etherscan.io/api",
        },
      },
    },
    
    arbitrum: {
      url: "https://arb1.arbitrum.io/rpc",
      accounts: ["0x..."],
      verify: {
        etherscan: {
          apiKey: process.env.ARBISCAN_API_KEY,
          apiUrl: "https://api.arbiscan.io/api",
        },
      },
    },
  },
  
  // Global verification settings
  verify: {
    etherscan: {
      apiKey: {
        mainnet: process.env.ETHERSCAN_API_KEY,
        polygon: process.env.POLYGONSCAN_API_KEY,
        bsc: process.env.BSCSCAN_API_KEY,
        optimisticEthereum: process.env.OPTIMISM_API_KEY,
        arbitrumOne: process.env.ARBISCAN_API_KEY,
      },
    },
  },
  
  sourcify: {
    enabled: true,
    endpoint: "https://sourcify.dev/server/",
  },
};

// Usage in deploy scripts
const func: DeployFunction = async function (hre) {
  const { deployments, network } = hre;
  const { deploy } = deployments;
  const { deployer } = await getNamedAccounts();

  const deployment = await deploy("MultiNetworkContract", {
    from: deployer,
    args: ["universal", "contract"],
    log: true,
  });

  // Verify on all networks except localhost/hardhat
  if (deployment.newlyDeployed && network.live) {
    console.log(`Verifying on ${network.name}...`);
    
    // Try Etherscan first
    try {
      await submitSources("MultiNetworkContract");
      console.log("✓ Etherscan verification submitted");
    } catch (error) {
      console.error("✗ Etherscan verification failed:", error);
    }
    
    // Also submit to Sourcify
    try {
      await submitSourcesToSourcify("MultiNetworkContract");
      console.log("✓ Sourcify verification submitted");
    } catch (error) {
      console.error("✗ Sourcify verification failed:", error);
    }
  }
};

Advanced Verification Scenarios

Handle complex verification scenarios including proxy contracts, libraries, and multi-file contracts.

Usage Examples:

// Verify proxy implementation
const func: DeployFunction = async function (hre) {
  const { deployments } = hre;
  const { deploy } = deployments;
  const { deployer } = await getNamedAccounts();

  // Deploy with proxy
  const proxyDeployment = await deploy("MyContract", {
    from: deployer,
    args: ["init", "args"],
    proxy: {
      proxyContract: "OpenZeppelinTransparentProxy",
      execute: {
        init: {
          methodName: "initialize",
          args: ["init", "args"],
        },
      },
    },
    log: true,
  });

  if (proxyDeployment.newlyDeployed) {
    // Verify both proxy and implementation
    await submitSources("MyContract"); // This verifies the implementation
    
    // The proxy contract is automatically handled
    console.log("✓ Proxy and implementation verified");
  }
};

// Verify contracts with libraries
const func: DeployFunction = async function (hre) {
  const { deployments } = hre;
  const { deploy } = deployments;
  const { deployer } = await getNamedAccounts();

  // Deploy library first
  const mathLib = await deploy("MathLibrary", {
    from: deployer,
    log: true,
  });

  // Deploy contract using library
  const contractWithLib = await deploy("CalculatorContract", {
    from: deployer,
    args: [100],
    libraries: {
      MathLibrary: mathLib.address,
    },
    log: true,
  });

  if (contractWithLib.newlyDeployed) {
    // Verify library first
    await submitSources("MathLibrary");
    
    // Then verify main contract (library linkage is handled automatically)
    await submitSources("CalculatorContract");
    
    console.log("✓ Library and main contract verified");
  }
};

// Batch verification with retry logic
const verifyWithRetry = async (contractName: string, maxRetries = 3) => {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await submitSources(contractName, {
        sleep: 1000 * (i + 1), // Increasing delay
      });
      console.log(`✓ ${contractName} verified on attempt ${i + 1}`);
      return;
    } catch (error) {
      console.log(`Attempt ${i + 1} failed for ${contractName}:`, error);
      if (i === maxRetries - 1) {
        throw error;
      }
    }
  }
};

// Use in deployment
const func: DeployFunction = async function (hre) {
  const contracts = ["TokenA", "TokenB", "DEX", "Governance"];
  
  // Deploy all contracts first
  for (const contractName of contracts) {
    await deployments.deploy(contractName, {
      from: deployer,
      args: [], // Contract-specific args
      log: true,
    });
  }
  
  // Then verify all with retry logic
  for (const contractName of contracts) {
    try {
      await verifyWithRetry(contractName);
    } catch (error) {
      console.error(`Failed to verify ${contractName} after retries:`, error);
    }
  }
};

Verification Status and Management

Check verification status and manage verification workflows.

Usage Examples:

// Check if contract needs verification
const func: DeployFunction = async function (hre) {
  const { deployments, network } = hre;
  const contracts = await deployments.all();
  
  // Only verify on live networks
  if (!network.live) {
    console.log("Skipping verification on local network");
    return;
  }
  
  console.log(`Checking ${Object.keys(contracts).length} contracts for verification...`);
  
  for (const [name, deployment] of Object.entries(contracts)) {
    // Check if deployment has enough metadata for verification
    if (!deployment.metadata || !deployment.solcInputHash) {
      console.log(`⚠️  ${name}: Missing verification metadata`);
      continue;
    }
    
    // Check if recently deployed (needs verification)
    if (deployment.receipt && deployment.receipt.blockNumber) {
      const currentBlock = await hre.ethers.provider.getBlockNumber();
      const deploymentBlock = deployment.receipt.blockNumber;
      
      if (currentBlock - deploymentBlock < 100) { // Recently deployed
        console.log(`🔍 ${name}: Recently deployed, submitting for verification`);
        try {
          await submitSources(name);
          console.log(`✓ ${name}: Verification submitted`);
        } catch (error) {
          console.error(`✗ ${name}: Verification failed -`, error);
        }
      } else {
        console.log(`ℹ️  ${name}: Already verified or old deployment`);
      }
    }
  }
};

// Environment-specific verification
const func: DeployFunction = async function (hre) {
  const { deployments, network } = hre;
  
  // Different verification strategies per environment
  const verificationConfig = {
    mainnet: {
      etherscan: true,
      sourcify: true,
      sleep: 5000, // Conservative rate limiting
    },
    testnet: {
      etherscan: true,
      sourcify: false, // Skip Sourcify for testnets
      sleep: 2000,
    },
    development: {
      etherscan: false,
      sourcify: false,
    },
  };
  
  const config = verificationConfig[network.tags?.production ? 'mainnet' : 
                                  network.tags?.testnet ? 'testnet' : 'development'];
  
  if (config.etherscan) {
    console.log("Starting Etherscan verification...");
    await etherscanVerify({
      sleep: config.sleep,
    });
  }
  
  if (config.sourcify) {
    console.log("Starting Sourcify verification...");
    await sourcifyVerify();
  }
  
  if (!config.etherscan && !config.sourcify) {
    console.log("Verification skipped for development environment");
  }
};

Types

interface EtherscanVerificationOptions {
  /** Etherscan API key */
  apiKey?: string;
  /** Etherscan API URL (different per network) */
  apiUrl?: string;
  /** SPDX license identifier */
  license?: string;
  /** Force license override */
  forceLicense?: boolean;
  /** Use full solc input instead of minimal sources */
  solcInput?: boolean;
  /** Sleep time between API calls (ms) */
  sleep?: number;
}

interface SourcifyVerificationOptions {
  /** Sourcify server endpoint */
  endpoint?: string;
  /** Write metadata files on verification failure for debugging */
  writeFailingMetadata?: boolean;
}

interface VerificationConfig {
  etherscan?: {
    apiKey?: string | { [network: string]: string };
    apiUrl?: string;
  };
}

type VerificationResult = {
  success: boolean;
  contractName: string;
  address: string;
  error?: string;
};