From e6b3b97f18fcefde53fc91adcb9666dc95e4dd1c Mon Sep 17 00:00:00 2001 From: Michael Zaikin Date: Wed, 26 Jun 2024 15:20:24 +0100 Subject: [PATCH] EVM: add FA bridge precompile and FA withdrawal execution Co-authored-by: Rodi-Can Bozman Co-authored-by: Valentin Chaboche --- etherlink/CHANGES_KERNEL.md | 1 + .../evm_execution/src/fa_bridge/mod.rs | 91 +++- .../evm_execution/src/fa_bridge/test_utils.rs | 81 +++- .../evm_execution/src/fa_bridge/tests.rs | 182 +++++++- .../evm_execution/src/fa_bridge/withdrawal.rs | 12 +- .../kernel_evm/evm_execution/src/handler.rs | 2 +- .../src/precompiles/fa_bridge.rs | 401 ++++++++++++++++++ .../evm_execution/src/precompiles/mod.rs | 6 + .../kernel_evm/evm_execution/src/storage.rs | 58 +++ 9 files changed, 821 insertions(+), 13 deletions(-) create mode 100644 etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index 4d919f194bb4..7c2200fdfe09 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -41,6 +41,7 @@ - Add FA withdrawal structure and helper methods for parsing and encoding. (!13843) - Rework the semantics of migrations in order to allow a network to skip frozen versions. (!13895) +- Add FA withdrawal execution methods and FA bridge precompile. (!13941) ## Version ec7c3b349624896b269e179384d0a45cf39e1145 diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs index ecb0fd75c732..70fbf0e6c7f8 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs @@ -40,11 +40,13 @@ use primitive_types::{H160, U256}; use tezos_ethereum::block::BlockConstants; use tezos_evm_logging::{log, Level::Info}; use ticket_table::{TicketTable, TICKET_TABLE_ACCOUNT}; +use withdrawal::FaWithdrawal; use crate::{ account_storage::EthereumAccountStorage, handler::{CreateOutcome, EvmHandler, ExecutionOutcome}, - precompiles::PrecompileBTreeMap, + precompiles::{PrecompileBTreeMap, PrecompileOutcome}, + storage::withdraw_nonce, transaction::TransactionContext, EthereumError, }; @@ -76,6 +78,10 @@ pub const TICKS_PER_FA_DEPOSIT_PARSING: u64 = 2_000_000; /// to execute a FA deposit. pub const FA_DEPOSIT_TOTAL_TICKS: u64 = 10_000_000; +/// TODO: Overapproximation of the amount of ticks for updating +/// the global ticket table, and emitting withdraw event. +pub const FA_WITHDRAWAL_INNER_TICKS: u64 = 3_000_000; + macro_rules! create_outcome_error { ($($arg:tt)*) => { (evm::ExitReason::Error(evm::ExitError::Other( @@ -157,6 +163,54 @@ pub fn execute_fa_deposit<'a, Host: Runtime>( Ok(outcome) } +/// Executes FA withdrawal. +/// +/// From the EVM perspective this is a precompile contract +/// call, that can be potentially an internal invocation from +/// another smart contract. +/// +/// We assume there is an open account storage transaction. +pub fn execute_fa_withdrawal( + handler: &mut EvmHandler, + caller: H160, + withdrawal: FaWithdrawal, +) -> Result { + log!( + handler.borrow_host(), + Info, + "Going to execute a {}", + withdrawal.display() + ); + + let mut withdrawals = Vec::with_capacity(1); + + let (mut exit_status, _, _) = inner_execute_withdrawal(handler, &withdrawal)?; + + // Withdrawal execution might fail because of non sufficient balance + // so we need to rollback the entire transaction in that case. + if exit_status.is_succeed() { + // In most cases sender is user's EOA and ticket owner is ERC wrapper contract + if withdrawal.ticket_owner != withdrawal.sender { + // If the proxy call fails we need to rollback the entire transaction + (exit_status, _, _) = inner_execute_proxy( + handler, + caller, + withdrawal.ticket_owner, + withdrawal.calldata(), + )?; + } + // Submit outbox message to the queue + withdrawals.push(withdrawal.into_outbox_message()) + } + + Ok(PrecompileOutcome { + exit_status, + withdrawals, + output: vec![], + estimated_ticks: FA_WITHDRAWAL_INNER_TICKS, + }) +} + /// Updates ticket table according to the deposit and actual ticket owner. /// Assuming there is an open account storage transaction. fn inner_execute_deposit( @@ -190,6 +244,41 @@ fn inner_execute_deposit( } } +/// Updates ticket ledger and outbox counter according to the withdrawal. +/// Assuming there is an open account storage transaction. +fn inner_execute_withdrawal( + handler: &mut EvmHandler, + withdrawal: &FaWithdrawal, +) -> Result { + // Updating the ticket table in accordance with the ownership. + let mut system = handler.get_or_create_account(TICKET_TABLE_ACCOUNT)?; + + if system.ticket_balance_remove( + handler.borrow_host(), + &withdrawal.ticket_hash, + &withdrawal.ticket_owner, + withdrawal.amount, + )? { + // NOTE that the nonce will remain incremented even if the precompile call fails. + // That is fine, since we only care about its uniqueness and determinism. + let withdrawal_id = withdraw_nonce::get_and_increment(handler.borrow_host()) + .map_err(|e| EthereumError::WrappedError(Cow::from(format!("{:?}", e))))?; + + handler + .add_log(withdrawal.event_log(withdrawal_id)) + .map_err(|e| EthereumError::WrappedError(Cow::from(format!("{:?}", e))))?; + + Ok((ExitReason::Succeed(evm::ExitSucceed::Stopped), None, vec![])) + } else { + Ok(create_outcome_error!( + "Insufficient ticket balance: {} of {} at {}", + withdrawal.amount, + withdrawal.ticket_hash, + withdrawal.ticket_owner + )) + } +} + /// Invokes proxy (ERC wrapper) contract from within a deposit or /// withdrawal handling function. /// Assuming there is an open account storage transaction. diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/test_utils.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/test_utils.rs index 7d76910145a7..7e6fd19954af 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/test_utils.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/test_utils.rs @@ -20,16 +20,17 @@ use tezos_smart_rollup_mock::MockHost; use crate::{ account_storage::{account_path, read_u256, EthereumAccountStorage}, - handler::ExecutionOutcome, - precompiles::precompile_set, + handler::{EvmHandler, ExecutionOutcome}, + precompiles::{self, precompile_set}, run_transaction, utilities::keccak256_hash, }; use super::{ deposit::{ticket_hash, FaDeposit}, - execute_fa_deposit, + execute_fa_deposit, execute_fa_withdrawal, ticket_table::{ticket_balance_path, TicketTable, TICKET_TABLE_ACCOUNT}, + withdrawal::FaWithdrawal, }; sol!( @@ -261,3 +262,77 @@ pub fn convert_log(log: &Log) -> alloy_primitives::LogData { log.data.clone().into(), ) } + +/// Create block constants +pub fn dummy_first_block() -> BlockConstants { + let block_fees = BlockFees::new( + U256::from(21000), + U256::from(21000), + U256::from(2_000_000_000_000u64), + ); + let gas_limit = 1u64; + BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + gas_limit, + H160::zero(), + ) +} + +/// Create FA withdrawal given ticket and sender/owner addresses +pub fn dummy_fa_withdrawal( + ticket: FA2_1Ticket, + sender: H160, + ticket_owner: H160, +) -> FaWithdrawal { + FaWithdrawal { + sender, + receiver: Contract::from_b58check("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU") + .unwrap(), + proxy: Contract::from_b58check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT").unwrap(), + amount: 42.into(), + ticket_hash: ticket_hash(&ticket).expect("Failed to calc ticket hash"), + ticket, + ticket_owner, + } +} + +/// Execute FA withdrawal directly without going through the precompile +pub fn fa_bridge_precompile_call_withdraw( + host: &mut MockHost, + evm_account_storage: &mut EthereumAccountStorage, + withdrawal: FaWithdrawal, + caller: H160, +) -> ExecutionOutcome { + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(); + let config = Config::shanghai(); + + let mut handler = EvmHandler::new( + host, + evm_account_storage, + caller, + &block, + &config, + &precompiles, + 1_000_000_000, + U256::from(21000), + false, + None, + ); + + handler.begin_initial_transaction(false, None).unwrap(); + + let res = execute_fa_withdrawal(&mut handler, caller, withdrawal); + + let execution_result = match res { + Ok(mut outcome) => { + handler.add_withdrawals(&mut outcome.withdrawals).unwrap(); + Ok((outcome.exit_status, None, vec![])) + } + Err(err) => Err(err), + }; + + handler.end_initial_transaction(execution_result).unwrap() +} diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs index cc2ce0ff4fd2..2b165b334a38 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: MIT +use alloy_primitives::FixedBytes; use alloy_sol_types::SolEvent; +use evm::{ExitError, ExitReason}; use primitive_types::{H160, U256}; use tezos_smart_rollup_mock::MockHost; @@ -10,9 +12,12 @@ use crate::{ account_storage::{account_path, init_account_storage}, fa_bridge::test_utils::{ convert_h160, convert_log, convert_u256, deploy_mock_wrapper, dummy_fa_deposit, - dummy_ticket, get_storage_flag, kernel_wrapper, run_fa_deposit, - ticket_balance_add, ticket_balance_get, token_wrapper, + dummy_fa_withdrawal, dummy_ticket, fa_bridge_precompile_call_withdraw, + get_storage_flag, kernel_wrapper, run_fa_deposit, ticket_balance_add, + ticket_balance_get, token_wrapper, }, + handler::ExtendedExitReason, + storage::withdraw_nonce, }; #[test] @@ -276,3 +281,176 @@ fn fa_deposit_proxy_state_reverted_if_ticket_balance_overflows() { U256::MAX ); } + +#[test] +fn fa_withdrawal_executed_via_l2_proxy_contract() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let sender = H160::from_low_u64_be(1); + let caller = H160::zero(); + let ticket = dummy_ticket(); + + let proxy = deploy_mock_wrapper( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &caller, + 0, + ) + .new_address + .unwrap(); + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &proxy, + withdrawal.amount, + ); + + let res = fa_bridge_precompile_call_withdraw( + &mut mock_runtime, + &mut evm_account_storage, + withdrawal, + caller, + ); + assert!(res.is_success()); + assert!(!res.withdrawals.is_empty()); + assert_eq!(2, res.logs.len()); + + // Re-create withdrawal struct + let withdrawal = dummy_fa_withdrawal(dummy_ticket(), sender, proxy); + + // Ensure proxy contract state changed + let flag = get_storage_flag(&mock_runtime, &evm_account_storage, proxy); + assert_eq!(withdrawal.amount.as_u32(), flag); + + // Ensure ticket balance reduced to zero (if not then will overflow) + assert!(ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &withdrawal.ticket_owner, + U256::MAX, + )); + + // Ensure events are emitted correctly + let withdrawal_event = + kernel_wrapper::Withdrawal::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Withdrawal event"); + + let burn_event = + token_wrapper::Burn::decode_log_data(&convert_log(&res.logs[1]), true) + .expect("Failed to parse Burn event"); + + assert_eq!( + withdrawal_event.ticketHash, + alloy_primitives::U256::from_be_bytes(withdrawal.ticket_hash.0) + ); + assert_eq!(withdrawal_event.sender, convert_h160(&sender)); + assert_eq!( + withdrawal_event.ticketOwner, + convert_h160(&withdrawal.ticket_owner) + ); + assert_eq!(withdrawal_event.receiver, FixedBytes::new([0u8; 22])); + assert_eq!(withdrawal_event.amount, convert_u256(&withdrawal.amount)); + assert_eq!(withdrawal_event.withdrawalId, convert_u256(&U256::from(0))); + + assert_eq!(burn_event.sender, convert_h160(&withdrawal.sender)); + assert_eq!(burn_event.amount, convert_u256(&withdrawal.amount)); + + // Ensure withdrawal counter is updated + assert_eq!( + U256::one(), + withdraw_nonce::get_and_increment(&mut mock_runtime).unwrap() + ); +} + +#[test] +fn fa_withdrawal_fails_due_to_faulty_l2_proxy() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let sender = H160::from_low_u64_be(1); + let caller = H160::zero(); + let ticket = dummy_ticket(); + let proxy = H160::from_low_u64_be(2); // non-existing contract + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &proxy, + withdrawal.amount, + ); + + let res = fa_bridge_precompile_call_withdraw( + &mut mock_runtime, + &mut evm_account_storage, + withdrawal, + caller, + ); + assert!(!res.is_success()); + assert!(res.withdrawals.is_empty()); + assert!(res.logs.is_empty()); + + // Re-create withdrawal struct + let withdrawal = dummy_fa_withdrawal(dummy_ticket(), sender, proxy); + + // Ensure ticket balance is non-zero (should overflow) + assert!(!ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &withdrawal.ticket_owner, + U256::MAX, + )); + + // Ensure withdrawal counter is updated + assert_eq!( + U256::one(), + withdraw_nonce::get_and_increment(&mut mock_runtime).unwrap() + ); + assert!( + matches!(res.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("Proxy contract does not exist")) + ); +} + +#[test] +fn fa_withdrawal_fails_due_to_insufficient_balance() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let sender = H160::from_low_u64_be(1); + let caller = H160::zero(); + let ticket = dummy_ticket(); + let proxy = H160::from_low_u64_be(2); // non-existing contract + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + let res = fa_bridge_precompile_call_withdraw( + &mut mock_runtime, + &mut evm_account_storage, + withdrawal, + caller, + ); + assert!(!res.is_success()); + assert!(res.withdrawals.is_empty()); + assert!(res.logs.is_empty()); + + // Ensure withdrawal counter is not updated (returned before incrementing the nonce) + assert_eq!( + U256::zero(), + withdraw_nonce::get_and_increment(&mut mock_runtime).unwrap() + ); + assert!( + matches!(res.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("Insufficient ticket balance")) + ); +} diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/withdrawal.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/withdrawal.rs index 07c7d7bae23b..93761977aa09 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/withdrawal.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/withdrawal.rs @@ -41,7 +41,7 @@ use tezos_smart_rollup_encoding::{ }; use crate::{ - abi::{self, ABI_B22_RIGHT_PADDING, ABI_H160_LEFT_PADDING, ABI_U32_LEFT_PADDING}, + abi::{self, ABI_B22_RIGHT_PADDING, ABI_H160_LEFT_PADDING}, handler::Withdrawal, utilities::keccak256_hash, }; @@ -144,7 +144,7 @@ impl FaWithdrawal { /// It also contains unique withdrawal identifier. /// /// Signature: Withdrawal(uint256,address,address,bytes22,uint256,uint256) - pub fn event_log(&self, withdrawal_id: u32) -> Log { + pub fn event_log(&self, withdrawal_id: U256) -> Log { let mut data = Vec::with_capacity(6 * 32); data.extend_from_slice(&ABI_H160_LEFT_PADDING); @@ -163,8 +163,8 @@ impl FaWithdrawal { data.extend_from_slice(&Into::<[u8; 32]>::into(self.amount)); debug_assert!(data.len() % 32 == 0); - data.extend_from_slice(&ABI_U32_LEFT_PADDING); - data.extend_from_slice(&withdrawal_id.to_be_bytes()); + data.extend_from_slice(&Into::<[u8; 32]>::into(withdrawal_id)); + debug_assert!(data.len() % 32 == 0); Log { address: H160::zero(), @@ -251,7 +251,7 @@ fn ticket_hash_from_raw_parts(ticketer: &[u8], content: &[u8]) -> H256 { #[cfg(test)] mod tests { use alloy_sol_types::{SolCall, SolEvent}; - use primitive_types::H160; + use primitive_types::{H160, U256}; use tezos_data_encoding::enc::BinWriter; use tezos_smart_rollup_encoding::contract::Contract; @@ -353,7 +353,7 @@ mod tests { fn fa_withdrawal_verify_eventlog_encoding() { let withdrawal = dummy_fa_withdrawal(); - let log = withdrawal.event_log(1); + let log = withdrawal.event_log(U256::one()); let withdrawal_event = kernel_wrapper::Withdrawal::decode_log_data(&convert_log(&log), true) diff --git a/etherlink/kernel_evm/evm_execution/src/handler.rs b/etherlink/kernel_evm/evm_execution/src/handler.rs index b3e898f54885..55c846c1a1f0 100644 --- a/etherlink/kernel_evm/evm_execution/src/handler.rs +++ b/etherlink/kernel_evm/evm_execution/src/handler.rs @@ -578,7 +578,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { } /// Add withdrawals to the current transaction layer - fn add_withdrawals( + pub fn add_withdrawals( &mut self, withdrawals: &mut Vec, ) -> Result<(), EthereumError> { diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs new file mode 100644 index 000000000000..12d34ff4131a --- /dev/null +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs @@ -0,0 +1,401 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +//! FA bridge precompiled contract. +//! +//! Provides users with EVM interface for: +//! * Submitting ticket withdrawal requests +//! +//! This is a stateful precompile: +//! * Alters ticket table (changes balance) +//! * Increments outbox counter + +use evm::{Context, Transfer}; +use primitive_types::H160; +use tezos_smart_rollup_host::runtime::Runtime; + +use crate::{ + fa_bridge::{execute_fa_withdrawal, withdrawal::FaWithdrawal}, + fail_if_too_much, + handler::EvmHandler, + EthereumError, +}; + +use super::{PrecompileOutcome, FA_BRIDGE_PRECOMPILE_ADDRESS}; + +/// TODO: Overapproximation of the amount of ticks for parsing +/// FA withdrawal from calldata, and checking transfer value. +pub const FA_WITHDRAWAL_OUTER_TICKS: u64 = 3_000_000; + +/// TODO: Cost of doing FA withdrawal excluding the gas consumed +/// by the inner proxy contract call. +pub const FA_WITHDRAWAL_OUTER_GAS_COST: u64 = 1500; + +macro_rules! precompile_outcome_error { + ($($arg:tt)*) => { + crate::precompiles::PrecompileOutcome { + exit_status: evm::ExitReason::Error(evm::ExitError::Other( + std::borrow::Cow::from(format!($($arg)*)) + )), + withdrawals: vec![], + output: vec![], + estimated_ticks: FA_WITHDRAWAL_OUTER_TICKS, + } + }; +} + +/// FA bridge precompile entrypoint. +#[allow(unused)] +pub fn fa_bridge_precompile( + handler: &mut EvmHandler, + input: &[u8], + context: &Context, + is_static: bool, + transfer: Option, +) -> Result { + fail_if_too_much!(FA_WITHDRAWAL_OUTER_TICKS, handler); + + if handler.record_cost(FA_WITHDRAWAL_OUTER_GAS_COST).is_err() { + return Ok(precompile_outcome_error!( + "FA withdrawal: gas limit too low" + )); + } + + if is_static { + // It is a STATICCALL that prevents storage modification + // see https://eips.ethereum.org/EIPS/eip-214 + return Ok(precompile_outcome_error!( + "FA withdrawal: static call not allowed" + )); + } + + if context.address != FA_BRIDGE_PRECOMPILE_ADDRESS { + // It is a DELEGATECALL or CALLCODE (deprecated) which can be impersonating + // see https://eips.ethereum.org/EIPS/eip-7 + return Ok(precompile_outcome_error!( + "FA withdrawal: delegate call not allowed" + )); + } + + if transfer + .as_ref() + .map(|t| !t.value.is_zero()) + .unwrap_or(false) + { + return Ok(precompile_outcome_error!( + "FA withdrawal: unexpected value transfer {:?}", + transfer + )); + } + + match input { + // "withdraw"'s selector | 4 first bytes of keccak256("withdraw(address,bytes,uint256,bytes22,bytes)") + [0x80, 0xfc, 0x1f, 0xe3, input_data @ ..] => { + // Withdrawal initiator is the precompile caller. + // NOTE that since we deny delegate calls, it can either be EOA or + // a smart contract that calls the precompile directly (e.g. AA wallet). + match FaWithdrawal::try_parse(input_data, context.caller) { + Ok(withdrawal) => { + // Using Zero account here so that the inner proxy call + // has the same sender as during the FA deposit + // (so that the proxy contract has a single admin). + execute_fa_withdrawal(handler, H160::zero(), withdrawal) + } + Err(err) => Ok(precompile_outcome_error!( + "FA withdrawal: parsing failed w/ `{err}`" + )), + } + } + _ => Ok(precompile_outcome_error!( + "FA withdrawal: unexpected selector" + )), + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use alloy_sol_types::SolCall; + use evm::{Config, ExitError, ExitReason}; + use primitive_types::{H160, U256}; + use tezos_smart_rollup_mock::MockHost; + + use crate::{ + account_storage::{init_account_storage, EthereumAccountStorage}, + fa_bridge::{ + deposit::ticket_hash, + test_utils::{ + convert_h160, convert_u256, dummy_first_block, dummy_ticket, + kernel_wrapper, set_balance, ticket_balance_add, ticket_id, + }, + }, + handler::{EvmHandler, ExecutionOutcome, ExtendedExitReason}, + precompiles::{self, PrecompileFn, FA_BRIDGE_PRECOMPILE_ADDRESS}, + transaction::TransactionContext, + utilities::{bigint_to_u256, keccak256_hash}, + }; + + use super::fa_bridge_precompile; + + fn execute_precompile( + host: &mut MockHost, + evm_account_storage: &mut EthereumAccountStorage, + caller: H160, + value: Option, + input: Vec, + gas_limit: Option, + is_static: bool, + ) -> ExecutionOutcome { + let block = dummy_first_block(); + let config = Config::shanghai(); + let callee = FA_BRIDGE_PRECOMPILE_ADDRESS; + + let mut precompiles = precompiles::precompile_set::(); + precompiles.insert( + FA_BRIDGE_PRECOMPILE_ADDRESS, + fa_bridge_precompile as PrecompileFn, + ); + + let mut handler = EvmHandler::new( + host, + evm_account_storage, + caller, + &block, + &config, + &precompiles, + 1_000_000_000, + U256::from(21000), + false, + None, + ); + + handler + .call_contract(caller, callee, value, input, gas_limit, is_static) + .expect("Failed to invoke precompile") + } + + #[test] + fn fa_bridge_precompile_fails_due_to_bad_selector() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + H160::from_low_u64_be(1), + None, + vec![0x00, 0x01, 0x02, 0x03], + None, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("unexpected selector")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_low_gas_limit() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + H160::from_low_u64_be(1), + None, + vec![0x80, 0xfc, 0x1f, 0xe3], + // Cover only basic cost + Some(21000 + 16 * 4), + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("gas limit too low")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_non_zero_value() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::from_low_u64_be(1); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + 1_000_000_000.into(), + ); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + caller, + Some(1_000_000_000.into()), + vec![0x80, 0xfc, 0x1f, 0xe3], + None, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("unexpected value transfer")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_static_call() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::from_low_u64_be(1); + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + caller, + None, + vec![0x80, 0xfc, 0x1f, 0xe3], + None, + true, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("static call not allowed")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_delegate_call() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::from_low_u64_be(1); + let callee = H160::from_low_u64_be(2); + let block = dummy_first_block(); + let config = Config::shanghai(); + + let mut precompiles = precompiles::precompile_set::(); + precompiles.insert( + FA_BRIDGE_PRECOMPILE_ADDRESS, + fa_bridge_precompile as PrecompileFn, + ); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + 1_000_000_000, + U256::from(21000), + false, + None, + ); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call( + FA_BRIDGE_PRECOMPILE_ADDRESS, + None, + vec![0x80, 0xfc, 0x1f, 0xe3], + TransactionContext::new(caller, callee, U256::zero()), + ); + + let outcome = handler.end_initial_transaction(result).unwrap(); + + assert!(!outcome.is_success()); + assert!( + matches!(outcome.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("delegate call not allowed")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_invalid_input() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + H160::from_low_u64_be(1), + None, + vec![0x80, 0xfc, 0x1f, 0xe3], + None, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.reason, ExtendedExitReason::Exit(ExitReason::Error(ExitError::Other(err))) if err.contains("parsing failed")) + ); + } + + #[test] + fn fa_bridge_precompile_succeeds_without_l2_proxy_contract() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let ticket_owner = H160::from_low_u64_be(1); + let ticket = dummy_ticket(); + let ticket_hash = ticket_hash(&ticket).unwrap(); + let amount = bigint_to_u256(ticket.amount()).unwrap(); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &ticket_hash, + &ticket_owner, + amount, + ); + + let (ticketer, content) = ticket_id(&ticket); + + let routing_info = [ + [0u8; 22].to_vec(), + vec![0x01], + [0u8; 20].to_vec(), + vec![0x00], + ] + .concat(); + + let input = kernel_wrapper::withdrawCall::new(( + convert_h160(&ticket_owner), + routing_info.into(), + convert_u256(&amount), + ticketer.into(), + content.into(), + )) + .abi_encode(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + ticket_owner, + None, + input, + Some(40000), + false, + ); + assert!(outcome.is_success()); + assert_eq!(1, outcome.withdrawals.len()); + assert_eq!(1, outcome.logs.len()); + } + + #[test] + fn fa_bridge_precompile_address() { + assert_eq!( + FA_BRIDGE_PRECOMPILE_ADDRESS, + H160::from_str("ff00000000000000000000000000000000000002").unwrap() + ); + } + + #[test] + fn fa_bridge_precompile_withdraw_method_id() { + let method_hash = + keccak256_hash(b"withdraw(address,bytes,uint256,bytes22,bytes)"); + assert_eq!(method_hash.0[0..4], [0x80, 0xfc, 0x1f, 0xe3]); + } +} diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs index ece31c5d6071..37a6cf61cb0e 100644 --- a/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs @@ -15,6 +15,7 @@ use std::vec; mod blake2; mod ecdsa; +mod fa_bridge; mod hash; mod identity; mod modexp; @@ -35,6 +36,11 @@ use primitive_types::H160; use withdrawal::withdrawal_precompile; use zero_knowledge::{ecadd_precompile, ecmul_precompile, ecpairing_precompile}; +/// FA bridge precompile address +pub const FA_BRIDGE_PRECOMPILE_ADDRESS: H160 = H160([ + 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, +]); + /// Outcome of executing a precompiled contract. Covers both successful /// return, stop and revert and additionally, it covers contract execution /// failures (malformed input etc.). This is encoded using the `ExitReason` diff --git a/etherlink/kernel_evm/evm_execution/src/storage.rs b/etherlink/kernel_evm/evm_execution/src/storage.rs index 7a5325b9ac1c..b9f1968c8cd2 100644 --- a/etherlink/kernel_evm/evm_execution/src/storage.rs +++ b/etherlink/kernel_evm/evm_execution/src/storage.rs @@ -235,3 +235,61 @@ pub mod blocks { } } } + +// API to interact with the withdraw nonce storage +pub mod withdraw_nonce { + use host::{path::RefPath, runtime::Runtime}; + use primitive_types::U256; + + use crate::account_storage::{read_u256, write_u256, AccountStorageError}; + + pub const WITHDRAW_NONCE_PATH: RefPath = + RefPath::assert_from(b"/evm/world_state/withdraw_nonce"); + + /// All errors that may happen as result of using this storage interface. + #[derive(thiserror::Error, Debug, PartialEq)] + pub enum WithdrawNonceStorageError { + #[error("Runtime error: {0:?}")] + RuntimeError(#[from] host::runtime::RuntimeError), + #[error("Nonce overflow")] + NonceOverflow, + #[error("Failed to read: {0:?}")] + StorageError(#[from] AccountStorageError), + } + + /// Returns current nonce from the storage (or 0 if it's not initialized) + /// and increments & store the new value (will fail in case of overflow). + pub fn get_and_increment( + host: &mut impl Runtime, + ) -> Result { + let old_value = read_u256(host, &WITHDRAW_NONCE_PATH, U256::zero())?; + let new_value = old_value + .checked_add(U256::one()) + .ok_or(WithdrawNonceStorageError::NonceOverflow)?; + + write_u256(host, &WITHDRAW_NONCE_PATH, new_value)?; + Ok(old_value) + } + + #[cfg(test)] + mod tests { + use host::runtime::{Runtime, ValueType}; + use primitive_types::U256; + use tezos_smart_rollup_mock::MockHost; + + use crate::storage::withdraw_nonce::WITHDRAW_NONCE_PATH; + + use super::get_and_increment; + + #[test] + fn withdraw_nonce_initializes_and_increments() { + let mut mock_host = MockHost::default(); + assert_eq!(U256::zero(), get_and_increment(&mut mock_host).unwrap()); + assert_eq!( + mock_host.store_has(&WITHDRAW_NONCE_PATH).unwrap(), + Some(ValueType::Value) + ); + assert_eq!(U256::one(), get_and_increment(&mut mock_host).unwrap()); + } + } +} -- GitLab