diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index f65ffeeddb1111b44f6def1bbd7a4ad9818f78ff..18c6ccf7d2e22933266dda8a7748aecd3163ce74 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -24,6 +24,8 @@ [kgov]: https://better-call.dev/mainnet/KT1FPG4NApqTJjwvmhWvqA14m5PJxu9qgpBK/operations [sgov]: https://better-call.dev/mainnet/KT1GRAN26ni19mgd6xpL6tsH52LNnhKSQzP2/operations [sqgov]: https://better-call.dev/mainnet/KT1UvCsnXpLAssgeJmrbQ6qr3eFkYXxsTG9U/operations +- A fast withdrawal entrypoint was added to the withdrawal precompiled contract + under a feature flag. (!16417) ### Internal diff --git a/etherlink/bin_node/lib_dev/kernel_config.ml b/etherlink/bin_node/lib_dev/kernel_config.ml index 7708e28f7a7351c21f7c02d1bc73fa6fc5654bee..d5b0b7076546073bf1b9f5cf00b4cd0951b7c66b 100644 --- a/etherlink/bin_node/lib_dev/kernel_config.ml +++ b/etherlink/bin_node/lib_dev/kernel_config.ml @@ -28,8 +28,8 @@ let make ~mainnet_compat ~boostrap_balance ?bootstrap_accounts ?kernel_root_hash ?da_fee_per_byte ?delayed_inbox_timeout ?delayed_inbox_min_levels ?sequencer_pool_address ?maximum_allowed_ticks ?maximum_gas_per_transaction ?max_blueprint_lookahead_in_seconds ?remove_whitelist ?enable_fa_bridge - ?enable_dal ?dal_slots ?enable_multichain ?set_account_code - ?max_delayed_inbox_blueprint_length ~output () = + ?enable_dal ?dal_slots ?enable_fast_withdrawal ?enable_multichain + ?set_account_code ?max_delayed_inbox_blueprint_length ~output () = let bootstrap_accounts = match bootstrap_accounts with | None -> [] @@ -103,6 +103,9 @@ let make ~mainnet_compat ~boostrap_balance ?bootstrap_accounts ?kernel_root_hash @ make_instr remove_whitelist @ make_instr ~path_prefix:"/evm/feature_flags/" enable_fa_bridge @ make_instr ~path_prefix:"/evm/feature_flags/" enable_dal + @ make_instr + ~path_prefix:"/evm/world_state/feature_flags/" + enable_fast_withdrawal @ make_instr ~convert:decimal_list_to_bytes dal_slots @ make_instr ~path_prefix:"/evm/feature_flags/" enable_multichain @ make_instr diff --git a/etherlink/bin_node/lib_dev/kernel_config.mli b/etherlink/bin_node/lib_dev/kernel_config.mli index 503151a4a3daf929e856fc636b135aceb3475496..506f31c444bf8aff8a7e4c3edde5b2901a033119 100644 --- a/etherlink/bin_node/lib_dev/kernel_config.mli +++ b/etherlink/bin_node/lib_dev/kernel_config.mli @@ -34,6 +34,7 @@ val make : ?enable_fa_bridge:string * string -> ?enable_dal:string * string -> ?dal_slots:string * string -> + ?enable_fast_withdrawal:string * string -> ?enable_multichain:string * string -> ?set_account_code:(string * string) list -> ?max_delayed_inbox_blueprint_length:string * string -> diff --git a/etherlink/bin_node/main.ml b/etherlink/bin_node/main.ml index 540f2b6f05cd80f00fd4b1d858176d87670ec146..c6e2f3cc0737e3285ab750ea1b73e9dcd3f7c34c 100644 --- a/etherlink/bin_node/main.ml +++ b/etherlink/bin_node/main.ml @@ -1903,11 +1903,12 @@ let make_kernel_config_command = (config_key_flag ~name:"enable_fa_bridge") (config_key_flag ~name:"enable_dal") (config_key_arg ~name:"dal_slots" ~placeholder:"0,1,4,6,...")) - (args2 + (args3 (config_key_flag ~name:"enable_multichain") (config_key_arg ~name:"max_delayed_inbox_blueprint_length" - ~placeholder:"1000"))) + ~placeholder:"1000") + (config_key_flag ~name:"enable_fast_withdrawal"))) (prefixes ["make"; "kernel"; "installer"; "config"] @@ param ~name:"kernel config file" @@ -1939,7 +1940,9 @@ let make_kernel_config_command = enable_fa_bridge, enable_dal, dal_slots ), - (enable_multichain, max_delayed_inbox_blueprint_length) ) + ( enable_multichain, + max_delayed_inbox_blueprint_length, + enable_fast_withdrawal ) ) output () -> Evm_node_lib_dev.Kernel_config.make @@ -1970,6 +1973,7 @@ let make_kernel_config_command = ?enable_multichain ?set_account_code ?max_delayed_inbox_blueprint_length + ?enable_fast_withdrawal ~output ()) diff --git a/etherlink/kernel_evm/evm_execution/src/abi.rs b/etherlink/kernel_evm/evm_execution/src/abi.rs index dba9970df6d83510bbd105303b2ca85b4d0689b4..bd0b3aa31d27d0ee04ffb01f0c9430b7fe262724 100644 --- a/etherlink/kernel_evm/evm_execution/src/abi.rs +++ b/etherlink/kernel_evm/evm_execution/src/abi.rs @@ -9,8 +9,11 @@ //! at //! [Contract ABI specification](https://docs.soliditylang.org/en/develop/abi-spec.html) -// TODO investigate if we can use this Rust package instead: -// https://docs.rs/ethabi/latest/ethabi/ +// TODO: https://gitlab.com/tezos/tezos/-/issues/7722 +// This whole file is hack-ish. +// In the long term we should get rid of this file completely and rely on a proper +// implementation for abi parameters (for instance see cast's implementation to +// depend on the same crates). use primitive_types::{H160, U256}; @@ -44,6 +47,41 @@ pub fn string_parameter(input_data: &[u8], parameter_number: usize) -> Option<&s core::str::from_utf8(bytes_parameter(input_data, parameter_number)?).ok() } +pub fn fast_withdrawal_parameters(input_data: &[u8]) -> Option<(&str, &str, Vec)> { + // Disclaimer: The following implementation is REALLY hack-ish. But it's the most straighforward + // decoding function until we find a better crate to handle these encodings. + // We ignore the first three slices of 32 bytes, they are the position of the parameters. + // In our case, since it's a hack-ish implementation, we can directly infer them. + const U256_SIZE: usize = 32; + + let first_arg_size: usize = U256::from(input_data.get(3 * U256_SIZE..4 * U256_SIZE)?) + .try_into() + .ok()?; + let number_of_slice = first_arg_size.div_ceil(U256_SIZE); + let read_to = first_arg_size + 4 * U256_SIZE; + let arg_1 = &input_data.get(4 * U256_SIZE..read_to)?; + let arg_1 = core::str::from_utf8(arg_1).ok()?; + + let from = U256_SIZE * number_of_slice + 4 * U256_SIZE; + let second_args_size: usize = U256::from(input_data.get(from..(from + U256_SIZE))?) + .try_into() + .ok()?; + let number_of_slice = second_args_size.div_ceil(U256_SIZE); + let read_to = (from + U256_SIZE) + second_args_size; + let arg_2 = &input_data.get((from + U256_SIZE)..read_to)?; + let arg_2 = core::str::from_utf8(arg_2).ok()?; + + let from = U256_SIZE * number_of_slice + (from + U256_SIZE); + let third_args_size: usize = U256::from(input_data.get(from..(from + U256_SIZE))?) + .try_into() + .ok()?; + let read_to = third_args_size + (from + U256_SIZE); + let arg_3 = &input_data.get((from + U256_SIZE)..read_to)?; + let arg_3 = arg_3.to_vec(); + + Some((arg_1, arg_2, arg_3)) +} + /// Get an address parameter from the input data buffer pub fn h160_parameter(input_data: &[u8], parameter_number: usize) -> Option { let location = parameter_number * 32; @@ -86,3 +124,17 @@ pub fn fixed_bytes_parameter( None } } + +#[cfg(test)] +mod test { + use super::fast_withdrawal_parameters; + + #[test] + fn test_fast_withdrawal_parameters() { + let test_case = hex::decode("000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024747a316670356e63446d7159775943353638665245597a3969775154674751754b5a7158000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024747a316670356e63446d7159775943353638665245597a3969775154674751754b5a7158000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let (arg_1, arg_2, arg_3) = fast_withdrawal_parameters(&test_case).unwrap(); + assert_eq!("tz1fp5ncDmqYwYC568fREYz9iwQTgGQuKZqX", arg_1); + assert_eq!("tz1fp5ncDmqYwYC568fREYz9iwQTgGQuKZqX", arg_2); + assert_eq!(Vec::::new(), arg_3) + } +} 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 526ac36ad537ca945462c8a2bafb7d8acceffe85..d3aeaea73b951499b87a0e1b0f7709a1f4ae6376 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/withdrawal.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/withdrawal.rs @@ -187,7 +187,9 @@ impl FaWithdrawal { // L1 proxy accepts ticket and the final receiver address parameters: MichelsonPair(MichelsonContract(self.receiver), self.ticket), }; - OutboxMessage::AtomicTransactionBatch(vec![message].into()) + crate::handler::Withdrawal::Standard(OutboxMessage::AtomicTransactionBatch( + vec![message].into(), + )) } /// Formats FA withdrawal structure for logging purposes. diff --git a/etherlink/kernel_evm/evm_execution/src/handler.rs b/etherlink/kernel_evm/evm_execution/src/handler.rs index d2c52d374502f817d7d335044cb372208e0d12f3..04899ca99b535b28fbf3a8ecbc259876daddd996 100644 --- a/etherlink/kernel_evm/evm_execution/src/handler.rs +++ b/etherlink/kernel_evm/evm_execution/src/handler.rs @@ -43,19 +43,59 @@ use sha3::{Digest, Keccak256}; use std::cmp::min; use std::collections::HashMap; use std::fmt::Debug; +use tezos_data_encoding::enc::{BinResult, BinWriter}; use tezos_ethereum::block::BlockConstants; use tezos_evm_logging::{log, Level::*}; use tezos_evm_runtime::runtime::Runtime; use tezos_smart_rollup_encoding::michelson::ticket::FA2_1Ticket; -use tezos_smart_rollup_encoding::michelson::{MichelsonContract, MichelsonPair}; +use tezos_smart_rollup_encoding::michelson::{ + MichelsonBytes, MichelsonContract, MichelsonNat, MichelsonPair, MichelsonTimestamp, +}; use tezos_smart_rollup_encoding::outbox::OutboxMessage; use tezos_smart_rollup_storage::StorageError; /// Withdrawal interface of the ticketer contract pub type RouterInterface = MichelsonPair; -/// Outbox message that implements RouterInterface, ready to be encoded and posted -pub type Withdrawal = OutboxMessage; +/// Interface of the default entrypoint of the fast withdrawal contract. +/// +/// The parameters corresponds to (from left to right w.r.t. `MichelsonPair`): +/// * withdrawal_id +/// * ticket +/// * timestamp +/// * withdrawer's address +/// * generic payload +pub type FastWithdrawalInterface = MichelsonPair< + MichelsonNat, + MichelsonPair< + FA2_1Ticket, + MichelsonPair< + MichelsonTimestamp, + MichelsonPair, + >, + >, +>; + +/// Outbox messages that implements the different withdrawal interfaces, +/// ready to be encoded and posted. +#[derive(Debug, PartialEq, Eq)] +pub enum Withdrawal { + Standard(OutboxMessage), + Fast(OutboxMessage), +} + +impl BinWriter for Withdrawal { + fn bin_write(&self, output: &mut Vec) -> BinResult { + match self { + Withdrawal::Standard(outbox_message_full) => { + outbox_message_full.bin_write(output) + } + Withdrawal::Fast(outbox_message_full) => { + outbox_message_full.bin_write(output) + } + } + } +} #[derive(Debug, Eq, PartialEq)] pub enum ExecutionResult { diff --git a/etherlink/kernel_evm/evm_execution/src/lib.rs b/etherlink/kernel_evm/evm_execution/src/lib.rs index efff45a8512016ca064f53cb2f64c9d832463e08..c72670a1d812d8c9c88b99779303e139cee4f156 100755 --- a/etherlink/kernel_evm/evm_execution/src/lib.rs +++ b/etherlink/kernel_evm/evm_execution/src/lib.rs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2022-2024 TriliTech -// SPDX-FileCopyrightText: 2023-2024 Functori +// SPDX-FileCopyrightText: 2023-2025 Functori // // SPDX-License-Identifier: MIT @@ -430,6 +430,15 @@ pub fn read_ticketer(host: &impl Runtime) -> Option { ContractKt1Hash::from_b58check(&kt1_b58).ok() } +// Path to the fast withdrawals feature flag. If there is nothing at this +// path, fast withdrawals are not used. +pub const ENABLE_FAST_WITHDRAWAL: RefPath = + RefPath::assert_from(b"/evm/world_state/feature_flags/enable_fast_withdrawal"); + +pub fn fast_withdrawals_enabled(host: &Host) -> bool { + host.store_read_all(&ENABLE_FAST_WITHDRAWAL).is_ok() +} + #[cfg(test)] mod test { use crate::account_storage::EthereumAccount; diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs index ceeb6eea8e4149990ca0c179508533306ac3fa3a..73ef9f48d1a393f67a7492266da1835522abe0bc 100644 --- a/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2022-2023 TriliTech // SPDX-FileCopyrightText: 2023-2024 PK Lab -// SPDX-FileCopyrightText: 2024 Functori +// SPDX-FileCopyrightText: 2024-2025 Functori // // SPDX-License-Identifier: MIT @@ -8,21 +8,27 @@ use std::borrow::Cow; use crate::abi::ABI_B22_RIGHT_PADDING; use crate::abi::ABI_H160_LEFT_PADDING; +use crate::fast_withdrawals_enabled; use crate::handler::EvmHandler; +use crate::handler::FastWithdrawalInterface; use crate::handler::RouterInterface; use crate::handler::Withdrawal; use crate::precompiles::tick_model; use crate::precompiles::PrecompileOutcome; use crate::precompiles::{SYSTEM_ACCOUNT_ADDRESS, WITHDRAWAL_ADDRESS}; use crate::read_ticketer; +use crate::utilities::u256_to_bigint; use crate::withdrawal_counter::WithdrawalCounter; use crate::{abi, fail_if_too_much, EthereumError}; +use crypto::hash::ContractKt1Hash; +use crypto::hash::HashTrait; use evm::Handler; use evm::{Context, ExitReason, ExitRevert, ExitSucceed, Transfer}; use primitive_types::H160; use primitive_types::H256; use primitive_types::U256; use tezos_data_encoding::enc::BinWriter; +use tezos_data_encoding::types::Zarith; use tezos_ethereum::wei::mutez_from_wei; use tezos_ethereum::wei::ErrorMutezFromWei; use tezos_ethereum::Log; @@ -32,6 +38,9 @@ use tezos_evm_runtime::runtime::Runtime; use tezos_smart_rollup_encoding::contract::Contract; use tezos_smart_rollup_encoding::entrypoint::Entrypoint; use tezos_smart_rollup_encoding::michelson::ticket::FA2_1Ticket; +use tezos_smart_rollup_encoding::michelson::MichelsonBytes; +use tezos_smart_rollup_encoding::michelson::MichelsonNat; +use tezos_smart_rollup_encoding::michelson::MichelsonTimestamp; use tezos_smart_rollup_encoding::michelson::{ MichelsonContract, MichelsonOption, MichelsonPair, }; @@ -64,6 +73,14 @@ pub const WITHDRAWAL_EVENT_TOPIC: [u8; 32] = [ 102, 53, 63, 221, 233, 204, 248, 19, 244, 91, 132, 250, 130, ]; +/// Keccak256 of FastWithdrawal(bytes22,uint256,uint256,uint256,bytes) +/// Arguments in this order: l1 target address, withdrawal_id, amount, timestamp +pub const FAST_WITHDRAWAL_EVENT_TOPIC: [u8; 32] = [ + 0xc3, 0x7b, 0x6b, 0x21, 0x84, 0x92, 0x3c, 0x72, 0xcb, 0xef, 0xf8, 0x85, 0x3f, 0x31, + 0x82, 0x14, 0xa8, 0xa1, 0x6d, 0xc1, 0xbe, 0xaa, 0x34, 0x82, 0x46, 0x41, 0xfe, 0x04, + 0xde, 0xa1, 0x69, 0x75, +]; + /// Calculate precompile gas cost given the estimated amount of ticks and gas price. fn estimate_gas_cost(estimated_ticks: u64, gas_price: U256) -> u64 { // Using 1 gas unit ~= 1000 ticks convert ratio @@ -87,10 +104,158 @@ fn prepare_message( destination, }; - let outbox_message = OutboxMessage::AtomicTransactionBatch(vec![message].into()); + let outbox_message = + Withdrawal::Standard(OutboxMessage::AtomicTransactionBatch(vec![message].into())); Some(outbox_message) } +fn prepare_fast_withdraw_message( + parameters: FastWithdrawalInterface, + destination: Contract, + entrypoint: Option<&str>, +) -> Option { + let entrypoint = + Entrypoint::try_from(String::from(entrypoint.unwrap_or("default"))).ok()?; + + let message = OutboxMessageTransaction { + parameters, + entrypoint, + destination, + }; + + let outbox_message = + Withdrawal::Fast(OutboxMessage::AtomicTransactionBatch(vec![message].into())); + Some(outbox_message) +} + +fn revert_withdrawal() -> PrecompileOutcome { + PrecompileOutcome { + exit_status: ExitReason::Revert(ExitRevert::Reverted), + output: vec![], + withdrawals: vec![], + estimated_ticks: tick_model::ticks_of_withdraw(), + } +} + +pub struct WithdrawalBase { + ticketer: ContractKt1Hash, + base_transfer_value: U256, + target: Contract, + withdrawal_id: U256, + ticket: FA2_1Ticket, +} + +pub type BaseWithdrawOutcome = (Option, Option); + +pub fn base_withdrawal_preliminary( + handler: &mut EvmHandler, + address_str: String, + transfer: Transfer, +) -> Result { + let Some(target) = Contract::from_b58check(&address_str).ok() else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: invalid target address string" + ); + return Ok((None, Some(revert_withdrawal()))); + }; + + let base_transfer_value = transfer.value; + + let amount = match mutez_from_wei(base_transfer_value) { + Ok(amount) => amount, + Err(ErrorMutezFromWei::NonNullRemainder) => { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: rounding would lose wei" + ); + return Ok((None, Some(revert_withdrawal()))); + } + Err(ErrorMutezFromWei::AmountTooLarge) => { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: amount is too large" + ); + return Ok((None, Some(revert_withdrawal()))); + } + }; + + // Burn the withdrawn amount + let mut withdrawal_precompiled = handler.get_or_create_account(WITHDRAWAL_ADDRESS)?; + withdrawal_precompiled.balance_remove(handler.borrow_host(), base_transfer_value)?; + + log!( + handler.borrow_host(), + Info, + "Withdrawal of {} to {:?}", + amount, + target + ); + + let Some(ticketer) = read_ticketer(handler.borrow_host()) else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to read ticketer" + ); + return Ok((None, Some(revert_withdrawal()))); + }; + + let mut system = handler.get_or_create_account(SYSTEM_ACCOUNT_ADDRESS)?; + let withdrawal_id = + system.withdrawal_counter_get_and_increment(handler.borrow_host())?; + + let Some(ticket) = FA2_1Ticket::new( + Contract::Originated(ticketer.clone()), + MichelsonPair(0.into(), MichelsonOption(None)), + amount, + ) + .ok() else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: ticket amount is invalid" + ); + return Ok((None, Some(revert_withdrawal()))); + }; + + let outcome = WithdrawalBase { + ticketer, + base_transfer_value, + target, + withdrawal_id, + ticket, + }; + + Ok((Some(outcome), None)) +} + +pub fn emit_log_and_return( + handler: &mut EvmHandler, + message: Withdrawal, + estimated_ticks: u64, + withdrawal_event: Log, +) -> Result { + // TODO we need to measure number of ticks and translate this number into + // Ethereum gas units + + let withdrawals = vec![message]; + + handler + .add_log(withdrawal_event) + .map_err(|e| EthereumError::WrappedError(Cow::from(format!("{:?}", e))))?; + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: vec![], + withdrawals, + estimated_ticks, + }) +} + /// Implementation of Etherlink specific withdrawals precompiled contract. pub fn withdrawal_precompile( handler: &mut EvmHandler, @@ -100,14 +265,6 @@ pub fn withdrawal_precompile( transfer: Option, ) -> Result { let estimated_ticks = fail_if_too_much!(tick_model::ticks_of_withdraw(), handler); - fn revert_withdrawal() -> PrecompileOutcome { - PrecompileOutcome { - exit_status: ExitReason::Revert(ExitRevert::Reverted), - output: vec![], - withdrawals: vec![], - estimated_ticks: tick_model::ticks_of_withdraw(), - } - } let estimated_gas_cost = estimate_gas_cost(estimated_ticks, handler.gas_price()); if let Err(err) = handler.record_cost(estimated_gas_cost) { @@ -145,6 +302,7 @@ pub fn withdrawal_precompile( match input { // "cda4fee2" is the function selector for `withdraw_base58(string)` [0xcd, 0xa4, 0xfe, 0xe2, input_data @ ..] => { + // Execute base withdrawal preliminary let Some(address_str) = abi::string_parameter(input_data, 0) else { log!( handler.borrow_host(), @@ -154,112 +312,172 @@ pub fn withdrawal_precompile( return Ok(revert_withdrawal()); }; - let Some(target) = Contract::from_b58check(address_str).ok() else { + let (base_withdraw, precompile_outcome) = + base_withdrawal_preliminary(handler, address_str.to_string(), transfer)?; + + if let Some(precompile_outcome) = precompile_outcome { + return Ok(precompile_outcome); + } + + let Some(WithdrawalBase { + ticketer, + base_transfer_value, + target, + withdrawal_id, + ticket, + }) = base_withdraw + else { log!( handler.borrow_host(), Info, - "Withdrawal precompiled contract: invalid target address string" + "Withdrawal precompiled contract: failed to execute base withdrawal" ); return Ok(revert_withdrawal()); }; - let amount = match mutez_from_wei(transfer.value) { - Ok(amount) => amount, - Err(ErrorMutezFromWei::NonNullRemainder) => { - log!( - handler.borrow_host(), - Info, - "Withdrawal precompiled contract: rounding would lose wei" - ); - return Ok(revert_withdrawal()); - } - Err(ErrorMutezFromWei::AmountTooLarge) => { - log!( - handler.borrow_host(), - Info, - "Withdrawal precompiled contract: amount is too large" - ); - return Ok(revert_withdrawal()); - } - }; - - // Burn the withdrawn amount - let mut withdrawal_precompiled = - handler.get_or_create_account(WITHDRAWAL_ADDRESS)?; - withdrawal_precompiled - .balance_remove(handler.borrow_host(), transfer.value)?; - - log!( - handler.borrow_host(), - Info, - "Withdrawal of {} to {:?}", - amount, - target + // Prepare the outbox message + let parameters = MichelsonPair::( + MichelsonContract(target.clone()), + ticket, ); - let Some(ticketer) = read_ticketer(handler.borrow_host()) else { + let Some(message) = + prepare_message(parameters, Contract::Originated(ticketer), Some("burn")) + else { log!( handler.borrow_host(), Info, - "Withdrawal precompiled contract: failed to read ticketer" + "Withdrawal precompiled contract: failed to encode outbox message" ); return Ok(revert_withdrawal()); }; - let Some(ticket) = FA2_1Ticket::new( - Contract::Originated(ticketer.clone()), - MichelsonPair(0.into(), MichelsonOption(None)), - amount, - ) - .ok() else { + // Emit log and return + let withdrawal_event = event_log( + &base_transfer_value, + &context.caller, + &target, + withdrawal_id, + ); + + emit_log_and_return(handler, message, estimated_ticks, withdrawal_event) + } + // "67a32cd7" is the function selector for `fast_withdraw_base58(string,string,bytes)` + [0x67, 0xa3, 0x2c, 0xd7, input_data @ ..] => { + if !fast_withdrawals_enabled(handler.host) { + let output = "The fast withdrawal feature flag is not enabled, \ + cannot call this entrypoint."; + return Ok(PrecompileOutcome { + exit_status: ExitReason::Revert(ExitRevert::Reverted), + output: output.as_bytes().to_vec(), + withdrawals: vec![], + estimated_ticks: tick_model::ticks_of_withdraw(), + }); + }; + + let Some((address_str, fast_withdrawal, payload)) = + abi::fast_withdrawal_parameters(input_data) + else { log!( handler.borrow_host(), Info, - "Withdrawal precompiled contract: ticket amount is invalid" + "Withdrawal precompiled contract: unable to get address argument" ); return Ok(revert_withdrawal()); }; - let mut system = handler.get_or_create_account(SYSTEM_ACCOUNT_ADDRESS)?; - let withdrawal_id = - system.withdrawal_counter_get_and_increment(handler.borrow_host())?; + // Execute base withdrawal preliminary + let (base_withdraw, precompile_outcome) = + base_withdrawal_preliminary(handler, address_str.to_string(), transfer)?; - // We use the original amount in order not to lose additional information - let withdrawal_event = - event_log(&transfer.value, &context.caller, &target, withdrawal_id); + if let Some(precompile_outcome) = precompile_outcome { + return Ok(precompile_outcome); + } - let parameters = MichelsonPair::( - MichelsonContract(target), + let Some(WithdrawalBase { + ticketer: _, + base_transfer_value, + target, + withdrawal_id, ticket, - ); + }) = base_withdraw + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to execute base withdrawal" + ); + return Ok(revert_withdrawal()); + }; - let Some(message) = - prepare_message(parameters, Contract::Originated(ticketer), Some("burn")) + // Prepare the outbox message + let Some(fast_withdrawal) = + ContractKt1Hash::from_b58check(fast_withdrawal).ok() else { log!( handler.borrow_host(), Info, - "Withdrawal precompiled contract: failed to encode outbox message" + "Withdrawal precompiled contract: failed to read the fast withdrawal + contract address" + ); + return Ok(revert_withdrawal()); + }; + + let timestamp_u256 = handler.block_timestamp(); + + let Some(withdrawal_id_nat) = + MichelsonNat::new(Zarith(u256_to_bigint(withdrawal_id))) + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: the withdrawal id is negative" ); return Ok(revert_withdrawal()); }; - // TODO we need to measure number of ticks and translate this number into - // Ethereum gas units + let mut payload_event = payload.clone(); - let withdrawals = vec![message]; + let bytes_payload = MichelsonBytes(payload); - // Emit withdrawal event - handler.add_log(withdrawal_event).map_err(|e| { - EthereumError::WrappedError(Cow::from(format!("{:?}", e))) - })?; + let timestamp: MichelsonTimestamp = + MichelsonTimestamp(Zarith(u256_to_bigint(timestamp_u256))); + let contract_address = MichelsonContract(target.clone()); - Ok(PrecompileOutcome { - exit_status: ExitReason::Succeed(ExitSucceed::Returned), - output: vec![], - withdrawals, - estimated_ticks, - }) + let parameters = MichelsonPair( + withdrawal_id_nat, + MichelsonPair( + ticket, + MichelsonPair( + timestamp, + MichelsonPair(contract_address, bytes_payload), + ), + ), + ); + + let Some(message) = prepare_fast_withdraw_message( + parameters, + Contract::Originated(fast_withdrawal), + Some("default"), + ) else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to encode outbox message" + ); + return Ok(revert_withdrawal()); + }; + + // Emit log and return + let withdrawal_event = event_log_fast_withdrawal( + &withdrawal_id, + &base_transfer_value, + ×tamp_u256, + &target, + &mut payload_event, + ); + + emit_log_and_return(handler, message, estimated_ticks, withdrawal_event) } // TODO A contract "function" to do withdrawal to byte encoded address _ => { @@ -302,19 +520,67 @@ fn event_log( debug_assert!(data.len() % 32 == 0); Log { - address: H160::zero(), + address: WITHDRAWAL_ADDRESS, topics: vec![H256(WITHDRAWAL_EVENT_TOPIC)], data, } } +/// Construct fast withdrawal event log from parts: +/// * `withdrawal_id` - unique withdrawal ID (incremented on every successful or failed XTZ/FA withdrawal) +/// * `amount` - TEZ amount in wei +/// * `timestamp` - timestamp in milliseconds since Epoch +/// * `receiver` - account on L1 that initiated the fast withdrawal +/// * `payload` - generic payload +fn event_log_fast_withdrawal( + withdrawal_id: &U256, + amount: &U256, + timestamp: &U256, + receiver: &Contract, + payload: &mut Vec, +) -> Log { + let mut data = vec![]; + + // It is safe to unwrap, underlying implementation never fails (always returns Ok(())) + receiver.bin_write(&mut data).unwrap(); + data.extend_from_slice(&ABI_B22_RIGHT_PADDING); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&Into::<[u8; 32]>::into(*withdrawal_id)); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&Into::<[u8; 32]>::into(*amount)); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&Into::<[u8; 32]>::into(*timestamp)); + debug_assert!(data.len() % 32 == 0); + + // 4*32 bytes of arguments (receiver, withdrawal_id, amount, timestamp) + // + 1*32 bytes its position + let payload_position = U256::from(32 * 5); + data.extend_from_slice(&Into::<[u8; 32]>::into(payload_position)); + debug_assert!(data.len() % 32 == 0); + let payload_size = payload.len(); + data.extend_from_slice(&Into::<[u8; 32]>::into(U256::from(payload_size))); + debug_assert!(data.len() % 32 == 0); + let payload_padding = payload_size % 32; + payload.resize(payload_size + payload_padding, 0); + data.extend_from_slice(payload); + + Log { + address: WITHDRAWAL_ADDRESS, + topics: vec![H256(FAST_WITHDRAWAL_EVENT_TOPIC)], + data, + } +} + #[cfg(test)] mod tests { use crate::{ handler::{ExecutionOutcome, ExecutionResult, Withdrawal}, precompiles::{ test_helpers::{execute_precompiled, DUMMY_TICKETER}, - withdrawal::WITHDRAWAL_EVENT_TOPIC, + withdrawal::{FAST_WITHDRAWAL_EVENT_TOPIC, WITHDRAWAL_EVENT_TOPIC}, WITHDRAWAL_ADDRESS, }, }; @@ -371,6 +637,15 @@ mod tests { ); } + #[test] + fn fast_withdrawal_event_signature() { + assert_eq!( + FAST_WITHDRAWAL_EVENT_TOPIC.to_vec(), + Keccak256::digest(b"FastWithdrawal(bytes22,uint256,uint256,uint256,bytes)") + .to_vec() + ); + } + #[test] fn withdrawal_event_codec() { assert_eq!(events::Withdrawal::SIGNATURE_HASH.0, WITHDRAWAL_EVENT_TOPIC); @@ -460,7 +735,7 @@ mod tests { + 15_000_000; // cost of calling withdrawal precompiled contract (hard cap because of low gas price) let expected_log = Log { - address: H160::zero(), + address: WITHDRAWAL_ADDRESS, topics: vec![H256(WITHDRAWAL_EVENT_TOPIC)], data: [ [ @@ -538,7 +813,7 @@ mod tests { + 15_000_000; // cost of calling withdrawal precompiled contract (hard cap because of low gas price) let expected_log = Log { - address: H160::zero(), + address: WITHDRAWAL_ADDRESS, topics: vec![H256(WITHDRAWAL_EVENT_TOPIC)], data: [ [ diff --git a/etherlink/kernel_evm/evm_execution/src/utilities.rs b/etherlink/kernel_evm/evm_execution/src/utilities.rs index 3c14ecc4955abfd7286e3547c34b6717fc791390..05c73fc5cac966ccc3371446c4141466d882688a 100644 --- a/etherlink/kernel_evm/evm_execution/src/utilities.rs +++ b/etherlink/kernel_evm/evm_execution/src/utilities.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Functori +// SPDX-FileCopyrightText: 2024-2025 Functori // SPDX-FileCopyrightText: 2023 draganrakita // // SPDX-License-Identifier: MIT @@ -6,7 +6,7 @@ use core::cmp::min; use alloc::vec::Vec; -use num_bigint::BigInt; +use num_bigint::{BigInt, Sign}; use primitive_types::{H160, H256, U256}; use sha3::{Digest, Keccak256}; @@ -70,3 +70,10 @@ pub fn bigint_to_u256(value: &BigInt) -> Result { } Ok(U256::from_little_endian(&bytes)) } + +/// Converts a U256 to a BigInt +pub fn u256_to_bigint(value: U256) -> BigInt { + let mut bytes = vec![0u8; 32]; + value.to_big_endian(&mut bytes); + BigInt::from_bytes_be(Sign::Plus, &bytes) +} diff --git a/etherlink/kernel_evm/kernel/src/apply.rs b/etherlink/kernel_evm/kernel/src/apply.rs index 19e346888a65016e7c76524e1569ff701e91d0e0..e7328911d3649dcdb237d0208d600612d20ed7e3 100644 --- a/etherlink/kernel_evm/kernel/src/apply.rs +++ b/etherlink/kernel_evm/kernel/src/apply.rs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2023 Marigold -// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023, 2025 Functori // SPDX-FileCopyrightText: 2022-2024 TriliTech // SPDX-FileCopyrightText: 2023 Nomadic Labs // SPDX-FileCopyrightText: 2023-2024 PK Lab @@ -11,7 +11,8 @@ use evm_execution::account_storage::{EthereumAccount, EthereumAccountStorage}; use evm_execution::fa_bridge::deposit::FaDeposit; use evm_execution::fa_bridge::{execute_fa_deposit, FA_DEPOSIT_PROXY_GAS_LIMIT}; use evm_execution::handler::{ - ExecutionOutcome, ExecutionResult as ExecutionOutcomeResult, RouterInterface, + ExecutionOutcome, ExecutionResult as ExecutionOutcomeResult, FastWithdrawalInterface, + RouterInterface, }; use evm_execution::precompiles::{self, PrecompileBTreeMap}; use evm_execution::run_transaction; @@ -552,9 +553,18 @@ pub fn handle_transaction_result( log!(host, Benchmarking, "gas_used: {:?}", outcome.gas_used); log!(host, Benchmarking, "reason: {:?}", outcome.result); for message in outcome.withdrawals.drain(..) { - let outbox_message: OutboxMessage = message; - let len = outbox_queue.queue_message(host, outbox_message)?; - log!(host, Debug, "Length of the outbox queue: {}", len); + match message { + evm_execution::handler::Withdrawal::Standard(message) => { + let outbox_message: OutboxMessage = message; + let len = outbox_queue.queue_message(host, outbox_message)?; + log!(host, Debug, "Length of the outbox queue: {}", len); + } + evm_execution::handler::Withdrawal::Fast(message) => { + let outbox_message: OutboxMessage = message; + let len = outbox_queue.queue_message(host, outbox_message)?; + log!(host, Debug, "Length of the outbox queue: {}", len); + } + } } } diff --git a/etherlink/tezos_contracts/fast_withdrawal.abi b/etherlink/tezos_contracts/fast_withdrawal.abi new file mode 100644 index 0000000000000000000000000000000000000000..1d2c27c57aafe0dbdd748f133a5cd74c528b8310 --- /dev/null +++ b/etherlink/tezos_contracts/fast_withdrawal.abi @@ -0,0 +1,13 @@ +[ + { + "type": "function", + "name": "fast_withdraw_base58", + "constant": false, + "payable": true, + "inputs": [ + { "type": "string", "name": "target" }, + { "type": "string", "name": "fast_withdrawal_contract" }, + { "type": "bytes", "name": "payload" } + ] + } +] diff --git a/etherlink/tezt/lib/contract_path.ml b/etherlink/tezt/lib/contract_path.ml index db7a6c2a91818f409bd33e6898a7c6000c09c229..45e1eaad5b7f3c87eccf92a5231ae6933de1cdf8 100644 --- a/etherlink/tezt/lib/contract_path.ml +++ b/etherlink/tezt/lib/contract_path.ml @@ -16,9 +16,18 @@ let admin_path () = Base.(project_root // "etherlink/tezos_contracts/admin.tz") let withdrawal_abi_path () = Base.(project_root // "etherlink/tezos_contracts/withdrawal.abi") +let fast_withdrawal_path () = + Base.(project_root // "etherlink/tezos_contracts/fast_withdrawal_mockup.tz") + +let fast_withdrawal_abi_path () = + Base.(project_root // "etherlink/tezos_contracts/fast_withdrawal.abi") + let fa_withdrawal_abi_path () = Base.(project_root // "etherlink/tezos_contracts/fa_withdrawal.abi") +let service_provider_path () = + Base.(project_root // "etherlink/tezos_contracts/service_provider.tz") + let delayed_path ~kernel = (* The path to the delayed transaction bridge depends on the version of the kernel. The versions which don't support chunking must use diff --git a/etherlink/tezt/lib/durable_storage_path.ml b/etherlink/tezt/lib/durable_storage_path.ml index 44a44518948550e9346e5b771a44a1074f02941b..973e60469402bc7be8db775c7ecd007d6b1c8086 100644 --- a/etherlink/tezt/lib/durable_storage_path.ml +++ b/etherlink/tezt/lib/durable_storage_path.ml @@ -91,6 +91,9 @@ let enable_fa_bridge = evm "/feature_flags/enable_fa_bridge" let enable_multichain = evm "/feature_flags/enable_multichain" +let enable_fast_withdrawal = + evm "/world_state/feature_flags/enable_fast_withdrawal" + module Ticket_table = struct let ticket_table = sf diff --git a/etherlink/tezt/lib/durable_storage_path.mli b/etherlink/tezt/lib/durable_storage_path.mli index 91d66ab4944321dd8ffc3e519afa94e782c06bf3..fd29e603f75b3e00aad80950dfaee7ed6e01f8ab 100644 --- a/etherlink/tezt/lib/durable_storage_path.mli +++ b/etherlink/tezt/lib/durable_storage_path.mli @@ -110,6 +110,9 @@ val enable_fa_bridge : path (** [enable_multichain] is the path to the feature flag to activate multichain functions *) val enable_multichain : path +(** [enable_fast_withdrawal] is the path to the feature flag to activate fast withdrawals. *) +val enable_fast_withdrawal : path + module Ticket_table : sig (** [balance ~ticket_hash ~account] returns the path where the balance of [account] of ticket [ticket_hash] is. *) diff --git a/etherlink/tezt/lib/eth_cli.ml b/etherlink/tezt/lib/eth_cli.ml index 503e0456994b25b9fc75ea612eef5c0c4540e3a4..2046f03f9a2dbfe9c00ab594e70b8724e79bea27 100644 --- a/etherlink/tezt/lib/eth_cli.ml +++ b/etherlink/tezt/lib/eth_cli.ml @@ -184,6 +184,12 @@ let encode_method ?runner ~abi_label ~method_ () = in return (String.trim data) +let decode_method ?runner ~abi_label ~method_ () = + let* data = + spawn_command_and_read_string ?runner ["method:decode"; abi_label; method_] + in + return (String.trim data) + let gen_eth_account ?runner () = spawn_command_and_read_json ?runner ["address:random"] (fun json -> Some (Eth_account.of_json json)) diff --git a/etherlink/tezt/lib/eth_cli.mli b/etherlink/tezt/lib/eth_cli.mli index 22bca2549e48cb59759322ae32666f0ecc0cb9f6..a9b6b0647bb50d390cfae94f84d70f8e6b96ecce 100644 --- a/etherlink/tezt/lib/eth_cli.mli +++ b/etherlink/tezt/lib/eth_cli.mli @@ -211,6 +211,9 @@ val get_receipt : val encode_method : ?runner:Runner.t -> abi_label:string -> method_:string -> unit -> string Lwt.t +val decode_method : + ?runner:Runner.t -> abi_label:string -> method_:string -> unit -> string Lwt.t + (** [gen_eth_account ()] genarate a fresh eth account. The [runner] optional argument can be specified to execute the command on a remote runner. If omitted, the command is executed locally.*) diff --git a/etherlink/tezt/lib/evm_node.ml b/etherlink/tezt/lib/evm_node.ml index 58d0991747097b74efe0b815b2c3cafa6fb882e9..2c0639566928112c179b9a0278ee2e2f751e5314 100644 --- a/etherlink/tezt/lib/evm_node.ml +++ b/etherlink/tezt/lib/evm_node.ml @@ -1589,7 +1589,8 @@ let make_kernel_installer_config ?max_delayed_inbox_blueprint_length ?maximum_gas_per_transaction ?(max_blueprint_lookahead_in_seconds = ten_years_in_seconds) ?(set_account_code = []) ?(enable_fa_bridge = false) ?(enable_dal = false) - ?dal_slots ?(enable_multichain = false) ~output () = + ?dal_slots ?(enable_fast_withdrawal = false) ?(enable_multichain = false) + ~output () = let set_account_code = List.flatten @@ List.map @@ -1650,6 +1651,7 @@ let make_kernel_installer_config ?max_delayed_inbox_blueprint_length @ Cli_arg.optional_switch "enable-fa-bridge" enable_fa_bridge @ Cli_arg.optional_switch "enable-multichain" enable_multichain @ Cli_arg.optional_switch "enable-dal" enable_dal + @ Cli_arg.optional_switch "enable-fast-withdrawal" enable_fast_withdrawal @ Cli_arg.optional_arg "dal-slots" (fun l -> String.concat "," (List.map string_of_int l)) diff --git a/etherlink/tezt/lib/evm_node.mli b/etherlink/tezt/lib/evm_node.mli index 9a9c166f1b8ed1530b4f92dd12cb15ba7e3c3369..aeb9ac66c291e4d3a30b156e1f33d6ee9b1d43a8 100644 --- a/etherlink/tezt/lib/evm_node.mli +++ b/etherlink/tezt/lib/evm_node.mli @@ -578,6 +578,7 @@ val make_kernel_installer_config : ?enable_fa_bridge:bool -> ?enable_dal:bool -> ?dal_slots:int list -> + ?enable_fast_withdrawal:bool -> ?enable_multichain:bool -> output:string -> unit -> diff --git a/etherlink/tezt/lib/setup.ml b/etherlink/tezt/lib/setup.ml index d976fcccb3775a762daa0d2105117ae403d5f3ac..336fec316e94bb41ce8941eb4c178e4f27a342c7 100644 --- a/etherlink/tezt/lib/setup.ml +++ b/etherlink/tezt/lib/setup.ml @@ -201,7 +201,8 @@ let setup_sequencer ?max_delayed_inbox_blueprint_length ?next_wasm_runtime ?(kernel = Constant.WASM.evm_kernel) ?da_fee ?minimum_base_fee_per_gas ?preimages_dir ?maximum_allowed_ticks ?maximum_gas_per_transaction ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge - ?(threshold_encryption = false) ?(drop_duplicate_when_injection = true) + ?enable_fast_withdrawal ?(threshold_encryption = false) + ?(drop_duplicate_when_injection = true) ?(blueprints_publisher_order_enabled = true) ?rollup_history_mode ~enable_dal ?dal_slots ~enable_multichain ?rpc_server ?websockets ?history_mode protocol = @@ -256,6 +257,7 @@ let setup_sequencer ?max_delayed_inbox_blueprint_length ?next_wasm_runtime ?maximum_allowed_ticks ?maximum_gas_per_transaction ~enable_dal + ?enable_fast_withdrawal ?dal_slots ~enable_multichain ?max_blueprint_lookahead_in_seconds @@ -388,9 +390,10 @@ let register_test ~__FILE__ ?max_delayed_inbox_blueprint_length ?delayed_inbox_min_levels ?max_number_of_chunks ?bootstrap_accounts ?sequencer ?sequencer_pool_address ~kernel ?da_fee ?minimum_base_fee_per_gas ?preimages_dir ?maximum_allowed_ticks ?maximum_gas_per_transaction - ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge ?commitment_period - ?challenge_window ?(threshold_encryption = false) ?(uses = uses) - ?(additional_uses = []) ?rollup_history_mode ~enable_dal + ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge + ?enable_fast_withdrawal ?commitment_period ?challenge_window + ?(threshold_encryption = false) ?(uses = uses) ?(additional_uses = []) + ?rollup_history_mode ~enable_dal ?(dal_slots = if enable_dal then Some [0; 1; 2; 3] else None) ~enable_multichain ?rpc_server ?websockets ?history_mode body ~title ~tags protocols = @@ -437,6 +440,7 @@ let register_test ~__FILE__ ?max_delayed_inbox_blueprint_length ?maximum_gas_per_transaction ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge + ?enable_fast_withdrawal ~threshold_encryption ?rollup_history_mode ?websockets @@ -488,8 +492,8 @@ let register_test_for_kernels ~__FILE__ ?max_delayed_inbox_blueprint_length ?maximum_gas_per_transaction ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge ?rollup_history_mode ?commitment_period ?challenge_window ?additional_uses ~threshold_encryption ~enable_dal ?dal_slots - ~enable_multichain ?rpc_server ?websockets ?history_mode ~title ~tags body - protocols = + ~enable_multichain ?rpc_server ?websockets ?enable_fast_withdrawal + ?history_mode ~title ~tags body protocols = List.iter (fun kernel -> register_test @@ -519,6 +523,7 @@ let register_test_for_kernels ~__FILE__ ?max_delayed_inbox_blueprint_length ?maximum_gas_per_transaction ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge + ?enable_fast_withdrawal ?additional_uses ?rpc_server ?websockets diff --git a/etherlink/tezt/lib/setup.mli b/etherlink/tezt/lib/setup.mli index b2569694f6d0397850a7645a7edce408104f8630..9c704292ca0183e69890abed7087a08b874c8007 100644 --- a/etherlink/tezt/lib/setup.mli +++ b/etherlink/tezt/lib/setup.mli @@ -82,6 +82,7 @@ val register_test : ?maximum_gas_per_transaction:int64 -> ?max_blueprint_lookahead_in_seconds:int64 -> ?enable_fa_bridge:bool -> + ?enable_fast_withdrawal:bool -> ?commitment_period:int -> ?challenge_window:int -> ?threshold_encryption:bool -> @@ -138,6 +139,7 @@ val register_test_for_kernels : enable_multichain:bool -> ?rpc_server:Evm_node.rpc_server -> ?websockets:bool -> + ?enable_fast_withdrawal:bool -> ?history_mode:Evm_node.history_mode -> title:string -> tags:string list -> @@ -173,6 +175,7 @@ val setup_sequencer : ?maximum_gas_per_transaction:int64 -> ?max_blueprint_lookahead_in_seconds:int64 -> ?enable_fa_bridge:bool -> + ?enable_fast_withdrawal:bool -> ?threshold_encryption:bool -> ?drop_duplicate_when_injection:bool -> ?blueprints_publisher_order_enabled:bool -> diff --git a/etherlink/tezt/tests/evm_rollup.ml b/etherlink/tezt/tests/evm_rollup.ml index f227a8a93bc3626ddd9223ead79e07c01443b5ba..0c8f9b90b7b805c6707058f7a39dbfefb2230b6e 100644 --- a/etherlink/tezt/tests/evm_rollup.ml +++ b/etherlink/tezt/tests/evm_rollup.ml @@ -55,6 +55,7 @@ type l1_contracts = { kernel_governance : string; kernel_security_governance : string; sequencer_governance : string option; + fast_withdrawal_contract_address : string; } type full_evm_setup = { @@ -277,6 +278,16 @@ let setup_l1_contracts ~admin ?sequencer_admin client = return (Some sequencer_admin) | None -> return None in + let* fast_withdrawal_contract_address = + Client.originate_contract + ~alias:"fast_withdrawal_contract_address" + ~amount:Tez.zero + ~src:Constant.bootstrap5.public_key_hash + ~init:(sf "Pair %S {}" exchanger) + ~prg:(fast_withdrawal_path ()) + ~burn_cap:Tez.one + client + in return { exchanger; @@ -285,6 +296,7 @@ let setup_l1_contracts ~admin ?sequencer_admin client = kernel_governance; sequencer_governance; kernel_security_governance; + fast_withdrawal_contract_address; } type setup_mode = @@ -310,7 +322,8 @@ let setup_evm_kernel ?additional_config ?(setup_kernel_root_hash = true) ?max_number_of_chunks ?(setup_mode = Setup_proxy) ?(force_install_kernel = true) ?whitelist ?maximum_allowed_ticks ?restricted_rpcs ?(enable_dal = false) ?dal_slots - ?(enable_multichain = false) ?websockets protocol = + ?(enable_multichain = false) ?websockets ?(enable_fast_withdrawal = false) + protocol = let _, kernel_installee = Kernel.to_uses_and_tags kernel in let* node, client = setup_l1 ?commitment_period ?challenge_window ?timestamp protocol @@ -384,6 +397,7 @@ let setup_evm_kernel ?additional_config ?(setup_kernel_root_hash = true) ~output:output_config ~enable_dal ~enable_multichain + ~enable_fast_withdrawal ?dal_slots () in @@ -522,7 +536,7 @@ let register_test ~title ~tags ?(kernels = Kernel.all) ?additional_config ?admin ?bootstrap_accounts ?whitelist ?da_fee_per_byte ?minimum_base_fee_per_gas ?rollup_operator_key ?maximum_allowed_ticks ?restricted_rpcs ~setup_mode ~enable_dal ?(dal_slots = if enable_dal then Some [4] else None) - ~enable_multichain ?websockets f protocols = + ~enable_multichain ?websockets ?enable_fast_withdrawal f protocols = let extra_tag = match setup_mode with | Setup_proxy -> "proxy" @@ -577,6 +591,7 @@ let register_test ~title ~tags ?(kernels = Kernel.all) ?additional_config ?admin ?dal_slots ~enable_multichain ?websockets + ?enable_fast_withdrawal protocol in f ~protocol ~evm_setup) @@ -586,7 +601,8 @@ let register_test ~title ~tags ?(kernels = Kernel.all) ?additional_config ?admin let register_proxy ~title ~tags ?kernels ?additional_uses ?additional_config ?admin ?commitment_period ?challenge_window ?bootstrap_accounts ?da_fee_per_byte ?minimum_base_fee_per_gas ?whitelist ?rollup_operator_key - ?maximum_allowed_ticks ?restricted_rpcs ?websockets f protocols = + ?maximum_allowed_ticks ?restricted_rpcs ?websockets ?enable_fast_withdrawal + f protocols = let register ~enable_dal ~enable_multichain : unit = register_test ~title @@ -605,6 +621,7 @@ let register_proxy ~title ~tags ?kernels ?additional_uses ?additional_config ?maximum_allowed_ticks ?restricted_rpcs ?websockets + ?enable_fast_withdrawal f protocols ~enable_dal @@ -2423,6 +2440,34 @@ let call_withdraw ?expect_failure ~sender ~endpoint ~value ~produce_block in wait_for_application ~produce_block call_withdraw +let call_fast_withdraw ?expect_failure ~sender ~endpoint ~value ~produce_block + ~receiver ~fast_withdrawal_contract_address () = + let* () = + Eth_cli.add_abi + ~label:"fast_withdraw_base58" + ~abi:(fast_withdrawal_abi_path ()) + () + in + let call_fast_withdraw = + Eth_cli.contract_send + ?expect_failure + ~source_private_key:sender.Eth_account.private_key + ~endpoint + ~abi_label:"fast_withdraw_base58" + ~address:"0xff00000000000000000000000000000000000001" + (* NB: the third parameter is unused for now, could be used later for + maximum fees to pay, whitelist of service providers etc. *) + ~method_call: + (sf + {|fast_withdraw_base58("%s","%s","%s")|} + receiver + fast_withdrawal_contract_address + "0x0000000000000000000000000000000000000000000000000000000000000001") + ~value + ~gas:16_000_000 + in + wait_for_application ~produce_block call_fast_withdraw + let withdraw ~commitment_period ~challenge_window ~amount_wei ~sender ~receiver ~produce_block ~evm_node ~sc_rollup_node ~sc_rollup_address ~client ~endpoint = @@ -2450,6 +2495,34 @@ let withdraw ~commitment_period ~challenge_window ~amount_wei ~sender ~receiver in unit +let fast_withdraw ~commitment_period ~challenge_window ~amount_wei ~sender + ~receiver ~fast_withdrawal_contract_address ~produce_block ~evm_node + ~sc_rollup_node ~sc_rollup_address ~client ~endpoint = + let* withdrawal_level = Client.level client in + (* Call the withdrawal precompiled contract. *) + let* _tx = + call_fast_withdraw + ~produce_block + ~sender + ~endpoint + ~value:amount_wei + ~receiver + ~fast_withdrawal_contract_address + () + in + let* _ = + find_and_execute_withdrawal + ~withdrawal_level + ~commitment_period + ~challenge_window + ~evm_node + ~sc_rollup_node + ~sc_rollup_address + ~client + () + in + unit + let check_balance ~receiver ~endpoint expected_balance = let* balance = Eth_cli.balance ~account:receiver ~endpoint () in let balance = Wei.truncate_to_mutez balance in @@ -2485,6 +2558,7 @@ let test_deposit_and_withdraw = exchanger = _; sequencer_governance = _; kernel_security_governance = _; + _; } = match l1_contracts with | Some x -> x @@ -2539,6 +2613,310 @@ let test_deposit_and_withdraw = ~error_msg:(sf "Expected %%R amount instead of %%L after withdrawal") ; return () +let test_deposit_and_fast_withdraw = + let admin = Constant.bootstrap5 in + let commitment_period = 5 and challenge_window = 5 in + register_proxy + ~tags:["evm"; "deposit"; "fast_withdraw"] + ~title:"Deposit and fast withdraw tez" + ~admin + ~commitment_period + ~challenge_window + ~enable_fast_withdrawal:true + ~kernels:[Kernel.Latest] + @@ fun ~protocol:_ + ~evm_setup: + { + client; + sc_rollup_address; + l1_contracts; + sc_rollup_node; + endpoint; + evm_node; + produce_block; + _; + } -> + let { + bridge; + admin = _; + kernel_governance = _; + exchanger; + sequencer_governance = _; + kernel_security_governance = _; + fast_withdrawal_contract_address; + } = + match l1_contracts with + | Some x -> x + | None -> Test.fail ~__LOC__ "The test needs the L1 bridge" + in + let* service_provider = + Client.originate_contract + ~alias:"service_provider" + ~amount:Tez.zero + ~src:Constant.bootstrap1.public_key_hash + ~init: + "Pair \"KT1CeFqjJRJPNVvhvznQrWfHad2jCiDZ6Lyj\" \ + \"KT1CeFqjJRJPNVvhvznQrWfHad2jCiDZ6Lyj\" 0 \ + \"tz1etHLky7fuVumvBDi92ogXQZZPESiFimWR\" 0 \ + \"tz1etHLky7fuVumvBDi92ogXQZZPESiFimWR\" 0x00" + ~prg:(service_provider_path ()) + ~burn_cap:(Tez.of_int 890) + client + in + let withdraw_amount = Tez.of_int 50 in + (* Define the service provider's Tezos public key hash (PKH). This address will act as the service provider in the test. *) + let service_provider_pkh = "tz1TGKSrZrBpND3PELJ43nVdyadoeiM1WMzb" in + let* initial_service_provider_balance = + Client.get_balance_for ~account:service_provider_pkh client + in + + let fast_withdrawal_event_signature = + "FastWithdrawal(bytes22,uint256,uint256,uint256,bytes)" + in + (* Define the fast withdrawal event log topic, which will be searched for in the EVM logs. + This topic is a hashed identifier that corresponds to the fast withdrawal transaction event. *) + let fast_withdrawal_event_topic = + Tezos_crypto.Hacl.Hash.Keccak_256.digest + (Bytes.of_string fast_withdrawal_event_signature) + |> Hex.of_bytes |> Hex.show |> add_0x + in + + (* Function to extract the event data from the EVM logs based on the provided topic. + This searches the transaction logs for the specified topic and returns the corresponding data. *) + let extract_data_by_topic ~topic (logs : Transaction.tx_log list) = + List.find_opt + (fun tx -> + match tx.Transaction.topics with + | [x] when String.equal x topic -> true + | _ -> false) + logs + |> Option.map (fun tx_log -> tx_log.Transaction.data) + in + + (* Function to execute the payout by invoking the `payout_proxy` entrypoint on the L1 contract. + The payout transfers funds to the service provider based on the withdrawal details. *) + let execute_payout ~withdrawal_id ~fast_withdrawal_contract_address ~target + ~timestamp ~service_provider_proxy ~payload = + Client.transfer + ~force:true + ~wait:"1" + ~fee:(Tez.of_int 1) (* Small fee for the transaction *) + ~fee_cap:(Tez.of_int 1) + ~gas_limit:100_000_000 + ~storage_limit:Int.max_int + ~burn_cap:(Tez.of_int 100) + ~amount:withdraw_amount + (* Transfer 50 tez as part of the payout to the service provider *) + ~giver:Constant.bootstrap1.public_key_hash + ~receiver:service_provider_proxy + ~entrypoint:"payout_proxy" + ~arg: + (Printf.sprintf + "(Pair %S %S %s %S %s %S %s)" + fast_withdrawal_contract_address + exchanger + withdrawal_id + target + timestamp + service_provider_pkh + payload) + client + in + + (* Function to handle the payout after detecting the fast withdrawal event. + It continuously checks the EVM logs for the fast withdrawal event and executes the payout once it's found. *) + let payout_service_provider ~withdraw_receiver = + let rec loop_until_event_detected () = + let*@ all_logs = Rpc.get_logs ~from_block:(Number 0) evm_node in + let data = + extract_data_by_topic ~topic:fast_withdrawal_event_topic all_logs + |> Option.map (fun s -> String.sub s 2 (String.length s - 2)) + in + let* () = Lwt_unix.sleep 1. in + match data with + | Some data -> return data + | None -> loop_until_event_detected () + in + let* data = loop_until_event_detected () in + (* Decode the fast withdrawal event to extract the withdrawal details, including target address, withdrawal ID, and timestamp. *) + let* res = + Eth_cli.decode_method + ~abi_label:fast_withdrawal_event_signature + ~method_:(String.sub fast_withdrawal_event_topic 2 8 ^ data) + () + in + let open Ezjsonm in + let _target, withdrawal_id, amount, timestamp, payload = + let json = from_string res in + match json with + | `A + [ + `String target; + `String withdrawal_id; + `String amount; + `String timestamp; + `String payload; + ] -> + (target, withdrawal_id, amount, timestamp, payload) + | _ -> assert false + in + assert (Wei.of_tez withdraw_amount = Wei.of_string amount) ; + + (* Execute the payout to the service provider using the withdrawal details extracted from the event. *) + let* () = + execute_payout + ~service_provider_proxy:service_provider + ~withdrawal_id + ~fast_withdrawal_contract_address + ~target:withdraw_receiver + ~timestamp + ~payload + in + Lwt.return_unit + in + (* Define the amount to deposit in tez (100 tez), and specify the Ethereum-based receiver for the rollup. *) + let deposit_amount = Tez.of_int 100 in + let receiver = + Eth_account. + { + address = "0x1074Fd1EC02cbeaa5A90450505cF3B48D834f3EB"; + private_key = + "0xb7c548b5442f5b28236f0dcd619f65aaaafd952240908adcf9642d8e616587ee"; + } + in + + (* Define the Tezos address that will receive the fast withdrawal on L1. *) + let withdraw_receiver = "tz1fp5ncDmqYwYC568fREYz9iwQTgGQuKZqX" in + + (* Check the initial balance of the L1 withdraw receiver. It should be 0 before the fast withdrawal occurs. *) + let* balance_withdraw_receiver = + Client.get_balance_for ~account:withdraw_receiver client + in + Check.((balance_withdraw_receiver = Tez.of_int 0) Tez.typ) + ~error_msg:"Expected %R as initial balance instead of %L" ; + + (* Execute the deposit of 100 tez to the rollup. The depositor is the admin account, and the receiver is the Ethereum address. *) + let* () = + deposit + ~amount_mutez:deposit_amount + ~sc_rollup_address + ~bridge + ~depositor:admin + ~receiver:receiver.address + ~produce_block + client + in + + (* Check that the receiver's balance in the rollup matches the deposited amount. *) + let* () = check_balance ~receiver:receiver.address ~endpoint deposit_amount in + + (* Define the amount for fast withdrawal as 50 tez (half of the deposited amount). *) + let withdraw_amount_wei = Wei.of_tez withdraw_amount in + + (* Perform the fast withdrawal from the rollup, transferring 50 tez to the L1 withdrawal contract. *) + let* _tx = + fast_withdraw + ~produce_block + ~evm_node + ~sc_rollup_address + ~commitment_period + ~challenge_window + ~amount_wei:withdraw_amount_wei + ~sender:receiver + ~receiver:withdraw_receiver + ~fast_withdrawal_contract_address + ~sc_rollup_node + ~client + ~endpoint + and* () = payout_service_provider ~withdraw_receiver in + + let* balance = Client.get_balance_for ~account:withdraw_receiver client in + let* final_withdraw_receiver_balance = + Client.get_balance_for ~account:service_provider_pkh client + in + + (* Check that the L1 withdrawal contract now has a balance of 50 tez after the fast withdrawal is complete. *) + Check.((balance = withdraw_amount) Tez.typ) + ~error_msg:"Expected %R amount instead of %L after withdrawal" ; + + (* Verify that the service provider's balance increased by 50 tez after the fast withdrawal payout. *) + Check.( + (Tez.(initial_service_provider_balance + withdraw_amount) + = final_withdraw_receiver_balance) + Tez.typ) + ~error_msg: + "Expected %R amount instead of %L after outbox message was executed" ; + return () + +let test_fast_withdraw_feature_flag_deactivated = + let admin = Constant.bootstrap5 in + let commitment_period = 5 and challenge_window = 5 in + register_proxy + ~tags:["evm"; "feature_flag"; "fast_withdraw"] + ~title:"Check fast withdraw tez is deactivated with feature flag" + ~admin + ~commitment_period + ~challenge_window + ~enable_fast_withdrawal:false + ~kernels:[Kernel.Latest] + @@ fun ~protocol:_ + ~evm_setup: + { + client; + sc_rollup_address; + l1_contracts; + endpoint; + produce_block; + _; + } -> + let {bridge; fast_withdrawal_contract_address; _} = + match l1_contracts with + | Some x -> x + | None -> Test.fail ~__LOC__ "The test needs the L1 bridge" + in + let withdraw_amount = Tez.of_int 50 in + (* Define the amount to deposit in tez (100 tez), and specify the Ethereum-based receiver for the rollup. *) + let deposit_amount = Tez.of_int 100 in + let receiver = + Eth_account. + { + address = "0x1074Fd1EC02cbeaa5A90450505cF3B48D834f3EB"; + private_key = + "0xb7c548b5442f5b28236f0dcd619f65aaaafd952240908adcf9642d8e616587ee"; + } + in + + (* Define the Tezos address that will receive the fast withdrawal on L1. *) + let withdraw_receiver = "tz1fp5ncDmqYwYC568fREYz9iwQTgGQuKZqX" in + (* Execute the deposit of 100 tez to the rollup. The depositor is the admin account, and the receiver is the Ethereum address. *) + let* () = + deposit + ~amount_mutez:deposit_amount + ~sc_rollup_address + ~bridge + ~depositor:admin + ~receiver:receiver.address + ~produce_block + client + in + (* Define the amount for fast withdrawal as 50 tez (half of the deposited amount). *) + let withdraw_amount_wei = Wei.of_tez withdraw_amount in + + (* Perform the fast withdrawal from the rollup, transferring 50 tez to the L1 withdrawal contract. *) + let* err = + call_fast_withdraw + ~expect_failure:true + ~produce_block + ~value:withdraw_amount_wei + ~sender:receiver + ~receiver:withdraw_receiver + ~fast_withdrawal_contract_address + ~endpoint + () + in + if not (err =~ rex "Error") then Test.fail "Test should fail with error" ; + unit + let test_withdraw_amount = let admin = Constant.bootstrap5 in register_proxy @@ -6257,6 +6635,8 @@ let register_evm_node ~protocols = test_eth_call_input protocols ; test_preinitialized_evm_kernel protocols ; test_deposit_and_withdraw protocols ; + test_deposit_and_fast_withdraw protocols ; + test_fast_withdraw_feature_flag_deactivated protocols ; test_withdraw_amount protocols ; test_withdraw_via_calls protocols ; test_estimate_gas protocols ; diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index 0ce86ca3aaf8d8d6907affa968670fcac17e38a5..9a020ac63e0446655c0935dd3bb8585d3c91d629 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -249,7 +249,7 @@ let register_all ?max_delayed_inbox_blueprint_length ?sequencer_rpc_port ?maximum_allowed_ticks ?maximum_gas_per_transaction ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge ?rollup_history_mode ?commitment_period ?challenge_window ?additional_uses ?rpc_server - ?websockets ?history_mode + ?websockets ?enable_fast_withdrawal ?history_mode ?(use_threshold_encryption = default_threshold_encryption_registration) ?(use_dal = default_dal_registration) ?(use_multichain = default_multichain_registration) ~title ~tags body @@ -310,6 +310,7 @@ let register_all ?max_delayed_inbox_blueprint_length ?sequencer_rpc_port ?maximum_gas_per_transaction ?max_blueprint_lookahead_in_seconds ?enable_fa_bridge + ?enable_fast_withdrawal ?additional_uses ?rpc_server ?websockets @@ -7902,6 +7903,24 @@ let test_multichain_feature_flag = "Multichain feature flag in the durable storage is %L, expected %R" ; unit +let test_fast_withdrawal_feature_flag = + register_all + ~tags:["fast_withdrawal"; "feature_flag"] + ~title:"Fast withdrawal feature is set in storage" + ~enable_fast_withdrawal:true + @@ fun {sequencer; _} _protocol -> + (* We simply check that the flag is set in the storage. *) + let*@ flag = + Rpc.state_value sequencer Durable_storage_path.enable_fast_withdrawal + in + Check.is_true + (Option.is_some flag) + ~error_msg: + (sf + "Expected to have a value at %s" + Durable_storage_path.enable_fast_withdrawal) ; + unit + let test_trace_call = register_all ~kernels:[Latest] @@ -9883,6 +9902,7 @@ let () = test_miner protocols ; test_fa_bridge_feature_flag protocols ; test_multichain_feature_flag protocols ; + test_fast_withdrawal_feature_flag protocols ; test_trace_call protocols ; test_trace_empty_block protocols ; test_trace_block protocols ; diff --git a/etherlink/tezt/tests/expected/evm_sequencer.ml/EVM Node- man.out b/etherlink/tezt/tests/expected/evm_sequencer.ml/EVM Node- man.out index 28b6188a4eb89660e67ab48e3093b894a856491f..e37867e6fd05c61502847129291434a42effae5b 100644 --- a/etherlink/tezt/tests/expected/evm_sequencer.ml/EVM Node- man.out +++ b/etherlink/tezt/tests/expected/evm_sequencer.ml/EVM Node- man.out @@ -414,6 +414,7 @@ Miscellaneous commands: [--bootstrap-account <0x...>] [--set-code <0x...,0x....>] [--enable-fa-bridge] [--enable-dal] [--dal-slots <0,1,4,6,...>] [--enable-multichain] [--max-delayed-inbox-blueprint-length <1000>] + [--enable-fast-withdrawal] Transforms the JSON list of instructions to a RLP list : file path where the config will be written to --mainnet-compat: Generate a configuration compatible with the first Etherlink Mainnet kernel @@ -443,6 +444,7 @@ Miscellaneous commands: --dal-slots <0,1,4,6,...>: value for dal_slots in the installer config --enable-multichain: enable flag enable_multichain in the installer config --max-delayed-inbox-blueprint-length <1000>: value for max_delayed_inbox_blueprint_length in the installer config + --enable-fast-withdrawal: enable flag enable_fast_withdrawal in the installer config patch state at with [--data-dir ] [--block-number ] [-f --force] diff --git a/tezt/lib_tezos/client.ml b/tezt/lib_tezos/client.ml index 73d63466a0f747f4f2bf39d57a4254e1e9b0bbfa..d90fd8e8e23161cd06d47631b1598b59cc80fe34 100644 --- a/tezt/lib_tezos/client.ml +++ b/tezt/lib_tezos/client.ml @@ -1072,8 +1072,9 @@ let add_address ?force client ~alias ~src = spawn_add_address ?force client ~alias ~src |> Process.check let spawn_transfer ?env ?hooks ?log_output ?endpoint ?(wait = "none") ?burn_cap - ?fee ?gas_limit ?safety_guard ?storage_limit ?counter ?entrypoint ?arg - ?(simulation = false) ?(force = false) ~amount ~giver ~receiver client = + ?fee ?fee_cap ?gas_limit ?safety_guard ?storage_limit ?counter ?entrypoint + ?arg ?(simulation = false) ?(force = false) ~amount ~giver ~receiver client + = spawn_command ?env ?log_output @@ -1087,6 +1088,7 @@ let spawn_transfer ?env ?hooks ?log_output ?endpoint ?(wait = "none") ?burn_cap ~some:(fun f -> ["--fee"; Tez.to_string f; "--force-low-fee"]) fee @ optional_arg "burn-cap" Tez.to_string burn_cap + @ optional_arg "fee-cap" Tez.to_string fee_cap @ optional_arg "gas-limit" string_of_int gas_limit @ optional_arg "safety-guard" string_of_int safety_guard @ optional_arg "storage-limit" string_of_int storage_limit @@ -1096,9 +1098,9 @@ let spawn_transfer ?env ?hooks ?log_output ?endpoint ?(wait = "none") ?burn_cap @ (if simulation then ["--simulation"] else []) @ if force then ["--force"] else []) -let transfer ?env ?hooks ?log_output ?endpoint ?wait ?burn_cap ?fee ?gas_limit - ?safety_guard ?storage_limit ?counter ?entrypoint ?arg ?simulation ?force - ?expect_failure ~amount ~giver ~receiver client = +let transfer ?env ?hooks ?log_output ?endpoint ?wait ?burn_cap ?fee ?fee_cap + ?gas_limit ?safety_guard ?storage_limit ?counter ?entrypoint ?arg + ?simulation ?force ?expect_failure ~amount ~giver ~receiver client = spawn_transfer ?env ?log_output @@ -1107,6 +1109,7 @@ let transfer ?env ?hooks ?log_output ?endpoint ?wait ?burn_cap ?fee ?gas_limit ?wait ?burn_cap ?fee + ?fee_cap ?gas_limit ?safety_guard ?storage_limit diff --git a/tezt/lib_tezos/client.mli b/tezt/lib_tezos/client.mli index e656c5075c7a3b061428b82e68a472ab68079272..86b66fe1259b7a001a6bf03f84f80963aa8cde7e 100644 --- a/tezt/lib_tezos/client.mli +++ b/tezt/lib_tezos/client.mli @@ -826,6 +826,7 @@ val transfer : ?wait:string -> ?burn_cap:Tez.t -> ?fee:Tez.t -> + ?fee_cap:Tez.t -> ?gas_limit:int -> ?safety_guard:int -> ?storage_limit:int -> @@ -850,6 +851,7 @@ val spawn_transfer : ?wait:string -> ?burn_cap:Tez.t -> ?fee:Tez.t -> + ?fee_cap:Tez.t -> ?gas_limit:int -> ?safety_guard:int -> ?storage_limit:int ->