From 81d7607d9d551aea4d6de78ec3ff535ec5d5a29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 22 Nov 2024 20:03:39 +0000 Subject: [PATCH 01/15] draft: amm --- noir-projects/noir-contracts/Nargo.toml | 1 + .../contracts/amm_contract/Nargo.toml | 11 + .../contracts/amm_contract/src/lib.nr | 61 +++ .../contracts/amm_contract/src/main.nr | 477 ++++++++++++++++++ .../contracts/amm_contract/src/state.nr | 29 ++ .../contracts/amm_contract/src/test.nr | 2 + .../amm_contract/src/test/full_flow.nr | 115 +++++ .../contracts/amm_contract/src/test/utils.nr | 87 ++++ .../lending_contract/src/interest_math.nr | 2 +- yarn-project/end-to-end/src/e2e_amm.test.ts | 155 ++++++ 10 files changed, 939 insertions(+), 1 deletion(-) create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/main.nr create mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/state.nr 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 create mode 100644 yarn-project/end-to-end/src/e2e_amm.test.ts diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index df510e994320..18ba10820a7c 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/amm_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml new file mode 100644 index 000000000000..f2b6f48ba033 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml @@ -0,0 +1,11 @@ +[package] +name = "amm_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +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/lib.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr new file mode 100644 index 000000000000..29faca8f887d --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr @@ -0,0 +1,61 @@ +/// 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 +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: 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 * 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: 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) +} + +/// 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 new file mode 100644 index 000000000000..a1a4cdb00130 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -0,0 +1,477 @@ +mod lib; +mod state; +mod test; + +use dep::aztec::macros::aztec; + +/// ## 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 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 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_amounts_to_add}, state::State}; + use dep::aztec::{ + macros::{ + events::event, + functions::{initializer, internal, private, public, view}, + storage::storage, + }, + prelude::{AztecAddress, SharedImmutable}, + }; + use dep::token::Token; + + #[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, + } + + /// 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. + // TODO(#9480): 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) { + 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. + #[private] + fn add_liquidity( + amount0_desired: Field, + amount1_desired: Field, + amount0_min: Field, + amount1_min: Field, + nonce: 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); + + assert( + amount0_desired > U128::zero() & amount1_desired > U128::zero(), + "INSUFFICIENT_INPUT_AMOUNTS", + ); + + let state = storage.state.read_private(); + + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + let liquidity_token = Token::at(state.liquidity_token); + + // We transfer the desired amounts of tokens to this contract. + token0 + .transfer_to_public( + context.msg_sender(), + context.this_address(), + amount0_desired.to_integer(), + nonce, + ) + .call(&mut context); + token1 + .transfer_to_public( + context.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. + let refund_token0_hiding_point_slot = + token0.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + let refund_token1_hiding_point_slot = + 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_private_balance_increase(context.msg_sender()).call(&mut context); + + AMM::at(context.this_address()) + ._add_liquidity( + state, + refund_token0_hiding_point_slot, + refund_token1_hiding_point_slot, + liquidity_hiding_point_slot, + amount0_desired.to_integer(), + amount1_desired.to_integer(), + 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 public storage again. + state: State, + 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, + 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); + + let token0 = Token::at(state.token0); + let token1 = Token::at(state.token1); + let liquidity_token = Token::at(state.liquidity_token); + + 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; + + // Calculate the amounts to be added to the pool + 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; + + // 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 > U128::zero()) { + 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_hiding_point_slot, + refund_amount_token1.to_integer(), + ) + .call(&mut context); + } + + // Calculate the amount of liquidity tokens to mint + let total_supply = U128::from_integer(liquidity_token.total_supply().view(&mut context)); + 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). + liquidity = INITIAL_LIQUIDITY; + 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, + amount1 * total_supply / reserve1, + ); + } + assert(liquidity > U128::zero(), "INSUFFICIENT_LIQUIDITY_MINTED"); + 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 + /// 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: Field, amount0_min: Field, amount1_min: Field, nonce: Field) { + 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); + + // 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(context.msg_sender(), context.this_address(), liquidity, nonce) + .call(&mut context); + let token0_hiding_point_slot = + token0.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + let token1_hiding_point_slot = + token1.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + + AMM::at(context.this_address()) + ._remove_liquidity( + state, + token0_hiding_point_slot, + token1_hiding_point_slot, + liquidity, + amount0_min, + amount1_min, + ) + .enqueue(&mut context); + } + + #[public] + #[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_hiding_point_slot: Field, + token1_hiding_point_slot: Field, + 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 = 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. + let amount0 = liquidity * reserve0 / total_supply; + let amount1 = liquidity * reserve1 / total_supply; + + // 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.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 + /// 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( + token_in: AztecAddress, + token_out: AztecAddress, + amount_in: Field, + amount_out_min: Field, + nonce: Field, + ) { + let state = storage.state.read_private(); + + // 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_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_contract + .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_private_balance_increase(context.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_out_hiding_point_slot, + ) + .enqueue(&mut context); + } + + #[public] + #[internal] + fn _swap_exact_tokens_for_tokens( + token_in: AztecAddress, + token_out: AztecAddress, + amount_in: Field, + amount_out_min: 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); + let amount_out_min = U128::from_integer(amount_out_min); + + 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_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_contract + .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); + assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); + + // Transfer the output token to the user. + token_out_contract + .finalize_transfer_to_private(amount_out.to_integer(), token_out_hiding_point_slot) + .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( + token_in: AztecAddress, + token_out: AztecAddress, + amount_out: Field, + amount_in_max: Field, + nonce: Field, + ) { + // TODO(#8271): Type the args as U128 and nuke these ugly casts + let amount_out = U128::from_integer(amount_out); + + let state = storage.state.read_private(); + + // 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_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_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_private_balance_increase(context.msg_sender()).call(&mut context); + let token_out_hiding_point_slot = + token_out_contract.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + + AMM::at(context.this_address()) + ._swap_tokens_for_exact_tokens( + token_in, + token_out, + amount_in_max, + amount_out.to_integer(), + refund_token_in_hiding_point_slot, + token_out_hiding_point_slot, + ) + .enqueue(&mut context); + } + + #[public] + #[internal] + fn _swap_tokens_for_exact_tokens( + token_in: AztecAddress, + token_out: AztecAddress, + amount_in_max: Field, + amount_out: 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); + let amount_in_max = U128::from_integer(amount_in_max); + + 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_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_contract + .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); + 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 > U128::zero()) { + token_in_contract + .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(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 000000000000..b1d126af2b4c --- /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]), + } + } +} 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 000000000000..28ba21602e17 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr @@ -0,0 +1,2 @@ +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 new file mode 100644 index 000000000000..7c391e0ed9b5 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr @@ -0,0 +1,115 @@ +use crate::{AMM, test::utils::setup}; +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, liquidity_provider, swapper, lp_balance_0, lp_balance_1, swapper_balance_0) = + setup(); + 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 + // 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( + liquidity_provider, + amm_address, + amount0_desired, + nonce_for_authwits, + ), + ); + 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, + ), + ); + + // 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( + amount0_desired, + amount1_desired, + amount0_min, + amount1_min, + 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); + + // 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, + AMM::INITIAL_LIQUIDITY.to_integer(), + note_randomness, + ); + check_private_balance( + liquidity_token_address, + liquidity_provider, + 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.to_integer(), + ); + + // 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 new file mode 100644 index 000000000000..418883cdc918 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr @@ -0,0 +1,87 @@ +use crate::AMM; +use dep::token::{test::utils::{check_private_balance, mint_to_private}, 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}, +}; + +pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, Field, Field, Field) { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + + // 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); + + // Deploy tokens to be swapped and a liquidity token + let token0_address = env + .deploy("./@token_contract", "Token") + .with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestToken0000000000000000000000", + "TT00000000000000000000000000000", + 18, + )) + .to_address(); + + let token1_address = env + .deploy("./@token_contract", "Token") + .with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestToken1000000000000000000000", + "TT10000000000000000000000000000", + 18, + )) + .to_address(); + + let liquidity_token_address = env + .deploy("./@token_contract", "Token") + .with_public_void_initializer(Token::interface().constructor( + token_admin, + "TestLiquidityToken0000000000000", + "TLT0000000000000000000000000000", + 18, + )) + .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(); + + // 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; + + mint_to_private(&mut env, token0_address, liquidity_provider, lp_balance_0); + check_private_balance(token0_address, liquidity_provider, lp_balance_0); + + mint_to_private(&mut env, token1_address, liquidity_provider, lp_balance_1); + check_private_balance(token1_address, liquidity_provider, lp_balance_1); + + 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); + ( + &mut env, amm_address, token0_address, token1_address, liquidity_token_address, + liquidity_provider, swapper, lp_balance_0, lp_balance_1, swapper_balance_0, + ) +} 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 e92c91f908d4..e3e1e2e1d1b1 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 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 000000000000..0424f2c5c79b --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -0,0 +1,155 @@ +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 + 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 (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(); + + 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(); + + // 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, + ); + }); +}); From e2851e4f88f8fe3827fba6386849788ff44f4511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 22 Nov 2024 21:58:40 +0000 Subject: [PATCH 02/15] Fix warnings --- .../noir-contracts/contracts/amm_contract/src/main.nr | 3 +-- .../noir-contracts/contracts/amm_contract/src/state.nr | 8 ++++---- 2 files changed, 5 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 a1a4cdb00130..7d72cdb489cc 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -34,8 +34,7 @@ contract AMM { use crate::{lib::{get_amount_in, get_amount_out, get_amounts_to_add}, state::State}; use dep::aztec::{ macros::{ - events::event, - functions::{initializer, internal, private, public, view}, + functions::{initializer, internal, private, public}, storage::storage, }, prelude::{AztecAddress, SharedImmutable}, diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr index b1d126af2b4c..9a860d2fd1f9 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr @@ -5,10 +5,10 @@ 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, +pub struct State { + pub token0: AztecAddress, + pub token1: AztecAddress, + pub liquidity_token: AztecAddress, } // Note: I could not get #[derive(Serialize)] to work so I had to implement it manually. From c80f926c6b94aef8d24cec9a7c5cdf3c704dac53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 25 Nov 2024 15:41:58 +0000 Subject: [PATCH 03/15] Small readability edits --- .../amm_contract/src/{state.nr => config.nr} | 14 +- .../contracts/amm_contract/src/lib.nr | 97 +++--- .../contracts/amm_contract/src/main.nr | 276 +++++++++--------- yarn-project/end-to-end/src/e2e_amm.test.ts | 214 +++++++------- 4 files changed, 317 insertions(+), 284 deletions(-) rename noir-projects/noir-contracts/contracts/amm_contract/src/{state.nr => config.nr} (75%) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr b/noir-projects/noir-contracts/contracts/amm_contract/src/config.nr similarity index 75% rename from noir-projects/noir-contracts/contracts/amm_contract/src/state.nr rename to noir-projects/noir-contracts/contracts/amm_contract/src/config.nr index 9a860d2fd1f9..c83648c4a395 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/state.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/config.nr @@ -1,26 +1,26 @@ use dep::aztec::protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}}; -global STATE_LENGTH: u32 = 3; +global CONFIG_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). -pub struct State { +pub struct Config { pub token0: AztecAddress, pub token1: AztecAddress, pub 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] { +impl Serialize for Config { + fn serialize(self: Self) -> [Field; CONFIG_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 { +impl Deserialize for Config { + fn deserialize(fields: [Field; CONFIG_LENGTH]) -> Self { + Self { token0: AztecAddress::from_field(fields[0]), token1: AztecAddress::from_field(fields[1]), liquidity_token: AztecAddress::from_field(fields[2]), 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 29faca8f887d..c17711c3d089 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr @@ -1,61 +1,72 @@ -/// 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 -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: U128, reserve_in: U128, reserve_out: U128) -> U128 { +/// Given an input amount of an asset and pair balances, returns the maximum output amount of the other asset. +pub fn get_amount_out(amount_in: U128, balance_in: U128, balance_out: U128) -> U128 { + // Based on Uniswap v2: https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L43 assert(amount_in > U128::zero(), "INSUFFICIENT_INPUT_AMOUNT"); - assert((reserve_in > U128::zero()) & (reserve_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); + assert((balance_in > U128::zero()) & (balance_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 * U128::from_integer(1000) + amount_in_with_fee; + let numerator = amount_in_with_fee * balance_out; + let denominator = balance_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: U128, reserve_in: U128, reserve_out: U128) -> U128 { +/// Given an output amount of an asset and pair balances, returns a required input amount of the other asset. +pub fn get_amount_in(amount_out: U128, balance_in: U128, balance_out: U128) -> U128 { + // Based on Uniswap v2: https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L53 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); + assert((balance_in > U128::zero()) & (balance_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); + let numerator = balance_in * amount_out * U128::from_integer(1000); + let denominator = (balance_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. +/// Given the desired amounts and balances 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_max: U128, + amount1_max: U128, amount0_min: U128, amount1_min: U128, - reserve0: U128, - reserve1: U128, + balance0: U128, + balance1: 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; + // When adding tokens, both balances must grow by the same ratio, which means that their spot price is unchanged. + // Since any swaps would affect these ratios, liquidity providers supply a range of minimum and maximum balances + // they are willing to supply for each token (which translates to minimum and maximum relative prices of the + // tokens, preventing loss of value outside of this range due to e.g. front-running). + + if (balance0 == U128::zero()) | (balance1 == U128::zero()) { + // The token balances should only be zero when initializing the pool. In this scenario there is no prior ratio + // to follow so we simply transfer the full maximum balance - it is up to the caller to make sure that the ratio + // they've chosen results in a a reasonable spot price. + (amount0_max, amount1_max) + } else { + // There is a huge number of amount combinations that respect the minimum and maximum for each token, but we'll + // only consider the two scenarios in which one of the amounts is the maximum amount. + + // First we calculate the token1 amount that'd need to be supplied if we used the maximum amount for token0. + let amount1_equivalent = get_equivalent_amount(amount0_max, balance0, balance1); + if (amount1_equivalent <= amount1_max) { + assert(amount1_equivalent >= amount1_min, "AMOUNT_1_BELOW_MINIMUM"); + (amount0_max, amount1_equivalent) } 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; + // If the max amount for token0 results in a token1 amount larger than the maximum, then we try with the + // maximum token1 amount, hoping that it'll result in a token0 amount larger than the minimum. + let amount0_equivalent = get_equivalent_amount(amount1_max, balance1, balance0); + // This should never happen, as it'd imply that the maximum is lower than the minimum. + assert(amount0_equivalent <= amount0_max); + + assert(amount0_equivalent >= amount0_min, "AMOUNT_0_BELOW_MINIMUM"); + (amount0_equivalent, amount1_max) } } +} + +/// Given some amount of an asset and pair balances, returns an equivalent amount of the other asset. Tokens should be +/// added and removed from the Pool respecting this ratio. +fn get_equivalent_amount(amount0: U128, balance0: U128, balance1: U128) -> U128 { + // Based on Uniswap v2: https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 + assert((balance0 > U128::zero()) & (balance1 > U128::zero()), "INSUFFICIENT_LIQUIDITY"); - (amount0, amount1) + // This is essentially the Rule of Three, since we're computing proportional ratios. Note we divide at the end to + // avoid introducing too much error due to truncation. + (amount0 * balance1) / balance0 } 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 7d72cdb489cc..389601cf1931 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,5 @@ mod lib; -mod state; +mod config; mod test; use dep::aztec::macros::aztec; @@ -10,6 +10,10 @@ use dep::aztec::macros::aztec; /// - Anyone can observe **what actions** were performed. /// - All amounts involved are visible, but **who** performed the action remains private. /// +/// Unlike most Ethereum AMMs, the AMM contract is not itself the token that tracks participation of liquidity +/// providers, mostly due to Noir lacking inheritance as a feature. Instead, the AMM is expected to have mint and burn +/// permission over an external token contract. +/// /// **Note:** /// This is purely a demonstration. The **Aztec team** does not consider this the optimal design for building a DEX. /// @@ -31,12 +35,9 @@ 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}, state::State}; + use crate::{config::Config, lib::{get_amount_in, get_amount_out, get_amounts_to_add}}; use dep::aztec::{ - macros::{ - functions::{initializer, internal, private, public}, - storage::storage, - }, + macros::{functions::{initializer, internal, private, public}, storage::storage}, prelude::{AztecAddress, SharedImmutable}, }; use dep::token::Token; @@ -46,88 +47,78 @@ contract AMM { // 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, + config: SharedImmutable, } - /// 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. + /// Amount of liquidity which gets locked when liquidity is provided for the first time. Its purpose 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); + /// We set it to 100 times the minimum liquidity. That way the first LP gets 99% of the value of their deposit. + global INITIAL_LIQUIDITY = U128::from_integer(100000); - // 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(#9480): Either deploy the liquidity contract in the constructor or verify it that it corresponds to what - // this contract expects. + // this contract expects (i.e. that the AMM has permission to mint and burn). #[public] #[initializer] fn constructor(token0: AztecAddress, token1: AztecAddress, liquidity_token: AztecAddress) { - storage.state.initialize(State { token0, token1, liquidity_token }); + storage.config.initialize(Config { 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. The identity of the liquidity provider not revealed, but the action and + /// amounts is. This function receives the minimum and maximum number of tokens the caller is willing to add, in + /// order to account for changing market conditions, and will try to add as many tokens as possible. + // `nonce` can be any non-zero value, as it's only used to isolate token transfer authwits to this specific call. #[private] fn add_liquidity( - amount0_desired: Field, - amount1_desired: Field, + amount0_max: Field, + amount1_max: Field, amount0_min: Field, amount1_min: Field, nonce: 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); - - assert( - amount0_desired > U128::zero() & amount1_desired > U128::zero(), - "INSUFFICIENT_INPUT_AMOUNTS", - ); + assert(amount0_min.lt(amount0_max) | (amount0_min == amount0_max), "INCORRECT_TOKEN0_LIMITS"); + assert(amount1_min.lt(amount1_max) | (amount1_min == amount1_max), "INCORRECT_TOKEN1_LIMITS"); + assert(0.lt(amount0_max) & 0.lt(amount1_max), "INSUFFICIENT_INPUT_AMOUNTS"); - let state = storage.state.read_private(); + let config = storage.config.read_private(); - let token0 = Token::at(state.token0); - let token1 = Token::at(state.token1); - let liquidity_token = Token::at(state.liquidity_token); + let token0 = Token::at(config.token0); + let token1 = Token::at(config.token1); + let liquidity_token = Token::at(config.liquidity_token); - // We transfer the desired amounts of tokens to this contract. + // We don't yet know how many tokens the sender will actually supply - that can only be computed during public + // execution since the amounts supplied must have the same ratio as the live balances. We therefore transfer the + // maximum amounts here, and prepare partial notes that return the change to the sender (if any). token0 - .transfer_to_public( - context.msg_sender(), - context.this_address(), - amount0_desired.to_integer(), - nonce, - ) - .call(&mut context); - token1 - .transfer_to_public( - context.msg_sender(), - context.this_address(), - amount1_desired.to_integer(), - nonce, - ) + .transfer_to_public(context.msg_sender(), context.this_address(), amount0_max, 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_hiding_point_slot = token0.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + + token1 + .transfer_to_public(context.msg_sender(), context.this_address(), amount1_max, nonce) + .call(&mut context); let refund_token1_hiding_point_slot = 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_private_balance_increase(context.msg_sender()).call(&mut context); + // The number of liquidity tokens to mint for the caller depends on both the live balances and the amount + // supplied, both of which can only be known during public execution. We therefore prepare a partial note that + // will get completed via minting. + let liquidity_hiding_point_slot = liquidity_token + .prepare_private_balance_increase(context.msg_sender()) + .call(&mut context); + + // We then complete the flow in public. Note that the type of operation and amounts will all be publicly known, + // but the identity of the caller is not revealed despite us being able to send tokens to them by completing the + // partial notees. AMM::at(context.this_address()) ._add_liquidity( - state, + config, refund_token0_hiding_point_slot, refund_token1_hiding_point_slot, liquidity_hiding_point_slot, - amount0_desired.to_integer(), - amount1_desired.to_integer(), + amount0_max, + amount1_max, amount0_min, amount1_min, ) @@ -137,87 +128,101 @@ contract AMM { #[public] #[internal] fn _add_liquidity( - // We pass the state as an argument in order to not have to read it from public storage again. - state: State, + config: Config, // We could read this in public, but it's cheaper to receive from private refund_token0_hiding_point_slot: Field, refund_token1_hiding_point_slot: Field, liquidity_hiding_point_slot: Field, - amount0_desired: Field, - amount1_desired: Field, + amount0_max: Field, + amount1_max: 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_max = U128::from_integer(amount0_max); + let amount1_max = U128::from_integer(amount1_max); 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); + let token0 = Token::at(config.token0); + let token1 = Token::at(config.token1); + let liquidity_token = Token::at(config.liquidity_token); - let reserve0_with_amount0_desired = U128::from_integer(token0 + // We read the current AMM balance of both tokens. Note that by the time this function is called the token + // transfers have already been completed (since those calls were enqueued before this call), and so we need to + // substract the transfer amount to get the pre-deposit balance. + let balance0_plus_amount0_max = U128::from_integer(token0 .balance_of_public(context.this_address()) .view(&mut context)); - let reserve1_with_amount1_desired = U128::from_integer(token1 + let balance0 = balance0_plus_amount0_max - amount0_max; + + let balance1_plus_amount1_max = U128::from_integer(token1 .balance_of_public(context.this_address()) .view(&mut context)); + let balance1 = balance1_plus_amount1_max - amount1_max; - 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 + // With the current balances known, we can calculate the token amounts to the pool, respecting the user's + // minimum deposit preferences. let (amount0, amount1) = get_amounts_to_add( - amount0_desired, - amount1_desired, + amount0_max, + amount1_max, amount0_min, amount1_min, - reserve0, - reserve1, + balance0, + balance1, ); - let refund_amount_token0 = amount0_desired - amount0; - let refund_amount_token1 = amount1_desired - amount1; + // Return any excess from the original token deposits. + let refund_amount_token0 = amount0_max - amount0; + let refund_amount_token1 = amount1_max - amount1; - // 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. + // We can simply skip the refund if the amount to return is 0 in order to save gas: the partial note will + // simply stay in public storage and not be completed, but this is not an issue. if (refund_amount_token0 > U128::zero()) { token0 .finalize_transfer_to_private( - refund_token0_hiding_point_slot, refund_amount_token0.to_integer(), + refund_token0_hiding_point_slot, ) .call(&mut context); } if (refund_amount_token1 > U128::zero()) { token1 .finalize_transfer_to_private( - refund_token1_hiding_point_slot, refund_amount_token1.to_integer(), + refund_token1_hiding_point_slot, ) .call(&mut context); } - // Calculate the amount of liquidity tokens to mint + // With the deposit amounts known, we can compute the number of liquidity tokens to mint and finalize the + // depositor's partial note. let total_supply = U128::from_integer(liquidity_token.total_supply().view(&mut context)); - 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). - liquidity = INITIAL_LIQUIDITY; + let liquidity_amount = if total_supply != U128::zero() { + // The liquidity token supply increases by the same ratio as the balances. In case one of the token balances + // increased with a ratio different from the other one, we simply take the smallest value. + std::cmp::min( + (amount0 * total_supply) / balance0, + (amount1 * total_supply) / balance1, + ) + } else { + // The zero total supply case (i.e. pool initialization) is special as we can't increase the supply + // proportionally. We instead set the initial liquidity to an arbitrary amount. + // We could set the initial liquidity to be equal to the pool invariant (i.e. sqrt(amount0 * amount1)) if + // we wanted to collect protocol fees over swap fees (in the style of Uniswap v2), but we choose not to in + // order to keep things simple. + + // As part of initialization, we mint some tokens to the zero address to 'lock' them (i.e. make them + // impossible to redeem), guaranteeing total supply will never be zero again. 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, - amount1 * total_supply / reserve1, - ); - } - assert(liquidity > U128::zero(), "INSUFFICIENT_LIQUIDITY_MINTED"); + .call(&mut context); + + INITIAL_LIQUIDITY + }; + + assert(liquidity_amount > U128::zero(), "INSUFFICIENT_LIQUIDITY_MINTED"); liquidity_token - .finalize_mint_to_private(liquidity.to_integer(), liquidity_hiding_point_slot) + .finalize_mint_to_private(liquidity_amount.to_integer(), liquidity_hiding_point_slot) .call(&mut context); } @@ -227,15 +232,15 @@ contract AMM { /// TODO(#8271): Type the args as U128 #[private] fn remove_liquidity(liquidity: Field, amount0_min: Field, amount1_min: Field, nonce: Field) { - let state = storage.state.read_private(); + let config = storage.config.read_private(); - let liquidity_token = Token::at(state.liquidity_token); - let token0 = Token::at(state.token0); - let token1 = Token::at(state.token1); + let liquidity_token = Token::at(config.liquidity_token); + let token0 = Token::at(config.token0); + let token1 = Token::at(config.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. + // call would reveal who the owner was. The only way to pbalance their identity is to first privately transfer. liquidity_token .transfer_to_public(context.msg_sender(), context.this_address(), liquidity, nonce) .call(&mut context); @@ -246,7 +251,7 @@ contract AMM { AMM::at(context.this_address()) ._remove_liquidity( - state, + config, token0_hiding_point_slot, token1_hiding_point_slot, liquidity, @@ -260,7 +265,7 @@ contract AMM { #[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, + config: Config, token0_hiding_point_slot: Field, token1_hiding_point_slot: Field, liquidity: Field, @@ -272,23 +277,23 @@ contract AMM { 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); + let token0 = Token::at(config.token0); + let token1 = Token::at(config.token1); + let liquidity_token = Token::at(config.liquidity_token); - // We get the reserves and the liquidity token total supply. - let reserve0 = U128::from_integer(token0.balance_of_public(context.this_address()).view( + // We get the balances and the liquidity token total supply. + let balance0 = 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( + let balance1 = 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. - let amount0 = liquidity * reserve0 / total_supply; - let amount1 = liquidity * reserve1 / total_supply; + let amount0 = liquidity * balance0 / total_supply; + let amount1 = liquidity * balance1 / total_supply; // We check if the amounts are greater than the minimum amounts the user is willing to accept. assert(amount0 >= amount0_min, "INSUFFICIENT_0_AMOUNT"); @@ -318,12 +323,12 @@ contract AMM { amount_out_min: Field, nonce: Field, ) { - let state = storage.state.read_private(); + let config = storage.config.read_private(); // 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 == config.token0) | (token_in == config.token1)); + assert((token_out == config.token0) | (token_out == config.token1)); let token_in_contract = Token::at(token_in); let token_out_contract = Token::at(token_out); @@ -332,8 +337,9 @@ contract AMM { token_in_contract .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_private_balance_increase(context.msg_sender()).call(&mut context); + let token_out_hiding_point_slot = token_out_contract + .prepare_private_balance_increase(context.msg_sender()) + .call(&mut context); AMM::at(context.this_address()) ._swap_exact_tokens_for_tokens( @@ -362,17 +368,17 @@ contract AMM { 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_contract + // We get the balances. The `amount_in` was already transferred to this contract so we need to subtract it. + let balance_in_plus_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_contract + let balance_in = balance_in_plus_amount_in - amount_in; + let balance_out = U128::from_integer(token_out_contract .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); + let amount_out = get_amount_out(amount_in, balance_in, balance_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); // Transfer the output token to the user. @@ -395,12 +401,12 @@ contract AMM { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount_out = U128::from_integer(amount_out); - let state = storage.state.read_private(); + let config = storage.config.read_private(); // 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 == config.token0) | (token_in == config.token1)); + assert((token_out == config.token0) | (token_out == config.token1)); let token_in_contract = Token::at(token_in); let token_out_contract = Token::at(token_out); @@ -410,10 +416,12 @@ contract AMM { 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_private_balance_increase(context.msg_sender()).call(&mut context); - let token_out_hiding_point_slot = - token_out_contract.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + let refund_token_in_hiding_point_slot = token_in_contract + .prepare_private_balance_increase(context.msg_sender()) + .call(&mut context); + let token_out_hiding_point_slot = token_out_contract + .prepare_private_balance_increase(context.msg_sender()) + .call(&mut context); AMM::at(context.this_address()) ._swap_tokens_for_exact_tokens( @@ -444,17 +452,17 @@ contract AMM { 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_contract + // We get the balances. The `amount_in_max` was already transferred to this contract so we need to subtract it. + let balance_in_plus_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_contract + let balance_in = balance_in_plus_amount_in_max - amount_in_max; + let balance_out = U128::from_integer(token_out_contract .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); + let amount_in = get_amount_in(amount_out, balance_in, balance_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. 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 0424f2c5c79b..00d5a65d0d1b 100644 --- a/yarn-project/end-to-end/src/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -2,14 +2,13 @@ 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 { describe } from 'node:test'; 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); @@ -50,106 +49,121 @@ describe('AMM', () => { await mintTokensToPrivate(token0, adminWallet, swapper.getAddress(), swapperBalance0); }); - afterAll(() => teardown()); - - it('full flow', async () => { - // ADDING LIQUIDITY - 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 (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(); - - 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, - ), + afterAll(async () => await teardown()); + + describe('full flow', () => { + // This is an integration test in which we perform an entire run of the happy path. Thorough unit testing is not + // included. + it.only('add liquidity', async () => { + const amount0Max = lpBalance0 / 2n; + const amount1Max = 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. These + // authwits are for the full amount, since the AMM will first transfer that to itself, and later refund any excess + // during public execution. Note that we need to tell the AMM of the nonce we used in the authwit. + const nonceForAuthwits = Fr.random(); + + await liquidityProvider.createAuthWit({ + caller: amm.address, + action: token0.methods.transfer_to_public( + liquidityProvider.getAddress(), + amm.address, + amount0Max, + nonceForAuthwits, + ), + }); + await liquidityProvider.createAuthWit({ + caller: amm.address, + action: token1.methods.transfer_to_public( + liquidityProvider.getAddress(), + amm.address, + amount1Max, + nonceForAuthwits, + ), + }); + + await amm + .withWallet(liquidityProvider) + .methods.add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, nonceForAuthwits) + .send() + .wait(); + + // Since the LP was the first one to enter the pool, the maximum amounts of tokens should have been deposited. + expect(await token0.methods.balance_of_private(amm.address).simulate()).toEqual( + amount0Max, + ); + expect(await token0.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toEqual( + lpBalance0 - amount0Max, + ); + expect(await token1.methods.balance_of_private(amm.address).simulate()).toEqual( + amount1Max, + ); + expect(await token1.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toEqual( + lpBalance1 - amount1Max, + ); + + // Liquidity tokens should also be minted for the liquidity provider, as well as locked at the zero address. + expect( + await liquidityToken.methods.balance_of_private(liquidityProvider.getAddress()).simulate(), + ).toEqual(1000n); + expect( + await liquidityToken.methods.total_supply().simulate(), + ).toEqual(100000n); }); - await amm - .withWallet(liquidityProvider) - .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), + it('the rest', async () => { + // 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, + ); }); - - // 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, - ); }); }); From 0fde9ba6837d6e1b70a5e5ad16df99b7e5acbb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 26 Nov 2024 13:37:36 +0000 Subject: [PATCH 04/15] Migrate to publicimmutable --- .../contracts/amm_contract/src/main.nr | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 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 389601cf1931..f368fb3f3e86 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -38,16 +38,13 @@ contract AMM { use crate::{config::Config, lib::{get_amount_in, get_amount_out, get_amounts_to_add}}; use dep::aztec::{ macros::{functions::{initializer, internal, private, public}, storage::storage}, - prelude::{AztecAddress, SharedImmutable}, + prelude::{AztecAddress, PublicImmutable}, }; use dep::token::Token; #[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. - config: SharedImmutable, + config: PublicImmutable, } /// Amount of liquidity which gets locked when liquidity is provided for the first time. Its purpose is to prevent @@ -80,7 +77,7 @@ contract AMM { assert(amount1_min.lt(amount1_max) | (amount1_min == amount1_max), "INCORRECT_TOKEN1_LIMITS"); assert(0.lt(amount0_max) & 0.lt(amount1_max), "INSUFFICIENT_INPUT_AMOUNTS"); - let config = storage.config.read_private(); + let config = storage.config.read(); let token0 = Token::at(config.token0); let token1 = Token::at(config.token1); @@ -232,7 +229,7 @@ contract AMM { /// TODO(#8271): Type the args as U128 #[private] fn remove_liquidity(liquidity: Field, amount0_min: Field, amount1_min: Field, nonce: Field) { - let config = storage.config.read_private(); + let config = storage.config.read(); let liquidity_token = Token::at(config.liquidity_token); let token0 = Token::at(config.token0); @@ -323,7 +320,7 @@ contract AMM { amount_out_min: Field, nonce: Field, ) { - let config = storage.config.read_private(); + let config = storage.config.read(); // We check the tokens are valid assert(token_in != token_out); @@ -401,7 +398,7 @@ contract AMM { // TODO(#8271): Type the args as U128 and nuke these ugly casts let amount_out = U128::from_integer(amount_out); - let config = storage.config.read_private(); + let config = storage.config.read(); // We check the tokens are valid assert(token_in != token_out); From 71a1266eb666e90e2f5286988b977566ce493681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 27 Nov 2024 22:46:26 +0000 Subject: [PATCH 05/15] Multiple tweaks --- .../src/core/libraries/ConstantsGen.sol | 4 +- .../aztec/src/macros/functions/interfaces.nr | 2 +- .../aztec-nr/aztec/src/macros/mod.nr | 14 +- .../contracts/amm_contract/src/lib.nr | 31 +- .../contracts/amm_contract/src/main.nr | 259 ++++++++------- .../contracts/token_contract/src/main.nr | 3 +- .../src/test/transfer_to_private.nr | 8 +- .../crates/types/src/constants.nr | 2 +- yarn-project/circuits.js/src/constants.gen.ts | 4 +- yarn-project/end-to-end/src/e2e_amm.test.ts | 304 ++++++++++++++---- 10 files changed, 431 insertions(+), 200 deletions(-) diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 4aea0600b23d..d0e4bc038fb2 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -18,7 +18,7 @@ library Constants { uint256 internal constant ARGS_LENGTH = 16; uint256 internal constant MAX_NOTE_HASHES_PER_CALL = 16; uint256 internal constant MAX_NULLIFIERS_PER_CALL = 16; - uint256 internal constant MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL = 4; + uint256 internal constant MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL = 5; uint256 internal constant MAX_ENQUEUED_CALLS_PER_CALL = 16; uint256 internal constant MAX_L2_TO_L1_MSGS_PER_CALL = 2; uint256 internal constant MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_CALL = 64; @@ -202,7 +202,7 @@ library Constants { uint256 internal constant TX_REQUEST_LENGTH = 12; uint256 internal constant TOTAL_FEES_LENGTH = 1; uint256 internal constant HEADER_LENGTH = 24; - uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 490; + uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 498; uint256 internal constant PUBLIC_CIRCUIT_PUBLIC_INPUTS_LENGTH = 866; uint256 internal constant PRIVATE_CONTEXT_INPUTS_LENGTH = 37; uint256 internal constant FEE_RECIPIENT_LENGTH = 2; diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr index ff768788ffff..353943c94df8 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr @@ -131,7 +131,7 @@ pub comptime fn stub_fn(f: FunctionDefinition) -> Quoted { $args let selector = dep::aztec::protocol_types::abis::function_selector::FunctionSelector::from_field($fn_selector); $call_interface_name { - target_contract: self.target_contract, + target_contract: self.addr, selector, name: $fn_name_str, $args_hash diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 924c5bcf8e01..feb51002963f 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -71,20 +71,20 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { quote { pub struct $module_name { - target_contract: dep::aztec::protocol_types::address::AztecAddress + addr: dep::aztec::protocol_types::address::AztecAddress } impl $module_name { $fn_stubs_quote pub fn at( - target_contract: aztec::protocol_types::address::AztecAddress + addr: aztec::protocol_types::address::AztecAddress ) -> Self { - Self { target_contract } + Self { addr } } pub fn interface() -> Self { - Self { target_contract: aztec::protocol_types::address::AztecAddress::zero() } + Self { addr: aztec::protocol_types::address::AztecAddress::zero() } } $storage_layout_getter @@ -92,14 +92,14 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { #[contract_library_method] pub fn at( - target_contract: aztec::protocol_types::address::AztecAddress + addr: aztec::protocol_types::address::AztecAddress ) -> $module_name { - $module_name { target_contract } + $module_name { addr } } #[contract_library_method] pub fn interface() -> $module_name { - $module_name { target_contract: aztec::protocol_types::address::AztecAddress::zero() } + $module_name { addr: aztec::protocol_types::address::AztecAddress::zero() } } $library_storage_layout_getter 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 c17711c3d089..7d0537a07840 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr @@ -1,19 +1,33 @@ /// Given an input amount of an asset and pair balances, returns the maximum output amount of the other asset. pub fn get_amount_out(amount_in: U128, balance_in: U128, balance_out: U128) -> U128 { - // Based on Uniswap v2: https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L43 assert(amount_in > U128::zero(), "INSUFFICIENT_INPUT_AMOUNT"); assert((balance_in > U128::zero()) & (balance_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); + + // The expression below is: + // (amount_in * 997 * balance_out) / (balance_in * 10000 + amount_in * 997) + // which is equivalent to: + // balance_out * ((amount_in * 0.997) / (balance_in + amount_in * 0.997)) + // resulting in an implicit 0.3% fee on the amount in, as the fee tokens are not taken into consideration. + let amount_in_with_fee = amount_in * U128::from_integer(997); let numerator = amount_in_with_fee * balance_out; let denominator = balance_in * U128::from_integer(1000) + amount_in_with_fee; numerator / denominator + } /// Given an output amount of an asset and pair balances, returns a required input amount of the other asset. pub fn get_amount_in(amount_out: U128, balance_in: U128, balance_out: U128) -> U128 { - // Based on Uniswap v2: https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L53 assert(amount_out > U128::zero(), "INSUFFICIENT_OUTPUT_AMOUNT"); assert((balance_in > U128::zero()) & (balance_out > U128::zero()), "INSUFFICIENT_LIQUIDITY"); + + // The expression below is: + // (balance_in * amount_out * 1000) / (balance_out - amout_out * 997) + 1 + // which is equivalent to: + // balance_in * (amount_out / (balance_in + amount_in)) * 1/0.997 + 1 + // resulting in an implicit 0.3% fee on the amount in, as the fee tokens are not taken into consideration. The +1 + // at the end ensures the rounding error favors the pool. + let numerator = balance_in * amount_out * U128::from_integer(1000); let denominator = (balance_out - amount_out) * U128::from_integer(997); (numerator / denominator) + U128::from_integer(1) @@ -60,10 +74,21 @@ pub fn get_amounts_to_add( } } +/// Returns the amount of tokens to return to a liquidity provider when they remove liquidity from the pool. +pub fn get_amounts_on_remove( + to_burn: U128, + total_supply: U128, + balance0: U128, + balance1: U128, +) -> (U128, U128) { + // Since the liquidity token tracks ownership of the pool, the liquidity provider gets a proportional share of each + // token. + (to_burn * balance0 / total_supply, to_burn * balance1 / total_supply) +} + /// Given some amount of an asset and pair balances, returns an equivalent amount of the other asset. Tokens should be /// added and removed from the Pool respecting this ratio. fn get_equivalent_amount(amount0: U128, balance0: U128, balance1: U128) -> U128 { - // Based on Uniswap v2: https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L36 assert((balance0 > U128::zero()) & (balance1 > U128::zero()), "INSUFFICIENT_LIQUIDITY"); // This is essentially the Rule of Three, since we're computing proportional ratios. Note we divide at the end to 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 f368fb3f3e86..872cb5d06fd1 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -35,7 +35,10 @@ use dep::aztec::macros::aztec; /// `AMM.private_fn --> AMM.public_fn --> ExternalContract.fn --> AMM.public_fn`. #[aztec] contract AMM { - use crate::{config::Config, lib::{get_amount_in, get_amount_out, get_amounts_to_add}}; + use crate::{ + config::Config, + lib::{get_amount_in, get_amount_out, get_amounts_on_remove, get_amounts_to_add}, + }; use dep::aztec::{ macros::{functions::{initializer, internal, private, public}, storage::storage}, prelude::{AztecAddress, PublicImmutable}, @@ -50,8 +53,8 @@ contract AMM { /// Amount of liquidity which gets locked when liquidity is provided for the first time. Its purpose 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 100 times the minimum liquidity. That way the first LP gets 99% of the value of their deposit. - global INITIAL_LIQUIDITY = U128::from_integer(100000); + /// We set it to 99 times the minimum liquidity. That way the first LP gets 99% of the value of their deposit. + global INITIAL_LIQUIDITY = U128::from_integer(99000); // TODO(#9480): Either deploy the liquidity contract in the constructor or verify it that it corresponds to what // this contract expects (i.e. that the AMM has permission to mint and burn). @@ -61,10 +64,13 @@ contract AMM { storage.config.initialize(Config { token0, token1, liquidity_token }); } - /// Privately adds liquidity to the pool. The identity of the liquidity provider not revealed, but the action and - /// amounts is. This function receives the minimum and maximum number of tokens the caller is willing to add, in - /// order to account for changing market conditions, and will try to add as many tokens as possible. - // `nonce` can be any non-zero value, as it's only used to isolate token transfer authwits to this specific call. + /// Privately adds liquidity to the pool. This function receives the minimum and maximum number of tokens the caller + /// is willing to add, in order to account for changing market conditions, and will try to add as many tokens as + /// possible. + /// + /// `nonce` can be any non-zero value, as it's only used to isolate token transfer authwits to this specific call. + /// + /// The identity of the liquidity provider is not revealed, but the action and amounts are. #[private] fn add_liquidity( amount0_max: Field, @@ -73,8 +79,14 @@ contract AMM { amount1_min: Field, nonce: Field, ) { - assert(amount0_min.lt(amount0_max) | (amount0_min == amount0_max), "INCORRECT_TOKEN0_LIMITS"); - assert(amount1_min.lt(amount1_max) | (amount1_min == amount1_max), "INCORRECT_TOKEN1_LIMITS"); + assert( + amount0_min.lt(amount0_max) | (amount0_min == amount0_max), + "INCORRECT_TOKEN0_LIMITS", + ); + assert( + amount1_min.lt(amount1_max) | (amount1_min == amount1_max), + "INCORRECT_TOKEN1_LIMITS", + ); assert(0.lt(amount0_max) & 0.lt(amount1_max), "INSUFFICIENT_INPUT_AMOUNTS"); let config = storage.config.read(); @@ -83,27 +95,28 @@ contract AMM { let token1 = Token::at(config.token1); let liquidity_token = Token::at(config.liquidity_token); + let sender = context.msg_sender(); + // We don't yet know how many tokens the sender will actually supply - that can only be computed during public // execution since the amounts supplied must have the same ratio as the live balances. We therefore transfer the // maximum amounts here, and prepare partial notes that return the change to the sender (if any). - token0 - .transfer_to_public(context.msg_sender(), context.this_address(), amount0_max, nonce) - .call(&mut context); + token0.transfer_to_public(sender, context.this_address(), amount0_max, nonce).call( + &mut context, + ); let refund_token0_hiding_point_slot = - token0.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + token0.prepare_private_balance_increase(sender, sender).call(&mut context); - token1 - .transfer_to_public(context.msg_sender(), context.this_address(), amount1_max, nonce) - .call(&mut context); + token1.transfer_to_public(sender, context.this_address(), amount1_max, nonce).call( + &mut context, + ); let refund_token1_hiding_point_slot = - token1.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + token1.prepare_private_balance_increase(sender, sender).call(&mut context); // The number of liquidity tokens to mint for the caller depends on both the live balances and the amount // supplied, both of which can only be known during public execution. We therefore prepare a partial note that // will get completed via minting. - let liquidity_hiding_point_slot = liquidity_token - .prepare_private_balance_increase(context.msg_sender()) - .call(&mut context); + let liquidity_hiding_point_slot = + liquidity_token.prepare_private_balance_increase(sender, sender).call(&mut context); // We then complete the flow in public. Note that the type of operation and amounts will all be publicly known, // but the identity of the caller is not revealed despite us being able to send tokens to them by completing the @@ -223,10 +236,12 @@ contract AMM { .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. - /// TODO(#8271): Type the args as U128 + /// Privately removes liquidity from the pool. This function receives how many liquidity tokens to burn, and the + /// minimum number of tokens the caller is willing to receive, in order to account for changing market conditions. + /// + /// `nonce` can be any non-zero value, as it's only used to isolate token transfer authwits to this specific call. + /// + /// The identity of the liquidity provider is not revealed, but the action and amounts are. #[private] fn remove_liquidity(liquidity: Field, amount0_min: Field, amount1_min: Field, nonce: Field) { let config = storage.config.read(); @@ -235,23 +250,31 @@ contract AMM { let token0 = Token::at(config.token0); let token1 = Token::at(config.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 pbalance their identity is to first privately transfer. - liquidity_token - .transfer_to_public(context.msg_sender(), context.this_address(), liquidity, nonce) - .call(&mut context); + let sender = context.msg_sender(); + + // Liquidity tokens are burned when liquidity is removed in order to reduce the total supply. However, we lack + // a function to privately burn, so we instead transfer the tokens into the AMM's public balance, and them have + // the AMM publicly burn its own tokens. + liquidity_token.transfer_to_public(sender, context.this_address(), liquidity, nonce).call( + &mut context, + ); + + // We don't yet know how many tokens the sender will get - that can only be computed during public execution + // since the it depends on the live balances. We therefore simply prepare partial notes to the sender. let token0_hiding_point_slot = - token0.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + token0.prepare_private_balance_increase(sender, sender).call(&mut context); let token1_hiding_point_slot = - token1.prepare_private_balance_increase(context.msg_sender()).call(&mut context); + token1.prepare_private_balance_increase(sender, sender).call(&mut context); + // We then complete the flow in public. Note that the type of operation and amounts will all be publicly known, + // but the identity of the caller is not revealed despite us being able to send tokens to them by completing the + // partial notees. AMM::at(context.this_address()) ._remove_liquidity( config, + liquidity, token0_hiding_point_slot, token1_hiding_point_slot, - liquidity, amount0_min, amount1_min, ) @@ -261,11 +284,10 @@ contract AMM { #[public] #[internal] fn _remove_liquidity( - // We pass the state as an argument in order to not have to read it from public storage again. - config: Config, + config: Config, // We could read this in public, but it's cheaper to receive from private + liquidity: Field, token0_hiding_point_slot: Field, token1_hiding_point_slot: Field, - liquidity: Field, amount0_min: Field, amount1_min: Field, ) { @@ -278,7 +300,8 @@ contract AMM { let token1 = Token::at(config.token1); let liquidity_token = Token::at(config.liquidity_token); - // We get the balances and the liquidity token total supply. + // We need the current balance of both tokens as well as the liquidity token total supply in order to compute + // the amounts to send the user. let balance0 = U128::from_integer(token0.balance_of_public(context.this_address()).view( &mut context, )); @@ -288,15 +311,13 @@ contract AMM { 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. - let amount0 = liquidity * balance0 / total_supply; - let amount1 = liquidity * balance1 / total_supply; - - // We check if the amounts are greater than the minimum amounts the user is willing to accept. + // are removing, and check that they are above the minimum amounts they requested. + let (amount0, amount1) = get_amounts_on_remove(liquidity, total_supply, balance0, balance1); 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. + // We can now burn the liquidity tokens that had been privately transferred into the AMM, as well as complete + // both partial notes. liquidity_token.burn_public(context.this_address(), liquidity.to_integer(), 0).call( &mut context, ); @@ -308,10 +329,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 + /// Privately swaps `amount_in` `token_in` tokens for at least `amount_out_mint` `token_out` tokens with the pool. + /// + /// `nonce` can be any non-zero value, as it's only used to isolate token transfer authwits to this specific call. + /// + /// The identity of the swapper is not revealed, but the action and amounts are. #[private] fn swap_exact_tokens_for_tokens( token_in: AztecAddress, @@ -322,20 +344,19 @@ contract AMM { ) { let config = storage.config.read(); - // We check the tokens are valid - assert(token_in != token_out); - assert((token_in == config.token0) | (token_in == config.token1)); - assert((token_out == config.token0) | (token_out == config.token1)); + assert((token_in == config.token0) | (token_in == config.token1), "TOKEN_IN_IS_INVALID"); + assert((token_out == config.token0) | (token_out == config.token1), "TOKEN_OUT_IS_INVALID"); + assert(token_in != token_out, "SAME_TOKEN_SWAP"); - let token_in_contract = Token::at(token_in); - let token_out_contract = Token::at(token_out); + let sender = context.msg_sender(); - // We transfer the `amount_in` to this contract and we prepare partial note for the output token. - token_in_contract - .transfer_to_public(context.msg_sender(), context.this_address(), amount_in, nonce) + // We transfer the full amount in, since it is an exact amount, and prepare a partial note for the amount out, + // which will only be known during public execution as it depends on the live balances. + Token::at(token_in) + .transfer_to_public(sender, context.this_address(), amount_in, nonce) .call(&mut context); - let token_out_hiding_point_slot = token_out_contract - .prepare_private_balance_increase(context.msg_sender()) + let token_out_hiding_point_slot = Token::at(token_out) + .prepare_private_balance_increase(sender, sender) .call(&mut context); AMM::at(context.this_address()) @@ -362,31 +383,32 @@ contract AMM { let amount_in = U128::from_integer(amount_in); let amount_out_min = U128::from_integer(amount_out_min); - let token_in_contract = Token::at(token_in); - let token_out_contract = Token::at(token_out); - - // We get the balances. The `amount_in` was already transferred to this contract so we need to subtract it. - let balance_in_plus_amount_in = U128::from_integer(token_in_contract + // In order to compute the amount to swap we need the live token balances. Note that at this state the token in + // transfer has already been completed as that function call was enqueued before this one. We therefore need to + // subtract the amount in to get the pre-swap balances. + let balance_in_plus_amount_in = U128::from_integer(Token::at(token_in) .balance_of_public(context.this_address()) .view(&mut context)); let balance_in = balance_in_plus_amount_in - amount_in; - let balance_out = U128::from_integer(token_out_contract + + let balance_out = U128::from_integer(Token::at(token_out) .balance_of_public(context.this_address()) .view(&mut context)); - // Calculate the amount of output token we will get. + // We can now compute the number of tokens to transfer and complete the partial note. let amount_out = get_amount_out(amount_in, balance_in, balance_out); assert(amount_out >= amount_out_min, "INSUFFICIENT_OUTPUT_AMOUNT"); - // Transfer the output token to the user. - token_out_contract + Token::at(token_out) .finalize_transfer_to_private(amount_out.to_integer(), token_out_hiding_point_slot) .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. + /// Privately swaps at most `amount_in_max` `token_in` tokens for `amount_out` `token_out` tokens with the pool. + /// + /// `nonce` can be any non-zero value, as it's only used to isolate token transfer authwits to this specific call. + /// + /// The identity of the swapper is not revealed, but the action and amounts are. #[private] fn swap_tokens_for_exact_tokens( token_in: AztecAddress, @@ -395,29 +417,28 @@ contract AMM { amount_in_max: Field, nonce: Field, ) { - // TODO(#8271): Type the args as U128 and nuke these ugly casts - let amount_out = U128::from_integer(amount_out); - let config = storage.config.read(); - // We check the tokens are valid - assert(token_in != token_out); - assert((token_in == config.token0) | (token_in == config.token1)); - assert((token_out == config.token0) | (token_out == config.token1)); + assert((token_in == config.token0) | (token_in == config.token1), "TOKEN_IN_IS_INVALID"); + assert((token_out == config.token0) | (token_out == config.token1), "TOKEN_OUT_IS_INVALID"); + assert(token_in != token_out, "SAME_TOKEN_SWAP"); - let token_in_contract = Token::at(token_in); - let token_out_contract = Token::at(token_out); + let sender = context.msg_sender(); - // We transfer the `amount_in_max` to this contract and we prepare partial notes for refund and for the output - // token. - 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_private_balance_increase(context.msg_sender()) + // We don't know how many tokens we'll receive from the user, since the swap amount will only be known during + // public execution as it depends on the live balances. We therefore transfer the full maximum amount and + // prepare partial notes both for the token out and the refund. + // Technically the token out note does not need to be partial, since we do know the amount out, but we do want + // to wait until the swap has been completed before commiting the note to the tree to avoid it being spent too + // early. + Token::at(token_in) + .transfer_to_public(sender, context.this_address(), amount_in_max, nonce) .call(&mut context); - let token_out_hiding_point_slot = token_out_contract - .prepare_private_balance_increase(context.msg_sender()) + let change_token_in_hiding_point_slot = + Token::at(token_in).prepare_private_balance_increase(sender, sender).call(&mut context); + + let token_out_hiding_point_slot = Token::at(token_out) + .prepare_private_balance_increase(sender, sender) .call(&mut context); AMM::at(context.this_address()) @@ -425,8 +446,8 @@ contract AMM { token_in, token_out, amount_in_max, - amount_out.to_integer(), - refund_token_in_hiding_point_slot, + amount_out, + change_token_in_hiding_point_slot, token_out_hiding_point_slot, ) .enqueue(&mut context); @@ -439,43 +460,59 @@ contract AMM { token_out: AztecAddress, amount_in_max: Field, amount_out: Field, - refund_token_in_hiding_point_slot: Field, + change_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); let amount_in_max = U128::from_integer(amount_in_max); - let token_in_contract = Token::at(token_in); - let token_out_contract = Token::at(token_out); - - // We get the balances. The `amount_in_max` was already transferred to this contract so we need to subtract it. - let balance_in_plus_amount_in_max = U128::from_integer(token_in_contract + // In order to compute the amount to swap we need the live token balances. Note that at this state the token in + // transfer has already been completed as that function call was enqueued before this one. We therefore need to + // subtract the amount in to get the pre-swap balances. + let balance_in_plus_amount_in_max = U128::from_integer(Token::at(token_in) .balance_of_public(context.this_address()) .view(&mut context)); let balance_in = balance_in_plus_amount_in_max - amount_in_max; - let balance_out = U128::from_integer(token_out_contract + + let balance_out = U128::from_integer(Token::at(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. + // We can now compute the number of tokens we need to receive and complete the partial note with the change. let amount_in = get_amount_in(amount_out, balance_in, balance_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 > U128::zero()) { - token_in_contract - .finalize_transfer_to_private( - refund_amount.to_integer(), - refund_token_in_hiding_point_slot, - ) - .call(&mut context); + assert(amount_in <= amount_in_max, "INSUFFICIENT_OUTPUT_AMOUNT"); + + let change = amount_in_max - amount_in; + if (change > U128::zero()) { + Token::at(token_in) + .finalize_transfer_to_private(change.to_integer(), change_token_in_hiding_point_slot + ) + .call(&mut context); } - // Transfer the output token to the user. - token_out_contract + // Note again that we already knew the amount out, but for consistency we want to only commit this note once + // all other steps have been performed. + Token::at(token_out) .finalize_transfer_to_private(amount_out.to_integer(), token_out_hiding_point_slot) .call(&mut context); } + + unconstrained fn get_amount_out_for_exact_in( + balance_in: Field, + balance_out: Field, + amount_in: Field, + ) -> Field { + // Ideally we'd call the token contract in order to read the current balance, but we can't due to #7524. + get_amount_out(U128::from_integer(amount_in), U128::from_integer(balance_in), U128::from_integer(balance_out)).to_integer() + } + + unconstrained fn get_amount_in_for_exact_out( + balance_in: Field, + balance_out: Field, + amount_out: Field, + ) -> Field { + // Ideally we'd call the token contract in order to read the current balance, but we can't due to #7524. + get_amount_in(U128::from_integer(amount_out), U128::from_integer(balance_in), U128::from_integer(balance_out)).to_integer() + } } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 53eff85b18e9..794e71301f55 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -446,8 +446,7 @@ contract Token { /// some of the finalization functions (`finalize_transfer_to_private`, `finalize_mint_to_private`). /// Returns a hiding point slot. #[private] - fn prepare_private_balance_increase(to: AztecAddress) -> Field { - let from = context.msg_sender(); + fn prepare_private_balance_increase(to: AztecAddress, from: AztecAddress) -> Field { _prepare_private_balance_increase(from, to, &mut context, storage) } // docs:end:prepare_private_balance_increase diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_private.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_private.nr index f48bfb6127e5..6c2ce2239166 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_private.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_private.nr @@ -23,7 +23,7 @@ unconstrained fn transfer_to_private_internal_orchestration() { #[test] unconstrained fn transfer_to_private_external_orchestration() { // Setup without account contracts. We are not using authwits here, so dummy accounts are enough - let (env, token_contract_address, _, recipient, amount) = + let (env, token_contract_address, owner, recipient, amount) = utils::setup_and_mint_to_public(/* with_account_contracts */ false); let note_randomness = random(); @@ -33,7 +33,7 @@ unconstrained fn transfer_to_private_external_orchestration() { // We prepare the transfer let hiding_point_slot: Field = Token::at(token_contract_address) - .prepare_private_balance_increase(recipient) + .prepare_private_balance_increase(recipient, owner) .call(&mut env.private()); // Finalize the transfer of the tokens (message sender owns the tokens in public) @@ -72,14 +72,14 @@ unconstrained fn transfer_to_private_transfer_not_prepared() { #[test(should_fail_with = "Assertion failed: attempt to subtract with underflow 'hi == high'")] unconstrained fn transfer_to_private_failure_not_an_owner() { // Setup without account contracts. We are not using authwits here, so dummy accounts are enough - let (env, token_contract_address, _, not_owner, amount) = + let (env, token_contract_address, owner, not_owner, amount) = utils::setup_and_mint_to_public(/* with_account_contracts */ false); // (For this specific test we could set a random value for the commitment and not do the call to `prepare...` // as the token balance check is before we use the value but that would made the test less robust against changes // in the contract.) let hiding_point_slot: Field = Token::at(token_contract_address) - .prepare_private_balance_increase(not_owner) + .prepare_private_balance_increase(not_owner, owner) .call(&mut env.private()); // Try transferring someone else's token balance diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index fcf4127112fd..3e26f3a90d0e 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -28,7 +28,7 @@ pub global ARGS_LENGTH: u32 = 16; // "PER CALL" CONSTANTS pub global MAX_NOTE_HASHES_PER_CALL: u32 = 16; pub global MAX_NULLIFIERS_PER_CALL: u32 = 16; -pub global MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL: u32 = 4; +pub global MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL: u32 = 5; pub global MAX_ENQUEUED_CALLS_PER_CALL: u32 = 16; pub global MAX_L2_TO_L1_MSGS_PER_CALL: u32 = 2; pub global MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_CALL: u32 = 64; diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index ff6911cf4f34..7ad6de18c10d 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -4,7 +4,7 @@ export const MAX_FIELD_VALUE = 2188824287183927522224640574525727508854836440041 export const ARGS_LENGTH = 16; export const MAX_NOTE_HASHES_PER_CALL = 16; export const MAX_NULLIFIERS_PER_CALL = 16; -export const MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL = 4; +export const MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL = 5; export const MAX_ENQUEUED_CALLS_PER_CALL = 16; export const MAX_L2_TO_L1_MSGS_PER_CALL = 2; export const MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_CALL = 64; @@ -180,7 +180,7 @@ export const TX_CONTEXT_LENGTH = 8; export const TX_REQUEST_LENGTH = 12; export const TOTAL_FEES_LENGTH = 1; export const HEADER_LENGTH = 24; -export const PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 490; +export const PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 498; export const PUBLIC_CIRCUIT_PUBLIC_INPUTS_LENGTH = 866; export const PRIVATE_CONTEXT_INPUTS_LENGTH = 37; export const FEE_RECIPIENT_LENGTH = 2; 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 00d5a65d0d1b..d4c0786d2926 100644 --- a/yarn-project/end-to-end/src/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -1,11 +1,9 @@ -import { type AccountWallet, type DebugLogger, Fr } from '@aztec/aztec.js'; +import { type AccountWallet, type DebugLogger, Fr, type Wallet } from '@aztec/aztec.js'; import { AMMContract, type TokenContract } from '@aztec/noir-contracts.js'; -import { jest } from '@jest/globals'; -import { describe } from 'node:test'; - import { deployToken, mintTokensToPrivate } from './fixtures/token_utils.js'; import { setup } from './fixtures/utils.js'; +import { jest } from '@jest/globals'; const TIMEOUT = 120_000; @@ -18,6 +16,7 @@ describe('AMM', () => { let adminWallet: AccountWallet; let liquidityProvider: AccountWallet; + let otherLiquidityProvider: AccountWallet; let swapper: AccountWallet; let token0: TokenContract; @@ -26,14 +25,13 @@ describe('AMM', () => { let amm: AMMContract; - const lpBalance0 = 20000n; - const lpBalance1 = 10000n; - const swapperBalance0 = 5000n; + const INITIAL_AMM_TOTAL_SUPPLY = 100000n; + + // We need a large token amount so that the swap fee (0.3%) is observable. + const INITIAL_TOKEN_BALANCE = 1_000_000_000n; beforeAll(async () => { - let wallets: AccountWallet[]; - ({ teardown, wallets, logger } = await setup(3)); - [adminWallet, liquidityProvider, swapper] = wallets; + ({ teardown, wallets: [adminWallet, liquidityProvider, otherLiquidityProvider, swapper], logger } = await setup(4)); token0 = await deployToken(adminWallet, 0n, logger); token1 = await deployToken(adminWallet, 0n, logger); @@ -43,28 +41,90 @@ describe('AMM', () => { .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); + // TODO(#9480): consider deploying the token by some factory when the AMM is deployed, and making the AMM be the + // minter there. + await liquidityToken.methods.set_minter(amm.address, true).send().wait(); + + // We mint the tokens to both liquidity providers and the swapper + await mintTokensToPrivate(token0, adminWallet, liquidityProvider.getAddress(), INITIAL_TOKEN_BALANCE); + await mintTokensToPrivate(token1, adminWallet, liquidityProvider.getAddress(), INITIAL_TOKEN_BALANCE); + + await mintTokensToPrivate(token0, adminWallet, otherLiquidityProvider.getAddress(), INITIAL_TOKEN_BALANCE); + await mintTokensToPrivate(token1, adminWallet, otherLiquidityProvider.getAddress(), INITIAL_TOKEN_BALANCE); + + // Note that the swapper only holds token0, not token1 + await mintTokensToPrivate(token0, adminWallet, swapper.getAddress(), INITIAL_TOKEN_BALANCE); }); - afterAll(async () => await teardown()); + afterAll(() => teardown()); describe('full flow', () => { // This is an integration test in which we perform an entire run of the happy path. Thorough unit testing is not // included. - it.only('add liquidity', async () => { - const amount0Max = lpBalance0 / 2n; - const amount1Max = lpBalance1 / 2n; - const amount0Min = lpBalance0 / 3n; - const amount1Min = lpBalance1 / 3n; + + type Balance = { + token0: bigint, + token1: bigint, + } + + type Delta = { + type: 'gt' | 'gte' | 'lt' | 'lte'; + amount: bigint; + } + + type BalanceDelta = { + token0: bigint | Delta, + token1: bigint | Delta, + } + + async function getAmmBalances(): Promise { + return { + token0: await token0.methods.balance_of_public(amm.address).simulate(), + token1: await token1.methods.balance_of_public(amm.address).simulate() + }; + } + + async function getWalletBalances(lp: Wallet): Promise { + return { + token0: await token0.withWallet(lp).methods.balance_of_private(lp.getAddress()).simulate(), + token1: await token1.withWallet(lp).methods.balance_of_private(lp.getAddress()).simulate() + }; + } + + function assertBalancesDelta(before: Balance, after: Balance, delta: BalanceDelta) { + assertTokenBalanceDelta(before.token0, after.token0, delta.token0); + assertTokenBalanceDelta(before.token1, after.token1, delta.token1); + } + + function assertTokenBalanceDelta(before: bigint, after: bigint, delta: bigint | Delta) { + if (typeof delta === 'bigint') { + expect(after - before).toEqual(delta); + } else if (delta.type == 'gt') { + expect(after - before).toBeGreaterThan(delta.amount); + } else if (delta.type == 'gte') { + expect(after - before).toBeGreaterThanOrEqual(delta.amount); + } else if (delta.type == 'lt') { + expect(after - before).toBeLessThan(delta.amount); + } else if (delta.type == 'lte') { + expect(after - before).toBeLessThanOrEqual(delta.amount); + } else { + throw new Error(`Unknwon delta type ${delta.type}`) + } + } + + it('add initial liquidity', async () => { + const ammBalancesBefore = await getAmmBalances(); + const lpBalancesBefore = await getWalletBalances(liquidityProvider); + + const amount0Max = lpBalancesBefore.token0; + const amount0Min = lpBalancesBefore.token0 / 2n; + const amount1Max = lpBalancesBefore.token1; + const amount1Min = lpBalancesBefore.token1 / 2n; // First we need to add authwits such that the AMM can transfer the tokens from the liquidity provider. These // authwits are for the full amount, since the AMM will first transfer that to itself, and later refund any excess - // during public execution. Note that we need to tell the AMM of the nonce we used in the authwit. + // during public execution. const nonceForAuthwits = Fr.random(); - await liquidityProvider.createAuthWit({ caller: amm.address, action: token0.methods.transfer_to_public( @@ -90,80 +150,190 @@ describe('AMM', () => { .send() .wait(); - // Since the LP was the first one to enter the pool, the maximum amounts of tokens should have been deposited. - expect(await token0.methods.balance_of_private(amm.address).simulate()).toEqual( - amount0Max, - ); - expect(await token0.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toEqual( - lpBalance0 - amount0Max, - ); - expect(await token1.methods.balance_of_private(amm.address).simulate()).toEqual( - amount1Max, - ); - expect(await token1.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toEqual( - lpBalance1 - amount1Max, - ); + const ammBalancesAfter = await getAmmBalances(); + const lpBalancesAfter = await getWalletBalances(liquidityProvider); + + // Since the LP was the first one to enter the pool, the maximum amounts of tokens should have been deposited as + // there is no prior token ratio to follow. + assertBalancesDelta(ammBalancesBefore, ammBalancesAfter, { token0: amount0Max, token1: amount1Max }); + assertBalancesDelta(lpBalancesBefore, lpBalancesAfter, { token0: -amount0Max, token1: -amount1Max }); // Liquidity tokens should also be minted for the liquidity provider, as well as locked at the zero address. + const expectedLiquidityTokens = (INITIAL_AMM_TOTAL_SUPPLY * 99n) / 100n; expect( await liquidityToken.methods.balance_of_private(liquidityProvider.getAddress()).simulate(), - ).toEqual(1000n); + ).toEqual(expectedLiquidityTokens); + expect( + await liquidityToken.methods.total_supply().simulate(), + ).toEqual(INITIAL_AMM_TOTAL_SUPPLY); + }); + + it('add liquidity from another lp', async () => { + // This is the same as when we add liquidity for the first time, but we'll be going through a different code path + // since total supply for the liquidity token is non-zero + + const ammBalancesBefore = await getAmmBalances(); + const lpBalancesBefore = await getWalletBalances(otherLiquidityProvider); + + const liquidityTokenSupplyBefore = await liquidityToken.methods.total_supply().simulate(); + + // The pool currently has the same number of tokens for token0 and token1, since that is the ratio the first + // liquidity provider used. Our maximum values have a diferent ratio (6:5 instead of 1:1), so we will end up + // adding the maximum amount that does result in the correct ratio (i.e. using amount1Max and a 1:1 ratio). + const amount0Max = (lpBalancesBefore.token0 * 6n) / 10n; + const amount0Min = (lpBalancesBefore.token0 * 4n) / 10n; + const amount1Max = (lpBalancesBefore.token1 * 5n) / 10n; + const amount1Min = (lpBalancesBefore.token1 * 4n) / 10n; + + const expectedAmount0 = amount1Max; + const expectedAmount1 = amount1Max; + + // We again add authwits such that the AMM can transfer the tokens from the liquidity provider. These authwits are + // for the full amount, since the AMM will first transfer that to itself, and later refund any excess during + // public execution. We expect for there to be excess since our maximum amounts do not have the same balance ratio + // as the pool currently holds. + const nonceForAuthwits = Fr.random(); + await otherLiquidityProvider.createAuthWit({ + caller: amm.address, + action: token0.methods.transfer_to_public( + otherLiquidityProvider.getAddress(), + amm.address, + amount0Max, + nonceForAuthwits, + ), + }); + await otherLiquidityProvider.createAuthWit({ + caller: amm.address, + action: token1.methods.transfer_to_public( + otherLiquidityProvider.getAddress(), + amm.address, + amount1Max, + nonceForAuthwits, + ), + }); + + await amm + .withWallet(otherLiquidityProvider) + .methods.add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, nonceForAuthwits) + .send() + .wait(); + + const ammBalancesAfter = await getAmmBalances(); + const lpBalancesAfter = await getWalletBalances(otherLiquidityProvider); + + assertBalancesDelta(ammBalancesBefore, ammBalancesAfter, { token0: expectedAmount0, token1: expectedAmount1 }); + assertBalancesDelta(lpBalancesBefore, lpBalancesAfter, { token0: -expectedAmount0, token1: -expectedAmount1 }); + + // The liquidity token supply should have grown with the same proportion as the pool balances + const expectedTotalSupply = (liquidityTokenSupplyBefore * (ammBalancesBefore.token0 + expectedAmount0)) / ammBalancesBefore.token0; + const expectedLiquidityTokens = expectedTotalSupply - INITIAL_AMM_TOTAL_SUPPLY; + expect( await liquidityToken.methods.total_supply().simulate(), - ).toEqual(100000n); + ).toEqual(expectedTotalSupply); + expect( + await liquidityToken.methods.balance_of_private(otherLiquidityProvider.getAddress()).simulate(), + ).toEqual(expectedLiquidityTokens); }); - it('the rest', async () => { - // SWAPPING EXACT TOKENS FOR TOKENS - // We try swapping half of swapper's token 0 balance for token 1 - const amountIn = swapperBalance0 / 2n; + it('swap exact tokens in', async () => { + const swapperBalancesBefore = await getWalletBalances(swapper); + const ammBalancesBefore = await getAmmBalances(); - // We need to add authwit such that the AMM can transfer the tokens from the swapper + // The token in will be token0 + const amountIn = swapperBalancesBefore.token0 / 10n; + + // Swaps also transfer tokens into the AMM, so we provide an authwit for the full amount in. + const nonceForAuthwits = Fr.random(); 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 + // We compute the expected amount out and set it as the minimum. In a real-life scenario we'd choose a slightly + // lower value to account for slippage, but since we're the only actor interacting with the AMM we can afford to + // just pass the exact value. Of course any lower value would also suffice. + const amountOutMin = await amm.methods.get_amount_out_for_exact_in(ammBalancesBefore.token0, ammBalancesBefore.token1, amountIn).simulate(); + await amm.withWallet(swapper).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); + // We know exactly how many tokens we're supposed to get because we know nobody else interacted with the AMM + // before we did. + const swapperBalancesAfter = await getWalletBalances(swapper); + assertBalancesDelta(swapperBalancesBefore, swapperBalancesAfter, { token0: -amountIn, token1: amountOutMin }); + }); - // 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); + it('swap exact tokens out', async () => { + const swapperBalancesBefore = await getWalletBalances(swapper); + const ammBalancesBefore = await getAmmBalances(); - // 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 want to undo the previous swap (except for the fees, which we can't recover), so we try to send the full + // token1 balance (since the swapper held no token1 tokens prior to the swap). However, we're using the method + // that receives an exact amount of tokens *out*, not in, so we can't quite specify this. What we do instead is + // query the contract for how much token0 we'd get if we sent our entire token1 balance, and then request exactly + // that amount. This would fail in a real-life scenario since we'd need to account for slippage, but we can do it + // in this test environment since there's nobody else interacting with the AMM. + const amountOut = await amm.methods.get_amount_out_for_exact_in(ammBalancesBefore.token1, ammBalancesBefore.token0, swapperBalancesBefore.token1).simulate(); + const amountInMax = swapperBalancesBefore.token1; - // We need to add authwit such that the AMM can transfer the tokens from the swapper + // Swaps also transfer tokens into the AMM, so we provide an authwit for the full amount in (any change will be + // later returned, though in this case there won't be any). + const nonceForAuthwits = Fr.random(); await swapper.createAuthWit({ caller: amm.address, - action: token1.methods.transfer_to_public(swapper.getAddress(), amm.address, amount1InMax, nonceForAuthwits), + action: token1.methods.transfer_to_public(swapper.getAddress(), amm.address, amountInMax, nonceForAuthwits), }); - await amm.methods - .swap_tokens_for_exact_tokens(token1.address, token0.address, amount0Out, amount1InMax, nonceForAuthwits) + await amm.withWallet(swapper).methods + .swap_tokens_for_exact_tokens(token1.address, token0.address, amountOut, amountInMax, 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, - ); + // Because nobody else interacted with the AMM, we know the amount in will be the maximum (i.e. the value the + // contract returned as what we'd need to send in order to get the amount out we requested). + const swapperBalancesAfter = await getWalletBalances(swapper); + assertBalancesDelta(swapperBalancesBefore, swapperBalancesAfter, { token0: amountOut, token1: -amountInMax }); - // 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, - ); + // We can also check that the swapper ends up with fewer tokens than they started with, since they had to pay + // swap fees during both swaps. + expect(swapperBalancesAfter.token0).toBeLessThan(INITIAL_TOKEN_BALANCE); }); + + it('remove liquidity', async () => { + // We now withdraw all of the tokens of one of the liquidity providers by burning their entire liquidity token + // balance. + const liquidityTokenBalance = await liquidityToken.withWallet(otherLiquidityProvider).methods.balance_of_private(otherLiquidityProvider.getAddress()).simulate(); + + // Because private burning requires first transfering the tokens into the AMM, we again need to provide an + // authwit. + const nonceForAuthwits = Fr.random(); + await otherLiquidityProvider.createAuthWit({ + caller: amm.address, + action: liquidityToken.methods.transfer_to_public(otherLiquidityProvider.getAddress(), amm.address, liquidityTokenBalance, nonceForAuthwits), + }); + + // We don't bother setting the minimum amounts, since we know nobody else is interacting with the AMM. In a + // real-life scenario we'd need to choose sensible amounts to avoid losing value due to slippage. + const amount0Min = 1n; + const amount1Min = 1n; + await amm.withWallet(otherLiquidityProvider).methods.remove_liquidity(liquidityTokenBalance, amount0Min, amount1Min, nonceForAuthwits).send().wait(); + + // The liquidity provider should have no remaining liquidity tokens, and should have recovered the value they + // originally deposited. + expect(await liquidityToken.withWallet(otherLiquidityProvider).methods.balance_of_private(otherLiquidityProvider.getAddress()).simulate()).toEqual(0n); + + // We now assert that the liquidity provider ended up with more tokens than they began with. These extra tokens + // come from the swap fees paid during each of the swaps. While swap fees are always collected on the token in, + // the net fees will all be accrued on token0 due to how the swaps were orchestrated. This can be intuited by the + // fact that the swapper held no token1 initially, so it'd be impossible for them to cause an increase in the + // AMM's token1 balance. + // We perform this test using the second liquidity provider, since the first one did lose some percentage of the + // value of their deposit during setup when liquidity was locked by minting tokens for the zero address. + const lpBalancesAfter = await getWalletBalances(otherLiquidityProvider); + expect(lpBalancesAfter.token0).toBeGreaterThan(INITIAL_TOKEN_BALANCE); + expect(lpBalancesAfter.token1).toEqual(INITIAL_TOKEN_BALANCE); + }) }); }); From 32f72e568ad540a2c72dc855dec20575a44574ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 28 Nov 2024 01:59:48 +0000 Subject: [PATCH 06/15] Simplify test fns --- yarn-project/end-to-end/src/e2e_amm.test.ts | 32 ++------------------- 1 file changed, 3 insertions(+), 29 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 d4c0786d2926..164492f287ef 100644 --- a/yarn-project/end-to-end/src/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -67,16 +67,6 @@ describe('AMM', () => { token1: bigint, } - type Delta = { - type: 'gt' | 'gte' | 'lt' | 'lte'; - amount: bigint; - } - - type BalanceDelta = { - token0: bigint | Delta, - token1: bigint | Delta, - } - async function getAmmBalances(): Promise { return { token0: await token0.methods.balance_of_public(amm.address).simulate(), @@ -91,25 +81,9 @@ describe('AMM', () => { }; } - function assertBalancesDelta(before: Balance, after: Balance, delta: BalanceDelta) { - assertTokenBalanceDelta(before.token0, after.token0, delta.token0); - assertTokenBalanceDelta(before.token1, after.token1, delta.token1); - } - - function assertTokenBalanceDelta(before: bigint, after: bigint, delta: bigint | Delta) { - if (typeof delta === 'bigint') { - expect(after - before).toEqual(delta); - } else if (delta.type == 'gt') { - expect(after - before).toBeGreaterThan(delta.amount); - } else if (delta.type == 'gte') { - expect(after - before).toBeGreaterThanOrEqual(delta.amount); - } else if (delta.type == 'lt') { - expect(after - before).toBeLessThan(delta.amount); - } else if (delta.type == 'lte') { - expect(after - before).toBeLessThanOrEqual(delta.amount); - } else { - throw new Error(`Unknwon delta type ${delta.type}`) - } + function assertBalancesDelta(before: Balance, after: Balance, delta: Balance) { + expect(after.token0 - before.token0).toEqual(delta.token0); + expect(after.token1 - before.token1).toEqual(delta.token1); } it('add initial liquidity', async () => { From 5567f2f7b032773542a4a8432140ee2a1fde18d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 28 Nov 2024 17:10:50 +0000 Subject: [PATCH 07/15] Fix deps --- .../noir-contracts/contracts/amm_contract/Nargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml index f2b6f48ba033..e5c4e342ed83 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/amm_contract/Nargo.toml @@ -6,6 +6,4 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } -token = { path = "../token_contract" } -uint_note = { path = "../../../aztec-nr/uint-note" } -authwit = { path = "../../../aztec-nr/authwit" } +token = { path = "../token_contract" } \ No newline at end of file From f5847951f99d98e985eddbec85212354d14e0de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 28 Nov 2024 17:11:45 +0000 Subject: [PATCH 08/15] Remove txe tests --- .../contracts/amm_contract/src/main.nr | 1 - .../contracts/amm_contract/src/test.nr | 2 - .../amm_contract/src/test/full_flow.nr | 115 ------------------ .../contracts/amm_contract/src/test/utils.nr | 87 ------------- 4 files changed, 205 deletions(-) delete mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/test.nr delete mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr delete mode 100644 noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.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 872cb5d06fd1..362c0673e6f4 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,5 @@ mod lib; mod config; -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 deleted file mode 100644 index 28ba21602e17..000000000000 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test.nr +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 7c391e0ed9b5..000000000000 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/full_flow.nr +++ /dev/null @@ -1,115 +0,0 @@ -use crate::{AMM, test::utils::setup}; -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, liquidity_provider, swapper, lp_balance_0, lp_balance_1, swapper_balance_0) = - setup(); - 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 - // 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( - liquidity_provider, - amm_address, - amount0_desired, - nonce_for_authwits, - ), - ); - 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, - ), - ); - - // 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( - amount0_desired, - amount1_desired, - amount0_min, - amount1_min, - 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); - - // 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, - AMM::INITIAL_LIQUIDITY.to_integer(), - note_randomness, - ); - check_private_balance( - liquidity_token_address, - liquidity_provider, - 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.to_integer(), - ); - - // 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 deleted file mode 100644 index 418883cdc918..000000000000 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/test/utils.nr +++ /dev/null @@ -1,87 +0,0 @@ -use crate::AMM; -use dep::token::{test::utils::{check_private_balance, mint_to_private}, 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}, -}; - -pub unconstrained fn setup() -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, Field, Field, Field) { - // Setup env, generate keys - let mut env = TestEnvironment::new(); - - // 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); - - // Deploy tokens to be swapped and a liquidity token - let token0_address = env - .deploy("./@token_contract", "Token") - .with_public_void_initializer(Token::interface().constructor( - token_admin, - "TestToken0000000000000000000000", - "TT00000000000000000000000000000", - 18, - )) - .to_address(); - - let token1_address = env - .deploy("./@token_contract", "Token") - .with_public_void_initializer(Token::interface().constructor( - token_admin, - "TestToken1000000000000000000000", - "TT10000000000000000000000000000", - 18, - )) - .to_address(); - - let liquidity_token_address = env - .deploy("./@token_contract", "Token") - .with_public_void_initializer(Token::interface().constructor( - token_admin, - "TestLiquidityToken0000000000000", - "TLT0000000000000000000000000000", - 18, - )) - .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(); - - // 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; - - mint_to_private(&mut env, token0_address, liquidity_provider, lp_balance_0); - check_private_balance(token0_address, liquidity_provider, lp_balance_0); - - mint_to_private(&mut env, token1_address, liquidity_provider, lp_balance_1); - check_private_balance(token1_address, liquidity_provider, lp_balance_1); - - 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); - ( - &mut env, amm_address, token0_address, token1_address, liquidity_token_address, - liquidity_provider, swapper, lp_balance_0, lp_balance_1, swapper_balance_0, - ) -} From 59e7b570f2c4945d8deddaf4d971974fb61586d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 28 Nov 2024 17:47:04 +0000 Subject: [PATCH 09/15] Fix formatting --- .../contracts/amm_contract/src/lib.nr | 1 - .../contracts/amm_contract/src/main.nr | 14 ++- yarn-project/end-to-end/src/e2e_amm.test.ts | 89 ++++++++++++------- 3 files changed, 69 insertions(+), 35 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 7d0537a07840..6d8e4d897908 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr @@ -13,7 +13,6 @@ pub fn get_amount_out(amount_in: U128, balance_in: U128, balance_out: U128) -> U let numerator = amount_in_with_fee * balance_out; let denominator = balance_in * U128::from_integer(1000) + amount_in_with_fee; numerator / denominator - } /// Given an output amount of an asset and pair balances, returns a required input amount of the other asset. 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 362c0673e6f4..d97302c3735a 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -503,7 +503,12 @@ contract AMM { amount_in: Field, ) -> Field { // Ideally we'd call the token contract in order to read the current balance, but we can't due to #7524. - get_amount_out(U128::from_integer(amount_in), U128::from_integer(balance_in), U128::from_integer(balance_out)).to_integer() + get_amount_out( + U128::from_integer(amount_in), + U128::from_integer(balance_in), + U128::from_integer(balance_out), + ) + .to_integer() } unconstrained fn get_amount_in_for_exact_out( @@ -512,6 +517,11 @@ contract AMM { amount_out: Field, ) -> Field { // Ideally we'd call the token contract in order to read the current balance, but we can't due to #7524. - get_amount_in(U128::from_integer(amount_out), U128::from_integer(balance_in), U128::from_integer(balance_out)).to_integer() + get_amount_in( + U128::from_integer(amount_out), + U128::from_integer(balance_in), + U128::from_integer(balance_out), + ) + .to_integer() } } 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 164492f287ef..6b1d741487f4 100644 --- a/yarn-project/end-to-end/src/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -1,9 +1,10 @@ import { type AccountWallet, type DebugLogger, Fr, type Wallet } 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'; -import { jest } from '@jest/globals'; const TIMEOUT = 120_000; @@ -31,7 +32,11 @@ describe('AMM', () => { const INITIAL_TOKEN_BALANCE = 1_000_000_000n; beforeAll(async () => { - ({ teardown, wallets: [adminWallet, liquidityProvider, otherLiquidityProvider, swapper], logger } = await setup(4)); + ({ + teardown, + wallets: [adminWallet, liquidityProvider, otherLiquidityProvider, swapper], + logger, + } = await setup(4)); token0 = await deployToken(adminWallet, 0n, logger); token1 = await deployToken(adminWallet, 0n, logger); @@ -63,21 +68,21 @@ describe('AMM', () => { // included. type Balance = { - token0: bigint, - token1: bigint, - } + token0: bigint; + token1: bigint; + }; async function getAmmBalances(): Promise { return { token0: await token0.methods.balance_of_public(amm.address).simulate(), - token1: await token1.methods.balance_of_public(amm.address).simulate() + token1: await token1.methods.balance_of_public(amm.address).simulate(), }; } - async function getWalletBalances(lp: Wallet): Promise { + async function getWalletBalances(lp: Wallet): Promise { return { token0: await token0.withWallet(lp).methods.balance_of_private(lp.getAddress()).simulate(), - token1: await token1.withWallet(lp).methods.balance_of_private(lp.getAddress()).simulate() + token1: await token1.withWallet(lp).methods.balance_of_private(lp.getAddress()).simulate(), }; } @@ -134,12 +139,10 @@ describe('AMM', () => { // Liquidity tokens should also be minted for the liquidity provider, as well as locked at the zero address. const expectedLiquidityTokens = (INITIAL_AMM_TOTAL_SUPPLY * 99n) / 100n; - expect( - await liquidityToken.methods.balance_of_private(liquidityProvider.getAddress()).simulate(), - ).toEqual(expectedLiquidityTokens); - expect( - await liquidityToken.methods.total_supply().simulate(), - ).toEqual(INITIAL_AMM_TOTAL_SUPPLY); + expect(await liquidityToken.methods.balance_of_private(liquidityProvider.getAddress()).simulate()).toEqual( + expectedLiquidityTokens, + ); + expect(await liquidityToken.methods.total_supply().simulate()).toEqual(INITIAL_AMM_TOTAL_SUPPLY); }); it('add liquidity from another lp', async () => { @@ -199,15 +202,14 @@ describe('AMM', () => { assertBalancesDelta(lpBalancesBefore, lpBalancesAfter, { token0: -expectedAmount0, token1: -expectedAmount1 }); // The liquidity token supply should have grown with the same proportion as the pool balances - const expectedTotalSupply = (liquidityTokenSupplyBefore * (ammBalancesBefore.token0 + expectedAmount0)) / ammBalancesBefore.token0; + const expectedTotalSupply = + (liquidityTokenSupplyBefore * (ammBalancesBefore.token0 + expectedAmount0)) / ammBalancesBefore.token0; const expectedLiquidityTokens = expectedTotalSupply - INITIAL_AMM_TOTAL_SUPPLY; - expect( - await liquidityToken.methods.total_supply().simulate(), - ).toEqual(expectedTotalSupply); - expect( - await liquidityToken.methods.balance_of_private(otherLiquidityProvider.getAddress()).simulate(), - ).toEqual(expectedLiquidityTokens); + expect(await liquidityToken.methods.total_supply().simulate()).toEqual(expectedTotalSupply); + expect(await liquidityToken.methods.balance_of_private(otherLiquidityProvider.getAddress()).simulate()).toEqual( + expectedLiquidityTokens, + ); }); it('swap exact tokens in', async () => { @@ -227,9 +229,12 @@ describe('AMM', () => { // We compute the expected amount out and set it as the minimum. In a real-life scenario we'd choose a slightly // lower value to account for slippage, but since we're the only actor interacting with the AMM we can afford to // just pass the exact value. Of course any lower value would also suffice. - const amountOutMin = await amm.methods.get_amount_out_for_exact_in(ammBalancesBefore.token0, ammBalancesBefore.token1, amountIn).simulate(); - await amm.withWallet(swapper).methods - .swap_exact_tokens_for_tokens(token0.address, token1.address, amountIn, amountOutMin, nonceForAuthwits) + const amountOutMin = await amm.methods + .get_amount_out_for_exact_in(ammBalancesBefore.token0, ammBalancesBefore.token1, amountIn) + .simulate(); + await amm + .withWallet(swapper) + .methods.swap_exact_tokens_for_tokens(token0.address, token1.address, amountIn, amountOutMin, nonceForAuthwits) .send() .wait(); @@ -249,7 +254,9 @@ describe('AMM', () => { // query the contract for how much token0 we'd get if we sent our entire token1 balance, and then request exactly // that amount. This would fail in a real-life scenario since we'd need to account for slippage, but we can do it // in this test environment since there's nobody else interacting with the AMM. - const amountOut = await amm.methods.get_amount_out_for_exact_in(ammBalancesBefore.token1, ammBalancesBefore.token0, swapperBalancesBefore.token1).simulate(); + const amountOut = await amm.methods + .get_amount_out_for_exact_in(ammBalancesBefore.token1, ammBalancesBefore.token0, swapperBalancesBefore.token1) + .simulate(); const amountInMax = swapperBalancesBefore.token1; // Swaps also transfer tokens into the AMM, so we provide an authwit for the full amount in (any change will be @@ -260,8 +267,9 @@ describe('AMM', () => { action: token1.methods.transfer_to_public(swapper.getAddress(), amm.address, amountInMax, nonceForAuthwits), }); - await amm.withWallet(swapper).methods - .swap_tokens_for_exact_tokens(token1.address, token0.address, amountOut, amountInMax, nonceForAuthwits) + await amm + .withWallet(swapper) + .methods.swap_tokens_for_exact_tokens(token1.address, token0.address, amountOut, amountInMax, nonceForAuthwits) .send() .wait(); @@ -278,25 +286,42 @@ describe('AMM', () => { it('remove liquidity', async () => { // We now withdraw all of the tokens of one of the liquidity providers by burning their entire liquidity token // balance. - const liquidityTokenBalance = await liquidityToken.withWallet(otherLiquidityProvider).methods.balance_of_private(otherLiquidityProvider.getAddress()).simulate(); + const liquidityTokenBalance = await liquidityToken + .withWallet(otherLiquidityProvider) + .methods.balance_of_private(otherLiquidityProvider.getAddress()) + .simulate(); // Because private burning requires first transfering the tokens into the AMM, we again need to provide an // authwit. const nonceForAuthwits = Fr.random(); await otherLiquidityProvider.createAuthWit({ caller: amm.address, - action: liquidityToken.methods.transfer_to_public(otherLiquidityProvider.getAddress(), amm.address, liquidityTokenBalance, nonceForAuthwits), + action: liquidityToken.methods.transfer_to_public( + otherLiquidityProvider.getAddress(), + amm.address, + liquidityTokenBalance, + nonceForAuthwits, + ), }); // We don't bother setting the minimum amounts, since we know nobody else is interacting with the AMM. In a // real-life scenario we'd need to choose sensible amounts to avoid losing value due to slippage. const amount0Min = 1n; const amount1Min = 1n; - await amm.withWallet(otherLiquidityProvider).methods.remove_liquidity(liquidityTokenBalance, amount0Min, amount1Min, nonceForAuthwits).send().wait(); + await amm + .withWallet(otherLiquidityProvider) + .methods.remove_liquidity(liquidityTokenBalance, amount0Min, amount1Min, nonceForAuthwits) + .send() + .wait(); // The liquidity provider should have no remaining liquidity tokens, and should have recovered the value they // originally deposited. - expect(await liquidityToken.withWallet(otherLiquidityProvider).methods.balance_of_private(otherLiquidityProvider.getAddress()).simulate()).toEqual(0n); + expect( + await liquidityToken + .withWallet(otherLiquidityProvider) + .methods.balance_of_private(otherLiquidityProvider.getAddress()) + .simulate(), + ).toEqual(0n); // We now assert that the liquidity provider ended up with more tokens than they began with. These extra tokens // come from the swap fees paid during each of the swaps. While swap fees are always collected on the token in, @@ -308,6 +333,6 @@ describe('AMM', () => { const lpBalancesAfter = await getWalletBalances(otherLiquidityProvider); expect(lpBalancesAfter.token0).toBeGreaterThan(INITIAL_TOKEN_BALANCE); expect(lpBalancesAfter.token1).toEqual(INITIAL_TOKEN_BALANCE); - }) + }); }); }); From d31edae6b5c0db71de09482db369d492fe6a19e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 28 Nov 2024 17:58:28 +0000 Subject: [PATCH 10/15] Type globals --- .../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 d97302c3735a..c43febda753e 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -51,9 +51,9 @@ contract AMM { /// Amount of liquidity which gets locked when liquidity is provided for the first time. Its purpose is to prevent /// the pool from ever emptying which could lead to undefined behavior. - global MINIMUM_LIQUIDITY = U128::from_integer(1000); + global MINIMUM_LIQUIDITY: U128 = U128::from_integer(1000); /// We set it to 99 times the minimum liquidity. That way the first LP gets 99% of the value of their deposit. - global INITIAL_LIQUIDITY = U128::from_integer(99000); + global INITIAL_LIQUIDITY: U128 = U128::from_integer(99000); // TODO(#9480): Either deploy the liquidity contract in the constructor or verify it that it corresponds to what // this contract expects (i.e. that the AMM has permission to mint and burn). From d174448f05200bd6baa791913a4b1bcdb55adef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 28 Nov 2024 18:27:14 +0000 Subject: [PATCH 11/15] Link issues --- .../noir-contracts/contracts/amm_contract/src/main.nr | 4 ++++ .../noir-contracts/contracts/token_contract/src/main.nr | 2 ++ yarn-project/accounts/package.json | 2 +- yarn-project/circuits.js/package.json | 2 +- 4 files changed, 8 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 c43febda753e..fe405512cf4d 100644 --- a/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/amm_contract/src/main.nr @@ -99,6 +99,7 @@ contract AMM { // We don't yet know how many tokens the sender will actually supply - that can only be computed during public // execution since the amounts supplied must have the same ratio as the live balances. We therefore transfer the // maximum amounts here, and prepare partial notes that return the change to the sender (if any). + // TODO(#10286): consider merging these two calls token0.transfer_to_public(sender, context.this_address(), amount0_max, nonce).call( &mut context, ); @@ -254,6 +255,7 @@ contract AMM { // Liquidity tokens are burned when liquidity is removed in order to reduce the total supply. However, we lack // a function to privately burn, so we instead transfer the tokens into the AMM's public balance, and them have // the AMM publicly burn its own tokens. + // TODO(#10287): consider adding a private burn liquidity_token.transfer_to_public(sender, context.this_address(), liquidity, nonce).call( &mut context, ); @@ -351,6 +353,7 @@ contract AMM { // We transfer the full amount in, since it is an exact amount, and prepare a partial note for the amount out, // which will only be known during public execution as it depends on the live balances. + // TODO(#10286): consider merging these two calls Token::at(token_in) .transfer_to_public(sender, context.this_address(), amount_in, nonce) .call(&mut context); @@ -430,6 +433,7 @@ contract AMM { // Technically the token out note does not need to be partial, since we do know the amount out, but we do want // to wait until the swap has been completed before commiting the note to the tree to avoid it being spent too // early. + // TODO(#10286): consider merging these two calls Token::at(token_in) .transfer_to_public(sender, context.this_address(), amount_in_max, nonce) .call(&mut context); diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 794e71301f55..130f8c8614f3 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -447,6 +447,8 @@ contract Token { /// Returns a hiding point slot. #[private] fn prepare_private_balance_increase(to: AztecAddress, from: AztecAddress) -> Field { + // TODO(#9887): ideally we'd not have `from` here, but we do need a `from` address to produce a tagging secret + // with `to`. _prepare_private_balance_increase(from, to, &mut context, storage) } // docs:end:prepare_private_balance_increase diff --git a/yarn-project/accounts/package.json b/yarn-project/accounts/package.json index d73f4c9ebf35..6af84aee1c9c 100644 --- a/yarn-project/accounts/package.json +++ b/yarn-project/accounts/package.json @@ -102,4 +102,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/circuits.js/package.json b/yarn-project/circuits.js/package.json index 34f19cb3e6d1..f2d6a74bfccd 100644 --- a/yarn-project/circuits.js/package.json +++ b/yarn-project/circuits.js/package.json @@ -101,4 +101,4 @@ ] ] } -} \ No newline at end of file +} From 9af1fdcef426049df10af2b94410ee11224b8cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 28 Nov 2024 18:29:21 +0000 Subject: [PATCH 12/15] Add CI job --- scripts/ci/get_e2e_jobs.sh | 1 + yarn-project/end-to-end/scripts/e2e_test_config.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/ci/get_e2e_jobs.sh b/scripts/ci/get_e2e_jobs.sh index 2dbdb42ac40d..ed6379e6a46b 100755 --- a/scripts/ci/get_e2e_jobs.sh +++ b/scripts/ci/get_e2e_jobs.sh @@ -20,6 +20,7 @@ full_list=$(get_test_names) allow_list=( "e2e_2_pxes" "e2e_authwit" + "e2e_amm" "e2e_avm_simulator" "e2e_block_building" "e2e_cross_chain_messaging" diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml index a09f5f5a568f..8a65a011708d 100644 --- a/yarn-project/end-to-end/scripts/e2e_test_config.yml +++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml @@ -20,6 +20,7 @@ tests: command: './scripts/e2e_compose_test.sh bench_tx_size' e2e_2_pxes: {} e2e_account_contracts: {} + e2e_amm: {} e2e_authwit: {} e2e_avm_simulator: {} e2e_blacklist_token_contract: {} From e38196247f45f73f88b3f864d742e0f498f05513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 29 Nov 2024 15:05:53 +0000 Subject: [PATCH 13/15] Fix flamegraph to account for the AMM name --- noir-projects/noir-contracts/scripts/flamegraph.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/scripts/flamegraph.sh b/noir-projects/noir-contracts/scripts/flamegraph.sh index 8db540142e61..7db97d0e384f 100755 --- a/noir-projects/noir-contracts/scripts/flamegraph.sh +++ b/noir-projects/noir-contracts/scripts/flamegraph.sh @@ -50,7 +50,7 @@ function sed_wrapper() { } # convert contract name to following format: token_bridge_contract-TokenBridge.json -ARTIFACT=$(echo "$CONTRACT" | sed_wrapper -r 's/^([A-Z])/\L\1/; s/([a-z0-9])([A-Z])/\1_\L\2/g') +ARTIFACT=$(echo "$CONTRACT" | sed_wrapper -r 's/^([A-Z]+)/\L\1/; s/([a-z0-9])([A-Z])/\1_\L\2/g') ARTIFACT=$(echo "$ARTIFACT" | tr '[:upper:]' '[:lower:]') ARTIFACT_NAME="${ARTIFACT}_contract-${CONTRACT}" From 9d5f0dbe3a8ea8dc8e0070f0a2af9ca5e4af297c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 29 Nov 2024 15:09:15 +0000 Subject: [PATCH 14/15] Revert "Fix flamegraph to account for the AMM name" This reverts commit e38196247f45f73f88b3f864d742e0f498f05513. --- noir-projects/noir-contracts/scripts/flamegraph.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/scripts/flamegraph.sh b/noir-projects/noir-contracts/scripts/flamegraph.sh index 7db97d0e384f..8db540142e61 100755 --- a/noir-projects/noir-contracts/scripts/flamegraph.sh +++ b/noir-projects/noir-contracts/scripts/flamegraph.sh @@ -50,7 +50,7 @@ function sed_wrapper() { } # convert contract name to following format: token_bridge_contract-TokenBridge.json -ARTIFACT=$(echo "$CONTRACT" | sed_wrapper -r 's/^([A-Z]+)/\L\1/; s/([a-z0-9])([A-Z])/\1_\L\2/g') +ARTIFACT=$(echo "$CONTRACT" | sed_wrapper -r 's/^([A-Z])/\L\1/; s/([a-z0-9])([A-Z])/\1_\L\2/g') ARTIFACT=$(echo "$ARTIFACT" | tr '[:upper:]' '[:lower:]') ARTIFACT_NAME="${ARTIFACT}_contract-${CONTRACT}" From c962c7ccd20c2c1d28342c8d141cb0f16960758c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 3 Dec 2024 13:09:03 +0000 Subject: [PATCH 15/15] Undo 'addr' struct renaming, keep param --- .../aztec-nr/aztec/src/macros/functions/interfaces.nr | 2 +- noir-projects/aztec-nr/aztec/src/macros/mod.nr | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr index 257e6cbd95d3..5fffb892d8ea 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr @@ -131,7 +131,7 @@ pub comptime fn stub_fn(f: FunctionDefinition) -> Quoted { $args let selector = dep::aztec::protocol_types::abis::function_selector::FunctionSelector::from_field($fn_selector); $call_interface_name { - target_contract: self.addr, + target_contract: self.target_contract, selector, name: $fn_name_str, $args_hash diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index feb51002963f..883a20283269 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -71,7 +71,7 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { quote { pub struct $module_name { - addr: dep::aztec::protocol_types::address::AztecAddress + target_contract: dep::aztec::protocol_types::address::AztecAddress } impl $module_name { @@ -80,11 +80,11 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { pub fn at( addr: aztec::protocol_types::address::AztecAddress ) -> Self { - Self { addr } + Self { target_contract: addr } } pub fn interface() -> Self { - Self { addr: aztec::protocol_types::address::AztecAddress::zero() } + Self { target_contract: aztec::protocol_types::address::AztecAddress::zero() } } $storage_layout_getter @@ -94,12 +94,12 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { pub fn at( addr: aztec::protocol_types::address::AztecAddress ) -> $module_name { - $module_name { addr } + $module_name { target_contract: addr } } #[contract_library_method] pub fn interface() -> $module_name { - $module_name { addr: aztec::protocol_types::address::AztecAddress::zero() } + $module_name { target_contract: aztec::protocol_types::address::AztecAddress::zero() } } $library_storage_layout_getter