l2-deployment

star 8

Use when deploying contracts to L2 networks (Base, Optimism, Arbitrum). Covers L2-specific considerations, PUSH0 compatibility, gas estimation, L1 data costs, bridge integration, and deployment scripts.

ccashwell By ccashwell schedule Updated 4/10/2026

name: l2-deployment description: Use when deploying contracts to L2 networks (Base, Optimism, Arbitrum). Covers L2-specific considerations, PUSH0 compatibility, gas estimation, L1 data costs, bridge integration, and deployment scripts.

L2 Deployment Patterns

L2 Gas Economics

L2 transaction cost = L2 execution gas + L1 data posting cost

Operation L1 (Ethereum) L2 (Base/OP/Arb) Savings
ETH transfer ~$0.50 ~$0.0003 99.9%
ERC-20 transfer ~$2.00 ~$0.001 99.9%
Uniswap swap ~$8.00 ~$0.002-0.003 99.9%
NFT mint ~$5.00 ~$0.002 99.9%
Contract deploy ~$50-500 ~$0.05-0.50 99%

Post-EIP-4844 (blobs), L1 data costs dropped 10-100x for L2s.

L2-Specific Considerations

PUSH0 Opcode Compatibility

PUSH0 was introduced in the Shanghai upgrade (EVM version). Some L2s may lag behind on EVM version support:

# foundry.toml — set EVM version explicitly for L2
[profile.default]
evm_version = "cancun"     # Base, Optimism, Arbitrum all support cancun

# If targeting an L2 that doesn't support cancun:
# evm_version = "paris"    # No PUSH0

Block Time Differences

Chain Block Time Implication
Ethereum ~12s Standard
Base 2s Faster confirmations
Optimism 2s Faster confirmations
Arbitrum ~0.25s Near-instant

Avoid hardcoding time assumptions based on block numbers.

L2-Specific Addresses

// Optimism / Base predeploys (same addresses on all OP Stack chains)
address constant L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000007;
address constant L2_STANDARD_BRIDGE = 0x4200000000000000000000000000000000000010;
address constant L2_TO_L1_MESSAGE_PASSER = 0x4200000000000000000000000000000000000016;
address constant WETH = 0x4200000000000000000000000000000000000006;
address constant L1_BLOCK = 0x4200000000000000000000000000000000000015;
address constant GAS_PRICE_ORACLE = 0x420000000000000000000000000000000000000F;

// Arbitrum system addresses
address constant ARB_SYS = 0x0000000000000000000000000000000000000064;
address constant ARB_RETRYABLE_TX = 0x000000000000000000000000000000000000006E;

Foundry Multi-L2 Deployment Script

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console2} from "forge-std/Script.sol";
import {MyProtocol} from "../src/MyProtocol.sol";

contract DeployL2 is Script {
    struct L2Config {
        string rpcAlias;
        uint256 chainId;
        address weth;
        address usdc;
    }

    function getConfigs() internal pure returns (L2Config[] memory) {
        L2Config[] memory configs = new L2Config[](3);

        configs[0] = L2Config({
            rpcAlias: "base",
            chainId: 8453,
            weth: 0x4200000000000000000000000000000000000006,
            usdc: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
        });

        configs[1] = L2Config({
            rpcAlias: "optimism",
            chainId: 10,
            weth: 0x4200000000000000000000000000000000000006,
            usdc: 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85
        });

        configs[2] = L2Config({
            rpcAlias: "arbitrum",
            chainId: 42161,
            weth: 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1,
            usdc: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831
        });

        return configs;
    }

    function run() external {
        uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
        L2Config[] memory configs = getConfigs();

        for (uint256 i = 0; i < configs.length; i++) {
            L2Config memory cfg = configs[i];
            console2.log("Deploying to chain:", cfg.chainId);

            vm.createSelectFork(cfg.rpcAlias);
            vm.startBroadcast(deployerKey);

            MyProtocol protocol = new MyProtocol(cfg.weth, cfg.usdc);
            console2.log("  Deployed at:", address(protocol));

            vm.stopBroadcast();
        }
    }
}
# Deploy to all L2s
forge script script/DeployL2.s.sol --broadcast --multi --verify

L1 Data Cost Estimation

On OP Stack chains (Base, Optimism), query the gas price oracle:

interface IGasPriceOracle {
    function getL1Fee(bytes memory data) external view returns (uint256);
    function getL1GasUsed(bytes memory data) external view returns (uint256);
}

IGasPriceOracle oracle = IGasPriceOracle(0x420000000000000000000000000000000000000F);
uint256 l1Fee = oracle.getL1Fee(txData);

On Arbitrum, L1 data costs are embedded in the gas price.

Bridge Integration

OP Stack Bridge (Base / Optimism)

interface IL2StandardBridge {
    function bridgeETHTo(address to, uint32 minGasLimit, bytes calldata extraData) external payable;
    function bridgeERC20To(
        address localToken, address remoteToken,
        address to, uint256 amount, uint32 minGasLimit, bytes calldata extraData
    ) external;
}

// Bridge ETH from L2 to L1
IL2StandardBridge(0x4200000000000000000000000000000000000010).bridgeETHTo{value: amount}(
    recipient, 200_000, ""
);

Arbitrum Bridge

interface IArbSys {
    function withdrawEth(address destination) external payable returns (uint256);
    function sendTxToL1(address destination, bytes calldata data) external payable returns (uint256);
}

// Withdraw ETH from Arbitrum to L1
IArbSys(0x0000000000000000000000000000000000000064).withdrawEth{value: amount}(recipient);

Testing on Forked L2s

function setUp() public {
    vm.createSelectFork("base", 20_000_000);
}

function test_swapOnBase() public {
    address whale = 0x...; // Known USDC holder on Base
    vm.startPrank(whale);
    // Interact with real Base contracts
    vm.stopPrank();
}

L2 Deployment Checklist

  • Set correct evm_version in foundry.toml for target L2
  • Use L2-specific WETH and token addresses (not mainnet addresses)
  • Account for different block times in time-based logic
  • Test on forked L2 before deploying
  • Estimate L1 data costs for gas-sensitive operations
  • Verify contracts on the correct L2 block explorer
  • Configure bridge integration if cross-chain messaging needed
  • Test with L2-specific gas prices (much lower than L1)
  • Check L2 predeploy addresses for system contracts
  • Deploy to testnet first (Base Sepolia, OP Sepolia, Arbitrum Sepolia)
Install via CLI
npx skills add https://github.com/ccashwell/evm-cortex --skill l2-deployment
Repository Details
star Stars 8
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator