Skip to content

Latest commit

 

History

History
284 lines (209 loc) · 10.8 KB

COMPILATION.md

File metadata and controls

284 lines (209 loc) · 10.8 KB

Compilation Guide

Overview

This project uses a custom compilation setup to handle different compiler settings for different contracts. This document explains why this is necessary and how to compile contracts correctly.

The Problem

Gateway contracts have two conflicting requirements:

  1. Address Determinism: ERC1967Proxy and UpgradeablePlaceholder must maintain the same addresses across all chains. This requires consistent bytecode, which means these contracts MUST be compiled WITHOUT via_ir.

  2. Size Optimization: GatewayWallet and GatewayMinter need bytecode optimization to stay under the 24KB EIP-170 limit. This requires compiling WITH via_ir.

Why Proxy Address Determinism Matters

The Gateway contracts are deployed across multiple chains (Ethereum, Polygon, Arbitrum, Base, Optimism, etc.). For cross-chain operations, the proxy addresses must be identical on all chains. This allows:

  1. Consistent References: Off-chain systems can reference the same address across all chains
  2. Cross-Chain Messaging: Cross-chain messaging protocols can target consistent endpoints
  3. Simplified User Experience: Users see the same address everywhere
  4. Simplified Contract Verification: Auditing and verification can reference canonical addresses

CREATE2 deployment with deterministic bytecode ensures:

  • Same factory address + same salt + same bytecode = same address on all chains
  • This ONLY works if the bytecode is identical across deployments
  • Even a single byte difference in bytecode produces a completely different address
  • via_ir compilation produces different bytecode than non-via_ir compilation, breaking determinism

This is why proxy contracts (ERC1967Proxy, UpgradeablePlaceholder) MUST be compiled without via_ir, even though via_ir produces better optimized code.

Why implementations can use via_ir:

  • Each implementation has its own unique salt for CREATE2 deployment
  • Their addresses don't need to match any deterministic pattern
  • We prioritize size optimization over address consistency for implementations
  • The proxy pattern isolates the implementation address from the user-facing proxy address

The Solution

We use a custom compilation script (scripts/compile-artifacts.sh) that compiles different contracts with different settings:

Pass 1: Proxy & Placeholder (via_ir=false)

forge build \
  lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol \
  src/UpgradeablePlaceholder.sol \
  --force

Pass 2: Implementations (via_ir=true)

forge build \
  src/GatewayWallet.sol \
  src/GatewayMinter.sol \
  --via-ir \
  --force

How to Compile

✅ CORRECT: Use yarn commands

yarn build       # Recommended
yarn artifacts   # Same as above

❌ WRONG: Direct forge commands

forge build      # DON'T DO THIS!
forge clean && forge build  # ALSO DON'T DO THIS!

Why forge build Doesn't Work

Running forge build directly:

  1. Compiles all contracts with the same settings from foundry.toml (via_ir=false)
  2. Produces different bytecode than what's saved in script/compiled-contract-artifacts/
  3. Breaks address determinism for proxy contracts
  4. May cause implementation contracts to exceed size limits

Artifact Locations

Compiled artifacts are stored in two places:

1. out/ directory (Forge's default output)

  • Generated by forge build and forge test
  • Used by tests
  • WARNING: May contain incorrectly compiled contracts if you ran forge build directly

2. script/compiled-contract-artifacts/ directory (Deployment artifacts)

  • Generated by yarn artifacts
  • Used by deployment scripts (001, 002, 004)
  • Used by size verification (yarn sizes)
  • These are the source of truth for deployments

Development Workflow

Running Tests

yarn test           # Local tests only
yarn test:all       # All tests including fork tests

Tests automatically compile with via_ir=true using the [profile.test] in foundry.toml. This ensures:

  • Implementation contracts (GatewayWallet, GatewayMinter) compile successfully (they exceed size limits without via_ir)
  • Most tests can instantiate contracts using new operator
  • Contract size limits are validated during testing

Note: The test profile compiles ALL contracts with via_ir (including proxies), which differs from production's split compilation approach. This is necessary because most tests use DeployUtils to instantiate contracts directly with new, and GatewayWallet would exceed the 24KB size limit without via_ir optimization.

Tests that verify deployment addresses and bytecode (like DeployGatewayWalletTest) read from script/compiled-contract-artifacts/ to test the actual production bytecode with correct via_ir settings per contract.

Making Contract Changes

  1. Edit contract source files in src/
  2. Run tests: yarn test
  3. If deploying, compile artifacts: yarn artifacts
  4. Check sizes: yarn sizes
  5. Deploy using the deployment scripts

Before Deployment

ALWAYS run yarn artifacts before deploying:

yarn artifacts
# Now deployment artifacts are up to date
ENV=$ENV forge script script/001_DeployGatewayWallet.sol --rpc-url $RPC_URL ...

Before Upgrades

ALWAYS run yarn artifacts before upgrading contracts:

# 1. Make your contract changes in src/
# 2. Run tests to verify changes
yarn test

# 3. Compile artifacts with correct settings
yarn artifacts

# 4. Verify sizes are still under limits
yarn sizes

# 5. Verify addresses haven't changed (for proxies)
yarn verify-addresses

# 6. Now run the upgrade script
ENV=$ENV forge script script/004_UpgradeGatewayWallet.sol \
  --rpc-url $RPC_URL \
  --broadcast \
  --account deployer --account owner

Why this matters:

Upgrade scripts (004_UpgradeGatewayWallet.sol, 005_UpgradeGatewayMinter.sol) deploy new implementations by reading bytecode from script/compiled-contract-artifacts/.

If these artifacts are stale or were compiled incorrectly:

  • ❌ You'll deploy old code (if you didn't recompile after making changes)
  • ❌ You'll deploy wrong code (if you ran forge build instead of yarn artifacts)
  • ❌ The upgrade will succeed on-chain, but with unexpected bytecode
  • ❌ Your implementation may exceed the 24KB size limit if compiled without via_ir

