Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions .agent/rules/solidity_zksync.md

This file was deleted.

54 changes: 53 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

- **Safety First**:
- **Checks-Effects-Interactions (CEI)** pattern must be strictly followed.
- Use `Ownable2Step` over `Ownable` for privileged access.
- When a contract requires an owner (e.g., admin-configurable parameters), prefer `Ownable2Step` over `Ownable`. Do **not** add ownership to contracts that don't need it — many contracts are fully permissionless by design.
- Prefer `ReentrancyGuard` for external calls where appropriate.
- **Gas & Efficiency**:
- Use **Custom Errors** (`error MyError();`) instead of `require` strings.
Expand Down Expand Up @@ -48,3 +48,55 @@ For these contracts, use:
forge build --match-path src/swarms/SwarmRegistryL1.sol
forge test --match-path test/SwarmRegistryL1.t.sol
```

## ZkSync Source Code Verification

**IMPORTANT**: Do NOT use `forge script --verify` or `forge verify-contract` directly for ZkSync contracts. Both fail to achieve full verification due to path handling issues with the ZkSync block explorer verifier.

### The Problem (three broken paths)

1. `forge script --verify` sends **absolute file paths** (`/Users/me/project/src/...`) → verifier rejects.
2. `forge verify-contract` (standard JSON) sends OpenZeppelin sources containing `../` relative imports → verifier rejects "import with absolute or traversal path".
3. `forge verify-contract --flatten` or manual flattening eliminates imports but changes the source file path in the metadata hash → **"partially verified"** (metadata mismatch).

### The Solution

Use `ops/verify_zksync_contracts.py` which:

1. Generates standard JSON via `forge verify-contract --show-standard-json-input`
2. Rewrites all `../` relative imports in OpenZeppelin source content to resolved project-absolute paths (e.g., `../../utils/Foo.sol` → `lib/openzeppelin-contracts/contracts/utils/Foo.sol`)
3. Submits directly to the ZkSync verification API via HTTP

### Full vs Partial Verification

- **`bytecode_hash = "none"`** is set in `foundry.toml` (both `[profile.default]` and `[profile.zksync]`). This omits the CBOR metadata hash from bytecode. Contracts deployed with this setting achieve **full verification**.
- Contracts deployed **before** this setting was added (pre 2026-04-10) will always show "partially verified" — this is cosmetic only. The source code is correct and auditable.

### Usage

```bash
# After deployment — verify all contracts from broadcast:
python3 ops/verify_zksync_contracts.py \
--broadcast broadcast/DeploySwarmUpgradeableZkSync.s.sol/324/run-latest.json \
--verifier-url https://zksync2-mainnet-explorer.zksync.io/contract_verification

# Verify a single contract:
python3 ops/verify_zksync_contracts.py \
--address 0x1234... \
--contract src/swarms/FleetIdentityUpgradeable.sol:FleetIdentityUpgradeable \
--verifier-url https://zksync2-mainnet-explorer.zksync.io/contract_verification

