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.
Gateway contracts have two conflicting requirements:
-
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. -
Size Optimization: GatewayWallet and GatewayMinter need bytecode optimization to stay under the 24KB EIP-170 limit. This requires compiling WITH
via_ir.
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:
- Consistent References: Off-chain systems can reference the same address across all chains
- Cross-Chain Messaging: Cross-chain messaging protocols can target consistent endpoints
- Simplified User Experience: Users see the same address everywhere
- 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_ircompilation 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
We use a custom compilation script (scripts/compile-artifacts.sh) that compiles different contracts with different settings:
forge build \
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol \
src/UpgradeablePlaceholder.sol \
--forceforge build \
src/GatewayWallet.sol \
src/GatewayMinter.sol \
--via-ir \
--forceyarn build # Recommended
yarn artifacts # Same as aboveforge build # DON'T DO THIS!
forge clean && forge build # ALSO DON'T DO THIS!Running forge build directly:
- Compiles all contracts with the same settings from
foundry.toml(via_ir=false) - Produces different bytecode than what's saved in
script/compiled-contract-artifacts/ - Breaks address determinism for proxy contracts
- May cause implementation contracts to exceed size limits
Compiled artifacts are stored in two places:
- Generated by
forge buildandforge test - Used by tests
- WARNING: May contain incorrectly compiled contracts if you ran
forge builddirectly
- 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
yarn test # Local tests only
yarn test:all # All tests including fork testsTests 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
newoperator - 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.
- Edit contract source files in
src/ - Run tests:
yarn test - If deploying, compile artifacts:
yarn artifacts - Check sizes:
yarn sizes - Deploy using the deployment scripts
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 ...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 ownerWhy 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 buildinstead ofyarn 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:
- ✅ Artifacts are up to date:
yarn artifacts - ✅ Sizes are correct:
yarn sizes - ✅ Tests pass:
yarn test - ✅ Addresses verified (if applicable):
yarn verify-addresses
yarn ci
# Runs: format → lint → artifacts → sizes → test:allyarn verify-addresses
# or directly:
forge script script/ComputeProxyAddress.sol -vvvShould output:
✓ SUCCESS: Wallet proxy address matches!
✓ SUCCESS: Minter proxy address matches!
This verification is also run automatically in CI after compiling artifacts.
yarn sizesShould 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)
- Did you run
yarn artifactsinstead offorge build? - Check that
foundry.tomlhasvia_ir = falsein the default profile - Verify artifacts in
script/compiled-contract-artifacts/are up to date - Run
yarn verify-addressesto check all environments
- 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 sizesto see the actual artifact sizes
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 sizesThis 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 --sizesThis 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.
- Run
yarn artifactsto regenerate correct artifacts - Run
yarn testagain - Tests in
test/script/DeployGatewayWalletTest.t.solandtest/script/DeployGatewayMinterTest.t.solverify deployment addresses - These tests will fail if artifacts don't match expected bytecode
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:
- Build proxy/placeholder without via_ir
- Build implementations with via_ir flag
- Copy the correct artifacts to
script/compiled-contract-artifacts/
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
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)