diff --git a/contracts/transfer/src/lib.rs b/contracts/transfer/src/lib.rs index 915f8a7e6d..4566ba069e 100644 --- a/contracts/transfer/src/lib.rs +++ b/contracts/transfer/src/lib.rs @@ -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] diff --git a/contracts/transfer/src/state.rs b/contracts/transfer/src/state.rs index 874f2bd1c3..f836196dd8 100644 --- a/contracts/transfer/src/state.rs +++ b/contracts/transfer/src/state.rs @@ -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::{ @@ -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 diff --git a/contracts/transfer/tests/common.rs b/contracts/transfer/tests/common.rs index a18543dcb9..6b5e84ed37 100644 --- a/contracts/transfer/tests/common.rs +++ b/contracts/transfer/tests/common.rs @@ -183,21 +183,17 @@ pub fn filter_notes_owned_by>( } pub fn create_moonlight_transaction( - session: &mut Session, from_sk: &BlsSecretKey, to: Option, value: u64, deposit: u64, gas_limit: u64, gas_price: u64, + nonce: u64, exec: Option>, ) -> 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, diff --git a/contracts/transfer/tests/transfer.rs b/contracts/transfer/tests/transfer.rs index c145687703..27a6c89667 100644 --- a/contracts/transfer/tests/transfer.rs +++ b/contracts/transfer/tests/transfer.rs @@ -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::, ); @@ -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, ); @@ -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 @@ -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, + ¬e_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"); +}