# With constructor args:
python3 ops/verify_zksync_contracts.py \
--address 0x1234... \
--contract lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
--constructor-args 0xabcdef...
```

### Adding New Contract Types

When deploying a new contract type, add its mapping to `CONTRACT_SOURCE_MAP` in `ops/verify_zksync_contracts.py` so `--broadcast` mode can auto-detect it.

### Automated (via deploy script)

`ops/deploy_swarm_contracts_zksync.sh` calls `verify_zksync_contracts.py` automatically after deployment. No manual steps needed for the standard swarm contracts.
7 changes: 7 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ via_ir = true
optimizer = true
optimizer_runs = 200

# Omit CBOR metadata hash from bytecode. Required for full (not partial) source
# verification on ZkSync explorer. Without this, the metadata hash includes source
# file paths, and the ZkSync verifier rejects OpenZeppelin's "../" relative imports,
# forcing flattened/rewritten sources that produce a different metadata hash.
bytecode_hash = "none"

[lint]
# Exclude ERC20 transfer warning - false positive for ERC721.transferFrom in tests
exclude_lints = ["erc20-unchecked-transfer"]
Expand All @@ -23,6 +29,7 @@ solc = "0.8.26"
via_ir = true
optimizer = true
optimizer_runs = 200
bytecode_hash = "none"
# Exclude L1-only contracts that use SSTORE2/EXTCODECOPY
ignored_error_codes = []
ignored_warnings_from = []
9 changes: 7 additions & 2 deletions hardhat-deploy/DeploySwarmUpgradeable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dotenv.config({ path: ".env-test" });
* - DEPLOYER_PRIVATE_KEY: Private key for deployment
* - BOND_TOKEN: Address of the ERC20 bond token
* - BASE_BOND: Base bond amount in wei
* - OWNER: (optional) Owner address, defaults to deployer
* - L2_ADMIN: Owner address for all deployed L2 contracts (ZkSync Safe multisig)
*/
module.exports = async function (hre: HardhatRuntimeEnvironment) {
const bondToken = process.env.BOND_TOKEN!;
Expand All @@ -28,7 +28,12 @@ module.exports = async function (hre: HardhatRuntimeEnvironment) {
const wallet = new Wallet(process.env.DEPLOYER_PRIVATE_KEY!, provider);
const deployer = new Deployer(hre, wallet);

const owner = process.env.OWNER || wallet.address;
const owner = process.env.L2_ADMIN;
if (!owner) {
throw new Error(
"L2_ADMIN environment variable is required (ZkSync Safe multisig)",
);
}

console.log("=== Deploying Upgradeable Swarm Contracts on ZkSync ===");
console.log("Bond Token:", bondToken);
Expand Down
144 changes: 38 additions & 106 deletions ops/deploy_swarm_contracts_zksync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
# - NODL: Address of the NODL token (used as bond token)
# - FLEET_OPERATOR: Address of the backend swarm operator (whitelisted user)
# - BASE_BOND: Bond amount in wei (e.g., 100000000000000000000 for 100 NODL)
# - NODL_ADMIN: (optional) Owner address for all deployed contracts, defaults to deployer
# - PAYMASTER_WITHDRAWER: (optional) Address allowed to withdraw tokens from paymaster, defaults to NODL_ADMIN
# - L2_ADMIN: Owner address for all deployed L2 contracts (ZkSync Safe multisig)
# - PAYMASTER_WITHDRAWER: (optional) Address allowed to withdraw tokens from paymaster, defaults to L2_ADMIN
# - COUNTRY_MULTIPLIER: (optional) Country multiplier for bond calculation (0 = use default)
# - BOND_QUOTA: (optional) Max bond amount sponsorable per period in wei
# - BOND_PERIOD: (optional) Quota renewal period in seconds
Expand Down Expand Up @@ -183,8 +183,11 @@ preflight_checks() {
# Set defaults
export BOND_TOKEN="${BOND_TOKEN:-$NODL}"
export BASE_BOND="${BASE_BOND:-1000000000000000000000}" # 1000 NODL default
export NODL_ADMIN="${NODL_ADMIN:-$DEPLOYER_ADDRESS}"
export PAYMASTER_WITHDRAWER="${PAYMASTER_WITHDRAWER:-$NODL_ADMIN}"
if [ -z "$L2_ADMIN" ]; then
log_error "L2_ADMIN not set in $ENV_FILE (must be the ZkSync Safe multisig)"
exit 1
fi
export PAYMASTER_WITHDRAWER="${PAYMASTER_WITHDRAWER:-$L2_ADMIN}"
export BOND_QUOTA="${BOND_QUOTA:-100000000000000000000000}" # 100000 NODL default
export BOND_PERIOD="${BOND_PERIOD:-86400}" # 1 day default

Expand Down Expand Up @@ -339,7 +342,7 @@ deploy_contracts() {
log_info "Would deploy with:"
log_info " BOND_TOKEN: $BOND_TOKEN"
log_info " BASE_BOND: $BASE_BOND"
log_info " NODL_ADMIN: ${NODL_ADMIN:-deployer}"
log_info " L2_ADMIN: $L2_ADMIN"
log_info " PAYMASTER_WITHDRAWER: ${PAYMASTER_WITHDRAWER:-deployer}"
log_info " FLEET_OPERATOR: $FLEET_OPERATOR"
log_info " BOND_QUOTA: $BOND_QUOTA"
Expand Down Expand Up @@ -441,16 +444,18 @@ verify_deployment() {
# =============================================================================
#
# Why a separate step:
# forge script --verify sends absolute file paths (e.g. /Users/me/project/src/...)
# which the ZkSync verifier rejects: "import with absolute or traversal path".
# forge script --verify sends absolute file paths that ZkSync verifier rejects.
# forge verify-contract sends standard JSON with "../" relative imports in
# OpenZeppelin sources, which the verifier also rejects.
#
# Workaround:
# 1. Flatten each contract into a single .sol file (no imports)
# 2. Use forge verify-contract with the flattened file
# 3. Clean up temporary flat files
# Solution:
# ops/verify_zksync_contracts.py generates standard JSON via forge, rewrites
# all "../" relative imports to resolved absolute-within-project paths, then
# submits directly to the ZkSync verification API.
#
# Constructor args are extracted from the broadcast JSON using the ZkSync
# ContractDeployer ABI: create(bytes32 salt, bytes32 bytecodeHash, bytes ctorInput)
# For contracts compiled with bytecode_hash = "none" (foundry.toml, added
# 2026-04-10), this achieves FULL verification. For older contracts, it
# achieves "partial" (metadata mismatch — cosmetic only, source is correct).
#
# =============================================================================

Expand All @@ -461,12 +466,10 @@ verify_source_code() {

log_info "Verifying source code on block explorer..."

# Get RPC URL for chain detection
# Determine chain ID for broadcast path
if [ "$NETWORK" = "mainnet" ]; then
RPC_URL="${L2_RPC:-https://mainnet.era.zksync.io}"
CHAIN_ID="324"
else
RPC_URL="${L2_RPC:-https://rpc.ankr.com/zksync_era_sepolia}"
CHAIN_ID="300"
fi

Expand All @@ -477,97 +480,26 @@ verify_source_code() {
return 1
fi

# Extract constructor args from broadcast JSON
# ZkSync ContractDeployer.create(): 0x9c4d535b + salt(32) + hash(32) + offset_to_ctor(32) + len(32) + ctor_data
log_info "Extracting constructor args from broadcast..."
CTOR_ARGS=$(python3 -c "
import json, sys
with open('$BROADCAST_JSON') as f:
data = json.load(f)
for tx in data['transactions']:
addr = (tx.get('additionalContracts') or [{}])[0].get('address', '')
inp = tx['transaction'].get('input', '')
payload = inp[10:] # skip 0x + 9c4d535b
offset = int(payload[128:192], 16)
ctor_start = offset * 2
ctor_len = int(payload[ctor_start:ctor_start+64], 16)
ctor_args = payload[ctor_start+64:ctor_start+64+ctor_len*2]
print(f'{addr}:{ctor_args}')
")

# Build lookup of address -> constructor args
declare -A CTOR_MAP
while IFS=: read -r addr args; do
CTOR_MAP["$addr"]="$args"
done <<< "$CTOR_ARGS"

# Create temporary directory for flattened sources
FLAT_DIR=$(mktemp -d)

# Flatten all unique contract sources
log_info "Flattening contract sources..."
forge flatten src/swarms/ServiceProviderUpgradeable.sol > "$FLAT_DIR/FlatSP.sol"
forge flatten src/swarms/FleetIdentityUpgradeable.sol > "$FLAT_DIR/FlatFI.sol"
forge flatten src/swarms/SwarmRegistryUniversalUpgradeable.sol > "$FLAT_DIR/FlatSR.sol"
forge flatten lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol > "$FLAT_DIR/FlatProxy.sol"
forge flatten src/paymasters/BondTreasuryPaymaster.sol > "$FLAT_DIR/FlatBTP.sol"

# Copy flat files into src/ so forge can find them
cp "$FLAT_DIR/FlatSP.sol" src/FlatSP.sol
cp "$FLAT_DIR/FlatFI.sol" src/FlatFI.sol
cp "$FLAT_DIR/FlatSR.sol" src/FlatSR.sol
cp "$FLAT_DIR/FlatProxy.sol" src/FlatProxy.sol
cp "$FLAT_DIR/FlatBTP.sol" src/FlatBTP.sol

VERIFY_FAILED=0

# Helper to verify a single contract
verify_one() {
local address="$1"
local source="$2"
local label="$3"
local ctor_key
ctor_key=$(echo "$address" | tr '[:upper:]' '[:lower:]')
local args="${CTOR_MAP[$ctor_key]}"

local VARGS=(
--zksync
--chain "$FORGE_CHAIN"
--verifier zksync
--verifier-url "$VERIFIER_URL"
"$address"
"$source"
)
if [ -n "$args" ]; then
VARGS+=(--constructor-args "$args")
fi
# Check python3 is available
if ! command -v python3 &> /dev/null; then
log_error "python3 not found. Install Python 3.8+ for source code verification."
log_warning "Skipping source code verification"
return 1
fi

log_info "Verifying $label at $address..."
if forge verify-contract "${VARGS[@]}" 2>&1; then
log_success "$label verified"
else
log_error "$label verification failed (can retry manually)"
VERIFY_FAILED=$((VERIFY_FAILED + 1))
fi
}

# Verify all 7 contracts
verify_one "$SERVICE_PROVIDER_IMPL" "src/FlatSP.sol:ServiceProviderUpgradeable" "ServiceProvider Implementation"
verify_one "$SERVICE_PROVIDER_PROXY" "src/FlatProxy.sol:ERC1967Proxy" "ServiceProvider Proxy"
verify_one "$FLEET_IDENTITY_IMPL" "src/FlatFI.sol:FleetIdentityUpgradeable" "FleetIdentity Implementation"
verify_one "$FLEET_IDENTITY_PROXY" "src/FlatProxy.sol:ERC1967Proxy" "FleetIdentity Proxy"
verify_one "$SWARM_REGISTRY_IMPL" "src/FlatSR.sol:SwarmRegistryUniversalUpgradeable" "SwarmRegistry Implementation"
verify_one "$SWARM_REGISTRY_PROXY" "src/FlatProxy.sol:ERC1967Proxy" "SwarmRegistry Proxy"
verify_one "$BOND_TREASURY_PAYMASTER" "src/FlatBTP.sol:BondTreasuryPaymaster" "BondTreasuryPaymaster"

# Clean up flat files from src/
rm -f src/FlatSP.sol src/FlatFI.sol src/FlatSR.sol src/FlatProxy.sol src/FlatBTP.sol
rm -rf "$FLAT_DIR"

if [ "$VERIFY_FAILED" -gt 0 ]; then
log_warning "$VERIFY_FAILED contract(s) failed source verification (deployment itself succeeded)"
python3 "$SCRIPT_DIR/verify_zksync_contracts.py" \
--broadcast "$BROADCAST_JSON" \
--verifier-url "$VERIFIER_URL" \
--compiler-version "0.8.26" \
--zksolc-version "v1.5.15" \
--project-root "$PROJECT_ROOT"

local exit_code=$?
if [ "$exit_code" -eq 0 ]; then
log_success "All contracts source-code verified on block explorer!"
else
log_success "All 7 contracts source-code verified on block explorer!"
log_warning "Some contracts failed source verification (deployment itself succeeded)"
log_info "Retry manually: python3 ops/verify_zksync_contracts.py --broadcast $BROADCAST_JSON --verifier-url $VERIFIER_URL"
fi
}

Expand Down Expand Up @@ -677,7 +609,7 @@ print_summary() {
echo " Explorer: $EXPLORER_URL/address/$BOND_TREASURY_PAYMASTER"
echo ""
echo "Configuration:"
echo " Owner: ${NODL_ADMIN:-deployer}"
echo " Owner: $L2_ADMIN"
echo " Withdrawer: ${PAYMASTER_WITHDRAWER:-deployer}"
echo " Fleet Operator: $FLEET_OPERATOR"
echo " Bond Token: $BOND_TOKEN"
Expand Down
Loading
Loading