From 1408679d5b563b2d449569929933db410717503d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 01:21:40 +0200 Subject: [PATCH 01/22] Tezlink/Kernel/Transfer/Tests: transfer to implicit with an argument This commit tests that transfers to implicit account are forbidden when the argument is not the default one. --- .../kernel_latest/tezos_execution/src/lib.rs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index fbdbd6907c92..60aeaf3bdf73 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -1320,4 +1320,59 @@ mod tests { "Counter should not have been incremented" ); } + + #[test] + fn apply_transfer_with_argument_to_implicit_fails() { + let mut host = MockKernelHost::default(); + let parser = mir::parser::Parser::new(); + + let src = bootstrap1(); + + let dest = bootstrap2(); + + init_account(&mut host, &src.pkh); + reveal_account(&mut host, &src); + + let operation = make_transfer_operation( + 15, + 1, + 4, + 5, + src.clone(), + 30_u64.into(), + Contract::Implicit(dest.pkh), + Some(Parameter { + entrypoint: mir::ast::entrypoint::Entrypoint::default(), + value: parser.parse("0").unwrap().encode(), + }), + ); + + let receipt = + apply_operation(&mut host, &context::Context::init_context(), operation) + .expect("apply_operation should not have failed with a kernel error"); + + let expected_receipt = OperationResultSum::Transfer(OperationResult { + balance_updates: vec![ + BalanceUpdate { + balance: Balance::Account(Contract::Implicit(src.pkh)), + changes: -15, + update_origin: UpdateOrigin::BlockApplication, + }, + BalanceUpdate { + balance: Balance::BlockFees, + changes: 15, + update_origin: UpdateOrigin::BlockApplication, + }, + ], + result: ContentResult::Failed( + vec![OperationError::Apply(ApplyOperationError::Transfer( + TransferError::NonSmartContractExecutionCall, + ))] + .into(), + ), + internal_operation_results: vec![], + }); + + assert_eq!(receipt, expected_receipt); + } } -- GitLab From 0f7a9fd7beafefdf462ca2ffed95f66ab1a76e7e Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Fri, 18 Jul 2025 10:46:44 +0200 Subject: [PATCH 02/22] Tezlink/Kernel/Transfer : Init MIR Context at transfer --- .../kernel_latest/tezos_execution/src/lib.rs | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 60aeaf3bdf73..b07a40ebc0a4 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -4,6 +4,7 @@ use account_storage::TezlinkAccount; use account_storage::{Manager, TezlinkImplicitAccount, TezlinkOriginatedAccount}; +use mir::ast::Entrypoint; use mir::{ ast::{IntoMicheline, Micheline}, context::Ctx, @@ -193,8 +194,22 @@ pub fn transfer( let code = dest_contract.code(host)?; let storage = dest_contract.storage(host)?; - let new_storage = execute_smart_contract(code, storage, ¶meter); - + // Init MIR parser and context + let parser = Parser::default(); + let mut ctx = Ctx::default(); + let (entrypoint, value) = match parameter { + Some(param) => ( + Some(param.entrypoint), + match Micheline::decode_raw(&parser.arena, ¶m.value) { + Ok(value) => value, + Err(err) => return Ok(Err(TransferError::from(err).into())), + }, + ), + None => (None, Micheline::from(())), + }; + let new_storage = execute_smart_contract( + code, storage, entrypoint, value, &parser, &mut ctx, + ); match new_storage { Ok(new_storage) => { let _ = dest_contract.set_storage(host, &new_storage); @@ -288,36 +303,30 @@ fn apply_balance_changes( Ok(()) } -/// Executes the entrypoint logic of an originated smart contract and returns the new storage and consumed gas. -fn execute_smart_contract( +/// Executes the entrypoint logic of an originated smart contract and returns the new storage. +fn execute_smart_contract<'a>( code: Vec, storage: Vec, - parameter: &Option, + entrypoint: Option, + value: Micheline<'a>, + parser: &'a Parser<'a>, + ctx: &mut Ctx<'a>, ) -> Result, TransferError> { - let parser = Parser::new(); + // Parse and typecheck the contract let contract_micheline = Micheline::decode_raw(&parser.arena, &code)?; + let contract_typechecked = contract_micheline.typecheck_script(ctx)?; + let storage_micheline = Micheline::decode_raw(&parser.arena, &storage)?; - let (entrypoint, value) = match parameter { - Some(param) => ( - Some(param.entrypoint.clone()), - Micheline::decode_raw(&parser.arena, ¶m.value)?, - ), - None => (None, Micheline::from(())), - }; - - let mut ctx = Ctx::default(); - let contract_typechecked = contract_micheline.typecheck_script(&mut ctx)?; - - let storage = Micheline::decode_raw(&parser.arena, &storage)?; - - let (_, new_storage) = contract_typechecked.interpret( - &mut ctx, + // Execute the contract + let (_internal_operations, new_storage) = contract_typechecked.interpret( + ctx, &parser.arena, value, entrypoint, - storage, + storage_micheline, )?; + // Encode the new storage let new_storage = new_storage .into_micheline_optimized_legacy(&parser.arena) .encode(); -- GitLab From 6d50420ec6e8d4b41ab63bc0b1b78ad42d8515c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Tue, 29 Jul 2025 23:30:00 +0200 Subject: [PATCH 03/22] Tezlink/Kernel/Transfer: extract transfer_tez from transfer This commit moves the part of transfer logic related to updating the balances to a new transfer_tez function. Co-authored-by: Brahima Dibassi --- .../kernel_latest/tezos_execution/src/lib.rs | 88 +++++++++++++------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index b07a40ebc0a4..459e2e7421a6 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -111,6 +111,43 @@ fn reveal( })) } +pub fn transfer_tez( + host: &mut Host, + src_contract: &Contract, + src_account: &mut impl TezlinkAccount, + amount: &Narith, + dest_contract: &Contract, + dest_account: &mut impl TezlinkAccount, +) -> ExecutionResult> { + let (src_update, dest_update) = + compute_balance_updates(src_contract, dest_contract, amount) + .map_err(ApplyKernelError::BigIntError)?; + + // Check source balance + let current_src_balance = src_account.balance(host)?.0; + + let new_source_balance = match current_src_balance.checked_sub(&amount.0) { + None => { + log!(host, Debug, "Balance is too low"); + let error = TransferError::BalanceTooLow(BalanceTooLow { + contract: src_contract.clone(), + balance: current_src_balance.into(), + amount: amount.clone(), + }); + return Ok(Err(error.into())); + } + Some(new_source_balance) => new_source_balance, + }; + apply_balance_changes( + host, + src_account, + new_source_balance, + dest_account, + &amount.0, + )?; + Ok(Ok(vec![src_update, dest_update])) +} + /// Handles manager transfer operations for both implicit and originated contracts. pub fn transfer( host: &mut Host, @@ -130,25 +167,7 @@ pub fn transfer( ); let src_contract = Contract::Implicit(src.clone()); - let (src_update, dest_update) = compute_balance_updates(&src_contract, dest, amount) - .map_err(ApplyKernelError::BigIntError)?; - - // Check source balance let mut src_account = TezlinkImplicitAccount::from_public_key_hash(context, src)?; - let current_src_balance = src_account.balance(host)?.0; - - let new_source_balance = match current_src_balance.checked_sub(&amount.0) { - None => { - log!(host, Debug, "Balance is too low"); - let error = TransferError::BalanceTooLow(BalanceTooLow { - contract: src_contract.clone(), - balance: current_src_balance.into(), - amount: amount.clone(), - }); - return Ok(Err(error.into())); - } - Some(new_source_balance) => new_source_balance, - }; // Delegate to appropriate handler let success = match dest { @@ -159,18 +178,23 @@ pub fn transfer( let allocated = TezlinkImplicitAccount::allocate(host, context, dest)?; let mut dest_account = TezlinkImplicitAccount::from_public_key_hash(context, dest_key_hash)?; - apply_balance_changes( + let balance_updates = match transfer_tez( host, + &src_contract, &mut src_account, - new_source_balance.clone(), + amount, + dest, &mut dest_account, - &amount.0, - )?; - + )? { + Ok(balance_updates) => balance_updates, + Err(err) => { + return Ok(Err(err)); + } + }; Ok(Ok(TransferTarget::ToContrat(TransferSuccess { storage: None, lazy_storage_diff: None, - balance_updates: vec![src_update, dest_update], + balance_updates, ticket_receipt: vec![], originated_contracts: vec![], consumed_gas: 0_u64.into(), @@ -183,13 +207,19 @@ pub fn transfer( Contract::Originated(_) => { let mut dest_contract = TezlinkOriginatedAccount::from_contract(context, dest)?; - apply_balance_changes( + let balance_updates = match transfer_tez( host, + &src_contract, &mut src_account, - new_source_balance.clone(), + amount, + dest, &mut dest_contract, - &amount.0, - )?; + )? { + Ok(balance_updates) => balance_updates, + Err(err) => { + return Ok(Err(err)); + } + }; let code = dest_contract.code(host)?; let storage = dest_contract.storage(host)?; @@ -216,7 +246,7 @@ pub fn transfer( Ok(Ok(TransferTarget::ToContrat(TransferSuccess { storage: Some(new_storage), lazy_storage_diff: None, - balance_updates: vec![src_update, dest_update], + balance_updates, ticket_receipt: vec![], originated_contracts: vec![], consumed_gas: 0_u64.into(), -- GitLab From 1aea5e070c392370692693fb6d293888c9709857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 00:23:15 +0200 Subject: [PATCH 04/22] Tezlink/Kernel/Transfer: parse arg in the implicit case too Parse the argument before calling transfer_tez in preparation of internal transfers for which this parsing is not needed. Co-authored-by: Brahima Dibassi --- .../kernel_latest/tezos_execution/src/lib.rs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 459e2e7421a6..bcc04e54a4fc 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -110,7 +110,6 @@ fn reveal( consumed_gas: 0_u64.into(), })) } - pub fn transfer_tez( host: &mut Host, src_contract: &Contract, @@ -169,10 +168,24 @@ pub fn transfer( let src_contract = Contract::Implicit(src.clone()); let mut src_account = TezlinkImplicitAccount::from_public_key_hash(context, src)?; + // Init MIR parser and context + let parser = Parser::default(); + let mut ctx = Ctx::default(); + let (entrypoint, value) = match parameter { + Some(param) => ( + Some(param.entrypoint), + match Micheline::decode_raw(&parser.arena, ¶m.value) { + Ok(value) => value, + Err(err) => return Ok(Err(TransferError::from(err).into())), + }, + ), + None => (None, Micheline::from(())), + }; + // Delegate to appropriate handler let success = match dest { Contract::Implicit(dest_key_hash) => { - if parameter.is_some() { + if Micheline::from(()) != value { return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); } let allocated = TezlinkImplicitAccount::allocate(host, context, dest)?; @@ -223,20 +236,6 @@ pub fn transfer( let code = dest_contract.code(host)?; let storage = dest_contract.storage(host)?; - - // Init MIR parser and context - let parser = Parser::default(); - let mut ctx = Ctx::default(); - let (entrypoint, value) = match parameter { - Some(param) => ( - Some(param.entrypoint), - match Micheline::decode_raw(&parser.arena, ¶m.value) { - Ok(value) => value, - Err(err) => return Ok(Err(TransferError::from(err).into())), - }, - ), - None => (None, Micheline::from(())), - }; let new_storage = execute_smart_contract( code, storage, entrypoint, value, &parser, &mut ctx, ); -- GitLab From ed0888e549c7c67b15bd2c536577cd2255033c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Tue, 29 Jul 2025 23:46:13 +0200 Subject: [PATCH 05/22] Tezlink/Kernel/Transfer: move param check to transfer_tez Co-authored-by: Brahima Dibassi --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index bcc04e54a4fc..964aa230b2a5 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -117,7 +117,13 @@ pub fn transfer_tez( amount: &Narith, dest_contract: &Contract, dest_account: &mut impl TezlinkAccount, + param: &Micheline, ) -> ExecutionResult> { + if let Contract::Implicit(_) = dest_contract { + if &Micheline::from(()) != param { + return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); + } + } let (src_update, dest_update) = compute_balance_updates(src_contract, dest_contract, amount) .map_err(ApplyKernelError::BigIntError)?; @@ -185,9 +191,6 @@ pub fn transfer( // Delegate to appropriate handler let success = match dest { Contract::Implicit(dest_key_hash) => { - if Micheline::from(()) != value { - return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); - } let allocated = TezlinkImplicitAccount::allocate(host, context, dest)?; let mut dest_account = TezlinkImplicitAccount::from_public_key_hash(context, dest_key_hash)?; @@ -198,6 +201,7 @@ pub fn transfer( amount, dest, &mut dest_account, + &value, )? { Ok(balance_updates) => balance_updates, Err(err) => { @@ -227,6 +231,7 @@ pub fn transfer( amount, dest, &mut dest_contract, + &value, )? { Ok(balance_updates) => balance_updates, Err(err) => { -- GitLab From 76d0d9020f4619bc699fe87498b24bff6cf5da0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Tue, 29 Jul 2025 23:53:22 +0200 Subject: [PATCH 06/22] Tezlink/Kernel/Transfer: transfer_tez produces the receipt This requires to move the allocation logic to transfer_tez. Co-authored-by: Brahima Dibassi --- .../kernel_latest/tezos_execution/src/lib.rs | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 964aa230b2a5..7df6ff40e258 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -110,19 +110,24 @@ fn reveal( consumed_gas: 0_u64.into(), })) } +#[allow(clippy::too_many_arguments)] pub fn transfer_tez( host: &mut Host, + context: &context::Context, src_contract: &Contract, src_account: &mut impl TezlinkAccount, amount: &Narith, dest_contract: &Contract, dest_account: &mut impl TezlinkAccount, param: &Micheline, -) -> ExecutionResult> { +) -> ExecutionResult { + let mut allocated_destination_contract = false; if let Contract::Implicit(_) = dest_contract { if &Micheline::from(()) != param { return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); } + allocated_destination_contract = + TezlinkImplicitAccount::allocate(host, context, dest_contract)?; } let (src_update, dest_update) = compute_balance_updates(src_contract, dest_contract, amount) @@ -130,7 +135,6 @@ pub fn transfer_tez( // Check source balance let current_src_balance = src_account.balance(host)?.0; - let new_source_balance = match current_src_balance.checked_sub(&amount.0) { None => { log!(host, Debug, "Balance is too low"); @@ -150,7 +154,17 @@ pub fn transfer_tez( dest_account, &amount.0, )?; - Ok(Ok(vec![src_update, dest_update])) + Ok(Ok(TransferSuccess { + storage: None, + lazy_storage_diff: None, + balance_updates: vec![src_update, dest_update], + ticket_receipt: vec![], + originated_contracts: vec![], + consumed_gas: 0_u64.into(), + storage_size: 0_u64.into(), + paid_storage_size_diff: 0_u64.into(), + allocated_destination_contract, + })) } /// Handles manager transfer operations for both implicit and originated contracts. @@ -191,11 +205,11 @@ pub fn transfer( // Delegate to appropriate handler let success = match dest { Contract::Implicit(dest_key_hash) => { - let allocated = TezlinkImplicitAccount::allocate(host, context, dest)?; let mut dest_account = TezlinkImplicitAccount::from_public_key_hash(context, dest_key_hash)?; - let balance_updates = match transfer_tez( + let receipt = match transfer_tez( host, + context, &src_contract, &mut src_account, amount, @@ -203,29 +217,20 @@ pub fn transfer( &mut dest_account, &value, )? { - Ok(balance_updates) => balance_updates, + Ok(receipt) => receipt, Err(err) => { return Ok(Err(err)); } }; - Ok(Ok(TransferTarget::ToContrat(TransferSuccess { - storage: None, - lazy_storage_diff: None, - balance_updates, - ticket_receipt: vec![], - originated_contracts: vec![], - consumed_gas: 0_u64.into(), - storage_size: 0_u64.into(), - paid_storage_size_diff: 0_u64.into(), - allocated_destination_contract: allocated, - }))) + Ok(Ok(TransferTarget::ToContrat(receipt))) } Contract::Originated(_) => { let mut dest_contract = TezlinkOriginatedAccount::from_contract(context, dest)?; - let balance_updates = match transfer_tez( + let receipt = match transfer_tez( host, + context, &src_contract, &mut src_account, amount, @@ -233,7 +238,7 @@ pub fn transfer( &mut dest_contract, &value, )? { - Ok(balance_updates) => balance_updates, + Ok(receipt) => receipt, Err(err) => { return Ok(Err(err)); } @@ -249,14 +254,7 @@ pub fn transfer( let _ = dest_contract.set_storage(host, &new_storage); Ok(Ok(TransferTarget::ToContrat(TransferSuccess { storage: Some(new_storage), - lazy_storage_diff: None, - balance_updates, - ticket_receipt: vec![], - originated_contracts: vec![], - consumed_gas: 0_u64.into(), - storage_size: 0_u64.into(), - paid_storage_size_diff: 0_u64.into(), - allocated_destination_contract: false, + ..receipt }))) } -- GitLab From 272825113e32999b69c540cf039d0a0ea2a470da Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Fri, 18 Jul 2025 11:06:23 +0200 Subject: [PATCH 07/22] Tezlink/Kernel/Transfer: rename transfer into transfer_external MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To free the name "transfer" for the common logic of internal and external transfers. Co-authored-by: Raphaël Cauderlier --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 7df6ff40e258..63ddba231ec0 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -167,8 +167,8 @@ pub fn transfer_tez( })) } -/// Handles manager transfer operations for both implicit and originated contracts. -pub fn transfer( +// Handles manager transfer operations. +pub fn transfer_external( host: &mut Host, context: &context::Context, src: &PublicKeyHash, @@ -444,7 +444,7 @@ pub fn apply_operation( destination, parameters, }) => { - let transfer_result = transfer( + let transfer_result = transfer_external( &mut safe_host, context, source, -- GitLab From a4c560243a610a23db7154160402b605d0344f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 01:29:10 +0200 Subject: [PATCH 08/22] Tezlink/Kernel/Transfer: move ToContract wrapping to apply_operation Co-authored-by: Brahima Dibassi --- .../kernel_latest/tezos_execution/src/lib.rs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 63ddba231ec0..9e3f3aab75e4 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -175,7 +175,7 @@ pub fn transfer_external( amount: &Narith, dest: &Contract, parameter: Option, -) -> ExecutionResult { +) -> ExecutionResult { log!( host, Debug, @@ -207,7 +207,7 @@ pub fn transfer_external( Contract::Implicit(dest_key_hash) => { let mut dest_account = TezlinkImplicitAccount::from_public_key_hash(context, dest_key_hash)?; - let receipt = match transfer_tez( + transfer_tez( host, context, &src_contract, @@ -216,13 +216,7 @@ pub fn transfer_external( dest, &mut dest_account, &value, - )? { - Ok(receipt) => receipt, - Err(err) => { - return Ok(Err(err)); - } - }; - Ok(Ok(TransferTarget::ToContrat(receipt))) + ) } Contract::Originated(_) => { @@ -252,10 +246,10 @@ pub fn transfer_external( match new_storage { Ok(new_storage) => { let _ = dest_contract.set_storage(host, &new_storage); - Ok(Ok(TransferTarget::ToContrat(TransferSuccess { + Ok(Ok(TransferSuccess { storage: Some(new_storage), ..receipt - }))) + })) } Err(err) => Ok(Err(err.into())), @@ -452,8 +446,10 @@ pub fn apply_operation( &destination, parameters, )?; - let manager_result = - produce_operation_result(vec![src_delta, block_fees], transfer_result); + let manager_result = produce_operation_result( + vec![src_delta, block_fees], + transfer_result.map(TransferTarget::ToContrat), + ); OperationResultSum::Transfer(manager_result) } }; -- GitLab From dc17bba7a73ed9df9f17c608de30c2a32ef54b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 01:32:32 +0200 Subject: [PATCH 09/22] Tezlink/Kernel/Transfer: add details in a log message Co-authored-by: Brahima Dibassi --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 9e3f3aab75e4..da64e9104c70 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -179,10 +179,11 @@ pub fn transfer_external( log!( host, Debug, - "Applying a transfer operation from {} to {:?} of {:?} mutez", + "Applying an external transfer operation from {} to {:?} of {:?} mutez with parameters {:?}", src, dest, - amount + amount, + parameter ); let src_contract = Contract::Implicit(src.clone()); -- GitLab From 66e7177999e8f4469d3ff09e381c42532a798616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 02:34:04 +0200 Subject: [PATCH 10/22] Tezlink/Kernel/Transfer: use Parser::new instead of default Co-authored-by: Brahima Dibassi --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index da64e9104c70..06c714602227 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -190,7 +190,7 @@ pub fn transfer_external( let mut src_account = TezlinkImplicitAccount::from_public_key_hash(context, src)?; // Init MIR parser and context - let parser = Parser::default(); + let parser = Parser::new(); let mut ctx = Ctx::default(); let (entrypoint, value) = match parameter { Some(param) => ( -- GitLab From 88b7be863e6070f93dba790d4e968748d5561f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 00:35:31 +0200 Subject: [PATCH 11/22] Tezlink/Kernel/Transfer: only initialize ctx when dest in originated Co-authored-by: Brahima Dibassi --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 06c714602227..3101fa921b6f 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -188,10 +188,7 @@ pub fn transfer_external( let src_contract = Contract::Implicit(src.clone()); let mut src_account = TezlinkImplicitAccount::from_public_key_hash(context, src)?; - - // Init MIR parser and context let parser = Parser::new(); - let mut ctx = Ctx::default(); let (entrypoint, value) = match parameter { Some(param) => ( Some(param.entrypoint), @@ -223,6 +220,7 @@ pub fn transfer_external( Contract::Originated(_) => { let mut dest_contract = TezlinkOriginatedAccount::from_contract(context, dest)?; + let mut ctx = Ctx::default(); let receipt = match transfer_tez( host, context, -- GitLab From 80bc7173b28fd469a0ca5a375cc5093610edce7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 00:35:31 +0200 Subject: [PATCH 12/22] Tezlink/Kernel/Transfer: rename dest_contract into dest_account Because it is of type TezlinkOriginatedAccount, not of type Contract. Co-authored-by: Brahima Dibassi --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 3101fa921b6f..470bb972fd4e 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -218,7 +218,7 @@ pub fn transfer_external( } Contract::Originated(_) => { - let mut dest_contract = + let mut dest_account = TezlinkOriginatedAccount::from_contract(context, dest)?; let mut ctx = Ctx::default(); let receipt = match transfer_tez( @@ -228,7 +228,7 @@ pub fn transfer_external( &mut src_account, amount, dest, - &mut dest_contract, + &mut dest_account, &value, )? { Ok(receipt) => receipt, @@ -237,14 +237,14 @@ pub fn transfer_external( } }; - let code = dest_contract.code(host)?; - let storage = dest_contract.storage(host)?; + let code = dest_account.code(host)?; + let storage = dest_account.storage(host)?; let new_storage = execute_smart_contract( code, storage, entrypoint, value, &parser, &mut ctx, ); match new_storage { Ok(new_storage) => { - let _ = dest_contract.set_storage(host, &new_storage); + let _ = dest_account.set_storage(host, &new_storage); Ok(Ok(TransferSuccess { storage: Some(new_storage), ..receipt -- GitLab From 6201f5d8eb132e9fd4efa07e92eeb94ca2075ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 03:14:46 +0200 Subject: [PATCH 13/22] Tezlink/Kernel/Transfer: don't ignore storage errors Co-authored-by: Brahima Dibassi --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 470bb972fd4e..b0fb173b63cd 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -244,7 +244,7 @@ pub fn transfer_external( ); match new_storage { Ok(new_storage) => { - let _ = dest_account.set_storage(host, &new_storage); + dest_account.set_storage(host, &new_storage)?; Ok(Ok(TransferSuccess { storage: Some(new_storage), ..receipt -- GitLab From 7827d362e60c9c825c1cba7a29ab0b670a099637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 30 Jul 2025 03:31:01 +0200 Subject: [PATCH 14/22] Tezlink/Kernel/Transfer: extract a transfer function from transfer_external Co-authored-by: Brahima Dibassi --- .../kernel_latest/tezos_execution/src/lib.rs | 81 +++++++++++++------ 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index b0fb173b63cd..13728e838340 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -167,6 +167,56 @@ pub fn transfer_tez( })) } +/// Handles manager transfer operations for both implicit and originated contracts but with a MIR context. +#[allow(clippy::too_many_arguments)] +pub fn transfer<'a, Host: Runtime>( + host: &mut Host, + context: &context::Context, + src_contract: &Contract, + src_account: &mut impl TezlinkAccount, + amount: &Narith, + dest_contract: &Contract, + dest_account: &mut impl TezlinkAccount, + entrypoint: Option, + param: Micheline<'a>, + parser: &'a Parser<'a>, + ctx: &mut Ctx<'a>, +) -> ExecutionResult { + let receipt = match transfer_tez( + host, + context, + src_contract, + src_account, + amount, + dest_contract, + dest_account, + ¶m, + )? { + Ok(success) => success, + Err(error) => return Ok(Err(error)), + }; + if let Contract::Originated(_) = dest_contract { + let dest_account = + TezlinkOriginatedAccount::from_contract(context, dest_contract)?; + let code = dest_account.code(host)?; + let storage = dest_account.storage(host)?; + let new_storage = + execute_smart_contract(code, storage, entrypoint, param, parser, ctx); + match new_storage { + Ok(new_storage) => { + dest_account.set_storage(host, &new_storage)?; + Ok(Ok(TransferSuccess { + storage: Some(new_storage), + ..receipt + })) + } + Err(err) => Ok(Err(err.into())), + } + } else { + Ok(Ok(receipt)) + } +} + // Handles manager transfer operations. pub fn transfer_external( host: &mut Host, @@ -221,7 +271,7 @@ pub fn transfer_external( let mut dest_account = TezlinkOriginatedAccount::from_contract(context, dest)?; let mut ctx = Ctx::default(); - let receipt = match transfer_tez( + transfer( host, context, &src_contract, @@ -229,30 +279,11 @@ pub fn transfer_external( amount, dest, &mut dest_account, - &value, - )? { - Ok(receipt) => receipt, - Err(err) => { - return Ok(Err(err)); - } - }; - - let code = dest_account.code(host)?; - let storage = dest_account.storage(host)?; - let new_storage = execute_smart_contract( - code, storage, entrypoint, value, &parser, &mut ctx, - ); - match new_storage { - Ok(new_storage) => { - dest_account.set_storage(host, &new_storage)?; - Ok(Ok(TransferSuccess { - storage: Some(new_storage), - ..receipt - })) - } - - Err(err) => Ok(Err(err.into())), - } + entrypoint, + value, + &parser, + &mut ctx, + ) } }; // TODO : Counter Increment should be done after successful validation (see issue #8031) -- GitLab From 11cd2388a4de68dd2ac5ac620dbdc3b9f68e5be3 Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Fri, 18 Jul 2025 11:06:23 +0200 Subject: [PATCH 15/22] Tezlink/Kernel/Transfer : Internal Transfer Handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Raphaël Cauderlier --- .../tezos/src/operation_result.rs | 1 + .../kernel_latest/tezos_execution/src/lib.rs | 122 ++++++++++++++++-- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/etherlink/kernel_latest/tezos/src/operation_result.rs b/etherlink/kernel_latest/tezos/src/operation_result.rs index d193215781d6..45f1303a79fc 100644 --- a/etherlink/kernel_latest/tezos/src/operation_result.rs +++ b/etherlink/kernel_latest/tezos/src/operation_result.rs @@ -96,6 +96,7 @@ impl From for TransferError { pub enum ApplyOperationError { Reveal(RevealError), Transfer(TransferError), + UnSupportedOperation(String), } impl From for ApplyOperationError { diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 13728e838340..17906a15115d 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -4,13 +4,13 @@ use account_storage::TezlinkAccount; use account_storage::{Manager, TezlinkImplicitAccount, TezlinkOriginatedAccount}; -use mir::ast::Entrypoint; +use mir::ast::{AddressHash, Entrypoint, OperationInfo, TransferTokens}; use mir::{ ast::{IntoMicheline, Micheline}, context::Ctx, parser::Parser, }; -use num_bigint::BigInt; +use num_bigint::{BigInt, BigUint}; use num_traits::ops::checked::CheckedSub; use tezos_crypto_rs::{base58::FromBase58CheckError, PublicKeyWithHash}; use tezos_data_encoding::enc::BinError; @@ -19,6 +19,7 @@ use tezos_evm_logging::{log, Level::*, Verbosity}; use tezos_evm_runtime::{runtime::Runtime, safe_storage::SafeStorage}; use tezos_smart_rollup::types::{Contract, PublicKey, PublicKeyHash}; use tezos_tezlink::operation::Operation; +use tezos_tezlink::operation_result::{ApplyOperationError, TransferTarget}; use tezos_tezlink::{ operation::{ ManagerOperation, OperationContent, Parameter, RevealContent, TransferContent, @@ -26,7 +27,7 @@ use tezos_tezlink::{ operation_result::{ is_applied, produce_operation_result, Balance, BalanceTooLow, BalanceUpdate, OperationError, OperationResultSum, Reveal, RevealError, RevealSuccess, - TransferError, TransferSuccess, TransferTarget, UpdateOrigin, + TransferError, TransferSuccess, UpdateOrigin, }, }; use thiserror::Error; @@ -50,6 +51,8 @@ pub enum ApplyKernelError { BigIntError(num_bigint::TryFromBigIntError), #[error("Serialization failed because of {0}")] BinaryError(String), + #[error("Apply operation failed because of an unsupported address error")] + MirAddressUnsupportedError, } // 'FromBase58CheckError' doesn't implement PartialEq and Eq @@ -110,6 +113,15 @@ fn reveal( consumed_gas: 0_u64.into(), })) } + +fn contract_from_address(address: AddressHash) -> Result { + match address { + AddressHash::Kt1(kt1) => Ok(Contract::Originated(kt1)), + AddressHash::Implicit(pkh) => Ok(Contract::Implicit(pkh)), + AddressHash::Sr1(_) => Err(ApplyKernelError::MirAddressUnsupportedError), + } +} + #[allow(clippy::too_many_arguments)] pub fn transfer_tez( host: &mut Host, @@ -167,6 +179,86 @@ pub fn transfer_tez( })) } +pub fn execute_internal_operations<'a, Host: Runtime>( + host: &mut Host, + context: &context::Context, + internal_operations: impl Iterator>, + sender_contract: &Contract, + sender_account: &mut TezlinkOriginatedAccount, + parser: &'a Parser<'a>, + ctx: &mut Ctx<'a>, +) -> ExecutionResult<()> { + for internal_op in internal_operations { + log!( + host, + Debug, + "Executing internal operation: {:?}", + internal_op + ); + let internal_receipt = match internal_op.operation { + mir::ast::Operation::TransferTokens(TransferTokens { + param, + destination_address, + amount, + }) => { + let amount = Narith(amount.try_into().unwrap_or(BigUint::ZERO)); + let dest_contract = contract_from_address(destination_address.hash)?; + match &dest_contract { + Contract::Implicit(_) => transfer_tez( + host, + context, + sender_contract, + sender_account, + &amount, + &dest_contract, + &mut TezlinkImplicitAccount::from_contract( + context, + &dest_contract, + )?, + ¶m.into_micheline_optimized_legacy(&parser.arena), + ), + Contract::Originated(_) => transfer( + host, + context, + sender_contract, + sender_account, + &amount, + &dest_contract, + &mut TezlinkOriginatedAccount::from_contract( + context, + &dest_contract, + )?, + Some(destination_address.entrypoint), + param.into_micheline_optimized_legacy(&parser.arena), + parser, + ctx, + ), + } + } + _ => { + return Ok(Err(ApplyOperationError::UnSupportedOperation( + "Unsupported internal operation".to_string(), + ) + .into())); + } + }?; + match internal_receipt { + Ok(receipt) => { + log!( + host, + Debug, + "Internal operation executed successfully: {:?}", + receipt + ); + } + Err(error) => { + return Ok(Err(error)); + } + } + } + Ok(Ok(())) +} + /// Handles manager transfer operations for both implicit and originated contracts but with a MIR context. #[allow(clippy::too_many_arguments)] pub fn transfer<'a, Host: Runtime>( @@ -196,15 +288,24 @@ pub fn transfer<'a, Host: Runtime>( Err(error) => return Ok(Err(error)), }; if let Contract::Originated(_) = dest_contract { - let dest_account = + let mut dest_account = TezlinkOriginatedAccount::from_contract(context, dest_contract)?; let code = dest_account.code(host)?; let storage = dest_account.storage(host)?; - let new_storage = + let result = execute_smart_contract(code, storage, entrypoint, param, parser, ctx); - match new_storage { - Ok(new_storage) => { + match result { + Ok((internal_operations, new_storage)) => { dest_account.set_storage(host, &new_storage)?; + let _internal_receipt = execute_internal_operations( + host, + context, + internal_operations, + dest_contract, + &mut dest_account, + parser, + ctx, + )?; Ok(Ok(TransferSuccess { storage: Some(new_storage), ..receipt @@ -367,14 +468,14 @@ fn execute_smart_contract<'a>( value: Micheline<'a>, parser: &'a Parser<'a>, ctx: &mut Ctx<'a>, -) -> Result, TransferError> { +) -> Result<(impl Iterator>, Vec), TransferError> { // Parse and typecheck the contract let contract_micheline = Micheline::decode_raw(&parser.arena, &code)?; let contract_typechecked = contract_micheline.typecheck_script(ctx)?; let storage_micheline = Micheline::decode_raw(&parser.arena, &storage)?; // Execute the contract - let (_internal_operations, new_storage) = contract_typechecked.interpret( + let (internal_operations, new_storage) = contract_typechecked.interpret( ctx, &parser.arena, value, @@ -386,7 +487,8 @@ fn execute_smart_contract<'a>( let new_storage = new_storage .into_micheline_optimized_legacy(&parser.arena) .encode(); - Ok(new_storage) + + Ok((internal_operations, new_storage)) } pub fn apply_operation( -- GitLab From b21aa3779e67b6184725787bcea19e92eb85625c Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Wed, 30 Jul 2025 10:47:55 +0200 Subject: [PATCH 16/22] Tezlink/Kernel/Transfer : Set proper sender and source for MIR context --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 17906a15115d..3fd5febd4695 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -122,6 +122,13 @@ fn contract_from_address(address: AddressHash) -> Result AddressHash { + match contract { + Contract::Originated(kt1) => AddressHash::Kt1(kt1), + Contract::Implicit(hash) => AddressHash::Implicit(hash), + } +} + #[allow(clippy::too_many_arguments)] pub fn transfer_tez( host: &mut Host, @@ -288,6 +295,7 @@ pub fn transfer<'a, Host: Runtime>( Err(error) => return Ok(Err(error)), }; if let Contract::Originated(_) = dest_contract { + ctx.sender = address_from_contract(src_contract.clone()); let mut dest_account = TezlinkOriginatedAccount::from_contract(context, dest_contract)?; let code = dest_account.code(host)?; @@ -372,6 +380,7 @@ pub fn transfer_external( let mut dest_account = TezlinkOriginatedAccount::from_contract(context, dest)?; let mut ctx = Ctx::default(); + ctx.source = address_from_contract(src_contract.clone()); transfer( host, context, -- GitLab From 4b604f694e811152921606ff04c188b28ced2dff Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Fri, 18 Jul 2025 11:00:45 +0200 Subject: [PATCH 17/22] Tezlink/Kernel/Transfer : Add unit test for transfer operation to originated faucet --- etherlink/kernel_latest/Cargo.lock | 1 + .../kernel_latest/tezos_execution/Cargo.toml | 3 + .../kernel_latest/tezos_execution/src/lib.rs | 66 ++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index 5a0caf59edf2..7072378f9a58 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -3061,6 +3061,7 @@ dependencies = [ "nom", "num-bigint", "num-traits", + "pretty_assertions", "primitive-types", "tezos-evm-logging-latest", "tezos-evm-runtime-latest", diff --git a/etherlink/kernel_latest/tezos_execution/Cargo.toml b/etherlink/kernel_latest/tezos_execution/Cargo.toml index 92e4d1b8fe47..4664fd40cdd6 100644 --- a/etherlink/kernel_latest/tezos_execution/Cargo.toml +++ b/etherlink/kernel_latest/tezos_execution/Cargo.toml @@ -30,3 +30,6 @@ tezos-smart-rollup.workspace = true tezos_tezlink.workspace = true tezos-evm-logging.workspace = true mir.workspace = true + +[dev-dependencies] +pretty_assertions = "1.4.0" diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 3fd5febd4695..5e0ed6254ca3 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -607,7 +607,9 @@ pub fn apply_operation( #[cfg(test)] mod tests { - use crate::{TezlinkImplicitAccount, TezlinkOriginatedAccount}; + use crate::{account_storage::TezlinkOriginatedAccount, TezlinkImplicitAccount}; + use mir::ast::{Entrypoint, Micheline}; + use pretty_assertions::assert_eq; use tezos_crypto_rs::hash::{ContractKt1Hash, SecretKeyEd25519}; use tezos_data_encoding::types::Narith; use tezos_evm_runtime::runtime::{MockKernelHost, Runtime}; @@ -1337,6 +1339,68 @@ mod tests { assert_eq!(receipt, expected_receipt); } + #[test] + fn apply_transfer_to_originated_faucet() { + let mut host = MockKernelHost::default(); + let context = context::Context::init_context(); + let (requester_balance, faucet_balance, fees) = (50, 1000, 15); + let src = bootstrap1(); + let desthash = + ContractKt1Hash::from_base58_check("KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton") + .expect("ContractKt1Hash b58 conversion should have succeeded"); + // Setup accounts with 50 mutez in their balance + let requester = init_account(&mut host, &src.pkh); + reveal_account(&mut host, &src); + let (code, storage) = ( + r#" + parameter (mutez %fund); + storage unit; + code + { + UNPAIR; + SENDER; + CONTRACT unit; + IF_NONE { FAILWITH } {}; + SWAP; + UNIT; + TRANSFER_TOKENS; + NIL operation; + SWAP; + CONS; + PAIR + } + "#, + "Unit", + ); + let faucet = init_contract(&mut host, &desthash, code, storage, &1000.into()); + let requested_amount = 100; + let operation = make_transfer_operation( + fees, + 1, + 4, + 5, + src.clone(), + 0.into(), + Contract::Originated(desthash).clone(), + Some(Parameter { + entrypoint: Entrypoint::try_from("fund") + .expect("Entrypoint should be valid"), + value: Micheline::from(requested_amount as i128).encode(), + }), + ); + let res = apply_operation(&mut host, &context, operation) + .expect("apply_operation should not have failed with a kernel error"); + println!("Result: {:?}", res); + assert_eq!( + faucet.balance(&host).unwrap(), + (faucet_balance - requested_amount).into() + ); + assert_eq!( + requester.balance(&host).unwrap(), + (requester_balance + requested_amount - fees).into() + ); // The faucet should have transferred 100 mutez to the source + } + #[test] fn apply_transfer_with_execution() { let mut host = MockKernelHost::default(); -- GitLab From c9c58772fff9a3a3630196a663e6e5c583a27e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Fri, 4 Jul 2025 14:46:04 +0200 Subject: [PATCH 18/22] Tezlink/Node/Tezt : Test basic successful internal operation --- etherlink/tezt/lib/michelson_contracts.ml | 10 +++++ etherlink/tezt/tests/tezlink.ml | 40 ++++++++++++++++++- .../mini_scenarios/faucet.tz | 16 ++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 michelson_test_scripts/mini_scenarios/faucet.tz diff --git a/etherlink/tezt/lib/michelson_contracts.ml b/etherlink/tezt/lib/michelson_contracts.ml index a4e9116252fc..5563c4cda807 100644 --- a/etherlink/tezt/lib/michelson_contracts.ml +++ b/etherlink/tezt/lib/michelson_contracts.ml @@ -16,3 +16,13 @@ let concat_hello () = find ["opcodes"; "concat_hello"] tezlink_protocol |> path); initial_storage = "{ \"initial\" }"; } + +let faucet_contract () = + Evm_node. + { + address = "KT1QuofAgnsWffHzLA7D78rxytJruGHDe7XG"; + path = + Michelson_script.( + find ["mini_scenarios"; "faucet"] tezlink_protocol |> path); + initial_storage = "Unit"; + } diff --git a/etherlink/tezt/tests/tezlink.ml b/etherlink/tezt/tests/tezlink.ml index cb56bcb91508..8e6142f96e4e 100644 --- a/etherlink/tezt/tests/tezlink.ml +++ b/etherlink/tezt/tests/tezlink.ml @@ -1074,6 +1074,43 @@ let test_tezlink_sandbox () = ~error_msg:"Wrong balance for bootstrap2: expected %R, actual %L" ; unit +let test_tezlink_internal_operation = + let bootstrap_balance = Tez.of_mutez_int 3_800_000_000_000 in + let faucet = Tezt_etherlink.Michelson_contracts.faucet_contract () in + register_tezlink_test + ~title:"Internal operation" + ~tags:["internal"; "operation"] + ~bootstrap_accounts:[Constant.bootstrap1] + ~bootstrap_contracts:[faucet] + @@ fun {sequencer; client; _} _protocol -> + let endpoint = + Client.( + Foreign_endpoint + Endpoint. + {(Evm_node.rpc_endpoint_record sequencer) with path = "/tezlink"}) + in + let* () = + Client.transfer + ~endpoint + ~fee:Tez.zero + ~amount:Tez.zero + ~giver:Constant.bootstrap1.alias + ~receiver:faucet.address + ~burn_cap:Tez.one + ~entrypoint:"fund" + ~arg:"1000000" + client + in + let*@ _ = produce_block sequencer in + let* balance = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap1.alias client + in + Check.( + (Tez.to_mutez balance = Tez.to_mutez bootstrap_balance + Tez.(to_mutez one)) + int) + ~error_msg:"Wrong balance for bootstrap1: exptected %R, actual %L" ; + unit + let () = test_observer_starts [Alpha] ; test_describe_endpoint [Alpha] ; @@ -1102,4 +1139,5 @@ let () = test_tezlink_storage [Alpha] ; test_tezlink_execution [Alpha] ; test_tezlink_bootstrap_block_info [Alpha] ; - test_tezlink_sandbox () + test_tezlink_sandbox () ; + test_tezlink_internal_operation [Alpha] diff --git a/michelson_test_scripts/mini_scenarios/faucet.tz b/michelson_test_scripts/mini_scenarios/faucet.tz new file mode 100644 index 000000000000..c8cdb75f3f06 --- /dev/null +++ b/michelson_test_scripts/mini_scenarios/faucet.tz @@ -0,0 +1,16 @@ +parameter (mutez %fund); +storage unit; +code + { + UNPAIR; + SENDER; + CONTRACT unit; + ASSERT_SOME; + SWAP; + UNIT; + TRANSFER_TOKENS; + NIL operation; + SWAP; + CONS; + PAIR + } -- GitLab From c58ffa9ad18516eb900039a5d36e1aceefd5d190 Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Wed, 30 Jul 2025 17:58:43 +0200 Subject: [PATCH 19/22] Tezlink/Kernel/Transfer : Add unit test for transfer operation with non-default entrypoint to implicit account --- .../kernel_latest/tezos_execution/src/lib.rs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 5e0ed6254ca3..bd8c22265628 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -1617,4 +1617,60 @@ mod tests { assert_eq!(receipt, expected_receipt); } + + #[test] + fn apply_transfer_with_non_default_entrypoint_to_implicit_fails() { + let mut host = MockKernelHost::default(); + let parser = mir::parser::Parser::new(); + + let src = bootstrap1(); + + let dest = bootstrap2(); + + init_account(&mut host, &src.pkh); + reveal_account(&mut host, &src); + + let operation = make_transfer_operation( + 15, + 1, + 4, + 5, + src.clone(), + 30_u64.into(), + Contract::Implicit(dest.pkh), + Some(Parameter { + entrypoint: mir::ast::entrypoint::Entrypoint::try_from("non_default") + .expect("Entrypoint should be valid"), + value: parser.parse("0").unwrap().encode(), + }), + ); + + let receipt = + apply_operation(&mut host, &context::Context::init_context(), operation) + .expect("apply_operation should not have failed with a kernel error"); + + let expected_receipt = OperationResultSum::Transfer(OperationResult { + balance_updates: vec![ + BalanceUpdate { + balance: Balance::Account(Contract::Implicit(src.pkh)), + changes: -15, + update_origin: UpdateOrigin::BlockApplication, + }, + BalanceUpdate { + balance: Balance::BlockFees, + changes: 15, + update_origin: UpdateOrigin::BlockApplication, + }, + ], + result: ContentResult::Failed( + vec![OperationError::Apply(ApplyOperationError::Transfer( + TransferError::NonSmartContractExecutionCall, + ))] + .into(), + ), + internal_operation_results: vec![], + }); + + assert_eq!(receipt, expected_receipt); + } } -- GitLab From 832aa0392a96cbbeeef39a731747e4cefa875542 Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Wed, 30 Jul 2025 18:23:37 +0200 Subject: [PATCH 20/22] Tezlink/Kernel/Transfer : Refactor transfer functions --- .../kernel_latest/tezos_execution/src/lib.rs | 209 ++++++++---------- 1 file changed, 89 insertions(+), 120 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index bd8c22265628..5d4871f4c9cd 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -129,25 +129,14 @@ fn address_from_contract(contract: Contract) -> AddressHash { } } -#[allow(clippy::too_many_arguments)] pub fn transfer_tez( host: &mut Host, - context: &context::Context, src_contract: &Contract, src_account: &mut impl TezlinkAccount, amount: &Narith, dest_contract: &Contract, dest_account: &mut impl TezlinkAccount, - param: &Micheline, ) -> ExecutionResult { - let mut allocated_destination_contract = false; - if let Contract::Implicit(_) = dest_contract { - if &Micheline::from(()) != param { - return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); - } - allocated_destination_contract = - TezlinkImplicitAccount::allocate(host, context, dest_contract)?; - } let (src_update, dest_update) = compute_balance_updates(src_contract, dest_contract, amount) .map_err(ApplyKernelError::BigIntError)?; @@ -182,7 +171,7 @@ pub fn transfer_tez( consumed_gas: 0_u64.into(), storage_size: 0_u64.into(), paid_storage_size_diff: 0_u64.into(), - allocated_destination_contract, + allocated_destination_contract: false, })) } @@ -210,37 +199,18 @@ pub fn execute_internal_operations<'a, Host: Runtime>( }) => { let amount = Narith(amount.try_into().unwrap_or(BigUint::ZERO)); let dest_contract = contract_from_address(destination_address.hash)?; - match &dest_contract { - Contract::Implicit(_) => transfer_tez( - host, - context, - sender_contract, - sender_account, - &amount, - &dest_contract, - &mut TezlinkImplicitAccount::from_contract( - context, - &dest_contract, - )?, - ¶m.into_micheline_optimized_legacy(&parser.arena), - ), - Contract::Originated(_) => transfer( - host, - context, - sender_contract, - sender_account, - &amount, - &dest_contract, - &mut TezlinkOriginatedAccount::from_contract( - context, - &dest_contract, - )?, - Some(destination_address.entrypoint), - param.into_micheline_optimized_legacy(&parser.arena), - parser, - ctx, - ), - } + transfer( + host, + context, + sender_contract, + sender_account, + &amount, + &dest_contract, + Some(destination_address.entrypoint), + param.into_micheline_optimized_legacy(&parser.arena), + parser, + ctx, + ) } _ => { return Ok(Err(ApplyOperationError::UnSupportedOperation( @@ -275,54 +245,75 @@ pub fn transfer<'a, Host: Runtime>( src_account: &mut impl TezlinkAccount, amount: &Narith, dest_contract: &Contract, - dest_account: &mut impl TezlinkAccount, entrypoint: Option, param: Micheline<'a>, parser: &'a Parser<'a>, ctx: &mut Ctx<'a>, ) -> ExecutionResult { - let receipt = match transfer_tez( - host, - context, - src_contract, - src_account, - amount, - dest_contract, - dest_account, - ¶m, - )? { - Ok(success) => success, - Err(error) => return Ok(Err(error)), - }; - if let Contract::Originated(_) = dest_contract { - ctx.sender = address_from_contract(src_contract.clone()); - let mut dest_account = - TezlinkOriginatedAccount::from_contract(context, dest_contract)?; - let code = dest_account.code(host)?; - let storage = dest_account.storage(host)?; - let result = - execute_smart_contract(code, storage, entrypoint, param, parser, ctx); - match result { - Ok((internal_operations, new_storage)) => { - dest_account.set_storage(host, &new_storage)?; - let _internal_receipt = execute_internal_operations( - host, - context, - internal_operations, - dest_contract, - &mut dest_account, - parser, - ctx, - )?; - Ok(Ok(TransferSuccess { - storage: Some(new_storage), - ..receipt - })) + match dest_contract { + Contract::Implicit(pkh) => { + let is_entrypoint_default = + entrypoint.is_none() || entrypoint == Some(Entrypoint::default()); + if param != Micheline::from(()) || !is_entrypoint_default { + return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); + } + // Allocate the implicit account if it doesn't exist + let allocated = + TezlinkImplicitAccount::allocate(host, context, dest_contract)?; + match transfer_tez( + host, + src_contract, + src_account, + amount, + dest_contract, + &mut TezlinkImplicitAccount::from_public_key_hash(context, pkh)?, + )? { + Ok(success) => Ok(Ok(TransferSuccess { + allocated_destination_contract: allocated, + ..success + })), + Err(error) => Ok(Err(error)), + } + } + Contract::Originated(_) => { + let mut dest_account = + TezlinkOriginatedAccount::from_contract(context, dest_contract)?; + let receipt = match transfer_tez( + host, + src_contract, + src_account, + amount, + dest_contract, + &mut dest_account, + )? { + Ok(success) => success, + Err(error) => return Ok(Err(error)), + }; + ctx.sender = address_from_contract(src_contract.clone()); + let code = dest_account.code(host)?; + let storage = dest_account.storage(host)?; + let result = + execute_smart_contract(code, storage, entrypoint, param, parser, ctx); + match result { + Ok((internal_operations, new_storage)) => { + dest_account.set_storage(host, &new_storage)?; + let _internal_receipt = execute_internal_operations( + host, + context, + internal_operations, + dest_contract, + &mut dest_account, + parser, + ctx, + )?; + Ok(Ok(TransferSuccess { + storage: Some(new_storage), + ..receipt + })) + } + Err(err) => Ok(Err(err.into())), } - Err(err) => Ok(Err(err.into())), } - } else { - Ok(Ok(receipt)) } } @@ -358,44 +349,22 @@ pub fn transfer_external( ), None => (None, Micheline::from(())), }; + let mut ctx = Ctx::default(); + ctx.source = address_from_contract(src_contract.clone()); // Delegate to appropriate handler - let success = match dest { - Contract::Implicit(dest_key_hash) => { - let mut dest_account = - TezlinkImplicitAccount::from_public_key_hash(context, dest_key_hash)?; - transfer_tez( - host, - context, - &src_contract, - &mut src_account, - amount, - dest, - &mut dest_account, - &value, - ) - } - - Contract::Originated(_) => { - let mut dest_account = - TezlinkOriginatedAccount::from_contract(context, dest)?; - let mut ctx = Ctx::default(); - ctx.source = address_from_contract(src_contract.clone()); - transfer( - host, - context, - &src_contract, - &mut src_account, - amount, - dest, - &mut dest_account, - entrypoint, - value, - &parser, - &mut ctx, - ) - } - }; + let success = transfer( + host, + context, + &src_contract, + &mut src_account, + amount, + dest, + entrypoint, + value, + &parser, + &mut ctx, + ); // TODO : Counter Increment should be done after successful validation (see issue #8031) src_account.increment_counter(host)?; success -- GitLab From 1a42d5a6e98f8030051d79e92e78bd9316c23129 Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Wed, 30 Jul 2025 18:25:42 +0200 Subject: [PATCH 21/22] Tezlink/Kernel/Transfer : Refactor transfer functions to remove Option from entrypoint parameter --- .../kernel_latest/tezos_execution/src/lib.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 5d4871f4c9cd..db635505283f 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -206,7 +206,7 @@ pub fn execute_internal_operations<'a, Host: Runtime>( sender_account, &amount, &dest_contract, - Some(destination_address.entrypoint), + destination_address.entrypoint, param.into_micheline_optimized_legacy(&parser.arena), parser, ctx, @@ -245,16 +245,14 @@ pub fn transfer<'a, Host: Runtime>( src_account: &mut impl TezlinkAccount, amount: &Narith, dest_contract: &Contract, - entrypoint: Option, + entrypoint: Entrypoint, param: Micheline<'a>, parser: &'a Parser<'a>, ctx: &mut Ctx<'a>, ) -> ExecutionResult { match dest_contract { Contract::Implicit(pkh) => { - let is_entrypoint_default = - entrypoint.is_none() || entrypoint == Some(Entrypoint::default()); - if param != Micheline::from(()) || !is_entrypoint_default { + if param != Micheline::from(()) || !entrypoint.is_default() { return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); } // Allocate the implicit account if it doesn't exist @@ -341,13 +339,13 @@ pub fn transfer_external( let parser = Parser::new(); let (entrypoint, value) = match parameter { Some(param) => ( - Some(param.entrypoint), + param.entrypoint, match Micheline::decode_raw(&parser.arena, ¶m.value) { Ok(value) => value, Err(err) => return Ok(Err(TransferError::from(err).into())), }, ), - None => (None, Micheline::from(())), + None => (Entrypoint::default(), Micheline::from(())), }; let mut ctx = Ctx::default(); ctx.source = address_from_contract(src_contract.clone()); @@ -442,7 +440,7 @@ fn apply_balance_changes( fn execute_smart_contract<'a>( code: Vec, storage: Vec, - entrypoint: Option, + entrypoint: Entrypoint, value: Micheline<'a>, parser: &'a Parser<'a>, ctx: &mut Ctx<'a>, @@ -457,7 +455,7 @@ fn execute_smart_contract<'a>( ctx, &parser.arena, value, - entrypoint, + Some(entrypoint), storage_micheline, )?; -- GitLab From f3475f3ef4b6fcd2117d840736cf1f19fd76f00e Mon Sep 17 00:00:00 2001 From: Brahima Dibassi Date: Wed, 30 Jul 2025 20:19:40 +0200 Subject: [PATCH 22/22] Tezlink/Kernel/Transfer : Remove comment in transfer_external function --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index db635505283f..73bdf839d1da 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -350,7 +350,6 @@ pub fn transfer_external( let mut ctx = Ctx::default(); ctx.source = address_from_contract(src_contract.clone()); - // Delegate to appropriate handler let success = transfer( host, context, -- GitLab