Skip to content

Commit

Permalink
transfer-contract: add transfer_to_contract function
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Eduardo Leegwater Simões committed Sep 4, 2024
1 parent ea0ada5 commit 452c019
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 4 deletions.
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 @@ -28,7 +28,8 @@ use execution_core::{
withdraw::{
Withdraw, WithdrawReceiver, WithdrawReplayToken, WithdrawSignature,
},
Transaction, TRANSFER_CONTRACT,
ReceiveFromContract, Transaction, TransferToContract,
TRANSFER_CONTRACT,
},
BlsScalar, ContractError, ContractId,
};
Expand Down Expand Up @@ -302,6 +303,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.
///
/// Delegates to [`Self::spend_and_execute_phoenix`] and
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"
);
}

0 comments on commit 452c019

Please sign in to comment.