Always verify before upgrading:

  1. ✅ Artifacts are up to date: yarn artifacts
  2. ✅ Sizes are correct: yarn sizes
  3. ✅ Tests pass: yarn test
  4. ✅ Addresses verified (if applicable): yarn verify-addresses

Full CI Pipeline

yarn ci
# Runs: format → lint → artifacts → sizes → test:all

Verification

Check Proxy Addresses

yarn verify-addresses
# or directly:
forge script script/ComputeProxyAddress.sol -vvv

Should output:

✓ SUCCESS: Wallet proxy address matches!
✓ SUCCESS: Minter proxy address matches!

This verification is also run automatically in CI after compiling artifacts.

Check Contract Sizes

yarn sizes

Should output:

✅ GatewayWallet (deployed bytecode size: 21779) is within the EIP-170 size limit (24576)
✅ GatewayMinter (deployed bytecode size: 11619) is within the EIP-170 size limit (24576)
✅ UpgradeablePlaceholder (deployed bytecode size: 2799) is within the EIP-170 size limit (24576)

Troubleshooting

"My proxy addresses don't match expected values"

  • Did you run yarn artifacts instead of forge build?
  • Check that foundry.toml has via_ir = false in the default profile
  • Verify artifacts in script/compiled-contract-artifacts/ are up to date
  • Run yarn verify-addresses to check all environments

"My implementation contract exceeds 24KB"

  • Make sure you compiled with yarn artifacts (which uses via_ir for implementations)
  • Check that the via_ir compilation step completed successfully
  • Review the compilation output for any warnings
  • Run yarn sizes to see the actual artifact sizes

"forge build --sizes shows GatewayWallet over 24KB"

This is expected behavior!

Running forge build directly compiles with via_ir=false (per foundry.toml default profile), which produces larger, non-optimized bytecode:

  • GatewayWallet without via_ir: ~24,653 bytes (over limit) ❌
  • GatewayWallet with via_ir: ~21,779 bytes (under limit) ✅

The actual deployed bytecode is compiled with via_ir and is safely under the limit.

To check actual deployed sizes:

yarn sizes

This checks the correctly compiled artifacts in script/compiled-contract-artifacts/ that will actually be deployed.

Why forge build output differs:

The foundry.toml sets via_ir = false for the default profile to support artifact compilation (where the script selectively applies via_ir with the --via-ir flag for implementations). This means direct forge build invocations compile everything without via_ir, producing larger bytecode.

For development compilation with size-optimized bytecode:

FOUNDRY_PROFILE=test forge build --sizes

This will show implementation sizes similar to production (both use via_ir), but proxy sizes will differ (test profile uses via_ir for proxies, production doesn't). For exact deployed sizes, always use yarn sizes.

"Tests are failing after I ran forge build"

  • Run yarn artifacts to regenerate correct artifacts
  • Run yarn test again
  • Tests in test/script/DeployGatewayWalletTest.t.sol and test/script/DeployGatewayMinterTest.t.sol verify deployment addresses
  • These tests will fail if artifacts don't match expected bytecode

Technical Details

Why Two Compilation Passes?

Foundry's via_ir flag is global per build command. We can't selectively enable it for some contracts within a single forge build invocation. Therefore, we:

  1. Build proxy/placeholder without via_ir
  2. Build implementations with via_ir flag
  3. Copy the correct artifacts to script/compiled-contract-artifacts/

What is via_ir?

via_ir (via intermediate representation) tells Solidity to use the Yul IR-based compilation pipeline, which:

  • Produces more optimized bytecode (smaller size)
  • Takes longer to compile
  • May produce slightly different gas characteristics

For our use case:

  • Proxy/placeholder: Don't need optimization, MUST have consistent bytecode → via_ir=false
  • Implementations: Need optimization for size → via_ir=true

Contract Size Limit (EIP-170)

Ethereum's EIP-170 limits deployed contract bytecode to 24,576 bytes (24KB). GatewayWallet comes close to this limit, which is why via_ir optimization is necessary.

Current sizes (with via_ir):

  • GatewayWallet: ~21.8 KB (2.8 KB margin)
  • GatewayMinter: ~11.6 KB (13 KB margin)

References