Skip to content

Commit 787c584

Browse files
committed
feat: L1 contract etherscan verification
1 parent 7534d82 commit 787c584

File tree

9 files changed

+336
-4
lines changed

9 files changed

+336
-4
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Usage:
5+
# scripts/verify-from-json.sh <path-to-l1-verify.json> [--api-key <etherscan-api-key>]
6+
#
7+
# Notes:
8+
# - This script is used to verify contracts from a JSON file generated by the deploy-l1-contracts command.
9+
# - The format of the JSON file is as follows:
10+
# {
11+
# "chainId": <chain-id>,
12+
# "records": [
13+
# {
14+
# "name": <contract-name>,
15+
# "address": <contract-address>,
16+
# "constructorArgsHex": <constructor-args-hex>,
17+
# "libraries": [
18+
# {
19+
# "file": <library-file>,
20+
# "contract": <library-contract>,
21+
# "address": <library-address>
22+
# }
23+
# ]
24+
# }
25+
# ]
26+
# }
27+
# - The script will verify each contract in the JSON file.
28+
# - Runs from repo root automatically to ensure paths resolve, then returns to l1-contracts.
29+
# - Expects Foundry (forge) and jq available.
30+
31+
ROOT_DIR=$(git rev-parse --show-toplevel)
32+
cd "$ROOT_DIR/l1-contracts"
33+
34+
if ! command -v jq >/dev/null 2>&1; then
35+
echo "jq is required" >&2
36+
exit 1
37+
fi
38+
if ! command -v forge >/dev/null 2>&1; then
39+
echo "forge is required" >&2
40+
exit 1
41+
fi
42+
if ! command -v curl >/dev/null 2>&1; then
43+
echo "curl is required" >&2
44+
exit 1
45+
fi
46+
47+
if [[ $# -lt 1 ]]; then
48+
echo "Usage: scripts/verify-from-json.sh <path-to-l1-verify.json> [--chain <id|name>] [--api-key <key>]" >&2
49+
exit 1
50+
fi
51+
52+
JSON_PATH="$1"
53+
shift || true
54+
55+
API_KEY_ARG=""
56+
57+
while [[ $# -gt 0 ]]; do
58+
case "$1" in
59+
--api-key)
60+
API_KEY_ARG="$2"
61+
shift 2
62+
;;
63+
*)
64+
echo "Unknown argument: $1" >&2
65+
exit 1
66+
;;
67+
esac
68+
done
69+
70+
if [[ ! -f "$JSON_PATH" ]]; then
71+
echo "File not found: $JSON_PATH" >&2
72+
exit 1
73+
fi
74+
75+
# Derive chain and api key
76+
CHAIN_ID_FROM_JSON=$(jq -r .chainId "$JSON_PATH")
77+
CHAIN_VALUE="$CHAIN_ID_FROM_JSON"
78+
API_KEY_VALUE="${API_KEY_ARG:-${ETHERSCAN_API_KEY:-}}"
79+
80+
if [[ -z "$API_KEY_VALUE" ]]; then
81+
echo "ETHERSCAN API key not provided. Set env ETHERSCAN_API_KEY or pass --api-key <key>." >&2
82+
exit 1
83+
fi
84+
85+
# Sourcify server (can override via $SOURCIFY_SERVER_URL)
86+
SOURCIFY_SERVER_URL="${SOURCIFY_SERVER_URL:-https://sourcify.dev/server}"
87+
88+
# Import a verified contract from Etherscan into Sourcify v2
89+
sourcify_import() {
90+
local address="$1"
91+
local chain_id="$CHAIN_VALUE"
92+
local endpoint="$SOURCIFY_SERVER_URL/v2/verify/etherscan/$chain_id/$address"
93+
local payload
94+
# Only etherscanApiKey is required in body for this endpoint
95+
payload=$(jq -n --arg key "$API_KEY_VALUE" '{apiKey:$key}')
96+
echo " Importing to Sourcify: $address (chain $chain_id)"
97+
curl -sS -X POST -H 'Content-Type: application/json' -d "$payload" "$endpoint" | sed 's/^/ sourcify: /'
98+
}
99+
100+
# Map deployment "name" to FQN "<path>:<ContractName>"
101+
resolve_fqn() {
102+
local name="$1"
103+
case "$name" in
104+
FeeAsset|StakingAsset)
105+
echo "src/mock/TestERC20.sol:TestERC20" ;;
106+
GSE)
107+
echo "src/governance/GSE.sol:GSE" ;;
108+
Registry)
109+
echo "src/governance/Registry.sol:Registry" ;;
110+
GovernanceProposer)
111+
echo "src/governance/proposer/GovernanceProposer.sol:GovernanceProposer" ;;
112+
Governance)
113+
echo "src/governance/Governance.sol:Governance" ;;
114+
CoinIssuer)
115+
echo "src/governance/CoinIssuer.sol:CoinIssuer" ;;
116+
FeeAssetHandler)
117+
echo "src/mock/FeeAssetHandler.sol:FeeAssetHandler" ;;
118+
StakingAssetHandler)
119+
echo "src/mock/StakingAssetHandler.sol:StakingAssetHandler" ;;
120+
MockVerifier)
121+
echo "src/mock/MockVerifier.sol:MockVerifier" ;;
122+
Rollup)
123+
echo "src/core/Rollup.sol:Rollup" ;;
124+
SlashFactory)
125+
echo "src/periphery/SlashFactory.sol:SlashFactory" ;;
126+
Inbox)
127+
echo "src/core/messagebridge/Inbox.sol:Inbox" ;;
128+
Outbox)
129+
echo "src/core/messagebridge/Outbox.sol:Outbox" ;;
130+
*)
131+
echo "" ;;
132+
esac
133+
}
134+
135+
# Append libraries as separate flags: --libraries file:Contract:address
136+
append_libraries_flags() {
137+
local record_json="$1"
138+
local -n __cmd_ref=$2
139+
while IFS= read -r lib; do
140+
[[ -z "$lib" ]] && continue
141+
local file contract address
142+
file=$(echo "$lib" | jq -r .file)
143+
contract=$(echo "$lib" | jq -r .contract)
144+
address=$(echo "$lib" | jq -r .address)
145+
if [[ -n "$file" && -n "$contract" && -n "$address" ]]; then
146+
__cmd_ref+=(--libraries "$file:$contract:$address")
147+
fi
148+
done < <(echo "$record_json" | jq -c '.libraries[]?')
149+
}
150+
151+
# Iterate records
152+
records_len=$(jq '.records | length' "$JSON_PATH")
153+
echo "Verifying $records_len contracts from $JSON_PATH on chain $CHAIN_VALUE"
154+
155+
# First, verify all unique libraries referenced across records
156+
declare -A __libs_seen
157+
mapfile -t __all_libs < <(jq -c '.records[].libraries[]?' "$JSON_PATH")
158+
if [[ ${#__all_libs[@]} -gt 0 ]]; then
159+
echo "Found ${#__all_libs[@]} library references. Verifying unique libraries first..."
160+
for lib in "${__all_libs[@]}"; do
161+
file=$(echo "$lib" | jq -r .file)
162+
contract=$(echo "$lib" | jq -r .contract)
163+
address=$(echo "$lib" | jq -r .address)
164+
key="$file:$contract:$address"
165+
if [[ -z "${__libs_seen[$key]:-}" ]]; then
166+
__libs_seen[$key]=1
167+
echo "==> Verifying library $contract at $address"
168+
echo " FQN: $file:$contract"
169+
forge verify-contract \
170+
--chain "$CHAIN_VALUE" \
171+
--etherscan-api-key "$API_KEY_VALUE" \
172+
"$address" "$file:$contract" \
173+
--compiler-version v0.8.27
174+
175+
sourcify_import "$address"
176+
fi
177+
done
178+
fi
179+
180+
for i in $(seq 0 $((records_len - 1))); do
181+
rec=$(jq -c ".records[$i]" "$JSON_PATH")
182+
name=$(echo "$rec" | jq -r .name)
183+
addr=$(echo "$rec" | jq -r .address)
184+
ctor=$(echo "$rec" | jq -r .constructorArgsHex)
185+
fqn=$(resolve_fqn "$name")
186+
187+
if [[ -z "$fqn" ]]; then
188+
echo "[skip] Unknown contract name '$name' at $addr" >&2
189+
continue
190+
fi
191+
192+
cmd=(forge verify-contract --chain "$CHAIN_VALUE" --etherscan-api-key "$API_KEY_VALUE" "$addr" "$fqn" --compiler-version v0.8.27)
193+
if [[ -n "$ctor" && "$ctor" != "0x" ]]; then
194+
cmd+=(--constructor-args "$ctor")
195+
fi
196+
append_libraries_flags "$rec" cmd
197+
198+
echo "==> Verifying $name at $addr"
199+
echo " FQN: $fqn"
200+
libs_summary=$(echo "$rec" | jq -r '.libraries[]? | "\(.file):\(.contract):\(.address)"' | paste -sd "," -)
201+
if [[ -n "$libs_summary" ]]; then echo " Libraries: $libs_summary"; fi
202+
if [[ -n "$ctor" && "$ctor" != "0x" ]]; then echo " Constructor args: (hex) ${#ctor} bytes"; fi
203+
204+
"${cmd[@]}"
205+
206+
# Wait a bit to ensure etherscan verification is complete
207+
sleep 5
208+
209+
sourcify_import "$addr"
210+
done
211+
212+
echo "All verification commands executed."
213+
214+

yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export async function deployL1Contracts(
1919
sponsoredFPC: boolean,
2020
acceleratedTestDeployments: boolean,
2121
json: boolean,
22+
createVerificationJson: string | false,
2223
initialValidators: EthAddress[],
2324
realVerifier: boolean,
2425
log: LogFn,
@@ -50,6 +51,7 @@ export async function deployL1Contracts(
5051
acceleratedTestDeployments,
5152
config,
5253
realVerifier,
54+
createVerificationJson,
5355
debugLogger,
5456
);
5557

@@ -77,6 +79,7 @@ export async function deployL1Contracts(
7779
log(`SlashFactory Address: ${l1ContractAddresses.slashFactoryAddress?.toString()}`);
7880
log(`FeeAssetHandler Address: ${l1ContractAddresses.feeAssetHandlerAddress?.toString()}`);
7981
log(`StakingAssetHandler Address: ${l1ContractAddresses.stakingAssetHandlerAddress?.toString()}`);
82+
log(`ZK Passport Verifier Address: ${l1ContractAddresses.zkPassportVerifierAddress?.toString()}`);
8083
log(`Initial funded accounts: ${initialFundedAccounts.map(a => a.toString()).join(', ')}`);
8184
log(`Initial validators: ${initialValidators.map(a => a.toString()).join(', ')}`);
8285
log(`Genesis archive root: ${genesisArchiveRoot.toString()}`);

yarn-project/cli/src/cmds/l1/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger
4646
.option('--sponsored-fpc', 'Populate genesis state with a testing sponsored FPC contract')
4747
.option('--accelerated-test-deployments', 'Fire and forget deployment transactions, use in testing only', false)
4848
.option('--real-verifier', 'Deploy the real verifier', false)
49+
.option('--create-verification-json [path]', 'Create JSON file for etherscan contract verification', false)
4950
.action(async options => {
5051
const { deployL1Contracts } = await import('./deploy_l1_contracts.js');
5152

@@ -62,6 +63,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger
6263
options.sponsoredFpc,
6364
options.acceleratedTestDeployments,
6465
options.json,
66+
options.createVerificationJson,
6567
initialValidators,
6668
options.realVerifier,
6769
log,

yarn-project/cli/src/utils/aztec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export async function deployAztecContracts(
5858
acceleratedTestDeployments: boolean,
5959
config: L1ContractsConfig,
6060
realVerifier: boolean,
61+
createVerificationJson: string | false,
6162
debugLogger: Logger,
6263
): Promise<DeployL1ContractsReturnType> {
6364
const { createEthereumChain, deployL1Contracts } = await import('@aztec/ethereum');
@@ -70,7 +71,7 @@ export async function deployAztecContracts(
7071

7172
const { getVKTreeRoot } = await import('@aztec/noir-protocol-circuits-types/vk-tree');
7273

73-
return await deployL1Contracts(
74+
const result = await deployL1Contracts(
7475
chain.rpcUrls,
7576
account,
7677
chain.chainInfo,
@@ -87,7 +88,10 @@ export async function deployAztecContracts(
8788
...config,
8889
},
8990
config,
91+
createVerificationJson,
9092
);
93+
94+
return result;
9195
}
9296

9397
export async function deployNewRollupContracts(

yarn-project/ethereum/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
],
3333
"dependencies": {
3434
"@aztec/blob-lib": "workspace:^",
35+
"@aztec/constants": "workspace:^",
3536
"@aztec/foundation": "workspace:^",
3637
"@aztec/l1-artifacts": "workspace:^",
3738
"@viem/anvil": "^0.0.10",

yarn-project/ethereum/src/contracts/registry.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ describe('Registry', () => {
6363
'stakingAssetHandlerAddress',
6464
'zkPassportVerifierAddress',
6565
);
66+
// Updating the coin issuer address to the deployer address bc we don't yet set CoinIssuer contract as the owner of the fee asset
67+
// TODO: should remove once #16630 is merged
68+
deployedAddresses.coinIssuerAddress = EthAddress.fromString(privateKey.address);
6669
registry = new RegistryContract(l1Client, deployedAddresses.registryAddress);
6770

6871
const rollup = new RollupContract(l1Client, deployedAddresses.rollupAddress);

0 commit comments

Comments
 (0)