DEV Community

ohmygod
ohmygod

Posted on

Auditing for Ethereum's Parallel Execution Era: New Attack Vectors and a Foundry Toolkit for Glamsterdam

Ethereum's Glamsterdam upgrade — expected H1 2026 — will triple the gas limit to ~180M and introduce parallel transaction execution. For DeFi protocols, this isn't just a performance upgrade. It's a security paradigm shift that breaks assumptions auditors have relied on for years.

This article maps the new attack surface and provides a Foundry-based testing toolkit to catch parallel-execution vulnerabilities before they hit mainnet.

What Changes With Parallel Execution

Sequential Ethereum gave us one critical guarantee: transactions within a block execute in strict order. DeFi protocols have leaned on this for years — often without realizing it.

Glamsterdam's parallel execution model (inspired by Sei and Monad) uses optimistic concurrent execution: transactions run simultaneously in separate threads, then a conflict detection pass validates that no two touched the same state. Conflicting transactions get re-executed sequentially.

Sounds safe. It isn't — at least not for protocols that make implicit assumptions about execution ordering.

The Three New Attack Classes

1. State Contention Races

When two transactions touch the same storage slot in the same block, the parallel executor must resolve the conflict. The danger isn't in the resolver — it's in how your protocol behaves when transactions that used to be sequential are now concurrent.

Consider a lending protocol's liquidate() function:

function liquidate(address borrower) external {
    uint256 debt = userDebt[borrower];         // Read
    uint256 collateral = userCollateral[borrower]; // Read
    require(collateral * price / debt < threshold, "healthy");

    // Transfer collateral to liquidator
    userCollateral[borrower] = 0;              // Write
    userDebt[borrower] = 0;                    // Write
    IERC20(collateralToken).transfer(msg.sender, collateral);
}
Enter fullscreen mode Exit fullscreen mode

In sequential execution, only one liquidator wins. In parallel execution, two liquidators reading the same borrower's state simultaneously both see the full collateral. The conflict detector catches the write-write conflict and re-executes one — but the first liquidator already received the tokens via the external transfer() call. The re-execution of the second creates a state inconsistency if the protocol doesn't handle the zero-collateral edge case.

2. Cross-Contract Ordering Assumptions

Many DeFi composability patterns assume that operations within a block happen in a predictable sequence. Arbitrage bots, for example, rely on:

  1. Swap on DEX A (changes price)
  2. Swap on DEX B (captures arbitrage)

With parallel execution, if these target different state, they run concurrently — meaning the price impact of swap A isn't visible when swap B executes. This breaks MEV extraction patterns, but more critically, it can break protocol-level assumptions about price consistency within a block.

// Oracle update pattern that assumes sequential execution
function updatePrice() external {
    uint256 dexAPrice = IDex(dexA).getPrice(token);
    uint256 dexBPrice = IDex(dexB).getPrice(token);
    // Assumes both prices reflect same block state
    require(abs(dexAPrice - dexBPrice) < MAX_DEVIATION, "price mismatch");
    price = (dexAPrice + dexBPrice) / 2;
}
Enter fullscreen mode Exit fullscreen mode

If dexA and dexB were updated by parallel transactions, these prices may reflect different execution states within the same block.

3. Reentrancy Guard Bypass via Parallel Paths

Traditional reentrancy guards use a storage mutex:

uint256 private _status;
modifier nonReentrant() {
    require(_status != 2, "reentrant");
    _status = 2;
    _;
    _status = 1;
}
Enter fullscreen mode Exit fullscreen mode

In sequential execution, this is bulletproof. But consider: what if an attacker submits two separate transactions in the same block that both call the same nonReentrant function? In parallel execution, both read _status == 1, both set it to 2, and both proceed. The conflict detector catches the write-write on _status and re-executes one — but the first transaction's side effects (external calls, token transfers) have already occurred.

This isn't traditional reentrancy. It's parallel entry — and existing reentrancy guards don't protect against it.

EIP-7928: Block Access Lists

Ethereum's proposed defense is EIP-7928, which requires transactions to declare their read/write state footprints upfront. Transactions with overlapping state get serialized; non-overlapping transactions run in parallel.

