Skip to content

Commit 6482149

Browse files
committed
fix: bound ABI decoding in Zone transaction paths
1 parent 2868509 commit 6482149

6 files changed

Lines changed: 133 additions & 40 deletions

File tree

crates/evm/src/executor.rs

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use alloy_evm::{
1313
},
1414
eth::{EthBlockExecutor, EthTxResult},
1515
};
16-
use alloy_sol_types::SolEvent as _;
16+
use alloy_sol_types::{SolCall as _, SolEvent as _};
1717
use reth_evm::block::StateDB;
1818
use reth_revm::{Inspector, context::result::ResultAndState};
1919
use tempo_evm::{TempoBlockExecutionCtx, TempoReceiptBuilder};
@@ -22,9 +22,7 @@ use tempo_revm::evm::TempoContext;
2222
use tempo_zone_contracts::IZoneOutbox;
2323
use zone_chainspec::ZoneChainSpec;
2424
use zone_l1::state::L1StateProvider;
25-
use zone_precompiles::{
26-
ADVANCE_TEMPO_SELECTOR, L1StorageReader, is_finalize_withdrawal_batch_calldata,
27-
};
25+
use zone_precompiles::{ADVANCE_TEMPO_SELECTOR, L1StorageReader};
2826
use zone_primitives::constants::{ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS};
2927

3028
use crate::{L1OverlayDB, ZoneEvm};
@@ -106,7 +104,8 @@ impl ZoneTransactionKind {
106104
}
107105

