A protocol deployed a time-locked withdrawal contract on Arbitrum. The lock was set using block.number, and the developers assumed the same roughly 12-second block cadence as Ethereum mainnet. On Arbitrum, block numbers advance with every transaction — meaning hundreds or thousands of "blocks" can pass in the time a single Ethereum block takes. The withdrawal window opened far sooner than intended. No funds were lost, but the team only caught it during a post-deployment audit.
That is the L2 security problem in miniature: code that is correct on L1 behaves differently on L2 because the execution environment — while EVM-compatible — is not EVM-equivalent in ways that matter for security.
Why L2 Security Differs from L1
Most Solidity developers know the L1 threat model: reentrancy, flash loans, MEV, oracle manipulation. L2s share those risks and add a distinct set on top.
Sequencer centralization. Both Optimism and Arbitrum rely on a single sequencer controlled by the protocol operator (OP Labs and Offchain Labs, respectively). The sequencer orders transactions before they reach L1. This creates front-running and censorship risk that does not exist on a decentralized L1.
EVM equivalence vs. EVM compatibility. Optimism's Bedrock release is close to EVM-equivalent — most opcodes behave identically. Arbitrum uses its own virtual machine (AVM/Nitro) that is EVM-compatible but not identical. zkSync Era uses a zkEVM that diverges more significantly: different precompiles, different handling of tx.origin, and differences in how account abstraction interacts with transaction context.
Different opcodes and precompiles. block.number, block.timestamp, block.basefee, and tx.origin all behave differently across chains. Contracts that port L1 assumptions about these values without review carry those mismatches into production.
Finality is not immediate. Optimistic rollups have a 7-day challenge period before L2 state is final on L1. zkSync uses ZK proofs with faster finality, but still not instant. Protocols treating an L2 transaction confirmation as final settlement are operating on a misunderstanding.
Vulnerability 1: block.number and block.timestamp Are Not What You Expect
On Ethereum mainnet, block.number increments once per block, roughly every 12 seconds. On Arbitrum and Optimism, the behavior is different enough to break contracts that rely on these values for timing.
Arbitrum: block.number does not return the Arbitrum block number. It returns the most recently observed L1 block number — an approximation that advances at L1 cadence, not at transaction cadence. Arbitrum's actual throughput is much higher than one block per 12 seconds. If you want the Arbitrum-native block count, you need ArbSys(address(100)).arbBlockNumber().
Optimism: block.number returns the L2 block number, which advances with every L2 transaction, not with L1 blocks. On Optimism mainnet, blocks are produced as quickly as 2 seconds apart.
Consider a time-locked contract ported from L1:
// VULNERABLE: assumes ~12s/block like mainnet
contract TimeLock {
uint256 public unlockBlock;
constructor(uint256 blocksToWait) {
// On Arbitrum, block.number is an L1 block estimate.
// On Optimism, blocks come every ~2s, not ~12s.
// Either way, the intended lock duration is wrong.
unlockBlock = block.number + blocksToWait;
}
function withdraw() external {
require(block.number >= unlockBlock, "Still locked");
// transfer funds
}
}
On Optimism with 2-second blocks, passing blocksToWait = 7200 (intended as ~24 hours at 12s/block) actually unlocks in ~4 hours. On Arbitrum, the same value might behave closer to L1 expectations — but relying on that without verification is itself a bug.
// SAFER: use block.timestamp for duration-based locks
contract TimeLock {
uint256 public unlockTimestamp;
constructor(uint256 secondsToWait) {
unlockTimestamp = block.timestamp + secondsToWait;
}
function withdraw() external {
require(block.timestamp >= unlockTimestamp, "Still locked");
// transfer funds
}
}
block.timestamp on Optimism (Bedrock) maps to the L2 block timestamp, which advances monotonically and tracks wall-clock time reasonably well. It is a better basis for duration-based logic than block.number on any L2. For Arbitrum specifically, if you need L1-synchronized timing, query the L1 block timestamp via the precompile rather than inferring from block.number.
Vulnerability 2: Sequencer Centralization Risk
Both Optimism and Arbitrum run a single centralized sequencer. Protocols that assume the absence of MEV — because they associate MEV with decentralized mempools — are wrong about how L2 sequencing works.
The sequencer sees all pending transactions before they are ordered. It can front-run user transactions, delay transactions from specific addresses (soft censorship), or reorder within a batch to capture arbitrage.
The Arbitrum and Optimism sequencers are operated by entities committed not to exploit users. That commitment is social and legal, not technical. A contract that depends on "MEV is impossible here" as a security assumption is depending on operator goodwill.
Sequencer downtime is also real. Both networks have experienced outages. A time-sensitive protocol — auction settlement, liquidation, option expiry — that requires a transaction within a specific window can fail if the sequencer is offline. On L1, you pay a higher gas price to get included. On an L2 with a single sequencer, there is no alternative path.
Mitigation: design time-sensitive operations with sequencer downtime tolerance. Extend windows, use forgiving deadlines, and document the sequencer dependency in your threat model.
Vulnerability 3: Cross-Domain Message Replay and Address Aliasing
When an L1 contract sends a message to L2 via Optimism's CrossDomainMessenger, the L1 contract's address is not directly usable on L2 as-is. Optimism applies address aliasing: the L2 representation of the L1 sender is the original L1 address plus a fixed offset (0x1111000000000000000000000000000000001111).
This exists to prevent address collisions — an L1 contract at address 0xABCD... and an L2 EOA at the same address are different entities. Without aliasing, an L2 EOA could impersonate any L1 contract by sharing its address.
The vulnerability: contracts that accept cross-domain messages and check msg.sender against an expected L1 address fail because the aliased address does not match.
// VULNERABLE: raw address comparison fails for L1->L2 messages
contract L2Receiver {
address public trustedL1Contract;
function receiveMessage(bytes calldata data) external {
// msg.sender here is the aliased address, not trustedL1Contract directly
require(msg.sender == trustedL1Contract, "Not authorized");
// process message
}
}
The correct approach on Optimism is to accept messages through the CrossDomainMessenger and use its xDomainMessageSender() function, which returns the original L1 sender before aliasing. Alternatively, apply the alias manually using AddressAliasHelper:
import { AddressAliasHelper } from "@eth-optimism/contracts/libraries/AddressAliasHelper.sol";
contract L2Receiver {
address public trustedL1Contract;
function receiveMessage(bytes calldata data) external {
// Apply alias to the trusted L1 address before comparing
address expectedSender = AddressAliasHelper.applyL1ToL2Alias(trustedL1Contract);
require(msg.sender == expectedSender, "Not authorized");
// process message
}
}
Without this, a legitimate L1→L2 governance message gets rejected, or worse, an attacker who understands the aliasing can craft a message that passes a naive check.
Vulnerability 4: tx.origin Behavior on zkSync
On Ethereum and most EVM chains, tx.origin is the EOA that signed the transaction. The only exception is account abstraction wallets, which do not change tx.origin on L1 today because native AA does not exist there.
On zkSync Era, native account abstraction is part of the protocol. Every transaction goes through an account contract, and paymasters can sponsor transactions. In this context, tx.origin for a transaction sent via a paymaster is the paymaster address, not the user's address.
This breaks any contract logic that uses tx.origin for access control or identity checks:
// VULNERABLE on zkSync: tx.origin may be a paymaster, not the user
contract Whitelist {
mapping(address => bool) public allowed;
function action() external {
require(allowed[tx.origin], "Not whitelisted");
// This check passes for the paymaster, not the user
}
}
The correct pattern is to use msg.sender for identifying the direct caller and avoid tx.origin for anything security-relevant — a best practice on L1 as well, but mandatory on zkSync where tx.origin is meaningfully different.
Vulnerability 5: Gas Price Assumptions and block.basefee
On Ethereum L1, block.basefee reflects the cost of computation within that block. On L2, computation is cheap. The dominant cost for most L2 transactions is the calldata posted to L1 (or blobs, after EIP-4844). That cost is not reflected in block.basefee on the L2 itself.
A contract that uses block.basefee to gate operations — for instance, refusing to execute when gas is "too expensive" to protect users from bad conditions — is measuring the wrong thing on L2:
// VULNERABLE: block.basefee on L2 does not reflect L1 data costs
contract AutoRebalancer {
uint256 public constant MAX_BASEFEE = 50 gwei;
function rebalance() external {
require(block.basefee <= MAX_BASEFEE, "Gas too expensive");
// execute rebalance
}
}
On Optimism and Arbitrum, block.basefee can be low even when the actual total transaction cost is high due to L1 calldata fees. The circuit breaker based on MAX_BASEFEE fires at the wrong threshold or never fires at all.
Optimism exposes an L1Block precompile (address 0x4200000000000000000000000000000000000015) that provides the current L1 base fee and other L1 context. Arbitrum exposes similar data through its precompile system. If your contract needs to gate on actual transaction cost, read from these precompiles rather than assuming block.basefee tells the full story.
Vulnerability 6: Finality Assumptions in Optimistic Rollups
Optimistic rollups — Optimism and Arbitrum — post transaction batches to L1 and allow a 7-day window for fraud proofs. Only after that window closes without a successful challenge is the state considered final on L1.
For most user-facing interactions, this does not matter. Withdrawing from L2 to L1 takes 7 days through the canonical bridge — users know this. The problem appears in protocols that treat an L2 transaction confirmation as equivalent to L1 finality for security purposes.
Bridge protocols are the clearest example. A protocol that watches for deposits on the L2 and releases assets on L1 immediately, without waiting for the L2 state to be finalized on L1, is trusting uncommitted state. If that L2 state is later successfully challenged and rolled back, the L1 release has no legitimate backing.
User deposits on L2 → Protocol sees L2 confirmation
Protocol releases on L1 → (7-day window still open)
L2 state is challenged → L2 deposit rolls back
→ L1 release has no legitimate origin
The mitigation is to either enforce the full 7-day waiting period for withdrawals (as the canonical bridges do), use a liquidity provider model with independent risk capital (as fast-bridge protocols like Across do), or build fraud-proof awareness into the protocol's trust model explicitly.
Vulnerable vs. Secure Code Side-by-Side
block.timestamp for a time lock:
// VULNERABLE: block.number timing is chain-specific
function startVesting(uint256 duration) external {
vestingEnd[msg.sender] = block.number + (duration / 12);
// Assumes 12s/block — wrong on Optimism (~2s), unreliable on Arbitrum
}
// SECURE: use seconds-based timestamp
function startVesting(uint256 durationInSeconds) external {
vestingEnd[msg.sender] = block.timestamp + durationInSeconds;
// Consistent across chains
}
Cross-domain sender check on Optimism:
// VULNERABLE: direct address comparison misses aliasing
function onlyFromL1Governor(bytes calldata data) external {
require(msg.sender == l1Governor, "Unauthorized");
_executeGovernance(data);
}
// SECURE: use CrossDomainMessenger and xDomainMessageSender
function onlyFromL1Governor(bytes calldata data) external {
require(msg.sender == address(crossDomainMessenger), "Not messenger");
require(
crossDomainMessenger.xDomainMessageSender() == l1Governor,
"Not L1 governor"
);
_executeGovernance(data);
}
What ContractScan Detects
L2-specific vulnerabilities present a challenge for static analysis tools. The contract code itself often looks correct — the bug is in the assumption about the runtime environment.
| Vulnerability | Slither | Mythril | Semgrep | ContractScan AI |
|---|---|---|---|---|
block.number used for timing |
⚠️ flag only | ⚠️ flag only | ⚠️ flag only | ✅ chain-aware analysis |
| Sequencer downtime in time-sensitive ops | ❌ | ❌ | ❌ | ✅ |
| Missing address aliasing for L1→L2 | ❌ | ❌ | ❌ | ✅ |
tx.origin AA misuse on zkSync |
❌ | ❌ | ❌ | ✅ |
block.basefee not reflecting L1 cost |
❌ | ❌ | ❌ | ✅ |
| Premature finality assumptions | ❌ | ❌ | ❌ | ✅ |
Pattern-matching tools can flag block.number usage, but cannot determine whether that usage is problematic given the target deployment chain, the intended timing semantics, or the protocol's withdrawal model. Reasoning about those things requires understanding the contract's purpose and the L2's execution environment simultaneously — which is where AI-based analysis provides coverage that static tools cannot.
Porting a contract from L1 to L2 is not a copy-paste operation. The EVM compatibility that makes rollups attractive creates a false sense of equivalence. The six vulnerability classes above have all appeared in production contracts. Block timing, sequencer trust, message aliasing, tx.origin semantics, gas cost models, and finality assumptions all require explicit review when the deployment target changes.
Scan your L2 contract at ContractScan — the AI engine is chain-aware and flags L2-specific patterns that static analyzers miss.
Related: Cross-Chain Bridge Security: Why Bridges Are the Biggest Target in 2026
Related: Front-Running and MEV for Solidity Developers
Important Notes
This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.