Layer 2 is finally here (and it works)

September 15, 2025 / 9 min read / - views​

TL;DR

  • Ethereum L2s matured in 2024–2025. Fees dropped sharply after EIP‑4844, tooling is excellent, and main stacks (Arbitrum Nitro/Orbit, OP Stack, ZK Stack, Starknet Cairo) are stable for production.
  • You can deploy to Arbitrum with the exact same Solidity you use on L1. It’s still EVM, just cheaper and faster.
  • Optimistic vs ZK rollups isn’t theology. Optimistic gives instant UX and a 7‑day exit window, ZK gives provable finality but more complex proving infra.
  • If you only read one section, read the Step‑by‑step deploy to Arbitrum and the L2 comparison table.

Why this post

I started pushing more of my side projects off L1 when blobs landed and my wallet stopped crying. Between mentoring teams at hackathons and maintaining a slightly feral homelab, I’ve deployed the same set of contracts across Arbitrum, Base, OP Mainnet, zkSync Era, Starknet and Scroll to gauge real DX. This post is the distilled version: practical steps, a plain‑English mental model of rollups, and a map of the L2 landscape as it stands today.

Audience: engineers who’ve shipped at least a couple Solidity contracts, comfy with Node and CLI. If you’re brand new to smart contracts, you can still follow along, but I won’t re‑explain uint256.


Part 1 — Deploying a contract to Arbitrum (mainnet or Sepolia)

Prerequisites

  • Node.js 18+ and pnpm/npm
  • A wallet private key (funded on Arbitrum One for mainnet, or Arbitrum Sepolia for testnet)
  • An RPC endpoint. Public RPCs work, but for serious work grab a provider key.
  • Hardhat or Foundry. I show both.

Tip: public Arbitrum RPCs are HTTP only. If your app expects WebSockets, use a provider that offers them or switch to polling.

Option A — Hardhat quick deploy

  1. Scaffold
mkdir l2-hello && cd l2-hello
npm init -y
npm i -D hardhat @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-ethers ethers dotenv
npx hardhat init --typescript
  1. Add a simple contract contracts/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
contract Counter {
    uint256 public value;
    event Bump(uint256 newValue);
 
    function inc(uint256 by) external {
        value += by;
        emit Bump(value);
    }
}
  1. Configure networks hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
dotenv.config();
 
const config: HardhatUserConfig = {
  solidity: "0.8.24",
  networks: {
    // Arbitrum One (mainnet)
    arbitrum: {
      url: process.env.ARBITRUM_RPC || "https://arb1.arbitrum.io/rpc",
      chainId: 42161,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
    },
    // Arbitrum Sepolia (testnet)
    arbitrumSepolia: {
      url: process.env.ARBITRUM_SEPOLIA_RPC || "https://sepolia-rollup.arbitrum.io/rpc",
      chainId: 421614,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
    }
  },
  etherscan: {
    // Arbiscan supports verification via Etherscan API keys
    apiKey: process.env.ARBISCAN_API_KEY || process.env.ETHERSCAN_API_KEY
  }
};
export default config;
  1. Deploy script scripts/deploy.ts
import { ethers } from "hardhat";
 
async function main() {
  const Counter = await ethers.getContractFactory("Counter");
  const counter = await Counter.deploy();
  await counter.waitForDeployment();
  console.log("Counter deployed to:", await counter.getAddress());
}
 
main().catch((e) => {
  console.error(e);
  process.exit(1);
});
  1. Run it
# Testnet first
ARBITRUM_SEPOLIA_RPC=<your_rpc> PRIVATE_KEY=<0x...> npx hardhat run --network arbitrumSepolia scripts/deploy.ts
 
# Then mainnet when ready
ARBITRUM_RPC=<your_rpc> PRIVATE_KEY=<0x...> npx hardhat run --network arbitrum scripts/deploy.ts
  1. Verify on Arbiscan
ETHERSCAN_API_KEY=<your_key> npx hardhat verify --network arbitrumSepolia <DEPLOYED_ADDRESS>
# or on mainnet
ETHERSCAN_API_KEY=<your_key> npx hardhat verify --network arbitrum <DEPLOYED_ADDRESS>

Gotcha: If verification fails, re‑compile with the exact same compiler version and settings used at deploy, then try again. For multi‑file projects ensure the full metadata is available. For Stylus contracts there’s a dedicated flow.


Option B — Foundry deploy

  1. Install & init
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init l2-counter && cd l2-counter
  1. Contract src/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
contract Counter {
    uint256 public value;
    event Bump(uint256 newValue);
    function inc(uint256 by) external {
        value += by;
        emit Bump(value);
    }
}
  1. Broadcast script script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {Counter} from "src/Counter.sol";
 
contract Deploy is Script {
    function run() external {
        uint256 pk = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(pk);
        Counter c = new Counter();
        vm.stopBroadcast();
        console.log("Counter deployed:", address(c));
    }
}
  1. Deploy
# Testnet
forge script script/Deploy.s.sol \
  --rpc-url $ARBITRUM_SEPOLIA_RPC \
  --broadcast --verify \
  --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
 
# Mainnet
forge script script/Deploy.s.sol \
  --rpc-url $ARBITRUM_RPC \
  --broadcast --verify \
  --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY

Tip: On public RPCs you may hit rate limits. For CI/CD I use provider endpoints with retries + --with-gas-price <gwei> if base fee spikes.


Test ETH and bridging

  • Arbitrum Sepolia ETH: grab from any reputable faucet, or bridge from Ethereum Sepolia via the official bridge.
  • Withdrawals from Arbitrum One or Nova to L1 have a roughly one‑week challenge window. Use third‑party bridges if you need instant liquidity and can tolerate extra trust/cost.

