Skip to content

Commit 242af5f

Browse files
authored
Deploy swarms production improvements (#105)
1 parent 11b9258 commit 242af5f

8 files changed

Lines changed: 415 additions & 68 deletions

.cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"backgrounded",
9696
"reconstructable",
9797
"Württemberg",
98-
"delegatecall"
98+
"delegatecall",
99+
"sponsorable"
99100
]
100101
}

ops/deploy_swarm_contracts_l1.sh

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
# - DEPLOYER_PRIVATE_KEY: Private key with ETH for gas
4545
# - BOND_TOKEN: Address of the ERC20 bond token (NODL)
4646
# - BASE_BOND: Bond amount in wei (e.g., 100000000000000000000 for 100 NODL)
47-
# - OWNER: (optional) Contract owner address, defaults to deployer
47+
# - NODL_ADMIN: (optional) Owner address for all deployed contracts, defaults to deployer
48+
# - COUNTRY_MULTIPLIER: (optional) Country multiplier for bond calculation (0 = use default)
4849
# - L1_RPC: RPC URL for L1 (Sepolia or Mainnet)
4950
# - ETHERSCAN_API_KEY: For contract verification
5051
#
@@ -157,9 +158,18 @@ preflight_checks() {
157158
exit 1
158159
fi
159160

161+
# Ensure DEPLOYER_PRIVATE_KEY has 0x prefix
162+
if [[ "$DEPLOYER_PRIVATE_KEY" != 0x* ]]; then
163+
export DEPLOYER_PRIVATE_KEY="0x${DEPLOYER_PRIVATE_KEY}"
164+
fi
165+
166+
# Derive deployer address for defaults
167+
DEPLOYER_ADDRESS=$(cast wallet address "$DEPLOYER_PRIVATE_KEY")
168+
160169
# Set defaults
161170
export BOND_TOKEN="${BOND_TOKEN:-$NODL}"
162171
export BASE_BOND="${BASE_BOND:-100000000000000000000}" # 100 NODL default
172+
export NODL_ADMIN="${NODL_ADMIN:-$DEPLOYER_ADDRESS}"
163173

164174
log_success "Pre-flight checks passed"
165175
}
@@ -214,7 +224,7 @@ deploy_contracts() {
214224
log_info "Would deploy with:"
215225
log_info " BOND_TOKEN: $BOND_TOKEN"
216226
log_info " BASE_BOND: $BASE_BOND"
217-
log_info " OWNER: ${OWNER:-deployer}"
227+
log_info " NODL_ADMIN: ${NODL_ADMIN:-deployer}"
218228
log_info " RPC: $L1_RPC"
219229
fi
220230

@@ -372,7 +382,7 @@ print_summary() {
372382
echo " Explorer: $EXPLORER_URL/address/$SWARM_REGISTRY_PROXY"
373383
echo ""
374384
echo "Configuration:"
375-
echo " Owner: ${OWNER:-deployer}"
385+
echo " Owner: ${NODL_ADMIN:-deployer}"
376386
echo " Bond Token: $BOND_TOKEN"
377387
echo " Base Bond: $BASE_BOND wei"
378388
echo ""

ops/deploy_swarm_contracts_zksync.sh

Lines changed: 194 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@
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
453623
FLEET_IDENTITY_IMPL=$FLEET_IDENTITY_IMPL
454624
SWARM_REGISTRY_PROXY=$SWARM_REGISTRY_PROXY
455625
SWARM_REGISTRY_IMPL=$SWARM_REGISTRY_IMPL
626+
BOND_TREASURY_PAYMASTER=$BOND_TREASURY_PAYMASTER
456627
BASE_BOND=$BASE_BOND
628+
BOND_QUOTA=$BOND_QUOTA
629+
BOND_PERIOD=$BOND_PERIOD
457630
EOF
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

script/DeploySwarmUpgradeable.s.sol

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {SwarmRegistryL1Upgradeable} from "../src/swarms/SwarmRegistryL1Upgradeab
3232
* - DEPLOYER_PRIVATE_KEY: Private key for deployment
3333
* - BOND_TOKEN: Address of the ERC20 bond token
3434
* - BASE_BOND: Base bond amount in wei
35-
* - OWNER: Owner address for upgrade authorization (defaults to deployer)
35+
* - COUNTRY_MULTIPLIER: (optional) Country multiplier for bond calculation (0 = use default)
36+
* - NODL_ADMIN: (optional) Owner address for all deployed contracts (defaults to deployer)
3637
*/
3738
contract DeploySwarmUpgradeableL1 is Script {
3839
// Deployment artifacts
@@ -49,7 +50,7 @@ contract DeploySwarmUpgradeableL1 is Script {
4950
address bondToken = vm.envAddress("BOND_TOKEN");
5051
uint256 baseBond = vm.envUint("BASE_BOND");
5152
uint256 countryMultiplier = vm.envOr("COUNTRY_MULTIPLIER", uint256(0)); // 0 means use the default
52-
address owner = vm.envOr("OWNER", vm.addr(deployerPrivateKey));
53+
address owner = vm.envOr("NODL_ADMIN", vm.addr(deployerPrivateKey));
5354

5455
console.log("=== Deploying Upgradeable Swarm Contracts (L1) ===");
5556
console.log("Bond Token:", bondToken);
@@ -76,8 +77,9 @@ contract DeploySwarmUpgradeableL1 is Script {
7677
fleetIdentityImpl = address(new FleetIdentityUpgradeable());
7778
console.log(" Implementation:", fleetIdentityImpl);
7879

79-
bytes memory fleetIdentityInitData =
80-
abi.encodeWithSelector(FleetIdentityUpgradeable.initialize.selector, owner, bondToken, baseBond, countryMultiplier);
80+
bytes memory fleetIdentityInitData = abi.encodeWithSelector(
81+
FleetIdentityUpgradeable.initialize.selector, owner, bondToken, baseBond, countryMultiplier
82+
);
8183
fleetIdentityProxy = address(new ERC1967Proxy(fleetIdentityImpl, fleetIdentityInitData));
8284
console.log(" Proxy:", fleetIdentityProxy);
8385
console.log("");

0 commit comments

Comments
 (0)