From 4513738c065a0f10998b75c43e2cadf7867a8eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Thu, 5 Sep 2024 21:57:48 +0200 Subject: [PATCH 1/3] execution-core: add `TransferToAccount` This structure will be used as an argument to a transfer contract function allowing for a contract to send Dusk to a Moonlight account. --- execution-core/CHANGELOG.md | 3 +++ execution-core/src/transfer.rs | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/execution-core/CHANGELOG.md b/execution-core/CHANGELOG.md index 36c85753d3..413f91d0e0 100644 --- a/execution-core/CHANGELOG.md +++ b/execution-core/CHANGELOG.md @@ -48,6 +48,9 @@ signatures::{ } } transfer::{ + TransferToContract; + ReceiveFromContract; + TransferToAccount; data::{ ContractBytecode; ContractCall; diff --git a/execution-core/src/transfer.rs b/execution-core/src/transfer.rs index c62b7c6698..bd1dd4b1bc 100644 --- a/execution-core/src/transfer.rs +++ b/execution-core/src/transfer.rs @@ -358,3 +358,14 @@ pub struct ReceiveFromContract { /// Extra data sent by the sender. pub data: Vec, } + +/// The payload sent by a contract to the transfer contract to transfer some of +/// its funds to an account. +#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct TransferToAccount { + /// Account to transfer funds to. + pub account: AccountPublicKey, + /// Amount to send to the account. + pub value: u64, +} From 34cd01c48e7759ea155b3dec1e444eb356e7ee99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Thu, 5 Sep 2024 21:59:55 +0200 Subject: [PATCH 2/3] alice-contract: add `transfer_to_account` function This will allow us to test that the contract successfully transfers funds to an account using the "transfer_to_account" function taking a `TransferToAccount` as argument. --- contracts/alice/src/lib.rs | 5 +++++ contracts/alice/src/state.rs | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/contracts/alice/src/lib.rs b/contracts/alice/src/lib.rs index 291ae8dc67..ba185195fc 100644 --- a/contracts/alice/src/lib.rs +++ b/contracts/alice/src/lib.rs @@ -38,4 +38,9 @@ mod wasm { unsafe fn transfer_to_contract(arg_len: u32) -> u32 { rusk_abi::wrap_call(arg_len, |arg| STATE.transfer_to_contract(arg)) } + + #[no_mangle] + unsafe fn transfer_to_account(arg_len: u32) -> u32 { + rusk_abi::wrap_call(arg_len, |arg| STATE.transfer_to_account(arg)) + } } diff --git a/contracts/alice/src/state.rs b/contracts/alice/src/state.rs index dfbc23835d..fd48fcc39b 100644 --- a/contracts/alice/src/state.rs +++ b/contracts/alice/src/state.rs @@ -5,7 +5,8 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use execution_core::transfer::{ - withdraw::Withdraw, TransferToContract, TRANSFER_CONTRACT, + withdraw::Withdraw, TransferToAccount, TransferToContract, + TRANSFER_CONTRACT, }; /// Alice contract. @@ -35,4 +36,13 @@ impl Alice { ) .expect("Transferring to contract should succeed"); } + + pub fn transfer_to_account(&mut self, transfer: TransferToAccount) { + rusk_abi::call::<_, ()>( + TRANSFER_CONTRACT, + "transfer_to_account", + &transfer, + ) + .expect("Transferring to account should succeed"); + } } From 2971fbf33204e7f1099f7d6987e189f5b7f0e1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Thu, 5 Sep 2024 22:01:45 +0200 Subject: [PATCH 3/3] transfer-contract: add fn `transfer_to_account` The `transfer_to_account` can be called by contract using the `TransferToAccount` structure to move some of its funds to a Moonlight account. --- contracts/transfer/src/lib.rs | 5 + contracts/transfer/src/state.rs | 39 ++- contracts/transfer/tests/transfer.rs | 359 ++++++++++++++++++++++++++- 3 files changed, 398 insertions(+), 5 deletions(-) diff --git a/contracts/transfer/src/lib.rs b/contracts/transfer/src/lib.rs index d21bc6bc64..7cc56f902c 100644 --- a/contracts/transfer/src/lib.rs +++ b/contracts/transfer/src/lib.rs @@ -48,6 +48,11 @@ unsafe fn transfer_to_contract(arg_len: u32) -> u32 { rusk_abi::wrap_call(arg_len, |arg| STATE.transfer_to_contract(arg)) } +#[no_mangle] +unsafe fn transfer_to_account(arg_len: u32) -> u32 { + rusk_abi::wrap_call(arg_len, |arg| STATE.transfer_to_account(arg)) +} + // Queries #[no_mangle] diff --git a/contracts/transfer/src/state.rs b/contracts/transfer/src/state.rs index 14289a84f1..c550f7ddc4 100644 --- a/contracts/transfer/src/state.rs +++ b/contracts/transfer/src/state.rs @@ -27,8 +27,8 @@ use execution_core::{ withdraw::{ Withdraw, WithdrawReceiver, WithdrawReplayToken, WithdrawSignature, }, - ReceiveFromContract, Transaction, TransferToContract, - PANIC_NONCE_NOT_READY, TRANSFER_CONTRACT, + ReceiveFromContract, Transaction, TransferToAccount, + TransferToContract, PANIC_NONCE_NOT_READY, TRANSFER_CONTRACT, }, BlsScalar, ContractError, ContractId, }; @@ -352,6 +352,41 @@ impl TransferState { .expect("Calling receiver should succeed") } + /// Transfer funds from a contract balance to a Moonlight account. + /// + /// Contracts can call the function and expect that if it succeeds the funds + /// are successfully transferred to the account they specify. + /// + /// # Panics + /// The function will panic if it is not being called by a contract, if it + /// is called by the transfer contract itself, or if the calling contract + /// doesn't have enough funds. + pub fn transfer_to_account(&mut self, transfer: TransferToAccount) { + let from = rusk_abi::caller() + .expect("A transfer to an account 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"); + } + + let account = self + .accounts + .entry(transfer.account.to_bytes()) + .or_insert(EMPTY_ACCOUNT); + + *from_balance -= transfer.value; + account.balance += transfer.value; + } + /// 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 a895aa4d59..b4149d46dd 100644 --- a/contracts/transfer/tests/transfer.rs +++ b/contracts/transfer/tests/transfer.rs @@ -30,9 +30,9 @@ use execution_core::{ ViewKey as PhoenixViewKey, }, withdraw::{Withdraw, WithdrawReceiver, WithdrawReplayToken}, - TransferToContract, TRANSFER_CONTRACT, + TransferToAccount, TransferToContract, TRANSFER_CONTRACT, }, - ContractId, JubJubScalar, LUX, + ContractError, ContractId, JubJubScalar, LUX, }; use rusk_abi::{ContractData, Session, VM}; @@ -99,7 +99,7 @@ fn instantiate( ContractData::builder() .owner(OWNER) .contract_id(BOB_ID) - .constructor_arg(&1u8), + .init_arg(&1u8), GAS_LIMIT, ) .expect("Deploying the bob contract should succeed"); @@ -1058,3 +1058,356 @@ fn transfer_to_contract() { "Bob must have the transfer value as balance" ); } + +/// In this test we deposit some Dusk from a moonlight account to the Alice +/// contract, and subsequently call the Alice contract to trigger a transfer +/// back to the same account. +#[test] +fn transfer_to_account() { + 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"); + + 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" + ); + + 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, + ) + .expect("Creating moonlight transaction should succeed"); + + 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"); + + 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" + ); + + let transfer = TransferToAccount { + account: moonlight_pk, + value: TRANSFER_VALUE, + }; + 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_account"), + fn_args, + }); + + let transaction = MoonlightTransaction::new( + &moonlight_sk, + None, + 0, + 0, + GAS_LIMIT, + LUX, + acc.nonce + 1, + chain_id, + contract_call, + ) + .expect("Creating moonlight transaction should succeed"); + + let receipt = + execute(session, transaction).expect("Transaction should succeed"); + let gas_spent_send = receipt.gas_spent; + + println!("MOONLIGHT SEND_TO_ACCOUNT: {:?}", receipt.data); + println!("MOONLIGHT SEND_TO_ACCOUNT: {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"); + + assert_eq!( + acc.balance, + MOONLIGHT_GENESIS_VALUE + - gas_spent_deposit + - gas_spent_send + - DEPOSIT_VALUE + + TRANSFER_VALUE, + "The account should decrease by the amount spent and the deposit sent, \ + and increase by the transfer" + ); + assert_eq!( + alice_balance, DEPOSIT_VALUE - TRANSFER_VALUE, + "Alice must have the deposit minus the transferred amount in their balance" + ); +} + +/// In this test we try to transfer some Dusk from a contract to an account, +/// when the contract doesn't have sufficient funds. +#[test] +fn transfer_to_account_insufficient_funds() { + // Transfer value larger than DEPOSIT + const DEPOSIT_VALUE: u64 = MOONLIGHT_GENESIS_VALUE / 2; + const TRANSFER_VALUE: u64 = 2 * DEPOSIT_VALUE; + + 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"); + + 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" + ); + + 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, + ) + .expect("Creating moonlight transaction should succeed"); + + 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"); + + 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" + ); + + let transfer = TransferToAccount { + account: moonlight_pk, + value: TRANSFER_VALUE, + }; + 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_account"), + fn_args, + }); + + let transaction = MoonlightTransaction::new( + &moonlight_sk, + None, + 0, + 0, + GAS_LIMIT, + LUX, + acc.nonce + 1, + chain_id, + contract_call, + ) + .expect("Creating moonlight transaction should succeed"); + + let receipt = + execute(session, transaction).expect("Transaction should succeed"); + let gas_spent_send = receipt.gas_spent; + + println!( + "MOONLIGHT SEND_TO_ACCOUNT INSUFFICIENT FUNDS: {:?}", + receipt.data + ); + println!( + "MOONLIGHT SEND_TO_ACCOUNT INSUFFICIENT_FUNDS: {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"); + + assert!( + matches!(receipt.data, Err(_)), + "Alice should error because the transfer contract panics" + ); + 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, + "Alice must have the deposit amount still in their balance" + ); +} + +/// In this test we try to call the function directly - i.e. not initiated by a +/// contract, but by the transaction itself. +#[test] +fn transfer_to_account_direct_call() { + const TRANSFER_VALUE: u64 = MOONLIGHT_GENESIS_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"); + + assert_eq!( + acc.balance, MOONLIGHT_GENESIS_VALUE, + "The depositer account should have the genesis value" + ); + + let transfer = TransferToAccount { + account: moonlight_pk, + value: TRANSFER_VALUE, + }; + let fn_args = rkyv::to_bytes::<_, 256>(&transfer) + .expect("Serializing should succeed") + .to_vec(); + let contract_call = Some(ContractCall { + contract: TRANSFER_CONTRACT, + fn_name: String::from("transfer_to_account"), + fn_args, + }); + + let chain_id = + chain_id(session).expect("Getting the chain ID should succeed"); + + let transaction = MoonlightTransaction::new( + &moonlight_sk, + None, + 0, + 0, + GAS_LIMIT, + LUX, + acc.nonce + 1, + chain_id, + contract_call, + ) + .expect("Creating moonlight transaction should succeed"); + + let receipt = + execute(session, transaction).expect("Transaction should succeed"); + let gas_spent_send = receipt.gas_spent; + + println!("MOONLIGHT SEND_TO_ACCOUNT DIRECTLY: {:?}", receipt.data); + println!("MOONLIGHT SEND_TO_ACCOUNT DIRECTLY: {gas_spent_send} gas"); + + let acc = account(session, &moonlight_pk) + .expect("Getting the account should succeed"); + + assert!( + matches!(receipt.data, Err(ContractError::Panic(_))), + "The transfer contract should panic on a direct call" + ); + assert_eq!( + acc.balance, + MOONLIGHT_GENESIS_VALUE - gas_spent_send, + "The account should decrease by the amount spent" + ); +}