前置条件
- 已部署在 Plasma 测试网上的合约。如果尚未部署,请参阅部署合约。
- 合约的源代码和编译设置。
- 部署时使用的构造函数参数(如果有)。
plasmascan.to)提供一个与 Etherscan 兼容的验证 API。下面的示例直接使用该 API——提交验证无需第三方账号。
示例合约
我们将使用两个示例合约,演示简单与复杂构造函数参数的情况:简单合约
一个基础的数据存储合约,演示智能合约的基本概念。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
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;
}
}
复杂合约
一个用于管理代币存款的更复杂的金库系统,带有可配置参数。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
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;
}
}
使用 Foundry 验证
Foundry 提供了forge verify-contract 命令来验证合约。
-
确保你的
foundry.toml包含验证所需的设置:[profile.default] src = "src" out = "out" libs = ["lib"] solc_version = "0.8.28" optimizer = true optimizer_runs = 200 [rpc_endpoints] plasma_testnet = "https://testnet-rpc.plasma.to" -
验证合约。对带有 string 构造函数参数的 SimpleStorage 合约:
对于含有多个构造函数参数的 TokenVault 合约:
forge verify-contract \ --chain-id 9746 \ --num-of-optimizations 200 \ --watch \ --constructor-args $(cast abi-encode "constructor(string)" "Hello, Plasma!") \ --compiler-version v0.8.28+commit.7893614a \ 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \ src/SimpleStorage.sol:SimpleStorage# 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 \ --compiler-version v0.8.28+commit.7893614a \ 0x9876543210987654321098765432109876543210 \ src/TokenVault.sol:TokenVault -
查看验证状态。使用
--watch标志时,Foundry 会显示实时验证状态。请关注如下输出:Submitting verification for [src/SimpleStorage.sol:SimpleStorage] 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F. Submitted contract for verification: Response: `OK` GUID: `abc123def456ghi789` URL: https://testnet.plasmascan.to/address/0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F Contract verification status: Response: `NOTOK` Details: `Pending in queue` Contract verification status: Response: `OK` Details: `Pass - Verified` Contract successfully verified
使用 Hardhat 验证
Hardhat 通过@nomicfoundation/hardhat-verify 插件提供合约验证。
-
更新
hardhat.config.js,加入验证配置:require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-verify"); require("dotenv").config(); module.exports = { solidity: { version: "0.8.28", settings: { optimizer: { enabled: true, runs: 200 } } }, networks: { plasmaTestnet: { url: process.env.RPC_URL, chainId: 9746, accounts: [process.env.PRIVATE_KEY] } }, etherscan: { customChains: [ { network: "plasmaTestnet", chainId: 9746, urls: { apiURL: "https://testnet.plasmascan.to/api", browserURL: "https://testnet.plasmascan.to/" } } ] }, sourcify: { enabled: false } }; -
验证合约。对于简单合约:
对于含多个构造函数参数的合约,创建验证脚本
npx hardhat verify \ --network plasmaTestnet \ 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \ "Hello, Plasma!"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 (1e18, assuming 18 decimals) "10000000000000000000", // uint256 _maxDeposit (1e19, assuming 18 decimals) 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); }); -
运行验证脚本:
npx hardhat run scripts/verify.js --network plasmaTestnet
在部署过程中自动验证
你也可以在部署时一并自动验证合约: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();
使用 Ethers.js 验证
Ethers.js 需要手动调用区块浏览器的 API 来验证。这种方式可以让你对验证流程拥有完全的控制权。-
安装所需依赖:
npm install ethers dotenv axios form-data -
创建验证脚本
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.plasmascan.to/api"; const data = { module: 'contract', action: 'verifysourcecode', contractaddress: contractAddress, sourceCode: sourceCode, codeformat: 'solidity-single-file', contractname: contractName, compilerversion: 'v0.8.28+commit.7893614a', 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.plasmascan.to/api"; const maxAttempts = 30; let attempts = 0; while (attempts < maxAttempts) { try { const response = await axios.get(apiUrl, { params: { 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(); -
编码构造函数参数。对于复杂构造函数参数,你需要对其进行 ABI 编码。可以使用 Foundry 的
cast工具:# 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 -
最后运行验证:
node verify-ethers.js
检查验证状态
验证后,你可以确认它是否成功:在区块浏览器上
- 访问 Plasma 测试网区块浏览器。
- 搜索你的合约地址。
- 查看 “Contract” 标签旁是否有绿色对勾。
- 点击 “Contract” 标签查看已验证的源代码。
以编程方式
-
创建状态检查脚本
check-verification.js:const axios = require('axios'); require('dotenv').config(); async function checkIfVerified(contractAddress) { const apiUrl = "https://testnet.plasmascan.to/api"; try { const response = await axios.get(apiUrl, { params: { module: 'contract', action: 'getsourcecode', address: contractAddress, } }); 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");
备用方式
虽然区块浏览器是最常见的验证方式,你也可以使用:Sourcify
Sourcify 提供去中心化的合约验证:# Install Sourcify CLI.
npm install -g @ethereum-sourcify/cli
# Verify contract.
sourcify verify \
--network 9746 \
--address 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
--files contracts/SimpleStorage.sol
Tenderly
Tenderly 将验证作为其调试平台的一部分提供。详见其文档。合约扁平化
对于含有导入或库的合约,你可能需要在验证前将合约扁平化为单个文件。常用工具包括: 对于带有外部库的复杂项目,请参阅 Hardhat 验证文档了解高级配置选项。故障排查
构造函数参数不匹配
Error: Invalid constructor arguments provided
cast abi-encode 验证编码。
编译器版本不匹配
Error: Compilation failed
优化设置不匹配
Error: Bytecode doesn't match
已经验证
Error: Contract source code already verified
速率限制
Error: Rate limit exceeded