From cadd2354be5ff945fb51a2d2d28c6e37b1c0b0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Tue, 3 Sep 2024 17:19:54 +0200 Subject: [PATCH 1/4] execution-core: add `TransferToContract` and `ReceiveFromContract` The `TransferToContract` is sent by a sender contract to the transfer contract, signifying the sender's intent to send Dusk to a receiver contract. `ReceiveFromContract` is used by the transfer contract to inform the receiver of the sender and the amount being sent. --- execution-core/src/transfer.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/execution-core/src/transfer.rs b/execution-core/src/transfer.rs index b93e253f41..e64ec4cec1 100644 --- a/execution-core/src/transfer.rs +++ b/execution-core/src/transfer.rs @@ -7,7 +7,9 @@ //! Types related to Dusk's transfer contract that are shared across the //! network. +use alloc::string::String; use alloc::vec::Vec; + use core::fmt::Debug; use bytecheck::CheckBytes; @@ -316,3 +318,31 @@ impl From for Transaction { Self::Moonlight(tx) } } + +/// The payload sent by a contract to the transfer contract to transfer some of +/// its funds to another contract. +#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct TransferToContract { + /// Contract to transfer funds to. + pub contract: ContractId, + /// Amount to send to the contract. + pub value: u64, + /// Function name to call on the contract. + pub fn_name: String, + /// Extra data sent along with [`ReceiveFromContract`] + pub data: Vec, +} + +/// The payload sent by the transfer contract to a contract receiving funds from +/// another contract. +#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct ReceiveFromContract { + /// Contract that sent the funds. + pub contract: ContractId, + /// Amount sent by the contract. + pub value: u64, + /// Extra data sent by the sender. + pub data: Vec, +} From bd97182c5f24e9e66d76063d8f31e9deb9225e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Tue, 3 Sep 2024 17:25:46 +0200 Subject: [PATCH 2/4] transfer-contract: add `transfer_to_contract` function The `transfer_to_contract` function can be called by a sender contract to transfer Dusk to a receiver contract. Once called, a function specified by the sender is called on the receiver contract to inform it of the sender's ID and the amount of Dusk sent. The receiver can choose to accept the transfer, by successfully concluding the execution of the function called to inform it, or it can panic and effectively reject the transfer. The `TransferToContract` is used as an argument to the `transfer_to_contract`, and is constructed by the contract calling it. `ReceiveFromContract` is the ata the receiver contract *must* accept for a function to be able to successfully receive funds from another contract. If a contract wished to expose one or more of these functions, it is heavily recommended that they panic if called by anyone else apart from the transfer contract. --- contracts/transfer/src/lib.rs | 5 + contracts/transfer/src/state.rs | 53 ++++++++- contracts/transfer/tests/transfer.rs | 154 ++++++++++++++++++++++++++- 3 files changed, 208 insertions(+), 4 deletions(-) diff --git a/contracts/transfer/src/lib.rs b/contracts/transfer/src/lib.rs index 2389de01bf..d21bc6bc64 100644 --- a/contracts/transfer/src/lib.rs +++ b/contracts/transfer/src/lib.rs @@ -43,6 +43,11 @@ unsafe fn convert(arg_len: u32) -> u32 { rusk_abi::wrap_call(arg_len, |arg| STATE.convert(arg)) } +#[no_mangle] +unsafe fn transfer_to_contract(arg_len: u32) -> u32 { + rusk_abi::wrap_call(arg_len, |arg| STATE.transfer_to_contract(arg)) +} + // Queries #[no_mangle] diff --git a/contracts/transfer/src/state.rs b/contracts/transfer/src/state.rs index b747783c81..14289a84f1 100644 --- a/contracts/transfer/src/state.rs +++ b/contracts/transfer/src/state.rs @@ -27,7 +27,8 @@ use execution_core::{ withdraw::{ Withdraw, WithdrawReceiver, WithdrawReplayToken, WithdrawSignature, }, - Transaction, PANIC_NONCE_NOT_READY, TRANSFER_CONTRACT, + ReceiveFromContract, Transaction, TransferToContract, + PANIC_NONCE_NOT_READY, TRANSFER_CONTRACT, }, BlsScalar, ContractError, ContractId, }; @@ -301,6 +302,56 @@ impl TransferState { } } + /// Transfer funds from one contract's balance to another. + /// + /// Contracts can call the function and expect that if it succeeds the funds + /// are succesfully transferred to the contract they specify. Contracts + /// receiving funds are expected to expose the function specified by the + /// sender, which is called using a [`ReceiveFromContract`] as argument. It + /// is recommended that the receiving contract check that the call + /// originates from the transfer contract, and subsequently run any logic it + /// may wish to handle the transfer - including panicking, which will + /// effectively reject the transfer. + /// + /// # Panics + /// The function will panic if it is not being called by a contract (or if + /// it is called by the transfer contract itself), if the call to the + /// receiving contract fails, or if the sending contract doesn't have enough + /// funds. + pub fn transfer_to_contract(&mut self, transfer: TransferToContract) { + let from = rusk_abi::caller() + .expect("A transfer to a contract must happen in the context of a transaction"); + + if from == TRANSFER_CONTRACT { + panic!("Cannot be called directly by the transfer contract"); + } + + let from_balance = self + .contract_balances + .get_mut(&from) + .expect("Caller must have a balance"); + + if *from_balance < transfer.value { + panic!("Caller must have enough balance"); + } + + *from_balance -= transfer.value; + + let to_balance = + self.contract_balances.entry(transfer.contract).or_insert(0); + + *to_balance += transfer.value; + + let receive = ReceiveFromContract { + contract: from, + value: transfer.value, + data: transfer.data, + }; + + rusk_abi::call(transfer.contract, &transfer.fn_name, &receive) + .expect("Calling receiver should succeed") + } + /// The top level transaction execution function. /// /// This will emplace the deposit in the state, if it exists - making it diff --git a/contracts/transfer/tests/transfer.rs b/contracts/transfer/tests/transfer.rs index af40fe58da..5a9123c8e8 100644 --- a/contracts/transfer/tests/transfer.rs +++ b/contracts/transfer/tests/transfer.rs @@ -30,7 +30,7 @@ use execution_core::{ ViewKey as PhoenixViewKey, }, withdraw::{Withdraw, WithdrawReceiver, WithdrawReplayToken}, - TRANSFER_CONTRACT, + TransferToContract, TRANSFER_CONTRACT, }, ContractId, JubJubScalar, LUX, }; @@ -70,7 +70,7 @@ fn instantiate( "../../../target/dusk/wasm32-unknown-unknown/release/alice.wasm" ); let bob_bytecode = include_bytes!( - "../../../target/dusk/wasm32-unknown-unknown/release/alice.wasm" + "../../../target/dusk/wasm32-unknown-unknown/release/bob.wasm" ); let mut session = rusk_abi::new_genesis_session(vm, CHAIN_ID); @@ -96,7 +96,10 @@ fn instantiate( session .deploy( bob_bytecode, - ContractData::builder().owner(OWNER).contract_id(BOB_ID), + ContractData::builder() + .owner(OWNER) + .contract_id(BOB_ID) + .constructor_arg(&1u8), GAS_LIMIT, ) .expect("Deploying the bob contract should succeed"); @@ -904,3 +907,148 @@ fn swap_wrong_contract_targeted() { assert!(notes.is_empty(), "A new note should not been created"); } + +/// In this test we deposit some Dusk to the Alice contract, and subsequently +/// proceed to call Alice's `transfer_to_contract` function, targetting Bob as +/// the receiver of the transfer. +#[test] +fn transfer_to_contract() { + const DEPOSIT_VALUE: u64 = MOONLIGHT_GENESIS_VALUE / 2; + const TRANSFER_VALUE: u64 = DEPOSIT_VALUE / 2; + + let rng = &mut StdRng::seed_from_u64(0xfeeb); + + let vm = &mut rusk_abi::new_ephemeral_vm() + .expect("Creating ephemeral VM should work"); + + let phoenix_pk = PhoenixPublicKey::from(&PhoenixSecretKey::random(rng)); + + let moonlight_sk = AccountSecretKey::random(rng); + let moonlight_pk = AccountPublicKey::from(&moonlight_sk); + + let session = &mut instantiate(rng, vm, &phoenix_pk, &moonlight_pk); + + let acc = account(session, &moonlight_pk) + .expect("Getting the account should succeed"); + let alice_balance = contract_balance(session, ALICE_ID) + .expect("Querying the contract balance should succeed"); + let bob_balance = contract_balance(session, BOB_ID) + .expect("Querying the contract balance should succeed"); + + assert_eq!( + acc.balance, MOONLIGHT_GENESIS_VALUE, + "The depositer account should have the genesis value" + ); + assert_eq!( + alice_balance, 0, + "Alice must have an initial balance of zero" + ); + assert_eq!(bob_balance, 0, "Bob must have an initial balance of zero"); + + let fn_args = rkyv::to_bytes::<_, 256>(&DEPOSIT_VALUE) + .expect("Serializing should succeed") + .to_vec(); + let contract_call = Some(ContractCall { + contract: ALICE_ID, + fn_name: String::from("deposit"), + fn_args, + }); + + let chain_id = + chain_id(session).expect("Getting the chain ID should succeed"); + + let transaction = MoonlightTransaction::new( + &moonlight_sk, + None, + 0, + DEPOSIT_VALUE, + GAS_LIMIT, + LUX, + acc.nonce + 1, + chain_id, + contract_call, + ); + + let receipt = + execute(session, transaction).expect("Transaction should succeed"); + let gas_spent_deposit = receipt.gas_spent; + + println!("MOONLIGHT DEPOSIT: {:?}", receipt.data); + println!("MOONLIGHT DEPOSIT: {gas_spent_deposit} gas"); + + let acc = account(session, &moonlight_pk) + .expect("Getting the account should succeed"); + let alice_balance = contract_balance(session, ALICE_ID) + .expect("Querying the contract balance should succeed"); + let bob_balance = contract_balance(session, BOB_ID) + .expect("Querying the contract balance should succeed"); + + assert_eq!( + acc.balance, + MOONLIGHT_GENESIS_VALUE - gas_spent_deposit - DEPOSIT_VALUE, + "The account should decrease by the amount spent and the deposit sent" + ); + assert_eq!( + alice_balance, DEPOSIT_VALUE, + "Alice must have the deposit in their balance" + ); + assert_eq!(bob_balance, 0, "Bob must have a balance of zero"); + + let transfer = TransferToContract { + contract: BOB_ID, + value: TRANSFER_VALUE, + fn_name: String::from("recv_transfer"), + data: vec![], + }; + let fn_args = rkyv::to_bytes::<_, 256>(&transfer) + .expect("Serializing should succeed") + .to_vec(); + let contract_call = Some(ContractCall { + contract: ALICE_ID, + fn_name: String::from("transfer_to_contract"), + fn_args, + }); + + let transaction = MoonlightTransaction::new( + &moonlight_sk, + None, + 0, + 0, + GAS_LIMIT, + LUX, + acc.nonce + 1, + chain_id, + contract_call, + ); + + let receipt = + execute(session, transaction).expect("Transaction should succeed"); + let gas_spent_send = receipt.gas_spent; + + println!("MOONLIGHT SEND_TO_CONTRACT: {:?}", receipt.data); + println!("MOONLIGHT SEND_TO_CONTRACT: {gas_spent_send} gas"); + + let acc = account(session, &moonlight_pk) + .expect("Getting the account should succeed"); + let alice_balance = contract_balance(session, ALICE_ID) + .expect("Querying the contract balance should succeed"); + let bob_balance = contract_balance(session, BOB_ID) + .expect("Querying the contract balance should succeed"); + + assert_eq!( + acc.balance, + MOONLIGHT_GENESIS_VALUE + - gas_spent_deposit + - gas_spent_send + - DEPOSIT_VALUE, + "The account should decrease by the amount spent and the deposit sent" + ); + assert_eq!( + alice_balance, DEPOSIT_VALUE - TRANSFER_VALUE, + "Alice must have the deposit minus the transferred amount in their balance" + ); + assert_eq!( + bob_balance, TRANSFER_VALUE, + "Bob must have the transfer value as balance" + ); +} From 69aa51fba8e583a9d2d723dd933c6d5456911850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Tue, 3 Sep 2024 17:40:50 +0200 Subject: [PATCH 3/4] alice-contract: add `transfer_to_contract` function --- contracts/alice/src/lib.rs | 5 +++++ contracts/alice/src/state.rs | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/contracts/alice/src/lib.rs b/contracts/alice/src/lib.rs index 975bc2d147..291ae8dc67 100644 --- a/contracts/alice/src/lib.rs +++ b/contracts/alice/src/lib.rs @@ -33,4 +33,9 @@ mod wasm { unsafe fn deposit(arg_len: u32) -> u32 { rusk_abi::wrap_call(arg_len, |arg| STATE.deposit(arg)) } + + #[no_mangle] + unsafe fn transfer_to_contract(arg_len: u32) -> u32 { + rusk_abi::wrap_call(arg_len, |arg| STATE.transfer_to_contract(arg)) + } } diff --git a/contracts/alice/src/state.rs b/contracts/alice/src/state.rs index 7d1c9287c5..dfbc23835d 100644 --- a/contracts/alice/src/state.rs +++ b/contracts/alice/src/state.rs @@ -4,7 +4,9 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use execution_core::transfer::{withdraw::Withdraw, TRANSFER_CONTRACT}; +use execution_core::transfer::{ + withdraw::Withdraw, TransferToContract, TRANSFER_CONTRACT, +}; /// Alice contract. #[derive(Debug, Clone)] @@ -22,6 +24,15 @@ impl Alice { pub fn deposit(&mut self, value: u64) { let _: () = rusk_abi::call(TRANSFER_CONTRACT, "deposit", &value) - .expect("Transparent withdrawal transaction should succeed"); + .expect("Transparent deposit transaction should succeed"); + } + + pub fn transfer_to_contract(&mut self, transfer: TransferToContract) { + let _: () = rusk_abi::call( + TRANSFER_CONTRACT, + "transfer_to_contract", + &transfer, + ) + .expect("Transferring to contract should succeed"); } } From 635107e141f1790da63c8fb012418e385a6172cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Tue, 3 Sep 2024 17:41:16 +0200 Subject: [PATCH 4/4] bob-contract: add `recv_transfer` function --- contracts/bob/Cargo.toml | 2 +- contracts/bob/src/lib.rs | 5 +++++ contracts/bob/src/state.rs | 12 +++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/contracts/bob/Cargo.toml b/contracts/bob/Cargo.toml index 812a98cacc..c19b43d0ea 100644 --- a/contracts/bob/Cargo.toml +++ b/contracts/bob/Cargo.toml @@ -8,8 +8,8 @@ resolver = "2" crate-type = ["cdylib", "rlib"] [dependencies] -rusk-abi = { version = "0.13.0-rc", path = "../../rusk-abi", features = ["debug"] } execution-core = { version = "0.1.0", path = "../../execution-core" } +rusk-abi = { version = "0.13.0-rc", path = "../../rusk-abi" } rkyv = { version = "0.7", default-features = false, features = ["size_32"] } bytecheck = { version = "0.6", default-features = false } dusk-bytes = "0.1" diff --git a/contracts/bob/src/lib.rs b/contracts/bob/src/lib.rs index 77e2cf6f26..5e1beb0e56 100644 --- a/contracts/bob/src/lib.rs +++ b/contracts/bob/src/lib.rs @@ -54,4 +54,9 @@ mod wasm { unsafe fn nonce(arg_len: u32) -> u32 { rusk_abi::wrap_call(arg_len, |()| STATE.nonce()) } + + #[no_mangle] + unsafe fn recv_transfer(arg_len: u32) -> u32 { + rusk_abi::wrap_call(arg_len, |arg| STATE.recv_transfer(arg)) + } } diff --git a/contracts/bob/src/state.rs b/contracts/bob/src/state.rs index 5c173f63e7..cf1e37d6c8 100644 --- a/contracts/bob/src/state.rs +++ b/contracts/bob/src/state.rs @@ -10,6 +10,7 @@ use bytecheck::CheckBytes; use dusk_bytes::Serializable; use execution_core::{ signatures::bls::{PublicKey as BlsPublicKey, Signature as BlsSignature}, + transfer::ReceiveFromContract, ContractId, }; use rkyv::{Archive, Deserialize, Serialize}; @@ -28,11 +29,16 @@ pub struct OwnerMessage { pub struct Bob { value: u8, nonce: u64, + total_dusk: u64, } impl Bob { pub const fn new() -> Self { - Self { value: 0, nonce: 0 } + Self { + value: 0, + nonce: 0, + total_dusk: 0, + } } #[allow(dead_code)] @@ -91,4 +97,8 @@ impl Bob { pub fn nonce(&mut self) -> u64 { self.nonce } + + pub fn recv_transfer(&mut self, recv: ReceiveFromContract) { + self.total_dusk += recv.value; + } }