For auditors, this means a new question: does your protocol's access list accurately describe its state footprint?

A protocol that uses delegatecall to a proxy, for instance, accesses state in the proxy's storage — but the access list might only declare the caller's slots. Dynamic storage patterns (mappings computed at runtime, assembly-level sload/sstore) are particularly dangerous because they're hard to statically predict.

A Foundry Toolkit for Parallel Execution Auditing

Here's a practical framework for testing parallel-execution vulnerabilities:

Test 1: Concurrent Liquidation Race

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

import "forge-std/Test.sol";

contract ParallelLiquidationTest is Test {
    LendingProtocol protocol;

    function setUp() public {
        protocol = new LendingProtocol();
        // Setup borrower with liquidatable position
        protocol.deposit(borrower, 100e18);
        protocol.borrow(borrower, 90e18);
        // Price drop makes position liquidatable
        protocol.setPrice(0.5e18);
    }

    function test_concurrentLiquidation() public {
        // Simulate parallel execution: snapshot state, execute both
        uint256 snapshot = vm.snapshot();

        // Liquidator A
        vm.prank(liquidatorA);
        protocol.liquidate(borrower);
        uint256 balanceA = token.balanceOf(liquidatorA);

        // Revert to pre-liquidation state (simulating parallel read)
        vm.revertTo(snapshot);

        // Liquidator B sees same state
        vm.prank(liquidatorB);
        protocol.liquidate(borrower);
        uint256 balanceB = token.balanceOf(liquidatorB);

        // In sequential execution, only one succeeds.
        // If both show gains, the protocol has a parallel-execution vulnerability.
        bool bothProfited = balanceA > 0 && balanceB > 0;
        assertFalse(bothProfited, "VULN: Double liquidation possible in parallel");
    }
}
Enter fullscreen mode Exit fullscreen mode

Test 2: Reentrancy Guard Parallel Entry

contract ParallelEntryTest is Test {
    Vault vault;

    function test_parallelNonReentrantBypass() public {
        // Two separate callers invoke the same nonReentrant function
        // Simulating parallel execution where both read _status == 1

        uint256 snap = vm.snapshot();

        vm.prank(attackerA);
        vault.withdraw(50e18);
        uint256 vaultBalanceAfterA = token.balanceOf(address(vault));

        vm.revertTo(snap);

        vm.prank(attackerB);
        vault.withdraw(50e18);
        uint256 vaultBalanceAfterB = token.balanceOf(address(vault));

        // If vault had 80e18, sequential execution allows only one 50e18 withdrawal.
        // Parallel execution might allow both if conflict resolution fails.
        // Check: would combined withdrawals exceed vault balance?
        uint256 totalWithdrawn = (100e18 - vaultBalanceAfterA) + (100e18 - vaultBalanceAfterB);
        assertLe(totalWithdrawn, 100e18, "VULN: Parallel entry drained more than available");
    }
}
Enter fullscreen mode Exit fullscreen mode

Test 3: Cross-Contract State Consistency

