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);
}
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:
- Swap on DEX A (changes price)
- 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;
}
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;
}
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");
}
}
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");
}
}
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"
);
}
}
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 bydeposit()?) - [ ] 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
nonReentrantguards 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
delegatecalltargets 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
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)