From f14cf65a4b6060f100023baaf155c2d4c2d415dd Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 28 May 2024 21:23:51 +0300 Subject: [PATCH 1/4] promise transfer no data example --- .../promises-features/src/common.rs | 3 + .../src/fwd_call_promises.rs | 47 ++++++++++ .../promises-features/wasm/src/lib.rs | 8 +- .../promises_call_transfer_callback.scen.json | 94 +++++++++++++++++++ .../tests/composability_scenario_go_test.rs | 6 ++ .../tests/composability_scenario_rs_test.rs | 6 ++ 6 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 contracts/feature-tests/composability/scenarios/promises_call_transfer_callback.scen.json diff --git a/contracts/feature-tests/composability/promises-features/src/common.rs b/contracts/feature-tests/composability/promises-features/src/common.rs index 9d0e245fb9..cbc494ce1b 100644 --- a/contracts/feature-tests/composability/promises-features/src/common.rs +++ b/contracts/feature-tests/composability/promises-features/src/common.rs @@ -20,6 +20,9 @@ pub trait CommonModule { #[indexed] payment: &BigUint, ); + #[event("callback_result")] + fn callback_result(&self, #[indexed] result: MultiValueEncoded); + #[view] #[storage_mapper("callback_data")] fn callback_data(&self) -> VecMapper>; diff --git a/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs b/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs index 92a2c8e0e1..672ac20a40 100644 --- a/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs +++ b/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs @@ -55,4 +55,51 @@ pub trait CallPromisesModule: common::CommonModule { args: ManagedVec::new(), }); } + + #[endpoint] + #[payable("*")] + fn forward_payment_callback(&self, to: ManagedAddress) { + let payment = self.call_value().any_payment(); + let gas_limit = self.blockchain().get_gas_left() / 2; + + self.tx() + .to(&to) + .raw_call("") + .gas(gas_limit) + .payment(payment) + .callback(self.callbacks().transfer_callback()) + .register_promise(); + } + + #[promises_callback] + fn transfer_callback(&self, #[call_result] result: MultiValueEncoded) { + self.callback_result(result); + + let call_value = self.call_value().any_payment(); + match call_value { + EgldOrMultiEsdtPayment::Egld(egld) => { + self.retrieve_funds_callback_event(&EgldOrEsdtTokenIdentifier::egld(), 0, &egld); + let _ = self.callback_data().push(&CallbackData { + callback_name: ManagedBuffer::from(b"transfer_callback"), + token_identifier: EgldOrEsdtTokenIdentifier::egld(), + token_nonce: 0, + token_amount: egld, + args: ManagedVec::new(), + }); + }, + EgldOrMultiEsdtPayment::MultiEsdt(multi_esdt) => { + for esdt in multi_esdt.into_iter() { + let token_identifier = EgldOrEsdtTokenIdentifier::esdt(esdt.token_identifier); + self.retrieve_funds_callback_event(&token_identifier, 0, &esdt.amount); + let _ = self.callback_data().push(&CallbackData { + callback_name: ManagedBuffer::from(b"transfer_callback"), + token_identifier, + token_nonce: 0, + token_amount: esdt.amount, + args: ManagedVec::new(), + }); + } + }, + } + } } diff --git a/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs b/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs index 1dc649df0d..895ab88636 100644 --- a/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs +++ b/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs @@ -5,10 +5,10 @@ //////////////////////////////////////////////////// // Init: 1 -// Endpoints: 10 +// Endpoints: 11 // Async Callback (empty): 1 -// Promise callbacks: 3 -// Total number of exported functions: 15 +// Promise callbacks: 4 +// Total number of exported functions: 17 #![no_std] @@ -24,12 +24,14 @@ multiversx_sc_wasm_adapter::endpoints! { clear_callback_data => clear_callback_data forward_promise_accept_funds => forward_promise_accept_funds forward_promise_retrieve_funds => forward_promise_retrieve_funds + forward_payment_callback => forward_payment_callback promise_raw_single_token => promise_raw_single_token promise_raw_multi_transfer => promise_raw_multi_transfer forward_sync_retrieve_funds_bt => forward_sync_retrieve_funds_bt forward_sync_retrieve_funds_bt_twice => forward_sync_retrieve_funds_bt_twice forward_promise_retrieve_funds_back_transfers => forward_promise_retrieve_funds_back_transfers retrieve_funds_callback => retrieve_funds_callback + transfer_callback => transfer_callback the_one_callback => the_one_callback retrieve_funds_back_transfers_callback => retrieve_funds_back_transfers_callback ) diff --git a/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback.scen.json b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback.scen.json new file mode 100644 index 0000000000..3658141127 --- /dev/null +++ b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback.scen.json @@ -0,0 +1,94 @@ +{ + "steps": [ + { + "step": "setState", + "accounts": { + "address:a_user": { + "nonce": "0", + "balance": "1000" + }, + "sc:vault": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../vault/output/vault.mxsc.json" + }, + "sc:forwarder": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../promises-features/output/promises-features.mxsc.json" + } + } + }, + { + "step": "scCall", + "id": "1", + "tx": { + "from": "address:a_user", + "to": "sc:forwarder", + "egldValue": "1000", + "function": "forward_payment_callback", + "arguments": [ + "sc:vault" + ], + "gasLimit": "60,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "0", + "logs": [ + { + "address": "sc:forwarder", + "endpoint": "str:transferValueOnly", + "topics": [ + "1000", + "sc:vault" + ], + "data": [ + "str:AsyncCall", + "str:accept_funds" + ] + }, + { + "address": "sc:vault", + "endpoint": "str:accept_funds", + "topics": [ + "str:accept_funds", + "1000" + ], + "data": [ + "" + ] + } + ], + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "address:a_user": { + "nonce": "*", + "balance": "0", + "storage": {}, + "code": "" + }, + "sc:vault": { + "nonce": "0", + "balance": "1000", + "storage": { + "str:call_counts|nested:str:accept_funds": "1" + }, + "code": "mxsc:../vault/output/vault.mxsc.json" + }, + "sc:forwarder": { + "nonce": "0", + "balance": "0", + "storage": {}, + "code": "mxsc:../promises-features/output/promises-features.mxsc.json" + } + } + } + ] +} diff --git a/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs b/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs index 8f61befdbe..704ef3668c 100644 --- a/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs +++ b/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs @@ -437,6 +437,12 @@ fn promises_call_callback_directly_go() { world().run("scenarios/promises_call_callback_directly.scen.json"); } +#[test] +#[ignore = "VM does not support this"] +fn promises_call_transfer_callback_go() { + world().run("scenarios/promises_call_transfer_callback.scen.json"); +} + #[test] #[ignore = "TODO"] fn promises_multi_transfer_go() { diff --git a/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs b/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs index 33db8b4b49..2d0eee062b 100644 --- a/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs +++ b/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs @@ -487,6 +487,12 @@ fn promises_call_callback_directly_rs() { world().run("scenarios/promises_call_callback_directly.scen.json"); } +#[test] +#[ignore = "VM does not support this"] +fn promises_call_transfer_callback_rs() { + world().run("scenarios/promises_call_transfer_callback.scen.json"); +} + #[test] fn promises_multi_transfer_rs() { world().run("scenarios/promises_multi_transfer.scen.json"); From 6ea4e86cc4a44b8fd56772ecd2b50928bd4b072c Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 3 Jan 2025 16:37:48 +0200 Subject: [PATCH 2/4] promise transfer no data example: ESDT/EGLD --- chain/vm/src/vm_err_msg.rs | 2 + chain/vm/src/vm_hooks/vh_handler/vh_send.rs | 6 ++ ...ises_call_transfer_callback_egld.scen.json | 45 ++++++++++++ ...ses_call_transfer_callback_esdt.scen.json} | 71 ++++++++++++++++--- .../tests/composability_scenario_go_test.rs | 10 ++- .../tests/composability_scenario_rs_test.rs | 10 ++- 6 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_egld.scen.json rename contracts/feature-tests/composability/scenarios/{promises_call_transfer_callback.scen.json => promises_call_transfer_callback_esdt.scen.json} (55%) diff --git a/chain/vm/src/vm_err_msg.rs b/chain/vm/src/vm_err_msg.rs index 339e926f4f..4356223a5e 100644 --- a/chain/vm/src/vm_err_msg.rs +++ b/chain/vm/src/vm_err_msg.rs @@ -14,3 +14,5 @@ pub const ERROR_SIGNALLED_BY_SMARTCONTRACT: &str = "error signalled by smartcont pub const ERROR_NO_CALLBACK_CLOSURE: &str = "no callback for closure, cannot call callback directly"; + +pub const PROMISES_TOKENIZE_FAILED: &str = "tokenize failed"; diff --git a/chain/vm/src/vm_hooks/vh_handler/vh_send.rs b/chain/vm/src/vm_hooks/vh_handler/vh_send.rs index 67d590f772..df8e24448a 100644 --- a/chain/vm/src/vm_hooks/vh_handler/vh_send.rs +++ b/chain/vm/src/vm_hooks/vh_handler/vh_send.rs @@ -5,6 +5,7 @@ use crate::{ }, tx_mock::{AsyncCallTxData, Promise, TxFunctionName, TxTokenTransfer}, types::{top_encode_big_uint, top_encode_u64, RawHandle, VMAddress, VMCodeMetadata}, + vm_err_msg, vm_hooks::VMHooksHandlerSource, }; use num_traits::Zero; @@ -215,6 +216,11 @@ pub trait VMHooksSend: VMHooksHandlerSource { let endpoint_name = self .m_types_lock() .mb_to_function_name(endpoint_name_handle); + if endpoint_name.is_empty() { + // immitating the behavior of the VM + // TODO: lift limitation from the VM, then also remove this condition here + self.vm_error(vm_err_msg::PROMISES_TOKENIZE_FAILED); + } let arg_buffer = self.m_types_lock().mb_get_vec_of_bytes(arg_buffer_handle); let tx_hash = self.tx_hash(); let callback_closure_data = self.m_types_lock().mb_get(callback_closure_handle).to_vec(); diff --git a/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_egld.scen.json b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_egld.scen.json new file mode 100644 index 0000000000..8d5a3f5a95 --- /dev/null +++ b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_egld.scen.json @@ -0,0 +1,45 @@ +{ + "steps": [ + { + "step": "setState", + "accounts": { + "address:a_user": { + "nonce": "0", + "balance": "1000" + }, + "sc:vault": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../vault/output/vault.mxsc.json" + }, + "sc:forwarder": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../promises-features/output/promises-features.mxsc.json" + } + } + }, + { + "step": "scCall", + "id": "1", + "tx": { + "from": "address:a_user", + "to": "sc:forwarder", + "egldValue": "1000", + "function": "forward_payment_callback", + "arguments": [ + "sc:vault" + ], + "gasLimit": "60,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "10", + "message": "str:tokenize failed", + "gas": "*", + "refund": "*" + } + } + ] +} diff --git a/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback.scen.json b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_esdt.scen.json similarity index 55% rename from contracts/feature-tests/composability/scenarios/promises_call_transfer_callback.scen.json rename to contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_esdt.scen.json index 3658141127..50cc085548 100644 --- a/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback.scen.json +++ b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_esdt.scen.json @@ -5,7 +5,10 @@ "accounts": { "address:a_user": { "nonce": "0", - "balance": "1000" + "balance": "0", + "esdt": { + "str:FWD-TOKEN": "1000" + } }, "sc:vault": { "nonce": "0", @@ -25,7 +28,12 @@ "tx": { "from": "address:a_user", "to": "sc:forwarder", - "egldValue": "1000", + "esdtValue": [ + { + "tokenIdentifier": "str:FWD-TOKEN", + "value": "1000" + } + ], "function": "forward_payment_callback", "arguments": [ "sc:vault" @@ -39,22 +47,52 @@ "logs": [ { "address": "sc:forwarder", - "endpoint": "str:transferValueOnly", + "endpoint": "str:ESDTTransfer", "topics": [ + "str:FWD-TOKEN", + "", "1000", "sc:vault" ], "data": [ "str:AsyncCall", - "str:accept_funds" + "str:ESDTTransfer", + "str:FWD-TOKEN", + "1000" ] }, { "address": "sc:vault", - "endpoint": "str:accept_funds", + "endpoint": "str:transferValueOnly", "topics": [ - "str:accept_funds", - "1000" + "", + "sc:forwarder" + ], + "data": [ + "str:AsyncCallback", + "str:transfer_callback", + "0x00" + ] + }, + { + "address": "sc:forwarder", + "endpoint": "str:transfer_callback", + "topics": [ + "str:callback_result", + "0x00" + ], + "data": [ + "" + ] + }, + { + "address": "sc:forwarder", + "endpoint": "str:transfer_callback", + "topics": [ + "str:retrieve_funds_callback", + "str:EGLD", + "", + "" ], "data": [ "" @@ -76,16 +114,29 @@ }, "sc:vault": { "nonce": "0", - "balance": "1000", + "balance": "0", + "esdt": { + "str:FWD-TOKEN": "1000" + }, "storage": { - "str:call_counts|nested:str:accept_funds": "1" + "str:call_counts|nested:str:accept_funds": "0" }, "code": "mxsc:../vault/output/vault.mxsc.json" }, "sc:forwarder": { "nonce": "0", "balance": "0", - "storage": {}, + "storage": { + "str:callback_data.len": "1", + "str:callback_data.item|u32:1": [ + "nested:str:transfer_callback", + "nested:str:EGLD", + "u64:0", + "u32:0", + "u32:0" + ] + + }, "code": "mxsc:../promises-features/output/promises-features.mxsc.json" } } diff --git a/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs b/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs index c947da3bff..e6b5144c19 100644 --- a/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs +++ b/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs @@ -438,9 +438,13 @@ fn promises_call_callback_directly_go() { } #[test] -#[ignore = "VM does not support this"] -fn promises_call_transfer_callback_go() { - world().run("scenarios/promises_call_transfer_callback.scen.json"); +fn promises_call_transfer_callback_egld_go() { + world().run("scenarios/promises_call_transfer_callback_egld.scen.json"); +} + +#[test] +fn promises_call_transfer_callback_esdt_go() { + world().run("scenarios/promises_call_transfer_callback_esdt.scen.json"); } #[test] diff --git a/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs b/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs index 7e04d2f9cb..771e60eddb 100644 --- a/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs +++ b/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs @@ -489,9 +489,13 @@ fn promises_call_callback_directly_rs() { } #[test] -#[ignore = "VM does not support this"] -fn promises_call_transfer_callback_rs() { - world().run("scenarios/promises_call_transfer_callback.scen.json"); +fn promises_call_transfer_callback_egld_rs() { + world().run("scenarios/promises_call_transfer_callback_egld.scen.json"); +} + +#[test] +fn promises_call_transfer_callback_esdt_rs() { + world().run("scenarios/promises_call_transfer_callback_esdt.scen.json"); } #[test] From 9288192575cbb1c4b69c12dba3e737869531d35d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 3 Jan 2025 16:37:54 +0200 Subject: [PATCH 3/4] proxy update --- .../promises-features/src/promises_feature_proxy.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs b/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs index d5fb14bf19..7c5a740b0f 100644 --- a/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs +++ b/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs @@ -127,6 +127,18 @@ where .original_result() } + pub fn forward_payment_callback< + Arg0: ProxyArg>, + >( + self, + to: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("forward_payment_callback") + .argument(&to) + .original_result() + } + pub fn promise_raw_single_token< Arg0: ProxyArg>, Arg1: ProxyArg>, From 15d1db77559189327d00c261c9f73f54872181e5 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 3 Jan 2025 17:02:11 +0200 Subject: [PATCH 4/4] promise transfer no data - syntactic sugar --- .../src/fwd_call_promises.rs | 1 - .../tx_exec/tx_exec_async_promises.rs | 36 +++++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs b/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs index 672ac20a40..04fe76e64e 100644 --- a/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs +++ b/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs @@ -64,7 +64,6 @@ pub trait CallPromisesModule: common::CommonModule { self.tx() .to(&to) - .raw_call("") .gas(gas_limit) .payment(payment) .callback(self.callbacks().transfer_callback()) diff --git a/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs b/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs index 38e38fcbcd..43f28cd6d8 100644 --- a/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs +++ b/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs @@ -188,26 +188,26 @@ where } } -impl Tx, (), To, Payment, (), FunctionCall, Callback> +impl + Tx, (), To, Payment, ExplicitGas, (), Callback> where Api: CallTypeApi, To: TxToSpecified>, Payment: TxPayment>, + GasValue: TxGasValue>, Callback: TxPromisesCallback, { - /// ## Incorrect call - /// - /// Must set **gas** in order to call `register_promise`. + /// Launches a transaction as an asynchronous promise (async v2 mechanism), + /// but without calling any function on the destination. /// - /// ## Safety - /// - /// This version of the method must never be called. It is only here to provide a more readable error. - pub unsafe fn register_promise(self) { - ErrorHelper::::signal_error_with_message("register_promise requires explicit gas"); + /// Such calls are useful for appending callbacks to simple transfers, + /// mitigating edge cases such as non-payable SCs and frozen assets. + pub fn register_promise(self) { + self.raw_call("").register_promise(); } } -impl Tx, (), To, Payment, (), (), Callback> +impl Tx, (), To, Payment, (), FunctionCall, Callback> where Api: CallTypeApi, To: TxToSpecified>, @@ -216,36 +216,34 @@ where { /// ## Incorrect call /// - /// Must set **gas** and **function call** in order to call `register_promise`. + /// Must set **gas** in order to call `register_promise`. /// /// ## Safety /// /// This version of the method must never be called. It is only here to provide a more readable error. pub unsafe fn register_promise(self) { - ErrorHelper::::signal_error_with_message( - "register_promise requires explicit gas and function call", - ); + ErrorHelper::::signal_error_with_message("register_promise requires explicit gas"); } } -impl - Tx, (), To, Payment, ExplicitGas, (), Callback> +impl Tx, (), To, Payment, (), (), Callback> where Api: CallTypeApi, To: TxToSpecified>, Payment: TxPayment>, - GasValue: TxGasValue>, Callback: TxPromisesCallback, { /// ## Incorrect call /// - /// Must set **function call** in order to call `register_promise`. + /// Must set **gas** in order to call `register_promise`, even when no SC endpoint is called. /// /// ## Safety /// /// This version of the method must never be called. It is only here to provide a more readable error. pub unsafe fn register_promise(self) { - ErrorHelper::::signal_error_with_message("register_promise requires function call"); + ErrorHelper::::signal_error_with_message( + "register_promise requires explicit gas (even when no SC endpoint is called)", + ); } }