Part 2 — Rollups in plain English

The model

A rollup splits work in two:

  • Execution happens on the L2. You bundle lots of user transactions cheaply.

  • Data availability (DA) and dispute/validity happen on L1. You post the transaction data so anyone can reconstruct state. Then either:

    • Optimistic: assume it’s valid unless someone proves otherwise within a window (fraud/fault proofs).
    • ZK: provide a succinct proof that the batch is valid; L1 verifies the math.

EIP‑4844 added blob space with its own fee market so L2s can post data way cheaper than calldata. That’s why your swaps feel nearly free these days.

Finality and exits

  • On optimistic L2s, you usually wait about a week to finalize withdrawals to L1. Apps often use liquidity bridges to make this instant at the UX layer.
  • On ZK L2s, once the validity proof is verified on L1, the state is final. Prover latency can vary but keeps improving.

Sequencers and decentralisation

  • Today most L2s run a single active sequencer per chain for UX and MEV control. That’s changing. Roadmaps across stacks include permissionless validation and multi‑operator sequencing. You should architect apps to be neutral to who orders your txs.

Part 3 — Arbitrum in 2025: what you actually get

  • Nitro execution with full EVM equivalence. Your Solidity just works.
  • Stylus lets you write contracts in Rust, C and C++ compiled to WASM, interoperable with EVM. Great for crypto‑heavy code or when gas micro‑optimisations matter.
  • AnyTrust (Nova) for ultra‑low fees via a DA committee; Rollup mode (One) for pure L1 DA.
  • BoLD upgrades the dispute game to permissionless validation across Arbitrum chains.
  • Orbit to launch your own L2/L3 with pick‑your‑own gas token, DA, and throughput.

If you’re building general‑purpose dApps, deploy on Arbitrum One first. If you’re doing high‑volume socials/games with thin margins, evaluate Nova. If you’re an enterprise team and want custom levers, look at Orbit.


Part 4 — L2 comparison in a page

Snapshot as of September 2025. Things move. Always check docs before you ship.

ChainTypeEVM levelProofs/ValidationExit to L1Notables
Arbitrum OneOptimistic (Rollup)EVM‑equivalentInteractive fault proofs; BoLD enables permissionless validation~7 daysStylus (Rust/C/C++ to WASM), mature DeFi/liquidity, Orbit chains
Arbitrum NovaOptimistic (AnyTrust)EVM‑equivalentFault proofs; DA via committee~7 daysUltra‑cheap txs, great for gaming/social
OP MainnetOptimistic (OP Stack)EVM‑equivalentPermissionless Fault Proofs live~7 daysSuperchain vision; easy app portability across OP chains
BaseOptimistic (OP Stack)EVM‑equivalentFault proof infra via OP Stack; rapidly decentralising ops~7 daysBig retail funnel, excellent infra, OP‑compatible tooling
zkSync EraZK rollupEVM‑compatible (Era VM)Validity proofs; native account abstractionMinutes to hours (proof)ZK Stack chains with native interop
StarknetZK rollupCairo 1 (not EVM)Validity proofsMinutes to hours (proof)Powerful proving, fast pre‑confirms, decentralising sequencer
Scroll / LineaZK rollupsEVM‑equivalentValidity proofsMinutes to hoursStrong EVM parity; good for porting L1 code

How I choose:

  • Need drop‑in EVM, big liquidity, and sane fees? Arbitrum One or Base.
  • Need instant UX across multiple OP chains? OP Stack gets you there with shared standards.
  • Need protocol‑level AA and ZK proofs? zkSync Era.
  • Need Cairo performance primitives? Starknet.

Part 5 — Production checklist (hard‑won notes)

  • RPCs: don’t ship on free RPCs. Rate limits will bite at the worst time.
  • Gas configs: let providers suggest fees but cap the maximum so a blob price spike doesn’t nuke your budget during airdrops.
  • Bridging UX: if you support L1 exits on optimistic rollups, integrate a reputable fast bridge or at least communicate the delay clearly in UI copy.
  • Monitoring: track both L2 tx status and L1 posting/proofs. Users care about “done‑done.”
  • Verifiers: wire contract verification into CI so your addresses and commit SHAs are recorded and public minutes after deploy.
  • Key management: same as L1. Use deployer roles and delayed timelocks. Don’t YOLO admin keys because it’s “just L2.”
  • Cross‑chain: if you’re on multiple L2s, design around message passing latencies and reorg boundaries. Don’t assume synchronous miracles.

A — Minimal Hardhat + Foundry repo layout

.
├── contracts/Counter.sol
├── scripts/deploy.ts
├── hardhat.config.ts
├── .env (PRIVATE_KEY, ARBITRUM_RPC, ARBITRUM_SEPOLIA_RPC, ETHERSCAN_API_KEY)
└── foundry (optional)
    ├── src/Counter.sol
    └── script/Deploy.s.sol

B — Common errors and fixes

  • insufficient funds for gas * price + value on testnet: you’re on L1 Sepolia not Arbitrum Sepolia, or vice versa. Check chain ID 421614.
  • Verification fails: wrong compiler version or missing constructor args. Rebuild with the exact compiler and settings.
  • “replacement fee too low”: you tried to speed up with a small tip. On L2, either wait or bump more aggressively.
  • Event not showing in subgraph: confirm your indexer supports that L2 and the correct chain ID.
  • WebSocket connect errors: public Arbitrum RPCs are HTTP only; switch provider or poll.