41878 [BC-High] edge case allows replaying user transactions to fill the mempool

#41878 [BC-High] Edge-case allows replaying user transactions to fill the mempool

Submitted on Mar 19th 2025 at 05:13:59 UTC by @Capybara for Attackathon | Movement Labs

  • Report ID: #41878

  • Report Type: Blockchain/DLT

  • Report severity: High

  • Target: https://github.com/immunefi-team/attackathon-movement/tree/main/protocol-units/execution/maptos/opt-executor

  • Impacts:

    • Causing network processing nodes to process transactions from the mempool beyond set parameters

Description

Summary

Nodes maintain a pool of "used sequence numbers" (UsedSequenceNumberPool) to ensure that only transactions with valid sequence numbers are added to the mempool.

Transactions will not be inserted if the sequence number is too old or too new.

Every 30 seconds, the UsedSequenceNumberPool clears older records.

An edge case allows a malicious user to insert duplicate transactions in the mempool by combining two flags:

  • 1- The opt-executor sends the wrong sequence number to the TransactionStore (an in-memory storage for all transactions in the mempool) and the StateView (a snapshot of the global state).

  • 2- The pool of "used sequence numbers" (UsedSequenceNumberPool) contains the correct value but clears after 30 seconds. The vector becomes possible once the transaction is removed from the pool.

Details

Ensuring valid sequence numbers

When submitting a transaction, the function submit_transaction(transaction: SignedTransaction) checks the validity of the sequence number

https://github.com/immunefi-team/attackathon-movement/blob/a2790c6ac17b7cf02a69aea172c2b38d2be8ce00/protocol-units/execution/maptos/opt-executor/src/background/transaction_pipe.rs#L267-L272

The function has_invalid_sequence_number(...) does the following:

  • Reads from the "used sequence number pool" the cached value and returns 0 if none.

  • Reads the account sequence number from the global state in get_account_sequence_number.

  • Picks the min possible sequence number as the maximum value between the values extracted in the above two steps.

  • Ensures the sequence number of the current transaction is within a valid range:

The max value is calculated as: committed_sequence_number + TOO_NEW_TOLERANCE and TOO_NEW_TOLERANCE is equal to 32.

Below is the full function as reference:

https://github.com/immunefi-team/attackathon-movement/blob/a2790c6ac17b7cf02a69aea172c2b38d2be8ce00/protocol-units/execution/maptos/opt-executor/src/background/transaction_pipe.rs#L164-L218

Updating an account's latest sequence number

The Movement Network enables users to send transactions with a sequence number up to +32 higher than the account's current sequence number.

If the sequence number of an account is 1, he's allowed to submit a transaction with 30 as the sequence number, which would be valid.

In such cases, the Transaction Pipe updates the account's sequence number and temporarily saves the most recently used sequence number (30, in our example) into the UsedSequenceNumberPool.

Refer to the following test, commented step-by-step so it is easy to understand:

The bug

The bug, is that the Transaction Pipie should update the account's sequence number by increasing +1 the value of its latest committed transaction (30 + 1 = 31, using our previous example). Instead, it increases +1 the value stored in the global state view for this account (1 + 1 = 2).

As a result, in our example, a user with a sequence number of 1 submits a transaction with a sequence number of 30, it gets executed, and the global state view of his account updates to a sequence number of 2, (instead of 31). Fortunately, the UsedSequenceNumberPool is updated with the correct value, preventing an immediate replay attack.

But 30 seconds later, the UsedSequenceNumberPool is cleared, returning 0 as the account's sequence number, and the global state view returns 2 (instead of 31) as the account's sequence number.

When someone maliciously resubmits the exact same signed transaction from that user with the same sequence number it originally had (30), it bypasses the check that prevents too old or too new sequence numbers from entering the mempool, and the transaction will be inserted.

The transaction won't get re-executed, but it will take space in the mempool from other legitimate user transactions.

Impact

The number of transactions stored in the mempool is limited and can be reduced by exploiting the vector outlined in this report.

Replaying transactions to fill the mempool prevents legitimate user transactions from getting processed faster.

From the list of impacts in scope, the one described in this report is: Causing the network nodes to process transactions from the mempool beyond set parameters.

Proof of Concept

Proof of Concept

Add the test below to ./attackathon-movement/protocol-units/execution/maptos/opt-executor/src/background/transaction_pipe.rs

It is very well documented, so it is easy to understand.

• Adds a transaction with sequence number 1; it is accepted into the mempool. • Attempts to submit the same sequence number again; it is rejected as InvalidSeqNumber. • Adds a transaction with a higher sequence number (30); it is accepted. • Delay before checking garbage collection (GC). • If the GC interval has elapsed, clean up expired transactions and sequence numbers. • The same transaction is submitted again; it is accepted this time.

Code:

The function below is a helper, and it goes inside aptos-core; ./types/src/test_helpers/transaction_test_helpers.rs

Was this helpful?