Skip to main content
Contract verification publishes your source code on block explorers, allowing anyone to verify that the deployed bytecode matches the source code.

Why Verify Contracts?

Transparency

  • Users can read and audit the contract code
  • Builds trust in the protocol
  • Enables community security review

Functionality

  • Block explorers can decode function calls
  • Events display with readable parameter names
  • Users can interact with contracts directly via UI

Compliance

  • Required for many DEX listings
  • Necessary for security audits
  • Standard practice for DeFi protocols

Automated Verification

The zkp2p-contracts repository includes automated verification scripts.

Base Mainnet

yarn etherscan:base
This command:
  1. Reads all deployments from deployments/base/
  2. Verifies each contract on Basescan
  3. Uses a 600ms delay between verifications (rate limiting)
  4. Skips already verified contracts
  5. Reports verification status

Base Sepolia Testnet

yarn etherscan:base_sepolia
Uses a 5-second delay between verifications.

Base Staging

yarn etherscan:base_staging
Verifies staging deployments on Base mainnet Basescan.

Manual Verification

Using Hardhat Verify Plugin

Single Contract

npx hardhat verify --network base \
  <CONTRACT_ADDRESS> \
  <CONSTRUCTOR_ARG_1> \
  <CONSTRUCTOR_ARG_2>

Example: Verify Escrow

npx hardhat verify --network base \
  0x2f121CDDCA6d652f35e8B3E560f9760898888888 \
  "0x0bC26FF515411396DD588Abd6Ef6846E04470227" \
  8453 \
  "0xPaymentVerifierRegistryAddress" \
  "0x0bC26FF515411396DD588Abd6Ef6846E04470227" \
  100000 \
  200 \
  21600

Constructor Arguments from Deployment

Extract constructor args from deployment file:
cat deployments/base/Escrow.json | jq '.args'
Output:
[
  "0x0bC26FF515411396DD588Abd6Ef6846E04470227",
  8453,
  "0xPaymentVerifierRegistryAddress",
  "0x0bC26FF515411396DD588Abd6Ef6846E04470227",
  100000,
  200,
  21600
]

Using Basescan Web Interface

1. Navigate to Contract

Go to your contract on Basescan:
https://basescan.org/address/<CONTRACT_ADDRESS>

2. Click “Verify and Publish”

On the contract page, select the “Contract” tab, then “Verify and Publish”.

3. Fill Verification Form

Compiler Type: Solidity (Single file) or Solidity (Standard-JSON-Input) Compiler Version: v0.8.18+commit.87f61d96 Open Source License Type: MIT Optimization: Enabled Optimization Runs: 200 Via IR: Yes These values match hardhat.config.ts:
solidity: {
  compilers: [
    {
      version: "0.8.18",
      settings: {
        optimizer: {
          enabled: true,
          runs: 200,
        },
        viaIR: true
      },
    },
  ],
}

4. Upload Contract Code

For Standard JSON Input:
# Generate standard JSON input
npx hardhat verify --network base \
  <CONTRACT_ADDRESS> \
  --constructor-args arguments.js \
  --show-stack-traces
Or manually combine:
  • Main contract source
  • All imported dependencies
  • OpenZeppelin contracts
  • Interfaces

5. Submit for Verification

Basescan will:
  • Compile your source code
  • Compare bytecode to deployed contract
  • Display verification result

Verification Script Details

The automated verification task is defined in tasks/etherscanVerifyWithDelay.ts:
task('etherscan-verify-with-delay', 
  'Verify contracts on Etherscan with delays to avoid rate limiting'
)
  .addOptionalParam('delay', 'Delay in milliseconds', '600')
  .setAction(async ({ delay }, hre) => {
    const delayMs = parseInt(delay);
    const deployments = await hre.deployments.all();
    
    for (const [contractName, deployment] of Object.entries(deployments)) {
      try {
        await hre.run('verify:verify', {
          address: deployment.address,
          constructorArguments: deployment.args || [],
        });
        
        console.log(`✅ ${contractName} verified`);
      } catch (error) {
        if (error.message.includes('already verified')) {
          console.log(`⏭️  ${contractName} already verified`);
        } else {
          console.error(`❌ ${contractName} failed: ${error.message}`);
        }
      }
      
      await sleep(delayMs);
    }
  });

How It Works

  1. Load Deployments: Reads all .json files from deployments/<network>/
  2. Iterate Contracts: Processes each contract sequentially
  3. Verify: Calls Hardhat verify plugin with saved constructor args
  4. Handle Errors: Skips already-verified contracts, logs failures
  5. Rate Limiting: Waits specified delay between requests
  6. Summary: Reports verification statistics

Configuration

API Keys

Set your Basescan API key in .env:
BASESCAN_API_KEY=your_basescan_api_key_here
ETHERSCAN_KEY=your_etherscan_api_key_here
Get API keys:

Hardhat Config

Configure verification in hardhat.config.ts:
import '@nomicfoundation/hardhat-verify';

const config: HardhatUserConfig = {
  etherscan: {
    apiKey: process.env.ETHERSCAN_KEY || "",
  },
  networks: {
    base_staging: {
      verify: {
        etherscan: {
          apiUrl: "https://api.basescan.org/",
          apiKey: process.env.BASESCAN_API_KEY
        }
      },
    },
  },
};

