# #43288 \[BC-Critical] Attackers could force Nodes to process TraAttackers could force Nodes to process Transactions in wrong order, by attacking moveRocks/sequencing implementation

**Submitted on Apr 4th 2025 at 10:38:09 UTC by @Berserk for** [**Attackathon | Movement Labs**](https://immunefi.com/audit-competition/movement-labs-attackathon)

* **Report ID:** #43288
* **Report Type:** Blockchain/DLT
* **Report severity:** Critical
* **Target:** <https://github.com/immunefi-team/attackathon-movement/tree/main/protocol-units/sequencing/memseq/sequencer>
* **Impacts:**
  * Causing network processing nodes to process transactions from the mempool beyond set parameters
  * Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours beyond standard difficulty adjustments

## Description

## Brief/Intro

There is a vulnerability in the sequencer implementation that allows attackers to force nodes to process transactions from the same account in a wrong sequence number order.\
(e.g., 3 transactions from Account A: seqnr 1, seqnr 2, seqnr 3. Attackers will force nodes to process tx with seq 3 first, seq 2 second, and seqnr 1 last, leading to all transactions failing (other than seqnr 1 of course))\
The mempool allows 32 transactions per account (by abusing this, attackers could force nodes to process at least 31 invalid transactions per account at a time at no cost)

## Vulnerability Details

### Root cause brief

Ordering of transactions in MoveRocks/RocksDB prioritizes the gas\_unit\_price first and doesn't **differentiate between accounts**. So if we have 2 transactions in rocks db from the **same account**:

* Transaction 1: seqnr 1 / gas unit price 100
* Transaction 2: seqnr 2 / gas unit price 150

When the sequencer tries to build the block, it will put Transaction 2 first and then Transaction 1 because Transaction 2 has a bigger gas unit price.\
\=> This will mess up the order of transactions as they should be executed in correct ascending seqnr order (so in this case Transaction 2 will fail because seqnr 2 > account\_seqnr(0) + 1)

## Detailed description of implementation

The sequencer relies on a newly developed mempool implementation that relies on rocks DB. when recieving transactions from the full node through batch\_write, the sequencer will insert the batch in the database and will construct for each one of them a key to allow for ordering/prioritizing.

`Source:protocol-units/mempool/move-rocks/src/lib.rs#L22-L36`

```rust
fn construct_mempool_transaction_key(transaction: &MempoolTransaction) -> Result<String, Error> {
	// Pre-allocate a string with the required capacity
	let mut key = String::with_capacity(32 + 1 + 32 + 1 + 32 + 1 + 32);
	// Write key components. The numbers are zero-padded to 32 characters.
	key.write_fmt(format_args!(
		"{:032}:{:032}:{:032}:{}",
@>		transaction.transaction.application_priority(),
		transaction.timestamp,
		transaction.transaction.sequence_number(),
		transaction.transaction.id(),
	))
	.map_err(|_| Error::msg("Error writing mempool transaction key"))?;
	Ok(key)
}
```

As we can see the first part of the key used contains the transaction application priority, which is `U64::max - transaction.gas_unit_price`

When building a new block to be submitted to DA the sequencer will pop transactions from the RockDB database:

```rust
	/// Waits for the next block to be built, either when the block size is reached or the building time expires.
	async fn wait_for_next_block(&self) -> Result<Option<Block>, anyhow::Error> {
		let mut transactions = Vec::with_capacity(self.block_size as usize);

		let now = Instant::now();

		loop {
			let current_block_size = transactions.len() as u32;
			if current_block_size >= self.block_size {
				break;
			}

			let remaining = self.block_size - current_block_size;
@>>			let mut transactions_to_add = self.mempool.pop_transactions(remaining as usize).await?;
			transactions.append(&mut transactions_to_add);
---
```

The order by which those transactions stored in the db wil be poped is defiend by the key constructed.

1. gas\_unit\_price (highest first)
2. addition to rocksdb timestamp (oldest first)
3. sequence nr (lowest first)
4. transaction id

Meaning if two transactions from same account if they have a differing `gas_unit_price` we will order them first based on the `gas_unit_price` and not the `sequence_nr`

## Impact Details

* Force network processing nodes to process transactions in the wrong sequence number order
* DoS network by flooding the network with up to 31 invalid transactions per account at a time at no cost

## References

* `Source:protocol-units/mempool/move-rocks/src/lib.rs#L22-L36`
* `Source:protocol-units/sequencing/memseq/sequencer/src/lib.rs#L100-L122`

## Proof of Concept

## Proof of Concept

### Attack Scenario

To demonstrate the vulenrabilty we will create a batch of 4 transactions and submit them through the rpc.\
The batch contains the following transactions (valid transfer transactions):

* Tx1: sender: Account A, seqnr 0, gas\_unit\_price 100
* Tx2: sender: Account A, seqnr 1, gas\_unit\_price 110
* Tx3: sender: Account A, seqnr 2, gas\_unit\_price 120
* Tx4: sender: Account A, seqnr 3, gas\_unit\_price 130

All of the transactions provided are valid transactions and the order is correct when submitting it through the rpc.

However after exectuting the 4 transactions we will notice from the fullnode logs that the order in which the transactions are executed

1. Tx4, Result: Discard(SEQUENCE\_NUMBER\_TOO\_NEW)
2. Tx3, Result: Discard(SEQUENCE\_NUMBER\_TOO\_NEW)
3. Tx2, Result: Discard(SEQUENCE\_NUMBER\_TOO\_NEW)
4. Tx1, Result: Keep(Success)

### Coded Poc

* main.rs: <https://gist.github.com/aliX40/92e94f9713d17ab22d5801958003f7a2>
* cargo.toml: <https://gist.github.com/aliX40/1257e284afc4b01f5f5430d5e37d0a9b>
* full node log: <https://gist.github.com/aliX40/72da56e976a6aeabbe3d42860c43d0f0>

By executing the main.rs script the following result was printed:

```log
     Running `/root/attack/attackathon-movement/target/debug/transaction-tester`
2025-04-04T10:26:44.814504Z  INFO transaction_tester: Initializing transaction test
2025-04-04T10:26:44.871918Z  INFO transaction_tester: Account address: 0x879daa9a9dda13c4722df4930114341c2918ffd44ee2764210bd2118eb1d981d
2025-04-04T10:26:44.872018Z  INFO transaction_tester: Auth key: 879daa9a9dda13c4722df4930114341c2918ffd44ee2764210bd2118eb1d981d
2025-04-04T10:26:44.872086Z  INFO transaction_tester: Public key: 80fc94ab796101eef9d3b6d46b7aa2da9c7ffdea0655b0c48c901d765f01bb12
2025-04-04T10:26:55.390127Z  INFO transaction_tester: Account address: 0xf12e7acff612f85187b7c8444de00ea716a11b6f4c273b31bec29e335e558af1
2025-04-04T10:26:55.390213Z  INFO transaction_tester: Auth key: f12e7acff612f85187b7c8444de00ea716a11b6f4c273b31bec29e335e558af1
2025-04-04T10:26:55.390241Z  INFO transaction_tester: Public key: fbd4b4439c2db86073f8d20994d5cbbe3e0db78bdc98adde02cb1b62fdd9fc71
Balance Before: 10000700
2025-04-04T10:27:24.304391Z  INFO transaction_tester: Creating test transactions
2025-04-04T10:27:24.942662Z  INFO transaction_tester: Batch of 4 transactions submitted successfully
Response: Response { inner: TransactionsBatchSubmissionResult { transaction_failures: [] }, state: State { chain_id: 27, epoch: 70082, version: 1014122, timestamp_usecs: 1743762417361210, oldest_ledger_version: 0, oldest_block_height: 0, block_height: 70082, cursor: None } }
Balance After: 9999200
2025-04-04T10:27:39.972507Z  INFO transaction_tester: Transaction test completed successfully
```

To check the execution result of the 4 submitted transactions we need to grep for `compute_status_for_input_txns` in the full node logs:

```log
compute_status_for_input_txns: [Keep(Success), // system transaction
Discard(SEQUENCE_NUMBER_TOO_NEW), // tx4 dropped
Discard(SEQUENCE_NUMBER_TOO_NEW), // tx3 dropped
Discard(SEQUENCE_NUMBER_TOO_NEW), // tx2 dropped
Keep(Success)] // tx1 accepted
```

To check the full logs of the full node, please see <https://gist.github.com/aliX40/72da56e976a6aeabbe3d42860c43d0f0>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://reports.immunefi.com/movement-labs-attackathon/43288-bc-critical-attackers-could-force-nodes-to-process-traattackers-could-force-nodes-to-process-t.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
