From 6e0686b0fb511849b32ecec3ff4abcb192e4c405 Mon Sep 17 00:00:00 2001 From: Michael Zaikin Date: Thu, 13 Jun 2024 20:42:49 +0100 Subject: [PATCH] EVM: add methods for FA deposit execution --- etherlink/CHANGES_KERNEL.md | 1 + etherlink/kernel_evm/Cargo.lock | 212 ++++++++++++- etherlink/kernel_evm/Cargo.toml | 4 + etherlink/kernel_evm/evm_execution/Cargo.toml | 12 +- .../evm_execution/src/fa_bridge/mod.rs | 189 ++++++++++++ .../evm_execution/src/fa_bridge/test_utils.rs | 263 +++++++++++++++++ .../evm_execution/src/fa_bridge/tests.rs | 278 ++++++++++++++++++ .../src/fa_bridge/ticket_table.rs | 5 +- .../kernel_evm/evm_execution/src/handler.rs | 37 ++- 9 files changed, 966 insertions(+), 35 deletions(-) create mode 100644 etherlink/kernel_evm/evm_execution/src/fa_bridge/test_utils.rs create mode 100644 etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index 7f2bc0caf9f3..a195ce828008 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -13,6 +13,7 @@ ## Internal - Add FA deposit structure and helper methods for its parsing and formatting. (!13720) +- Add FA deposit execution methods. (!13773) - Add ticket table to account for FA deposits. (!12072) - Refactor withdrawals handling to keep `OutboxMessage` in the `ExecutionOutcome`. (!13751) diff --git a/etherlink/kernel_evm/Cargo.lock b/etherlink/kernel_evm/Cargo.lock index b1df97feeda2..d260f2a10d6a 100644 --- a/etherlink/kernel_evm/Cargo.lock +++ b/etherlink/kernel_evm/Cargo.lock @@ -17,6 +17,105 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloy-json-abi" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaeaccd50238126e3a0ff9387c7c568837726ad4f4e399b528ca88104d6c25ef" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f783611babedbbe90db3478c120fb5f5daacceffc210b39adc0af4fe0da70bad" +dependencies = [ + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "ruint", + "serde", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bad41a7c19498e3f6079f7744656328699f8ea3e783bdd10d85788cd439f572" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9899da7d011b4fe4c406a524ed3e3f963797dbc93b45479d60341d3a27b252" +dependencies = [ + "alloy-json-abi", + "alloy-sol-macro-input", + "const-hex", + "heck 0.5.0", + "indexmap 2.2.6", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.66", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32d595768fdc61331a132b6f65db41afae41b9b97d36c21eb1b955c422a7e60" +dependencies = [ + "alloy-json-abi", + "const-hex", + "dunce", + "heck 0.5.0", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.66", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa2fbd22d353d8685bd9fee11ba2d8b5c3b1d11e56adb3265fcf1f32bfdf404" +dependencies = [ + "winnow 0.6.13", +] + +[[package]] +name = "alloy-sol-types" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a49042c6d3b66a9fe6b2b5a8bf0d39fc2ae1ee0310a2a26ffedd79fb097878dd" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "const-hex", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -188,6 +287,9 @@ name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -222,6 +324,25 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5241cd7938b1b415942e943ea96f615953d500b50347b505b0b507080bad5a6f" +[[package]] +name = "const-hex" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8a24a26d37e1ffd45343323dc9fe6654ceea44c12f2fcb3d7ac29e610bc6" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -333,8 +454,10 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version", "syn 1.0.109", ] @@ -393,6 +516,12 @@ dependencies = [ "libc", ] +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "ecdsa" version = "0.12.4" @@ -577,6 +706,8 @@ dependencies = [ name = "evm-execution" version = "0.1.0" dependencies = [ + "alloy-primitives", + "alloy-sol-types", "aurora-engine-modexp", "const-decoder", "evm", @@ -818,6 +949,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit" version = "0.7.2" @@ -1311,6 +1448,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1398,20 +1541,19 @@ dependencies = [ [[package]] name = "proptest" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", - "bitflags 1.3.2", - "byteorder", + "bit-vec", + "bitflags 2.4.2", "lazy_static", "num-traits", - "quick-error 2.0.1", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.6.28", + "regex-syntax 0.8.2", "rusty-fork", "tempfile", "unarray", @@ -1423,12 +1565,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" version = "1.0.36" @@ -1628,6 +1764,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ruint" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" +dependencies = [ + "proptest", + "rand 0.8.5", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -1699,7 +1855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", - "quick-error 1.2.3", + "quick-error", "tempfile", "wait-timeout", ] @@ -1910,7 +2066,7 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro-error", "proc-macro2", "quote", @@ -1929,7 +2085,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro2", "quote", "syn 1.0.109", @@ -1976,6 +2132,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d71e19bca02c807c9faa67b5a47673ff231b6e7449b251695188522f1dc44b2" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "tap" version = "1.0.1" @@ -2349,7 +2517,7 @@ checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap 1.9.3", "toml_datetime", - "winnow", + "winnow 0.4.1", ] [[package]] @@ -2458,6 +2626,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vec_map" version = "0.8.2" @@ -2766,6 +2940,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" + [[package]] name = "wyz" version = "0.5.1" diff --git a/etherlink/kernel_evm/Cargo.toml b/etherlink/kernel_evm/Cargo.toml index ebb73676e952..9113c8e09e47 100644 --- a/etherlink/kernel_evm/Cargo.toml +++ b/etherlink/kernel_evm/Cargo.toml @@ -73,3 +73,7 @@ tezos-smart-rollup-storage = { path = "../../src/kernel_sdk/storage" } rand = { version = "0.8" } proptest = { version = "1.0" } pretty_assertions = { version = "1.4.0" } + +# alloy +alloy-sol-types = { version = "0.7.6", default-features = false, features = ["json"]} +alloy-primitives = { version = "0.7.6", default-features = false } diff --git a/etherlink/kernel_evm/evm_execution/Cargo.toml b/etherlink/kernel_evm/evm_execution/Cargo.toml index 677cdf348e64..b0bb1bf587b4 100644 --- a/etherlink/kernel_evm/evm_execution/Cargo.toml +++ b/etherlink/kernel_evm/evm_execution/Cargo.toml @@ -52,13 +52,21 @@ tezos_data_encoding.workspace = true rand = { workspace = true, optional = true } proptest = { workspace = true, optional = true } +# Enabled when testing feature is on +tezos-smart-rollup-mock = { workspace = true, optional = true } +alloy-sol-types = { workspace = true, optional = true } +alloy-primitives = { workspace = true, optional = true } + [dev-dependencies] -tezos-smart-rollup-mock.workspace = true pretty_assertions.workspace = true +tezos-smart-rollup-mock.workspace = true +alloy-sol-types.workspace = true +alloy-primitives.workspace = true + [features] default = ["evm_execution"] -testing = ["rand", "proptest"] +testing = ["rand", "proptest", "dep:tezos-smart-rollup-mock", "dep:alloy-primitives", "dep:alloy-sol-types"] evm_execution = [] debug = ["tezos-evm-logging/debug"] # the `benchmark` and `benchmark-opcodes` feature flags instrument the kernel for profiling 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 0baa0e6081d9..e4013a403a98 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs @@ -31,6 +31,195 @@ //! using the transactional Eth account storage, so that they are discarded //! in case of a revert/failure. +use std::borrow::Cow; + +use deposit::FaDeposit; +use evm::{Config, ExitReason}; +use host::runtime::Runtime; +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 crate::{ + account_storage::EthereumAccountStorage, + handler::{CreateOutcome, EvmHandler, ExecutionOutcome}, + precompiles::PrecompileBTreeMap, + transaction::TransactionContext, + EthereumError, +}; + pub mod deposit; pub mod error; pub mod ticket_table; + +#[cfg(test)] +mod tests; + +#[cfg(any(test, feature = "testing"))] +pub mod test_utils; + +/// TODO: Gas limit for calling "deposit" method of the proxy contract call. +/// Since we cannot control a particular destination, +/// we need to make sure there's no DoS attack vector. +pub const FA_DEPOSIT_PROXY_GAS_LIMIT: u64 = 1_200_000; + +/// TODO: Overapproximation of the amount of ticks for updating +/// the global ticket table and emitting deposit event. +pub const FA_DEPOSIT_INNER_TICKS: u64 = 2_000_000; + +/// TODO: Overapproximation of the amount of ticks required +/// to execute a FA deposit. +pub const FA_DEPOSIT_TOTAL_TICKS: u64 = 10_000_000; + +macro_rules! create_outcome_error { + ($($arg:tt)*) => { + (evm::ExitReason::Error(evm::ExitError::Other( + std::borrow::Cow::from(format!($($arg)*)) + )), None, vec![]) + }; +} + +/// Executes FA deposit. +/// +/// From the EVM perspective this is a "system contract" call, +/// that tries to perform an internal invocation of the proxy +/// contract, and emits an additional deposit event. +/// +/// This method can only be called by the kernel, not by any +/// other contract. Therefore we assume there is no open +/// account storage transaction, and we can open one. +#[allow(clippy::too_many_arguments)] +pub fn execute_fa_deposit<'a, Host: Runtime>( + host: &'a mut Host, + block: &'a BlockConstants, + evm_account_storage: &'a mut EthereumAccountStorage, + precompiles: &'a PrecompileBTreeMap, + config: Config, + caller: H160, + deposit: &FaDeposit, + allocated_ticks: u64, +) -> Result { + log!(host, Info, "Going to execute a {}", deposit.display()); + + let mut handler = EvmHandler::<'_, Host>::new( + host, + evm_account_storage, + caller, + block, + &config, + precompiles, + allocated_ticks, + block.base_fee_per_gas(), + // Warm-cold access only used for evaluation (for checking EVM compatibility), but not in production + false, + None, + ); + + handler.begin_initial_transaction(false, Some(FA_DEPOSIT_PROXY_GAS_LIMIT))?; + + // It's ok if internal proxy call fails, we will update the ticket table anyways. + let ticket_owner = if let Some(proxy) = deposit.proxy { + let (exit_reason, _, _) = + inner_execute_proxy(&mut handler, caller, proxy, deposit.calldata())?; + // If proxy contract call succeeded, proxy becomes the owner, + // otherwise we fall back and set the receiver as the owner instead. + if exit_reason.is_succeed() { + proxy + } else { + log!( + handler.borrow_host(), + Info, + "FA deposit: proxy call failed w/ {:?}", + exit_reason + ); + deposit.receiver + } + } else { + // Proxy contract is not specified + deposit.receiver + }; + + // Deposit execution might fail because of the balance overflow + // so we need to rollback the entire transaction in that case. + let deposit_res = inner_execute_deposit(&mut handler, ticket_owner, deposit); + + let mut outcome = handler.end_initial_transaction(deposit_res)?; + + // Adjust resource consumption to account for the outer transaction + outcome.gas_used += config.gas_transaction_call; + outcome.estimated_ticks_used += FA_DEPOSIT_INNER_TICKS; + + Ok(outcome) +} + +/// Updates ticket table according to the deposit and actual ticket owner. +/// Assuming there is an open account storage transaction. +fn inner_execute_deposit( + handler: &mut EvmHandler, + ticket_owner: H160, + deposit: &FaDeposit, +) -> 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_add( + handler.borrow_host(), + &deposit.ticket_hash, + &ticket_owner, + deposit.amount, + )? { + handler + .add_log(deposit.event_log(&ticket_owner)) + .map_err(|e| EthereumError::WrappedError(Cow::from(format!("{:?}", e))))?; + Ok(( + ExitReason::Succeed(evm::ExitSucceed::Returned), + None, + vec![], + )) + } else { + Ok(create_outcome_error!( + "Ticket table balance overflow: {} at {}", + deposit.ticket_hash, + ticket_owner + )) + } +} + +/// Invokes proxy (ERC wrapper) contract from within a deposit or +/// withdrawal handling function. +/// Assuming there is an open account storage transaction. +fn inner_execute_proxy( + handler: &mut EvmHandler, + caller: H160, + proxy: H160, + input: Vec, +) -> Result { + // We need to check that the proxy contract exists and has code, + // because otherwise the inner call will succeed although without + // any effect. + // + // Of course, we cannot protect from cases where proxy contract + // executes without errors, but does not actually update the ledger. + // At very least we can protect from typos and other mistakes. + if let Some(account) = handler.get_account(proxy)? { + if let Ok(true) = account.code_exists(handler.borrow_host()) { + handler.execute_call( + proxy, + None, + input, + TransactionContext::new(caller, proxy, U256::zero()), + ) + } else { + Ok(create_outcome_error!( + "Proxy contract does not have code: {}", + proxy + )) + } + } else { + Ok(create_outcome_error!( + "Proxy contract does not exist: {}", + proxy + )) + } +} 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 new file mode 100644 index 000000000000..7d76910145a7 --- /dev/null +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/test_utils.rs @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +use alloy_sol_types::{sol, SolConstructor}; +use crypto::hash::ContractKt1Hash; +use evm::Config; +use host::runtime::Runtime; +use primitive_types::{H160, H256, U256}; +use tezos_data_encoding::enc::BinWriter; +use tezos_ethereum::{ + block::{BlockConstants, BlockFees}, + Log, +}; +use tezos_smart_rollup_encoding::{ + contract::Contract, + michelson::{ticket::FA2_1Ticket, MichelsonOption, MichelsonPair}, +}; +use tezos_smart_rollup_mock::MockHost; + +use crate::{ + account_storage::{account_path, read_u256, EthereumAccountStorage}, + handler::ExecutionOutcome, + precompiles::precompile_set, + run_transaction, + utilities::keccak256_hash, +}; + +use super::{ + deposit::{ticket_hash, FaDeposit}, + execute_fa_deposit, + ticket_table::{ticket_balance_path, TicketTable, TICKET_TABLE_ACCOUNT}, +}; + +sol!( + token_wrapper, + "tests/contracts/artifacts/MockFaBridgeWrapper.abi" +); +sol!( + kernel_wrapper, + "tests/contracts/artifacts/MockFaBridgePrecompile.abi" +); + +const MOCK_WRAPPER_BYTECODE: &[u8] = + include_bytes!("../../tests/contracts/artifacts/MockFaBridgeWrapper.bytecode"); + +/// Create a smart contract in the storage with the mocked token code +pub fn deploy_mock_wrapper( + host: &mut MockHost, + evm_account_storage: &mut EthereumAccountStorage, + ticket: &FA2_1Ticket, + caller: &H160, + flag: u32, +) -> ExecutionOutcome { + let code = MOCK_WRAPPER_BYTECODE.to_vec(); + let (ticketer, content) = ticket_id(ticket); + let calldata = token_wrapper::constructorCall::new(( + ticketer.into(), + content.into(), + caller.0.into(), + convert_u256(&U256::from(flag)), + )); + + let block = dummy_block_constants(); + let precompiles = precompile_set::(); + + set_balance(host, evm_account_storage, caller, U256::from(1_000_000)); + run_transaction( + host, + &block, + evm_account_storage, + &precompiles, + Config::shanghai(), + None, + *caller, + [code, calldata.abi_encode()].concat(), + Some(300_000), + U256::one(), + None, + false, + 1_000_000_000, + false, + false, + None, + ) + .expect("Failed to deploy") + .unwrap() +} + +/// Execute FA deposit +pub fn run_fa_deposit( + host: &mut MockHost, + evm_account_storage: &mut EthereumAccountStorage, + deposit: &FaDeposit, + caller: &H160, +) -> ExecutionOutcome { + let block = dummy_block_constants(); + let precompiles = precompile_set::(); + + execute_fa_deposit( + host, + &block, + evm_account_storage, + &precompiles, + Config::shanghai(), + *caller, + deposit, + 1_000_000_000, + ) + .expect("Failed to execute deposit") +} + +/// Create FA deposit given ticket and proxy address (optional) +pub fn dummy_fa_deposit(ticket: FA2_1Ticket, proxy: Option) -> FaDeposit { + FaDeposit { + ticket_hash: ticket_hash(&ticket).expect("Failed to calc ticket hash"), + proxy, + amount: 42.into(), + receiver: H160([4u8; 20]), + inbox_level: 0, + inbox_msg_id: 0, + } +} + +/// Get value of a specific slot in the proxy contract storage +/// It is used to determine if the said contract was called +/// +/// See MockFaBridgeWrapper.sol where the flag is set: +/// +/// function setFlag(uint256 value) internal { +/// bytes32 slot = keccak256(abi.encodePacked("FLAG_TAG")); +/// assembly { +/// sstore(slot, value) +/// } +/// } +pub fn get_storage_flag( + host: &MockHost, + evm_account_storage: &EthereumAccountStorage, + proxy: H160, +) -> u32 { + let proxy_account = evm_account_storage + .get(host, &account_path(&proxy).unwrap()) + .unwrap() + .unwrap(); + + let flag = proxy_account + .get_storage(host, &keccak256_hash(b"FLAG_TAG")) + .unwrap(); + U256::from_big_endian(&flag.0).as_u32() +} + +/// Block constants for testing +pub fn dummy_block_constants() -> 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(), + ) +} + +/// Provision ticket balance for a specified account +pub fn ticket_balance_add( + host: &mut impl Runtime, + evm_account_storage: &mut EthereumAccountStorage, + ticket_hash: &H256, + address: &H160, + balance: U256, +) -> bool { + let mut system = evm_account_storage + .get_or_create(host, &account_path(&TICKET_TABLE_ACCOUNT).unwrap()) + .unwrap(); + system + .ticket_balance_add(host, ticket_hash, address, balance) + .unwrap() +} + +/// Get ticket balance for a specified account +pub fn ticket_balance_get( + host: &impl Runtime, + evm_account_storage: &EthereumAccountStorage, + ticket_hash: &H256, + address: &H160, +) -> U256 { + let system = evm_account_storage + .get(host, &account_path(&TICKET_TABLE_ACCOUNT).unwrap()) + .unwrap() + .unwrap(); + + let path = system + .custom_path(&ticket_balance_path(ticket_hash, address).unwrap()) + .unwrap(); + read_u256(host, &path, U256::zero()).unwrap() +} + +/// Provision TEZ balance for a specified account +pub fn set_balance( + host: &mut impl Runtime, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + balance: U256, +) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + let current_balance = account.balance(host).unwrap(); + if current_balance > balance { + account + .balance_remove(host, current_balance - balance) + .unwrap(); + } else { + account + .balance_add(host, balance - current_balance) + .unwrap(); + } +} + +/// Create ticket with dummy creator and content +pub fn dummy_ticket() -> FA2_1Ticket { + let ticketer = ContractKt1Hash([1u8; 20].to_vec()); + FA2_1Ticket::new( + Contract::from_b58check(&ticketer.to_base58_check()).unwrap(), + MichelsonPair(0.into(), MichelsonOption(None)), + 1i32, + ) + .expect("Failed to construct ticket") +} + +/// Return ticket creator and content in forged form +pub fn ticket_id(ticket: &FA2_1Ticket) -> ([u8; 22], Vec) { + let mut ticketer = Vec::new(); + ticket.creator().0.bin_write(&mut ticketer).unwrap(); + + let mut content = Vec::new(); + ticket.contents().bin_write(&mut content).unwrap(); + + (ticketer.try_into().unwrap(), content) +} + +/// Convert U256 to the alloy primitive type +pub fn convert_u256(value: &U256) -> alloy_primitives::U256 { + alloy_primitives::U256::from_limbs(value.0) +} + +/// Convert H160 to the alloy primitive type +pub fn convert_h160(value: &H160) -> alloy_primitives::Address { + alloy_primitives::Address::from_slice(&value.0) +} + +/// Convert EVM Log to the alloy primitive type +pub fn convert_log(log: &Log) -> alloy_primitives::LogData { + alloy_primitives::LogData::new_unchecked( + log.topics.iter().map(|x| x.0.into()).collect(), + log.data.clone().into(), + ) +} diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs new file mode 100644 index 000000000000..3d572f33e197 --- /dev/null +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/tests.rs @@ -0,0 +1,278 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +use alloy_sol_types::SolEvent; +use primitive_types::{H160, U256}; +use tezos_smart_rollup_mock::MockHost; + +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, + }, +}; + +#[test] +fn fa_deposit_reached_wrapper_contract() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + 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 deposit = dummy_fa_deposit(ticket, Some(proxy)); + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + ); + assert!(res.is_success); + assert_eq!(2, res.logs.len()); + + let flag = get_storage_flag(&mock_runtime, &evm_account_storage, proxy); + assert_eq!(deposit.amount.as_u32(), flag); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + U256::zero() + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &proxy + ), + deposit.amount + ); + + let mint_event = + token_wrapper::Mint::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Mint event"); + let deposit_event = + kernel_wrapper::Deposit::decode_log_data(&convert_log(&res.logs[1]), true) + .expect("Failed to parse Deposit event"); + + assert_eq!(mint_event.amount, convert_u256(&deposit.amount)); + assert_eq!(mint_event.receiver, convert_h160(&deposit.receiver)); + + assert_eq!( + deposit_event.ticketHash, + convert_u256(&U256::from(deposit.ticket_hash.as_bytes())) + ); + assert_eq!( + deposit_event.ticketOwner, + convert_h160(&deposit.proxy.unwrap()) + ); // ticket owner is now wrapper contract + assert_eq!(deposit_event.receiver, convert_h160(&deposit.receiver)); + assert_eq!(deposit_event.amount, convert_u256(&deposit.amount)); + assert_eq!( + deposit_event.inboxLevel, + convert_u256(&U256::from(deposit.inbox_level)) + ); + assert_eq!( + deposit_event.inboxMsgId, + convert_u256(&U256::from(deposit.inbox_msg_id)) + ); +} + +#[test] +fn fa_deposit_refused_due_non_existing_contract() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let ticket = dummy_ticket(); + let deposit = dummy_fa_deposit(ticket, Some(H160([1u8; 20]))); + + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + ); + assert_eq!(1, res.logs.len()); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + deposit.amount + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.proxy.unwrap() + ), + U256::zero() + ); + + let deposit_event = + kernel_wrapper::Deposit::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Deposit event"); + + assert_eq!( + deposit_event.ticketHash, + convert_u256(&U256::from(deposit.ticket_hash.as_bytes())) + ); + assert_eq!(deposit_event.ticketOwner, convert_h160(&deposit.receiver)); // ticket owner is deposit receiver + assert_eq!(deposit_event.receiver, convert_h160(&deposit.receiver)); + assert_eq!(deposit_event.amount, convert_u256(&deposit.amount)); + assert_eq!( + deposit_event.inboxLevel, + convert_u256(&U256::from(deposit.inbox_level)) + ); + assert_eq!( + deposit_event.inboxMsgId, + convert_u256(&U256::from(deposit.inbox_msg_id)) + ); +} + +#[test] +fn fa_deposit_refused_non_compatible_interface() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let proxy = H160([1u8; 20]); + let ticket = dummy_ticket(); + let deposit = dummy_fa_deposit(ticket, Some(proxy)); + + // Making it look as a smart contract + let mut account = evm_account_storage + .get_or_create(&mock_runtime, &account_path(&proxy).unwrap()) + .unwrap(); + account.set_code(&mut mock_runtime, &[255u8; 1024]).unwrap(); + + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + ); + assert_eq!(1, res.logs.len()); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + deposit.amount + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.proxy.unwrap() + ), + U256::zero() + ); + + let deposit_event = + kernel_wrapper::Deposit::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Deposit event"); + + assert_eq!( + deposit_event.ticketHash, + convert_u256(&U256::from(deposit.ticket_hash.as_bytes())) + ); + assert_eq!(deposit_event.ticketOwner, convert_h160(&deposit.receiver)); // ticket owner is deposit receiver + assert_eq!(deposit_event.receiver, convert_h160(&deposit.receiver)); + assert_eq!(deposit_event.amount, convert_u256(&deposit.amount)); + assert_eq!( + deposit_event.inboxLevel, + convert_u256(&U256::from(deposit.inbox_level)) + ); + assert_eq!( + deposit_event.inboxMsgId, + convert_u256(&U256::from(deposit.inbox_msg_id)) + ); +} + +#[test] +fn fa_deposit_proxy_state_reverted_if_ticket_balance_overflows() { + let mut mock_runtime = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let ticket = dummy_ticket(); + + let proxy = deploy_mock_wrapper( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &caller, + 100500, + ) + .new_address + .unwrap(); + + let deposit = dummy_fa_deposit(ticket, Some(proxy)); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &deposit.ticket_hash, + &proxy, + U256::MAX, + ); + + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + ); + assert!(!res.is_success); + assert!(res.logs.is_empty()); + + let flag = get_storage_flag(&mock_runtime, &evm_account_storage, proxy); + assert_eq!(100500, flag); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + U256::zero() + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.proxy.unwrap() + ), + U256::MAX + ); +} diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/ticket_table.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/ticket_table.rs index dc76fa3c87e5..9a0510f95ddb 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/ticket_table.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/ticket_table.rs @@ -21,6 +21,9 @@ use crate::account_storage::{ /// Path where global ticket table is stored const TICKET_TABLE_PATH: RefPath = RefPath::assert_from(b"/ticket_table"); +/// Global ticket table belongs to the zero account (system) +pub const TICKET_TABLE_ACCOUNT: H160 = H160::zero(); + pub trait TicketTable { /// Increases ticket balance fn ticket_balance_add( @@ -41,7 +44,7 @@ pub trait TicketTable { ) -> Result; } -fn ticket_balance_path( +pub(crate) fn ticket_balance_path( ticket_hash: &H256, address: &H160, ) -> Result { diff --git a/etherlink/kernel_evm/evm_execution/src/handler.rs b/etherlink/kernel_evm/evm_execution/src/handler.rs index 067ff0b182f4..d91143959e15 100644 --- a/etherlink/kernel_evm/evm_execution/src/handler.rs +++ b/etherlink/kernel_evm/evm_execution/src/handler.rs @@ -117,7 +117,7 @@ pub enum Precondition { /// be able to use the `end_xxx_transaction` functions for both contract -create and /// -call. In this case, the last element of the triple can be non-empty, and the /// address will be `None`. -type CreateOutcome = (ExitReason, Option, Vec); +pub(crate) type CreateOutcome = (ExitReason, Option, Vec); /// Wrap ethereum errors in the SputnikVM errors /// @@ -590,6 +590,16 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { } } + /// Add log to the current transaction layer + pub(crate) fn add_log(&mut self, log: Log) -> Result<(), ExitError> { + if let Some(top_data) = self.transaction_data.last_mut() { + top_data.logs.push(log); + Ok(()) + } else { + Err(ExitError::Other(Cow::from("No transaction data for log"))) + } + } + /// Have the caller account pay for gas. Returns `Ok(true)` if the payment /// went through; returns `Ok(false)` if `caller` doesn't have the funds. /// Return `Err(...)` in case something is at fault with durable storage or @@ -1077,7 +1087,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// The outcome is encoded as a SputnikVM _Create_ outcome for easy transaction /// handling. The new address "field" in the triple is always `None`. #[allow(clippy::too_many_arguments)] - fn execute_call( + pub(crate) fn execute_call( &mut self, address: H160, transfer: Option, @@ -1233,7 +1243,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { self.end_initial_transaction(result) } - fn get_or_create_account( + pub(crate) fn get_or_create_account( &self, address: H160, ) -> Result { @@ -1245,7 +1255,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { .map_err(EthereumError::from) } - fn get_account( + pub(crate) fn get_account( &self, address: H160, ) -> Result, StorageError> { @@ -1375,7 +1385,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// This requires that no other transaction is in progress. If there is a /// transaction in progress, then the function returns an error to report /// this. - fn begin_initial_transaction( + pub(crate) fn begin_initial_transaction( &mut self, is_static: bool, gas_limit: Option, @@ -1538,7 +1548,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// End the initial transaction with either a commit or a rollback. The /// outcome depends on the execution result given. - fn end_initial_transaction( + pub(crate) fn end_initial_transaction( &mut self, execution_result: Result, ) -> Result { @@ -2134,16 +2144,11 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { topics: Vec, data: Vec, ) -> Result<(), ExitError> { - if let Some(top_data) = self.transaction_data.last_mut() { - top_data.logs.push(Log { - address, - topics, - data, - }); - Ok(()) - } else { - Err(ExitError::Other(Cow::from("No transaction data for log"))) - } + self.add_log(Log { + address, + topics, + data, + }) } fn mark_delete(&mut self, address: H160, target: H160) -> Result<(), ExitError> { -- GitLab