For the complete documentation index, see llms.txt. This page is also available as Markdown.

76483 bc low external timeout around non cancellation safe txmanager send can leak the proposer s nonce and stall l1 proposal submission

Submitted on May 4th 2026 at 16:28:45 UTC by @ZeroExRes for Audit Comp | Base Azul

  • Report ID: #76483

  • Report Type: Blockchain/DLT

  • Report severity: Low

  • Target: https://github.com/base/base/tree/v0.8.0-rc.28

  • Impacts:

    • A bug in the respective layer 0/1/2 network code that results in unintended smart contract behavior with no concrete funds at direct risk

Description

Brief/Intro

The proposer wraps L1 dispute game creation in a 10-minute tokio::time::timeout, but the transaction path underneath it is not safe to cancel during initial preparation. If that outer timeout fires after the TxManager reserves a nonce but before the transaction is published, the nonce is lost from the in-memory nonce manager. After that, the proposer can keep signing later transactions with higher nonces that cannot be mined because the missing nonce was never published.

Vulnerability Details

SimpleTxManager::prepare() documents the exact problem:

/// This future is **not** cancellation-safe.
/// ...
/// Callers must not wrap this future in `tokio::select!`,
/// `tokio::time::timeout`, or similar combinators that may drop it
/// mid-flight.

The proposer does exactly that during submission:

That call reaches:

Because nonce_override is None, craft_tx_with_caps() reserves a new nonce through NonceManager::next_nonce():

The problem is what happens if the outer timeout drops the future here. NonceGuard::Drop does not roll the nonce back; it only logs that the nonce was consumed. Rollback only happens through an explicit rollback() call on the signing error path.

The TxManager has an internal cleanup path for TxManagerError::SendTimeout, but it is disabled by default. tx_send_timeout defaults to Duration::ZERO, and the CLI default is "0s". So the internal timeout never fires, and the nonce reset logic only runs if send_event_loop().await returns. With the proposer’s outer timeout, the whole future is dropped before that cleanup can run.

Impact Details

This is a high-severity liveness issue for the proposer. Once nonce n is leaked, every later proposal transaction is signed with nonce n+1 or higher. Ethereum will not mine those transactions while nonce n is missing, so the proposer can get stuck behind a transaction that was never even published.

The practical result is ugly: L1 dispute game / output proposal submission can stop until someone notices and recovers the service by restarting, resetting nonce state, or manually clearing the gap. In the meantime, legitimate proposals are not created on L1. That means the smart-contract workflow is not just delayed by normal network conditions; it is completely wedged by the proposer’s own nonce state.

There is no direct theft here, and this does not halt the whole L1/L2 network. But for the proposal system, it is a hard availability failure. The trigger is realistic too: a congested L1, degraded RPC, repeated prepare retries, fee-bump churn, or a slow remote signer/HSM can all push the outer 10-minute timeout over the edge. With tx_send_timeout disabled by default, a stock proposer has no earlier internal timeout cleanup to catch it first. One timeout at the wrong point can turn into an indefinite submission outage for that proposer instance.

References

  • crates/utilities/tx-manager/src/manager.rs

  • crates/proof/proposer/src/pipeline.rs

  • crates/proof/proposer/src/output_proposer.rs

  • crates/utilities/tx-manager/src/nonce.rs

  • crates/utilities/tx-manager/src/config.rs

  • crates/utilities/tx-manager/src/macros.rs

Proof of Concept

Make these changes

which outputs

test poc_outer_timeout_during_initial_send_leaks_nonce ... ok

The test uses a signer that blocks after signing starts, after the nonce has already been reserved. The outer timeout cancels send(), and the next nonce returned by the manager is 1, showing nonce 0 was leaked without publication. This test demonstrates the TxManager failure mode directly; the production proposer reaches the same send() path through ProposalSubmitter::propose_output() under the outer timeout in pipeline.rs.

Was this helpful?