From 4f17ef0115ba48965d0dacbba719975fc00f5d9f Mon Sep 17 00:00:00 2001 From: Michael Zaikin Date: Wed, 31 Jul 2024 19:50:01 +0100 Subject: [PATCH] EVM/Kernel: prevent circular calls of impure precompiles --- etherlink/CHANGES_KERNEL.md | 1 + .../evm_execution/src/fa_bridge/test_utils.rs | 58 ++++- .../kernel_evm/evm_execution/src/handler.rs | 24 +++ .../src/precompiles/fa_bridge.rs | 125 ++++++++++- .../evm_execution/src/precompiles/mod.rs | 1 + .../src/precompiles/reentrancy_guard.rs | 71 +++++++ .../evm_execution/tests/contracts/Makefile | 4 + .../contracts/artifacts/ReentrancyTester.abi | 200 ++++++++++++++++++ .../artifacts/ReentrancyTester.bytecode | Bin 0 -> 3026 bytes .../tests/contracts/src/ReentrancyTester.sol | 106 ++++++++++ 10 files changed, 578 insertions(+), 12 deletions(-) create mode 100644 etherlink/kernel_evm/evm_execution/src/precompiles/reentrancy_guard.rs create mode 100644 etherlink/kernel_evm/evm_execution/tests/contracts/artifacts/ReentrancyTester.abi create mode 100644 etherlink/kernel_evm/evm_execution/tests/contracts/artifacts/ReentrancyTester.bytecode create mode 100644 etherlink/kernel_evm/evm_execution/tests/contracts/src/ReentrancyTester.sol diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index 63a19eaeb14a..d75c5ee3ce72 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -153,6 +153,7 @@ repository, but is part of [`etherlink-mainnet-launch`][mainnet-branch] instead. - 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) +- Circular calls to impure precompiles are forbidden. (!14390) ### Security Upgrades 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 84fd8cc36b8e..51388c37a229 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 @@ -24,7 +24,9 @@ use tezos_storage::read_u256_le_default; use crate::{ account_storage::{account_path, EthereumAccountStorage}, handler::{EvmHandler, ExecutionOutcome}, - precompiles::{self, precompile_set, SYSTEM_ACCOUNT_ADDRESS}, + precompiles::{ + self, precompile_set, FA_BRIDGE_PRECOMPILE_ADDRESS, SYSTEM_ACCOUNT_ADDRESS, + }, run_transaction, utilities::keccak256_hash, withdrawal_counter::WITHDRAWAL_COUNTER_PATH, @@ -45,10 +47,17 @@ sol!( kernel_wrapper, "tests/contracts/artifacts/MockFaBridgePrecompile.abi" ); +sol!( + reentrancy_tester, + "tests/contracts/artifacts/ReentrancyTester.abi" +); const MOCK_WRAPPER_BYTECODE: &[u8] = include_bytes!("../../tests/contracts/artifacts/MockFaBridgeWrapper.bytecode"); +const REENTRANCY_TESTER_BYTECODE: &[u8] = + include_bytes!("../../tests/contracts/artifacts/ReentrancyTester.bytecode"); + /// Create a smart contract in the storage with the mocked token code pub fn deploy_mock_wrapper( host: &mut MockKernelHost, @@ -92,6 +101,53 @@ pub fn deploy_mock_wrapper( .unwrap() } +/// Create a smart contract in the storage with the mocked token code +pub fn deploy_reentrancy_tester( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + ticket: &FA2_1Ticket, + caller: &H160, + withdrawal_amount: U256, + withdrawal_count: U256, +) -> ExecutionOutcome { + let code = REENTRANCY_TESTER_BYTECODE.to_vec(); + let (ticketer, content) = ticket_id(ticket); + let dummy_routing_info = [vec![0u8; 22], vec![1u8], vec![0u8; 21]].concat(); + let calldata = reentrancy_tester::constructorCall::new(( + convert_h160(&FA_BRIDGE_PRECOMPILE_ADDRESS), + dummy_routing_info.into(), + convert_u256(&withdrawal_amount), + ticketer.into(), + content.into(), + convert_u256(&withdrawal_count), + )); + + let block = dummy_block_constants(); + let precompiles = precompile_set::(false); + + 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(1_000_000), + U256::one(), + U256::zero(), + false, + 1_000_000_000, + false, + false, + None, + ) + .expect("Failed to deploy") + .unwrap() +} + /// Execute FA deposit pub fn run_fa_deposit( host: &mut MockKernelHost, diff --git a/etherlink/kernel_evm/evm_execution/src/handler.rs b/etherlink/kernel_evm/evm_execution/src/handler.rs index d51a2ab7e88d..f7020161e605 100644 --- a/etherlink/kernel_evm/evm_execution/src/handler.rs +++ b/etherlink/kernel_evm/evm_execution/src/handler.rs @@ -14,6 +14,8 @@ use crate::account_storage::{ account_path, AccountStorageError, EthereumAccount, EthereumAccountStorage, CODE_HASH_DEFAULT, }; +use crate::precompiles::reentrancy_guard::ReentrancyGuard; +use crate::precompiles::{FA_BRIDGE_PRECOMPILE_ADDRESS, WITHDRAWAL_ADDRESS}; use crate::storage::blocks::{get_block_hash, BLOCKS_STORED}; use crate::storage::tracer; use crate::tick_model_opcodes; @@ -326,6 +328,8 @@ pub struct EvmHandler<'a, Host: Runtime> { /// used for caching and optimise the VM's execution in terms /// of speed. storage_state: Vec, + /// Reentrancy guard prevents circular calls to impure precompiles + reentrancy_guard: ReentrancyGuard, } #[allow(clippy::too_many_arguments)] @@ -424,6 +428,10 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { enable_warm_cold_access, tracer, storage_state: vec![], + reentrancy_guard: ReentrancyGuard::new(vec![ + WITHDRAWAL_ADDRESS, + FA_BRIDGE_PRECOMPILE_ADDRESS, + ]), } } @@ -1181,6 +1189,14 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { #[cfg(feature = "benchmark-opcodes")] benchmarks::start_precompile_section(self.host, address, &input); + if let Err(err) = self.reentrancy_guard.begin_precompile_call(&address) { + return Ok(( + ExitReason::Fatal(evm::ExitFatal::CallErrorAsFatal(err)), + None, + vec![], + )); + } + let precompile_execution_result = self.precompiles.execute( self, address, @@ -1190,6 +1206,8 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { transfer, ); + self.reentrancy_guard.end_precompile_call(); + #[cfg(feature = "benchmark-opcodes")] benchmarks::end_precompile_section(self.host); @@ -2002,6 +2020,12 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { ExitReason::Succeed(_) => vec![], } } + + /// Test helper used to demonstrate that reentrancy guard is actually the exit reason. + #[cfg(test)] + pub(crate) fn disable_reentrancy_guard(&mut self) { + self.reentrancy_guard.disable(); + } } #[allow(unused_variables)] diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs index cfb4c22c9d99..7e1753d1f10e 100644 --- a/etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/fa_bridge.rs @@ -160,6 +160,7 @@ mod tests { use alloy_sol_types::SolCall; use evm::{Config, ExitError}; use primitive_types::{H160, U256}; + use tezos_data_encoding::enc::BinWriter; use tezos_evm_runtime::runtime::MockKernelHost; use crate::{ @@ -167,8 +168,9 @@ mod tests { 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, + convert_h160, convert_u256, deploy_reentrancy_tester, + dummy_fa_withdrawal, dummy_first_block, dummy_ticket, kernel_wrapper, + set_balance, ticket_balance_add, ticket_id, }, }, handler::{EvmHandler, ExecutionOutcome, ExecutionResult}, @@ -177,14 +179,16 @@ mod tests { utilities::{bigint_to_u256, keccak256_hash}, }; + #[allow(clippy::too_many_arguments)] fn execute_precompile( host: &mut MockKernelHost, evm_account_storage: &mut EthereumAccountStorage, caller: H160, - value: Option, + value: U256, input: Vec, gas_limit: Option, is_static: bool, + disable_reentrancy_guard: bool, ) -> ExecutionOutcome { let block = dummy_first_block(); let config = Config::shanghai(); @@ -199,14 +203,18 @@ mod tests { &block, &config, &precompiles, - 1_000_000_000, + 100_000_000_000, U256::from(21000), false, None, ); + if disable_reentrancy_guard { + handler.disable_reentrancy_guard(); + } + handler - .call_contract(caller, callee, value, input, gas_limit, is_static) + .call_contract(caller, callee, Some(value), input, gas_limit, is_static) .expect("Failed to invoke precompile") } @@ -219,10 +227,11 @@ mod tests { &mut mock_runtime, &mut evm_account_storage, H160::from_low_u64_be(1), - Some(U256::zero()), + U256::zero(), vec![0x00, 0x01, 0x02, 0x03], None, false, + false, ); assert!(!outcome.is_success()); assert!( @@ -239,11 +248,12 @@ mod tests { &mut mock_runtime, &mut evm_account_storage, H160::from_low_u64_be(1), - None, + U256::zero(), vec![0x80, 0xfc, 0x1f, 0xe3], // Cover only basic cost Some(21000 + 16 * 4), false, + false, ); assert!(!outcome.is_success()); assert!( @@ -268,10 +278,11 @@ mod tests { &mut mock_runtime, &mut evm_account_storage, caller, - Some(1_000_000_000.into()), + 1_000_000_000.into(), vec![0x80, 0xfc, 0x1f, 0xe3], None, false, + false, ); assert!(!outcome.is_success()); assert!( @@ -289,10 +300,11 @@ mod tests { &mut mock_runtime, &mut evm_account_storage, caller, - None, + U256::zero(), vec![0x80, 0xfc, 0x1f, 0xe3], None, true, + false, ); assert!(!outcome.is_success()); assert!( @@ -351,10 +363,11 @@ mod tests { &mut mock_runtime, &mut evm_account_storage, H160::from_low_u64_be(1), - Some(U256::zero()), + U256::zero(), vec![0x80, 0xfc, 0x1f, 0xe3], None, false, + false, ); assert!(!outcome.is_success()); assert!( @@ -404,10 +417,11 @@ mod tests { &mut mock_runtime, &mut evm_account_storage, ticket_owner, - Some(U256::zero()), + U256::zero(), input, Some(30_000_000), false, + false, ); assert!(outcome.is_success()); assert_eq!(1, outcome.withdrawals.len()); @@ -428,4 +442,93 @@ mod tests { keccak256_hash(b"withdraw(address,bytes,uint256,bytes22,bytes)"); assert_eq!(method_hash.0[0..4], [0x80, 0xfc, 0x1f, 0xe3]); } + + #[test] + fn fa_bridge_precompile_cannot_call_itself() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let system = H160::zero(); + let sender = H160::from_low_u64_be(1); + let ticket = dummy_ticket(); + + let proxy = deploy_reentrancy_tester( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &system, + U256::from(2), + U256::from(4), + ) + .new_address() + .expect("Failed to deploy reentrancy tester"); + + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &ticket_hash(&ticket).unwrap(), + &proxy, + U256::from(100), + ); + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + let mut receiver = Vec::new(); + withdrawal.receiver.bin_write(&mut receiver).unwrap(); + + let mut proxy = Vec::new(); + withdrawal.proxy.bin_write(&mut proxy).unwrap(); + + let mut ticketer = Vec::new(); + withdrawal + .ticket + .creator() + .0 + .bin_write(&mut ticketer) + .unwrap(); + + let mut contents = Vec::new(); + withdrawal + .ticket + .contents() + .bin_write(&mut contents) + .unwrap(); + + let input = kernel_wrapper::withdrawCall::new(( + convert_h160(&withdrawal.ticket_owner), + [receiver, proxy].concat().into(), + convert_u256(&withdrawal.amount), + TryInto::<[u8; 22]>::try_into(ticketer).unwrap().into(), + contents.into(), + )); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + sender, + U256::zero(), + input.abi_encode(), + // Note that we set gas limit larger than hard cap for a single transaction: + // that is to overcome the added cost per FA withdrawal which is pretty large + // for the given gas price (up to 15M). + Some(100_000_000), + false, + false, + ); + assert!(!outcome.is_success()); + // we cannot capture the actual revert reason here because it's not propagated + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + sender, + U256::zero(), + input.abi_encode(), + Some(100_000_000), + false, + true, + ); + assert!(outcome.is_success()); + assert!(!outcome.withdrawals.is_empty()); + } } diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs index f15b0175c9b0..fea4fa772972 100644 --- a/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs @@ -19,6 +19,7 @@ mod fa_bridge; mod hash; mod identity; mod modexp; +pub(crate) mod reentrancy_guard; mod withdrawal; mod zero_knowledge; diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/reentrancy_guard.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/reentrancy_guard.rs new file mode 100644 index 000000000000..40176f7b2542 --- /dev/null +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/reentrancy_guard.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Reentrancy guard prevents circular impure precompile calls, such as: +//! * FA bridge -> Arbitrary contract -> XTZ bridge +//! * FA bridge -> Arbitrary contract -> FA bridge +//! +//! Although such calls do not immediately introduce a vulnerability, +//! it's still a good practice that can prevent potential issues. +//! +//! NOTE that this does not prevent batched precompile calls. +//! +//! Read more: https://blog.openzeppelin.com/reentrancy-after-istanbul + +use std::borrow::Cow; + +use evm::ExitError; +use primitive_types::H160; + +#[derive(Debug)] +pub struct ReentrancyGuard { + /// List of impure precompiles + stoplist: Vec, + /// Flag is set when kernel encounters the first "impure" precompile + /// in the call stack. + enabled_at: Option, + /// Current call depth (only precompile calls count) + level: u32, +} + +impl ReentrancyGuard { + /// Create new reentrancy guard from a list of impure precompiles. + pub fn new(stoplist: Vec) -> Self { + Self { + stoplist, + enabled_at: None, + level: 0, + } + } + + /// Try to begin a precompile call. + /// + /// If the address belongs to an impure precompile this method AND + /// this is a circular call then this method will fail with an error. + pub fn begin_precompile_call(&mut self, address: &H160) -> Result<(), ExitError> { + if self.stoplist.contains(address) { + if self.enabled_at.is_some() { + return Err(ExitError::Other(Cow::from( + "Circular calls are not allowed", + ))); + } + self.enabled_at = Some(self.level); + } + self.level += 1; + Ok(()) + } + + /// End precompile call. + pub fn end_precompile_call(&mut self) { + self.level -= 1; + if self.enabled_at == Some(self.level) { + self.enabled_at = None; + } + } + + #[cfg(test)] + pub fn disable(&mut self) { + self.stoplist.clear(); + } +} diff --git a/etherlink/kernel_evm/evm_execution/tests/contracts/Makefile b/etherlink/kernel_evm/evm_execution/tests/contracts/Makefile index bef969bae60c..9abc26138630 100644 --- a/etherlink/kernel_evm/evm_execution/tests/contracts/Makefile +++ b/etherlink/kernel_evm/evm_execution/tests/contracts/Makefile @@ -7,6 +7,10 @@ artifacts: mkdir artifacts || true forge build + jq ".abi" build/ReentrancyTester.sol/ReentrancyTester.json > artifacts/ReentrancyTester.abi jq ".abi" build/MockFaBridgeWrapper.sol/MockFaBridgeWrapper.json > artifacts/MockFaBridgeWrapper.abi jq ".abi" build/MockFaBridgePrecompile.sol/MockFaBridgePrecompile.json > artifacts/MockFaBridgePrecompile.abi + jq -r ".bytecode.object" build/ReentrancyTester.sol/ReentrancyTester.json | xxd -r -p > artifacts/ReentrancyTester.bytecode jq -r ".bytecode.object" build/MockFaBridgeWrapper.sol/MockFaBridgeWrapper.json | xxd -r -p > artifacts/MockFaBridgeWrapper.bytecode + rm -rf ./build + rm -rf ./cache \ No newline at end of file diff --git a/etherlink/kernel_evm/evm_execution/tests/contracts/artifacts/ReentrancyTester.abi b/etherlink/kernel_evm/evm_execution/tests/contracts/artifacts/ReentrancyTester.abi new file mode 100644 index 000000000000..dfe161a7ca0b --- /dev/null +++ b/etherlink/kernel_evm/evm_execution/tests/contracts/artifacts/ReentrancyTester.abi @@ -0,0 +1,200 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "withdrawalPrecompile_", + "type": "address", + "internalType": "address" + }, + { + "name": "routingInfo_", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount_", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "ticketer_", + "type": "bytes22", + "internalType": "bytes22" + }, + { + "name": "content_", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "callsCount_", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "amount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "callsCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "content", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "routingInfo", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setParameters", + "inputs": [ + { + "name": "withdrawalPrecompile_", + "type": "address", + "internalType": "address" + }, + { + "name": "routingInfo_", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount_", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "ticketer_", + "type": "bytes22", + "internalType": "bytes22" + }, + { + "name": "content_", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "callsCount_", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "ticketer", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes22", + "internalType": "bytes22" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdrawalPrecompile", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + } +] diff --git a/etherlink/kernel_evm/evm_execution/tests/contracts/artifacts/ReentrancyTester.bytecode b/etherlink/kernel_evm/evm_execution/tests/contracts/artifacts/ReentrancyTester.bytecode new file mode 100644 index 0000000000000000000000000000000000000000..1dc3633920c6d77f5480840d819a3effb8a460cb GIT binary patch literal 3026 zcmYdjNN@-;X%J0fU=R#XU}*Ro9gyGv~+CO#oFfia1Jp|Y(h ziGk@@Sad)FQ_F=5 zNO^+1P|Q6!0haRo#?L7{^&aKf|zRDUrZgUBW^ zFkb0u?gj<$5s(MF7y}a$x)}o}&Ip(mFcG3DK$=;yNvsKE<{yZzgoaMWz==}=AYsHL z1Q88LVqjA2Z0dp}%;DmrlQz0lUI)SmVQD#DuSVIy6lUH~`eUsS4fanB; zCV@ugBnGDJaO7xGf=5%p#0em;%Y$Qt>0o#QLnA026%rT}669MM6If*$850!RK*7m0 zCp@4b8YHpalF&?zm9 zQxX^_Ok|wU!kEOs>=YIq&%uV6#t&D+Sb2}zTC;SkTkrZnT1$iJKAc1j0qcr1$ z(15V$L=J<7BnIY12@IAA44?nOa%duh05pCQSj+?x877A(FqkGhkZDMc46gMQOk_w4 zhcHDF8D+y8l8e_{1d1dw8iY3_clk!8izG6-hBqXy>QSr_No4d5Z%E#H^jncgBEx-% zsSU~OGLtz(5*cQMHzadEn}1d$k>NymLo(mLtZuNXhGhGK&+a0L3}3=QnH-esLJ}EP zPXH=7r7ji7`FPrVZ+ih)9j5tQ2+CNfX>9G%E;F@ZG%qG|$IE|Fn>Sac%8r$mOg zW(h2b%zMM46B)z8q8XwSm_rg787CwJNJG?T{)Yoen8|S6jM^aGER#XHlR<<F0_#|aOl#U})T z3x3v+Ns|K-nNCcCrHTm?#U{2)Ok_G877fb#jM0saf{Dzt!V?%;VM@eW+7cWB6PZ3t zoRG-A9TXV}4uK8L%#E$>Q6DD-1O(VNBrr6JBr@xUCpZL73`mfdOt80Q6l-Eka0s$( z3bIXLNKj~Dv`b_*3X4uim>3Wp5HKO2A(6Qqq&~?_`fS2eX;Ankurx6SCCDYHG%*I% zJ16GkD3s(Ylq43VrlQ8m)L>8k&<`1Aq!(G88 zvP6X^vP6N^L2Ea7&6dbg3^BVoL7|;7L7|z^92B;MtFJ_sH4u%7EL+#f`@ea) zY;PLDj%;WRN>E5(Y;F{55KUy~3aIF}5)VO=t#H#EI;BVbPt8EsWCwK*d8qKmaTn zAuZK5P-V3sL7|DU5mb^*NMvuA(9GDL$aX(0x;4R}g)s;@UABrPB(yLFH8nCPG_)`V zC9Yy~TwF&z{qAPfq$rocq@lZ`F#bOcI35NjKn7{RHS{R>J`5rj9Dz{>tDO35rp zD~?bSQt&z-FDCe8$sd`_V%5XXJlg|B6}s2ABtPJvl*$@7d8$H6aehv+GXsYhgE0Wf CW9Fy; literal 0 HcmV?d00001 diff --git a/etherlink/kernel_evm/evm_execution/tests/contracts/src/ReentrancyTester.sol b/etherlink/kernel_evm/evm_execution/tests/contracts/src/ReentrancyTester.sol new file mode 100644 index 000000000000..70c4e659f4d7 --- /dev/null +++ b/etherlink/kernel_evm/evm_execution/tests/contracts/src/ReentrancyTester.sol @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2024 PK Lab +// +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.19; + +contract ReentrancyTester { + address public withdrawalPrecompile; + bytes public routingInfo; + uint256 public amount; + bytes22 public ticketer; + bytes public content; + uint256 public callsCount; + + /** + * @notice Constructs the TokenProxyTester. + * @param withdrawalPrecompile_ The address of the withdrawal precompile contract. + * @param routingInfo_ The routing information for the withdrawal. + * @param amount_ The amount of tokens to withdraw. + * @param ticketer_ The address of the ticketer. + * @param content_ The content of the ticket. + * @param callsCount_ The number of calls to make during deposit and withdrawal. + */ + constructor( + address withdrawalPrecompile_, + bytes memory routingInfo_, + uint256 amount_, + bytes22 ticketer_, + bytes memory content_, + uint256 callsCount_ + ) { + setParameters( + withdrawalPrecompile_, + routingInfo_, + amount_, + ticketer_, + content_, + callsCount_ + ); + } + + /** + * @notice Sets the parameters for the TokenProxyTester. + * @param withdrawalPrecompile_ The address of the withdrawal precompile contract. + * @param routingInfo_ The routing information for the withdrawal. + * @param amount_ The amount of tokens to withdraw. + * @param ticketer_ The address of the ticketer. + * @param content_ The content of the ticket. + * @param callsCount_ The number of calls to make during deposit and withdrawal. + */ + function setParameters( + address withdrawalPrecompile_, + bytes memory routingInfo_, + uint256 amount_, + bytes22 ticketer_, + bytes memory content_, + uint256 callsCount_ + ) public { + withdrawalPrecompile = withdrawalPrecompile_; + routingInfo = routingInfo_; + amount = amount_; + ticketer = ticketer_; + content = content_; + callsCount = callsCount_; + } + + function _makeCallMultipleTimes( + address callee, + bytes memory data, + uint256 value, + uint256 times + ) internal { + uint256 counter = 1; + while (counter <= times) { + (bool success,) = callee.call{value: value}(data); + require(success, "Call to target contract failed"); + counter += 1; + } + } + + function _makeCallsToWithdrawalPrecompile() internal { + bytes memory data = abi.encodeWithSignature( + "withdraw(address,bytes,uint256,bytes22,bytes)", + address(this), + routingInfo, + amount, + ticketer, + content + ); + _makeCallMultipleTimes(withdrawalPrecompile, data, 0, callsCount); + } + + /** + * @notice Mocks the deposit function. + */ + function deposit(address, uint256, uint256) public { + _makeCallsToWithdrawalPrecompile(); + } + + /** + * @notice Mocks the withdraw function. + */ + function withdraw(address, uint256, uint256) public { + _makeCallsToWithdrawalPrecompile(); + } +} -- GitLab