Verification Status

Check if a contract is verified:

Via Block Explorer

Visit the contract page. Verified contracts show:
  • Green checkmark next to contract address
  • “Contract Source Code Verified” badge
  • Readable source code in “Contract” tab

Via API

curl "https://api.basescan.org/api
  ?module=contract
  &action=getsourcecode
  &address=<CONTRACT_ADDRESS>
  &apikey=<YOUR_API_KEY>"
Response includes:
{
  "status": "1",
  "message": "OK",
  "result": [{
    "SourceCode": "contract Escrow { ... }",
    "ContractName": "Escrow",
    "CompilerVersion": "v0.8.18+commit.87f61d96",
    "OptimizationUsed": "1",
    "Runs": "200"
  }]
}

Troubleshooting

”Already Verified” Error

Cause: Contract was previously verified Solution: No action needed. Contract is verified. Verify: Check block explorer for green checkmark

”Invalid API Key” Error

Cause: Missing or incorrect Basescan API key Solution:
  1. Get API key from basescan.org/apis
  2. Add to .env: BASESCAN_API_KEY=your_key_here
  3. Restart verification

”Bytecode Does Not Match” Error

Cause: Deployed bytecode differs from compiled source Possible Reasons:
  • Wrong compiler version
  • Different optimization settings
  • Wrong via IR setting
  • Modified source code after deployment
  • Incorrect constructor arguments
Solution:
  1. Check compiler settings match hardhat.config.ts
  2. Verify constructor arguments from deployment JSON
  3. Ensure source code matches deployed version
  4. Try compiling locally and comparing bytecode:
npx hardhat compile
npx hardhat verify --network base <ADDRESS> <ARGS>

“Max Rate Limit Reached” Error

Cause: Too many verification requests in short time Solution: Increase delay between verifications:
yarn hardhat --network base \
  etherscan-verify-with-delay \
  --delay 1000  # 1 second delay
Or wait 5-10 minutes and retry.

”Contract Size Exceeds Limit” Error

Cause: Contract bytecode larger than 24KB Solution:
  • Refactor contract into smaller modules
  • Use libraries for common functionality
  • Enable optimizer with higher runs
  • Remove unnecessary code

”Failed to Fetch Constructor Arguments” Error

Cause: Can’t automatically detect constructor args from blockchain Solution: Manually specify arguments:
# Create arguments.js
module.exports = [
  "0x0bC26FF515411396DD588Abd6Ef6846E04470227",
  8453,
  // ... other args
];

# Verify with arguments file
npx hardhat verify --network base \
  <ADDRESS> \
  --constructor-args arguments.js

Verification Checklist

After verification, confirm:
  • Contract shows as verified on Basescan
  • Source code is readable on “Contract” tab
  • Constructor arguments are correct
  • Read/Write functions work on block explorer UI
  • Contract ABI matches deployment JSON
  • All dependencies are verified (registries, etc.)
  • Contract name matches source file

Best Practices

1. Verify Immediately After Deployment

Run verification right after deployment:
yarn deploy:base && yarn etherscan:base

2. Save Deployment Artifacts

Commit deployments/ directory to version control:
git add deployments/base/
git commit -m "Add Base mainnet deployment artifacts"
This preserves:
  • Contract addresses
  • Constructor arguments
  • Deployment transaction hashes
  • ABIs

3. Document Verification

Record verification details:
## Base Mainnet Deployment

Deployed: 2024-03-04 14:30:00 UTC
Deployer: 0x...
Escrow: https://basescan.org/address/0x2f121CDDCA6d652f35e8B3E560f9760898888888#code
Orchestrator: https://basescan.org/address/0x88888883Ed048FF0a415271B28b2F52d431810D0#code
Verified: ✅ All contracts

4. Test on Testnet First

Practice verification on Base Sepolia:
yarn deploy:base_sepolia
yarn etherscan:base_sepolia
Confirm the process works before mainnet deployment.

5. Verify Dependencies

Ensure all registry contracts are verified:
  • PaymentVerifierRegistry
  • NullifierRegistry
  • EscrowRegistry
  • RelayerRegistry
  • PostIntentHookRegistry

Reading Verified Contracts

Once verified, anyone can:

View Source Code

https://basescan.org/address/<CONTRACT_ADDRESS>#code

Interact with Contract

https://basescan.org/address/<CONTRACT_ADDRESS>#writeContract
Connect wallet and call functions directly.

Read Contract State

https://basescan.org/address/<CONTRACT_ADDRESS>#readContract
Query view functions without connecting wallet.

View Events

https://basescan.org/address/<CONTRACT_ADDRESS>#events
See decoded event logs with parameter names.

Next Steps

After verifying contracts:
  1. Configure payment methods
  2. Set up relayers
  3. Test core functionality
  4. Monitor contract events
Verification is a one-time process per deployment. Once verified, the contract remains verified permanently on the block explorer.
Always verify the contract address matches your deployment before interacting. Scammers may deploy fake contracts with similar names.

Build docs developers (and LLMs) love