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-executorsends the wrong sequence number to theTransactionStore(an in-memory storage for all transactions in the mempool) and theStateView(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
0if 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_TOLERANCEandTOO_NEW_TOLERANCEis 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?