Skip to content

Commit

Permalink
transfer-contract: allow converting between Phoenix and Moonlight Dusk
Browse files Browse the repository at this point in the history
We add the `convert` function to the contract, leveraging the deposit
and withdrawal capabilities to atomically swap Dusk between the Phoenix
and Moonlight models.

The function is meant to be called directly - meaning as the first and
only call - and takes a `Withdraw` as an argument, such that the user
can prove ownership of either the account or address being deposited to.

Resolves: #1994
  • Loading branch information
Eduardo Leegwater Simões committed Jul 25, 2024
1 parent 0a3a130 commit f618d34
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 9 deletions.
5 changes: 5 additions & 0 deletions contracts/transfer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ unsafe fn withdraw(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |arg| STATE.withdraw(arg))
}

#[no_mangle]
unsafe fn convert(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |arg| STATE.convert(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 @@ -15,7 +15,7 @@ use alloc::vec::Vec;
use dusk_bytes::Serializable;
use poseidon_merkle::Opening as PoseidonOpening;
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
use rusk_abi::{ContractError, ContractId, STAKE_CONTRACT};
use rusk_abi::{ContractError, ContractId, STAKE_CONTRACT, TRANSFER_CONTRACT};

use execution_core::{
transfer::{
Expand Down Expand Up @@ -201,6 +201,57 @@ impl TransferState {
self.mint_withdrawal("WITHDRAW", withdraw);
}

/// Takes the deposit addressed to this contract, and immediately withdraws
/// it, effectively performing a conversion between Phoenix notes and
/// Moonlight balance.
///
/// This functions checks whether the deposit included with the transaction
/// is the exact value included in `convert`, and imposes that the
/// caller is indeed this contract.
///
/// # Panics
/// This can only be called by this contract - the transfer contract - and
/// will panic if this is not the case.
pub fn convert(&mut self, convert: Withdraw) {
if rusk_abi::caller() != TRANSFER_CONTRACT {
panic!("Only a direct call can be a conversion");
}

let deposit = transitory::deposit_info_mut();
match deposit {
Deposit::Available(_, deposit_value) => {
let deposit_value = *deposit_value;

if convert.value() != deposit_value {
panic!("The value to convert doesn't match the value in the transaction");
}

// This is a direct contract call, as the first check in this
// function indicates, and the target of a deposit is always the
// direct contract call. Therefore, there is no need to check
// the available deposit is targets this contract.
//
// if deposit_contract != TRANSFER_CONTRACT {
// panic!();
// }

// Handle the withdrawal part of the conversion and set the
// deposit as being taken. Interesting to note is that we don't
// need to change the value held by the contract at all, since
// it never changes.
self.mint_withdrawal("CONVERT", convert);
*deposit = Deposit::Taken(TRANSFER_CONTRACT, deposit_value);
}
Deposit::None => panic!("There is no deposit in the transaction"),
// The transfer contract never calls this function directly, and the
// first check in this function is that the caller is the transfer
// contract. Therefore, the only way this code is reached is as a
// first contract call, and a deposit already being taken is
// impossible.
_ => unreachable!(),
}
}

/// Deposit funds to a contract's balance.
///
/// This function checks whether a deposit has been placed earlier on the
Expand Down
6 changes: 1 addition & 5 deletions contracts/transfer/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,17 @@ pub fn filter_notes_owned_by<I: IntoIterator<Item = Note>>(
}

pub fn create_moonlight_transaction(
session: &mut Session,
from_sk: &BlsSecretKey,
to: Option<BlsPublicKey>,
value: u64,
deposit: u64,
gas_limit: u64,
gas_price: u64,
nonce: u64,
exec: Option<impl Into<ContractExec>>,
) -> MoonlightTransaction {
let from = BlsPublicKey::from(from_sk);

let account =
account(session, &from).expect("Getting the account should work");
let nonce = account.nonce + 1;

let payload = MoonlightPayload {
from,
to,
Expand Down
190 changes: 187 additions & 3 deletions contracts/transfer/tests/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,13 @@ fn moonlight_transfer() {
);

let transaction = create_moonlight_transaction(
session,
&moonlight_sender_sk,
Some(moonlight_receiver_pk),
TRANSFER_VALUE,
0,
GAS_LIMIT,
LUX,
sender_account.nonce + 1,
None::<ContractExec>,
);

Expand Down Expand Up @@ -374,13 +374,13 @@ fn moonlight_alice_ping() {
);

let transaction = create_moonlight_transaction(
session,
&moonlight_sk,
None,
0,
0,
GAS_LIMIT,
LUX,
acc.nonce + 1,
contract_call,
);

Expand Down Expand Up @@ -456,7 +456,7 @@ fn phoenix_deposit_and_withdraw() {
println!("EXECUTE_DEPOSIT: {} gas", gas_spent);

let leaves = leaves_from_height(session, 1)
.expect("Getting the notes should succeed");
.expect("getting the notes should succeed");
assert_eq!(
PHOENIX_GENESIS_VALUE,
transfer_value
Expand Down Expand Up @@ -553,3 +553,187 @@ fn phoenix_deposit_and_withdraw() {
"Alice should have no balance after it is withdrawn"
);
}

#[test]
fn phoenix_to_moonlight_swap() {
const SWAP_VALUE: u64 = dusk(1.0);

let rng = &mut StdRng::seed_from_u64(0xfeeb);

let phoenix_sk = SecretKey::random(rng);
let phoenix_vk = ViewKey::from(&phoenix_sk);
let phoenix_pk = PublicKey::from(&phoenix_sk);

let moonlight_sk = BlsSecretKey::random(rng);
let moonlight_pk = BlsPublicKey::from(&moonlight_sk);

let vm = &mut rusk_abi::new_ephemeral_vm()
.expect("Creating ephemeral VM should work");
let mut session = &mut instantiate(rng, vm, &phoenix_pk, &moonlight_pk);

let swapper_account = account(&mut session, &moonlight_pk)
.expect("Getting account should succeed");

assert_eq!(
swapper_account.balance, MOONLIGHT_GENESIS_VALUE,
"The swapper's account should have the genesis value"
);

let leaves = leaves_from_height(session, 0)
.expect("getting the notes should succeed");
let notes = filter_notes_owned_by(
phoenix_vk,
leaves.into_iter().map(|leaf| leaf.note),
);

assert_eq!(notes.len(), 1, "There should be one note at this height");

let convert = Withdraw::new(
rng,
&moonlight_sk,
TRANSFER_CONTRACT.to_bytes(),
SWAP_VALUE,
WithdrawReceiver::Moonlight(moonlight_pk),
WithdrawReplayToken::Phoenix(vec![notes[0].gen_nullifier(&phoenix_sk)]),
);

let contract_call = ContractCall {
contract: TRANSFER_CONTRACT.to_bytes(),
fn_name: String::from("convert"),
fn_args: rkyv::to_bytes::<_, 1024>(&convert)
.expect("should serialize conversion correctly")
.to_vec(),
};

let tx = create_phoenix_transaction(
session,
&phoenix_sk,
&phoenix_pk,
GAS_LIMIT,
LUX,
[0],
42,
true,
SWAP_VALUE,
Some(contract_call),
);

let gas_spent = execute(session, tx).expect("Executing TX should succeed");
update_root(session).expect("Updating the root should succeed");

println!("CONVERT phoenix to moonlight: {} gas", gas_spent);

let swapper_account = account(&mut session, &moonlight_pk)
.expect("Getting account should succeed");

assert_eq!(
swapper_account.balance,
MOONLIGHT_GENESIS_VALUE + SWAP_VALUE,
"The swapper's account should have swap value added"
);

let leaves = leaves_from_height(session, 1)
.expect("getting the notes should succeed");
let notes = filter_notes_owned_by(
phoenix_vk,
leaves.into_iter().map(|leaf| leaf.note),
);

assert_eq!(
notes.len(),
3,
"New notes should have been created as transfer to self, change, and refund"
);
}

#[test]
fn moonlight_to_phoenix_swap() {
const SWAP_VALUE: u64 = dusk(1.0);

let rng = &mut StdRng::seed_from_u64(0xfeeb);

let phoenix_sk = SecretKey::random(rng);
let phoenix_vk = ViewKey::from(&phoenix_sk);
let phoenix_pk = PublicKey::from(&phoenix_sk);

let moonlight_sk = BlsSecretKey::random(rng);
let moonlight_pk = BlsPublicKey::from(&moonlight_sk);

let vm = &mut rusk_abi::new_ephemeral_vm()
.expect("Creating ephemeral VM should work");
let mut session = &mut instantiate(rng, vm, &phoenix_pk, &moonlight_pk);

let swapper_account = account(&mut session, &moonlight_pk)
.expect("Getting account should succeed");
let nonce = swapper_account.nonce + 1;

assert_eq!(
swapper_account.balance, MOONLIGHT_GENESIS_VALUE,
"The swapper's account should have the genesis value"
);

let leaves = leaves_from_height(session, 1)
.expect("getting the notes should succeed");
let notes = filter_notes_owned_by(
phoenix_vk,
leaves.into_iter().map(|leaf| leaf.note),
);

assert_eq!(notes.len(), 0, "There should be no notes at this height");

let address =
phoenix_pk.gen_stealth_address(&JubJubScalar::random(&mut *rng));
let note_sk = phoenix_sk.gen_note_sk(&address);

let convert = Withdraw::new(
rng,
&note_sk,
TRANSFER_CONTRACT.to_bytes(),
SWAP_VALUE,
WithdrawReceiver::Phoenix(address),
WithdrawReplayToken::Moonlight(nonce),
);

let contract_call = ContractCall {
contract: TRANSFER_CONTRACT.to_bytes(),
fn_name: String::from("convert"),
fn_args: rkyv::to_bytes::<_, 1024>(&convert)
.expect("should serialize conversion correctly")
.to_vec(),
};

let tx = create_moonlight_transaction(
&moonlight_sk,
None,
0,
SWAP_VALUE,
GAS_LIMIT,
LUX,
nonce,
Some(contract_call),
);

let gas_spent = execute(&mut session, tx)
.expect("Executing transaction should succeed");
update_root(session).expect("Updating the root should succeed");

println!("CONVERT moonlight to phoenix: {} gas", gas_spent);

let swapper_account = account(&mut session, &moonlight_pk)
.expect("Getting account should succeed");

assert_eq!(
swapper_account.balance,
MOONLIGHT_GENESIS_VALUE - gas_spent - SWAP_VALUE,
"The swapper's account should have had the swap value subtracted along with gas spent"
);

let leaves = leaves_from_height(session, 1)
.expect("getting the notes should succeed");
let notes = filter_notes_owned_by(
phoenix_vk,
leaves.into_iter().map(|leaf| leaf.note),
);

assert_eq!(notes.len(), 1, "A new note should have been created");
}

0 comments on commit f618d34

Please sign in to comment.