#42930 [BC-High] Users are unable to increase their gas resulting in stuck funds
Submitted on Mar 29th 2025 at 14:32:03 UTC by @okmxuse for Attackathon | Movement Labs
Report ID: #42930
Report Type: Blockchain/DLT
Report severity: High
Target: https://github.com/immunefi-team/attackathon-movement/tree/main/protocol-units/execution/maptos/opt-executor
Impacts:
Permanent freezing of funds (fix requires hardfork)
Direct loss of funds
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
Description
The Aptos mempool, like other networks, allows users to increase the gas price of a previously submitted transaction.
For this to happen, the newly submitted transaction must have the same sequence number as the original transaction the user intends to speed up. This requirement is enforced in:
transaction_store.rs::insert
if let Some(txns) = self.transactions.get_mut(&address) {
if let Some(current_version) = txns.get_mut(&txn_seq_num) {
// ..code
If these conditions are met, the user's transaction with the increased gas price is accepted:
if let Some(txns) = self.transactions.get_mut(&address) {
if let Some(current_version) = txns.get_mut(&txn_seq_num) {
// ..code
else if current_version.get_gas_price() < txn.get_gas_price() {
// Update transaction if gas unit price is higher than before
if let Some(txn) = txns.remove(&txn_seq_num) {
self.index_remove(&txn);
}
//..code
MempoolStatus::new(MempoolStatusCode::Accepted)
}
Despite this intended behavior, a conflicting check in transaction_pipe.rs::has_invalid_sequence_number
will always prevent gas-increase transactions from succeeding:
if transaction.sequence_number() < min_sequence_number {
info!("Transaction sequence number too old: {:?}", transaction.sequence_number());
return Ok(SequenceNumberValidity::Invalid((
MempoolStatus::new(MempoolStatusCode::InvalidSeqNumber),
Some(DiscardedVMStatus::SEQUENCE_NUMBER_TOO_OLD),
)));
}
This check will never allow the same sequence number to be submitted.
Note that this check is different than the one made in mempool.rs::add_txn
and this is not the reason why the transactions fails.
// don't accept old transactions (e.g. seq is less than account's current seq_number)
if txn.sequence_number() < db_sequence_number {
return MempoolStatus::new(MempoolStatusCode::InvalidSeqNumber).with_message(format!(
"transaction sequence number is {}, current sequence number is {}",
txn.sequence_number(),
db_sequence_number,
));
}
We will further prove all this in the POC.
Impact
This issue inevitably results in a user's funds becoming stuck. At worst, it can cause a user to have a direct loss of funds since the transaction might become stuck permanently since a user is never able to speed it up.
Recommendation
We would recommend mitigating this issue properly.
Proof of Concept
Proof of Concept
Add the following test function** inside
transaction_pipe.rs
:
async fn test_user_cannot_increase_gas() -> Result<(), anyhow::Error> {
// Set up environment
let maptos_config = Config::default();
let (_context, mut transaction_pipe, _tx_receiver, _tempdir) = setup().await;
// Submit a transaction with a valid sequence number
let user_transaction = create_signed_transaction(1, &maptos_config);
let (mempool_status, _) = transaction_pipe.submit_transaction(user_transaction).await?;
assert_eq!(mempool_status.code, MempoolStatusCode::Accepted);
// Attempt to submit another transaction with the same sequence number but increased gas
let user_transaction = create_signed_transaction(1, &maptos_config);
let (mempool_status, _) = transaction_pipe.submit_transaction(user_transaction).await?;
assert_eq!(mempool_status.code, MempoolStatusCode::InvalidSeqNumber);
Ok(())
}
Run the test:
nix develop -c cargo test test_user_cannot_increase_gas
Logs
running 1 test test background::transaction_pipe::tests::test_user_cannot_increase_gas ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 15 filtered out; finished in 3.46s
To confirm that the issue is not related to the mempool.rs::add_txn
check, modify transaction_pipe.rs::has_invalid_sequence_number
by removing the root issue check:
+ // if transaction.sequence_number() < min_sequence_number {
+ // info!("Transaction sequence number too old: {:?}", transaction.sequence_number());
+ // return Ok(SequenceNumberValidity::Invalid((
+ // MempoolStatus::new(MempoolStatusCode::InvalidSeqNumber),
+ // Some(DiscardedVMStatus::SEQUENCE_NUMBER_TOO_OLD),
+ // )));
+ // }
Re-run the test:
nix develop -c cargo test test_user_cannot_increase_gas
Failure Logs:
assertion `left == right` failed left: Accepted right: InvalidSeqNumber note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: background::transaction_pipe::tests::test_user_cannot_increase_gas test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 15 filtered out; finished in 3.53s error: test failed, to rerun pass `--lib`
Since the test now fails with Accepted
instead of InvalidSeqNumber
, it confirms that the original rejection was due to the has_invalid_sequence_number
check and not the one from mempool.rs::add_txn
:
// don't accept old transactions (e.g. seq is less than account's current seq_number)
if txn.sequence_number() < db_sequence_number {
return MempoolStatus::new(MempoolStatusCode::InvalidSeqNumber).with_message(format!(
"transaction sequence number is {}, current sequence number is {}",
txn.sequence_number(),
db_sequence_number,
));
}
Was this helpful?