#43312 [BC-Medium] get_state_proof() is called with the current version leading to the epoch_changes of the StateProof always being empty
Submitted on Apr 4th 2025 at 14:00:01 UTC by @HollaDieWaldfee for Attackathon | Movement Labs
Report ID: #43312
Report Type: Blockchain/DLT
Report severity: Medium
Target: https://github.com/immunefi-team/attackathon-movement/tree/main/protocol-units/execution/maptos/opt-executor
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
When executing a block, get_state_proof()
is called but with the wrong version. As a result, the epoch_changes of the StateProof is always going to be empty, producing an invalid block commitment.
Vulnerability Details
After a block is executed, get_state_proof()
is called with the version being the state_compute.version()
after the new block is executed. This version is the current version and as a result, when get_state_proof_with_ledger_info()
is called, it always enters the else-block which returns a StateProof with empty epoch_changes:
fn get_state_proof_with_ledger_info(
&self,
known_version: u64,
ledger_info_with_sigs: LedgerInfoWithSignatures,
) -> Result<StateProof> {
gauged_api("get_state_proof_with_ledger_info", || {
let ledger_info = ledger_info_with_sigs.ledger_info();
ensure!(
known_version <= ledger_info.version(),
"Client known_version {} larger than ledger version {}.",
known_version,
ledger_info.version(),
);
let known_epoch = self.ledger_db.metadata_db().get_epoch(known_version)?;
let end_epoch = ledger_info.next_block_epoch();
let epoch_change_proof = if known_epoch < end_epoch {
let (ledger_infos_with_sigs, more) =
self.get_epoch_ending_ledger_infos(known_epoch, end_epoch)?;
EpochChangeProof::new(ledger_infos_with_sigs, more)
@> } else {
EpochChangeProof::new(vec![], /* more = */ false)
};
Ok(StateProof::new(ledger_info_with_sigs, epoch_change_proof))
})
}
Instead, get_state_proof()
should be called with the version BEFORE the block is executed such that a change in the epoch can be detected in the if-case.
Impact Details
The epoch_changes of the state proof after executing a block is always going to be empty. As a result, the block commitment that is calculated from the state proof can be invalid:
let commitment = Commitment::digest_state_proof(&proof);
Ok(BlockCommitment::new(block_height.into(), Id::new(*block_id.clone()), commitment))
References
(1): https://github.com/immunefi-team/attackathon-movement/blob/a2790c6ac17b7cf02a69aea172c2b38d2be8ce00/protocol-units/execution/maptos/opt-executor/src/executor/execution.rs#L63-L89
(2): https://github.com/immunefi-team/attackathon-movement-aptos-core/blob/627b4f9e0b63c33746fa5dae6cd672cbee3d8631/storage/aptosdb/src/db/include/aptosdb_reader.rs#L399-L424
Proof of Concept
Proof of Concept
During the block execution flow,
execute_block()
is called. After the block is executed,get_state_proof()
is called. However, this function is called with the wrong version, namely the version after the block is executed (reference (1)).As a result,
get_state_proof_with_ledger_info()
always returns a StateProof with empty epoch_changes sinceknown_epoch == end_epoch
and the function always enters the else-block.
Was this helpful?