Skip to content

Commit 172f9c4

Browse files
authored
Add auto-verification of contracts on Etherscan (#2717)
* Add auto-verification of contracts on Etherscan * Removed skipping of forked environments
1 parent b72a152 commit 172f9c4

File tree

2 files changed

+194
-10
lines changed

2 files changed

+194
-10
lines changed

contracts/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,10 +409,22 @@ Validator public key: 90db8ae56a9e741775ca37dd960606541306974d4a998ef6a6227c85a9
409409

410410
The Hardhat plug-in [@nomiclabs/hardhat-verify](https://www.npmjs.com/package/@nomiclabs/hardhat-etherscan) is used to verify contracts on Etherscan. Etherscan has migrated to V2 api where all the chains use the same endpoint. Hardhat verify should be run with `--contract` parameter otherwise there is a significant slowdown while hardhat is gathering contract information.
411411

412+
### Auto-verification
413+
414+
When deploying contracts, set `VERIFY_CONTRACTS=true` environment variable to verify contract immediately after deployment with no manual action.
415+
416+
```
417+
VERIFY_CONTRACTS=true npx hardhat deploy:mainnet
418+
```
419+
420+
If it reverts for any reason, it'll print out the command that you can use to run manually or debug.
421+
422+
### Manual verification
423+
412424
**IMPORTANT:**
413425

414426
- Currently only yarn works. Do not use npx/pnpm
415-
- Also if you switch package manager do run "hardhat compile" first to mitigate potential bytecode missmatch errors
427+
- Also if you switch package manager do run "hardhat compile" first to mitigate potential bytecode mismatch errors
416428

417429
There's an example
418430

contracts/utils/deploy.js

Lines changed: 181 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,157 @@ function log(msg, deployResult = null) {
6363
}
6464
}
6565

66+
/**
67+
* Verifies a contract on Etherscan
68+
* @param {string} contractName - Name of the contract (for logging)
69+
* @param {string} contractAddress - Address of the deployed contract
70+
* @param {Array} constructorArgs - Constructor arguments used for deployment
71+
* @param {string} contract - Actual contract name in source code
72+
* @param {string|null} contractPath - Optional contract path (e.g., "contracts/vault/VaultAdmin.sol:VaultAdmin")
73+
*/
74+
const verifyContractOnEtherscan = async (
75+
contractName,
76+
contractAddress,
77+
constructorArgs,
78+
contract,
79+
contractPath = null
80+
) => {
81+
// Declare finalContractPath outside try block so it's accessible in catch
82+
let finalContractPath = contractPath;
83+
84+
try {
85+
log(`Verifying ${contractName} at ${contractAddress}...`);
86+
87+
// Note: constructorArguments should be in the same format as used for deployment
88+
// Structs should be passed as arrays/tuples (e.g., [[addr1, addr2]] for a struct with 2 addresses)
89+
// Since we're using the same `args` that were used for deployment, structs will work correctly
90+
const verifyArgs = {
91+
address: contractAddress,
92+
constructorArguments: constructorArgs || [],
93+
};
94+
95+
// Try to get contract path from artifacts if not provided
96+
if (!finalContractPath) {
97+
try {
98+
// Use the contract name (which is the actual contract name in source code)
99+
const actualContractName =
100+
typeof contract === "string" ? contract : contractName;
101+
const artifact = await hre.artifacts.readArtifact(actualContractName);
102+
103+
// artifact.sourceName contains the path like "contracts/vault/VaultAdmin.sol"
104+
// We need to format it as "contracts/vault/VaultAdmin.sol:VaultAdmin"
105+
if (artifact.sourceName) {
106+
finalContractPath = `${artifact.sourceName}:${actualContractName}`;
107+
log(`Auto-detected contract path: ${finalContractPath}`);
108+
}
109+
} catch (artifactError) {
110+
// If we can't read the artifact, continue without contract path
111+
// Verification will still work but may be slower
112+
log(`Could not auto-detect contract path: ${artifactError.message}`);
113+
}
114+
}
115+
116+
// If we have a contract path, use it (faster verification)
117+
if (finalContractPath) {
118+
verifyArgs.contract = finalContractPath;
119+
}
120+
121+
// Note: "verify:verify" is the full task name in Hardhat's task system
122+
// The CLI command "hardhat verify" is actually calling the "verify:verify" subtask
123+
// This is Hardhat's namespace convention: <taskGroup>:<subtask>
124+
await hre.run("verify:verify", verifyArgs);
125+
126+
log(`Verified ${contractName} at ${contractAddress}`);
127+
} catch (error) {
128+
// Log verification error but don't fail deployment
129+
if (error.message.includes("Already Verified")) {
130+
log(`${contractName} at ${contractAddress} is already verified`);
131+
} else {
132+
log(
133+
`Warning: Failed to verify ${contractName} at ${contractAddress}: ${error.message}`
134+
);
135+
136+
// Print the manual verification command for debugging
137+
const networkName = hre.network.name;
138+
let manualCommand = `yarn hardhat verify --network ${networkName}`;
139+
140+
if (finalContractPath) {
141+
manualCommand += ` --contract ${finalContractPath}`;
142+
}
143+
144+
// Format constructor arguments
145+
if (constructorArgs && constructorArgs.length > 0) {
146+
// Check if args are complex (contain arrays/objects) - if so, suggest using a file
147+
const hasComplexArgs = constructorArgs.some(
148+
(arg) =>
149+
Array.isArray(arg) ||
150+
(typeof arg === "object" &&
151+
arg !== null &&
152+
!BigNumber.isBigNumber(arg))
153+
);
154+
155+
if (hasComplexArgs) {
156+
// For complex args, suggest creating a file
157+
// Format args as a JavaScript module export
158+
const formatArg = (arg) => {
159+
if (Array.isArray(arg)) {
160+
return `[${arg.map(formatArg).join(", ")}]`;
161+
} else if (BigNumber.isBigNumber(arg)) {
162+
return `"${arg.toString()}"`;
163+
} else if (typeof arg === "string") {
164+
return `"${arg}"`;
165+
} else if (typeof arg === "object" && arg !== null) {
166+
return JSON.stringify(arg);
167+
}
168+
return String(arg);
169+
};
170+
171+
const argsCode = `module.exports = [${constructorArgs
172+
.map(formatArg)
173+
.join(", ")}];`;
174+
log(
175+
`\nTo verify manually, create a file (e.g., verify-args.js) with:`
176+
);
177+
log(argsCode);
178+
log(`\nThen run:`);
179+
log(
180+
`${manualCommand} --constructor-args verify-args.js ${contractAddress}`
181+
);
182+
} else {
183+
// Simple args can be passed directly
184+
const argsStr = constructorArgs
185+
.map((arg) => {
186+
if (BigNumber.isBigNumber(arg)) {
187+
return arg.toString();
188+
} else if (typeof arg === "string" && arg.startsWith("0x")) {
189+
return arg;
190+
}
191+
return String(arg);
192+
})
193+
.join(" ");
194+
manualCommand += ` ${contractAddress} ${argsStr}`;
195+
log(`\nTo verify manually, run:`);
196+
log(manualCommand);
197+
}
198+
} else {
199+
manualCommand += ` ${contractAddress}`;
200+
log(`\nTo verify manually, run:`);
201+
log(manualCommand);
202+
}
203+
}
204+
}
205+
};
206+
66207
const deployWithConfirmation = async (
67208
contractName,
68209
args,
69210
contract,
70211
skipUpgradeSafety = false,
71212
libraries = {},
72213
gasLimit,
73-
useFeeData
214+
useFeeData,
215+
verifyContract = false,
216+
contractPath = null
74217
) => {
75218
// check that upgrade doesn't corrupt the storage slots
76219
if (!isTest && !skipUpgradeSafety) {
@@ -109,6 +252,21 @@ const deployWithConfirmation = async (
109252
await storeStorageLayoutForContract(hre, contractName, contract);
110253
}
111254

255+
log(`Deployed ${contractName}`, result);
256+
// Verify contract on Etherscan if requested and on a live network
257+
// Can be enabled via parameter or VERIFY_CONTRACTS environment variable
258+
const shouldVerify =
259+
verifyContract || process.env.VERIFY_CONTRACTS === "true";
260+
if (shouldVerify && !isTest && !isFork && result.address) {
261+
await verifyContractOnEtherscan(
262+
contractName,
263+
result.address,
264+
args,
265+
contract,
266+
contractPath
267+
);
268+
}
269+
112270
log(`Deployed ${contractName}`, result);
113271
return result;
114272
};
@@ -1128,28 +1286,41 @@ function deploymentWithGuardianGovernor(opts, fn) {
11281286
return main;
11291287
}
11301288

1131-
function encodeSaltForCreateX(deployer, crossChainProtectionFlag, salt) {
1132-
// Generate encoded salt (deployer address || crossChainProtectionFlag || bytes11(keccak256(rewardToken, gauge)))
1289+
function encodeSaltForCreateX(deployer, crosschainProtectionFlag, salt) {
1290+
// Generate encoded salt (deployer address || crosschainProtectionFlag || bytes11(keccak256(rewardToken, gauge)))
11331291

11341292
// convert deployer address to bytes20
11351293
const addressDeployerBytes20 = ethers.utils.hexlify(
11361294
ethers.utils.zeroPad(deployer, 20)
11371295
);
11381296

1139-
// convert crossChainProtectionFlag to bytes1
1140-
const crossChainProtectionFlagBytes1 = crossChainProtectionFlag
1297+
// convert crosschainProtectionFlag to bytes1
1298+
const crosschainProtectionFlagBytes1 = crosschainProtectionFlag
11411299
? "0x01"
11421300
: "0x00";
11431301

11441302
// this portion hexifies salt to bytes11
1145-
const saltBytes11 = ethers.utils.hexlify(
1146-
ethers.utils.zeroPad(ethers.utils.hexlify(salt), 11)
1147-
);
1303+
// For strings, hash them first (as per comment: bytes11(keccak256(rewardToken, gauge)))
1304+
// Then take the first 11 bytes of the hash (most significant bytes)
1305+
let saltBytes11;
1306+
if (typeof salt === "string" && !ethers.utils.isHexString(salt)) {
1307+
// Hash the string and take first 11 bytes (leftmost bytes)
1308+
const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(salt));
1309+
const hashBytes = ethers.utils.arrayify(hash);
1310+
// Take first 11 bytes and pad to 11 bytes (should already be 11, but ensure it)
1311+
saltBytes11 = ethers.utils.hexlify(
1312+
ethers.utils.zeroPad(hashBytes.slice(0, 11), 11)
1313+
);
1314+
} else {
1315+
// For numbers or hex strings, pad to 11 bytes
1316+
const saltBytes = ethers.utils.hexlify(salt);
1317+
saltBytes11 = ethers.utils.hexlify(ethers.utils.zeroPad(saltBytes, 11));
1318+
}
11481319
// concat all bytes into a bytes32
11491320
const encodedSalt = ethers.utils.hexlify(
11501321
ethers.utils.concat([
11511322
addressDeployerBytes20,
1152-
crossChainProtectionFlagBytes1,
1323+
crosschainProtectionFlagBytes1,
11531324
saltBytes11,
11541325
])
11551326
);
@@ -1267,6 +1438,7 @@ async function createPoolBoosterSonic({
12671438
module.exports = {
12681439
log,
12691440
deployWithConfirmation,
1441+
verifyContractOnEtherscan,
12701442
withConfirmation,
12711443
impersonateGuardian,
12721444
executeProposalOnFork,

0 commit comments

Comments
 (0)