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
5 changes: 5 additions & 0 deletions crates/anvil/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ impl NodeArgs {
.with_optimism(self.evm.optimism)
.with_odyssey(self.evm.odyssey)
.with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
.with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
.with_slots_in_an_epoch(self.slots_in_an_epoch)
.with_memory_limit(self.evm.memory_limit)
.with_cache_path(self.cache_path))
Expand Down Expand Up @@ -592,6 +593,10 @@ pub struct AnvilEvmArgs {
#[arg(long, visible_alias = "no-create2")]
pub disable_default_create2_deployer: bool,

/// Disable pool balance checks
#[arg(long)]
pub disable_pool_balance_checks: bool,

/// The memory limit per EVM execution in bytes.
#[arg(long)]
pub memory_limit: Option<u64>,
Expand Down
10 changes: 10 additions & 0 deletions crates/anvil/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ pub struct NodeConfig {
pub transaction_block_keeper: Option<usize>,
/// Disable the default CREATE2 deployer
pub disable_default_create2_deployer: bool,
/// Disable pool balance checks
pub disable_pool_balance_checks: bool,
/// Enable Optimism deposit transaction
pub enable_optimism: bool,
/// Slots in an epoch
Expand Down Expand Up @@ -484,6 +486,7 @@ impl Default for NodeConfig {
init_state: None,
transaction_block_keeper: None,
disable_default_create2_deployer: false,
disable_pool_balance_checks: false,
enable_optimism: false,
slots_in_an_epoch: 32,
memory_limit: None,
Expand Down Expand Up @@ -993,6 +996,13 @@ impl NodeConfig {
self
}

/// Sets whether to disable pool balance checks
#[must_use]
pub fn with_disable_pool_balance_checks(mut self, yes: bool) -> Self {
self.disable_pool_balance_checks = yes;
self
}

/// Injects precompiles to `anvil`'s EVM.
#[must_use]
pub fn with_precompile_factory(mut self, factory: impl PrecompileFactory + 'static) -> Self {
Expand Down
145 changes: 77 additions & 68 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ pub struct Backend {
// === wallet === //
capabilities: Arc<RwLock<WalletCapabilities>>,
executor_wallet: Arc<RwLock<Option<EthereumWallet>>>,
/// Disable pool balance checks
disable_pool_balance_checks: bool,
}

impl Backend {
Expand Down Expand Up @@ -309,9 +311,9 @@ impl Backend {
states = states.disk_path(cache_path);
}

let (slots_in_an_epoch, precompile_factory) = {
let (slots_in_an_epoch, precompile_factory, disable_pool_balance_checks) = {
let cfg = node_config.read().await;
(cfg.slots_in_an_epoch, cfg.precompile_factory.clone())
(cfg.slots_in_an_epoch, cfg.precompile_factory.clone(), cfg.disable_pool_balance_checks)
};

let (capabilities, executor_wallet) = if odyssey {
Expand Down Expand Up @@ -376,6 +378,7 @@ impl Backend {
mining: Arc::new(tokio::sync::Mutex::new(())),
capabilities: Arc::new(RwLock::new(capabilities)),
executor_wallet: Arc::new(RwLock::new(executor_wallet)),
disable_pool_balance_checks,
};

if let Some(interval_block_time) = automine_block_time {
Expand Down Expand Up @@ -3341,22 +3344,7 @@ impl TransactionValidator for Backend {
}
}

if tx.gas_limit() < MIN_TRANSACTION_GAS as u64 {
warn!(target: "backend", "[{:?}] gas too low", tx.hash());
return Err(InvalidTransactionError::GasTooLow);
}

// Check gas limit, iff block gas limit is set.
if !env.evm_env.cfg_env.disable_block_gas_limit
&& tx.gas_limit() > env.evm_env.block_env.gas_limit
{
warn!(target: "backend", "[{:?}] gas too high", tx.hash());
return Err(InvalidTransactionError::GasTooHigh(ErrDetail {
detail: String::from("tx.gas_limit > env.block.gas_limit"),
}));
}

// check nonce
// Nonce validation
let is_deposit_tx =
matches!(&pending.transaction.transaction, TypedTransaction::Deposit(_));
let nonce = tx.nonce();
Expand All @@ -3365,39 +3353,15 @@ impl TransactionValidator for Backend {
return Err(InvalidTransactionError::NonceTooLow);
}

if env.evm_env.cfg_env.spec >= SpecId::LONDON {
if tx.gas_price() < env.evm_env.block_env.basefee.into() && !is_deposit_tx {
warn!(target: "backend", "max fee per gas={}, too low, block basefee={}",tx.gas_price(), env.evm_env.block_env.basefee);
return Err(InvalidTransactionError::FeeCapTooLow);
}

if let (Some(max_priority_fee_per_gas), Some(max_fee_per_gas)) =
(tx.essentials().max_priority_fee_per_gas, tx.essentials().max_fee_per_gas)
&& max_priority_fee_per_gas > max_fee_per_gas
{
warn!(target: "backend", "max priority fee per gas={}, too high, max fee per gas={}", max_priority_fee_per_gas, max_fee_per_gas);
return Err(InvalidTransactionError::TipAboveFeeCap);
}
}

// EIP-4844 Cancun hard fork validation steps
// EIP-4844 structural validation
if env.evm_env.cfg_env.spec >= SpecId::CANCUN && tx.transaction.is_eip4844() {
// Light checks first: see if the blob fee cap is too low.
if let Some(max_fee_per_blob_gas) = tx.essentials().max_fee_per_blob_gas
&& let Some(blob_gas_and_price) = &env.evm_env.block_env.blob_excess_gas_and_price
&& max_fee_per_blob_gas < blob_gas_and_price.blob_gasprice
{
warn!(target: "backend", "max fee per blob gas={}, too low, block blob gas price={}", max_fee_per_blob_gas, blob_gas_and_price.blob_gasprice);
return Err(InvalidTransactionError::BlobFeeCapTooLow);
}

// Heavy (blob validation) checks
let tx = match &tx.transaction {
let blob_tx = match &tx.transaction {
TypedTransaction::EIP4844(tx) => tx.tx(),
_ => unreachable!(),
};

let blob_count = tx.tx().blob_versioned_hashes.len();
let blob_count = blob_tx.tx().blob_versioned_hashes.len();

// Ensure there are blob hashes.
if blob_count == 0 {
Expand All @@ -3412,40 +3376,85 @@ impl TransactionValidator for Backend {

// Check for any blob validation errors if not impersonating.
if !self.skip_blob_validation(Some(*pending.sender()))
&& let Err(err) = tx.validate(EnvKzgSettings::default().get())
&& let Err(err) = blob_tx.validate(EnvKzgSettings::default().get())
{
return Err(InvalidTransactionError::BlobTransactionValidationError(err));
}
}

let max_cost = tx.max_cost();
let value = tx.value();
// Balance and fee related checks
if !self.disable_pool_balance_checks {
// Gas limit validation
if tx.gas_limit() < MIN_TRANSACTION_GAS as u64 {
warn!(target: "backend", "[{:?}] gas too low", tx.hash());
return Err(InvalidTransactionError::GasTooLow);
}

match &tx.transaction {
TypedTransaction::Deposit(deposit_tx) => {
// Deposit transactions
// https://specs.optimism.io/protocol/deposits.html#execution
// 1. no gas cost check required since already have prepaid gas from L1
// 2. increment account balance by deposited amount before checking for sufficient
// funds `tx.value <= existing account value + deposited value`
if value > account.balance + U256::from(deposit_tx.mint) {
warn!(target: "backend", "[{:?}] insufficient balance={}, required={} account={:?}", tx.hash(), account.balance + U256::from(deposit_tx.mint), value, *pending.sender());
return Err(InvalidTransactionError::InsufficientFunds);
// Check gas limit against block gas limit, if block gas limit is set.
if !env.evm_env.cfg_env.disable_block_gas_limit
&& tx.gas_limit() > env.evm_env.block_env.gas_limit
{
warn!(target: "backend", "[{:?}] gas too high", tx.hash());
return Err(InvalidTransactionError::GasTooHigh(ErrDetail {
detail: String::from("tx.gas_limit > env.block.gas_limit"),
}));
}

// EIP-1559 fee validation (London hard fork and later)
if env.evm_env.cfg_env.spec >= SpecId::LONDON {
if tx.gas_price() < env.evm_env.block_env.basefee.into() && !is_deposit_tx {
warn!(target: "backend", "max fee per gas={}, too low, block basefee={}",tx.gas_price(), env.evm_env.block_env.basefee);
return Err(InvalidTransactionError::FeeCapTooLow);
}

if let (Some(max_priority_fee_per_gas), Some(max_fee_per_gas)) =
(tx.essentials().max_priority_fee_per_gas, tx.essentials().max_fee_per_gas)
&& max_priority_fee_per_gas > max_fee_per_gas
{
warn!(target: "backend", "max priority fee per gas={}, too high, max fee per gas={}", max_priority_fee_per_gas, max_fee_per_gas);
return Err(InvalidTransactionError::TipAboveFeeCap);
}
}

// EIP-4844 blob fee validation
if env.evm_env.cfg_env.spec >= SpecId::CANCUN
&& tx.transaction.is_eip4844()
&& let Some(max_fee_per_blob_gas) = tx.essentials().max_fee_per_blob_gas
&& let Some(blob_gas_and_price) = &env.evm_env.block_env.blob_excess_gas_and_price
&& max_fee_per_blob_gas < blob_gas_and_price.blob_gasprice
{
warn!(target: "backend", "max fee per blob gas={}, too low, block blob gas price={}", max_fee_per_blob_gas, blob_gas_and_price.blob_gasprice);
return Err(InvalidTransactionError::BlobFeeCapTooLow);
}
_ => {
// check sufficient funds: `gas * price + value`
let req_funds = max_cost.checked_add(value.saturating_to()).ok_or_else(|| {
warn!(target: "backend", "[{:?}] cost too high", tx.hash());
InvalidTransactionError::InsufficientFunds
})?;
if account.balance < U256::from(req_funds) {
warn!(target: "backend", "[{:?}] insufficient allowance={}, required={} account={:?}", tx.hash(), account.balance, req_funds, *pending.sender());
return Err(InvalidTransactionError::InsufficientFunds);

let max_cost = tx.max_cost();
let value = tx.value();
match &tx.transaction {
TypedTransaction::Deposit(deposit_tx) => {
// Deposit transactions
// https://specs.optimism.io/protocol/deposits.html#execution
// 1. no gas cost check required since already have prepaid gas from L1
// 2. increment account balance by deposited amount before checking for
// sufficient funds `tx.value <= existing account value + deposited value`
if value > account.balance + U256::from(deposit_tx.mint) {
warn!(target: "backend", "[{:?}] insufficient balance={}, required={} account={:?}", tx.hash(), account.balance + U256::from(deposit_tx.mint), value, *pending.sender());
return Err(InvalidTransactionError::InsufficientFunds);
}
}
_ => {
// check sufficient funds: `gas * price + value`
let req_funds =
max_cost.checked_add(value.saturating_to()).ok_or_else(|| {
warn!(target: "backend", "[{:?}] cost too high", tx.hash());
InvalidTransactionError::InsufficientFunds
})?;
if account.balance < U256::from(req_funds) {
warn!(target: "backend", "[{:?}] insufficient allowance={}, required={} account={:?}", tx.hash(), account.balance, req_funds, *pending.sender());
return Err(InvalidTransactionError::InsufficientFunds);
}
}
}
}

Ok(())
}

Expand Down
62 changes: 61 additions & 1 deletion crates/anvil/tests/it/txpool.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! txpool related tests

use alloy_network::TransactionBuilder;
use alloy_network::{ReceiptResponse, TransactionBuilder};
use alloy_primitives::U256;
use alloy_provider::{Provider, ext::TxPoolApi};
use alloy_rpc_types::TransactionRequest;
Expand Down Expand Up @@ -54,3 +54,63 @@ async fn geth_txpool() {
assert!(content.contains_key(&nonce.to_string()));
}
}

// Cf. https://github.com/foundry-rs/foundry/issues/11239
#[tokio::test(flavor = "multi_thread")]
async fn accepts_spend_after_funding_when_pool_checks_disabled() {
// Spawn with pool balance checks disabled
let (api, handle) = spawn(NodeConfig::test().with_disable_pool_balance_checks(true)).await;
let provider = handle.http_provider();

// Work with pending pool (no automine)
api.anvil_set_auto_mine(false).await.unwrap();

// Funder is a dev account controlled by the node
let funder = provider.get_accounts().await.unwrap().remove(0);

// Recipient/spender is a random address with zero balance that we'll impersonate
let spender = alloy_primitives::Address::random();
api.anvil_set_balance(spender, U256::from(0u64)).await.unwrap();
api.anvil_impersonate_account(spender).await.unwrap();

// Ensure tx1 (funding) has higher gas price so it's mined before tx2 within the same block
let gas_price_fund = 2_000_000_000_000u128; // 2_000 gwei
let gas_price_spend = 1_000_000_000u128; // 1 gwei

let fund_value = U256::from(1_000_000_000_000_000_000u128); // 1 ether

// tx1: fund spender from funder
let tx1 = TransactionRequest::default()
.with_from(funder)
.with_to(spender)
.with_value(fund_value)
.with_gas_price(gas_price_fund);
let tx1 = WithOtherFields::new(tx1);

// tx2: spender attempts to send value greater than their pre-funding balance (0),
// which would normally be rejected by pool balance checks, but should be accepted when disabled
let spend_value = fund_value - U256::from(21_000u64) * U256::from(gas_price_spend);
let tx2 = TransactionRequest::default()
.with_from(spender)
.with_to(funder)
.with_value(spend_value)
.with_gas_price(gas_price_spend);
let tx2 = WithOtherFields::new(tx2);

// Publish both transactions (funding first, then spend-before-funding-is-mined)
let sent1 = provider.send_transaction(tx1).await.unwrap();
let sent2 = provider.send_transaction(tx2).await.unwrap();

// Both should be accepted into the pool (pending)
let status = provider.txpool_status().await.unwrap();
assert_eq!(status.pending, 2);
assert_eq!(status.queued, 0);

// Mine a block and ensure both succeed
api.evm_mine(None).await.unwrap();

let receipt1 = sent1.get_receipt().await.unwrap();
let receipt2 = sent2.get_receipt().await.unwrap();
assert!(receipt1.status());
assert!(receipt2.status());
}
Loading