4747# The script loads from .env-test (testnet) or .env-prod (mainnet):
4848# - DEPLOYER_PRIVATE_KEY: Private key with ETH for gas
4949# - NODL: Address of the NODL token (used as bond token)
50+ # - FLEET_OPERATOR: Address of the backend swarm operator (whitelisted user)
5051# - BASE_BOND: Bond amount in wei (e.g., 100000000000000000000 for 100 NODL)
51- # - OWNER: (optional) Contract owner address, defaults to deployer
52+ # - NODL_ADMIN: (optional) Owner address for all deployed contracts, defaults to deployer
53+ # - PAYMASTER_WITHDRAWER: (optional) Address allowed to withdraw tokens from paymaster, defaults to NODL_ADMIN
54+ # - COUNTRY_MULTIPLIER: (optional) Country multiplier for bond calculation (0 = use default)
55+ # - BOND_QUOTA: (optional) Max bond amount sponsorable per period in wei
56+ # - BOND_PERIOD: (optional) Quota renewal period in seconds
5257#
5358# =============================================================================
5459
@@ -71,11 +76,15 @@ case "$NETWORK" in
7176 ENV_FILE=" .env-test"
7277 HARDHAT_NETWORK=" zkSyncSepoliaTestnet"
7378 EXPLORER_URL=" https://sepolia.explorer.zksync.io"
79+ VERIFIER_URL=" https://explorer.sepolia.era.zksync.dev/contract_verification"
80+ FORGE_CHAIN=" zksync-testnet"
7481 ;;
7582 mainnet)
7683 ENV_FILE=" .env-prod"
7784 HARDHAT_NETWORK=" zkSyncMainnet"
7885 EXPLORER_URL=" https://explorer.zksync.io"
86+ VERIFIER_URL=" https://zksync2-mainnet-explorer.zksync.io/contract_verification"
87+ FORGE_CHAIN=" zksync"
7988 ;;
8089 * )
8190 echo " Error: Unknown network '$NETWORK '. Use 'testnet' or 'mainnet'."
@@ -158,14 +167,26 @@ preflight_checks() {
158167 exit 1
159168 fi
160169
170+ if [ -z " $FLEET_OPERATOR " ]; then
171+ log_error " FLEET_OPERATOR not set in $ENV_FILE "
172+ exit 1
173+ fi
174+
161175 # Ensure DEPLOYER_PRIVATE_KEY has 0x prefix (required by forge vm.envUint)
162176 if [[ " $DEPLOYER_PRIVATE_KEY " != 0x* ]]; then
163177 export DEPLOYER_PRIVATE_KEY=" 0x${DEPLOYER_PRIVATE_KEY} "
164178 fi
165179
180+ # Derive deployer address for defaults
181+ DEPLOYER_ADDRESS=$( cast wallet address " $DEPLOYER_PRIVATE_KEY " )
182+
166183 # Set defaults
167184 export BOND_TOKEN=" ${BOND_TOKEN:- $NODL } "
168- export BASE_BOND=" ${BASE_BOND:- 100000000000000000000} " # 100 NODL default
185+ export BASE_BOND=" ${BASE_BOND:- 1000000000000000000000} " # 1000 NODL default
186+ export NODL_ADMIN=" ${NODL_ADMIN:- $DEPLOYER_ADDRESS } "
187+ export PAYMASTER_WITHDRAWER=" ${PAYMASTER_WITHDRAWER:- $NODL_ADMIN } "
188+ export BOND_QUOTA=" ${BOND_QUOTA:- 100000000000000000000000} " # 100000 NODL default
189+ export BOND_PERIOD=" ${BOND_PERIOD:- 86400} " # 1 day default
169190
170191 log_success " Pre-flight checks passed"
171192}
@@ -309,17 +330,20 @@ deploy_contracts() {
309330
310331 if [ " $BROADCAST " = " --broadcast" ]; then
311332 FORGE_ARGS+=(" --broadcast" " --slow" )
312-
313- # Add ZkSync-specific verification
314- if [ -n " $L2_VERIFIER_URL " ]; then
315- FORGE_ARGS+=(" --verify" " --verifier" " zksync" " --verifier-url" " $L2_VERIFIER_URL " )
316- fi
333+ # NOTE: We do NOT add --verify here. forge script --verify sends absolute
334+ # source paths which the ZkSync verifier rejects with "import with absolute
335+ # or traversal path". Source code verification is handled separately in
336+ # verify_source_code() using forge flatten + forge verify-contract.
317337 else
318338 log_warning " DRY RUN MODE - Add '--broadcast' to actually deploy"
319339 log_info " Would deploy with:"
320340 log_info " BOND_TOKEN: $BOND_TOKEN "
321341 log_info " BASE_BOND: $BASE_BOND "
322- log_info " OWNER: ${OWNER:- deployer} "
342+ log_info " NODL_ADMIN: ${NODL_ADMIN:- deployer} "
343+ log_info " PAYMASTER_WITHDRAWER: ${PAYMASTER_WITHDRAWER:- deployer} "
344+ log_info " FLEET_OPERATOR: $FLEET_OPERATOR "
345+ log_info " BOND_QUOTA: $BOND_QUOTA "
346+ log_info " BOND_PERIOD: $BOND_PERIOD "
323347 log_info " RPC: $RPC_URL "
324348 return 0
325349 fi
@@ -338,9 +362,10 @@ deploy_contracts() {
338362 FLEET_IDENTITY_IMPL=$( grep -o ' FleetIdentity Implementation: 0x[0-9a-fA-F]*' " $DEPLOY_LOG " | grep -o ' 0x[0-9a-fA-F]*' )
339363 SWARM_REGISTRY_PROXY=$( grep -o ' SwarmRegistry Proxy: 0x[0-9a-fA-F]*' " $DEPLOY_LOG " | grep -o ' 0x[0-9a-fA-F]*' )
340364 SWARM_REGISTRY_IMPL=$( grep -o ' SwarmRegistry Implementation: 0x[0-9a-fA-F]*' " $DEPLOY_LOG " | grep -o ' 0x[0-9a-fA-F]*' )
365+ BOND_TREASURY_PAYMASTER=$( grep -o ' BondTreasuryPaymaster: 0x[0-9a-fA-F]*' " $DEPLOY_LOG " | grep -o ' 0x[0-9a-fA-F]*' )
341366
342367 # Validate we got addresses
343- if [ -z " $SERVICE_PROVIDER_PROXY " ] || [ -z " $FLEET_IDENTITY_PROXY " ] || [ -z " $SWARM_REGISTRY_PROXY " ]; then
368+ if [ -z " $SERVICE_PROVIDER_PROXY " ] || [ -z " $FLEET_IDENTITY_PROXY " ] || [ -z " $SWARM_REGISTRY_PROXY " ] || [ -z " $BOND_TREASURY_PAYMASTER " ] ; then
344369 log_error " Could not extract all addresses from output"
345370 log_info " Full output saved to: $DEPLOY_LOG "
346371 cat " $DEPLOY_LOG "
@@ -401,9 +426,151 @@ verify_deployment() {
401426 SR_OWNER=$( cast call " $SWARM_REGISTRY_PROXY " " owner()(address)" --rpc-url " $RPC_URL " )
402427 log_success " SwarmRegistry owner: $SR_OWNER "
403428
429+ # Test BondTreasuryPaymaster
430+ log_info " Testing BondTreasuryPaymaster..."
431+ BTP_TOKEN=$( cast call " $BOND_TREASURY_PAYMASTER " " bondToken()(address)" --rpc-url " $RPC_URL " )
432+ BTP_QUOTA=$( cast call " $BOND_TREASURY_PAYMASTER " " quota()(uint256)" --rpc-url " $RPC_URL " )
433+ log_success " BondTreasuryPaymaster bondToken: $BTP_TOKEN "
434+ log_success " BondTreasuryPaymaster quota: $BTP_QUOTA "
435+
404436 log_success " All contracts verified successfully!"
405437}
406438
439+ # =============================================================================
440+ # Step 4b: Verify Source Code on Block Explorer
441+ # =============================================================================
442+ #
443+ # Why a separate step:
444+ # forge script --verify sends absolute file paths (e.g. /Users/me/project/src/...)
445+ # which the ZkSync verifier rejects: "import with absolute or traversal path".
446+ #
447+ # Workaround:
448+ # 1. Flatten each contract into a single .sol file (no imports)
449+ # 2. Use forge verify-contract with the flattened file
450+ # 3. Clean up temporary flat files
451+ #
452+ # Constructor args are extracted from the broadcast JSON using the ZkSync
453+ # ContractDeployer ABI: create(bytes32 salt, bytes32 bytecodeHash, bytes ctorInput)
454+ #
455+ # =============================================================================
456+
457+ verify_source_code () {
458+ if [ " $BROADCAST " != " --broadcast" ]; then
459+ return 0
460+ fi
461+
462+ log_info " Verifying source code on block explorer..."
463+
464+ # Get RPC URL for chain detection
465+ if [ " $NETWORK " = " mainnet" ]; then
466+ RPC_URL=" ${L2_RPC:- https:// mainnet.era.zksync.io} "
467+ CHAIN_ID=" 324"
468+ else
469+ RPC_URL=" ${L2_RPC:- https:// rpc.ankr.com/ zksync_era_sepolia} "
470+ CHAIN_ID=" 300"
471+ fi
472+
473+ BROADCAST_JSON=" broadcast/DeploySwarmUpgradeableZkSync.s.sol/${CHAIN_ID} /run-latest.json"
474+ if [ ! -f " $BROADCAST_JSON " ]; then
475+ log_error " Broadcast file not found: $BROADCAST_JSON "
476+ log_warning " Skipping source code verification"
477+ return 1
478+ fi
479+
480+ # Extract constructor args from broadcast JSON
481+ # ZkSync ContractDeployer.create(): 0x9c4d535b + salt(32) + hash(32) + offset_to_ctor(32) + len(32) + ctor_data
482+ log_info " Extracting constructor args from broadcast..."
483+ CTOR_ARGS=$( python3 -c "
484+ import json, sys
485+ with open('$BROADCAST_JSON ') as f:
486+ data = json.load(f)
487+ for tx in data['transactions']:
488+ addr = (tx.get('additionalContracts') or [{}])[0].get('address', '')
489+ inp = tx['transaction'].get('input', '')
490+ payload = inp[10:] # skip 0x + 9c4d535b
491+ offset = int(payload[128:192], 16)
492+ ctor_start = offset * 2
493+ ctor_len = int(payload[ctor_start:ctor_start+64], 16)
494+ ctor_args = payload[ctor_start+64:ctor_start+64+ctor_len*2]
495+ print(f'{addr}:{ctor_args}')
496+ " )
497+
498+ # Build lookup of address -> constructor args
499+ declare -A CTOR_MAP
500+ while IFS=: read -r addr args; do
501+ CTOR_MAP[" $addr " ]=" $args "
502+ done <<< " $CTOR_ARGS"
503+
504+ # Create temporary directory for flattened sources
505+ FLAT_DIR=$( mktemp -d)
506+
507+ # Flatten all unique contract sources
508+ log_info " Flattening contract sources..."
509+ forge flatten src/swarms/ServiceProviderUpgradeable.sol > " $FLAT_DIR /FlatSP.sol"
510+ forge flatten src/swarms/FleetIdentityUpgradeable.sol > " $FLAT_DIR /FlatFI.sol"
511+ forge flatten src/swarms/SwarmRegistryUniversalUpgradeable.sol > " $FLAT_DIR /FlatSR.sol"
512+ forge flatten lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol > " $FLAT_DIR /FlatProxy.sol"
513+ forge flatten src/paymasters/BondTreasuryPaymaster.sol > " $FLAT_DIR /FlatBTP.sol"
514+
515+ # Copy flat files into src/ so forge can find them
516+ cp " $FLAT_DIR /FlatSP.sol" src/FlatSP.sol
517+ cp " $FLAT_DIR /FlatFI.sol" src/FlatFI.sol
518+ cp " $FLAT_DIR /FlatSR.sol" src/FlatSR.sol
519+ cp " $FLAT_DIR /FlatProxy.sol" src/FlatProxy.sol
520+ cp " $FLAT_DIR /FlatBTP.sol" src/FlatBTP.sol
521+
522+ VERIFY_FAILED=0
523+
524+ # Helper to verify a single contract
525+ verify_one () {
526+ local address=" $1 "
527+ local source=" $2 "
528+ local label=" $3 "
529+ local ctor_key
530+ ctor_key=$( echo " $address " | tr ' [:upper:]' ' [:lower:]' )
531+ local args=" ${CTOR_MAP[$ctor_key]} "
532+
533+ local VARGS=(
534+ --zksync
535+ --chain " $FORGE_CHAIN "
536+ --verifier zksync
537+ --verifier-url " $VERIFIER_URL "
538+ " $address "
539+ " $source "
540+ )
541+ if [ -n " $args " ]; then
542+ VARGS+=(--constructor-args " $args " )
543+ fi
544+
545+ log_info " Verifying $label at $address ..."
546+ if forge verify-contract " ${VARGS[@]} " 2>&1 ; then
547+ log_success " $label verified"
548+ else
549+ log_error " $label verification failed (can retry manually)"
550+ VERIFY_FAILED=$(( VERIFY_FAILED + 1 ))
551+ fi
552+ }
553+
554+ # Verify all 7 contracts
555+ verify_one " $SERVICE_PROVIDER_IMPL " " src/FlatSP.sol:ServiceProviderUpgradeable" " ServiceProvider Implementation"
556+ verify_one " $SERVICE_PROVIDER_PROXY " " src/FlatProxy.sol:ERC1967Proxy" " ServiceProvider Proxy"
557+ verify_one " $FLEET_IDENTITY_IMPL " " src/FlatFI.sol:FleetIdentityUpgradeable" " FleetIdentity Implementation"
558+ verify_one " $FLEET_IDENTITY_PROXY " " src/FlatProxy.sol:ERC1967Proxy" " FleetIdentity Proxy"
559+ verify_one " $SWARM_REGISTRY_IMPL " " src/FlatSR.sol:SwarmRegistryUniversalUpgradeable" " SwarmRegistry Implementation"
560+ verify_one " $SWARM_REGISTRY_PROXY " " src/FlatProxy.sol:ERC1967Proxy" " SwarmRegistry Proxy"
561+ verify_one " $BOND_TREASURY_PAYMASTER " " src/FlatBTP.sol:BondTreasuryPaymaster" " BondTreasuryPaymaster"
562+
563+ # Clean up flat files from src/
564+ rm -f src/FlatSP.sol src/FlatFI.sol src/FlatSR.sol src/FlatProxy.sol src/FlatBTP.sol
565+ rm -rf " $FLAT_DIR "
566+
567+ if [ " $VERIFY_FAILED " -gt 0 ]; then
568+ log_warning " $VERIFY_FAILED contract(s) failed source verification (deployment itself succeeded)"
569+ else
570+ log_success " All 7 contracts source-code verified on block explorer!"
571+ fi
572+ }
573+
407574# =============================================================================
408575# Step 5: Update Environment File
409576# =============================================================================
@@ -438,6 +605,9 @@ update_env_file() {
438605 sed -i.bak ' /^SWARM_REGISTRY_PROXY=/d' " $ENV_FILE "
439606 sed -i.bak ' /^SWARM_REGISTRY_IMPL=/d' " $ENV_FILE "
440607 sed -i.bak ' /^BASE_BOND=/d' " $ENV_FILE "
608+ sed -i.bak ' /^BOND_TREASURY_PAYMASTER=/d' " $ENV_FILE "
609+ sed -i.bak ' /^BOND_QUOTA=/d' " $ENV_FILE "
610+ sed -i.bak ' /^BOND_PERIOD=/d' " $ENV_FILE "
441611 # Clean up trailing blank lines
442612 sed -i.bak -e :a -e ' /^\n*$/{$d;N;ba' -e ' }' " $ENV_FILE "
443613 rm -f " ${ENV_FILE} .bak"
@@ -453,7 +623,10 @@ FLEET_IDENTITY_PROXY=$FLEET_IDENTITY_PROXY
453623FLEET_IDENTITY_IMPL=$FLEET_IDENTITY_IMPL
454624SWARM_REGISTRY_PROXY=$SWARM_REGISTRY_PROXY
455625SWARM_REGISTRY_IMPL=$SWARM_REGISTRY_IMPL
626+ BOND_TREASURY_PAYMASTER=$BOND_TREASURY_PAYMASTER
456627BASE_BOND=$BASE_BOND
628+ BOND_QUOTA=$BOND_QUOTA
629+ BOND_PERIOD=$BOND_PERIOD
457630EOF
458631
459632 log_success " Environment file updated"
@@ -499,10 +672,18 @@ print_summary() {
499672 echo " Implementation: $SWARM_REGISTRY_IMPL "
500673 echo " Explorer: $EXPLORER_URL /address/$SWARM_REGISTRY_PROXY "
501674 echo " "
675+ echo " BondTreasuryPaymaster:"
676+ echo " Address: $BOND_TREASURY_PAYMASTER "
677+ echo " Explorer: $EXPLORER_URL /address/$BOND_TREASURY_PAYMASTER "
678+ echo " "
502679 echo " Configuration:"
503- echo " Owner: ${OWNER:- deployer} "
504- echo " Bond Token: $BOND_TOKEN "
505- echo " Base Bond: $BASE_BOND wei"
680+ echo " Owner: ${NODL_ADMIN:- deployer} "
681+ echo " Withdrawer: ${PAYMASTER_WITHDRAWER:- deployer} "
682+ echo " Fleet Operator: $FLEET_OPERATOR "
683+ echo " Bond Token: $BOND_TOKEN "
684+ echo " Base Bond: $BASE_BOND wei"
685+ echo " Bond Quota: $BOND_QUOTA wei"
686+ echo " Bond Period: $BOND_PERIOD seconds"
506687 echo " "
507688 echo " =============================================="
508689}
@@ -525,6 +706,7 @@ main() {
525706 compile_contracts
526707 deploy_contracts
527708 verify_deployment
709+ verify_source_code
528710 update_env_file
529711 print_summary
530712
0 commit comments