Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[advisories]
ignore = [
# hickory-proto NSEC3 closest-encloser proof validation has no fixed release yet.
"RUSTSEC-2026-0118",
# hickory-proto CPU exhaustion remains transitive through libp2p 0.56 and
# there is no compatible crates.io upgrade path in this workspace yet.
"RUSTSEC-2026-0119",
]
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ winterfell = "0.13"
ark-relations = { path = "deps/ark-relations" }
libp2p-yamux = { path = "deps/libp2p-yamux" }

[workspace.metadata.audit]
ignore = [
# hickory-proto NSEC3 closest-encloser proof validation has no fixed release yet.
"RUSTSEC-2026-0118",
# hickory-proto CPU exhaustion remains transitive through libp2p 0.56 and
# there is no compatible crates.io upgrade path in this workspace yet.
"RUSTSEC-2026-0119",
]

[profile.dev]
debug = 1
split-debuginfo = "unpacked"
Expand Down
28 changes: 28 additions & 0 deletions crates/core/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,14 @@ impl AaBundle {
.sum::<u128>()
}

/// Sum of inner-call ETH values. Caller MUST verify this is ≤ outer
/// `Transaction.value`.
pub fn inner_value_sum(&self) -> U256 {
self.inner_calls
.iter()
.fold(U256::ZERO, |acc, c| acc.saturating_add(c.value))
Comment on lines +843 to +846
}

/// Intrinsic gas surcharge added by the bundle on top of the standard
/// 21_000 base: 4_000 per *additional* inner call beyond the first,
/// plus 10_000 per PQ signature verify on the session key path (2 verifies).
Expand Down Expand Up @@ -1402,6 +1410,9 @@ impl SignedTransaction {
if aa_bundle.inner_gas_sum() > tx.gas_limit as u128 {
return Err("with_aa_bundle: sum(inner.gas_limit) exceeds outer gas_limit");
}
if aa_bundle.inner_value_sum() > tx.value {
return Err("with_aa_bundle: sum(inner.value) exceeds outer value");
}
Ok(Self {
from,
tx,
Expand Down Expand Up @@ -2847,6 +2858,23 @@ mod tests {
assert!(err.contains("exceeds outer gas_limit"));
}

#[test]
fn signed_tx_with_aa_bundle_rejects_inner_value_overspend() {
let mut tx = sample_aa_tx();
tx.value = U256::from(1u64);
let from = Address::from([0x42; 20]);
let sig = PQSignature::new(SignatureType::Dilithium3, vec![0xBB; 50]);
let bundle = AaBundle {
inner_calls: vec![sample_inner_call(2)],
paymaster: None,
paymaster_signature: None,
..Default::default()
};
let err = SignedTransaction::with_aa_bundle(from, tx, sig, PubkeyMode::Reference, bundle)
.unwrap_err();
assert!(err.contains("exceeds outer value"));
}

#[test]
fn batch_signing_hash_distinct_from_legacy_hash() {
let tx = sample_aa_tx();
Expand Down
42 changes: 39 additions & 3 deletions crates/evm/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ impl<S: KvStore + 'static> ShellEvm<S> {
let payer = bundle.paymaster.unwrap_or(sender);
let max_fee = U256::from(tx.max_fee_per_gas);
let is_sponsored = payer != sender;
let declared_value = tx.value;
let inner_value_sum = bundle.inner_value_sum();
if inner_value_sum > declared_value {
return Err(ExecutorError::Evm(format!(
"aa bundle inner value sum ({inner_value_sum}) exceeds outer value ({declared_value})"
)));
}
Comment on lines +344 to +350

// Capture pre-bundle state root for atomic rollback on inner failure.
let pre_root = self.state_db.world_state_mut().state_root()?;
Expand Down Expand Up @@ -896,7 +903,7 @@ pub fn commit_evm_state<S: KvStore + 'static>(
mod tests {
use super::*;
use shell_core::{Account, SignedTransaction, Transaction};
use shell_crypto::{DilithiumSigner, PQSignature, SignatureType, Signer};
use shell_crypto::{PQSignature, SignatureType};
use shell_storage::{ChainStore, MemoryDb, WorldState};
use std::sync::Arc;

Expand Down Expand Up @@ -3403,7 +3410,7 @@ mod tests {
chain_id: 1337,
nonce: 0,
to: None,
value: U256::ZERO,
value: U256::from(1u64),
data: shell_primitives::Bytes::new(),
gas_limit: 200_000,
max_fee_per_gas: 10,
Expand Down Expand Up @@ -3455,11 +3462,14 @@ mod tests {
paymaster: Option<ShellAddress>,
) -> SignedTransaction {
use shell_core::{AaBundle, AA_BUNDLE_TX_TYPE};
let value = inner_calls
.iter()
.fold(U256::ZERO, |acc, call| acc.saturating_add(call.value));
let tx = Transaction {
chain_id: 1337,
nonce,
to: None,
value: U256::ZERO,
value,
data: shell_primitives::Bytes::new(),
gas_limit,
max_fee_per_gas: max_fee,
Expand Down Expand Up @@ -3660,6 +3670,32 @@ mod tests {
assert_eq!(res.gas_used, 0);
}

#[test]
fn execute_aa_bundle_rejects_inner_value_overspend() {
use shell_core::InnerCall;
use shell_primitives::Bytes as PBytes;

let mut evm = setup_evm();
let sender = ShellAddress::from([0x42; 20]);
let dst = ShellAddress::from([0xAA; 20]);
fund_account(&mut evm, &sender, U256::from(10_000_000u64));

let inner_calls = vec![InnerCall {
to: Some(dst),
value: U256::from(2u64),
data: PBytes::new(),
gas_limit: 50_000,
}];
let mut signed = make_aa_signed(sender, 0, 200_000, 10, inner_calls, None);
signed.tx.value = U256::from(1u64);

let header = sample_header();
let res = evm.execute_aa_bundle(&signed, &header, 0, 0);
assert!(matches!(res, Err(ExecutorError::Evm(msg)) if msg.contains("exceeds outer value")));
assert_eq!(get_balance(&mut evm, &dst), U256::ZERO);
assert_eq!(get_nonce(&mut evm, &sender), 0);
}

#[test]
fn execute_aa_bundle_single_nonce_bump_regardless_of_inner_count() {
use shell_core::InnerCall;
Expand Down
Loading
Loading