Skip to content

Commit

Permalink
Merge pull request #2252 from dusk-network/contract-transfers
Browse files Browse the repository at this point in the history
Add transfers between contracts
  • Loading branch information
ureeves authored Sep 4, 2024
2 parents 0f003cf + 635107e commit 92179d3
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 8 deletions.
5 changes: 5 additions & 0 deletions contracts/alice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
15 changes: 13 additions & 2 deletions contracts/alice/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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");
}
}
2 changes: 1 addition & 1 deletion contracts/bob/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 5 additions & 0 deletions contracts/bob/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
12 changes: 11 additions & 1 deletion contracts/bob/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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)]
Expand Down Expand Up @@ -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;
}
}
5 changes: 5 additions & 0 deletions contracts/transfer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
53 changes: 52 additions & 1 deletion contracts/transfer/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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
Expand Down
154 changes: 151 additions & 3 deletions contracts/transfer/tests/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use execution_core::{
ViewKey as PhoenixViewKey,
},
withdraw::{Withdraw, WithdrawReceiver, WithdrawReplayToken},
TRANSFER_CONTRACT,
TransferToContract, TRANSFER_CONTRACT,
},
ContractId, JubJubScalar, LUX,
};
Expand Down Expand Up @@ -70,7 +70,7 @@ fn instantiate<Rng: RngCore + CryptoRng>(
"../../../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);
Expand All @@ -96,7 +96,10 @@ fn instantiate<Rng: RngCore + CryptoRng>(
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");
Expand Down Expand Up @@ -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"
);
}
30 changes: 30 additions & 0 deletions execution-core/src/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -316,3 +318,31 @@ impl From<MoonlightTransaction> 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<u8>,
}

/// 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<u8>,
}

0 comments on commit 92179d3

Please sign in to comment.