From cb72ad4ad851c41dbbddc588df116cd1ee777855 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 19 Sep 2024 14:24:38 +0000 Subject: [PATCH 01/67] feat: DEX draft --- .../contracts/dex_contract/Nargo.toml | 10 ++++ .../contracts/dex_contract/src/main.nr | 60 +++++++++++++++++++ .../noir-contracts/contracts/dex_contract/wat | 35 +++++++++++ 3 files changed, 105 insertions(+) create mode 100644 noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/dex_contract/src/main.nr create mode 100644 noir-projects/noir-contracts/contracts/dex_contract/wat diff --git a/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml new file mode 100644 index 00000000000..a0c93d4890e --- /dev/null +++ b/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dex_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } +compressed_string = { path = "../../../aztec-nr/compressed-string" } +authwit = { path = "../../../aztec-nr/authwit" } diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr new file mode 100644 index 00000000000..9d0c7670eb5 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -0,0 +1,60 @@ +use dep::aztec::macros::aztec; + +// Uniswap v2 style AMM DEX contract +#[aztec] +contract DEX { + use dep::compressed_string::FieldCompressedString; + use dep::aztec::{ + prelude::{NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress}, + encrypted_logs::{encrypted_note_emission::encode_and_encrypt_note_with_keys}, + hash::pedersen_hash, keys::getters::get_public_keys, note::constants::MAX_NOTES_PER_PAGE, + protocol_types::traits::is_empty, utils::comparison::Comparator, + protocol_types::{point::Point, traits::Serialize}, + macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}} + }; + use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; + use std::{embedded_curve_ops::EmbeddedCurvePoint, meta::derive}; + use crate::types::nft_note::NFTNote; + + #[event] + #[derive(Serialize)] + struct Swap { + amount0In: u32, + amount1In: u32, + amount0Out: u32, + amount1Out: u32, + } + + #[storage] + struct Storage { + + } + + #[public] + #[initializer] + fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>) { + + } + + #[public] + fn add_liquidity(from: AztecAddress, amountADesired: Field, amountBDesired: Field, amountAMin: Field, amountBMin: Field, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + + // transfer the amounts from the sender to the contract and mint the liquidity token to `from` + } + + /** + * Cancel a private authentication witness. + * @param inner_hash The inner hash of the authwit to cancel. + */ + #[private] + fn cancel_authwit(inner_hash: Field) { + let on_behalf_of = context.msg_sender(); + let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); + context.push_nullifier(nullifier); + } +} diff --git a/noir-projects/noir-contracts/contracts/dex_contract/wat b/noir-projects/noir-contracts/contracts/dex_contract/wat new file mode 100644 index 00000000000..343e50dc480 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/dex_contract/wat @@ -0,0 +1,35 @@ +univ2 dex + +public fn join pool + // maybe could keep the owner private, idk + +pool has public balances + + +swapper -> + pool: private fn swap(amount_in, min_amount_out) + token_in.unshield(from: swapper, to: pool, amount: amount_in) // transfer_from_to_public + authwit validation signed by the swapper (approval) + burns swapper notes + creates swapper change note + self.enqueue_public_call(increase_public_balance, pool, amount_in) + + token_out.prepare_partial_balance_note(recipient: swapper) -> (id, log) + partial balance note = [balance_slot, recipient, randomness] // missing amount + stored in public storage + self.enqueue_public_call(store_partial_note(partial_note)) + sstore + + pxe.once_this_tx_is_included_in_a_block check for a note with amount whatever... + + self.enqueue_public_call(settle_swap(amount_in, min_amount_out, partial note id for token out)) + read balances + compute amount out given amont in and swap fee (swap fee is public) // note that the pool has already received the tokens! + check min amount out + + emit event + + token_out.transfer_into_partial_note(from: pool, amount: amount out, partial note id) + reduce pool balance + assert partial note id exists (transient storage) + finalize and commit partial note with amount out \ No newline at end of file From 3750f6aa59cc8093d74c324478d3a7d05c577e57 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 20 Sep 2024 08:44:38 +0000 Subject: [PATCH 02/67] WIP --- .../contracts/dex_contract/Nargo.toml | 1 + .../contracts/dex_contract/src/lib.nr | 6 ++ .../contracts/dex_contract/src/main.nr | 88 ++++++++++++++++--- 3 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr diff --git a/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml index a0c93d4890e..0229d45cc72 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml @@ -8,3 +8,4 @@ type = "contract" aztec = { path = "../../../aztec-nr/aztec" } compressed_string = { path = "../../../aztec-nr/compressed-string" } authwit = { path = "../../../aztec-nr/authwit" } +token = { path = "../token_contract" } diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr new file mode 100644 index 00000000000..0eecc03e980 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr @@ -0,0 +1,6 @@ +// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset +pub fn get_quote(amountA: u32, reserveA: u32, reserveB: u32) -> u32 { + assert(amountA > 0, "INSUFFICIENT_AMOUNT"); + assert((reserveA > 0) & (reserveB > 0), "INSUFFICIENT_LIQUIDITY"); + (amountA * reserveB) / reserveA +} diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 9d0c7670eb5..22cd17b95ea 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -1,20 +1,20 @@ +mod lib; + use dep::aztec::macros::aztec; -// Uniswap v2 style AMM DEX contract +// A minimal implementation of Uniswap v2 style AMM DEX pool. #[aztec] contract DEX { - use dep::compressed_string::FieldCompressedString; + use crate::lib::get_quote; + use dep::aztec::{ - prelude::{NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress}, + prelude::{NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, PublicImmutable, PrivateSet, AztecAddress}, encrypted_logs::{encrypted_note_emission::encode_and_encrypt_note_with_keys}, hash::pedersen_hash, keys::getters::get_public_keys, note::constants::MAX_NOTES_PER_PAGE, - protocol_types::traits::is_empty, utils::comparison::Comparator, - protocol_types::{point::Point, traits::Serialize}, - macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}} + protocol_types::traits::is_empty, utils::comparison::Comparator }; use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; - use std::{embedded_curve_ops::EmbeddedCurvePoint, meta::derive}; - use crate::types::nft_note::NFTNote; + use dep::token::Token; #[event] #[derive(Serialize)] @@ -25,28 +25,94 @@ contract DEX { amount1Out: u32, } + // We store the settings of the pool in a struct such that to load it from PublicImmutable asserts only + // a single merkle proof. + // (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). + #[derive(Serialize)] + struct Settings { + token0: AztecAddress, + token1: AztecAddress, + } + + #[derive(Serialize)] + struct Reserves { + reserve0: u32, + reserve1: u32, + } + #[storage] struct Storage { - + settings: PublicImmutable, + reserves: PublicMutable, } #[public] #[initializer] - fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>) { + fn constructor(token0: AztecAddress, token1: AztecAddress) { + // Since we don't have inheritance it seems the easiest to deploy the standard token and use it as a liquidity + // tracking contract. This contract would be an admin of the liquidity contract. + + // TODO: either deploy here the liquidity contract or pass its address as an arg on input and verify that + // it was deployed correctly. + + let settings = Settings { token0, token1 }; + storage.settings.initialize(settings); + // We don't need to initialize the reserves as the default in storage is 0. } #[public] - fn add_liquidity(from: AztecAddress, amountADesired: Field, amountBDesired: Field, amountAMin: Field, amountBMin: Field, nonce: Field) { + fn add_liquidity(from: AztecAddress, amount0Desired: u32, amount1Desired: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { if (!from.eq(context.msg_sender())) { assert_current_call_valid_authwit_public(&mut context, from); } else { assert(nonce == 0, "invalid nonce"); } + assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); + + let reserves = storage.reserves.read(); + + let reserve0 = reserves.reserve0; + let reserve1 = reserves.reserve1; + + let mut amount0 = amount0Desired; + let mut amount1 = amount1Desired; + + if ((reserve0 != 0) | (reserve1 != 0)) { + let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); + if (amount1Optimal <= amount1Desired) { + assert(amount1Optimal >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + amount0 = amount0Desired; + amount1 = amount1Optimal; + } else { + let amount0Optimal = get_quote(amount1Desired, reserve1, reserve0); + assert(amount0Optimal <= amount0Desired); + assert(amount0Optimal >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + amount0 = amount0Optimal; + amount1 = amount1Desired; + } + } + + // TODO: how do we transfer the tokens to the contract? this does not work with authwits well + + // let settings = storage.settings.read(); + // Token::at(storage.token.read()).mint_public(to, amount).call(&mut context); + // transfer the amounts from the sender to the contract and mint the liquidity token to `from` } + #[public] + fn remove_liquidity(from: AztecAddress, liquidity: Field, amount0Min: Field, amount1Min: Field, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + + // burn the liquidity token from `from` and transfer the amounts to `from` + } + /** * Cancel a private authentication witness. * @param inner_hash The inner hash of the authwit to cancel. From e5d9f8126a9ccf508bbc1e336dc2d597a27247fa Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 20 Sep 2024 10:31:15 +0000 Subject: [PATCH 03/67] WIP --- .../contracts/dex_contract/src/main.nr | 50 ++++++++++++------- .../lending_contract/src/interest_math.nr | 2 +- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 22cd17b95ea..53b7b590ae0 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -16,15 +16,6 @@ contract DEX { use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; use dep::token::Token; - #[event] - #[derive(Serialize)] - struct Swap { - amount0In: u32, - amount1In: u32, - amount0Out: u32, - amount1Out: u32, - } - // We store the settings of the pool in a struct such that to load it from PublicImmutable asserts only // a single merkle proof. // (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). @@ -32,10 +23,13 @@ contract DEX { struct Settings { token0: AztecAddress, token1: AztecAddress, + liquidity_token: AztecAddress, } #[derive(Serialize)] struct Reserves { + // TODO: Replace the use of u32 with larger type everywhere in this contract. + // Didn't use U128 here because of https://github.com/AztecProtocol/aztec-packages/issues/8271 reserve0: u32, reserve1: u32, } @@ -46,6 +40,8 @@ contract DEX { reserves: PublicMutable, } + global MINIMUM_LIQUIDITY: u32 = 1000; + #[public] #[initializer] fn constructor(token0: AztecAddress, token1: AztecAddress) { @@ -54,17 +50,18 @@ contract DEX { // TODO: either deploy here the liquidity contract or pass its address as an arg on input and verify that // it was deployed correctly. + let liquidity_token = AztecAddress::zero(); - let settings = Settings { token0, token1 }; + let settings = Settings { token0, token1, liquidity_token }; storage.settings.initialize(settings); // We don't need to initialize the reserves as the default in storage is 0. } #[public] - fn add_liquidity(from: AztecAddress, amount0Desired: u32, amount1Desired: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { - if (!from.eq(context.msg_sender())) { - assert_current_call_valid_authwit_public(&mut context, from); + fn add_liquidity(liquidity_provider: AztecAddress, amount0Desired: u32, amount1Desired: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { + if (!liquidity_provider.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, liquidity_provider); } else { assert(nonce == 0, "invalid nonce"); } @@ -72,13 +69,14 @@ contract DEX { assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); let reserves = storage.reserves.read(); + let settings = storage.settings.read(); + // Calculate the amounts to be added to the pool let reserve0 = reserves.reserve0; let reserve1 = reserves.reserve1; let mut amount0 = amount0Desired; let mut amount1 = amount1Desired; - if ((reserve0 != 0) | (reserve1 != 0)) { let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); if (amount1Optimal <= amount1Desired) { @@ -94,12 +92,26 @@ contract DEX { } } - // TODO: how do we transfer the tokens to the contract? this does not work with authwits well - - // let settings = storage.settings.read(); - // Token::at(storage.token.read()).mint_public(to, amount).call(&mut context); + // TODO: Transfer the tokens to this contract. How do we do it? this does not work with authwits well + + // Calculate the amount of liquidity tokens to mint + let liquidity_token = Token::at(settings.liquidity_token); + let total_supply = liquidity_token.total_supply().call(&mut context); + let mut liquidity: u32 = 0; + if (total_supply == 0) { + // This is using Tonelli-Shanks to compute sqrt but Uni is using babylonia method. TODO: is it fine to use a different one? + // TODO: avoid the casts here. Shall we use a method natively working with some integer type? + liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u32; + liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens + } else { + liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); + } + assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); + liquidity_token.mint_public(liquidity_provider, liquidity as Field).call(&mut context); - // transfer the amounts from the sender to the contract and mint the liquidity token to `from` + // Update the reserves + let updated_reserves = Reserves { reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; + storage.reserves.write(updated_reserves); } #[public] diff --git a/noir-projects/noir-contracts/contracts/lending_contract/src/interest_math.nr b/noir-projects/noir-contracts/contracts/lending_contract/src/interest_math.nr index e92c91f908d..e3e1e2e1d1b 100644 --- a/noir-projects/noir-contracts/contracts/lending_contract/src/interest_math.nr +++ b/noir-projects/noir-contracts/contracts/lending_contract/src/interest_math.nr @@ -1,7 +1,7 @@ // Binomial approximation of exponential // using lower than desired precisions for everything due to u128 limit // (1+x)^n = 1+n*x+[n/2*(n-1)]*x^2+[n/6*(n-1)*(n-2)*x^3]... -// we are loosing around almost 8 digits of precision from yearly -> daily interest +// we are losing around almost 8 digits of precision from yearly -> daily interest // dividing with 31536000 (seconds per year). // rate must be measured with higher precision than 10^9. // we use e18, and rates >= 4% yearly. Otherwise need more precision From 2109f23d19456bcb4f2b30972fbb53b518e47972 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 20 Sep 2024 10:37:38 +0000 Subject: [PATCH 04/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 53b7b590ae0..f4e3c366c7a 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -29,7 +29,7 @@ contract DEX { #[derive(Serialize)] struct Reserves { // TODO: Replace the use of u32 with larger type everywhere in this contract. - // Didn't use U128 here because of https://github.com/AztecProtocol/aztec-packages/issues/8271 + // Didn't use U128 here because of https://github.com/AztecProtocol/aztec-packages/issues/8271 and because it might be insufficient. reserve0: u32, reserve1: u32, } @@ -92,14 +92,14 @@ contract DEX { } } - // TODO: Transfer the tokens to this contract. How do we do it? this does not work with authwits well + // TODO: Transfer the tokens to this contract. How do we do it? this does not work with authwits as we don't know the amounts before calling this function. // Calculate the amount of liquidity tokens to mint let liquidity_token = Token::at(settings.liquidity_token); let total_supply = liquidity_token.total_supply().call(&mut context); let mut liquidity: u32 = 0; if (total_supply == 0) { - // This is using Tonelli-Shanks to compute sqrt but Uni is using babylonia method. TODO: is it fine to use a different one? + // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonia method. Is it fine to use a different one? // TODO: avoid the casts here. Shall we use a method natively working with some integer type? liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u32; liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens From 3c6e17ab6f585a99c583f3c64cbec723e751ce17 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 20 Sep 2024 10:49:42 +0000 Subject: [PATCH 05/67] WIP --- .../noir-contracts/contracts/dex_contract/{wat => notes} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename noir-projects/noir-contracts/contracts/dex_contract/{wat => notes} (100%) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/wat b/noir-projects/noir-contracts/contracts/dex_contract/notes similarity index 100% rename from noir-projects/noir-contracts/contracts/dex_contract/wat rename to noir-projects/noir-contracts/contracts/dex_contract/notes From 1d29890de7a846869058df77f2651f513540507e Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 20 Sep 2024 11:31:23 +0000 Subject: [PATCH 06/67] WIP --- .../contracts/dex_contract/src/main.nr | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index f4e3c366c7a..2df9e6c96c4 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -93,13 +93,26 @@ contract DEX { } // TODO: Transfer the tokens to this contract. How do we do it? this does not work with authwits as we don't know the amounts before calling this function. + // We could use partial notes for this: + // 1. A user knows the maximum amount0, amount1 they want to deposit, + // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, + // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! + // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, + // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transient_storage_slot_randomness), + // --> this function will: + // 4.1 load both the partial note and the burned amount from transient storage + // 4.2 check that actual_amount < burned_amount, + // 4.3 publicly mint the actual_amount to msg_sender (the DEX in our case), + // 4.4 finalize the partial note amount with `burned_amount - actual_amount` and emit the note. + // + // Note 1: This is essentially a 1 person alternative to the fee refund flow where we have a user and an FPC. // Calculate the amount of liquidity tokens to mint let liquidity_token = Token::at(settings.liquidity_token); let total_supply = liquidity_token.total_supply().call(&mut context); let mut liquidity: u32 = 0; if (total_supply == 0) { - // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonia method. Is it fine to use a different one? + // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? // TODO: avoid the casts here. Shall we use a method natively working with some integer type? liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u32; liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens From 408f0c915ca2da66943e68e4c69dbbdf0d0c991e Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 20 Sep 2024 14:45:52 +0000 Subject: [PATCH 07/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 2df9e6c96c4..2d47369ff4c 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -106,6 +106,9 @@ contract DEX { // 4.4 finalize the partial note amount with `burned_amount - actual_amount` and emit the note. // // Note 1: This is essentially a 1 person alternative to the fee refund flow where we have a user and an FPC. + // Cost of this flow: + // num calls: 2 private calls to setup the partial notes, 1 public call to add_liquidity, 2 public calls to finalize_transfer_to_public_with_refund + // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool // Calculate the amount of liquidity tokens to mint let liquidity_token = Token::at(settings.liquidity_token); From 6a65781423b135a4e6fd08565a55558e5ad48420 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 20 Sep 2024 15:01:33 +0000 Subject: [PATCH 08/67] WIP --- .../contracts/dex_contract/src/main.nr | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 2d47369ff4c..01a580b1241 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -123,6 +123,7 @@ contract DEX { liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); } assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); + // TODO: Should we mint here to private? It would be more costly. liquidity_token.mint_public(liquidity_provider, liquidity as Field).call(&mut context); // Update the reserves @@ -131,14 +132,28 @@ contract DEX { } #[public] - fn remove_liquidity(from: AztecAddress, liquidity: Field, amount0Min: Field, amount1Min: Field, nonce: Field) { + fn remove_liquidity(liquidity_provider: AztecAddress, liquidity: Field, amount0Min: Field, amount1Min: Field, nonce: Field) { if (!from.eq(context.msg_sender())) { assert_current_call_valid_authwit_public(&mut context, from); } else { assert(nonce == 0, "invalid nonce"); } + let settings = storage.settings.read(); + let liquidity_token = Token::at(settings.liquidity_token); + // burn the liquidity token from `from` and transfer the amounts to `from` + // TODO: shall we try to burn private balance? UX-wise burning public one is not an issue because the liquidity + // provider can always unshield and he can do it in 1 tx thanks to BatchCall. + // TODO: Current token's burn_public does not care about admin rights so this does not work without authwit. + liquidity_token.burn_public(liquidity_provider, liquidity).call(&mut context); + + // TODO: + // (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); + // (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); + // (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); + // require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + // require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); } /** From 873894ec8d575d44f918f614c0d95cc24256d6f9 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 08:44:39 +0000 Subject: [PATCH 09/67] WIP --- .../contracts/dex_contract/src/main.nr | 72 +++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 01a580b1241..fcd53f71cd3 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -2,7 +2,7 @@ mod lib; use dep::aztec::macros::aztec; -// A minimal implementation of Uniswap v2 style AMM DEX pool. +// A Noir implementation of simplified Uniswap v2 pool. #[aztec] contract DEX { use crate::lib::get_quote; @@ -58,6 +58,8 @@ contract DEX { // We don't need to initialize the reserves as the default in storage is 0. } + // Adds liquidity for `liquidity_provider` to the pool. `amount0Desired` and `amount1Desired` are the amounts + // of tokens we ideally want to add. `amount0Min` and `amount1Min` are the minimum amounts we are willing to add. #[public] fn add_liquidity(liquidity_provider: AztecAddress, amount0Desired: u32, amount1Desired: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { if (!liquidity_provider.eq(context.msg_sender())) { @@ -78,12 +80,16 @@ contract DEX { let mut amount0 = amount0Desired; let mut amount1 = amount1Desired; if ((reserve0 != 0) | (reserve1 != 0)) { + // First calculate the optimal amount of token1 based on the desired amount of token0. let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); if (amount1Optimal <= amount1Desired) { + // Revert if the optimal amount of token1 is less than the desired amount of token1. assert(amount1Optimal >= amount1Min, "INSUFFICIENT_1_AMOUNT"); amount0 = amount0Desired; amount1 = amount1Optimal; } else { + // We got more amount of token1 than desired so we try repeating the process but this time by quoting + // based on token1. let amount0Optimal = get_quote(amount1Desired, reserve1, reserve0); assert(amount0Optimal <= amount0Desired); assert(amount0Optimal >= amount0Min, "INSUFFICIENT_0_AMOUNT"); @@ -94,7 +100,7 @@ contract DEX { // TODO: Transfer the tokens to this contract. How do we do it? this does not work with authwits as we don't know the amounts before calling this function. // We could use partial notes for this: - // 1. A user knows the maximum amount0, amount1 they want to deposit, + // 1. A user knows the maximum amount0, amount1 (the desired amounts are max) they want to deposit, // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, @@ -139,21 +145,63 @@ contract DEX { assert(nonce == 0, "invalid nonce"); } + let reserves = storage.reserves.read(); let settings = storage.settings.read(); + + let token0 = Token::at(settings.token0); + let token1 = Token::at(settings.token1); let liquidity_token = Token::at(settings.liquidity_token); - // burn the liquidity token from `from` and transfer the amounts to `from` - // TODO: shall we try to burn private balance? UX-wise burning public one is not an issue because the liquidity - // provider can always unshield and he can do it in 1 tx thanks to BatchCall. - // TODO: Current token's burn_public does not care about admin rights so this does not work without authwit. - liquidity_token.burn_public(liquidity_provider, liquidity).call(&mut context); + // We transfer the liquidity tokens from the liquidity provider to this contract. + // TODO: I am transferring the liquidity tokens in public here to follow the Uniswap v2 implementation. + // --> There are 2 things we need to discuss here: + // 1. This is assuming the dev has the liquidity token in public. I think this is fine as we mint to public + // and I am not really sure if it makes sense to not have liquidity tokens in public as these tokens are + // commonly used in other DeFi (e.g. borrowing against the liquidity token in AAVE) and for that we'll + // most likely need them to be public. + // 2. The "transfer from" flows are less efficient as they check authwits. It might be the most efficient + // to just make users transfer the liquidity token to the pool in a `BatchCall`. + liquidity_token.transfer_public(liquidity_provider, liquidity).call(&mut context); // TODO: - // (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); - // (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); - // (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); - // require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); - // require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + + // Calculate the amounts to be added to the pool + let reserve0 = reserves.reserve0; + let reserve1 = reserves.reserve1; + + + let balance0 = token0.balance_of_public().call(&mut context); + let balance1 = token1.balance_of_public().call(&mut context); + + // Uniswap burns balance of the pool instead of just burning based on the `liquidity` arg on input. I assume + // the do this to release stranded liquidity (e.g. somebody transferring liquidity tokens by accident). This + // might not make sense here as the liqudity token is a separate contract and hence we need to do a costly + // public call to get the balance. + // TODO: Shall we streamline this and just burn based on the `liquidity` arg? Maybe we could even avoid + // the transfer and just allow the pool to directly call + // `liquidity_token.burn_public(liquidity_provider, liquidity)`. + let liquidity = liquidity_token.balance_of_public().call(&mut context); + + // bool feeOn = _mintFee(_reserve0, _reserve1); + // uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + // amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution + // amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution + // require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); + // _burn(address(this), liquidity); + // _safeTransfer(_token0, to, amount0); + // _safeTransfer(_token1, to, amount1); + // balance0 = IERC20(_token0).balanceOf(address(this)); + // balance1 = IERC20(_token1).balanceOf(address(this)); + + // _update(balance0, balance1, _reserve0, _reserve1); + // if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date + // emit Burn(msg.sender, amount0, amount1, to); + + + + + assert(amount0 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + assert(amount1 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); } /** From 687906deeebfe23e126be05f82f5cbdc1bbc2d37 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 10:02:30 +0000 Subject: [PATCH 10/67] WIP --- .../contracts/dex_contract/src/main.nr | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index fcd53f71cd3..4703ce3560b 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -118,7 +118,7 @@ contract DEX { // Calculate the amount of liquidity tokens to mint let liquidity_token = Token::at(settings.liquidity_token); - let total_supply = liquidity_token.total_supply().call(&mut context); + let total_supply = liquidity_token.total_supply().view(&mut context); let mut liquidity: u32 = 0; if (total_supply == 0) { // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? @@ -161,47 +161,46 @@ contract DEX { // most likely need them to be public. // 2. The "transfer from" flows are less efficient as they check authwits. It might be the most efficient // to just make users transfer the liquidity token to the pool in a `BatchCall`. - liquidity_token.transfer_public(liquidity_provider, liquidity).call(&mut context); - - // TODO: + liquidity_token.transfer_public(liquidity_provider, context.this_address(), liquidity, nonce).call(&mut context); // Calculate the amounts to be added to the pool let reserve0 = reserves.reserve0; let reserve1 = reserves.reserve1; - - let balance0 = token0.balance_of_public().call(&mut context); - let balance1 = token1.balance_of_public().call(&mut context); + let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); + let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); // Uniswap burns balance of the pool instead of just burning based on the `liquidity` arg on input. I assume - // the do this to release stranded liquidity (e.g. somebody transferring liquidity tokens by accident). This - // might not make sense here as the liqudity token is a separate contract and hence we need to do a costly - // public call to get the balance. + // they do this to release stranded liquidity (e.g. somebody transferring liquidity tokens directly to the pool + // by accident). This might not make sense here as the liqudity token is a separate contract and hence we need + // to do a costly public call to get the balance. // TODO: Shall we streamline this and just burn based on the `liquidity` arg? Maybe we could even avoid // the transfer and just allow the pool to directly call // `liquidity_token.burn_public(liquidity_provider, liquidity)`. - let liquidity = liquidity_token.balance_of_public().call(&mut context); - - // bool feeOn = _mintFee(_reserve0, _reserve1); - // uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee - // amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution - // amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution - // require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); - // _burn(address(this), liquidity); - // _safeTransfer(_token0, to, amount0); - // _safeTransfer(_token1, to, amount1); - // balance0 = IERC20(_token0).balanceOf(address(this)); - // balance1 = IERC20(_token1).balanceOf(address(this)); - - // _update(balance0, balance1, _reserve0, _reserve1); - // if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date - // emit Burn(msg.sender, amount0, amount1, to); - - - - - assert(amount0 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - assert(amount1 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); + let total_supply = liquidity_token.total_supply().view(&mut context); + + let amount0 = liquidity * balance0 / total_supply; + let amount1 = liquidity * balance1 / total_supply; + // TODO: Nuke these castings. Ideally make Token return integer and not Field. + assert(amount0 as u32 >= amount0Min as u32, "INSUFFICIENT_0_AMOUNT"); + assert(amount1 as u32 >= amount1Min as u32, "INSUFFICIENT_1_AMOUNT"); + + liquidity_token.burn_public(context.this_address(), liquidity, nonce).call(&mut context); + // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do + // if the expectation is that users will mostly want to have private balances. + token0.transfer_public(context.this_address(), liquidity_provider, amount0, nonce).call(&mut context); + token1.transfer_public(context.this_address(), liquidity_provider, amount1, nonce).call(&mut context); + + // We load the balances again directly from the token because Uni v2 does it like this. I assume it's solely + // to protect against reentrancy attacks from the token contracts (since we called transfer_public above). + // This might be to costly as these require a static public call. TODO: Consider lock designs. + balance0 = token0.balance_of_public(context.this_address()).view(&mut context); + balance1 = token1.balance_of_public(context.this_address()).view(&mut context); + + // TODO: Nuke these castings. Ideally make Token return integer and not Field. + let updated_reserves = Reserves { reserve0: balance0 as u32, reserve1: balance1 as u32}; + storage.reserves.write(updated_reserves); } /** From f510b6b1a848d5a9be48f69000692387cf8fa386 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 10:07:23 +0000 Subject: [PATCH 11/67] WIP --- .../contracts/dex_contract/src/main.nr | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 4703ce3560b..30fa3ad9951 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -16,11 +16,11 @@ contract DEX { use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; use dep::token::Token; - // We store the settings of the pool in a struct such that to load it from PublicImmutable asserts only - // a single merkle proof. + // We store the tokens of the pool in a struct such that to load it from PublicImmutable asserts only a single + // merkle proof. // (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). #[derive(Serialize)] - struct Settings { + struct Tokens { token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress, @@ -36,7 +36,7 @@ contract DEX { #[storage] struct Storage { - settings: PublicImmutable, + tokens: PublicImmutable, reserves: PublicMutable, } @@ -52,8 +52,8 @@ contract DEX { // it was deployed correctly. let liquidity_token = AztecAddress::zero(); - let settings = Settings { token0, token1, liquidity_token }; - storage.settings.initialize(settings); + let tokens = Tokens { token0, token1, liquidity_token }; + storage.tokens.initialize(tokens); // We don't need to initialize the reserves as the default in storage is 0. } @@ -71,7 +71,7 @@ contract DEX { assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); let reserves = storage.reserves.read(); - let settings = storage.settings.read(); + let tokens = storage.tokens.read(); // Calculate the amounts to be added to the pool let reserve0 = reserves.reserve0; @@ -117,7 +117,7 @@ contract DEX { // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool // Calculate the amount of liquidity tokens to mint - let liquidity_token = Token::at(settings.liquidity_token); + let liquidity_token = Token::at(tokens.liquidity_token); let total_supply = liquidity_token.total_supply().view(&mut context); let mut liquidity: u32 = 0; if (total_supply == 0) { @@ -145,12 +145,11 @@ contract DEX { assert(nonce == 0, "invalid nonce"); } - let reserves = storage.reserves.read(); - let settings = storage.settings.read(); + let tokens = storage.tokens.read(); - let token0 = Token::at(settings.token0); - let token1 = Token::at(settings.token1); - let liquidity_token = Token::at(settings.liquidity_token); + let token0 = Token::at(tokens.token0); + let token1 = Token::at(tokens.token1); + let liquidity_token = Token::at(tokens.liquidity_token); // We transfer the liquidity tokens from the liquidity provider to this contract. // TODO: I am transferring the liquidity tokens in public here to follow the Uniswap v2 implementation. @@ -164,9 +163,6 @@ contract DEX { liquidity_token.transfer_public(liquidity_provider, context.this_address(), liquidity, nonce).call(&mut context); // Calculate the amounts to be added to the pool - let reserve0 = reserves.reserve0; - let reserve1 = reserves.reserve1; - let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); From 0245ab3a77cdd7ec9ea513f7a465f9bc54e0c19c Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 10:16:12 +0000 Subject: [PATCH 12/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 30fa3ad9951..d30d6373abf 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -118,7 +118,7 @@ contract DEX { // Calculate the amount of liquidity tokens to mint let liquidity_token = Token::at(tokens.liquidity_token); - let total_supply = liquidity_token.total_supply().view(&mut context); + let total_supply = liquidity_token.total_supply().view(&mut context) as u32; // TODO: Nuke the cast here. let mut liquidity: u32 = 0; if (total_supply == 0) { // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? @@ -138,7 +138,7 @@ contract DEX { } #[public] - fn remove_liquidity(liquidity_provider: AztecAddress, liquidity: Field, amount0Min: Field, amount1Min: Field, nonce: Field) { + fn remove_liquidity(liquidity_provider: AztecAddress, liquidity: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { if (!from.eq(context.msg_sender())) { assert_current_call_valid_authwit_public(&mut context, from); } else { @@ -179,8 +179,8 @@ contract DEX { let amount0 = liquidity * balance0 / total_supply; let amount1 = liquidity * balance1 / total_supply; // TODO: Nuke these castings. Ideally make Token return integer and not Field. - assert(amount0 as u32 >= amount0Min as u32, "INSUFFICIENT_0_AMOUNT"); - assert(amount1 as u32 >= amount1Min as u32, "INSUFFICIENT_1_AMOUNT"); + assert(amount0 as u32 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + assert(amount1 as u32 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); liquidity_token.burn_public(context.this_address(), liquidity, nonce).call(&mut context); // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do From 0c0d17ae56b24ca8cd5a6e3cc979c1210b648f1b Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 11:28:17 +0000 Subject: [PATCH 13/67] locks --- .../contracts/dex_contract/src/main.nr | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index d30d6373abf..a8e33f0d820 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -8,7 +8,7 @@ contract DEX { use crate::lib::get_quote; use dep::aztec::{ - prelude::{NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, PublicImmutable, PrivateSet, AztecAddress}, + prelude::{NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, PublicImmutable, PrivateSet, AztecAddress, PublicContext}, encrypted_logs::{encrypted_note_emission::encode_and_encrypt_note_with_keys}, hash::pedersen_hash, keys::getters::get_public_keys, note::constants::MAX_NOTES_PER_PAGE, protocol_types::traits::is_empty, utils::comparison::Comparator @@ -41,6 +41,7 @@ contract DEX { } global MINIMUM_LIQUIDITY: u32 = 1000; + global LOCK_STORAGE_SLOT = 980908; // Arbitrarily chosen lock storage slot. #[public] #[initializer] @@ -62,6 +63,9 @@ contract DEX { // of tokens we ideally want to add. `amount0Min` and `amount1Min` are the minimum amounts we are willing to add. #[public] fn add_liquidity(liquidity_provider: AztecAddress, amount0Desired: u32, amount1Desired: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { + // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. + _lock(&mut context); + if (!liquidity_provider.eq(context.msg_sender())) { assert_current_call_valid_authwit_public(&mut context, liquidity_provider); } else { @@ -135,10 +139,16 @@ contract DEX { // Update the reserves let updated_reserves = Reserves { reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; storage.reserves.write(updated_reserves); + + // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. + _unlock(&mut context); } #[public] fn remove_liquidity(liquidity_provider: AztecAddress, liquidity: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { + // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. + _lock(&mut context); + if (!from.eq(context.msg_sender())) { assert_current_call_valid_authwit_public(&mut context, from); } else { @@ -188,15 +198,18 @@ contract DEX { token0.transfer_public(context.this_address(), liquidity_provider, amount0, nonce).call(&mut context); token1.transfer_public(context.this_address(), liquidity_provider, amount1, nonce).call(&mut context); - // We load the balances again directly from the token because Uni v2 does it like this. I assume it's solely - // to protect against reentrancy attacks from the token contracts (since we called transfer_public above). - // This might be to costly as these require a static public call. TODO: Consider lock designs. + // We load the balances again directly from the token because Uni v2 does it like this. But why do they do + // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird + // token accounting which could be affected by the transfer calls above? balance0 = token0.balance_of_public(context.this_address()).view(&mut context); balance1 = token1.balance_of_public(context.this_address()).view(&mut context); // TODO: Nuke these castings. Ideally make Token return integer and not Field. let updated_reserves = Reserves { reserve0: balance0 as u32, reserve1: balance1 as u32}; storage.reserves.write(updated_reserves); + + // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. + _unlock(&mut context); } /** @@ -209,4 +222,21 @@ contract DEX { let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); context.push_nullifier(nullifier); } + + // After locking and unlocking there will be 1 public data write because we always commit the last change and + // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce + // transient storage. + #[contract_library_method] + fn _lock(context: &mut PublicContext) { + let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); + assert(!already_locked, "Already locked"); + context.storage_write(LOCK_STORAGE_SLOT, true); + } + + #[contract_library_method] + fn _unlock(context: &mut PublicContext) { + let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); + assert(already_locked, "Not locked"); + context.storage_write(LOCK_STORAGE_SLOT, false); + } } From 70d6d42df812d630dce4007b5dc5a8d96d600cdc Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 13:29:44 +0000 Subject: [PATCH 14/67] WIP --- .../contracts/dex_contract/src/lib.nr | 1 + .../contracts/dex_contract/src/main.nr | 24 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr index 0eecc03e980..0fccfde419c 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr @@ -1,4 +1,5 @@ // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset +// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 pub fn get_quote(amountA: u32, reserveA: u32, reserveB: u32) -> u32 { assert(amountA > 0, "INSUFFICIENT_AMOUNT"); assert((reserveA > 0) & (reserveB > 0), "INSUFFICIENT_LIQUIDITY"); diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index a8e33f0d820..7ba37b9b6f1 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -28,10 +28,10 @@ contract DEX { #[derive(Serialize)] struct Reserves { - // TODO: Replace the use of u32 with larger type everywhere in this contract. + // TODO: Replace the use of u64 with larger type everywhere in this contract. // Didn't use U128 here because of https://github.com/AztecProtocol/aztec-packages/issues/8271 and because it might be insufficient. - reserve0: u32, - reserve1: u32, + reserve0: u64, + reserve1: u64, } #[storage] @@ -40,7 +40,7 @@ contract DEX { reserves: PublicMutable, } - global MINIMUM_LIQUIDITY: u32 = 1000; + global MINIMUM_LIQUIDITY: u64 = 1000; global LOCK_STORAGE_SLOT = 980908; // Arbitrarily chosen lock storage slot. #[public] @@ -62,7 +62,7 @@ contract DEX { // Adds liquidity for `liquidity_provider` to the pool. `amount0Desired` and `amount1Desired` are the amounts // of tokens we ideally want to add. `amount0Min` and `amount1Min` are the minimum amounts we are willing to add. #[public] - fn add_liquidity(liquidity_provider: AztecAddress, amount0Desired: u32, amount1Desired: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { + fn add_liquidity(liquidity_provider: AztecAddress, amount0Desired: u64, amount1Desired: u64, amount0Min: u64, amount1Min: u64, nonce: Field) { // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _lock(&mut context); @@ -122,12 +122,12 @@ contract DEX { // Calculate the amount of liquidity tokens to mint let liquidity_token = Token::at(tokens.liquidity_token); - let total_supply = liquidity_token.total_supply().view(&mut context) as u32; // TODO: Nuke the cast here. - let mut liquidity: u32 = 0; + let total_supply = liquidity_token.total_supply().view(&mut context) as u64; // TODO: Nuke the cast here. + let mut liquidity: u64 = 0; if (total_supply == 0) { // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? // TODO: avoid the casts here. Shall we use a method natively working with some integer type? - liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u32; + liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u64; liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); @@ -145,7 +145,7 @@ contract DEX { } #[public] - fn remove_liquidity(liquidity_provider: AztecAddress, liquidity: u32, amount0Min: u32, amount1Min: u32, nonce: Field) { + fn remove_liquidity(liquidity_provider: AztecAddress, liquidity: u64, amount0Min: u64, amount1Min: u64, nonce: Field) { // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _lock(&mut context); @@ -189,8 +189,8 @@ contract DEX { let amount0 = liquidity * balance0 / total_supply; let amount1 = liquidity * balance1 / total_supply; // TODO: Nuke these castings. Ideally make Token return integer and not Field. - assert(amount0 as u32 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - assert(amount1 as u32 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + assert(amount0 as u64 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + assert(amount1 as u64 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); liquidity_token.burn_public(context.this_address(), liquidity, nonce).call(&mut context); // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do @@ -205,7 +205,7 @@ contract DEX { balance1 = token1.balance_of_public(context.this_address()).view(&mut context); // TODO: Nuke these castings. Ideally make Token return integer and not Field. - let updated_reserves = Reserves { reserve0: balance0 as u32, reserve1: balance1 as u32}; + let updated_reserves = Reserves { reserve0: balance0 as u64, reserve1: balance1 as u64}; storage.reserves.write(updated_reserves); // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. From f7916b63c058388b4271190030a81e86cbc226da Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 13:30:25 +0000 Subject: [PATCH 15/67] WIP --- noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr index 0fccfde419c..c7d454fba58 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr @@ -1,6 +1,6 @@ // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset // copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 -pub fn get_quote(amountA: u32, reserveA: u32, reserveB: u32) -> u32 { +pub fn get_quote(amountA: u64, reserveA: u64, reserveB: u64) -> u64 { assert(amountA > 0, "INSUFFICIENT_AMOUNT"); assert((reserveA > 0) & (reserveB > 0), "INSUFFICIENT_LIQUIDITY"); (amountA * reserveB) / reserveA From 998ae1a847a136758f8faef2f1e90039ced0a796 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 14:47:04 +0000 Subject: [PATCH 16/67] WIP --- .../contracts/dex_contract/src/main.nr | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 7ba37b9b6f1..59902e6a6ac 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -59,28 +59,36 @@ contract DEX { // We don't need to initialize the reserves as the default in storage is 0. } - // Adds liquidity for `liquidity_provider` to the pool. `amount0Desired` and `amount1Desired` are the amounts - // of tokens we ideally want to add. `amount0Min` and `amount1Min` are the minimum amounts we are willing to add. + // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). + // `amount0Desired` and `amount1Desired` are the amounts of tokens we ideally want to add. `amount0Min` + // and `amount1Min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` + // is a storage slot commitment used for all 3 partial notes finalized in this tx (token0 refund note, token1 + // refund note, liquidity token partial note). + // (Note: It's fine to use 1 commitment for all 3 partial notes as it's used for transient storage in different + // contracts). It's necessary to prepare the 3 partial notes in a `BatchCall` before calling this function. + // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow + // where the token amount is not known in advance and we don't need to know the amounts for partial notes. #[public] - fn add_liquidity(liquidity_provider: AztecAddress, amount0Desired: u64, amount1Desired: u64, amount0Min: u64, amount1Min: u64, nonce: Field) { + fn add_liquidity(amount0Desired: u64, amount1Desired: u64, amount0Min: u64, amount1Min: u64, transfer_preparer_storage_slot_commitment: Field) { // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _lock(&mut context); - if (!liquidity_provider.eq(context.msg_sender())) { - assert_current_call_valid_authwit_public(&mut context, liquidity_provider); - } else { - assert(nonce == 0, "invalid nonce"); - } + // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared + // the partial notes. assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); let reserves = storage.reserves.read(); let tokens = storage.tokens.read(); - // Calculate the amounts to be added to the pool let reserve0 = reserves.reserve0; let reserve1 = reserves.reserve1; + let token0 = Token::at(tokens.token0); + let token1 = Token::at(tokens.token1); + let liquidity_token = Token::at(tokens.liquidity_token); + + // Calculate the amounts to be added to the pool let mut amount0 = amount0Desired; let mut amount1 = amount1Desired; if ((reserve0 != 0) | (reserve1 != 0)) { @@ -102,13 +110,12 @@ contract DEX { } } - // TODO: Transfer the tokens to this contract. How do we do it? this does not work with authwits as we don't know the amounts before calling this function. - // We could use partial notes for this: + // Now we transfer the tokens to this contract. This is how we'll do this: // 1. A user knows the maximum amount0, amount1 (the desired amounts are max) they want to deposit, // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, - // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transient_storage_slot_randomness), + // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transfer_preparer_storage_slot_commitment), // --> this function will: // 4.1 load both the partial note and the burned amount from transient storage // 4.2 check that actual_amount < burned_amount, @@ -119,9 +126,11 @@ contract DEX { // Cost of this flow: // num calls: 2 private calls to setup the partial notes, 1 public call to add_liquidity, 2 public calls to finalize_transfer_to_public_with_refund // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool + // TODO: Implement `prepare_transfer_to_public_with_refund` and `finalize_transfer_to_public_with_refund` in the Token contract. + token0.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount0).call(&mut context); + token1.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount1).call(&mut context); // Calculate the amount of liquidity tokens to mint - let liquidity_token = Token::at(tokens.liquidity_token); let total_supply = liquidity_token.total_supply().view(&mut context) as u64; // TODO: Nuke the cast here. let mut liquidity: u64 = 0; if (total_supply == 0) { @@ -133,8 +142,8 @@ contract DEX { liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); } assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); - // TODO: Should we mint here to private? It would be more costly. - liquidity_token.mint_public(liquidity_provider, liquidity as Field).call(&mut context); + // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. + liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); // Update the reserves let updated_reserves = Reserves { reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; From ba8afcf1a645d528628ecdf3a26317f33e0c21c2 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 15:08:11 +0000 Subject: [PATCH 17/67] WIP --- .../contracts/dex_contract/src/main.nr | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 59902e6a6ac..f3b8592f85e 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -26,6 +26,7 @@ contract DEX { liquidity_token: AztecAddress, } + // TODO: Nuke these reserves and just do balance_of_public on the tokens instead? #[derive(Serialize)] struct Reserves { // TODO: Replace the use of u64 with larger type everywhere in this contract. @@ -153,16 +154,22 @@ contract DEX { _unlock(&mut context); } + // Removes liquidity from the pool and transfers the tokens to the partial notes prepared on token0 and token1. + // It is necessary that the liquidity provider transfers the liquidity tokens to the contract in a `BatchCall` + // before calling this function. + // Note: Do we consider this to be too dangerous? E.g. users accidentally transferring liquidity tokens to the pool + // in a separate tx and then getting rugged by bots? Just transferring is the most efficient because we already + // know the liquidity amount so we don't need partial notes and it's the most flexible: users can either call + // `Token::transfer_in_public` or `Token::transfer_to_public` and this contract does not care. + // `transfer_preparer_storage_slot_commitment` is a storage slot commitment used for both partial notes finalized + // in this tx (token0 and token1 notes). #[public] - fn remove_liquidity(liquidity_provider: AztecAddress, liquidity: u64, amount0Min: u64, amount1Min: u64, nonce: Field) { + fn remove_liquidity(amount0Min: u64, amount1Min: u64, transfer_preparer_storage_slot_commitment: Field) { // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _lock(&mut context); - if (!from.eq(context.msg_sender())) { - assert_current_call_valid_authwit_public(&mut context, from); - } else { - assert(nonce == 0, "invalid nonce"); - } + // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared + // the partial notes. let tokens = storage.tokens.read(); @@ -170,28 +177,11 @@ contract DEX { let token1 = Token::at(tokens.token1); let liquidity_token = Token::at(tokens.liquidity_token); - // We transfer the liquidity tokens from the liquidity provider to this contract. - // TODO: I am transferring the liquidity tokens in public here to follow the Uniswap v2 implementation. - // --> There are 2 things we need to discuss here: - // 1. This is assuming the dev has the liquidity token in public. I think this is fine as we mint to public - // and I am not really sure if it makes sense to not have liquidity tokens in public as these tokens are - // commonly used in other DeFi (e.g. borrowing against the liquidity token in AAVE) and for that we'll - // most likely need them to be public. - // 2. The "transfer from" flows are less efficient as they check authwits. It might be the most efficient - // to just make users transfer the liquidity token to the pool in a `BatchCall`. - liquidity_token.transfer_public(liquidity_provider, context.this_address(), liquidity, nonce).call(&mut context); - // Calculate the amounts to be added to the pool let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - // Uniswap burns balance of the pool instead of just burning based on the `liquidity` arg on input. I assume - // they do this to release stranded liquidity (e.g. somebody transferring liquidity tokens directly to the pool - // by accident). This might not make sense here as the liqudity token is a separate contract and hence we need - // to do a costly public call to get the balance. - // TODO: Shall we streamline this and just burn based on the `liquidity` arg? Maybe we could even avoid - // the transfer and just allow the pool to directly call - // `liquidity_token.burn_public(liquidity_provider, liquidity)`. + let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); let total_supply = liquidity_token.total_supply().view(&mut context); @@ -204,8 +194,10 @@ contract DEX { liquidity_token.burn_public(context.this_address(), liquidity, nonce).call(&mut context); // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do // if the expectation is that users will mostly want to have private balances. - token0.transfer_public(context.this_address(), liquidity_provider, amount0, nonce).call(&mut context); - token1.transfer_public(context.this_address(), liquidity_provider, amount1, nonce).call(&mut context); + // TODO: Implement `prepare_transfer_to_private` and `finalize_transfer_to_private` in the Token contract + // (it's just on the NFT now). + token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); + token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); // We load the balances again directly from the token because Uni v2 does it like this. But why do they do // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird @@ -221,17 +213,6 @@ contract DEX { _unlock(&mut context); } - /** - * Cancel a private authentication witness. - * @param inner_hash The inner hash of the authwit to cancel. - */ - #[private] - fn cancel_authwit(inner_hash: Field) { - let on_behalf_of = context.msg_sender(); - let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); - context.push_nullifier(nullifier); - } - // After locking and unlocking there will be 1 public data write because we always commit the last change and // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce // transient storage. From 7c395790709cf3e17f3acaf64d106826e2e6aa1e Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 23 Sep 2024 15:10:32 +0000 Subject: [PATCH 18/67] Nuking unnecessary Reserves optimization --- .../contracts/dex_contract/src/main.nr | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index f3b8592f85e..a10e56b40d7 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -26,19 +26,9 @@ contract DEX { liquidity_token: AztecAddress, } - // TODO: Nuke these reserves and just do balance_of_public on the tokens instead? - #[derive(Serialize)] - struct Reserves { - // TODO: Replace the use of u64 with larger type everywhere in this contract. - // Didn't use U128 here because of https://github.com/AztecProtocol/aztec-packages/issues/8271 and because it might be insufficient. - reserve0: u64, - reserve1: u64, - } - #[storage] struct Storage { tokens: PublicImmutable, - reserves: PublicMutable, } global MINIMUM_LIQUIDITY: u64 = 1000; @@ -56,8 +46,6 @@ contract DEX { let tokens = Tokens { token0, token1, liquidity_token }; storage.tokens.initialize(tokens); - - // We don't need to initialize the reserves as the default in storage is 0. } // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). @@ -79,14 +67,15 @@ contract DEX { assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); - let reserves = storage.reserves.read(); let tokens = storage.tokens.read(); - let reserve0 = reserves.reserve0; - let reserve1 = reserves.reserve1; - let token0 = Token::at(tokens.token0); let token1 = Token::at(tokens.token1); + + + let reserve0 = token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. + let reserve1 = token1.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. + let liquidity_token = Token::at(tokens.liquidity_token); // Calculate the amounts to be added to the pool @@ -146,10 +135,6 @@ contract DEX { // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); - // Update the reserves - let updated_reserves = Reserves { reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; - storage.reserves.write(updated_reserves); - // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _unlock(&mut context); } @@ -205,10 +190,6 @@ contract DEX { balance0 = token0.balance_of_public(context.this_address()).view(&mut context); balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - // TODO: Nuke these castings. Ideally make Token return integer and not Field. - let updated_reserves = Reserves { reserve0: balance0 as u64, reserve1: balance1 as u64}; - storage.reserves.write(updated_reserves); - // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _unlock(&mut context); } From 59348eddecb07e5577d8f8c21abbc3af03e4dad5 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 07:45:20 +0000 Subject: [PATCH 19/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index a10e56b40d7..829d7833262 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -176,7 +176,7 @@ contract DEX { assert(amount0 as u64 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); assert(amount1 as u64 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); - liquidity_token.burn_public(context.this_address(), liquidity, nonce).call(&mut context); + liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do // if the expectation is that users will mostly want to have private balances. // TODO: Implement `prepare_transfer_to_private` and `finalize_transfer_to_private` in the Token contract @@ -184,12 +184,6 @@ contract DEX { token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); - // We load the balances again directly from the token because Uni v2 does it like this. But why do they do - // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird - // token accounting which could be affected by the transfer calls above? - balance0 = token0.balance_of_public(context.this_address()).view(&mut context); - balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _unlock(&mut context); } From 98c212e14f702f3d6d3ed2ab335872db53991093 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 08:52:01 +0000 Subject: [PATCH 20/67] WIP --- .../contracts/dex_contract/src/lib.nr | 11 +++ .../contracts/dex_contract/src/main.nr | 91 +++++++++++++------ 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr index c7d454fba58..394819a41d2 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr @@ -5,3 +5,14 @@ pub fn get_quote(amountA: u64, reserveA: u64, reserveB: u64) -> u64 { assert((reserveA > 0) & (reserveB > 0), "INSUFFICIENT_LIQUIDITY"); (amountA * reserveB) / reserveA } + +// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset +// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L43 +pub fn get_amount_out(amount_in: u64, reserve_in: u64, reserve_out: u64) -> u64 { + assert(amount_in > 0, "INSUFFICIENT_INPUT_AMOUNT"); + assert((reserve_in > 0) & (reserve_out > 0), "INSUFFICIENT_LIQUIDITY"); + let amount_in_with_fee = amount_in * 997; + let numerator = amount_in_with_fee * reserve_out; + let denominator = reserve_in * 1000 + amount_in_with_fee; + numerator / denominator +} \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 829d7833262..15d486bb43c 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -5,30 +5,27 @@ use dep::aztec::macros::aztec; // A Noir implementation of simplified Uniswap v2 pool. #[aztec] contract DEX { - use crate::lib::get_quote; - - use dep::aztec::{ - prelude::{NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, PublicImmutable, PrivateSet, AztecAddress, PublicContext}, - encrypted_logs::{encrypted_note_emission::encode_and_encrypt_note_with_keys}, - hash::pedersen_hash, keys::getters::get_public_keys, note::constants::MAX_NOTES_PER_PAGE, - protocol_types::traits::is_empty, utils::comparison::Comparator - }; - use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; + use crate::lib::{get_quote, get_amount_out}; + use dep::aztec::prelude::{AztecAddress, PublicImmutable, PublicContext}; use dep::token::Token; - // We store the tokens of the pool in a struct such that to load it from PublicImmutable asserts only a single - // merkle proof. + // We store the tokens of the pool and reserves in a struct such that to load it from PublicImmutable asserts only + // a single merkle proof. // (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). + // Note: We store the reserves instead of just doing `token{0,1}.balance_of_public(dex_address)` so that we can + // make the user send the `token_in` into the pool before the swap. #[derive(Serialize)] - struct Tokens { + struct State { token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress, + reserve0: u64, + reserve1: u64, } #[storage] struct Storage { - tokens: PublicImmutable, + state: PublicImmutable, } global MINIMUM_LIQUIDITY: u64 = 1000; @@ -44,8 +41,8 @@ contract DEX { // it was deployed correctly. let liquidity_token = AztecAddress::zero(); - let tokens = Tokens { token0, token1, liquidity_token }; - storage.tokens.initialize(tokens); + let state = State { token0, token1, liquidity_token, reserve0: 0, reserve1: 0 }; + storage.state.initialize(state); } // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). @@ -67,16 +64,14 @@ contract DEX { assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); - let tokens = storage.tokens.read(); + let state = storage.state.read(); - let token0 = Token::at(tokens.token0); - let token1 = Token::at(tokens.token1); + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + let reserve0 = state.reserve0; + let reserve1 = state.reserve1; - - let reserve0 = token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. - let reserve1 = token1.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. - - let liquidity_token = Token::at(tokens.liquidity_token); + let liquidity_token = Token::at(state.liquidity_token); // Calculate the amounts to be added to the pool let mut amount0 = amount0Desired; @@ -135,6 +130,12 @@ contract DEX { // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); + // Update the reserves + // (Note that we should not pay for token0, token1 and liquidity_token writes because kernel should squash them + // (once the optimization is done)). + let updated_state = State { token0: state.token0, token1: state.token1, liquidity_token: state.liquidity_token, reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; + storage.state.write(updated_state); + // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _unlock(&mut context); } @@ -156,11 +157,11 @@ contract DEX { // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared // the partial notes. - let tokens = storage.tokens.read(); + let state = storage.state.read(); - let token0 = Token::at(tokens.token0); - let token1 = Token::at(tokens.token1); - let liquidity_token = Token::at(tokens.liquidity_token); + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + let liquidity_token = Token::at(state.liquidity_token); // Calculate the amounts to be added to the pool let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); @@ -184,10 +185,46 @@ contract DEX { token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); + // We load the balances again directly from the tokens because Uni v2 does it like this. But why do they do + // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird + // token accounting which could be affected by the transfer calls above? + balance0 = token0.balance_of_public(context.this_address()).view(&mut context); + balance1 = token1.balance_of_public(context.this_address()).view(&mut context); + + // Update the reserves + let updated_state = State { token0: state.token0, token1: state.token1, liquidity_token: state.liquidity_token, reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; + storage.state.write(updated_state); + // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _unlock(&mut context); } + #[public] + fn swap_exact_tokens_for_tokens( + amount_in: u64, + amount_out_min: u64, + from_0_to_1: bool, + ) { + let state = storage.state.read(); + + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + + let reserve0 = token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. + let reserve1 = token1.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. + + let (reserve_in, reserve_out) = if from_0_to_1 { (reserve0, reserve1) } else { (reserve1, reserve0) }; + + let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); + assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); + // How do we transfer to the pool here? One way is to track the reserves and make the user transfer to the pool + // We could do public_transfer_from here but it would force user to unshield and we would have to pay + // for public authwit. + + // TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]); + // _swap(amounts, path, to); + } + // After locking and unlocking there will be 1 public data write because we always commit the last change and // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce // transient storage. From 766fef7e0ce50e88e826681064f10f9f631f8c12 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 09:15:00 +0000 Subject: [PATCH 21/67] WIP --- .../contracts/dex_contract/src/main.nr | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 15d486bb43c..882bf430c1c 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -56,7 +56,6 @@ contract DEX { // where the token amount is not known in advance and we don't need to know the amounts for partial notes. #[public] fn add_liquidity(amount0Desired: u64, amount1Desired: u64, amount0Min: u64, amount1Min: u64, transfer_preparer_storage_slot_commitment: Field) { - // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _lock(&mut context); // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared @@ -136,7 +135,6 @@ contract DEX { let updated_state = State { token0: state.token0, token1: state.token1, liquidity_token: state.liquidity_token, reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; storage.state.write(updated_state); - // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _unlock(&mut context); } @@ -151,7 +149,6 @@ contract DEX { // in this tx (token0 and token1 notes). #[public] fn remove_liquidity(amount0Min: u64, amount1Min: u64, transfer_preparer_storage_slot_commitment: Field) { - // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _lock(&mut context); // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared @@ -195,39 +192,45 @@ contract DEX { let updated_state = State { token0: state.token0, token1: state.token1, liquidity_token: state.liquidity_token, reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; storage.state.write(updated_state); - // TODO: It would be quite nice to have a reentrancy-guard macro as this will be quite common. _unlock(&mut context); } + // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates + // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is + // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. #[public] fn swap_exact_tokens_for_tokens( amount_in: u64, amount_out_min: u64, from_0_to_1: bool, ) { - let state = storage.state.read(); + _lock(&mut context); - let token0 = Token::at(state.token0); - let token1 = Token::at(state.token1); + let state = storage.state.read(); - let reserve0 = token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. - let reserve1 = token1.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Nuke the cast here. + let (token_address_in, token_address_out, reserve_in, reserve_out) = if from_0_to_1 { + (state.token0, state.token1, state.reserve0, state.reserve1) + } else { + (state.token1, state.token0, state.reserve1, state.reserve0) + }; + let token_in = Token::at(token_address_in); + let token_out = Token::at(token_address_out); - let (reserve_in, reserve_out) = if from_0_to_1 { (reserve0, reserve1) } else { (reserve1, reserve0) }; + let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - // How do we transfer to the pool here? One way is to track the reserves and make the user transfer to the pool - // We could do public_transfer_from here but it would force user to unshield and we would have to pay - // for public authwit. - // TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]); - // _swap(amounts, path, to); + // TODO: _swap(amounts, path, to); + + _unlock(&mut context); } // After locking and unlocking there will be 1 public data write because we always commit the last change and // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce // transient storage. + // TODO: It would be quite nice to have a reentrancy-guard macro as using locks will be quite common. #[contract_library_method] fn _lock(context: &mut PublicContext) { let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); From 0d08d418cf6bbd6f842ac3e6502b7628f29f869b Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 09:15:37 +0000 Subject: [PATCH 22/67] cleanup --- .../contracts/dex_contract/notes | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 noir-projects/noir-contracts/contracts/dex_contract/notes diff --git a/noir-projects/noir-contracts/contracts/dex_contract/notes b/noir-projects/noir-contracts/contracts/dex_contract/notes deleted file mode 100644 index 343e50dc480..00000000000 --- a/noir-projects/noir-contracts/contracts/dex_contract/notes +++ /dev/null @@ -1,35 +0,0 @@ -univ2 dex - -public fn join pool - // maybe could keep the owner private, idk - -pool has public balances - - -swapper -> - pool: private fn swap(amount_in, min_amount_out) - token_in.unshield(from: swapper, to: pool, amount: amount_in) // transfer_from_to_public - authwit validation signed by the swapper (approval) - burns swapper notes - creates swapper change note - self.enqueue_public_call(increase_public_balance, pool, amount_in) - - token_out.prepare_partial_balance_note(recipient: swapper) -> (id, log) - partial balance note = [balance_slot, recipient, randomness] // missing amount - stored in public storage - self.enqueue_public_call(store_partial_note(partial_note)) - sstore - - pxe.once_this_tx_is_included_in_a_block check for a note with amount whatever... - - self.enqueue_public_call(settle_swap(amount_in, min_amount_out, partial note id for token out)) - read balances - compute amount out given amont in and swap fee (swap fee is public) // note that the pool has already received the tokens! - check min amount out - - emit event - - token_out.transfer_into_partial_note(from: pool, amount: amount out, partial note id) - reduce pool balance - assert partial note id exists (transient storage) - finalize and commit partial note with amount out \ No newline at end of file From 4edb48a88ba6cf052352bfbfbc04869c514842ab Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 09:17:46 +0000 Subject: [PATCH 23/67] WIP --- .../contracts/dex_contract/src/main.nr | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 882bf430c1c..e316ef4783b 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -214,7 +214,6 @@ contract DEX { (state.token1, state.token0, state.reserve1, state.reserve0) }; let token_in = Token::at(token_address_in); - let token_out = Token::at(token_address_out); let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); @@ -227,6 +226,17 @@ contract DEX { _unlock(&mut context); } + // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates + // whether we are swapping `token0` for `token1` or vice versa. + #[public] + fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool) { + _lock(&mut context); + + // TODO + + _unlock(&mut context); + } + // After locking and unlocking there will be 1 public data write because we always commit the last change and // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce // transient storage. From cb6bf20e52b5aae60703eeebd09df657887e4781 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 09:34:38 +0000 Subject: [PATCH 24/67] WIP --- noir-projects/noir-contracts/Nargo.toml | 1 + .../noir-contracts/contracts/dex_contract/Nargo.toml | 2 -- .../noir-contracts/contracts/dex_contract/src/main.nr | 7 ++++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index df510e99432..18ba10820a7 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "contracts/amm_contract", "contracts/app_subscription_contract", "contracts/auth_contract", "contracts/auth_registry_contract", diff --git a/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml index 0229d45cc72..16fabd6cb86 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml @@ -6,6 +6,4 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } -compressed_string = { path = "../../../aztec-nr/compressed-string" } -authwit = { path = "../../../aztec-nr/authwit" } token = { path = "../token_contract" } diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index e316ef4783b..a7f8688c5e7 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -6,7 +6,12 @@ use dep::aztec::macros::aztec; #[aztec] contract DEX { use crate::lib::{get_quote, get_amount_out}; - use dep::aztec::prelude::{AztecAddress, PublicImmutable, PublicContext}; + use dep::aztec::{ + macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}}, + prelude::{AztecAddress, PublicImmutable, PublicContext}, + protocol_types::traits::Serialize, + }; + use std::meta::derive; use dep::token::Token; // We store the tokens of the pool and reserves in a struct such that to load it from PublicImmutable asserts only From 9279f0b29d29eff8873845db56a469043b3ad267 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 09:35:58 +0000 Subject: [PATCH 25/67] fmt --- .../contracts/dex_contract/src/lib.nr | 2 +- .../contracts/dex_contract/src/main.nr | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr index 394819a41d2..3a62d55a0c3 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr @@ -15,4 +15,4 @@ pub fn get_amount_out(amount_in: u64, reserve_in: u64, reserve_out: u64) -> u64 let numerator = amount_in_with_fee * reserve_out; let denominator = reserve_in * 1000 + amount_in_with_fee; numerator / denominator -} \ No newline at end of file +} diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index a7f8688c5e7..cd89862f9d2 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -8,8 +8,7 @@ contract DEX { use crate::lib::{get_quote, get_amount_out}; use dep::aztec::{ macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}}, - prelude::{AztecAddress, PublicImmutable, PublicContext}, - protocol_types::traits::Serialize, + prelude::{AztecAddress, PublicImmutable, PublicContext}, protocol_types::traits::Serialize }; use std::meta::derive; use dep::token::Token; @@ -60,7 +59,13 @@ contract DEX { // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow // where the token amount is not known in advance and we don't need to know the amounts for partial notes. #[public] - fn add_liquidity(amount0Desired: u64, amount1Desired: u64, amount0Min: u64, amount1Min: u64, transfer_preparer_storage_slot_commitment: Field) { + fn add_liquidity( + amount0Desired: u64, + amount1Desired: u64, + amount0Min: u64, + amount1Min: u64, + transfer_preparer_storage_slot_commitment: Field + ) { _lock(&mut context); // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared @@ -137,7 +142,13 @@ contract DEX { // Update the reserves // (Note that we should not pay for token0, token1 and liquidity_token writes because kernel should squash them // (once the optimization is done)). - let updated_state = State { token0: state.token0, token1: state.token1, liquidity_token: state.liquidity_token, reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; + let updated_state = State { + token0: state.token0, + token1: state.token1, + liquidity_token: state.liquidity_token, + reserve0: reserve0 + amount0, + reserve1: reserve1 + amount1 + }; storage.state.write(updated_state); _unlock(&mut context); @@ -153,7 +164,11 @@ contract DEX { // `transfer_preparer_storage_slot_commitment` is a storage slot commitment used for both partial notes finalized // in this tx (token0 and token1 notes). #[public] - fn remove_liquidity(amount0Min: u64, amount1Min: u64, transfer_preparer_storage_slot_commitment: Field) { + fn remove_liquidity( + amount0Min: u64, + amount1Min: u64, + transfer_preparer_storage_slot_commitment: Field + ) { _lock(&mut context); // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared @@ -169,7 +184,6 @@ contract DEX { let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); let total_supply = liquidity_token.total_supply().view(&mut context); @@ -194,7 +208,13 @@ contract DEX { balance1 = token1.balance_of_public(context.this_address()).view(&mut context); // Update the reserves - let updated_state = State { token0: state.token0, token1: state.token1, liquidity_token: state.liquidity_token, reserve0: reserve0 + amount0, reserve1: reserve1 + amount1 }; + let updated_state = State { + token0: state.token0, + token1: state.token1, + liquidity_token: state.liquidity_token, + reserve0: reserve0 + amount0, + reserve1: reserve1 + amount1 + }; storage.state.write(updated_state); _unlock(&mut context); @@ -204,11 +224,7 @@ contract DEX { // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. #[public] - fn swap_exact_tokens_for_tokens( - amount_in: u64, - amount_out_min: u64, - from_0_to_1: bool, - ) { + fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool) { _lock(&mut context); let state = storage.state.read(); From f2d96775e7d3ff1a3c5b511a7481df07400244b2 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 09:37:35 +0000 Subject: [PATCH 26/67] commenting out code --- .../contracts/dex_contract/src/main.nr | 452 +++++++++--------- 1 file changed, 226 insertions(+), 226 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index cd89862f9d2..6bac3a76610 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -49,230 +49,230 @@ contract DEX { storage.state.initialize(state); } - // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). - // `amount0Desired` and `amount1Desired` are the amounts of tokens we ideally want to add. `amount0Min` - // and `amount1Min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` - // is a storage slot commitment used for all 3 partial notes finalized in this tx (token0 refund note, token1 - // refund note, liquidity token partial note). - // (Note: It's fine to use 1 commitment for all 3 partial notes as it's used for transient storage in different - // contracts). It's necessary to prepare the 3 partial notes in a `BatchCall` before calling this function. - // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow - // where the token amount is not known in advance and we don't need to know the amounts for partial notes. - #[public] - fn add_liquidity( - amount0Desired: u64, - amount1Desired: u64, - amount0Min: u64, - amount1Min: u64, - transfer_preparer_storage_slot_commitment: Field - ) { - _lock(&mut context); - - // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared - // the partial notes. - - assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); - - let state = storage.state.read(); - - let token0 = Token::at(state.token0); - let token1 = Token::at(state.token1); - let reserve0 = state.reserve0; - let reserve1 = state.reserve1; - - let liquidity_token = Token::at(state.liquidity_token); - - // Calculate the amounts to be added to the pool - let mut amount0 = amount0Desired; - let mut amount1 = amount1Desired; - if ((reserve0 != 0) | (reserve1 != 0)) { - // First calculate the optimal amount of token1 based on the desired amount of token0. - let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); - if (amount1Optimal <= amount1Desired) { - // Revert if the optimal amount of token1 is less than the desired amount of token1. - assert(amount1Optimal >= amount1Min, "INSUFFICIENT_1_AMOUNT"); - amount0 = amount0Desired; - amount1 = amount1Optimal; - } else { - // We got more amount of token1 than desired so we try repeating the process but this time by quoting - // based on token1. - let amount0Optimal = get_quote(amount1Desired, reserve1, reserve0); - assert(amount0Optimal <= amount0Desired); - assert(amount0Optimal >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - amount0 = amount0Optimal; - amount1 = amount1Desired; - } - } - - // Now we transfer the tokens to this contract. This is how we'll do this: - // 1. A user knows the maximum amount0, amount1 (the desired amounts are max) they want to deposit, - // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, - // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! - // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, - // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transfer_preparer_storage_slot_commitment), - // --> this function will: - // 4.1 load both the partial note and the burned amount from transient storage - // 4.2 check that actual_amount < burned_amount, - // 4.3 publicly mint the actual_amount to msg_sender (the DEX in our case), - // 4.4 finalize the partial note amount with `burned_amount - actual_amount` and emit the note. - // - // Note 1: This is essentially a 1 person alternative to the fee refund flow where we have a user and an FPC. - // Cost of this flow: - // num calls: 2 private calls to setup the partial notes, 1 public call to add_liquidity, 2 public calls to finalize_transfer_to_public_with_refund - // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool - // TODO: Implement `prepare_transfer_to_public_with_refund` and `finalize_transfer_to_public_with_refund` in the Token contract. - token0.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount0).call(&mut context); - token1.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount1).call(&mut context); - - // Calculate the amount of liquidity tokens to mint - let total_supply = liquidity_token.total_supply().view(&mut context) as u64; // TODO: Nuke the cast here. - let mut liquidity: u64 = 0; - if (total_supply == 0) { - // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? - // TODO: avoid the casts here. Shall we use a method natively working with some integer type? - liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u64; - liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens - } else { - liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); - } - assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); - // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. - liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); - - // Update the reserves - // (Note that we should not pay for token0, token1 and liquidity_token writes because kernel should squash them - // (once the optimization is done)). - let updated_state = State { - token0: state.token0, - token1: state.token1, - liquidity_token: state.liquidity_token, - reserve0: reserve0 + amount0, - reserve1: reserve1 + amount1 - }; - storage.state.write(updated_state); - - _unlock(&mut context); - } - - // Removes liquidity from the pool and transfers the tokens to the partial notes prepared on token0 and token1. - // It is necessary that the liquidity provider transfers the liquidity tokens to the contract in a `BatchCall` - // before calling this function. - // Note: Do we consider this to be too dangerous? E.g. users accidentally transferring liquidity tokens to the pool - // in a separate tx and then getting rugged by bots? Just transferring is the most efficient because we already - // know the liquidity amount so we don't need partial notes and it's the most flexible: users can either call - // `Token::transfer_in_public` or `Token::transfer_to_public` and this contract does not care. - // `transfer_preparer_storage_slot_commitment` is a storage slot commitment used for both partial notes finalized - // in this tx (token0 and token1 notes). - #[public] - fn remove_liquidity( - amount0Min: u64, - amount1Min: u64, - transfer_preparer_storage_slot_commitment: Field - ) { - _lock(&mut context); - - // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared - // the partial notes. - - let state = storage.state.read(); - - let token0 = Token::at(state.token0); - let token1 = Token::at(state.token1); - let liquidity_token = Token::at(state.liquidity_token); - - // Calculate the amounts to be added to the pool - let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); - let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - - let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); - let total_supply = liquidity_token.total_supply().view(&mut context); - - let amount0 = liquidity * balance0 / total_supply; - let amount1 = liquidity * balance1 / total_supply; - // TODO: Nuke these castings. Ideally make Token return integer and not Field. - assert(amount0 as u64 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - assert(amount1 as u64 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); - - liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); - // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do - // if the expectation is that users will mostly want to have private balances. - // TODO: Implement `prepare_transfer_to_private` and `finalize_transfer_to_private` in the Token contract - // (it's just on the NFT now). - token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); - token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); - - // We load the balances again directly from the tokens because Uni v2 does it like this. But why do they do - // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird - // token accounting which could be affected by the transfer calls above? - balance0 = token0.balance_of_public(context.this_address()).view(&mut context); - balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - - // Update the reserves - let updated_state = State { - token0: state.token0, - token1: state.token1, - liquidity_token: state.liquidity_token, - reserve0: reserve0 + amount0, - reserve1: reserve1 + amount1 - }; - storage.state.write(updated_state); - - _unlock(&mut context); - } - - // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates - // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is - // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. - #[public] - fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool) { - _lock(&mut context); - - let state = storage.state.read(); - - let (token_address_in, token_address_out, reserve_in, reserve_out) = if from_0_to_1 { - (state.token0, state.token1, state.reserve0, state.reserve1) - } else { - (state.token1, state.token0, state.reserve1, state.reserve0) - }; - let token_in = Token::at(token_address_in); - - let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; - assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); - - let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); - assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - - // TODO: _swap(amounts, path, to); - - _unlock(&mut context); - } - - // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates - // whether we are swapping `token0` for `token1` or vice versa. - #[public] - fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool) { - _lock(&mut context); - - // TODO - - _unlock(&mut context); - } - - // After locking and unlocking there will be 1 public data write because we always commit the last change and - // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce - // transient storage. - // TODO: It would be quite nice to have a reentrancy-guard macro as using locks will be quite common. - #[contract_library_method] - fn _lock(context: &mut PublicContext) { - let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); - assert(!already_locked, "Already locked"); - context.storage_write(LOCK_STORAGE_SLOT, true); - } - - #[contract_library_method] - fn _unlock(context: &mut PublicContext) { - let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); - assert(already_locked, "Not locked"); - context.storage_write(LOCK_STORAGE_SLOT, false); - } + // // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). + // // `amount0Desired` and `amount1Desired` are the amounts of tokens we ideally want to add. `amount0Min` + // // and `amount1Min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` + // // is a storage slot commitment used for all 3 partial notes finalized in this tx (token0 refund note, token1 + // // refund note, liquidity token partial note). + // // (Note: It's fine to use 1 commitment for all 3 partial notes as it's used for transient storage in different + // // contracts). It's necessary to prepare the 3 partial notes in a `BatchCall` before calling this function. + // // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow + // // where the token amount is not known in advance and we don't need to know the amounts for partial notes. + // #[public] + // fn add_liquidity( + // amount0Desired: u64, + // amount1Desired: u64, + // amount0Min: u64, + // amount1Min: u64, + // transfer_preparer_storage_slot_commitment: Field + // ) { + // _lock(&mut context); + + // // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared + // // the partial notes. + + // assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); + + // let state = storage.state.read(); + + // let token0 = Token::at(state.token0); + // let token1 = Token::at(state.token1); + // let reserve0 = state.reserve0; + // let reserve1 = state.reserve1; + + // let liquidity_token = Token::at(state.liquidity_token); + + // // Calculate the amounts to be added to the pool + // let mut amount0 = amount0Desired; + // let mut amount1 = amount1Desired; + // if ((reserve0 != 0) | (reserve1 != 0)) { + // // First calculate the optimal amount of token1 based on the desired amount of token0. + // let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); + // if (amount1Optimal <= amount1Desired) { + // // Revert if the optimal amount of token1 is less than the desired amount of token1. + // assert(amount1Optimal >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + // amount0 = amount0Desired; + // amount1 = amount1Optimal; + // } else { + // // We got more amount of token1 than desired so we try repeating the process but this time by quoting + // // based on token1. + // let amount0Optimal = get_quote(amount1Desired, reserve1, reserve0); + // assert(amount0Optimal <= amount0Desired); + // assert(amount0Optimal >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + // amount0 = amount0Optimal; + // amount1 = amount1Desired; + // } + // } + + // // Now we transfer the tokens to this contract. This is how we'll do this: + // // 1. A user knows the maximum amount0, amount1 (the desired amounts are max) they want to deposit, + // // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, + // // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! + // // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, + // // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transfer_preparer_storage_slot_commitment), + // // --> this function will: + // // 4.1 load both the partial note and the burned amount from transient storage + // // 4.2 check that actual_amount < burned_amount, + // // 4.3 publicly mint the actual_amount to msg_sender (the DEX in our case), + // // 4.4 finalize the partial note amount with `burned_amount - actual_amount` and emit the note. + // // + // // Note 1: This is essentially a 1 person alternative to the fee refund flow where we have a user and an FPC. + // // Cost of this flow: + // // num calls: 2 private calls to setup the partial notes, 1 public call to add_liquidity, 2 public calls to finalize_transfer_to_public_with_refund + // // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool + // // TODO: Implement `prepare_transfer_to_public_with_refund` and `finalize_transfer_to_public_with_refund` in the Token contract. + // token0.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount0).call(&mut context); + // token1.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount1).call(&mut context); + + // // Calculate the amount of liquidity tokens to mint + // let total_supply = liquidity_token.total_supply().view(&mut context) as u64; // TODO: Nuke the cast here. + // let mut liquidity: u64 = 0; + // if (total_supply == 0) { + // // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? + // // TODO: avoid the casts here. Shall we use a method natively working with some integer type? + // liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u64; + // liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens + // } else { + // liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); + // } + // assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); + // // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. + // liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); + + // // Update the reserves + // // (Note that we should not pay for token0, token1 and liquidity_token writes because kernel should squash them + // // (once the optimization is done)). + // let updated_state = State { + // token0: state.token0, + // token1: state.token1, + // liquidity_token: state.liquidity_token, + // reserve0: reserve0 + amount0, + // reserve1: reserve1 + amount1 + // }; + // storage.state.write(updated_state); + + // _unlock(&mut context); + // } + + // // Removes liquidity from the pool and transfers the tokens to the partial notes prepared on token0 and token1. + // // It is necessary that the liquidity provider transfers the liquidity tokens to the contract in a `BatchCall` + // // before calling this function. + // // Note: Do we consider this to be too dangerous? E.g. users accidentally transferring liquidity tokens to the pool + // // in a separate tx and then getting rugged by bots? Just transferring is the most efficient because we already + // // know the liquidity amount so we don't need partial notes and it's the most flexible: users can either call + // // `Token::transfer_in_public` or `Token::transfer_to_public` and this contract does not care. + // // `transfer_preparer_storage_slot_commitment` is a storage slot commitment used for both partial notes finalized + // // in this tx (token0 and token1 notes). + // #[public] + // fn remove_liquidity( + // amount0Min: u64, + // amount1Min: u64, + // transfer_preparer_storage_slot_commitment: Field + // ) { + // _lock(&mut context); + + // // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared + // // the partial notes. + + // let state = storage.state.read(); + + // let token0 = Token::at(state.token0); + // let token1 = Token::at(state.token1); + // let liquidity_token = Token::at(state.liquidity_token); + + // // Calculate the amounts to be added to the pool + // let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); + // let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); + + // let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); + // let total_supply = liquidity_token.total_supply().view(&mut context); + + // let amount0 = liquidity * balance0 / total_supply; + // let amount1 = liquidity * balance1 / total_supply; + // // TODO: Nuke these castings. Ideally make Token return integer and not Field. + // assert(amount0 as u64 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + // assert(amount1 as u64 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + + // liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); + // // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do + // // if the expectation is that users will mostly want to have private balances. + // // TODO: Implement `prepare_transfer_to_private` and `finalize_transfer_to_private` in the Token contract + // // (it's just on the NFT now). + // token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); + // token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); + + // // We load the balances again directly from the tokens because Uni v2 does it like this. But why do they do + // // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird + // // token accounting which could be affected by the transfer calls above? + // balance0 = token0.balance_of_public(context.this_address()).view(&mut context); + // balance1 = token1.balance_of_public(context.this_address()).view(&mut context); + + // // Update the reserves + // let updated_state = State { + // token0: state.token0, + // token1: state.token1, + // liquidity_token: state.liquidity_token, + // reserve0: reserve0 + amount0, + // reserve1: reserve1 + amount1 + // }; + // storage.state.write(updated_state); + + // _unlock(&mut context); + // } + + // // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates + // // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is + // // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. + // #[public] + // fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool) { + // _lock(&mut context); + + // let state = storage.state.read(); + + // let (token_address_in, token_address_out, reserve_in, reserve_out) = if from_0_to_1 { + // (state.token0, state.token1, state.reserve0, state.reserve1) + // } else { + // (state.token1, state.token0, state.reserve1, state.reserve0) + // }; + // let token_in = Token::at(token_address_in); + + // let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + // assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); + + // let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); + // assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); + + // // TODO: _swap(amounts, path, to); + + // _unlock(&mut context); + // } + + // // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates + // // whether we are swapping `token0` for `token1` or vice versa. + // #[public] + // fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool) { + // _lock(&mut context); + + // // TODO + + // _unlock(&mut context); + // } + + // // After locking and unlocking there will be 1 public data write because we always commit the last change and + // // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce + // // transient storage. + // // TODO: It would be quite nice to have a reentrancy-guard macro as using locks will be quite common. + // #[contract_library_method] + // fn _lock(context: &mut PublicContext) { + // let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); + // assert(!already_locked, "Already locked"); + // context.storage_write(LOCK_STORAGE_SLOT, true); + // } + + // #[contract_library_method] + // fn _unlock(context: &mut PublicContext) { + // let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); + // assert(already_locked, "Not locked"); + // context.storage_write(LOCK_STORAGE_SLOT, false); + // } } From 0e74791a35a2cd50abd25ed9cf22057309c35608 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 09:47:35 +0000 Subject: [PATCH 27/67] Revert "commenting out code" This reverts commit e81e65bd3fb999bf4461e6dd13d36a59844eafcf. --- .../contracts/dex_contract/src/main.nr | 452 +++++++++--------- 1 file changed, 226 insertions(+), 226 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 6bac3a76610..cd89862f9d2 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -49,230 +49,230 @@ contract DEX { storage.state.initialize(state); } - // // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). - // // `amount0Desired` and `amount1Desired` are the amounts of tokens we ideally want to add. `amount0Min` - // // and `amount1Min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` - // // is a storage slot commitment used for all 3 partial notes finalized in this tx (token0 refund note, token1 - // // refund note, liquidity token partial note). - // // (Note: It's fine to use 1 commitment for all 3 partial notes as it's used for transient storage in different - // // contracts). It's necessary to prepare the 3 partial notes in a `BatchCall` before calling this function. - // // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow - // // where the token amount is not known in advance and we don't need to know the amounts for partial notes. - // #[public] - // fn add_liquidity( - // amount0Desired: u64, - // amount1Desired: u64, - // amount0Min: u64, - // amount1Min: u64, - // transfer_preparer_storage_slot_commitment: Field - // ) { - // _lock(&mut context); - - // // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared - // // the partial notes. - - // assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); - - // let state = storage.state.read(); - - // let token0 = Token::at(state.token0); - // let token1 = Token::at(state.token1); - // let reserve0 = state.reserve0; - // let reserve1 = state.reserve1; - - // let liquidity_token = Token::at(state.liquidity_token); - - // // Calculate the amounts to be added to the pool - // let mut amount0 = amount0Desired; - // let mut amount1 = amount1Desired; - // if ((reserve0 != 0) | (reserve1 != 0)) { - // // First calculate the optimal amount of token1 based on the desired amount of token0. - // let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); - // if (amount1Optimal <= amount1Desired) { - // // Revert if the optimal amount of token1 is less than the desired amount of token1. - // assert(amount1Optimal >= amount1Min, "INSUFFICIENT_1_AMOUNT"); - // amount0 = amount0Desired; - // amount1 = amount1Optimal; - // } else { - // // We got more amount of token1 than desired so we try repeating the process but this time by quoting - // // based on token1. - // let amount0Optimal = get_quote(amount1Desired, reserve1, reserve0); - // assert(amount0Optimal <= amount0Desired); - // assert(amount0Optimal >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - // amount0 = amount0Optimal; - // amount1 = amount1Desired; - // } - // } - - // // Now we transfer the tokens to this contract. This is how we'll do this: - // // 1. A user knows the maximum amount0, amount1 (the desired amounts are max) they want to deposit, - // // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, - // // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! - // // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, - // // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transfer_preparer_storage_slot_commitment), - // // --> this function will: - // // 4.1 load both the partial note and the burned amount from transient storage - // // 4.2 check that actual_amount < burned_amount, - // // 4.3 publicly mint the actual_amount to msg_sender (the DEX in our case), - // // 4.4 finalize the partial note amount with `burned_amount - actual_amount` and emit the note. - // // - // // Note 1: This is essentially a 1 person alternative to the fee refund flow where we have a user and an FPC. - // // Cost of this flow: - // // num calls: 2 private calls to setup the partial notes, 1 public call to add_liquidity, 2 public calls to finalize_transfer_to_public_with_refund - // // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool - // // TODO: Implement `prepare_transfer_to_public_with_refund` and `finalize_transfer_to_public_with_refund` in the Token contract. - // token0.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount0).call(&mut context); - // token1.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount1).call(&mut context); - - // // Calculate the amount of liquidity tokens to mint - // let total_supply = liquidity_token.total_supply().view(&mut context) as u64; // TODO: Nuke the cast here. - // let mut liquidity: u64 = 0; - // if (total_supply == 0) { - // // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? - // // TODO: avoid the casts here. Shall we use a method natively working with some integer type? - // liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u64; - // liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens - // } else { - // liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); - // } - // assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); - // // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. - // liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); - - // // Update the reserves - // // (Note that we should not pay for token0, token1 and liquidity_token writes because kernel should squash them - // // (once the optimization is done)). - // let updated_state = State { - // token0: state.token0, - // token1: state.token1, - // liquidity_token: state.liquidity_token, - // reserve0: reserve0 + amount0, - // reserve1: reserve1 + amount1 - // }; - // storage.state.write(updated_state); - - // _unlock(&mut context); - // } - - // // Removes liquidity from the pool and transfers the tokens to the partial notes prepared on token0 and token1. - // // It is necessary that the liquidity provider transfers the liquidity tokens to the contract in a `BatchCall` - // // before calling this function. - // // Note: Do we consider this to be too dangerous? E.g. users accidentally transferring liquidity tokens to the pool - // // in a separate tx and then getting rugged by bots? Just transferring is the most efficient because we already - // // know the liquidity amount so we don't need partial notes and it's the most flexible: users can either call - // // `Token::transfer_in_public` or `Token::transfer_to_public` and this contract does not care. - // // `transfer_preparer_storage_slot_commitment` is a storage slot commitment used for both partial notes finalized - // // in this tx (token0 and token1 notes). - // #[public] - // fn remove_liquidity( - // amount0Min: u64, - // amount1Min: u64, - // transfer_preparer_storage_slot_commitment: Field - // ) { - // _lock(&mut context); - - // // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared - // // the partial notes. - - // let state = storage.state.read(); - - // let token0 = Token::at(state.token0); - // let token1 = Token::at(state.token1); - // let liquidity_token = Token::at(state.liquidity_token); - - // // Calculate the amounts to be added to the pool - // let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); - // let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - - // let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); - // let total_supply = liquidity_token.total_supply().view(&mut context); - - // let amount0 = liquidity * balance0 / total_supply; - // let amount1 = liquidity * balance1 / total_supply; - // // TODO: Nuke these castings. Ideally make Token return integer and not Field. - // assert(amount0 as u64 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - // assert(amount1 as u64 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); - - // liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); - // // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do - // // if the expectation is that users will mostly want to have private balances. - // // TODO: Implement `prepare_transfer_to_private` and `finalize_transfer_to_private` in the Token contract - // // (it's just on the NFT now). - // token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); - // token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); - - // // We load the balances again directly from the tokens because Uni v2 does it like this. But why do they do - // // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird - // // token accounting which could be affected by the transfer calls above? - // balance0 = token0.balance_of_public(context.this_address()).view(&mut context); - // balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - - // // Update the reserves - // let updated_state = State { - // token0: state.token0, - // token1: state.token1, - // liquidity_token: state.liquidity_token, - // reserve0: reserve0 + amount0, - // reserve1: reserve1 + amount1 - // }; - // storage.state.write(updated_state); - - // _unlock(&mut context); - // } - - // // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates - // // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is - // // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. - // #[public] - // fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool) { - // _lock(&mut context); - - // let state = storage.state.read(); - - // let (token_address_in, token_address_out, reserve_in, reserve_out) = if from_0_to_1 { - // (state.token0, state.token1, state.reserve0, state.reserve1) - // } else { - // (state.token1, state.token0, state.reserve1, state.reserve0) - // }; - // let token_in = Token::at(token_address_in); - - // let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; - // assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); - - // let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); - // assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - - // // TODO: _swap(amounts, path, to); - - // _unlock(&mut context); - // } - - // // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates - // // whether we are swapping `token0` for `token1` or vice versa. - // #[public] - // fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool) { - // _lock(&mut context); - - // // TODO - - // _unlock(&mut context); - // } - - // // After locking and unlocking there will be 1 public data write because we always commit the last change and - // // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce - // // transient storage. - // // TODO: It would be quite nice to have a reentrancy-guard macro as using locks will be quite common. - // #[contract_library_method] - // fn _lock(context: &mut PublicContext) { - // let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); - // assert(!already_locked, "Already locked"); - // context.storage_write(LOCK_STORAGE_SLOT, true); - // } - - // #[contract_library_method] - // fn _unlock(context: &mut PublicContext) { - // let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); - // assert(already_locked, "Not locked"); - // context.storage_write(LOCK_STORAGE_SLOT, false); - // } + // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). + // `amount0Desired` and `amount1Desired` are the amounts of tokens we ideally want to add. `amount0Min` + // and `amount1Min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` + // is a storage slot commitment used for all 3 partial notes finalized in this tx (token0 refund note, token1 + // refund note, liquidity token partial note). + // (Note: It's fine to use 1 commitment for all 3 partial notes as it's used for transient storage in different + // contracts). It's necessary to prepare the 3 partial notes in a `BatchCall` before calling this function. + // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow + // where the token amount is not known in advance and we don't need to know the amounts for partial notes. + #[public] + fn add_liquidity( + amount0Desired: u64, + amount1Desired: u64, + amount0Min: u64, + amount1Min: u64, + transfer_preparer_storage_slot_commitment: Field + ) { + _lock(&mut context); + + // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared + // the partial notes. + + assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); + + let state = storage.state.read(); + + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + let reserve0 = state.reserve0; + let reserve1 = state.reserve1; + + let liquidity_token = Token::at(state.liquidity_token); + + // Calculate the amounts to be added to the pool + let mut amount0 = amount0Desired; + let mut amount1 = amount1Desired; + if ((reserve0 != 0) | (reserve1 != 0)) { + // First calculate the optimal amount of token1 based on the desired amount of token0. + let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); + if (amount1Optimal <= amount1Desired) { + // Revert if the optimal amount of token1 is less than the desired amount of token1. + assert(amount1Optimal >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + amount0 = amount0Desired; + amount1 = amount1Optimal; + } else { + // We got more amount of token1 than desired so we try repeating the process but this time by quoting + // based on token1. + let amount0Optimal = get_quote(amount1Desired, reserve1, reserve0); + assert(amount0Optimal <= amount0Desired); + assert(amount0Optimal >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + amount0 = amount0Optimal; + amount1 = amount1Desired; + } + } + + // Now we transfer the tokens to this contract. This is how we'll do this: + // 1. A user knows the maximum amount0, amount1 (the desired amounts are max) they want to deposit, + // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, + // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! + // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, + // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transfer_preparer_storage_slot_commitment), + // --> this function will: + // 4.1 load both the partial note and the burned amount from transient storage + // 4.2 check that actual_amount < burned_amount, + // 4.3 publicly mint the actual_amount to msg_sender (the DEX in our case), + // 4.4 finalize the partial note amount with `burned_amount - actual_amount` and emit the note. + // + // Note 1: This is essentially a 1 person alternative to the fee refund flow where we have a user and an FPC. + // Cost of this flow: + // num calls: 2 private calls to setup the partial notes, 1 public call to add_liquidity, 2 public calls to finalize_transfer_to_public_with_refund + // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool + // TODO: Implement `prepare_transfer_to_public_with_refund` and `finalize_transfer_to_public_with_refund` in the Token contract. + token0.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount0).call(&mut context); + token1.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount1).call(&mut context); + + // Calculate the amount of liquidity tokens to mint + let total_supply = liquidity_token.total_supply().view(&mut context) as u64; // TODO: Nuke the cast here. + let mut liquidity: u64 = 0; + if (total_supply == 0) { + // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? + // TODO: avoid the casts here. Shall we use a method natively working with some integer type? + liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u64; + liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens + } else { + liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); + } + assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); + // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. + liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); + + // Update the reserves + // (Note that we should not pay for token0, token1 and liquidity_token writes because kernel should squash them + // (once the optimization is done)). + let updated_state = State { + token0: state.token0, + token1: state.token1, + liquidity_token: state.liquidity_token, + reserve0: reserve0 + amount0, + reserve1: reserve1 + amount1 + }; + storage.state.write(updated_state); + + _unlock(&mut context); + } + + // Removes liquidity from the pool and transfers the tokens to the partial notes prepared on token0 and token1. + // It is necessary that the liquidity provider transfers the liquidity tokens to the contract in a `BatchCall` + // before calling this function. + // Note: Do we consider this to be too dangerous? E.g. users accidentally transferring liquidity tokens to the pool + // in a separate tx and then getting rugged by bots? Just transferring is the most efficient because we already + // know the liquidity amount so we don't need partial notes and it's the most flexible: users can either call + // `Token::transfer_in_public` or `Token::transfer_to_public` and this contract does not care. + // `transfer_preparer_storage_slot_commitment` is a storage slot commitment used for both partial notes finalized + // in this tx (token0 and token1 notes). + #[public] + fn remove_liquidity( + amount0Min: u64, + amount1Min: u64, + transfer_preparer_storage_slot_commitment: Field + ) { + _lock(&mut context); + + // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared + // the partial notes. + + let state = storage.state.read(); + + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + let liquidity_token = Token::at(state.liquidity_token); + + // Calculate the amounts to be added to the pool + let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); + let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); + + let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); + let total_supply = liquidity_token.total_supply().view(&mut context); + + let amount0 = liquidity * balance0 / total_supply; + let amount1 = liquidity * balance1 / total_supply; + // TODO: Nuke these castings. Ideally make Token return integer and not Field. + assert(amount0 as u64 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); + assert(amount1 as u64 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + + liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); + // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do + // if the expectation is that users will mostly want to have private balances. + // TODO: Implement `prepare_transfer_to_private` and `finalize_transfer_to_private` in the Token contract + // (it's just on the NFT now). + token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); + token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); + + // We load the balances again directly from the tokens because Uni v2 does it like this. But why do they do + // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird + // token accounting which could be affected by the transfer calls above? + balance0 = token0.balance_of_public(context.this_address()).view(&mut context); + balance1 = token1.balance_of_public(context.this_address()).view(&mut context); + + // Update the reserves + let updated_state = State { + token0: state.token0, + token1: state.token1, + liquidity_token: state.liquidity_token, + reserve0: reserve0 + amount0, + reserve1: reserve1 + amount1 + }; + storage.state.write(updated_state); + + _unlock(&mut context); + } + + // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates + // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is + // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. + #[public] + fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool) { + _lock(&mut context); + + let state = storage.state.read(); + + let (token_address_in, token_address_out, reserve_in, reserve_out) = if from_0_to_1 { + (state.token0, state.token1, state.reserve0, state.reserve1) + } else { + (state.token1, state.token0, state.reserve1, state.reserve0) + }; + let token_in = Token::at(token_address_in); + + let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); + + let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); + assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); + + // TODO: _swap(amounts, path, to); + + _unlock(&mut context); + } + + // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates + // whether we are swapping `token0` for `token1` or vice versa. + #[public] + fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool) { + _lock(&mut context); + + // TODO + + _unlock(&mut context); + } + + // After locking and unlocking there will be 1 public data write because we always commit the last change and + // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce + // transient storage. + // TODO: It would be quite nice to have a reentrancy-guard macro as using locks will be quite common. + #[contract_library_method] + fn _lock(context: &mut PublicContext) { + let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); + assert(!already_locked, "Already locked"); + context.storage_write(LOCK_STORAGE_SLOT, true); + } + + #[contract_library_method] + fn _unlock(context: &mut PublicContext) { + let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); + assert(already_locked, "Not locked"); + context.storage_write(LOCK_STORAGE_SLOT, false); + } } From a550c06f70c2c7ab78aee1dcc1bfd472c48e6ef3 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 24 Sep 2024 10:09:31 +0000 Subject: [PATCH 28/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index cd89862f9d2..cab6c86f7a6 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -237,12 +237,15 @@ contract DEX { let token_in = Token::at(token_address_in); let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + // TODO: If we want to support flashswaps we should move this check to the end of the function and accept + // calldata as an arg and perform the call defined by the calldata. Not doing this now as it's not essential + // for a minimal implementation. assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - // TODO: _swap(amounts, path, to); + // TODO: Implement UniswapV2Pair._swap(amounts, path, to) here. The code above is from router. _unlock(&mut context); } From df5da334163010db3c5b9890f9c71e5dc249d2f8 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 7 Oct 2024 17:41:54 +0000 Subject: [PATCH 29/67] WIP --- .../contracts/dex_contract/src/main.nr | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index cab6c86f7a6..008279854d1 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -223,8 +223,15 @@ contract DEX { // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. + // + // TODO: Do we want to support flash swaps? #[public] - fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool) { + fn swap_exact_tokens_for_tokens( + amount_in: u64, + amount_out_min: u64, + from_0_to_1: bool, + transfer_preparer_storage_slot_commitment: Field + ) { _lock(&mut context); let state = storage.state.read(); @@ -246,6 +253,11 @@ contract DEX { assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); // TODO: Implement UniswapV2Pair._swap(amounts, path, to) here. The code above is from router. + let token_out = Token::at(token_address_out); + token_out.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); + + // Do we want to bother wieht the 'UniswapV2: K' check here or is this fine for PoC? + // https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L182 _unlock(&mut context); } From 63ce8906f9c33617c5ef4cf83fb7771d084e6ec8 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 7 Oct 2024 17:43:59 +0000 Subject: [PATCH 30/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 008279854d1..2ae0294e730 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -262,16 +262,7 @@ contract DEX { _unlock(&mut context); } - // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates - // whether we are swapping `token0` for `token1` or vice versa. - #[public] - fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool) { - _lock(&mut context); - - // TODO - - _unlock(&mut context); - } + // Note: swap_tokens_for_exact_tokens is not important for our purposes so I am not doing it. // After locking and unlocking there will be 1 public data write because we always commit the last change and // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce From 5376993c5520321fae8b82fc0c058ed7d202b1f2 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 7 Oct 2024 17:52:25 +0000 Subject: [PATCH 31/67] WIP --- .../contracts/dex_contract/src/main.nr | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 2ae0294e730..876d7830069 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -252,13 +252,23 @@ contract DEX { let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - // TODO: Implement UniswapV2Pair._swap(amounts, path, to) here. The code above is from router. + // Below is a snippet from UniswapV2Pair._swap(amounts, path, to) here. The code above is from router. let token_out = Token::at(token_address_out); token_out.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); // Do we want to bother wieht the 'UniswapV2: K' check here or is this fine for PoC? // https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L182 + // Update the reserves + let updated_state = State { + token0: state.token0, + token1: state.token1, + liquidity_token: state.liquidity_token, + reserve0: token0.balance_of_public(context.this_address()).view(&mut context) as u64, + reserve1: token1.balance_of_public(context.this_address()).view(&mut context) as u64 + }; + storage.state.write(updated_state); + _unlock(&mut context); } From 91a6457c693f0d51f7decb5acd3fc5fc814adc5f Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 9 Oct 2024 16:46:27 +0000 Subject: [PATCH 32/67] WIP --- .../contracts/dex_contract/src/main.nr | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 876d7830069..722dba63557 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -8,7 +8,7 @@ contract DEX { use crate::lib::{get_quote, get_amount_out}; use dep::aztec::{ macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}}, - prelude::{AztecAddress, PublicImmutable, PublicContext}, protocol_types::traits::Serialize + prelude::{AztecAddress, SharedImmutable, PublicContext}, protocol_types::traits::Serialize }; use std::meta::derive; use dep::token::Token; @@ -23,13 +23,11 @@ contract DEX { token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress, - reserve0: u64, - reserve1: u64, } #[storage] struct Storage { - state: PublicImmutable, + state: SharedImmutable, } global MINIMUM_LIQUIDITY: u64 = 1000; @@ -223,53 +221,55 @@ contract DEX { // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. - // - // TODO: Do we want to support flash swaps? + #[private] + fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool, nonce: Field) { + let state = storage.state.read_private(); + + let (token_address_in, token_address_out) = if from_0_to_1 { + (state.token0, state.token1) + } else { + (state.token1, state.token0) + }; + + let token_in = Token::at(token_address_in); + let token_out = Token::at(token_address_out); + + token_in.transfer_to_public(msg.sender, context.this_address(), amount_in, nonce).call(&mut context); + let note_hiding_point_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); + + DEX::at(context.this_address())._swap_exact_tokens_for_tokens( + amount_in, + amount_out_min, + token_address_in, + token_address_out, + note_hiding_point_slot_commitment + ).enqueue(&mut context); + } + #[public] - fn swap_exact_tokens_for_tokens( + #[internal] + fn _swap_exact_tokens_for_tokens( amount_in: u64, amount_out_min: u64, - from_0_to_1: bool, - transfer_preparer_storage_slot_commitment: Field + token_address_in: AztecAddress, + token_address_out: AztecAddress, + note_hiding_point_slot_commitment: Field ) { - _lock(&mut context); + // We don't need any kind of reentrancy guard here because the only way to enter this public function is from + // `swap_exact_tokens_for_tokens` which is private and since public functions cannot call private ones + // it's impossible to reenter this function. - let state = storage.state.read(); - - let (token_address_in, token_address_out, reserve_in, reserve_out) = if from_0_to_1 { - (state.token0, state.token1, state.reserve0, state.reserve1) - } else { - (state.token1, state.token0, state.reserve1, state.reserve0) - }; let token_in = Token::at(token_address_in); + let token_out = Token::at(token_address_out); let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; - // TODO: If we want to support flashswaps we should move this check to the end of the function and accept - // calldata as an arg and perform the call defined by the calldata. Not doing this now as it's not essential - // for a minimal implementation. - assert(reserve_in_with_amount_in >= reserve_in + amount_in, "TOKEN_IN_NOT_TRANSFERRED_TO_POOL"); + let reserve_in = reserve_in_with_amount_in - amount_in; + let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - // Below is a snippet from UniswapV2Pair._swap(amounts, path, to) here. The code above is from router. - let token_out = Token::at(token_address_out); - token_out.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); - - // Do we want to bother wieht the 'UniswapV2: K' check here or is this fine for PoC? - // https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L182 - - // Update the reserves - let updated_state = State { - token0: state.token0, - token1: state.token1, - liquidity_token: state.liquidity_token, - reserve0: token0.balance_of_public(context.this_address()).view(&mut context) as u64, - reserve1: token1.balance_of_public(context.this_address()).view(&mut context) as u64 - }; - storage.state.write(updated_state); - - _unlock(&mut context); + token_out.finalize_transfer_to_private(amount_out, note_hiding_point_slot_commitment).call(&mut context); } // Note: swap_tokens_for_exact_tokens is not important for our purposes so I am not doing it. From 8b9a0e2e4fbade4ec51da07bba8b0d5aebbd3b7c Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 9 Oct 2024 17:26:17 +0000 Subject: [PATCH 33/67] WIP --- .../contracts/dex_contract/src/main.nr | 170 ++++++++++-------- 1 file changed, 92 insertions(+), 78 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 722dba63557..1354f8ac298 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -27,103 +27,132 @@ contract DEX { #[storage] struct Storage { + // The following is only needed in private but we use ShareImmutable here instead of PrivateImmutable because + // the value can be publicly known and SharedImmutable provides us with a better devex here because we don't + // have to bother with sharing the note between pixies of users. state: SharedImmutable, } global MINIMUM_LIQUIDITY: u64 = 1000; global LOCK_STORAGE_SLOT = 980908; // Arbitrarily chosen lock storage slot. + // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as + // a liquidity tracking contract. This contract would be an admin of the liquidity contract. + // TODO: Either deploy the liquidity contract in the constructor or somehow verify it. #[public] #[initializer] - fn constructor(token0: AztecAddress, token1: AztecAddress) { - // Since we don't have inheritance it seems the easiest to deploy the standard token and use it as a liquidity - // tracking contract. This contract would be an admin of the liquidity contract. - - // TODO: either deploy here the liquidity contract or pass its address as an arg on input and verify that - // it was deployed correctly. - let liquidity_token = AztecAddress::zero(); - - let state = State { token0, token1, liquidity_token, reserve0: 0, reserve1: 0 }; + fn constructor(token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress) { + let state = State { token0, token1, liquidity_token }; storage.state.initialize(state); } // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). - // `amount0Desired` and `amount1Desired` are the amounts of tokens we ideally want to add. `amount0Min` - // and `amount1Min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` + // `amount0_desired` and `amount1_desired` are the amounts of tokens we ideally want to add. `amount0_min` + // and `amount1_min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` // is a storage slot commitment used for all 3 partial notes finalized in this tx (token0 refund note, token1 // refund note, liquidity token partial note). // (Note: It's fine to use 1 commitment for all 3 partial notes as it's used for transient storage in different // contracts). It's necessary to prepare the 3 partial notes in a `BatchCall` before calling this function. // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow // where the token amount is not known in advance and we don't need to know the amounts for partial notes. - #[public] + #[private] fn add_liquidity( - amount0Desired: u64, - amount1Desired: u64, - amount0Min: u64, - amount1Min: u64, - transfer_preparer_storage_slot_commitment: Field + amount0_desired: u64, + amount1_desired: u64, + amount0_min: u64, + amount1_min: u64, + nonce: Field ) { - _lock(&mut context); + // TODO: Do we need reentrancy guards in the private funcs? And if yes how to do it? + assert(amount0_desired > 0 & amount1_desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); - // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared - // the partial notes. + let state = storage.state.read_private(); - assert(amount0Desired > 0 & amount1Desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + let liquidity_token = Token::at(state.liquidity_token); - let state = storage.state.read(); + // The following 2 functions burn user's notes worth `amount0_desired` and `amount1_desired`, they prepare + // the partial notes for refunds and enqueue 2 public calls that transfer the amounts to the DEX. + let refund_token0_slot_commitment = token0.prepare_transfer_to_public_with_refund(msg.sender, context.this_address(), amount0_desired, nonce).call(&mut context); + let refund_token1_slot_commitment = token1.prepare_transfer_to_public_with_refund(msg.sender, context.this_address(), amount1_desired, nonce).call(&mut context); + let liquidity_slot_commitment = liquidity_token.prepare_transfer_to_private(msg.sender).call(&mut context); + + DEX::at(context.this_address())._add_liquidity( + state, + refund_token0_slot_commitment, + refund_token1_slot_commitment, + liquidity_slot_commitment, + amount0_desired, + amount1_desired, + amount0_min, + amount1_min + ).enqueue(&mut context); + } + #[public] + #[internal] + fn _add_liquidity( + // We pass the state as an argument in order to not have to read it from storage again. + state: State, + refund_token0_slot_commitment: Field, + refund_token1_slot_commitment: Field, + liquidity_slot_commitment: Field, + amount0_desired: u64, + amount1_desired: u64, + amount0_min: u64, + amount1_min: u64 + ) { + // We don't need any kind of reentrancy guard here because the only way to enter this public function is from + // `add_liquidity` which is private and since public functions cannot call private ones it's impossible to + // reenter this function. let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); - let reserve0 = state.reserve0; - let reserve1 = state.reserve1; - let liquidity_token = Token::at(state.liquidity_token); + let reserve0_with_amount0_desired = token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Avoid the need for these casts. + let reserve1_with_amount1_desired = token1.balance_of_public(context.this_address()).view(&mut context) as u64; + + let reserve0 = reserve0_with_amount0_desired - amount0_desired; + let reserve1 = reserve1_with_amount1_desired - amount1_desired; + // Calculate the amounts to be added to the pool - let mut amount0 = amount0Desired; - let mut amount1 = amount1Desired; + let mut amount0 = amount0_desired; + let mut amount1 = amount1_desired; if ((reserve0 != 0) | (reserve1 != 0)) { // First calculate the optimal amount of token1 based on the desired amount of token0. - let amount1Optimal = get_quote(amount0Desired, reserve0, reserve1); - if (amount1Optimal <= amount1Desired) { + let amount1_optimal = get_quote(amount0_desired, reserve0, reserve1); + if (amount1_optimal <= amount1_desired) { // Revert if the optimal amount of token1 is less than the desired amount of token1. - assert(amount1Optimal >= amount1Min, "INSUFFICIENT_1_AMOUNT"); - amount0 = amount0Desired; - amount1 = amount1Optimal; + assert(amount1_optimal >= amount1_min, "INSUFFICIENT_1_AMOUNT"); + amount0 = amount0_desired; + amount1 = amount1_optimal; } else { // We got more amount of token1 than desired so we try repeating the process but this time by quoting // based on token1. - let amount0Optimal = get_quote(amount1Desired, reserve1, reserve0); - assert(amount0Optimal <= amount0Desired); - assert(amount0Optimal >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - amount0 = amount0Optimal; - amount1 = amount1Desired; + let amount0_optimal = get_quote(amount1_desired, reserve1, reserve0); + assert(amount0_optimal <= amount0_desired); + assert(amount0_optimal >= amount0_min, "INSUFFICIENT_0_AMOUNT"); + amount0 = amount0_optimal; + amount1 = amount1_desired; } } - // Now we transfer the tokens to this contract. This is how we'll do this: - // 1. A user knows the maximum amount0, amount1 (the desired amounts are max) they want to deposit, - // 2. user calls private `token{0, 1}.prepare_transfer_to_public_with_refund(from, to, amount{0,1}, transient_storage_slot_randomness)` functions, - // --> these functions will burn the amounts, prepare the partial notes and stores both the burned amounts and the partial notes in the transient storage! - // 3. user calls DEX.add_liquidity(..., transient_storage_slot_randomness) with the amounts they want to deposit, - // 4. the `add_liquidity` func computes the amounts to deposit and calls token{0, 1}.finalize_transfer_to_public_with_refund(actual_amount{0,1}, transfer_preparer_storage_slot_commitment), - // --> this function will: - // 4.1 load both the partial note and the burned amount from transient storage - // 4.2 check that actual_amount < burned_amount, - // 4.3 publicly mint the actual_amount to msg_sender (the DEX in our case), - // 4.4 finalize the partial note amount with `burned_amount - actual_amount` and emit the note. - // - // Note 1: This is essentially a 1 person alternative to the fee refund flow where we have a user and an FPC. - // Cost of this flow: - // num calls: 2 private calls to setup the partial notes, 1 public call to add_liquidity, 2 public calls to finalize_transfer_to_public_with_refund - // DA: up to 2 change notes when burning amount{0,1}, nullifiers to burn users notes, up to 2 refund notes, 1 public data write to mint pub balance to pool - // TODO: Implement `prepare_transfer_to_public_with_refund` and `finalize_transfer_to_public_with_refund` in the Token contract. - token0.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount0).call(&mut context); - token1.finalize_transfer_to_public_with_refund(transfer_preparer_storage_slot_commitment, amount1).call(&mut context); + let refund_amount_token0 = amount0_desired - amount0; + let refund_amount_token1 = amount1_desired - amount1; + + // The refund does not need to be finalized if the refund amount is 0 --> the partial note will either be wiped + // out from transient storage at the end of the tx (which is fine) or it will stay in public storage (which is + // also fine). + if (refund_amount_token0 > 0) { + token0.finalize_transfer_to_public_with_refund(refund_token0_slot_commitment, refund_amount_token0).call(&mut context); + } + if (refund_amount_token1 > 0) { + token1.finalize_transfer_to_public_with_refund(refund_token1_slot_commitment, refund_amount_token1).call(&mut context); + } // Calculate the amount of liquidity tokens to mint - let total_supply = liquidity_token.total_supply().view(&mut context) as u64; // TODO: Nuke the cast here. + let total_supply = liquidity_token.total_supply().view(&mut context) as u64; let mut liquidity: u64 = 0; if (total_supply == 0) { // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? @@ -134,22 +163,7 @@ contract DEX { liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); } assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); - // TODO: Implement `prepare_mint_to_private` and `finalize_mint_to_private` in the Token contract. - liquidity_token.finalize_mint_to_private(transfer_preparer_storage_slot_commitment, liquidity).call(&mut context); - - // Update the reserves - // (Note that we should not pay for token0, token1 and liquidity_token writes because kernel should squash them - // (once the optimization is done)). - let updated_state = State { - token0: state.token0, - token1: state.token1, - liquidity_token: state.liquidity_token, - reserve0: reserve0 + amount0, - reserve1: reserve1 + amount1 - }; - storage.state.write(updated_state); - - _unlock(&mut context); + liquidity_token.finalize_mint_to_private(liquidity_slot_commitment, liquidity).call(&mut context); } // Removes liquidity from the pool and transfers the tokens to the partial notes prepared on token0 and token1. @@ -163,8 +177,8 @@ contract DEX { // in this tx (token0 and token1 notes). #[public] fn remove_liquidity( - amount0Min: u64, - amount1Min: u64, + amount0_min: u64, + amount1_min: u64, transfer_preparer_storage_slot_commitment: Field ) { _lock(&mut context); @@ -188,8 +202,8 @@ contract DEX { let amount0 = liquidity * balance0 / total_supply; let amount1 = liquidity * balance1 / total_supply; // TODO: Nuke these castings. Ideally make Token return integer and not Field. - assert(amount0 as u64 >= amount0Min, "INSUFFICIENT_0_AMOUNT"); - assert(amount1 as u64 >= amount1Min, "INSUFFICIENT_1_AMOUNT"); + assert(amount0 as u64 >= amount0_min, "INSUFFICIENT_0_AMOUNT"); + assert(amount1 as u64 >= amount1_min, "INSUFFICIENT_1_AMOUNT"); liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do @@ -269,7 +283,7 @@ contract DEX { let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - token_out.finalize_transfer_to_private(amount_out, note_hiding_point_slot_commitment).call(&mut context); + token_out.finalize_transfer_to_private(note_hiding_point_slot_commitment, amount_out).call(&mut context); } // Note: swap_tokens_for_exact_tokens is not important for our purposes so I am not doing it. From 93f172ab41e4bc136ab7596f13704c62337f158e Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 9 Oct 2024 17:51:13 +0000 Subject: [PATCH 34/67] WIP --- .../contracts/dex_contract/src/main.nr | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 1354f8ac298..f2991d92f2a 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -46,15 +46,9 @@ contract DEX { storage.state.initialize(state); } - // Privately adds liquidity for `liquidity_provider` to the pool (identity of liquidity provider not revealed). - // `amount0_desired` and `amount1_desired` are the amounts of tokens we ideally want to add. `amount0_min` - // and `amount1_min` are the minimum amounts we are willing to add. `transfer_preparer_storage_slot_commitment` - // is a storage slot commitment used for all 3 partial notes finalized in this tx (token0 refund note, token1 - // refund note, liquidity token partial note). - // (Note: It's fine to use 1 commitment for all 3 partial notes as it's used for transient storage in different - // contracts). It's necessary to prepare the 3 partial notes in a `BatchCall` before calling this function. - // Note: We needed to make the identity of liquidity provider private because we don't have a transfer_from flow - // where the token amount is not known in advance and we don't need to know the amounts for partial notes. + // Privately adds liquidity to the pool (identity of liquidity provider not revealed). `amount0_desired` + // and `amount1_desired` are the amounts of tokens we ideally want to add. `amount0_min` and `amount1_min` + // are the minimum amounts we are willing to add. #[private] fn add_liquidity( amount0_desired: u64, @@ -93,7 +87,7 @@ contract DEX { #[public] #[internal] fn _add_liquidity( - // We pass the state as an argument in order to not have to read it from storage again. + // We pass the state as an argument in order to not have to read it from public storage again. state: State, refund_token0_slot_commitment: Field, refund_token1_slot_commitment: Field, From deb3a5c58ec75c87d706f1c5efe2bcb110c66222 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 9 Oct 2024 18:11:18 +0000 Subject: [PATCH 35/67] WIP --- .../contracts/dex_contract/src/main.nr | 120 +++++++----------- 1 file changed, 49 insertions(+), 71 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index f2991d92f2a..79db00f4cbd 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -34,11 +34,11 @@ contract DEX { } global MINIMUM_LIQUIDITY: u64 = 1000; - global LOCK_STORAGE_SLOT = 980908; // Arbitrarily chosen lock storage slot. // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as // a liquidity tracking contract. This contract would be an admin of the liquidity contract. - // TODO: Either deploy the liquidity contract in the constructor or somehow verify it. + // TODO: Either deploy the liquidity contract in the constructor or verify it that it corresponds to what this DEX + // expects. #[public] #[initializer] fn constructor(token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress) { @@ -48,7 +48,8 @@ contract DEX { // Privately adds liquidity to the pool (identity of liquidity provider not revealed). `amount0_desired` // and `amount1_desired` are the amounts of tokens we ideally want to add. `amount0_min` and `amount1_min` - // are the minimum amounts we are willing to add. + // are the minimum amounts we are willing to add. `nonce` can be arbitrary non-zero value and it's here to + // isolate authwits to this specific call. #[private] fn add_liquidity( amount0_desired: u64, @@ -160,37 +161,55 @@ contract DEX { liquidity_token.finalize_mint_to_private(liquidity_slot_commitment, liquidity).call(&mut context); } - // Removes liquidity from the pool and transfers the tokens to the partial notes prepared on token0 and token1. - // It is necessary that the liquidity provider transfers the liquidity tokens to the contract in a `BatchCall` - // before calling this function. - // Note: Do we consider this to be too dangerous? E.g. users accidentally transferring liquidity tokens to the pool - // in a separate tx and then getting rugged by bots? Just transferring is the most efficient because we already - // know the liquidity amount so we don't need partial notes and it's the most flexible: users can either call - // `Token::transfer_in_public` or `Token::transfer_to_public` and this contract does not care. - // `transfer_preparer_storage_slot_commitment` is a storage slot commitment used for both partial notes finalized - // in this tx (token0 and token1 notes). + // Removes `liquidity` from the pool and transfers the tokens back to the user. `amount0_min` and `amount1_min` are + // the minimum amounts of `token0` and `token1` the user is willing to accept. `nonce` can be arbitrary non-zero + // value and its purpose is to isolate authwits to this specific call. + #[private] + fn remove_liquidity(liquidity: u64, amount0_min: u64, amount1_min: u64, nonce: Field) { + // TODO: Do we need reentrancy guards in the private funcs? And if yes how to do it? + let state = storage.state.read_private(); + + let liquidity_token = Token::at(state.liquidity_token); + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + + liquidity_token.transfer_to_public(msg.sender, context.this_address(), liquidity, nonce).call(&mut context); + let token0_slot_commitment = token0.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); + let token1_slot_commitment = token1.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); + + DEX::at(context.this_address())._remove_liquidity( + state, + token0_slot_commitment, + token1_slot_commitment, + liquidity, + amount0_min, + amount1_min + ).enqueue(&mut context); + } + #[public] - fn remove_liquidity( + #[internal] + fn _remove_liquidity( + // We pass the state as an argument in order to not have to read it from public storage again. + state: State, + token0_slot_commitment: Field, + token1_slot_commitment: Field, + liquidity: u64, amount0_min: u64, - amount1_min: u64, - transfer_preparer_storage_slot_commitment: Field + amount1_min: u64 ) { - _lock(&mut context); - - // Note: We don't use authwit here as the permission from a user is given by the fact that he prepared - // the partial notes. - - let state = storage.state.read(); + // We don't need any kind of reentrancy guard here because the only way to enter this public function is from + // `remove_liquidity` which is private and since public functions cannot call private ones it's impossible to + // reenter this function. let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); - // Calculate the amounts to be added to the pool + // Calculate the amounts to be removed from the pool let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - let liquidity = liquidity_token.balance_of_public(context.this_address()).view(&mut context); let total_supply = liquidity_token.total_supply().view(&mut context); let amount0 = liquidity * balance0 / total_supply; @@ -199,31 +218,10 @@ contract DEX { assert(amount0 as u64 >= amount0_min, "INSUFFICIENT_0_AMOUNT"); assert(amount1 as u64 >= amount1_min, "INSUFFICIENT_1_AMOUNT"); + // At last we burn the liquidity tokens and transfer the token0 and token1 to the user. liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); - // Note: Here we could also transfer to private if we prepared the partial notes. This might make sense to do - // if the expectation is that users will mostly want to have private balances. - // TODO: Implement `prepare_transfer_to_private` and `finalize_transfer_to_private` in the Token contract - // (it's just on the NFT now). - token0.finalize_transfer_to_private(amount0, transfer_preparer_storage_slot_commitment).call(&mut context); - token1.finalize_transfer_to_private(amount1, transfer_preparer_storage_slot_commitment).call(&mut context); - - // We load the balances again directly from the tokens because Uni v2 does it like this. But why do they do - // this? It's not to protect against reentrancy attacks as there are locks. Is it to protect against some weird - // token accounting which could be affected by the transfer calls above? - balance0 = token0.balance_of_public(context.this_address()).view(&mut context); - balance1 = token1.balance_of_public(context.this_address()).view(&mut context); - - // Update the reserves - let updated_state = State { - token0: state.token0, - token1: state.token1, - liquidity_token: state.liquidity_token, - reserve0: reserve0 + amount0, - reserve1: reserve1 + amount1 - }; - storage.state.write(updated_state); - - _unlock(&mut context); + token0.finalize_transfer_to_private(token0_slot_commitment, amount0).call(&mut context); + token1.finalize_transfer_to_private(token1_slot_commitment, amount1).call(&mut context); } // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates @@ -243,14 +241,14 @@ contract DEX { let token_out = Token::at(token_address_out); token_in.transfer_to_public(msg.sender, context.this_address(), amount_in, nonce).call(&mut context); - let note_hiding_point_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); + let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); DEX::at(context.this_address())._swap_exact_tokens_for_tokens( amount_in, amount_out_min, token_address_in, token_address_out, - note_hiding_point_slot_commitment + token_out_slot_commitment ).enqueue(&mut context); } @@ -261,7 +259,7 @@ contract DEX { amount_out_min: u64, token_address_in: AztecAddress, token_address_out: AztecAddress, - note_hiding_point_slot_commitment: Field + token_out_slot_commitment: Field ) { // We don't need any kind of reentrancy guard here because the only way to enter this public function is from // `swap_exact_tokens_for_tokens` which is private and since public functions cannot call private ones @@ -277,26 +275,6 @@ contract DEX { let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - token_out.finalize_transfer_to_private(note_hiding_point_slot_commitment, amount_out).call(&mut context); - } - - // Note: swap_tokens_for_exact_tokens is not important for our purposes so I am not doing it. - - // After locking and unlocking there will be 1 public data write because we always commit the last change and - // "val1 --> val2 --> val1" does not result in 0 public data writes. TODO: Either optimize this or introduce - // transient storage. - // TODO: It would be quite nice to have a reentrancy-guard macro as using locks will be quite common. - #[contract_library_method] - fn _lock(context: &mut PublicContext) { - let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); - assert(!already_locked, "Already locked"); - context.storage_write(LOCK_STORAGE_SLOT, true); - } - - #[contract_library_method] - fn _unlock(context: &mut PublicContext) { - let already_locked: bool = context.storage_read(LOCK_STORAGE_SLOT); - assert(already_locked, "Not locked"); - context.storage_write(LOCK_STORAGE_SLOT, false); + token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); } } From 42b9ebd63930d9bf16144de08f3ebc92f16c9254 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 9 Oct 2024 18:13:59 +0000 Subject: [PATCH 36/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 79db00f4cbd..e261493a175 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -2,22 +2,20 @@ mod lib; use dep::aztec::macros::aztec; -// A Noir implementation of simplified Uniswap v2 pool. +// A Noir implementation of a simplified Uniswap v2 pool. #[aztec] contract DEX { use crate::lib::{get_quote, get_amount_out}; use dep::aztec::{ macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}}, - prelude::{AztecAddress, SharedImmutable, PublicContext}, protocol_types::traits::Serialize + prelude::{AztecAddress, SharedImmutable}, protocol_types::traits::Serialize }; use std::meta::derive; use dep::token::Token; - // We store the tokens of the pool and reserves in a struct such that to load it from PublicImmutable asserts only - // a single merkle proof. + // We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single + // merkle proof. // (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). - // Note: We store the reserves instead of just doing `token{0,1}.balance_of_public(dex_address)` so that we can - // make the user send the `token_in` into the pool before the swap. #[derive(Serialize)] struct State { token0: AztecAddress, From fff6530ae8f41de2e1bc588a14a01c6bd5ee2efd Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 9 Oct 2024 18:15:13 +0000 Subject: [PATCH 37/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index e261493a175..1dd0df042c8 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -223,8 +223,8 @@ contract DEX { } // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates - // whether we are swapping `token0` for `token1` or vice versa. Similartly to `remove_liquidity` function it is - // expected that the user transfers the `token_in` to the pool in a `BatchCall` before calling this function. + // whether we are swapping `token0` for `token1` or vice versa. `nonce` can be arbitrary non-zero value and its + // purpose is to isolate authwits to this specific call. #[private] fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool, nonce: Field) { let state = storage.state.read_private(); From 458094a98c651a375644fc9b14b490eaeab89138 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 9 Oct 2024 18:26:57 +0000 Subject: [PATCH 38/67] WIP --- .../contracts/dex_contract/src/main.nr | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 1dd0df042c8..faead1ff077 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -67,8 +67,10 @@ contract DEX { // The following 2 functions burn user's notes worth `amount0_desired` and `amount1_desired`, they prepare // the partial notes for refunds and enqueue 2 public calls that transfer the amounts to the DEX. - let refund_token0_slot_commitment = token0.prepare_transfer_to_public_with_refund(msg.sender, context.this_address(), amount0_desired, nonce).call(&mut context); - let refund_token1_slot_commitment = token1.prepare_transfer_to_public_with_refund(msg.sender, context.this_address(), amount1_desired, nonce).call(&mut context); + token0.transfer_to_public(msg.sender, context.this_address(), amount0_desired, nonce).call(&mut context); + token1.transfer_to_public(msg.sender, context.this_address(), amount1_desired, nonce).call(&mut context); + let refund_token0_slot_commitment = token0.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); + let refund_token1_slot_commitment = token1.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); let liquidity_slot_commitment = liquidity_token.prepare_transfer_to_private(msg.sender).call(&mut context); DEX::at(context.this_address())._add_liquidity( @@ -138,10 +140,10 @@ contract DEX { // out from transient storage at the end of the tx (which is fine) or it will stay in public storage (which is // also fine). if (refund_amount_token0 > 0) { - token0.finalize_transfer_to_public_with_refund(refund_token0_slot_commitment, refund_amount_token0).call(&mut context); + token0.finalize_transfer_to_private(refund_token0_slot_commitment, refund_amount_token0).call(&mut context); } if (refund_amount_token1 > 0) { - token1.finalize_transfer_to_public_with_refund(refund_token1_slot_commitment, refund_amount_token1).call(&mut context); + token1.finalize_transfer_to_private(refund_token1_slot_commitment, refund_amount_token1).call(&mut context); } // Calculate the amount of liquidity tokens to mint @@ -275,4 +277,54 @@ contract DEX { token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); } + + #[private] + fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool, nonce: Field) { + let state = storage.state.read_private(); + + let (token_address_in, token_address_out) = if from_0_to_1 { + (state.token0, state.token1) + } else { + (state.token1, state.token0) + }; + + let token_in = Token::at(token_address_in); + let token_out = Token::at(token_address_out); + + token_in.transfer_to_public(msg.sender, context.this_address(), amount_in_max, nonce).call(&mut context); + let refund_token_in_slot_commitment = token_in.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); + let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); + + DEX::at(context.this_address())._swap_tokens_for_exact_tokens( + refund_token_in_slot_commitment, + token_out_slot_commitment, + amount_out, + amount_in_max, + token_address_in, + token_address_out + ).enqueue(&mut context); + } + // #[public] + // #[internal] + // fn _swap_tokens_for_exact_tokens( + // refund_token_in_slot_commitment: Field, + // token_out_slot_commitment: Field, + // amount_out: u64, + // amount_in_max: u64, + // token_address_in: AztecAddress, + // token_address_out: AztecAddress, + // ) { + // // We don't need any kind of reentrancy guard here because the only way to enter this public function is from + // // `swap_tokens_for_exact_tokens` which is private and since public functions cannot call private ones + // // it's impossible to reenter this function. + // let token_in = Token::at(token_address_in); + // let token_out = Token::at(token_address_out); + // let reserve_in_with_amount_in_max = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + // let reserve_in = reserve_in_with_amount_in_max - amount_in_max; + // let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; + // let amount_in = get_amount_in(amount_out, reserve_in, reserve_out); + // assert(amount_in <= amount_in_max, "EXCESSIVE_INPUT_AMOUNT"); + // token_in.finalize_transfer_to_public_with_refund(refund_token_in_slot_commitment, amount_in).call(&mut context); + // token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); + // } } From 3bf84437076ac6bbb1287f4f3c7a6d18e15049f8 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 10:13:44 +0000 Subject: [PATCH 39/67] WIP --- .../contracts/dex_contract/src/lib.nr | 10 +++ .../contracts/dex_contract/src/main.nr | 90 ++++++++++++------- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr index 3a62d55a0c3..69be7409718 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr @@ -16,3 +16,13 @@ pub fn get_amount_out(amount_in: u64, reserve_in: u64, reserve_out: u64) -> u64 let denominator = reserve_in * 1000 + amount_in_with_fee; numerator / denominator } + +// given an output amount of an asset and pair reserves, returns a required input amount of the other asset +// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L53 +pub fn get_amount_in(amount_out: u64, reserve_in: u64, reserve_out: u64) -> u64 { + assert(amount_out > 0, "INSUFFICIENT_OUTPUT_AMOUNT"); + assert((reserve_in > 0) & (reserve_out > 0), "INSUFFICIENT_LIQUIDITY"); + let numerator = reserve_in * amount_out * 1000; + let denominator = (reserve_out - amount_out) * 997; + (numerator / denominator) + 1 +} diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index faead1ff077..8346756aa13 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -5,7 +5,7 @@ use dep::aztec::macros::aztec; // A Noir implementation of a simplified Uniswap v2 pool. #[aztec] contract DEX { - use crate::lib::{get_quote, get_amount_out}; + use crate::lib::{get_quote, get_amount_out, get_amount_in}; use dep::aztec::{ macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}}, prelude::{AztecAddress, SharedImmutable}, protocol_types::traits::Serialize @@ -173,6 +173,7 @@ contract DEX { let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); + // We transfer the liquidity tokens to the DEX and prepare partial notes for the output tokens. liquidity_token.transfer_to_public(msg.sender, context.this_address(), liquidity, nonce).call(&mut context); let token0_slot_commitment = token0.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); let token1_slot_commitment = token1.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); @@ -206,17 +207,19 @@ contract DEX { let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); - // Calculate the amounts to be removed from the pool - let mut balance0 = token0.balance_of_public(context.this_address()).view(&mut context); - let mut balance1 = token1.balance_of_public(context.this_address()).view(&mut context); + // We get the reserves and the liquidity token total supply. + let reserve0 = token0.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve1 = token1.balance_of_public(context.this_address()).view(&mut context) as u64; + let total_supply = liquidity_token.total_supply().view(&mut context) as u64; - let total_supply = liquidity_token.total_supply().view(&mut context); + // We calculate the amounts of token0 and token1 the user is entitled to based on the amount of liquidity they + // are removing. + let amount0 = liquidity * reserve0 / total_supply; + let amount1 = liquidity * reserve1 / total_supply; - let amount0 = liquidity * balance0 / total_supply; - let amount1 = liquidity * balance1 / total_supply; - // TODO: Nuke these castings. Ideally make Token return integer and not Field. - assert(amount0 as u64 >= amount0_min, "INSUFFICIENT_0_AMOUNT"); - assert(amount1 as u64 >= amount1_min, "INSUFFICIENT_1_AMOUNT"); + // We check if the amounts are greater than the minimum amounts the user is willing to accept. + assert(amount0 >= amount0_min, "INSUFFICIENT_0_AMOUNT"); + assert(amount1 >= amount1_min, "INSUFFICIENT_1_AMOUNT"); // At last we burn the liquidity tokens and transfer the token0 and token1 to the user. liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); @@ -240,6 +243,7 @@ contract DEX { let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); + // We transfer the `amount_in` to the DEX and we prepare partial note for the output token. token_in.transfer_to_public(msg.sender, context.this_address(), amount_in, nonce).call(&mut context); let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); @@ -268,16 +272,22 @@ contract DEX { let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); + // We get the reserves. The `amount_in` was already transferred to the DEX so we need to subtract it. let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; let reserve_in = reserve_in_with_amount_in - amount_in; let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; + // Calculate the amount of output token we will get. let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); + // Transfer the output token to the user. token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); } + // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates + // whether we are swapping `token0` for `token1` or vice versa. `nonce` can be arbitrary non-zero value and its + // purpose is to isolate authwits to this specific call. #[private] fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool, nonce: Field) { let state = storage.state.read_private(); @@ -291,6 +301,7 @@ contract DEX { let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); + // We transfer the `amount_in_max` to the DEX and we prepare partial notes for refund and for the output token. token_in.transfer_to_public(msg.sender, context.this_address(), amount_in_max, nonce).call(&mut context); let refund_token_in_slot_commitment = token_in.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); @@ -304,27 +315,40 @@ contract DEX { token_address_out ).enqueue(&mut context); } - // #[public] - // #[internal] - // fn _swap_tokens_for_exact_tokens( - // refund_token_in_slot_commitment: Field, - // token_out_slot_commitment: Field, - // amount_out: u64, - // amount_in_max: u64, - // token_address_in: AztecAddress, - // token_address_out: AztecAddress, - // ) { - // // We don't need any kind of reentrancy guard here because the only way to enter this public function is from - // // `swap_tokens_for_exact_tokens` which is private and since public functions cannot call private ones - // // it's impossible to reenter this function. - // let token_in = Token::at(token_address_in); - // let token_out = Token::at(token_address_out); - // let reserve_in_with_amount_in_max = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; - // let reserve_in = reserve_in_with_amount_in_max - amount_in_max; - // let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; - // let amount_in = get_amount_in(amount_out, reserve_in, reserve_out); - // assert(amount_in <= amount_in_max, "EXCESSIVE_INPUT_AMOUNT"); - // token_in.finalize_transfer_to_public_with_refund(refund_token_in_slot_commitment, amount_in).call(&mut context); - // token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); - // } + + #[public] + #[internal] + fn _swap_tokens_for_exact_tokens( + refund_token_in_slot_commitment: Field, + token_out_slot_commitment: Field, + amount_out: u64, + amount_in_max: u64, + token_address_in: AztecAddress, + token_address_out: AztecAddress + ) { + // We don't need any kind of reentrancy guard here because the only way to enter this public function is from + // `swap_tokens_for_exact_tokens` which is private and since public functions cannot call private ones + // it's impossible to reenter this function. + + let token_in = Token::at(token_address_in); + let token_out = Token::at(token_address_out); + + // We get the reserves. The `amount_in_max` was already transferred to the DEX so we need to subtract it. + let reserve_in_with_amount_in_max = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_in = reserve_in_with_amount_in_max - amount_in_max; + let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; + + // Calculate the amount of input token needed to get the desired amount of output token. + let amount_in = get_amount_in(amount_out, reserve_in, reserve_out); + assert(amount_in <= amount_in_max, "EXCESSIVE_INPUT_AMOUNT"); + + // If less than amount_in_max of input token was needed we refund the difference. + let refund_amount = amount_in_max - amount_in; + if (refund_amount > 0) { + token_in.finalize_transfer_to_private(refund_token_in_slot_commitment, refund_amount).call(&mut context); + } + + // Transfer the output token to the user. + token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); + } } From d8b8d209a08c17143483d680721ff5b496c779bf Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 10:20:24 +0000 Subject: [PATCH 40/67] WIP --- .../noir-contracts/contracts/dex_contract/src/main.nr | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 8346756aa13..376dbb9ff13 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -31,6 +31,8 @@ contract DEX { state: SharedImmutable, } + // Amount of liquidity which gets locked in the pool when liquidity is provided for the first time. It's purpose + // is to prevent the pool from ever emptying which could lead to undefined behavior. global MINIMUM_LIQUIDITY: u64 = 1000; // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as @@ -40,8 +42,7 @@ contract DEX { #[public] #[initializer] fn constructor(token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress) { - let state = State { token0, token1, liquidity_token }; - storage.state.initialize(state); + storage.state.initialize(State { token0, token1, liquidity_token }); } // Privately adds liquidity to the pool (identity of liquidity provider not revealed). `amount0_desired` @@ -65,12 +66,14 @@ contract DEX { let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); - // The following 2 functions burn user's notes worth `amount0_desired` and `amount1_desired`, they prepare - // the partial notes for refunds and enqueue 2 public calls that transfer the amounts to the DEX. + // We transfer the desired amounts of tokens to the DEX. token0.transfer_to_public(msg.sender, context.this_address(), amount0_desired, nonce).call(&mut context); token1.transfer_to_public(msg.sender, context.this_address(), amount1_desired, nonce).call(&mut context); + + // Since not all the desired amounts of tokens might be accepted we prepare partial notes for the refunds. let refund_token0_slot_commitment = token0.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); let refund_token1_slot_commitment = token1.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); + // We prepare a partial note for the liquidity tokens. let liquidity_slot_commitment = liquidity_token.prepare_transfer_to_private(msg.sender).call(&mut context); DEX::at(context.this_address())._add_liquidity( From 6db3508f93c1934f277d14b8a44420a3f94b9abf Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 10:44:23 +0000 Subject: [PATCH 41/67] WIP --- .../contracts/dex_contract/src/main.nr | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr index 376dbb9ff13..751f75c42cc 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr @@ -2,7 +2,10 @@ mod lib; use dep::aztec::macros::aztec; -// A Noir implementation of a simplified Uniswap v2 pool. +/// This contract is a demonstration of how an Automated Market Maker (AMM) that requires public state while still +/// achieving identity privacy can be implemented. It does not, however, provide function privacy. +/// +/// Note: This is only a demonstration and we (Aztec team) do not think this is the best way to build a DEX. #[aztec] contract DEX { use crate::lib::{get_quote, get_amount_out, get_amount_in}; @@ -13,9 +16,9 @@ contract DEX { use std::meta::derive; use dep::token::Token; - // We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single - // merkle proof. - // (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). + /// We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single + /// merkle proof. + /// (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). #[derive(Serialize)] struct State { token0: AztecAddress, @@ -31,8 +34,8 @@ contract DEX { state: SharedImmutable, } - // Amount of liquidity which gets locked in the pool when liquidity is provided for the first time. It's purpose - // is to prevent the pool from ever emptying which could lead to undefined behavior. + /// Amount of liquidity which gets locked in the pool when liquidity is provided for the first time. It's purpose + /// is to prevent the pool from ever emptying which could lead to undefined behavior. global MINIMUM_LIQUIDITY: u64 = 1000; // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as @@ -45,10 +48,10 @@ contract DEX { storage.state.initialize(State { token0, token1, liquidity_token }); } - // Privately adds liquidity to the pool (identity of liquidity provider not revealed). `amount0_desired` - // and `amount1_desired` are the amounts of tokens we ideally want to add. `amount0_min` and `amount1_min` - // are the minimum amounts we are willing to add. `nonce` can be arbitrary non-zero value and it's here to - // isolate authwits to this specific call. + /// Privately adds liquidity to the pool (identity of liquidity provider not revealed). `amount0_desired` + /// and `amount1_desired` are the amounts of tokens we ideally want to add. `amount0_min` and `amount1_min` + /// are the minimum amounts we are willing to add. `nonce` can be arbitrary non-zero value and it's here to + /// isolate authwits to this specific call. #[private] fn add_liquidity( amount0_desired: u64, @@ -164,9 +167,9 @@ contract DEX { liquidity_token.finalize_mint_to_private(liquidity_slot_commitment, liquidity).call(&mut context); } - // Removes `liquidity` from the pool and transfers the tokens back to the user. `amount0_min` and `amount1_min` are - // the minimum amounts of `token0` and `token1` the user is willing to accept. `nonce` can be arbitrary non-zero - // value and its purpose is to isolate authwits to this specific call. + /// Removes `liquidity` from the pool and transfers the tokens back to the user. `amount0_min` and `amount1_min` are + /// the minimum amounts of `token0` and `token1` the user is willing to accept. `nonce` can be arbitrary non-zero + /// value and its purpose is to isolate authwits to this specific call. #[private] fn remove_liquidity(liquidity: u64, amount0_min: u64, amount1_min: u64, nonce: Field) { // TODO: Do we need reentrancy guards in the private funcs? And if yes how to do it? @@ -230,11 +233,16 @@ contract DEX { token1.finalize_transfer_to_private(token1_slot_commitment, amount1).call(&mut context); } - // Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates - // whether we are swapping `token0` for `token1` or vice versa. `nonce` can be arbitrary non-zero value and its - // purpose is to isolate authwits to this specific call. + /// Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates + /// whether we are swapping `token0` for `token1` or vice versa. `nonce` can be arbitrary non-zero value and its + /// purpose is to isolate authwits to this specific call. #[private] - fn swap_exact_tokens_for_tokens(amount_in: u64, amount_out_min: u64, from_0_to_1: bool, nonce: Field) { + fn swap_exact_tokens_for_tokens( + amount_in: u64, + amount_out_min: u64, + from_0_to_1: bool, + nonce: Field + ) { let state = storage.state.read_private(); let (token_address_in, token_address_out) = if from_0_to_1 { @@ -288,11 +296,16 @@ contract DEX { token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); } - // Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates - // whether we are swapping `token0` for `token1` or vice versa. `nonce` can be arbitrary non-zero value and its - // purpose is to isolate authwits to this specific call. + /// Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates + /// whether we are swapping `token0` for `token1` or vice versa. `nonce` can be arbitrary non-zero value and its + /// purpose is to isolate authwits to this specific call. #[private] - fn swap_tokens_for_exact_tokens(amount_out: u64, amount_in_max: u64, from_0_to_1: bool, nonce: Field) { + fn swap_tokens_for_exact_tokens( + amount_out: u64, + amount_in_max: u64, + from_0_to_1: bool, + nonce: Field + ) { let state = storage.state.read_private(); let (token_address_in, token_address_out) = if from_0_to_1 { From 17a4efec90f19fa9ce03b1c735d9396a1a89f72f Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 16:47:25 +0000 Subject: [PATCH 42/67] DEX --> AMM --- .../{dex_contract => amm_contract}/Nargo.toml | 2 +- .../{dex_contract => amm_contract}/src/lib.nr | 0 .../src/main.nr | 27 ++++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) rename noir-projects/noir-contracts/contracts/{dex_contract => amm_contract}/Nargo.toml (88%) rename noir-projects/noir-contracts/contracts/{dex_contract => amm_contract}/src/lib.nr (100%) rename noir-projects/noir-contracts/contracts/{dex_contract => amm_contract}/src/main.nr (95%) diff --git a/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml similarity index 88% rename from noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml rename to noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml index 16fabd6cb86..c176425b543 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml @@ -1,5 +1,5 @@ [package] -name = "dex_contract" +name = "amm_contract" authors = [""] compiler_version = ">=0.25.0" type = "contract" diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr similarity index 100% rename from noir-projects/noir-contracts/contracts/dex_contract/src/lib.nr rename to noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr diff --git a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr similarity index 95% rename from noir-projects/noir-contracts/contracts/dex_contract/src/main.nr rename to noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 751f75c42cc..66a2d45a878 100644 --- a/noir-projects/noir-contracts/contracts/dex_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -7,7 +7,7 @@ use dep::aztec::macros::aztec; /// /// Note: This is only a demonstration and we (Aztec team) do not think this is the best way to build a DEX. #[aztec] -contract DEX { +contract AMM { use crate::lib::{get_quote, get_amount_out, get_amount_in}; use dep::aztec::{ macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}}, @@ -40,8 +40,8 @@ contract DEX { // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as // a liquidity tracking contract. This contract would be an admin of the liquidity contract. - // TODO: Either deploy the liquidity contract in the constructor or verify it that it corresponds to what this DEX - // expects. + // TODO: Either deploy the liquidity contract in the constructor or verify it that it corresponds to what + // this contract expects. #[public] #[initializer] fn constructor(token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress) { @@ -69,7 +69,7 @@ contract DEX { let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); - // We transfer the desired amounts of tokens to the DEX. + // We transfer the desired amounts of tokens to this contract. token0.transfer_to_public(msg.sender, context.this_address(), amount0_desired, nonce).call(&mut context); token1.transfer_to_public(msg.sender, context.this_address(), amount1_desired, nonce).call(&mut context); @@ -79,7 +79,7 @@ contract DEX { // We prepare a partial note for the liquidity tokens. let liquidity_slot_commitment = liquidity_token.prepare_transfer_to_private(msg.sender).call(&mut context); - DEX::at(context.this_address())._add_liquidity( + AMM::at(context.this_address())._add_liquidity( state, refund_token0_slot_commitment, refund_token1_slot_commitment, @@ -179,12 +179,12 @@ contract DEX { let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); - // We transfer the liquidity tokens to the DEX and prepare partial notes for the output tokens. + // We transfer the liquidity tokens to this contract and prepare partial notes for the output tokens. liquidity_token.transfer_to_public(msg.sender, context.this_address(), liquidity, nonce).call(&mut context); let token0_slot_commitment = token0.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); let token1_slot_commitment = token1.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); - DEX::at(context.this_address())._remove_liquidity( + AMM::at(context.this_address())._remove_liquidity( state, token0_slot_commitment, token1_slot_commitment, @@ -254,11 +254,11 @@ contract DEX { let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); - // We transfer the `amount_in` to the DEX and we prepare partial note for the output token. + // We transfer the `amount_in` to this contract and we prepare partial note for the output token. token_in.transfer_to_public(msg.sender, context.this_address(), amount_in, nonce).call(&mut context); let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); - DEX::at(context.this_address())._swap_exact_tokens_for_tokens( + AMM::at(context.this_address())._swap_exact_tokens_for_tokens( amount_in, amount_out_min, token_address_in, @@ -283,7 +283,7 @@ contract DEX { let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); - // We get the reserves. The `amount_in` was already transferred to the DEX so we need to subtract it. + // We get the reserves. The `amount_in` was already transferred to this contract so we need to subtract it. let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; let reserve_in = reserve_in_with_amount_in - amount_in; let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; @@ -317,12 +317,13 @@ contract DEX { let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); - // We transfer the `amount_in_max` to the DEX and we prepare partial notes for refund and for the output token. + // We transfer the `amount_in_max` to this contract and we prepare partial notes for refund and for the output + // token. token_in.transfer_to_public(msg.sender, context.this_address(), amount_in_max, nonce).call(&mut context); let refund_token_in_slot_commitment = token_in.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); - DEX::at(context.this_address())._swap_tokens_for_exact_tokens( + AMM::at(context.this_address())._swap_tokens_for_exact_tokens( refund_token_in_slot_commitment, token_out_slot_commitment, amount_out, @@ -349,7 +350,7 @@ contract DEX { let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); - // We get the reserves. The `amount_in_max` was already transferred to the DEX so we need to subtract it. + // We get the reserves. The `amount_in_max` was already transferred to this contract so we need to subtract it. let reserve_in_with_amount_in_max = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; let reserve_in = reserve_in_with_amount_in_max - amount_in_max; let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; From 5ee64b7756066120ed12a9fe685cd497773968f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Mon, 28 Oct 2024 10:48:12 -0600 Subject: [PATCH 43/67] Apply suggestions from code review --- .../contracts/amm_contract/src/main.nr | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 66a2d45a878..255bbe7abc9 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -3,7 +3,8 @@ mod lib; use dep::aztec::macros::aztec; /// This contract is a demonstration of how an Automated Market Maker (AMM) that requires public state while still -/// achieving identity privacy can be implemented. It does not, however, provide function privacy. +/// achieving identity privacy can be implemented. It does not, however, provide function privacy: anyone can see +/// what actions were performed and all amounts involved (but not _who_ performed the action). /// /// Note: This is only a demonstration and we (Aztec team) do not think this is the best way to build a DEX. #[aztec] @@ -73,7 +74,8 @@ contract AMM { token0.transfer_to_public(msg.sender, context.this_address(), amount0_desired, nonce).call(&mut context); token1.transfer_to_public(msg.sender, context.this_address(), amount1_desired, nonce).call(&mut context); - // Since not all the desired amounts of tokens might be accepted we prepare partial notes for the refunds. + // We may need to return some token amounts depending on public state (i.e. if the desired amounts do + // not have the same ratio as the live reserves), so we prepare partial notes for the refunds. let refund_token0_slot_commitment = token0.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); let refund_token1_slot_commitment = token1.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); // We prepare a partial note for the liquidity tokens. @@ -142,9 +144,8 @@ contract AMM { let refund_amount_token0 = amount0_desired - amount0; let refund_amount_token1 = amount1_desired - amount1; - // The refund does not need to be finalized if the refund amount is 0 --> the partial note will either be wiped - // out from transient storage at the end of the tx (which is fine) or it will stay in public storage (which is - // also fine). + // The refund does not need to be finalized if the refund amount is 0 --> the partial note will simply stay in + // public storage, which is fine. if (refund_amount_token0 > 0) { token0.finalize_transfer_to_private(refund_token0_slot_commitment, refund_amount_token0).call(&mut context); } @@ -179,7 +180,9 @@ contract AMM { let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); - // We transfer the liquidity tokens to this contract and prepare partial notes for the output tokens. + // We transfer the liquidity tokens to this contract and prepare partial notes for the output tokens. We are + // forced to first transfer into the AMM because it is not possible to burn in private - the enqueued public + // call would reveal who the owner was. The only way to preserve their identity is to first privately transfer. liquidity_token.transfer_to_public(msg.sender, context.this_address(), liquidity, nonce).call(&mut context); let token0_slot_commitment = token0.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); let token1_slot_commitment = token1.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); From 9e0eaf550021e014420bf7af3c45dba27d28a0d7 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 17:02:49 +0000 Subject: [PATCH 44/67] linking issue --- .../contracts/amm_contract/src/main.nr | 230 +++++++++++------- 1 file changed, 144 insertions(+), 86 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 255bbe7abc9..56f475ad762 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -3,19 +3,24 @@ mod lib; use dep::aztec::macros::aztec; /// This contract is a demonstration of how an Automated Market Maker (AMM) that requires public state while still -/// achieving identity privacy can be implemented. It does not, however, provide function privacy: anyone can see +/// achieving identity privacy can be implemented. It does not, however, provide function privacy: anyone can see /// what actions were performed and all amounts involved (but not _who_ performed the action). /// /// Note: This is only a demonstration and we (Aztec team) do not think this is the best way to build a DEX. #[aztec] contract AMM { - use crate::lib::{get_quote, get_amount_out, get_amount_in}; + use crate::lib::{get_amount_in, get_amount_out, get_quote}; use dep::aztec::{ - macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}}, - prelude::{AztecAddress, SharedImmutable}, protocol_types::traits::Serialize + macros::{ + events::event, + functions::{initializer, internal, private, public, view}, + storage::storage, + }, + prelude::{AztecAddress, SharedImmutable}, + protocol_types::traits::Serialize, }; - use std::meta::derive; use dep::token::Token; + use std::meta::derive; /// We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single /// merkle proof. @@ -41,7 +46,7 @@ contract AMM { // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as // a liquidity tracking contract. This contract would be an admin of the liquidity contract. - // TODO: Either deploy the liquidity contract in the constructor or verify it that it corresponds to what + // TODO(#9480): Either deploy the liquidity contract in the constructor or verify it that it corresponds to what // this contract expects. #[public] #[initializer] @@ -59,7 +64,7 @@ contract AMM { amount1_desired: u64, amount0_min: u64, amount1_min: u64, - nonce: Field + nonce: Field, ) { // TODO: Do we need reentrancy guards in the private funcs? And if yes how to do it? assert(amount0_desired > 0 & amount1_desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); @@ -71,26 +76,37 @@ contract AMM { let liquidity_token = Token::at(state.liquidity_token); // We transfer the desired amounts of tokens to this contract. - token0.transfer_to_public(msg.sender, context.this_address(), amount0_desired, nonce).call(&mut context); - token1.transfer_to_public(msg.sender, context.this_address(), amount1_desired, nonce).call(&mut context); - - // We may need to return some token amounts depending on public state (i.e. if the desired amounts do + token0.transfer_to_public(msg.sender, context.this_address(), amount0_desired, nonce).call( + &mut context, + ); + token1.transfer_to_public(msg.sender, context.this_address(), amount1_desired, nonce).call( + &mut context, + ); + + // We may need to return some token amounts depending on public state (i.e. if the desired amounts do // not have the same ratio as the live reserves), so we prepare partial notes for the refunds. - let refund_token0_slot_commitment = token0.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); - let refund_token1_slot_commitment = token1.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); + let refund_token0_slot_commitment = token0 + .prepare_transfer_to_private(msg.sender, context.this_address(), nonce) + .call(&mut context); + let refund_token1_slot_commitment = token1 + .prepare_transfer_to_private(msg.sender, context.this_address(), nonce) + .call(&mut context); // We prepare a partial note for the liquidity tokens. - let liquidity_slot_commitment = liquidity_token.prepare_transfer_to_private(msg.sender).call(&mut context); - - AMM::at(context.this_address())._add_liquidity( - state, - refund_token0_slot_commitment, - refund_token1_slot_commitment, - liquidity_slot_commitment, - amount0_desired, - amount1_desired, - amount0_min, - amount1_min - ).enqueue(&mut context); + let liquidity_slot_commitment = + liquidity_token.prepare_transfer_to_private(msg.sender).call(&mut context); + + AMM::at(context.this_address()) + ._add_liquidity( + state, + refund_token0_slot_commitment, + refund_token1_slot_commitment, + liquidity_slot_commitment, + amount0_desired, + amount1_desired, + amount0_min, + amount1_min, + ) + .enqueue(&mut context); } #[public] @@ -104,7 +120,7 @@ contract AMM { amount0_desired: u64, amount1_desired: u64, amount0_min: u64, - amount1_min: u64 + amount1_min: u64, ) { // We don't need any kind of reentrancy guard here because the only way to enter this public function is from // `add_liquidity` which is private and since public functions cannot call private ones it's impossible to @@ -113,8 +129,10 @@ contract AMM { let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); - let reserve0_with_amount0_desired = token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Avoid the need for these casts. - let reserve1_with_amount1_desired = token1.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve0_with_amount0_desired = + token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Avoid the need for these casts. + let reserve1_with_amount1_desired = + token1.balance_of_public(context.this_address()).view(&mut context) as u64; let reserve0 = reserve0_with_amount0_desired - amount0_desired; let reserve1 = reserve1_with_amount1_desired - amount1_desired; @@ -144,13 +162,17 @@ contract AMM { let refund_amount_token0 = amount0_desired - amount0; let refund_amount_token1 = amount1_desired - amount1; - // The refund does not need to be finalized if the refund amount is 0 --> the partial note will simply stay in + // The refund does not need to be finalized if the refund amount is 0 --> the partial note will simply stay in // public storage, which is fine. if (refund_amount_token0 > 0) { - token0.finalize_transfer_to_private(refund_token0_slot_commitment, refund_amount_token0).call(&mut context); + token0 + .finalize_transfer_to_private(refund_token0_slot_commitment, refund_amount_token0) + .call(&mut context); } if (refund_amount_token1 > 0) { - token1.finalize_transfer_to_private(refund_token1_slot_commitment, refund_amount_token1).call(&mut context); + token1 + .finalize_transfer_to_private(refund_token1_slot_commitment, refund_amount_token1) + .call(&mut context); } // Calculate the amount of liquidity tokens to mint @@ -160,12 +182,19 @@ contract AMM { // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? // TODO: avoid the casts here. Shall we use a method natively working with some integer type? liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u64; - liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens + liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call( + &mut context, + ); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { - liquidity = std::cmp::min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1); + liquidity = std::cmp::min( + amount0 * total_supply / reserve0, + amount1 * total_supply / reserve1, + ); } assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); - liquidity_token.finalize_mint_to_private(liquidity_slot_commitment, liquidity).call(&mut context); + liquidity_token.finalize_mint_to_private(liquidity_slot_commitment, liquidity).call( + &mut context, + ); } /// Removes `liquidity` from the pool and transfers the tokens back to the user. `amount0_min` and `amount1_min` are @@ -180,21 +209,29 @@ contract AMM { let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); - // We transfer the liquidity tokens to this contract and prepare partial notes for the output tokens. We are - // forced to first transfer into the AMM because it is not possible to burn in private - the enqueued public - // call would reveal who the owner was. The only way to preserve their identity is to first privately transfer. - liquidity_token.transfer_to_public(msg.sender, context.this_address(), liquidity, nonce).call(&mut context); - let token0_slot_commitment = token0.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); - let token1_slot_commitment = token1.prepare_transfer_to_private(context.this_address(), msg.sender, nonce).call(&mut context); - - AMM::at(context.this_address())._remove_liquidity( - state, - token0_slot_commitment, - token1_slot_commitment, - liquidity, - amount0_min, - amount1_min - ).enqueue(&mut context); + // We transfer the liquidity tokens to this contract and prepare partial notes for the output tokens. We are + // forced to first transfer into the AMM because it is not possible to burn in private - the enqueued public + // call would reveal who the owner was. The only way to preserve their identity is to first privately transfer. + liquidity_token + .transfer_to_public(msg.sender, context.this_address(), liquidity, nonce) + .call(&mut context); + let token0_slot_commitment = token0 + .prepare_transfer_to_private(context.this_address(), msg.sender, nonce) + .call(&mut context); + let token1_slot_commitment = token1 + .prepare_transfer_to_private(context.this_address(), msg.sender, nonce) + .call(&mut context); + + AMM::at(context.this_address()) + ._remove_liquidity( + state, + token0_slot_commitment, + token1_slot_commitment, + liquidity, + amount0_min, + amount1_min, + ) + .enqueue(&mut context); } #[public] @@ -206,12 +243,11 @@ contract AMM { token1_slot_commitment: Field, liquidity: u64, amount0_min: u64, - amount1_min: u64 + amount1_min: u64, ) { // We don't need any kind of reentrancy guard here because the only way to enter this public function is from // `remove_liquidity` which is private and since public functions cannot call private ones it's impossible to // reenter this function. - let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); @@ -244,7 +280,7 @@ contract AMM { amount_in: u64, amount_out_min: u64, from_0_to_1: bool, - nonce: Field + nonce: Field, ) { let state = storage.state.read_private(); @@ -258,16 +294,22 @@ contract AMM { let token_out = Token::at(token_address_out); // We transfer the `amount_in` to this contract and we prepare partial note for the output token. - token_in.transfer_to_public(msg.sender, context.this_address(), amount_in, nonce).call(&mut context); - let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); - - AMM::at(context.this_address())._swap_exact_tokens_for_tokens( - amount_in, - amount_out_min, - token_address_in, - token_address_out, - token_out_slot_commitment - ).enqueue(&mut context); + token_in.transfer_to_public(msg.sender, context.this_address(), amount_in, nonce).call( + &mut context, + ); + let token_out_slot_commitment = token_out + .prepare_transfer_to_private(context.this_address(), msg.sender) + .call(&mut context); + + AMM::at(context.this_address()) + ._swap_exact_tokens_for_tokens( + amount_in, + amount_out_min, + token_address_in, + token_address_out, + token_out_slot_commitment, + ) + .enqueue(&mut context); } #[public] @@ -277,26 +319,29 @@ contract AMM { amount_out_min: u64, token_address_in: AztecAddress, token_address_out: AztecAddress, - token_out_slot_commitment: Field + token_out_slot_commitment: Field, ) { // We don't need any kind of reentrancy guard here because the only way to enter this public function is from // `swap_exact_tokens_for_tokens` which is private and since public functions cannot call private ones // it's impossible to reenter this function. - let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); // We get the reserves. The `amount_in` was already transferred to this contract so we need to subtract it. - let reserve_in_with_amount_in = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_in_with_amount_in = + token_in.balance_of_public(context.this_address()).view(&mut context) as u64; let reserve_in = reserve_in_with_amount_in - amount_in; - let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_out = + token_out.balance_of_public(context.this_address()).view(&mut context) as u64; // Calculate the amount of output token we will get. let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); // Transfer the output token to the user. - token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); + token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( + &mut context, + ); } /// Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates @@ -307,7 +352,7 @@ contract AMM { amount_out: u64, amount_in_max: u64, from_0_to_1: bool, - nonce: Field + nonce: Field, ) { let state = storage.state.read_private(); @@ -322,18 +367,26 @@ contract AMM { // We transfer the `amount_in_max` to this contract and we prepare partial notes for refund and for the output // token. - token_in.transfer_to_public(msg.sender, context.this_address(), amount_in_max, nonce).call(&mut context); - let refund_token_in_slot_commitment = token_in.prepare_transfer_to_private(msg.sender, context.this_address(), nonce).call(&mut context); - let token_out_slot_commitment = token_out.prepare_transfer_to_private(context.this_address(), msg.sender).call(&mut context); - - AMM::at(context.this_address())._swap_tokens_for_exact_tokens( - refund_token_in_slot_commitment, - token_out_slot_commitment, - amount_out, - amount_in_max, - token_address_in, - token_address_out - ).enqueue(&mut context); + token_in.transfer_to_public(msg.sender, context.this_address(), amount_in_max, nonce).call( + &mut context, + ); + let refund_token_in_slot_commitment = token_in + .prepare_transfer_to_private(msg.sender, context.this_address(), nonce) + .call(&mut context); + let token_out_slot_commitment = token_out + .prepare_transfer_to_private(context.this_address(), msg.sender) + .call(&mut context); + + AMM::at(context.this_address()) + ._swap_tokens_for_exact_tokens( + refund_token_in_slot_commitment, + token_out_slot_commitment, + amount_out, + amount_in_max, + token_address_in, + token_address_out, + ) + .enqueue(&mut context); } #[public] @@ -344,19 +397,20 @@ contract AMM { amount_out: u64, amount_in_max: u64, token_address_in: AztecAddress, - token_address_out: AztecAddress + token_address_out: AztecAddress, ) { // We don't need any kind of reentrancy guard here because the only way to enter this public function is from // `swap_tokens_for_exact_tokens` which is private and since public functions cannot call private ones // it's impossible to reenter this function. - let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); // We get the reserves. The `amount_in_max` was already transferred to this contract so we need to subtract it. - let reserve_in_with_amount_in_max = token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_in_with_amount_in_max = + token_in.balance_of_public(context.this_address()).view(&mut context) as u64; let reserve_in = reserve_in_with_amount_in_max - amount_in_max; - let reserve_out = token_out.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_out = + token_out.balance_of_public(context.this_address()).view(&mut context) as u64; // Calculate the amount of input token needed to get the desired amount of output token. let amount_in = get_amount_in(amount_out, reserve_in, reserve_out); @@ -365,10 +419,14 @@ contract AMM { // If less than amount_in_max of input token was needed we refund the difference. let refund_amount = amount_in_max - amount_in; if (refund_amount > 0) { - token_in.finalize_transfer_to_private(refund_token_in_slot_commitment, refund_amount).call(&mut context); + token_in + .finalize_transfer_to_private(refund_token_in_slot_commitment, refund_amount) + .call(&mut context); } // Transfer the output token to the user. - token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call(&mut context); + token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( + &mut context, + ); } } From 59b002537b06e87ddbd6d16e150fd534eff7b1ab Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 17:38:28 +0000 Subject: [PATCH 45/67] improved re-entrancy guard comments --- .../contracts/amm_contract/src/main.nr | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 56f475ad762..ddd913f6818 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -2,11 +2,28 @@ mod lib; use dep::aztec::macros::aztec; -/// This contract is a demonstration of how an Automated Market Maker (AMM) that requires public state while still -/// achieving identity privacy can be implemented. It does not, however, provide function privacy: anyone can see -/// what actions were performed and all amounts involved (but not _who_ performed the action). +/// ## Overview +/// This contract demonstrates how to implement an **Automated Market Maker (AMM)** that maintains **public state** +/// while still achieving **identity privacy**. However, it does **not provide function privacy**: +/// - Anyone can observe **what actions** were performed. +/// - All amounts involved are visible, but **who** performed the action remains private. /// -/// Note: This is only a demonstration and we (Aztec team) do not think this is the best way to build a DEX. +/// **Note:** +/// This is purely a demonstration. The **Aztec team** does not consider this the optimal design for building a DEX. +/// +/// ## Reentrancy Guard Considerations +/// +/// ### 1. Private Functions: +/// Reentrancy protection is typically necessary if entering an intermediate state that is only valid when +/// the action completes uninterrupted. This follows the **Checks-Effects-Interactions** pattern. +/// +/// - In this contract, **private functions** do not introduce intermediate states. +/// - All operations will be fully executed in **public** without needing intermediate checks. +/// +/// ### 2. Public Functions: +/// No **reentrancy guard** is required for public functions because: +/// - All public functions are marked as **internal** with a **single callsite** - from a private function. +/// - Public functions **cannot call private functions**, eliminating the risk of reentrancy into them. #[aztec] contract AMM { use crate::lib::{get_amount_in, get_amount_out, get_quote}; @@ -66,7 +83,6 @@ contract AMM { amount1_min: u64, nonce: Field, ) { - // TODO: Do we need reentrancy guards in the private funcs? And if yes how to do it? assert(amount0_desired > 0 & amount1_desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); let state = storage.state.read_private(); @@ -202,7 +218,6 @@ contract AMM { /// value and its purpose is to isolate authwits to this specific call. #[private] fn remove_liquidity(liquidity: u64, amount0_min: u64, amount1_min: u64, nonce: Field) { - // TODO: Do we need reentrancy guards in the private funcs? And if yes how to do it? let state = storage.state.read_private(); let liquidity_token = Token::at(state.liquidity_token); @@ -245,9 +260,6 @@ contract AMM { amount0_min: u64, amount1_min: u64, ) { - // We don't need any kind of reentrancy guard here because the only way to enter this public function is from - // `remove_liquidity` which is private and since public functions cannot call private ones it's impossible to - // reenter this function. let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); @@ -321,9 +333,6 @@ contract AMM { token_address_out: AztecAddress, token_out_slot_commitment: Field, ) { - // We don't need any kind of reentrancy guard here because the only way to enter this public function is from - // `swap_exact_tokens_for_tokens` which is private and since public functions cannot call private ones - // it's impossible to reenter this function. let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); @@ -399,9 +408,6 @@ contract AMM { token_address_in: AztecAddress, token_address_out: AztecAddress, ) { - // We don't need any kind of reentrancy guard here because the only way to enter this public function is from - // `swap_tokens_for_exact_tokens` which is private and since public functions cannot call private ones - // it's impossible to reenter this function. let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); From dad820ba3df6dc0bac625bb8cb45d76c7e10444f Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 18:17:52 +0000 Subject: [PATCH 46/67] U128 instead of u64 --- .../contracts/amm_contract/src/lib.nr | 28 +-- .../contracts/amm_contract/src/main.nr | 168 ++++++++++++------ 2 files changed, 125 insertions(+), 71 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr index 69be7409718..4a5933d7278 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr @@ -1,28 +1,28 @@ // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset // copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 -pub fn get_quote(amountA: u64, reserveA: u64, reserveB: u64) -> u64 { - assert(amountA > 0, "INSUFFICIENT_AMOUNT"); - assert((reserveA > 0) & (reserveB > 0), "INSUFFICIENT_LIQUIDITY"); +pub fn get_quote(amountA: U128, reserveA: U128, reserveB: U128) -> U128 { + assert(amountA > U128::zero(), "INSUFFICIENT_AMOUNT"); + assert((reserveA > U128::zero()) & (reserveB > U128::zero()), "INSUFFICIENT_LIQUIDITY"); (amountA * reserveB) / reserveA } // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset // copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L43 -pub fn get_amount_out(amount_in: u64, reserve_in: u64, reserve_out: u64) -> u64 { - assert(amount_in > 0, "INSUFFICIENT_INPUT_AMOUNT"); - assert((reserve_in > 0) & (reserve_out > 0), "INSUFFICIENT_LIQUIDITY"); - let amount_in_with_fee = amount_in * 997; +pub fn get_amount_out(amount_in: U128, reserve_in: U128, reserve_out: U128) -> U128 { + assert(amount_in > U128::zero(), "INSUFFICIENT_INPUT_AMOUNT"); + assert((reserve_in > U128::zero()) & (reserve_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); + let amount_in_with_fee = amount_in * U128::from_integer(997); let numerator = amount_in_with_fee * reserve_out; - let denominator = reserve_in * 1000 + amount_in_with_fee; + let denominator = reserve_in * U128::from_integer(1000) + amount_in_with_fee; numerator / denominator } // given an output amount of an asset and pair reserves, returns a required input amount of the other asset // copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L53 -pub fn get_amount_in(amount_out: u64, reserve_in: u64, reserve_out: u64) -> u64 { - assert(amount_out > 0, "INSUFFICIENT_OUTPUT_AMOUNT"); - assert((reserve_in > 0) & (reserve_out > 0), "INSUFFICIENT_LIQUIDITY"); - let numerator = reserve_in * amount_out * 1000; - let denominator = (reserve_out - amount_out) * 997; - (numerator / denominator) + 1 +pub fn get_amount_in(amount_out: U128, reserve_in: U128, reserve_out: U128) -> U128 { + assert(amount_out > U128::zero(), "INSUFFICIENT_OUTPUT_AMOUNT"); + assert((reserve_in > U128::zero()) & (reserve_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); + let numerator = reserve_in * amount_out * U128::from_integer(1000); + let denominator = (reserve_out - amount_out) * U128::from_integer(997); + (numerator / denominator) + U128::from_integer(1) } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index ddd913f6818..183aaa593e4 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -59,7 +59,7 @@ contract AMM { /// Amount of liquidity which gets locked in the pool when liquidity is provided for the first time. It's purpose /// is to prevent the pool from ever emptying which could lead to undefined behavior. - global MINIMUM_LIQUIDITY: u64 = 1000; + global MINIMUM_LIQUIDITY = U128::from_integer(1000); // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as // a liquidity tracking contract. This contract would be an admin of the liquidity contract. @@ -77,13 +77,22 @@ contract AMM { /// isolate authwits to this specific call. #[private] fn add_liquidity( - amount0_desired: u64, - amount1_desired: u64, - amount0_min: u64, - amount1_min: u64, + amount0_desired: Field, + amount1_desired: Field, + amount0_min: Field, + amount1_min: Field, nonce: Field, ) { - assert(amount0_desired > 0 & amount1_desired > 0, "INSUFFICIENT_INPUT_AMOUNTS"); + // TODO(#8271): Type the args as U128 and nuke these ugly casts + let amount0_desired = U128::from_integer(amount0_desired); + let amount1_desired = U128::from_integer(amount1_desired); + let amount0_min = U128::from_integer(amount0_min); + let amount1_min = U128::from_integer(amount1_min); + + assert( + amount0_desired > U128::zero() & amount1_desired > U128::zero(), + "INSUFFICIENT_INPUT_AMOUNTS", + ); let state = storage.state.read_private(); @@ -92,12 +101,22 @@ contract AMM { let liquidity_token = Token::at(state.liquidity_token); // We transfer the desired amounts of tokens to this contract. - token0.transfer_to_public(msg.sender, context.this_address(), amount0_desired, nonce).call( - &mut context, - ); - token1.transfer_to_public(msg.sender, context.this_address(), amount1_desired, nonce).call( - &mut context, - ); + token0 + .transfer_to_public( + msg.sender, + context.this_address(), + amount0_desired.to_integer(), + nonce, + ) + .call(&mut context); + token1 + .transfer_to_public( + msg.sender, + context.this_address(), + amount1_desired.to_integer(), + nonce, + ) + .call(&mut context); // We may need to return some token amounts depending on public state (i.e. if the desired amounts do // not have the same ratio as the live reserves), so we prepare partial notes for the refunds. @@ -117,10 +136,10 @@ contract AMM { refund_token0_slot_commitment, refund_token1_slot_commitment, liquidity_slot_commitment, - amount0_desired, - amount1_desired, - amount0_min, - amount1_min, + amount0_desired.to_integer(), + amount1_desired.to_integer(), + amount0_min.to_integer(), + amount1_min.to_integer(), ) .enqueue(&mut context); } @@ -133,11 +152,17 @@ contract AMM { refund_token0_slot_commitment: Field, refund_token1_slot_commitment: Field, liquidity_slot_commitment: Field, - amount0_desired: u64, - amount1_desired: u64, - amount0_min: u64, - amount1_min: u64, + amount0_desired: Field, + amount1_desired: Field, + amount0_min: Field, + amount1_min: Field, ) { + // TODO(#8271): Type the args as U128 and nuke these ugly casts + let amount0_desired = U128::from_integer(amount0_desired); + let amount1_desired = U128::from_integer(amount1_desired); + let amount0_min = U128::from_integer(amount0_min); + let amount1_min = U128::from_integer(amount1_min); + // We don't need any kind of reentrancy guard here because the only way to enter this public function is from // `add_liquidity` which is private and since public functions cannot call private ones it's impossible to // reenter this function. @@ -145,10 +170,12 @@ contract AMM { let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); - let reserve0_with_amount0_desired = - token0.balance_of_public(context.this_address()).view(&mut context) as u64; // TODO: Avoid the need for these casts. - let reserve1_with_amount1_desired = - token1.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve0_with_amount0_desired = U128::from_integer(token0 + .balance_of_public(context.this_address()) + .view(&mut context)); + let reserve1_with_amount1_desired = U128::from_integer(token1 + .balance_of_public(context.this_address()) + .view(&mut context)); let reserve0 = reserve0_with_amount0_desired - amount0_desired; let reserve1 = reserve1_with_amount1_desired - amount1_desired; @@ -180,25 +207,25 @@ contract AMM { // The refund does not need to be finalized if the refund amount is 0 --> the partial note will simply stay in // public storage, which is fine. - if (refund_amount_token0 > 0) { + if (refund_amount_token0 > U128::zero()) { token0 .finalize_transfer_to_private(refund_token0_slot_commitment, refund_amount_token0) .call(&mut context); } - if (refund_amount_token1 > 0) { + if (refund_amount_token1 > U128::zero()) { token1 .finalize_transfer_to_private(refund_token1_slot_commitment, refund_amount_token1) .call(&mut context); } // Calculate the amount of liquidity tokens to mint - let total_supply = liquidity_token.total_supply().view(&mut context) as u64; - let mut liquidity: u64 = 0; - if (total_supply == 0) { + let total_supply = U128::from_integer(liquidity_token.total_supply().view(&mut context)); + let mut liquidity = U128::zero(); + if (total_supply == U128::zero()) { // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? - // TODO: avoid the casts here. Shall we use a method natively working with some integer type? - liquidity = std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) as Field) as u64; - liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY as Field).call( + liquidity = U128::from_integer(std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) + .to_integer())); + liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY.to_integer()).call( &mut context, ); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { @@ -207,7 +234,7 @@ contract AMM { amount1 * total_supply / reserve1, ); } - assert(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); + assert(liquidity > U128::zero(), "INSUFFICIENT_LIQUIDITY_MINTED"); liquidity_token.finalize_mint_to_private(liquidity_slot_commitment, liquidity).call( &mut context, ); @@ -216,8 +243,9 @@ contract AMM { /// Removes `liquidity` from the pool and transfers the tokens back to the user. `amount0_min` and `amount1_min` are /// the minimum amounts of `token0` and `token1` the user is willing to accept. `nonce` can be arbitrary non-zero /// value and its purpose is to isolate authwits to this specific call. + /// TODO(#8271): Type the args as U128 #[private] - fn remove_liquidity(liquidity: u64, amount0_min: u64, amount1_min: u64, nonce: Field) { + fn remove_liquidity(liquidity: Field, amount0_min: Field, amount1_min: Field, nonce: Field) { let state = storage.state.read_private(); let liquidity_token = Token::at(state.liquidity_token); @@ -256,18 +284,27 @@ contract AMM { state: State, token0_slot_commitment: Field, token1_slot_commitment: Field, - liquidity: u64, - amount0_min: u64, - amount1_min: u64, + liquidity: Field, + amount0_min: Field, + amount1_min: Field, ) { + // TODO(#8271): Type the args as U128 and nuke these ugly casts + let liquidity = U128::from_integer(liquidity); + let amount0_min = U128::from_integer(amount0_min); + let amount1_min = U128::from_integer(amount1_min); + let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); // We get the reserves and the liquidity token total supply. - let reserve0 = token0.balance_of_public(context.this_address()).view(&mut context) as u64; - let reserve1 = token1.balance_of_public(context.this_address()).view(&mut context) as u64; - let total_supply = liquidity_token.total_supply().view(&mut context) as u64; + let reserve0 = U128::from_integer(token0.balance_of_public(context.this_address()).view( + &mut context, + )); + let reserve1 = U128::from_integer(token1.balance_of_public(context.this_address()).view( + &mut context, + )); + let total_supply = U128::from_integer(liquidity_token.total_supply().view(&mut context)); // We calculate the amounts of token0 and token1 the user is entitled to based on the amount of liquidity they // are removing. @@ -287,10 +324,11 @@ contract AMM { /// Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates /// whether we are swapping `token0` for `token1` or vice versa. `nonce` can be arbitrary non-zero value and its /// purpose is to isolate authwits to this specific call. + /// TODO(#8271): Type the args as U128 #[private] fn swap_exact_tokens_for_tokens( - amount_in: u64, - amount_out_min: u64, + amount_in: Field, + amount_out_min: Field, from_0_to_1: bool, nonce: Field, ) { @@ -327,21 +365,27 @@ contract AMM { #[public] #[internal] fn _swap_exact_tokens_for_tokens( - amount_in: u64, - amount_out_min: u64, + amount_in: Field, + amount_out_min: Field, token_address_in: AztecAddress, token_address_out: AztecAddress, token_out_slot_commitment: Field, ) { + // TODO(#8271): Type the args as U128 and nuke these ugly casts + let amount_in = U128::from_integer(amount_in); + let amount_out_min = U128::from_integer(amount_out_min); + let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); // We get the reserves. The `amount_in` was already transferred to this contract so we need to subtract it. - let reserve_in_with_amount_in = - token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_in_with_amount_in = U128::from_integer(token_in + .balance_of_public(context.this_address()) + .view(&mut context)); let reserve_in = reserve_in_with_amount_in - amount_in; - let reserve_out = - token_out.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_out = U128::from_integer(token_out + .balance_of_public(context.this_address()) + .view(&mut context)); // Calculate the amount of output token we will get. let amount_out = get_amount_out(amount_in, reserve_in, reserve_out); @@ -358,11 +402,15 @@ contract AMM { /// purpose is to isolate authwits to this specific call. #[private] fn swap_tokens_for_exact_tokens( - amount_out: u64, - amount_in_max: u64, + amount_out: Field, + amount_in_max: Field, from_0_to_1: bool, nonce: Field, ) { + // TODO(#8271): Type the args as U128 and nuke these ugly casts + let amount_out = U128::from_integer(amount_out); + let amount_in_max = U128::from_integer(amount_in_max); + let state = storage.state.read_private(); let (token_address_in, token_address_out) = if from_0_to_1 { @@ -403,20 +451,26 @@ contract AMM { fn _swap_tokens_for_exact_tokens( refund_token_in_slot_commitment: Field, token_out_slot_commitment: Field, - amount_out: u64, - amount_in_max: u64, + amount_out: Field, + amount_in_max: Field, token_address_in: AztecAddress, token_address_out: AztecAddress, ) { + // TODO(#8271): Type the args as U128 and nuke these ugly casts + let amount_out = U128::from_integer(amount_out); + let amount_in_max = U128::from_integer(amount_in_max); + let token_in = Token::at(token_address_in); let token_out = Token::at(token_address_out); // We get the reserves. The `amount_in_max` was already transferred to this contract so we need to subtract it. - let reserve_in_with_amount_in_max = - token_in.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_in_with_amount_in_max = U128::from_integer(token_in + .balance_of_public(context.this_address()) + .view(&mut context)); let reserve_in = reserve_in_with_amount_in_max - amount_in_max; - let reserve_out = - token_out.balance_of_public(context.this_address()).view(&mut context) as u64; + let reserve_out = U128::from_integer(token_out + .balance_of_public(context.this_address()) + .view(&mut context)); // Calculate the amount of input token needed to get the desired amount of output token. let amount_in = get_amount_in(amount_out, reserve_in, reserve_out); @@ -424,7 +478,7 @@ contract AMM { // If less than amount_in_max of input token was needed we refund the difference. let refund_amount = amount_in_max - amount_in; - if (refund_amount > 0) { + if (refund_amount > U128::zero()) { token_in .finalize_transfer_to_private(refund_token_in_slot_commitment, refund_amount) .call(&mut context); From f7525fdc79e1462626b9e33bfae1fa47c83b367c Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 18:24:04 +0000 Subject: [PATCH 47/67] fix --- noir-projects/noir-contracts/contracts/amm_contract/src/main.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 183aaa593e4..054ecb0c556 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -183,7 +183,7 @@ contract AMM { // Calculate the amounts to be added to the pool let mut amount0 = amount0_desired; let mut amount1 = amount1_desired; - if ((reserve0 != 0) | (reserve1 != 0)) { + if ((reserve0 != U128::zero()) | (reserve1 != U128::zero())) { // First calculate the optimal amount of token1 based on the desired amount of token0. let amount1_optimal = get_quote(amount0_desired, reserve0, reserve1); if (amount1_optimal <= amount1_desired) { From 844ce57104aff893721fac439687cd3babbddf1f Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 18:41:49 +0000 Subject: [PATCH 48/67] clarifying re-entrancy comment --- .../noir-contracts/contracts/amm_contract/src/main.nr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 054ecb0c556..b5286754b0d 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -23,7 +23,10 @@ use dep::aztec::macros::aztec; /// ### 2. Public Functions: /// No **reentrancy guard** is required for public functions because: /// - All public functions are marked as **internal** with a **single callsite** - from a private function. -/// - Public functions **cannot call private functions**, eliminating the risk of reentrancy into them. +/// - Public functions **cannot call private functions**, eliminating the risk of reentering into them from private. +/// - Since public functions are internal-only, **external contracts cannot access them**, ensuring no external +/// contract can trigger a reentrant call. This eliminates the following attack vector: +/// `AMM.private_fn --> AMM.public_fn --> ExternalContract.fn --> AMM.public_fn`. #[aztec] contract AMM { use crate::lib::{get_amount_in, get_amount_out, get_quote}; @@ -163,9 +166,6 @@ contract AMM { let amount0_min = U128::from_integer(amount0_min); let amount1_min = U128::from_integer(amount1_min); - // We don't need any kind of reentrancy guard here because the only way to enter this public function is from - // `add_liquidity` which is private and since public functions cannot call private ones it's impossible to - // reenter this function. let token0 = Token::at(state.token0); let token1 = Token::at(state.token1); let liquidity_token = Token::at(state.liquidity_token); From 6f9db4a8298ca73f14dff56d9e877de549f2460e Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 18:59:42 +0000 Subject: [PATCH 49/67] get_amounts_to_add --- .../contracts/amm_contract/src/lib.nr | 45 ++++++++++++++++--- .../contracts/amm_contract/src/main.nr | 30 ++++--------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr index 4a5933d7278..1f76e5ad131 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr @@ -1,13 +1,13 @@ -// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset -// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 +/// Given some amount of an asset and pair reserves, returns an equivalent amount of the other asset. +/// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 pub fn get_quote(amountA: U128, reserveA: U128, reserveB: U128) -> U128 { assert(amountA > U128::zero(), "INSUFFICIENT_AMOUNT"); assert((reserveA > U128::zero()) & (reserveB > U128::zero()), "INSUFFICIENT_LIQUIDITY"); (amountA * reserveB) / reserveA } -// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset -// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L43 +/// Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset. +/// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L43 pub fn get_amount_out(amount_in: U128, reserve_in: U128, reserve_out: U128) -> U128 { assert(amount_in > U128::zero(), "INSUFFICIENT_INPUT_AMOUNT"); assert((reserve_in > U128::zero()) & (reserve_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); @@ -17,8 +17,8 @@ pub fn get_amount_out(amount_in: U128, reserve_in: U128, reserve_out: U128) -> U numerator / denominator } -// given an output amount of an asset and pair reserves, returns a required input amount of the other asset -// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L53 +/// Given an output amount of an asset and pair reserves, returns a required input amount of the other asset. +/// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L53 pub fn get_amount_in(amount_out: U128, reserve_in: U128, reserve_out: U128) -> U128 { assert(amount_out > U128::zero(), "INSUFFICIENT_OUTPUT_AMOUNT"); assert((reserve_in > U128::zero()) & (reserve_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); @@ -26,3 +26,36 @@ pub fn get_amount_in(amount_out: U128, reserve_in: U128, reserve_out: U128) -> U let denominator = (reserve_out - amount_out) * U128::from_integer(997); (numerator / denominator) + U128::from_integer(1) } + +/// Given the desired amounts and reserves of token0 and token1 returns the optimal amount of token0 and token1 to be added to the pool. +pub fn get_amounts_to_add( + amount0_desired: U128, + amount1_desired: U128, + amount0_min: U128, + amount1_min: U128, + reserve0: U128, + reserve1: U128, +) -> (U128, U128) { + let mut amount0 = amount0_desired; + let mut amount1 = amount1_desired; + if ((reserve0 != U128::zero()) | (reserve1 != U128::zero())) { + // First calculate the optimal amount of token1 based on the desired amount of token0. + let amount1_optimal = get_quote(amount0_desired, reserve0, reserve1); + if (amount1_optimal <= amount1_desired) { + // Revert if the optimal amount of token1 is less than the desired amount of token1. + assert(amount1_optimal >= amount1_min, "INSUFFICIENT_1_AMOUNT"); + amount0 = amount0_desired; + amount1 = amount1_optimal; + } else { + // We got more amount of token1 than desired so we try repeating the process but this time by quoting + // based on token1. + let amount0_optimal = get_quote(amount1_desired, reserve1, reserve0); + assert(amount0_optimal <= amount0_desired); + assert(amount0_optimal >= amount0_min, "INSUFFICIENT_0_AMOUNT"); + amount0 = amount0_optimal; + amount1 = amount1_desired; + } + } + + (amount0, amount1) +} diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index b5286754b0d..eeb7ff9497f 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -29,7 +29,7 @@ use dep::aztec::macros::aztec; /// `AMM.private_fn --> AMM.public_fn --> ExternalContract.fn --> AMM.public_fn`. #[aztec] contract AMM { - use crate::lib::{get_amount_in, get_amount_out, get_quote}; + use crate::lib::{get_amount_in, get_amount_out, get_amounts_to_add, get_quote}; use dep::aztec::{ macros::{ events::event, @@ -181,26 +181,14 @@ contract AMM { let reserve1 = reserve1_with_amount1_desired - amount1_desired; // Calculate the amounts to be added to the pool - let mut amount0 = amount0_desired; - let mut amount1 = amount1_desired; - if ((reserve0 != U128::zero()) | (reserve1 != U128::zero())) { - // First calculate the optimal amount of token1 based on the desired amount of token0. - let amount1_optimal = get_quote(amount0_desired, reserve0, reserve1); - if (amount1_optimal <= amount1_desired) { - // Revert if the optimal amount of token1 is less than the desired amount of token1. - assert(amount1_optimal >= amount1_min, "INSUFFICIENT_1_AMOUNT"); - amount0 = amount0_desired; - amount1 = amount1_optimal; - } else { - // We got more amount of token1 than desired so we try repeating the process but this time by quoting - // based on token1. - let amount0_optimal = get_quote(amount1_desired, reserve1, reserve0); - assert(amount0_optimal <= amount0_desired); - assert(amount0_optimal >= amount0_min, "INSUFFICIENT_0_AMOUNT"); - amount0 = amount0_optimal; - amount1 = amount1_desired; - } - } + let (amount0, amount1) = get_amounts_to_add( + amount0_desired, + amount1_desired, + amount0_min, + amount1_min, + reserve0, + reserve1, + ); let refund_amount_token0 = amount0_desired - amount0; let refund_amount_token1 = amount1_desired - amount1; From a569a199702eb82a5b3d464351bcccb9c0cb7d75 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 19:17:30 +0000 Subject: [PATCH 50/67] various improvements --- .../contracts/amm_contract/src/lib.nr | 2 +- .../contracts/amm_contract/src/main.nr | 88 +++++++++---------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr index 1f76e5ad131..29faca8f887 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr @@ -1,6 +1,6 @@ /// Given some amount of an asset and pair reserves, returns an equivalent amount of the other asset. /// copy of https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 -pub fn get_quote(amountA: U128, reserveA: U128, reserveB: U128) -> U128 { +fn get_quote(amountA: U128, reserveA: U128, reserveB: U128) -> U128 { assert(amountA > U128::zero(), "INSUFFICIENT_AMOUNT"); assert((reserveA > U128::zero()) & (reserveB > U128::zero()), "INSUFFICIENT_LIQUIDITY"); (amountA * reserveB) / reserveA diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index eeb7ff9497f..a124bdc7b15 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -29,7 +29,7 @@ use dep::aztec::macros::aztec; /// `AMM.private_fn --> AMM.public_fn --> ExternalContract.fn --> AMM.public_fn`. #[aztec] contract AMM { - use crate::lib::{get_amount_in, get_amount_out, get_amounts_to_add, get_quote}; + use crate::lib::{get_amount_in, get_amount_out, get_amounts_to_add}; use dep::aztec::{ macros::{ events::event, @@ -315,36 +315,36 @@ contract AMM { /// TODO(#8271): Type the args as U128 #[private] fn swap_exact_tokens_for_tokens( + token_in: AztecAddress, + token_out: AztecAddress, amount_in: Field, amount_out_min: Field, - from_0_to_1: bool, nonce: Field, ) { let state = storage.state.read_private(); - let (token_address_in, token_address_out) = if from_0_to_1 { - (state.token0, state.token1) - } else { - (state.token1, state.token0) - }; + // We check the tokens are valid + assert(token_in != token_out); + assert(token_in == state.token0 | token_in == state.token1); + assert(token_out == state.token0 | token_out == state.token1); - let token_in = Token::at(token_address_in); - let token_out = Token::at(token_address_out); + let token_in_contract = Token::at(token_in); + let token_out_contract = Token::at(token_out); // We transfer the `amount_in` to this contract and we prepare partial note for the output token. - token_in.transfer_to_public(msg.sender, context.this_address(), amount_in, nonce).call( - &mut context, - ); - let token_out_slot_commitment = token_out + token_in_contract + .transfer_to_public(msg.sender, context.this_address(), amount_in, nonce) + .call(&mut context); + let token_out_slot_commitment = token_out_contract .prepare_transfer_to_private(context.this_address(), msg.sender) .call(&mut context); AMM::at(context.this_address()) ._swap_exact_tokens_for_tokens( + token_in, + token_out, amount_in, amount_out_min, - token_address_in, - token_address_out, token_out_slot_commitment, ) .enqueue(&mut context); @@ -353,25 +353,25 @@ contract AMM { #[public] #[internal] fn _swap_exact_tokens_for_tokens( + token_in: AztecAddress, + token_out: AztecAddress, amount_in: Field, amount_out_min: Field, - token_address_in: AztecAddress, - token_address_out: AztecAddress, token_out_slot_commitment: Field, ) { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount_in = U128::from_integer(amount_in); let amount_out_min = U128::from_integer(amount_out_min); - let token_in = Token::at(token_address_in); - let token_out = Token::at(token_address_out); + let token_in_contract = Token::at(token_in); + let token_out_contract = Token::at(token_out); // We get the reserves. The `amount_in` was already transferred to this contract so we need to subtract it. - let reserve_in_with_amount_in = U128::from_integer(token_in + let reserve_in_with_amount_in = U128::from_integer(token_in_contract .balance_of_public(context.this_address()) .view(&mut context)); let reserve_in = reserve_in_with_amount_in - amount_in; - let reserve_out = U128::from_integer(token_out + let reserve_out = U128::from_integer(token_out_contract .balance_of_public(context.this_address()) .view(&mut context)); @@ -380,7 +380,7 @@ contract AMM { assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); // Transfer the output token to the user. - token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( + token_out_contract.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( &mut context, ); } @@ -390,9 +390,10 @@ contract AMM { /// purpose is to isolate authwits to this specific call. #[private] fn swap_tokens_for_exact_tokens( + token_in: AztecAddress, + token_out: AztecAddress, amount_out: Field, amount_in_max: Field, - from_0_to_1: bool, nonce: Field, ) { // TODO(#8271): Type the args as U128 and nuke these ugly casts @@ -401,35 +402,34 @@ contract AMM { let state = storage.state.read_private(); - let (token_address_in, token_address_out) = if from_0_to_1 { - (state.token0, state.token1) - } else { - (state.token1, state.token0) - }; + // We check the tokens are valid + assert(token_in != token_out); + assert(token_in == state.token0 | token_in == state.token1); + assert(token_out == state.token0 | token_out == state.token1); - let token_in = Token::at(token_address_in); - let token_out = Token::at(token_address_out); + let token_in_contract = Token::at(token_in); + let token_out_contract = Token::at(token_out); // We transfer the `amount_in_max` to this contract and we prepare partial notes for refund and for the output // token. token_in.transfer_to_public(msg.sender, context.this_address(), amount_in_max, nonce).call( &mut context, ); - let refund_token_in_slot_commitment = token_in + let refund_token_in_slot_commitment = token_in_contract .prepare_transfer_to_private(msg.sender, context.this_address(), nonce) .call(&mut context); - let token_out_slot_commitment = token_out + let token_out_slot_commitment = token_out_contract .prepare_transfer_to_private(context.this_address(), msg.sender) .call(&mut context); AMM::at(context.this_address()) ._swap_tokens_for_exact_tokens( + token_in, + token_out, + amount_in_max.to_integer(), + amount_out.to_integer(), refund_token_in_slot_commitment, token_out_slot_commitment, - amount_out, - amount_in_max, - token_address_in, - token_address_out, ) .enqueue(&mut context); } @@ -437,19 +437,19 @@ contract AMM { #[public] #[internal] fn _swap_tokens_for_exact_tokens( + token_in: AztecAddress, + token_out: AztecAddress, + amount_in_max: Field, + amount_out: Field, refund_token_in_slot_commitment: Field, token_out_slot_commitment: Field, - amount_out: Field, - amount_in_max: Field, - token_address_in: AztecAddress, - token_address_out: AztecAddress, ) { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount_out = U128::from_integer(amount_out); let amount_in_max = U128::from_integer(amount_in_max); - let token_in = Token::at(token_address_in); - let token_out = Token::at(token_address_out); + let token_in_contract = Token::at(token_in); + let token_out_contract = Token::at(token_out); // We get the reserves. The `amount_in_max` was already transferred to this contract so we need to subtract it. let reserve_in_with_amount_in_max = U128::from_integer(token_in @@ -467,13 +467,13 @@ contract AMM { // If less than amount_in_max of input token was needed we refund the difference. let refund_amount = amount_in_max - amount_in; if (refund_amount > U128::zero()) { - token_in + token_in_contract .finalize_transfer_to_private(refund_token_in_slot_commitment, refund_amount) .call(&mut context); } // Transfer the output token to the user. - token_out.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( + token_out_contract.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( &mut context, ); } From 6973d4cd3cac8d103b5bdf8a9d936295346e012a Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 19:31:34 +0000 Subject: [PATCH 51/67] fix --- .../noir-contracts/contracts/amm_contract/src/main.nr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index a124bdc7b15..5dafc8aab59 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -211,8 +211,8 @@ contract AMM { let mut liquidity = U128::zero(); if (total_supply == U128::zero()) { // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? - liquidity = U128::from_integer(std::ec::sqrt((amount0 * amount1 - MINIMUM_LIQUIDITY) - .to_integer())); + liquidity = U128::from_integer(std::ec::sqrt((amount0 * amount1).to_integer())) + - MINIMUM_LIQUIDITY; liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY.to_integer()).call( &mut context, ); // permanently lock the first MINIMUM_LIQUIDITY tokens From a0cbd37bbd8ba73be3b89ea26ae5b55231514be4 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 28 Oct 2024 19:48:40 +0000 Subject: [PATCH 52/67] no sqrt in init liquidity --- .../noir-contracts/contracts/amm_contract/src/main.nr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 5dafc8aab59..50abfe03bf5 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -210,9 +210,10 @@ contract AMM { let total_supply = U128::from_integer(liquidity_token.total_supply().view(&mut context)); let mut liquidity = U128::zero(); if (total_supply == U128::zero()) { - // TODO: This is using Tonelli-Shanks to compute sqrt but Uni is using babylonian method. Is it fine to use a different one? - liquidity = U128::from_integer(std::ec::sqrt((amount0 * amount1).to_integer())) - - MINIMUM_LIQUIDITY; + // Since we don't collect a protocol fee (unlike Uniswap V2) we can set initial liquidity to an arbitrary + // value instead of sqrt(amount0 * amount1). We set it to 9 times the minimum liquidity. That way + // the initial depositor gets 90% of the value of his deposit. + liquidity = MINIMUM_LIQUIDITY * U128::from_integer(9); liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY.to_integer()).call( &mut context, ); // permanently lock the first MINIMUM_LIQUIDITY tokens From ba4f1bdf93e33837d7c77fb2410bc78779bb855b Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 30 Oct 2024 21:36:22 +0000 Subject: [PATCH 53/67] fixes --- .../contracts/amm_contract/src/main.nr | 136 +++++++++--------- .../contracts/amm_contract/src/state.nr | 29 ++++ 2 files changed, 93 insertions(+), 72 deletions(-) create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/state.nr diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 50abfe03bf5..932d4dbaf86 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -1,4 +1,5 @@ mod lib; +mod state; use dep::aztec::macros::aztec; @@ -29,7 +30,7 @@ use dep::aztec::macros::aztec; /// `AMM.private_fn --> AMM.public_fn --> ExternalContract.fn --> AMM.public_fn`. #[aztec] contract AMM { - use crate::lib::{get_amount_in, get_amount_out, get_amounts_to_add}; + use crate::{lib::{get_amount_in, get_amount_out, get_amounts_to_add}, state::State}; use dep::aztec::{ macros::{ events::event, @@ -37,20 +38,8 @@ contract AMM { storage::storage, }, prelude::{AztecAddress, SharedImmutable}, - protocol_types::traits::Serialize, }; use dep::token::Token; - use std::meta::derive; - - /// We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single - /// merkle proof. - /// (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). - #[derive(Serialize)] - struct State { - token0: AztecAddress, - token1: AztecAddress, - liquidity_token: AztecAddress, - } #[storage] struct Storage { @@ -106,7 +95,7 @@ contract AMM { // We transfer the desired amounts of tokens to this contract. token0 .transfer_to_public( - msg.sender, + context.msg_sender(), context.this_address(), amount0_desired.to_integer(), nonce, @@ -114,7 +103,7 @@ contract AMM { .call(&mut context); token1 .transfer_to_public( - msg.sender, + context.msg_sender(), context.this_address(), amount1_desired.to_integer(), nonce, @@ -123,22 +112,20 @@ contract AMM { // We may need to return some token amounts depending on public state (i.e. if the desired amounts do // not have the same ratio as the live reserves), so we prepare partial notes for the refunds. - let refund_token0_slot_commitment = token0 - .prepare_transfer_to_private(msg.sender, context.this_address(), nonce) - .call(&mut context); - let refund_token1_slot_commitment = token1 - .prepare_transfer_to_private(msg.sender, context.this_address(), nonce) - .call(&mut context); + let refund_token0_hiding_point_slot = + token0.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + let refund_token1_hiding_point_slot = + token1.prepare_transfer_to_private(context.msg_sender()).call(&mut context); // We prepare a partial note for the liquidity tokens. - let liquidity_slot_commitment = - liquidity_token.prepare_transfer_to_private(msg.sender).call(&mut context); + let liquidity_hiding_point_slot = + liquidity_token.prepare_transfer_to_private(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._add_liquidity( state, - refund_token0_slot_commitment, - refund_token1_slot_commitment, - liquidity_slot_commitment, + refund_token0_hiding_point_slot, + refund_token1_hiding_point_slot, + liquidity_hiding_point_slot, amount0_desired.to_integer(), amount1_desired.to_integer(), amount0_min.to_integer(), @@ -152,9 +139,9 @@ contract AMM { fn _add_liquidity( // We pass the state as an argument in order to not have to read it from public storage again. state: State, - refund_token0_slot_commitment: Field, - refund_token1_slot_commitment: Field, - liquidity_slot_commitment: Field, + refund_token0_hiding_point_slot: Field, + refund_token1_hiding_point_slot: Field, + liquidity_hiding_point_slot: Field, amount0_desired: Field, amount1_desired: Field, amount0_min: Field, @@ -197,12 +184,18 @@ contract AMM { // public storage, which is fine. if (refund_amount_token0 > U128::zero()) { token0 - .finalize_transfer_to_private(refund_token0_slot_commitment, refund_amount_token0) + .finalize_transfer_to_private( + refund_token0_hiding_point_slot, + refund_amount_token0.to_integer(), + ) .call(&mut context); } if (refund_amount_token1 > U128::zero()) { token1 - .finalize_transfer_to_private(refund_token1_slot_commitment, refund_amount_token1) + .finalize_transfer_to_private( + refund_token1_hiding_point_slot, + refund_amount_token1.to_integer(), + ) .call(&mut context); } @@ -224,7 +217,7 @@ contract AMM { ); } assert(liquidity > U128::zero(), "INSUFFICIENT_LIQUIDITY_MINTED"); - liquidity_token.finalize_mint_to_private(liquidity_slot_commitment, liquidity).call( + liquidity_token.finalize_mint_to_private(liquidity_hiding_point_slot, liquidity).call( &mut context, ); } @@ -245,20 +238,20 @@ contract AMM { // forced to first transfer into the AMM because it is not possible to burn in private - the enqueued public // call would reveal who the owner was. The only way to preserve their identity is to first privately transfer. liquidity_token - .transfer_to_public(msg.sender, context.this_address(), liquidity, nonce) + .transfer_to_public(context.msg_sender(), context.this_address(), liquidity, nonce) .call(&mut context); - let token0_slot_commitment = token0 - .prepare_transfer_to_private(context.this_address(), msg.sender, nonce) + let token0_hiding_point_slot = token0 + .prepare_transfer_to_private(context.this_address(), context.msg_sender(), nonce) .call(&mut context); - let token1_slot_commitment = token1 - .prepare_transfer_to_private(context.this_address(), msg.sender, nonce) + let token1_hiding_point_slot = token1 + .prepare_transfer_to_private(context.this_address(), context.msg_sender(), nonce) .call(&mut context); AMM::at(context.this_address()) ._remove_liquidity( state, - token0_slot_commitment, - token1_slot_commitment, + token0_hiding_point_slot, + token1_hiding_point_slot, liquidity, amount0_min, amount1_min, @@ -271,8 +264,8 @@ contract AMM { fn _remove_liquidity( // We pass the state as an argument in order to not have to read it from public storage again. state: State, - token0_slot_commitment: Field, - token1_slot_commitment: Field, + token0_hiding_point_slot: Field, + token1_hiding_point_slot: Field, liquidity: Field, amount0_min: Field, amount1_min: Field, @@ -306,8 +299,8 @@ contract AMM { // At last we burn the liquidity tokens and transfer the token0 and token1 to the user. liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); - token0.finalize_transfer_to_private(token0_slot_commitment, amount0).call(&mut context); - token1.finalize_transfer_to_private(token1_slot_commitment, amount1).call(&mut context); + token0.finalize_transfer_to_private(token0_hiding_point_slot, amount0).call(&mut context); + token1.finalize_transfer_to_private(token1_hiding_point_slot, amount1).call(&mut context); } /// Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates @@ -334,11 +327,10 @@ contract AMM { // We transfer the `amount_in` to this contract and we prepare partial note for the output token. token_in_contract - .transfer_to_public(msg.sender, context.this_address(), amount_in, nonce) - .call(&mut context); - let token_out_slot_commitment = token_out_contract - .prepare_transfer_to_private(context.this_address(), msg.sender) + .transfer_to_public(context.msg_sender(), context.this_address(), amount_in, nonce) .call(&mut context); + let token_out_hiding_point_slot = + token_out_contract.prepare_transfer_to_private(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._swap_exact_tokens_for_tokens( @@ -346,7 +338,7 @@ contract AMM { token_out, amount_in, amount_out_min, - token_out_slot_commitment, + token_out_hiding_point_slot, ) .enqueue(&mut context); } @@ -358,7 +350,7 @@ contract AMM { token_out: AztecAddress, amount_in: Field, amount_out_min: Field, - token_out_slot_commitment: Field, + token_out_hiding_point_slot: Field, ) { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount_in = U128::from_integer(amount_in); @@ -381,9 +373,9 @@ contract AMM { assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); // Transfer the output token to the user. - token_out_contract.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( - &mut context, - ); + token_out_contract + .finalize_transfer_to_private(token_out_hiding_point_slot, amount_out) + .call(&mut context); } /// Swaps `amount_out` of `token_out` for at most `amount_in_max` of `token_in`. The `from_0_to_1` flag indicates @@ -399,7 +391,6 @@ contract AMM { ) { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount_out = U128::from_integer(amount_out); - let amount_in_max = U128::from_integer(amount_in_max); let state = storage.state.read_private(); @@ -413,24 +404,22 @@ contract AMM { // We transfer the `amount_in_max` to this contract and we prepare partial notes for refund and for the output // token. - token_in.transfer_to_public(msg.sender, context.this_address(), amount_in_max, nonce).call( - &mut context, - ); - let refund_token_in_slot_commitment = token_in_contract - .prepare_transfer_to_private(msg.sender, context.this_address(), nonce) - .call(&mut context); - let token_out_slot_commitment = token_out_contract - .prepare_transfer_to_private(context.this_address(), msg.sender) + token_in_contract + .transfer_to_public(context.msg_sender(), context.this_address(), amount_in_max, nonce) .call(&mut context); + let refund_token_in_hiding_point_slot = + token_in_contract.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + let token_out_hiding_point_slot = + token_out_contract.prepare_transfer_to_private(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._swap_tokens_for_exact_tokens( token_in, token_out, - amount_in_max.to_integer(), + amount_in_max, amount_out.to_integer(), - refund_token_in_slot_commitment, - token_out_slot_commitment, + refund_token_in_hiding_point_slot, + token_out_hiding_point_slot, ) .enqueue(&mut context); } @@ -442,8 +431,8 @@ contract AMM { token_out: AztecAddress, amount_in_max: Field, amount_out: Field, - refund_token_in_slot_commitment: Field, - token_out_slot_commitment: Field, + refund_token_in_hiding_point_slot: Field, + token_out_hiding_point_slot: Field, ) { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount_out = U128::from_integer(amount_out); @@ -453,11 +442,11 @@ contract AMM { let token_out_contract = Token::at(token_out); // We get the reserves. The `amount_in_max` was already transferred to this contract so we need to subtract it. - let reserve_in_with_amount_in_max = U128::from_integer(token_in + let reserve_in_with_amount_in_max = U128::from_integer(token_in_contract .balance_of_public(context.this_address()) .view(&mut context)); let reserve_in = reserve_in_with_amount_in_max - amount_in_max; - let reserve_out = U128::from_integer(token_out + let reserve_out = U128::from_integer(token_out_contract .balance_of_public(context.this_address()) .view(&mut context)); @@ -469,13 +458,16 @@ contract AMM { let refund_amount = amount_in_max - amount_in; if (refund_amount > U128::zero()) { token_in_contract - .finalize_transfer_to_private(refund_token_in_slot_commitment, refund_amount) + .finalize_transfer_to_private( + refund_amount.to_integer(), + refund_token_in_hiding_point_slot, + ) .call(&mut context); } // Transfer the output token to the user. - token_out_contract.finalize_transfer_to_private(token_out_slot_commitment, amount_out).call( - &mut context, - ); + token_out_contract + .finalize_transfer_to_private(amount_out.to_integer(), token_out_hiding_point_slot) + .call(&mut context); } } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr new file mode 100644 index 00000000000..b1d126af2b4 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr @@ -0,0 +1,29 @@ +use dep::aztec::protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}}; + +global STATE_LENGTH: u32 = 3; + +/// We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single +/// merkle proof. +/// (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022). +struct State { + token0: AztecAddress, + token1: AztecAddress, + liquidity_token: AztecAddress, +} + +// Note: I could not get #[derive(Serialize)] to work so I had to implement it manually. +impl Serialize for State { + fn serialize(self: Self) -> [Field; STATE_LENGTH] { + [self.token0.to_field(), self.token1.to_field(), self.liquidity_token.to_field()] + } +} + +impl Deserialize for State { + fn deserialize(fields: [Field; STATE_LENGTH]) -> Self { + State { + token0: AztecAddress::from_field(fields[0]), + token1: AztecAddress::from_field(fields[1]), + liquidity_token: AztecAddress::from_field(fields[2]), + } + } +} From ff9a39e7c086f161e3ea91ddd8418371ebe364f3 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 30 Oct 2024 23:59:51 +0000 Subject: [PATCH 54/67] fix: compilation issues --- .../contracts/amm_contract/src/main.nr | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 932d4dbaf86..131cc4de57e 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -217,9 +217,9 @@ contract AMM { ); } assert(liquidity > U128::zero(), "INSUFFICIENT_LIQUIDITY_MINTED"); - liquidity_token.finalize_mint_to_private(liquidity_hiding_point_slot, liquidity).call( - &mut context, - ); + liquidity_token + .finalize_mint_to_private(liquidity.to_integer(), liquidity_hiding_point_slot) + .call(&mut context); } /// Removes `liquidity` from the pool and transfers the tokens back to the user. `amount0_min` and `amount1_min` are @@ -240,12 +240,10 @@ contract AMM { liquidity_token .transfer_to_public(context.msg_sender(), context.this_address(), liquidity, nonce) .call(&mut context); - let token0_hiding_point_slot = token0 - .prepare_transfer_to_private(context.this_address(), context.msg_sender(), nonce) - .call(&mut context); - let token1_hiding_point_slot = token1 - .prepare_transfer_to_private(context.this_address(), context.msg_sender(), nonce) - .call(&mut context); + let token0_hiding_point_slot = + token0.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + let token1_hiding_point_slot = + token1.prepare_transfer_to_private(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._remove_liquidity( @@ -298,9 +296,15 @@ contract AMM { assert(amount1 >= amount1_min, "INSUFFICIENT_1_AMOUNT"); // At last we burn the liquidity tokens and transfer the token0 and token1 to the user. - liquidity_token.burn_public(context.this_address(), liquidity, 0).call(&mut context); - token0.finalize_transfer_to_private(token0_hiding_point_slot, amount0).call(&mut context); - token1.finalize_transfer_to_private(token1_hiding_point_slot, amount1).call(&mut context); + liquidity_token.burn_public(context.this_address(), liquidity.to_integer(), 0).call( + &mut context, + ); + token0.finalize_transfer_to_private(amount0.to_integer(), token0_hiding_point_slot).call( + &mut context, + ); + token1.finalize_transfer_to_private(amount1.to_integer(), token1_hiding_point_slot).call( + &mut context, + ); } /// Swaps `amount_in` of `token_in` for at least `amount_out_min` of `token_out`. The `from_0_to_1` flag indicates @@ -319,8 +323,8 @@ contract AMM { // We check the tokens are valid assert(token_in != token_out); - assert(token_in == state.token0 | token_in == state.token1); - assert(token_out == state.token0 | token_out == state.token1); + assert((token_in == state.token0) | (token_in == state.token1)); + assert((token_out == state.token0) | (token_out == state.token1)); let token_in_contract = Token::at(token_in); let token_out_contract = Token::at(token_out); @@ -374,7 +378,7 @@ contract AMM { // Transfer the output token to the user. token_out_contract - .finalize_transfer_to_private(token_out_hiding_point_slot, amount_out) + .finalize_transfer_to_private(amount_out.to_integer(), token_out_hiding_point_slot) .call(&mut context); } @@ -396,8 +400,8 @@ contract AMM { // We check the tokens are valid assert(token_in != token_out); - assert(token_in == state.token0 | token_in == state.token1); - assert(token_out == state.token0 | token_out == state.token1); + assert((token_in == state.token0) | (token_in == state.token1)); + assert((token_out == state.token0) | (token_out == state.token1)); let token_in_contract = Token::at(token_in); let token_out_contract = Token::at(token_out); From 1a8223a678fcba16e670eee0132a4f4d08c278f6 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 00:52:37 +0000 Subject: [PATCH 55/67] WIP --- .../contracts/amm_contract/Nargo.toml | 1 + .../contracts/amm_contract/src/main.nr | 3 +- .../contracts/amm_contract/src/test.nr | 1 + .../amm_contract/src/test/full_flow.nr | 9 +++ .../contracts/amm_contract/src/test/utils.nr | 81 +++++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/test.nr create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr diff --git a/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml index c176425b543..d00746cfeb1 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml @@ -7,3 +7,4 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } token = { path = "../token_contract" } +uint_note = { path = "../../../aztec-nr/uint-note" } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 131cc4de57e..60c896b4d2f 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -1,5 +1,6 @@ mod lib; mod state; +// mod test; use dep::aztec::macros::aztec; @@ -49,7 +50,7 @@ contract AMM { state: SharedImmutable, } - /// Amount of liquidity which gets locked in the pool when liquidity is provided for the first time. It's purpose + /// Amount of liquidity which gets locked token_contract0l when liquidity is provided for the first ttoken_contract0purpose /// is to prevent the pool from ever emptying which could lead to undefined behavior. global MINIMUM_LIQUIDITY = U128::from_integer(1000); diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr new file mode 100644 index 00000000000..94b4d0cbbd0 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr @@ -0,0 +1 @@ +mod full_flow; \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr new file mode 100644 index 00000000000..9b0bbb7f978 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr @@ -0,0 +1,9 @@ +#[test] +unconstrained fn full_flow() { + // Setup + let (env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) = setup(); + let amm = AMM::at(amm_address); + + // Now we add liquidity + +} \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr new file mode 100644 index 00000000000..3ba0521b1df --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -0,0 +1,81 @@ +use dep::uint_note::uint_note::UintNote; +use dep::token::Token; +use aztec::{ + keys::getters::get_public_keys, + oracle::{ + execution::{get_block_number, get_contract_address}, + random::random, + storage::storage_read, + }, + prelude::AztecAddress, + protocol_types::storage::map::derive_storage_slot_in_map, + test::helpers::{cheatcodes, test_environment::TestEnvironment}, +}; +use crate::AMM; + +pub unconstrained fn setup( +) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, Field, Field, Field) { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + + let token_admin = env.create_account_contract(1); + let liquidity_provider = env.create_account_contract(2); + let swapper = env.create_account_contract(2); + + // Start the test in the account contract address + env.impersonate(token_admin); + + // Deploy tokens to be swapped and a liquidity token + let token0 = + env.deploy_self("Token").with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestToken0000000000000000000000", + "TT00000000000000000000000000000", + 18, + )); + let token0_address = token0.to_address(); + + let token1 = + env.deploy_self("Token").with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestToken1000000000000000000000", + "TT10000000000000000000000000000", + 18, + )); + let token1_address = token1.to_address(); + + let liquidity_token = + env.deploy_self("Token").with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestLiquidityToken0000000000000", + "TLT0000000000000000000000000000", + 18, + )); + let liquidity_token_address = liquidity_token.to_address(); + + let amm = env.deploy_self("AMM").with_public_void_initializer(AMM::interface().constructor( + token0_address, + token1_address, + liquidity_token_address, + )); + let amm_address = amm.to_address(); + + // Now we mint both tokens to the liquidity provider and token0 to swapper + let lp_balance_0 = 20000; + let lp_balance_1 = 10000; + let swapper_balance_0 = 5000; + + token0.mint_to_private(liquidity_provider, lp_balance_0).call( + &mut env.private(), + ); + token1.mint_to_private(liquidity_provider, lp_balance_1).call( + &mut env.private(), + ); + token0.mint_private(swapper, swapper_balance_0).call( + &mut env.private(), + ); + + + env.advance_block_by(1); + (&mut env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) +} \ No newline at end of file From c9194c1328be473ab41029f16df22ed98a2794a8 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 01:00:16 +0000 Subject: [PATCH 56/67] WIP --- .../contracts/amm_contract/src/main.nr | 2 +- .../contracts/amm_contract/src/test.nr | 3 +- .../amm_contract/src/test/full_flow.nr | 12 ++- .../contracts/amm_contract/src/test/utils.nr | 96 ++++++++++--------- 4 files changed, 61 insertions(+), 52 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 60c896b4d2f..89e0ca7a116 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -1,6 +1,6 @@ mod lib; mod state; -// mod test; +mod test; use dep::aztec::macros::aztec; diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr index 94b4d0cbbd0..28ba21602e1 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr @@ -1 +1,2 @@ -mod full_flow; \ No newline at end of file +mod full_flow; +mod utils; diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr index 9b0bbb7f978..9ceac7558fd 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr @@ -1,9 +1,13 @@ +use crate::{AMM, test::utils::setup}; +use dep::token::Token; +use dep::uint_note::uint_note::UintNote; +use aztec::prelude::AztecAddress; + #[test] unconstrained fn full_flow() { // Setup - let (env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) = setup(); + let (env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) = + setup(); let amm = AMM::at(amm_address); - // Now we add liquidity - -} \ No newline at end of file +} diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index 3ba0521b1df..abcfb5e96a4 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -1,5 +1,6 @@ -use dep::uint_note::uint_note::UintNote; +use crate::AMM; use dep::token::Token; +use dep::uint_note::uint_note::UintNote; use aztec::{ keys::getters::get_public_keys, oracle::{ @@ -11,10 +12,8 @@ use aztec::{ protocol_types::storage::map::derive_storage_slot_in_map, test::helpers::{cheatcodes, test_environment::TestEnvironment}, }; -use crate::AMM; -pub unconstrained fn setup( -) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, Field, Field, Field) { +pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, Field, Field, Field) { // Setup env, generate keys let mut env = TestEnvironment::new(); @@ -26,56 +25,61 @@ pub unconstrained fn setup( env.impersonate(token_admin); // Deploy tokens to be swapped and a liquidity token - let token0 = - env.deploy_self("Token").with_public_void_initializer(Token::interface().constructor( - token_admin, - "TestToken0000000000000000000000", - "TT00000000000000000000000000000", - 18, - )); - let token0_address = token0.to_address(); + let token0_address = env + .deploy_self("Token") + .with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestToken0000000000000000000000", + "TT00000000000000000000000000000", + 18, + )) + .to_address(); + let token0 = Token::at(token0_address); - let token1 = - env.deploy_self("Token").with_public_void_initializer(Token::interface().constructor( - token_admin, - "TestToken1000000000000000000000", - "TT10000000000000000000000000000", - 18, - )); - let token1_address = token1.to_address(); + let token1_address = env + .deploy_self("Token") + .with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestToken1000000000000000000000", + "TT10000000000000000000000000000", + 18, + )) + .to_address(); + let token1 = Token::at(token1_address); - let liquidity_token = - env.deploy_self("Token").with_public_void_initializer(Token::interface().constructor( - token_admin, - "TestLiquidityToken0000000000000", - "TLT0000000000000000000000000000", - 18, - )); - let liquidity_token_address = liquidity_token.to_address(); + let liquidity_token_address = env + .deploy_self("Token") + .with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestLiquidityToken0000000000000", + "TLT0000000000000000000000000000", + 18, + )) + .to_address(); + let liquidity_token = Token::at(liquidity_token_address); - let amm = env.deploy_self("AMM").with_public_void_initializer(AMM::interface().constructor( - token0_address, - token1_address, - liquidity_token_address, - )); - let amm_address = amm.to_address(); + let amm_address = env + .deploy_self("AMM") + .with_public_void_initializer(AMM::interface().constructor( + token0_address, + token1_address, + liquidity_token_address, + )) + .to_address(); + let amm = AMM::at(amm_address); // Now we mint both tokens to the liquidity provider and token0 to swapper let lp_balance_0 = 20000; let lp_balance_1 = 10000; let swapper_balance_0 = 5000; - token0.mint_to_private(liquidity_provider, lp_balance_0).call( - &mut env.private(), - ); - token1.mint_to_private(liquidity_provider, lp_balance_1).call( - &mut env.private(), - ); - token0.mint_private(swapper, swapper_balance_0).call( - &mut env.private(), - ); - + token0.mint_to_private(liquidity_provider, lp_balance_0).call(&mut env.private()); + token1.mint_to_private(liquidity_provider, lp_balance_1).call(&mut env.private()); + token0.mint_to_private(swapper, swapper_balance_0).call(&mut env.private()); env.advance_block_by(1); - (&mut env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) -} \ No newline at end of file + ( + &mut env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, + liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0, + ) +} From 4441cbeaf6006e6c4cc22ce6f89cabafa71dc91e Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 01:11:53 +0000 Subject: [PATCH 57/67] fix --- .../noir-contracts/contracts/amm_contract/src/test/utils.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index abcfb5e96a4..c4e4699e27f 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -19,7 +19,7 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres let token_admin = env.create_account_contract(1); let liquidity_provider = env.create_account_contract(2); - let swapper = env.create_account_contract(2); + let swapper = env.create_account_contract(3); // Start the test in the account contract address env.impersonate(token_admin); From 47ee850ce8629452221a91b989f6a3e3af2ce0be Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 01:16:07 +0000 Subject: [PATCH 58/67] WIP --- .../noir-contracts/contracts/amm_contract/src/test/full_flow.nr | 2 +- .../noir-contracts/contracts/amm_contract/src/test/utils.nr | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr index 9ceac7558fd..82d66728240 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr @@ -8,6 +8,6 @@ unconstrained fn full_flow() { // Setup let (env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) = setup(); - let amm = AMM::at(amm_address); + // let amm = AMM::at(amm_address); // Now we add liquidity } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index c4e4699e27f..65207945b0d 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -56,7 +56,6 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres 18, )) .to_address(); - let liquidity_token = Token::at(liquidity_token_address); let amm_address = env .deploy_self("AMM") @@ -66,7 +65,6 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres liquidity_token_address, )) .to_address(); - let amm = AMM::at(amm_address); // Now we mint both tokens to the liquidity provider and token0 to swapper let lp_balance_0 = 20000; From 038e7d0435911a6c1cfc8a85232b8fc6999db01e Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 16:57:35 +0000 Subject: [PATCH 59/67] fixes --- .../contracts/amm_contract/src/test/utils.nr | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index 65207945b0d..9a88b03ebf4 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -1,5 +1,5 @@ use crate::AMM; -use dep::token::Token; +use dep::token::{test::utils::{check_private_balance, mint_private}, Token}; use dep::uint_note::uint_note::UintNote; use aztec::{ keys::getters::get_public_keys, @@ -8,10 +8,11 @@ use aztec::{ random::random, storage::storage_read, }, - prelude::AztecAddress, + prelude::{AztecAddress, NoteHeader}, protocol_types::storage::map::derive_storage_slot_in_map, test::helpers::{cheatcodes, test_environment::TestEnvironment}, }; +use std::test::OracleMock; pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, Field, Field, Field) { // Setup env, generate keys @@ -26,7 +27,7 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres // Deploy tokens to be swapped and a liquidity token let token0_address = env - .deploy_self("Token") + .deploy("./@token_contract", "Token") .with_public_void_initializer(Token::interface().constructor( token_admin, "TestToken0000000000000000000000", @@ -37,7 +38,7 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres let token0 = Token::at(token0_address); let token1_address = env - .deploy_self("Token") + .deploy("./@token_contract", "Token") .with_public_void_initializer(Token::interface().constructor( token_admin, "TestToken1000000000000000000000", @@ -48,7 +49,7 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres let token1 = Token::at(token1_address); let liquidity_token_address = env - .deploy_self("Token") + .deploy("./@token_contract", "Token") .with_public_void_initializer(Token::interface().constructor( token_admin, "TestLiquidityToken0000000000000", @@ -71,9 +72,14 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres let lp_balance_1 = 10000; let swapper_balance_0 = 5000; - token0.mint_to_private(liquidity_provider, lp_balance_0).call(&mut env.private()); - token1.mint_to_private(liquidity_provider, lp_balance_1).call(&mut env.private()); - token0.mint_to_private(swapper, swapper_balance_0).call(&mut env.private()); + mint_private(&mut env, token0_address, liquidity_provider, lp_balance_0); + check_private_balance(token0_address, liquidity_provider, lp_balance_0); + + mint_private(&mut env, token1_address, liquidity_provider, lp_balance_1); + check_private_balance(token1_address, liquidity_provider, lp_balance_1); + + mint_private(&mut env, token0_address, swapper, swapper_balance_0); + check_private_balance(token0_address, swapper, swapper_balance_0); env.advance_block_by(1); ( From 51088f44bea82aee82c13609c86819845c1daba8 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 18:00:24 +0000 Subject: [PATCH 60/67] WIP --- .../contracts/amm_contract/Nargo.toml | 1 + .../contracts/amm_contract/src/main.nr | 6 +-- .../amm_contract/src/test/full_flow.nr | 46 +++++++++++++++++-- .../contracts/amm_contract/src/test/utils.nr | 4 +- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml index d00746cfeb1..f2b6f48ba03 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml @@ -8,3 +8,4 @@ type = "contract" aztec = { path = "../../../aztec-nr/aztec" } token = { path = "../token_contract" } uint_note = { path = "../../../aztec-nr/uint-note" } +authwit = { path = "../../../aztec-nr/authwit" } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 89e0ca7a116..fada180cf87 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -79,8 +79,6 @@ contract AMM { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount0_desired = U128::from_integer(amount0_desired); let amount1_desired = U128::from_integer(amount1_desired); - let amount0_min = U128::from_integer(amount0_min); - let amount1_min = U128::from_integer(amount1_min); assert( amount0_desired > U128::zero() & amount1_desired > U128::zero(), @@ -129,8 +127,8 @@ contract AMM { liquidity_hiding_point_slot, amount0_desired.to_integer(), amount1_desired.to_integer(), - amount0_min.to_integer(), - amount1_min.to_integer(), + amount0_min, + amount1_min, ) .enqueue(&mut context); } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr index 82d66728240..702226662dc 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr @@ -1,6 +1,6 @@ use crate::{AMM, test::utils::setup}; +use dep::authwit::cheatcodes as authwit_cheatcodes; use dep::token::Token; -use dep::uint_note::uint_note::UintNote; use aztec::prelude::AztecAddress; #[test] @@ -8,6 +8,46 @@ unconstrained fn full_flow() { // Setup let (env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) = setup(); - // let amm = AMM::at(amm_address); - // Now we add liquidity + let amm = AMM::at(amm_address); + + // ADDING LIQUIDITY + // Ideally we would like to deposit all the tokens from the liquidity provider + let amount0_desired = lp_balance_0; + let amount1_desired = lp_balance_1; + let amount0_min = lp_balance_0 / 2; + let amount1_min = lp_balance_1 / 2; + + // First we need to add authwits such that the AMM can transfer the tokens from the liquidity provider + let nonce_for_authwits = 1; + authwit_cheatcodes::add_private_authwit_from_call_interface( + liquidity_provider, + amm_address, + Token::at(token0_address).transfer_to_public( + liquidity_provider, + amm_address, + amount0_desired, + nonce_for_authwits, + ), + ); + authwit_cheatcodes::add_private_authwit_from_call_interface( + liquidity_provider, + amm_address, + Token::at(token1_address).transfer_to_public( + liquidity_provider, + amm_address, + amount1_desired, + nonce_for_authwits, + ), + ); + + // Now we can add liquidity + amm + .add_liquidity( + amount0_desired, + amount1_desired, + amount0_min, + amount1_min, + nonce_for_authwits, + ) + .call(&mut env.private()); } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index 9a88b03ebf4..c826aa92a13 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -1,6 +1,5 @@ use crate::AMM; use dep::token::{test::utils::{check_private_balance, mint_private}, Token}; -use dep::uint_note::uint_note::UintNote; use aztec::{ keys::getters::get_public_keys, oracle::{ @@ -8,11 +7,10 @@ use aztec::{ random::random, storage::storage_read, }, - prelude::{AztecAddress, NoteHeader}, + prelude::AztecAddress, protocol_types::storage::map::derive_storage_slot_in_map, test::helpers::{cheatcodes, test_environment::TestEnvironment}, }; -use std::test::OracleMock; pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, Field, Field, Field) { // Setup env, generate keys From eb3379523b5423379d2984eececc91f8f277f0ef Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 18:54:55 +0000 Subject: [PATCH 61/67] WIP --- .../amm_contract/src/test/full_flow.nr | 73 +++++++++++++++++-- .../contracts/amm_contract/src/test/utils.nr | 6 +- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr index 702226662dc..47fdde4c5e8 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr @@ -1,12 +1,13 @@ use crate::{AMM, test::utils::setup}; -use dep::authwit::cheatcodes as authwit_cheatcodes; -use dep::token::Token; -use aztec::prelude::AztecAddress; +use dep::authwit::cheatcodes::add_private_authwit_from_call_interface; +use dep::token::{test::utils::{add_token_note, check_private_balance, check_public_balance}, Token}; +use aztec::oracle::random::random; +use std::test::OracleMock; #[test] unconstrained fn full_flow() { // Setup - let (env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0) = + let (env, amm_address, token0_address, token1_address, liquidity_token_address, liquidity_provider, swapper, lp_balance_0, lp_balance_1, swapper_balance_0) = setup(); let amm = AMM::at(amm_address); @@ -18,8 +19,9 @@ unconstrained fn full_flow() { let amount1_min = lp_balance_1 / 2; // First we need to add authwits such that the AMM can transfer the tokens from the liquidity provider - let nonce_for_authwits = 1; - authwit_cheatcodes::add_private_authwit_from_call_interface( + // The only purpose of this nonce is to make the authwit unique (function args are part of authwit hash preimage) + let nonce_for_authwits = random(); + add_private_authwit_from_call_interface( liquidity_provider, amm_address, Token::at(token0_address).transfer_to_public( @@ -29,7 +31,7 @@ unconstrained fn full_flow() { nonce_for_authwits, ), ); - authwit_cheatcodes::add_private_authwit_from_call_interface( + add_private_authwit_from_call_interface( liquidity_provider, amm_address, Token::at(token1_address).transfer_to_public( @@ -40,6 +42,10 @@ unconstrained fn full_flow() { ), ); + // We fix the note randomness as we need to add the notes manually. This will go away once #8771 is implemented. + let note_randomness = random(); + let _ = OracleMock::mock("getRandomField").returns(note_randomness); + // Now we can add liquidity amm .add_liquidity( @@ -50,4 +56,57 @@ unconstrained fn full_flow() { nonce_for_authwits, ) .call(&mut env.private()); + + // Since there was no liquidity in the pool before the pool should take the desired amounts of tokens in public + // The AMM should therefore hold the desired amounts + check_public_balance(token0_address, amm_address, amount0_desired); + check_public_balance(token1_address, amm_address, amount1_desired); + + // The liquidity token should have been minted to the liquidity provider + let expected_liquidity_token_balance = Amm::MINIMUM_LIQUIDITY * U128::from_integer(9); + // Since it was minted in private and #8771 is not yet implemented we need to add the note + add_token_note( + env, + liquidity_token_address, + liquidity_provider, + expected_liquidity_token_balance, + note_randomness, + ); + check_private_balance( + liquidity_token_address, + liquidity_provider, + expected_liquidity_token_balance, + ); + + // The AMM should have locked minimum liquidity to itself in public + check_public_balance(liquidity_token_address, amm_address, Amm::MINIMUM_LIQUIDITY); + + // SWAPPING + // Now we will try to swap the full balance of the swapper + let amount_in = swapper_balance_0; + // We don't care about slippage protection here so we set out min to 0 + let amount_out_min = 0; + + // We need to add authwits such that the AMM can transfer the tokens from the swapper + add_private_authwit_from_call_interface( + swapper, + amm_address, + Token::at(token0_address).transfer_to_public( + swapper, + amm_address, + amount_in, + nonce_for_authwits, + ), + ); + + // Now we can swap + amm + .swap_exact_tokens_for_tokens( + token0_address, + token1_address, + amount_in, + amount_out_min, + nonce_for_authwits, + ) + .call(&mut env.private()); } diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index c826aa92a13..60cba898f4c 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -33,7 +33,6 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres 18, )) .to_address(); - let token0 = Token::at(token0_address); let token1_address = env .deploy("./@token_contract", "Token") @@ -44,7 +43,6 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres 18, )) .to_address(); - let token1 = Token::at(token1_address); let liquidity_token_address = env .deploy("./@token_contract", "Token") @@ -81,7 +79,7 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres env.advance_block_by(1); ( - &mut env, amm_address, token0_address, token1_address, liquidity_token_address, token_admin, - liquidity_provider, lp_balance_0, lp_balance_1, swapper_balance_0, + &mut env, amm_address, token0_address, token1_address, liquidity_token_address, + liquidity_provider, swapper, lp_balance_0, lp_balance_1, swapper_balance_0, ) } From 854f7d6d308ff074bc83976ac6df49217edb8c49 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 18:59:55 +0000 Subject: [PATCH 62/67] optimization --- .../contracts/amm_contract/src/test/utils.nr | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index 60cba898f4c..68fddefef9d 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -16,9 +16,11 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres // Setup env, generate keys let mut env = TestEnvironment::new(); - let token_admin = env.create_account_contract(1); - let liquidity_provider = env.create_account_contract(2); - let swapper = env.create_account_contract(3); + // For token admin we don't ever need authwits so we don't need to deploy an account contract + let token_admin = env.create_account(); + // For the liquidity provider and swapper we do need authwits + let liquidity_provider = env.create_account_contract(1); + let swapper = env.create_account_contract(2); // Start the test in the account contract address env.impersonate(token_admin); From 552fb5ec60413245a60ce29bdc90b0c3f50de9e8 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 31 Oct 2024 19:19:14 +0000 Subject: [PATCH 63/67] WIP --- .../contracts/amm_contract/src/main.nr | 7 ++++--- .../contracts/amm_contract/src/test/full_flow.nr | 13 ++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index fada180cf87..2245f608c91 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -53,6 +53,8 @@ contract AMM { /// Amount of liquidity which gets locked token_contract0l when liquidity is provided for the first ttoken_contract0purpose /// is to prevent the pool from ever emptying which could lead to undefined behavior. global MINIMUM_LIQUIDITY = U128::from_integer(1000); + // We set it to 9 times the minimum liquidity. That way the first LP gets 90% of the value of his deposit. + global INITIAL_LIQUIDITY = U128::from_integer(9000); // Note: Since we don't have inheritance it seems the easiest to deploy the standard token and use it as // a liquidity tracking contract. This contract would be an admin of the liquidity contract. @@ -203,9 +205,8 @@ contract AMM { let mut liquidity = U128::zero(); if (total_supply == U128::zero()) { // Since we don't collect a protocol fee (unlike Uniswap V2) we can set initial liquidity to an arbitrary - // value instead of sqrt(amount0 * amount1). We set it to 9 times the minimum liquidity. That way - // the initial depositor gets 90% of the value of his deposit. - liquidity = MINIMUM_LIQUIDITY * U128::from_integer(9); + // value instead of sqrt(amount0 * amount1). + liquidity = INITIAL_LIQUIDITY; liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY.to_integer()).call( &mut context, ); // permanently lock the first MINIMUM_LIQUIDITY tokens diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr index 47fdde4c5e8..7c391e0ed9b 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr @@ -62,24 +62,27 @@ unconstrained fn full_flow() { check_public_balance(token0_address, amm_address, amount0_desired); check_public_balance(token1_address, amm_address, amount1_desired); - // The liquidity token should have been minted to the liquidity provider - let expected_liquidity_token_balance = Amm::MINIMUM_LIQUIDITY * U128::from_integer(9); + // Initial liquidity amount of liquidity token should have been minted to the liquidity provider // Since it was minted in private and #8771 is not yet implemented we need to add the note add_token_note( env, liquidity_token_address, liquidity_provider, - expected_liquidity_token_balance, + AMM::INITIAL_LIQUIDITY.to_integer(), note_randomness, ); check_private_balance( liquidity_token_address, liquidity_provider, - expected_liquidity_token_balance, + AMM::INITIAL_LIQUIDITY.to_integer(), ); // The AMM should have locked minimum liquidity to itself in public - check_public_balance(liquidity_token_address, amm_address, Amm::MINIMUM_LIQUIDITY); + check_public_balance( + liquidity_token_address, + amm_address, + AMM::MINIMUM_LIQUIDITY.to_integer(), + ); // SWAPPING // Now we will try to swap the full balance of the swapper From 1810b51ea7bfecf36715f4e018a0697a257be221 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 13 Nov 2024 19:41:22 +0000 Subject: [PATCH 64/67] fixes after rebase --- .../noir-contracts/contracts/amm_contract/src/main.nr | 6 +++--- .../contracts/amm_contract/src/test/utils.nr | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 2245f608c91..5ba57a8f8e5 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -207,9 +207,9 @@ contract AMM { // Since we don't collect a protocol fee (unlike Uniswap V2) we can set initial liquidity to an arbitrary // value instead of sqrt(amount0 * amount1). liquidity = INITIAL_LIQUIDITY; - liquidity_token.mint_public(AztecAddress::zero(), MINIMUM_LIQUIDITY.to_integer()).call( - &mut context, - ); // permanently lock the first MINIMUM_LIQUIDITY tokens + liquidity_token + .mint_to_public(AztecAddress::zero(), MINIMUM_LIQUIDITY.to_integer()) + .call(&mut context); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = std::cmp::min( amount0 * total_supply / reserve0, diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr index 68fddefef9d..418883cdc91 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -1,5 +1,5 @@ use crate::AMM; -use dep::token::{test::utils::{check_private_balance, mint_private}, Token}; +use dep::token::{test::utils::{check_private_balance, mint_to_private}, Token}; use aztec::{ keys::getters::get_public_keys, oracle::{ @@ -70,13 +70,13 @@ pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddres let lp_balance_1 = 10000; let swapper_balance_0 = 5000; - mint_private(&mut env, token0_address, liquidity_provider, lp_balance_0); + mint_to_private(&mut env, token0_address, liquidity_provider, lp_balance_0); check_private_balance(token0_address, liquidity_provider, lp_balance_0); - mint_private(&mut env, token1_address, liquidity_provider, lp_balance_1); + mint_to_private(&mut env, token1_address, liquidity_provider, lp_balance_1); check_private_balance(token1_address, liquidity_provider, lp_balance_1); - mint_private(&mut env, token0_address, swapper, swapper_balance_0); + mint_to_private(&mut env, token0_address, swapper, swapper_balance_0); check_private_balance(token0_address, swapper, swapper_balance_0); env.advance_block_by(1); From ee0c86d44f75421d9cd9f3709b417532d1e97382 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 13 Nov 2024 20:11:16 +0000 Subject: [PATCH 65/67] fix --- .../contracts/amm_contract/src/main.nr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr index 5ba57a8f8e5..a1a4cdb0013 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -114,12 +114,12 @@ contract AMM { // We may need to return some token amounts depending on public state (i.e. if the desired amounts do // not have the same ratio as the live reserves), so we prepare partial notes for the refunds. let refund_token0_hiding_point_slot = - token0.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + token0.prepare_private_balance_increase(context.msg_sender()).call(&mut context); let refund_token1_hiding_point_slot = - token1.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + token1.prepare_private_balance_increase(context.msg_sender()).call(&mut context); // We prepare a partial note for the liquidity tokens. let liquidity_hiding_point_slot = - liquidity_token.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + liquidity_token.prepare_private_balance_increase(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._add_liquidity( @@ -241,9 +241,9 @@ contract AMM { .transfer_to_public(context.msg_sender(), context.this_address(), liquidity, nonce) .call(&mut context); let token0_hiding_point_slot = - token0.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + token0.prepare_private_balance_increase(context.msg_sender()).call(&mut context); let token1_hiding_point_slot = - token1.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + token1.prepare_private_balance_increase(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._remove_liquidity( @@ -334,7 +334,7 @@ contract AMM { .transfer_to_public(context.msg_sender(), context.this_address(), amount_in, nonce) .call(&mut context); let token_out_hiding_point_slot = - token_out_contract.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + token_out_contract.prepare_private_balance_increase(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._swap_exact_tokens_for_tokens( @@ -412,9 +412,9 @@ contract AMM { .transfer_to_public(context.msg_sender(), context.this_address(), amount_in_max, nonce) .call(&mut context); let refund_token_in_hiding_point_slot = - token_in_contract.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + token_in_contract.prepare_private_balance_increase(context.msg_sender()).call(&mut context); let token_out_hiding_point_slot = - token_out_contract.prepare_transfer_to_private(context.msg_sender()).call(&mut context); + token_out_contract.prepare_private_balance_increase(context.msg_sender()).call(&mut context); AMM::at(context.this_address()) ._swap_tokens_for_exact_tokens( From f55c4c206a1cada9fc1c326bf90648fc0f7af717 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 13 Nov 2024 20:33:54 +0000 Subject: [PATCH 66/67] WIP on an AMM --- yarn-project/end-to-end/src/e2e_amm.test.ts | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 yarn-project/end-to-end/src/e2e_amm.test.ts diff --git a/yarn-project/end-to-end/src/e2e_amm.test.ts b/yarn-project/end-to-end/src/e2e_amm.test.ts new file mode 100644 index 00000000000..a35a542756b --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -0,0 +1,92 @@ +import { type AccountWallet, type DebugLogger, Fr } from '@aztec/aztec.js'; +import { AMMContract, type TokenContract } from '@aztec/noir-contracts.js'; + +import { jest } from '@jest/globals'; + +import { deployToken, mintTokensToPrivate } from './fixtures/token_utils.js'; +import { setup } from './fixtures/utils.js'; + +const TIMEOUT = 120_000; + +// This is a very simple test checking only the happy path. More complete tests might be done in an unimaginably far +// future (and hence irrelevant) once I return from Patagonia. +describe('AMM', () => { + jest.setTimeout(TIMEOUT); + + let teardown: () => Promise; + + let logger: DebugLogger; + + let adminWallet: AccountWallet; + let liquidityProvider: AccountWallet; + let swapper: AccountWallet; + + let token0: TokenContract; + let token1: TokenContract; + let liquidityToken: TokenContract; + + let amm: AMMContract; + + const lpBalance0 = 20000n; + const lpBalance1 = 10000n; + const swapperBalance0 = 5000n; + + beforeAll(async () => { + let wallets: AccountWallet[]; + ({ teardown, wallets, logger } = await setup(3)); + [adminWallet, liquidityProvider, swapper] = wallets; + + token0 = await deployToken(adminWallet, 0n, logger); + token1 = await deployToken(adminWallet, 0n, logger); + liquidityToken = await deployToken(adminWallet, 0n, logger); + + amm = await AMMContract.deploy(adminWallet, token0.address, token1.address, liquidityToken.address) + .send() + .deployed(); + + // We mint the tokens to lp and swapper + await mintTokensToPrivate(token0, adminWallet, liquidityProvider.getAddress(), lpBalance0); + await mintTokensToPrivate(token1, adminWallet, liquidityProvider.getAddress(), lpBalance1); + await mintTokensToPrivate(token0, adminWallet, swapper.getAddress(), swapperBalance0); + }); + + afterAll(() => teardown()); + + it('full flow', async () => { + // ADDING LIQUIDITY + // Ideally we would like to deposit all the tokens from the liquidity provider + const amount0Desired = lpBalance0; + const amount1Desired = lpBalance1; + const amount0Min = lpBalance0 / 2n; + const amount1Min = lpBalance1 / 2n; + + // First we need to add authwits such that the AMM can transfer the tokens from the liquidity provider + // The only purpose of this nonce is to make the authwit unique (function args are part of authwit hash preimage) + const nonceForAuthwits = Fr.random(); + + await liquidityProvider.createAuthWit({ + caller: amm.address, + action: token0.methods.transfer_to_public( + liquidityProvider.getAddress(), + amm.address, + amount0Desired, + nonceForAuthwits, + ), + }); + await liquidityProvider.createAuthWit({ + caller: amm.address, + action: token1.methods.transfer_to_public( + liquidityProvider.getAddress(), + amm.address, + amount1Desired, + nonceForAuthwits, + ), + }); + + await amm + .withWallet(liquidityProvider) + .methods.add_liquidity(amount0Desired, amount1Desired, amount0Min, amount1Min, nonceForAuthwits) + .send() + .wait(); + }); +}); From a404b58e7d049ee7a56310702046f03a624fd1ee Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 14 Nov 2024 15:57:41 +0000 Subject: [PATCH 67/67] WIP --- yarn-project/end-to-end/src/e2e_amm.test.ts | 75 +++++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_amm.test.ts b/yarn-project/end-to-end/src/e2e_amm.test.ts index a35a542756b..0424f2c5c79 100644 --- a/yarn-project/end-to-end/src/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -54,13 +54,12 @@ describe('AMM', () => { it('full flow', async () => { // ADDING LIQUIDITY - // Ideally we would like to deposit all the tokens from the liquidity provider - const amount0Desired = lpBalance0; - const amount1Desired = lpBalance1; - const amount0Min = lpBalance0 / 2n; - const amount1Min = lpBalance1 / 2n; + const amount0Desired = lpBalance0 / 2n; + const amount1Desired = lpBalance1 / 2n; + const amount0Min = lpBalance0 / 3n; + const amount1Min = lpBalance1 / 3n; - // First we need to add authwits such that the AMM can transfer the tokens from the liquidity provider + // First we need to add authwits such that the AMM can transfer the tokens from the liquidity provider (LP). // The only purpose of this nonce is to make the authwit unique (function args are part of authwit hash preimage) const nonceForAuthwits = Fr.random(); @@ -88,5 +87,69 @@ describe('AMM', () => { .methods.add_liquidity(amount0Desired, amount1Desired, amount0Min, amount1Min, nonceForAuthwits) .send() .wait(); + + // Since the LP was the first one to enter the pool, the desired amounts of tokens should have been deposited. + expect(await token0.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toEqual( + lpBalance0 - amount0Desired, + ); + expect(await token1.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toEqual( + lpBalance1 - amount1Desired, + ); + + // The LP should now have liquidity token + expect(await liquidityToken.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toBeGreaterThan( + 0n, + ); + + // SWAPPING EXACT TOKENS FOR TOKENS + // We try swapping half of swapper's token 0 balance for token 1 + const amountIn = swapperBalance0 / 2n; + + // We need to add authwit such that the AMM can transfer the tokens from the swapper + await swapper.createAuthWit({ + caller: amm.address, + action: token0.methods.transfer_to_public(swapper.getAddress(), amm.address, amountIn, nonceForAuthwits), + }); + + // We don't care about the minimum amount of token 1 we get in this test as long as it's non-zero. + const amountOutMin = 1n; + await amm.methods + .swap_exact_tokens_for_tokens(token0.address, token1.address, amountIn, amountOutMin, nonceForAuthwits) + .send() + .wait(); + + // All the amountIn should have been swapped so LP balance0 should be decreased by that amount + const lpBalance0AfterSwap1 = await token0.methods.balance_of_private(liquidityProvider.getAddress()).simulate(); + expect(lpBalance0AfterSwap1).toEqual(lpBalance0 - amountIn); + + // At this point a user should have a non-zero balance of token 1 + const lpBalance1AfterSwap1 = await token1.methods.balance_of_private(swapper.getAddress()).simulate(); + expect(lpBalance1AfterSwap1).toBeGreaterThan(0n); + + // SWAPPING TOKENS FOR EXACT TOKENS + const amount0Out = 1000n; + // We allow the AMM to take all our token1 balance (the difference will be refunded). + const amount1InMax = lpBalance1AfterSwap1; + + // We need to add authwit such that the AMM can transfer the tokens from the swapper + await swapper.createAuthWit({ + caller: amm.address, + action: token1.methods.transfer_to_public(swapper.getAddress(), amm.address, amount1InMax, nonceForAuthwits), + }); + + await amm.methods + .swap_tokens_for_exact_tokens(token1.address, token0.address, amount0Out, amount1InMax, nonceForAuthwits) + .send() + .wait(); + + // We should have received the exact amount of token0 + expect(await token0.methods.balance_of_private(swapper.getAddress()).simulate()).toEqual( + lpBalance0AfterSwap1 + amount0Out, + ); + + // We should have received a refund of token 1 (meaning we should have more than "previous balance - amount1InMax") + expect(await token1.methods.balance_of_private(swapper.getAddress()).simulate()).toBeGreaterThan( + lpBalance1AfterSwap1 - amount1InMax, + ); }); });