contract CrossContractConsistencyTest is Test {
    DEX dexA;
    DEX dexB;
    Oracle oracle;

    function test_parallelPriceInconsistency() public {
        // Simulate: large swap on dexA runs in parallel with oracle update
        uint256 snap = vm.snapshot();

        // Transaction 1: Whale swap on dexA
        vm.prank(whale);
        dexA.swap(token, 1_000_000e18);
        uint256 priceAfterSwap = dexA.getPrice(token);

        vm.revertTo(snap);

        // Transaction 2: Oracle reads dexA price (parallel — sees pre-swap price)
        uint256 oracleReading = dexA.getPrice(token);

        // The oracle would use stale price in parallel execution
        uint256 deviation = priceAfterSwap > oracleReading 
            ? priceAfterSwap - oracleReading 
            : oracleReading - priceAfterSwap;

        // Log the deviation for manual review
        emit log_named_uint("Price deviation in parallel scenario", deviation);

        // Flag if deviation exceeds acceptable threshold
        assertLe(
            deviation * 10000 / oracleReading, 
            500, // 5% threshold
            "VULN: Parallel execution creates exploitable price inconsistency"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The Audit Checklist for Glamsterdam Readiness

Before Glamsterdam goes live, every DeFi protocol audit should include:

1. State Dependency Mapping

  • [ ] Identify all storage slots modified by each external function
  • [ ] Map cross-function state dependencies (does withdraw() depend on state set by deposit()?)
  • [ ] Flag functions that read-then-write to the same slot without atomic guarantees

2. Implicit Ordering Assumptions

  • [ ] Search for patterns that assume "this happened earlier in the block"
  • [ ] Check oracle update mechanisms for within-block consistency assumptions
  • [ ] Verify that MEV-protection mechanisms don't rely on sequential ordering

3. Reentrancy Guard Scope

  • [ ] Evaluate if nonReentrant guards protect against parallel entry (they don't — by design)
  • [ ] Consider per-user mutexes: mapping(address => uint256) private _userStatus
  • [ ] Assess whether EIP-7928 access lists correctly cover all accessed state

4. Access List Completeness

  • [ ] Verify that delegatecall targets are included in access list declarations
  • [ ] Check for dynamic storage access patterns (computed mapping keys, assembly sload)
  • [ ] Test with intentionally incomplete access lists to verify conflict detection catches violations

5. External Call Atomicity

  • [ ] Ensure that state changes and external calls are either both in the access list or both serialized
  • [ ] Check for "optimistic" patterns where external call results are assumed consistent with local state

Slither Custom Detector: Parallel-Unsafe State Access

For static analysis, here's a Slither detector concept that flags parallel-unsafe patterns:

from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.declarations import Function

class ParallelUnsafeStateAccess(AbstractDetector):
    ARGUMENT = "parallel-unsafe"
    HELP = "Detects state access patterns unsafe under parallel execution"
    IMPACT = DetectorClassification.HIGH
    CONFIDENCE = DetectorClassification.MEDIUM

    WIKI = "https://github.com/example/parallel-safety-detectors"
    WIKI_TITLE = "Parallel-Unsafe State Access"
    WIKI_DESCRIPTION = "Functions that read and write shared state with external calls between"

    def _detect(self):
        results = []
        for contract in self.compilation_unit.contracts_derived:
            for function in contract.functions:
                if function.is_constructor or function.view or function.pure:
                    continue

                state_reads = set()
                state_writes = set()
                has_external_call_between = False

                for node in function.nodes:
                    reads = node.state_variables_read
                    writes = node.state_variables_written
                    externals = node.external_calls_as_expressions

                    if reads:
                        state_reads.update(reads)
                    if externals and state_reads:
                        has_external_call_between = True
                    if writes:
                        state_writes.update(writes)

                # Flag: reads state, makes external call, then writes same state
                overlap = state_reads & state_writes
                if overlap and has_external_call_between:
                    info = [
                        f"Parallel-unsafe pattern in {function.canonical_name}: ",
                        f"reads and writes {[str(v) for v in overlap]} ",
                        "with external calls between — vulnerable to parallel execution races\n"
                    ]
                    results.append(self.generate_result(info))

        return results
Enter fullscreen mode Exit fullscreen mode

The Bigger Picture

Parallel execution is coming whether protocols are ready or not. Monad and Sei have already demonstrated the model works. Glamsterdam brings it to Ethereum mainnet, where $100B+ in DeFi TVL sits in contracts designed for sequential execution.

The audit industry needs to evolve its tooling now. The Foundry snapshot-revert pattern shown above is a crude but effective simulation of parallel execution. Dedicated parallel execution fuzzers — think Echidna but with concurrent transaction scheduling — should be a priority for every audit firm's R&D team.

For protocol developers: don't assume conflict detection will save you. EIP-7928 access lists are only as good as their declarations. And the gap between "conflict detected, re-executed sequentially" and "funds safe" is wider than you think.

Start auditing for parallelism today. Glamsterdam won't wait.


This article is part of the DeFi Security Research series. Follow for weekly deep dives into smart contract vulnerabilities, audit tooling, and security best practices across EVM and Solana ecosystems.

Top comments (0)