Skip to content

Commit

Permalink
fix(anvil): for deposit transactions apply tx.mint value in tx valida…
Browse files Browse the repository at this point in the history
…tion (#8704)

* apply mint value before checking sufficient funds conditions

* remove comment

* update log

* add docs link

* add docs link
  • Loading branch information
jakim929 authored Aug 21, 2024
1 parent ddb49a4 commit 503fbee
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 51 deletions.
2 changes: 1 addition & 1 deletion crates/anvil/core/src/eth/transaction/optimism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ impl DepositTransaction {
None
}

pub(crate) fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
pub fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
out.put_u8(DEPOSIT_TX_TYPE_ID);
self.encode(out);
}
Expand Down
34 changes: 25 additions & 9 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2573,16 +2573,32 @@ impl TransactionValidator for Backend {

let max_cost = tx.max_cost();
let value = tx.value();
// check sufficient funds: `gas * price + value`
let req_funds = max_cost.checked_add(value.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);

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 + deposit_tx.mint {
warn!(target: "backend", "[{:?}] insufficient balance={}, required={} account={:?}", tx.hash(), account.balance + deposit_tx.mint, value, *pending.sender());
return Err(InvalidTransactionError::InsufficientFunds);
}
}
_ => {
// check sufficient funds: `gas * price + value`
let req_funds = max_cost.checked_add(value.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
89 changes: 48 additions & 41 deletions crates/anvil/tests/it/optimism.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
//! Tests for OP chain support.

use crate::utils::http_provider_with_signer;
use crate::utils::{http_provider, http_provider_with_signer};
use alloy_eips::eip2718::Encodable2718;
use alloy_network::{EthereumWallet, TransactionBuilder};
use alloy_primitives::{b256, Address, TxHash, U256};
use alloy_primitives::{b256, Address, TxHash, TxKind, U256};
use alloy_provider::Provider;
use alloy_rpc_types::{optimism::OptimismTransactionFields, TransactionRequest};
use alloy_serde::WithOtherFields;
use anvil::{spawn, Hardfork, NodeConfig};
use anvil_core::eth::transaction::optimism::DepositTransaction;

#[tokio::test(flavor = "multi_thread")]
async fn test_deposits_not_supported_if_optimism_disabled() {
Expand Down Expand Up @@ -148,13 +149,11 @@ async fn test_send_value_raw_deposit_transaction() {
#[tokio::test(flavor = "multi_thread")]
async fn test_deposit_transaction_hash_matches_sepolia() {
// enable the Optimism flag
let (api, handle) =
let (_api, handle) =
spawn(NodeConfig::test().with_optimism(true).with_hardfork(Some(Hardfork::Paris))).await;

let accounts: Vec<_> = handle.dev_wallets().collect();
let signer: EthereumWallet = accounts[0].clone().into();
let sender_addr = accounts[0].address();

// https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
.parse::<TxHash>()
Expand All @@ -165,52 +164,60 @@ async fn test_deposit_transaction_hash_matches_sepolia() {
"7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
)
.unwrap();
let deposit_tx_from = "0x778F2146F48179643473B82931c4CD7B8F153eFd".parse::<Address>().unwrap();

let provider = http_provider_with_signer(&handle.http_endpoint(), signer.clone());

// TODO: necessary right now because transaction validation fails for deposit tx
// with `from` account that doesn't have sufficient ETH balance.
// Should update the tx validation logic for deposit tx to
// 1. check if `tx.value > account.balance + tx.mint`
// 2. don't check `account.balance > gas * price + value` (the gas costs have been prepaid on
// L1)
// source: https://specs.optimism.io/protocol/deposits.html#execution
let fund_account_tx = TransactionRequest::default()
.with_chain_id(31337)
.with_nonce(0)
.with_from(sender_addr)
.with_to(deposit_tx_from)
.with_value(U256::from(1e18))
.with_gas_limit(21_000)
.with_max_fee_per_gas(20_000_000_000)
.with_max_priority_fee_per_gas(1_000_000_000);

provider
.send_transaction(WithOtherFields::new(fund_account_tx))
let receipt = provider
.send_raw_transaction(raw_deposit_tx.as_slice())
.await
.unwrap()
.register()
.get_receipt()
.await
.unwrap();

// mine block
api.evm_mine(None).await.unwrap();
assert_eq!(receipt.transaction_hash, tx_hash);
}

let pending = provider
.send_raw_transaction(raw_deposit_tx.as_slice())
.await
.unwrap()
.register()
.await
.unwrap();
#[tokio::test(flavor = "multi_thread")]
async fn test_deposit_tx_checks_sufficient_funds_after_applying_deposited_value() {
// enable the Optimism flag
let (_api, handle) =
spawn(NodeConfig::test().with_optimism(true).with_hardfork(Some(Hardfork::Paris))).await;

// mine block
api.evm_mine(None).await.unwrap();
let provider = http_provider(&handle.http_endpoint());

let receipt =
provider.get_transaction_receipt(pending.tx_hash().to_owned()).await.unwrap().unwrap();
let sender = Address::random();
let recipient = Address::random();
let send_value = 1_000_000_000_u128;

assert_eq!(pending.tx_hash(), &tx_hash);
assert_eq!(receipt.transaction_hash, tx_hash);
let sender_prev_balance = provider.get_balance(sender).await.unwrap();
assert_eq!(sender_prev_balance, U256::from(0));

let recipient_prev_balance = provider.get_balance(recipient).await.unwrap();
assert_eq!(recipient_prev_balance, U256::from(0));

let deposit_tx = DepositTransaction {
source_hash: b256!("0000000000000000000000000000000000000000000000000000000000000000"),
from: sender,
nonce: 0,
kind: TxKind::Call(recipient),
mint: U256::from(send_value),
value: U256::from(send_value),
gas_limit: 21_000,
is_system_tx: false,
input: Vec::new().into(),
};

let mut tx_buffer = Vec::new();
deposit_tx.encode_2718(&mut tx_buffer);

provider.send_raw_transaction(&tx_buffer).await.unwrap().get_receipt().await.unwrap();

let sender_new_balance = provider.get_balance(sender).await.unwrap();
// sender should've sent the entire deposited value to recipient
assert_eq!(sender_new_balance, U256::from(0));

let recipient_new_balance = provider.get_balance(recipient).await.unwrap();
// recipient should've received the entire deposited value
assert_eq!(recipient_new_balance, U256::from(send_value));
}

0 comments on commit 503fbee

Please sign in to comment.