Prerequisites
- A deployed contract on Plasma testnet. See deploy a contract if you haven’t deployed one yet.
- An API key from Routescan for programmatic verification.
- Your contract’s source code and compilation settings.
- Constructor arguments used during deployment (if any).
Get a Routescan API Key
- Visit Routescan.
- Create an account and navigate to your dashboard.
- Generate a free API key for Plasma testnet verification.
- Save this key in your environment variables as
ETHERSCAN_API_KEY
.
The free tier for Routescan’s API include 2 requests per second, up to 10,000 requests per day. This should be sufficient for validating your deployed contracts. If you are hitting the rate limit, however, consider upgrading to one of the paid tiers.
Example Contracts
We’ll use two example contracts to demonstrate both simple and complex constructor arguments:
Simple Contract
A basic data storage contract that demonstrates fundamental smart contract concepts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleStorage {
string private storedData;
address public owner;
constructor(string memory initialData) {
storedData = initialData;
owner = msg.sender;
}
function setData(string memory data) public {
require(msg.sender == owner, "Only owner can set data");
storedData = data;
}
function getData() public view returns (string memory) {
return storedData;
}
}
Complex Contract
A more sophisticated vault system for managing token deposits with configurable parameters.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract TokenVault {
struct VaultConfig {
uint256 minDeposit;
uint256 maxDeposit;
bool isActive;
}
address[] public authorisedTokens;
VaultConfig public config;
mapping(address => uint256) public balances;
constructor(
address[] memory _tokens,
uint256 _minDeposit,
uint256 _maxDeposit,
bool _isActive
) {
authorisedTokens = _tokens;
config = VaultConfig(_minDeposit, _maxDeposit, _isActive);
}
function deposit(address token, uint256 amount) external {
require(config.isActive, "Vault is not active");
require(amount >= config.minDeposit, "Amount below minimum");
require(amount <= config.maxDeposit, "Amount above maximum");
balances[msg.sender] += amount;
}
}
Verify with Foundry
Foundry provides the forge verify-contract
command for contract verification.
-
Ensure your foundry.toml
includes the verification settings:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.19"
optimizer = true
optimizer_runs = 200
[rpc_endpoints]
plasma_testnet = "https://testnet-rpc.plasma.to"
-
Verify the contract. For the SimpleStorage contract with a string constructor argument:
forge verify-contract \
--chain-id 9746 \
--num-of-optimizations 200 \
--watch \
--constructor-args $(cast abi-encode "constructor(string)" "Hello, Plasma!") \
--etherscan-api-key $ETHERSCAN_API_KEY \
--compiler-version v0.8.19+commit.7dd6d404 \
0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
src/SimpleStorage.sol:SimpleStorage
For the TokenVault contract with multiple constructor arguments:
# First, encode the constructor arguments.
CONSTRUCTOR_ARGS=$(cast abi-encode \
"constructor(address[],uint256,uint256,bool)" \
"[0x1234567890123456789012345678901234567890,0x0987654321098765432109876543210987654321]" \
1000000000000000000 \
10000000000000000000 \
true)
forge verify-contract \
--chain-id 9746 \
--num-of-optimizations 200 \
--watch \
--constructor-args $CONSTRUCTOR_ARGS \
--etherscan-api-key $ETHERSCAN_API_KEY \
--compiler-version v0.8.19+commit.7dd6d404 \
0x9876543210987654321098765432109876543210 \
src/TokenVault.sol:TokenVault
-
Check verification status. Foundry will show real-time verification status with the --watch
flag. Look for:
Submitting verification for [src/SimpleStorage.sol:SimpleStorage] 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F.
Submitted contract for verification:
Response: `OK`
GUID: `abc123def456ghi789`
URL: https://testnet.explorer.plasmalabs.to/address/0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
Verify with Hardhat
Hardhat offers contract verification through the @nomicfoundation/hardhat-verify
plugin.
-
Update your hardhat.config.js
to include verification settings:
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-verify");
require("dotenv").config();
module.exports = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
plasmaTestnet: {
url: process.env.RPC_URL,
chainId: 9746,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: {
plasmaTestnet: process.env.ETHERSCAN_API_KEY
},
customChains: [
{
network: "PlasmaTestnet",
chainId: 9746,
urls: {
apiURL: "https://testnet.plasmaexplorer.io/api",
browserURL: "https://testnet.plasmaexplorer.io/"
}
}
]
},
sourcify: {
enabled: false
}
};
-
Verify the contract. For the simple contract:
npx hardhat verify \
--network plasmaTestnet \
0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
"Hello, Plasma!"
For contracts with multiple constructor arguments, create a verification script scripts/verify.js
:
const hre = require("hardhat");
async function main() {
const contractAddress = "0x9876543210987654321098765432109876543210";
// Constructor arguments for TokenVault.
const constructorArgs = [
[
"0x1234567890123456789012345678901234567890",
"0x0987654321098765432109876543210987654321"
], // address[] _tokens
"1000000000000000000", // uint256 _minDeposit (1 ETH in wei)
"10000000000000000000", // uint256 _maxDeposit (10 ETH in wei)
true // bool _isActive
];
try {
await hre.run("verify:verify", {
address: contractAddress,
constructorArguments: constructorArgs,
});
console.log("Contract verified successfully!");
} catch (error) {
console.error("Verification failed:", error);
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
-
Run the verification script:
npx hardhat run scripts/verify.js --network plasmaTestnet
Automated Verification During Deployment
You can also verify contracts automatically during deployment by adding verification to your deployment script:
const { ethers } = require("hardhat");
async function main() {
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
const simpleStorage = await SimpleStorage.deploy("Hello, Plasma!");
await simpleStorage.waitForDeployment();
const contractAddress = await simpleStorage.getAddress();
console.log("SimpleStorage deployed to:", contractAddress);
// Wait for a few block confirmations before verifying.
console.log("Waiting for block confirmations...");
await simpleStorage.deploymentTransaction().wait(5);
// Verify the contract.
try {
await hre.run("verify:verify", {
address: contractAddress,
constructorArguments: ["Hello, Plasma!"],
});
console.log("Contract verified successfully!");
} catch (error) {
console.log("Verification failed:", error.message);
}
}
main();
Verify with Ethers.js
Ethers.js requires manual API calls to the block explorer for verification. This approach gives you full control over the verification process.
-
Install the required dependencies:
npm install ethers dotenv axios form-data
-
Create verification script verify-ethers.js
:
const axios = require('axios');
const fs = require('fs');
require('dotenv').config();
async function verifyContract(contractAddress, sourceCode, contractName, constructorArgs = "") {
const apiUrl = "https://testnet.plasmaexplorer.io/api";
const data = {
apikey: process.env.ETHERSCAN_API_KEY,
module: 'contract',
action: 'verifysourcecode',
contractaddress: contractAddress,
sourceCode: sourceCode,
codeformat: 'solidity-single-file',
contractname: contractName,
compilerversion: 'v0.8.19+commit.7dd6d404',
optimizationUsed: '1',
runs: '200',
constructorArguements: constructorArgs, // Note: API uses this spelling.
evmversion: 'default',
licenseType: '3' // MIT License
};
try {
console.log('Submitting contract for verification...');
const response = await axios.post(apiUrl, new URLSearchParams(data));
if (response.data.status === '1') {
const guid = response.data.result;
console.log('Verification submitted successfully!');
console.log('GUID:', guid);
// Check verification status.
await checkVerificationStatus(guid);
} else {
console.error('Verification submission failed:', response.data.result);
}
} catch (error) {
console.error('Error submitting verification:', error.message);
}
}
async function checkVerificationStatus(guid) {
const apiUrl = "https://testnet.plasmaexplorer.io/api";
const maxAttempts = 30;
let attempts = 0;
while (attempts < maxAttempts) {
try {
const response = await axios.get(apiUrl, {
params: {
apikey: process.env.ETHERSCAN_API_KEY,
module: 'contract',
action: 'checkverifystatus',
guid: guid
}
});
const status = response.data.status;
const result = response.data.result;
if (status === '1') {
console.log('✅ Contract verified successfully!');
console.log('Result:', result);
break;
} else if (result.includes('Fail')) {
console.error('❌ Verification failed:', result);
break;
} else {
console.log('⏳ Verification pending...');
attempts++;
if (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds.
}
}
} catch (error) {
console.error('Error checking status:', error.message);
break;
}
}
if (attempts >= maxAttempts) {
console.log('⚠️ Verification status check timed out. Please check manually.');
}
}
// Verify SimpleStorage contract.
async function verifySimpleStorage() {
const contractAddress = "0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F";
const sourceCode = fs.readFileSync('SimpleStorage.sol', 'utf8');
const constructorArgs = "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20506c61736d612100000000000000000000000000000000000000";
await verifyContract(contractAddress, sourceCode, "SimpleStorage", constructorArgs);
}
// Verify TokenVault contract.
async function verifyTokenVault() {
const contractAddress = "0x9876543210987654321098765432109876543210";
const sourceCode = fs.readFileSync('TokenVault.sol', 'utf8');
// Complex constructor arguments (ABI encoded).
const constructorArgs = "0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000123456789012345678901234567890123456789000000000000000000000000098765432109876543210987654321098765432100";
await verifyContract(contractAddress, sourceCode, "TokenVault", constructorArgs);
}
// Run verification (uncomment the one you need).
verifySimpleStorage();
// verifyTokenVault();
-
Encode constructor arguments. For complex constructor arguments, you need to ABI encode them. You can use Foundry’s cast
tool:
# For SimpleStorage with string argument.
cast abi-encode "constructor(string)" "Hello, Plasma!"
# For TokenVault with complex arguments.
cast abi-encode \
"constructor(address[],uint256,uint256,bool)" \
"[0x1234567890123456789012345678901234567890,0x0987654321098765432109876543210987654321]" \
1000000000000000000 \
10000000000000000000 \
true
-
Finally, run the verification:
Check Verification Status
After verification, you can check if it was successful:
On Block Explorer
- Visit Plasma testnet explorer.
- Search for your contract address.
- Look for a green checkmark next to “Contract” tab.
- Click the “Contract” tab to view the verified source code.
Programmatically
-
Create a status check script check-verification.js
:
const axios = require('axios');
require('dotenv').config();
async function checkIfVerified(contractAddress) {
const apiUrl = "https://testnet.plasmaexplorer.io/api";
try {
const response = await axios.get(apiUrl, {
params: {
module: 'contract',
action: 'getsourcecode',
address: contractAddress,
apikey: process.env.ETHERSCAN_API_KEY
}
});
const result = response.data.result[0];
if (result.SourceCode && result.SourceCode !== '') {
console.log('✅ Contract is verified!');
console.log('Contract Name:', result.ContractName);
console.log('Compiler Version:', result.CompilerVersion);
console.log('Optimisation Used:', result.OptimizationUsed);
return true;
} else {
console.log('❌ Contract is not verified');
return false;
}
} catch (error) {
console.error('Error checking verification status:', error.message);
return false;
}
}
// Check your contract.
checkIfVerified("0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F");
Alternative Methods
While block explorers are the most common verification method, you can also use:
Sourcify
Sourcify provides decentralised contract verification:
# Install Sourcify CLI.
npm install -g @ethereum-sourcify/cli
# Verify contract.
sourcify verify \
--network 9746 \
--address 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
--files contracts/SimpleStorage.sol
Tenderly
Tenderly offers verification as part of their debugging platform. See their documentation for details.
Contract Flattening
For contracts with imports or libraries, you may need to flatten your contract into a single file before verification. Popular tools include:
For complex projects with external libraries, see the Hardhat verification documentation for advanced configuration options.
Troubleshooting
Constructor Argument Mismatch
Error: Invalid constructor arguments provided
Double-check your constructor arguments match exactly what was used during deployment. Use cast abi-encode
to verify encoding.
Compiler Version Mismatch
Error: Compilation failed
Ensure the compiler version in your verification request matches the version used during compilation.
Optimisation Settings Mismatch
Error: Bytecode doesn't match
Verify that optimisation settings (enabled/disabled and runs count) match your compilation settings.
API Key Issues
Ensure your Routescan API key is valid and has sufficient quota. Check your environment variables are loaded correctly.
Already Verified
Error: Contract source code already verified
This means verification was successful previously. Check the block explorer to confirm.
Rate Limiting
Error: Rate limit exceeded
Wait a few minutes before retrying. Consider upgrading your API plan if you need higher limits.