108106
if tx.calls().any(|(kind, input)| {
109-
kind.to() == Some(&ZONE_OUTBOX_ADDRESS) && is_finalize_withdrawal_batch_calldata(input)
107+
kind.to() == Some(&ZONE_OUTBOX_ADDRESS)
108+
&& input.starts_with(&IZoneOutbox::finalizeWithdrawalBatchCall::SELECTOR)
110109
}) {
111110
return Self::FinalizeWithdrawalBatch;
112111
}
@@ -285,7 +284,7 @@ mod tests {
285284
use reth_chainspec::EthChainSpec as _;
286285
use reth_primitives_traits::Recovered;
287286
use revm::database::{CacheDB, EmptyDB};
288-
use tempo_chainspec::spec::DEV;
287+
use tempo_chainspec::{hardfork::TempoHardfork, spec::DEV};
289288
use tempo_evm::TempoBlockExecutionCtx;
290289
use tempo_precompiles::{
291290
DEFAULT_FEE_TOKEN, TIP_FEE_MANAGER_ADDRESS,
@@ -461,14 +460,74 @@ mod tests {
461460
ZONE_OUTBOX_ADDRESS,
462461
Bytes::copy_from_slice(&IZoneOutbox::finalizeWithdrawalBatchCall::SELECTOR),
463462
);
464-
let error = ZoneBlockPhase::Executing
465-
.validate_transaction(&malformed_finalize)
466-
.unwrap_err();
467463
assert_eq!(
468-
error.to_string(),
469-
"system transactions after advanceTempo must call \
470-
ZoneOutbox.finalizeWithdrawalBatch"
464+
ZoneBlockPhase::Executing
465+
.validate_transaction(&malformed_finalize)
466+
.unwrap(),
467+
ZoneBlockPhase::WithdrawalsFinalized
468+
);
469+
470+
let mut trailing_calldata = IZoneOutbox::finalizeWithdrawalBatchCall {
471+
count: U256::ZERO,
472+
blockNumber: 1,
473+
encryptedSenders: vec![],
474+
}
475+
.abi_encode();
476+
trailing_calldata.extend_from_slice(&[0; 32]);
477+
let trailing_finalize = system_tx(ZONE_OUTBOX_ADDRESS, trailing_calldata.into());
478+
assert_eq!(
479+
ZoneBlockPhase::Executing
480+
.validate_transaction(&trailing_finalize)
481+
.unwrap(),
482+
ZoneBlockPhase::WithdrawalsFinalized
483+
);
484+
}
485+
486+
#[test]
487+
fn malformed_t11_finalization_does_not_advance_block_phase() {
488+
let mut zone_genesis = DEV.genesis().clone();
489+
zone_genesis.config.chain_id = zone_chain_id(DEV.chain().id(), 2).unwrap();
490+
let chain_spec = std::sync::Arc::new(ZoneChainSpec::from_genesis(zone_genesis).unwrap());
491+
let factory =
492+
ZoneEvmFactory::new(chain_spec.clone(), MockL1Reader::default(), Address::ZERO);
493+
let mut env = EvmEnv::default();
494+
env.cfg_env.spec = TempoHardfork::T11;
495+
let evm = factory.create_evm(CacheDB::new(EmptyDB::default()), env);
496+
let ctx = TempoBlockExecutionCtx {
497+
inner: EthBlockExecutionCtx {
498+
parent_hash: B256::ZERO,
499+
parent_beacon_block_root: None,
500+
ommers: &[],
501+
withdrawals: None,
502+
extra_data: Bytes::new(),
503+
tx_count_hint: Some(1),
504+
slot_number: None,
505+
},
506+
general_gas_limit: 0,
507+
shared_gas_limit: 0,
508+
validator_set: None,
509+
consensus_context: None,
510+
subblock_fee_recipients: Default::default(),
511+
};
512+
let mut executor = ZoneBlockExecutor::new(evm, ctx, &chain_spec);
513+
executor.phase = ZoneBlockPhase::Executing;
514+
515+
let tx = Recovered::new_unchecked(
516+
system_tx(
517+
ZONE_OUTBOX_ADDRESS,
518+
Bytes::copy_from_slice(&IZoneOutbox::finalizeWithdrawalBatchCall::SELECTOR),
519+
),
520+
TEMPO_SYSTEM_TX_SENDER,
521+
);
522+
let error = executor.execute_transaction_without_commit(tx).unwrap_err();
523+
524+
assert!(
525+
error
526+
.to_string()
527+
.contains("system transaction execution failed"),
528+
"unexpected error: {error}"
471529
);
530+
assert_eq!(executor.phase, ZoneBlockPhase::Executing);
472531
}
473532

474533
#[test]

crates/node/src/replication.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use alloy_primitives::B256;
66
use alloy_provider::DynProvider;
77
use alloy_rlp::Decodable as _;
88
use alloy_rpc_types_engine::ForkchoiceState;
9-
use alloy_sol_types::SolCall as _;
9+
use alloy_sol_types::{SolCall as _, abi::AbiDecoderConfig};
1010
use futures::{StreamExt as _, stream::BoxStream};
1111
use reth_chain_state::PersistedBlockSubscriptions;
1212
use reth_node_api::{ConsensusEngineHandle, PayloadTypes as _};
@@ -18,6 +18,7 @@ use std::{
1818
time::Duration,
1919
};
2020
use tempo_alloy::TempoNetwork;
21+
use tempo_precompiles::dispatch::ABI_DECODER_MEMORY_LIMIT;
2122
use tempo_primitives::{Block, TempoHeader, TempoTxEnvelope};
2223
use tokio::sync::{mpsc, oneshot};
2324
use tokio_util::sync;
@@ -1375,8 +1376,13 @@ fn decode_advance_tempo(
13751376
if signed.tx().to != ZONE_INBOX_ADDRESS.into() {
13761377
eyre::bail!("first Tempo system transaction is not sent to IZoneInbox")
13771378
}
1378-
let call = IZoneInbox::advanceTempoCall::abi_decode(signed.tx().input.as_ref())
1379-
.map_err(|err| eyre::eyre!("first transaction does not decode as advanceTempo: {err}"))?;
1379+
let call = IZoneInbox::advanceTempoCall::abi_decode_with_config(
1380+
signed.tx().input.as_ref(),
1381+
AbiDecoderConfig::new()
1382+
.memory_limit(ABI_DECODER_MEMORY_LIMIT)
1383+
.strict(true),
1384+
)
1385+
.map_err(|err| eyre::eyre!("first transaction does not decode as advanceTempo: {err}"))?;
13801386

13811387
// 3. the system tx is valid.
13821388
let mut header_rlp = call.header.as_ref();

crates/precompiles/src/account_keychain.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Zone read-privacy rules for the upstream Tempo AccountKeychain precompile.
22
33
use alloy_primitives::Address;
4-
use alloy_sol_types::SolInterface;
4+
use alloy_sol_types::{SolCall, SolInterface};
55
use tempo_contracts::precompiles::IAccountKeychain;
66
use tempo_precompiles::dispatch::abi_decoder_config_for_spec;
77

@@ -15,11 +15,36 @@ use crate::{
1515
#[derive(Clone)]
1616
pub(crate) struct AccountKeychainRules;
1717

18+
const UNRESTRICTED_SELECTORS: &[[u8; 4]] = &[
19+
IAccountKeychain::authorizeKey_0Call::SELECTOR,
20+
IAccountKeychain::authorizeKey_1Call::SELECTOR,
21+
IAccountKeychain::authorizeKey_2Call::SELECTOR,
22+
IAccountKeychain::authorizeAdminKeyCall::SELECTOR,
23+
IAccountKeychain::burnKeyAuthorizationWitnessCall::SELECTOR,
24+
IAccountKeychain::revokeKeyCall::SELECTOR,
25+
IAccountKeychain::updateSpendingLimitCall::SELECTOR,
26+
IAccountKeychain::setAllowedCallsCall::SELECTOR,
27+
IAccountKeychain::removeAllowedCallsCall::SELECTOR,
28+
IAccountKeychain::getTransactionKeyCall::SELECTOR,
29+
];
30+
1831
impl CallRules for AccountKeychainRules {
1932
fn admit(&self, data: &[u8], caller: Address) -> CallCheck {
33+
let spec = StorageCtx::default().spec();
34+
35+
// These calls have no Zone-specific privacy policy. Defer directly to the upstream
36+
// dispatcher for selector scheduling and ABI decoding.
37+
if data.get(..4).is_some_and(|selector| {
38+
UNRESTRICTED_SELECTORS
39+
.iter()
40+
.any(|allowed| selector == allowed.as_slice())
41+
}) {
42+
return CallCheck::Continue;
43+
}
44+
2045
let Ok(call) = IAccountKeychain::IAccountKeychainCalls::abi_decode_with_config(
2146
data,
22-
abi_decoder_config_for_spec(StorageCtx::default().spec()),
47+
abi_decoder_config_for_spec(spec),
2348
) else {
2449
// Preserve the upstream error and gas behavior for malformed or unknown calldata.
2550
return CallCheck::Continue;
@@ -229,4 +254,18 @@ mod tests {
229254
CallCheck::Continue
230255
));
231256
}
257+
258+
#[test]
259+
fn malformed_unrestricted_calls_remain_deferred_to_upstream() {
260+
let rules = AccountKeychainRules;
261+
262+
for data in UNRESTRICTED_SELECTORS {
263+
for spec in [TempoHardfork::T10, TempoHardfork::T11] {
264+
assert!(matches!(
265+
admit_at(&rules, data, Address::ZERO, spec),
266+
CallCheck::Continue
267+
));
268+
}
269+
}
270+
}
232271
}

crates/precompiles/src/inbox/mod.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ use alloc::vec::Vec;
2020

2121
use alloy_evm::precompiles::DynPrecompile;
2222
use alloy_primitives::{Address, B256, U256};
23-
use alloy_sol_types::{SolCall, SolType, SolValue};
23+
use alloy_sol_types::{SolCall, SolValue, abi::AbiDecoderConfig};
2424
use tempo_precompiles::{
2525
PATH_USD_ADDRESS,
26+
dispatch::ABI_DECODER_MEMORY_LIMIT,
2627
error::TempoPrecompileError,
2728
storage::{Handler, Mapping, Slot, StorageCtx},
2829
tip20::{ISSUER_ROLE, ITIP20, TIP20Error, TIP20Token},
@@ -378,27 +379,24 @@ impl TryFrom<QueuedDeposit> for DecodedQueuedDeposit {
378379
type Error = ZonePrecompileError;
379380

380381
fn try_from(queued: QueuedDeposit) -> Result<Self, Self::Error> {
382+
let config = AbiDecoderConfig::new()
383+
.memory_limit(ABI_DECODER_MEMORY_LIMIT)
384+
.strict(true);
385+
381386
match queued.depositType {
382387
DepositType::WithdrawalBounceBack => {
383-
decode_canonical(&queued.depositData).map(Self::WithdrawalBounceBack)
388+
WithdrawalBounceBackDeposit::abi_decode_with_config(&queued.depositData, config)
389+
.map(Self::WithdrawalBounceBack)
390+
}
391+
DepositType::Deposit => {
392+
Deposit::abi_decode_with_config(&queued.depositData, config).map(Self::Deposit)
384393
}
385-
DepositType::Deposit => decode_canonical(&queued.depositData).map(Self::Deposit),
386394
_ => return Err(ZonePrecompileError::MalformedCalldata),
387395
}
388396
.map_err(|_| ZonePrecompileError::MalformedCalldata)
389397
}
390398
}
391399

392-
fn decode_canonical<T>(encoded: &[u8]) -> alloy_sol_types::Result<T>
393-
where
394-
T: SolValue + From<<T::SolType as SolType>::RustType>,
395-
{
396-
let value = T::abi_decode(encoded)?;
397-
(value.abi_encode().as_slice() == encoded)
398-
.then_some(value)
399-
.ok_or(alloy_sol_types::Error::ReserMismatch)
400-
}
401-
402400
fn decode_deposits(deposits: Vec<QueuedDeposit>) -> ZoneResult<Vec<DecodedQueuedDeposit>> {
403401
deposits.into_iter().map(TryInto::try_into).collect()
404402
}

crates/precompiles/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pub mod ztip20;
8484
pub use aes_gcm::AesGcmDecrypt;
8585
pub use chaum_pedersen::ChaumPedersenVerify;
8686
pub use inbox::{ADVANCE_TEMPO_SELECTOR, ZoneInbox};
87-
pub use outbox::{ZoneOutbox, is_finalize_withdrawal_batch_calldata};
87+
pub use outbox::ZoneOutbox;
8888
pub use storage::{L1State, L1StateError, L1StorageReader};
8989
pub use tempo_contracts::precompiles::TIP403_REGISTRY_ADDRESS;
9090
pub use tempo_state::TempoState;

crates/precompiles/src/outbox/mod.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ mod tests;
77
use alloc::vec::Vec;
88

99
use alloy_primitives::{Address, B256, Bytes, U256};
10-
use alloy_sol_types::SolCall;
1110
use tempo_precompiles::{
1211
Result as TempoResult,
1312
error::TempoPrecompileError,
@@ -32,14 +31,6 @@ use crate::{
3231
pub const MAX_CALLBACK_DATA_SIZE: usize = 1024;
3332
const WITHDRAWAL_BASE_GAS: u64 = 50_000;
3433

35-
/// Returns whether `calldata` is a canonical `finalizeWithdrawalBatch` call.
36-
pub fn is_finalize_withdrawal_batch_calldata(calldata: &[u8]) -> bool {
37-
let Ok(call) = IZoneOutbox::finalizeWithdrawalBatchCall::abi_decode(calldata) else {
38-
return false;
39-
};
40-
call.abi_encode() == calldata
41-
}
42-
4334
#[contract(addr = ZONE_OUTBOX_ADDRESS)]
4435
pub struct ZoneOutbox {
4536
tempo_gas_rate: u128,

0 commit comments

Comments
 (0)