Skip to content

Commit

Permalink
feat: AMM (#10153)
Browse files Browse the repository at this point in the history
Opening in favor of
#8644 due to
@benesjan being away on vacation, and that PR using Graphite. Work here
starts from commit a404b58, which has
been squashed into 81d7607.

This is the first implementation of Uniswap v2 style AMM that provides
identity privacy. The contract is a single pool for two tokens with a
fixed 0.3% swap fee. Adding and removing liquidity is done
proportionally to the current ratio, resulting in no price impact and
therefore no fees. Swaps can be performed by specifying either the
amount in or the amount out. All three operations are completed in a
single transaction each by leveraging partial notes.

I created #10225
to track pending work. Some of the tasks in that epic are only work that
arises from the AMM, but are not technically required to have a fully
functioning system: this PR already achieves that.

Only a happy-path end to end test is included here, since TXE currently
lacks some of the features required in order to properly deal with
partial notes. We should later write those as this will be a good test
of TXE's capabilities and user experience, given the complexity of the
setup.

---

I added the e2e to be run on CI on all branches since it combines
multiple complex features, and likely contains our largest transactions
yet.
  • Loading branch information
nventuro authored Dec 3, 2024
1 parent 7e19b39 commit 90668c3
Show file tree
Hide file tree
Showing 15 changed files with 1,023 additions and 16 deletions.
4 changes: 2 additions & 2 deletions l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -200,7 +200,7 @@ library Constants {
uint256 internal constant TOTAL_FEES_LENGTH = 1;
uint256 internal constant TOTAL_MANA_USED_LENGTH = 1;
uint256 internal constant HEADER_LENGTH = 25;
uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 731;
uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 739;
uint256 internal constant PUBLIC_CIRCUIT_PUBLIC_INPUTS_LENGTH = 867;
uint256 internal constant PRIVATE_CONTEXT_INPUTS_LENGTH = 38;
uint256 internal constant FEE_RECIPIENT_LENGTH = 2;
Expand Down
8 changes: 4 additions & 4 deletions noir-projects/aztec-nr/aztec/src/macros/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ comptime fn generate_contract_interface(m: Module) -> Quoted {
$fn_stubs_quote

pub fn at(
target_contract: aztec::protocol_types::address::AztecAddress
addr: aztec::protocol_types::address::AztecAddress
) -> Self {
Self { target_contract }
Self { target_contract: addr }
}

pub fn interface() -> Self {
Expand All @@ -92,9 +92,9 @@ 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 { target_contract: addr }
}

#[contract_library_method]
Expand Down
1 change: 1 addition & 0 deletions noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"contracts/amm_contract",
"contracts/app_subscription_contract",
"contracts/auth_contract",
"contracts/auth_registry_contract",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "amm_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
token = { path = "../token_contract" }
29 changes: 29 additions & 0 deletions noir-projects/noir-contracts/contracts/amm_contract/src/config.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use dep::aztec::protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}};

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 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<CONFIG_LENGTH> for Config {
fn serialize(self: Self) -> [Field; CONFIG_LENGTH] {
[self.token0.to_field(), self.token1.to_field(), self.liquidity_token.to_field()]
}
}

impl Deserialize<CONFIG_LENGTH> 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]),
}
}
}
96 changes: 96 additions & 0 deletions noir-projects/noir-contracts/contracts/amm_contract/src/lib.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/// 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 {
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 {
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)
}

/// 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_max: U128,
amount1_max: U128,
amount0_min: U128,
amount1_min: U128,
balance0: U128,
balance1: U128,
) -> (U128, U128) {
// 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 {
// 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)
}
}
}

/// 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 {
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
// avoid introducing too much error due to truncation.
(amount0 * balance1) / balance0
}
Loading

0 comments on commit 90668c3

Please sign in to comment.