From 8d0cd77130985b8bfe6bc597d07cb3bfb83b3a80 Mon Sep 17 00:00:00 2001 From: julio4 Date: Sun, 1 Oct 2023 15:18:58 +0900 Subject: [PATCH 1/8] feat: constant product amm contract --- .../ch02-applications/constant_amm/.gitignore | 1 + .../ch02-applications/constant_amm/Scarb.toml | 8 + .../constant_amm/src/constant_amm.cairo | 269 ++++++++++++++++++ .../constant_amm/src/lib.cairo | 1 + 4 files changed, 279 insertions(+) create mode 100644 listings/ch02-applications/constant_amm/.gitignore create mode 100644 listings/ch02-applications/constant_amm/Scarb.toml create mode 100644 listings/ch02-applications/constant_amm/src/constant_amm.cairo create mode 100644 listings/ch02-applications/constant_amm/src/lib.cairo diff --git a/listings/ch02-applications/constant_amm/.gitignore b/listings/ch02-applications/constant_amm/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/listings/ch02-applications/constant_amm/.gitignore @@ -0,0 +1 @@ +target diff --git a/listings/ch02-applications/constant_amm/Scarb.toml b/listings/ch02-applications/constant_amm/Scarb.toml new file mode 100644 index 00000000..c6ed5cbc --- /dev/null +++ b/listings/ch02-applications/constant_amm/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "constant_amm" +version = "0.1.0" + +[dependencies] +starknet = ">=2.2.0" + +[[target.starknet-contract]] diff --git a/listings/ch02-applications/constant_amm/src/constant_amm.cairo b/listings/ch02-applications/constant_amm/src/constant_amm.cairo new file mode 100644 index 00000000..3a06b421 --- /dev/null +++ b/listings/ch02-applications/constant_amm/src/constant_amm.cairo @@ -0,0 +1,269 @@ +use starknet::ContractAddress; + +// In order to make contract calls within our Vault, +// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher. +#[starknet::interface] +trait IERC20 { + fn name(self: @TContractState) -> felt252; + fn symbol(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::contract] +mod ConstantAmm { + use super::{IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use integer::u256_sqrt; + + + #[storage] + struct Storage { + token0: IERC20Dispatcher, + token1: IERC20Dispatcher, + reserve0: u256, + reserve1: u256, + total_supply: u256, + balance_of: LegacyMap:: + } + + #[constructor] + fn constructor(ref self: ContractState, token0: ContractAddress, token1: ContractAddress) { + self.token0.write(IERC20Dispatcher { contract_address: token0 }); + self.token1.write(IERC20Dispatcher { contract_address: token1 }); + } + + #[generate_trait] + impl PrivateFunctions of PrivateFunctionsTrait { + fn _mint(ref self: ContractState, to: ContractAddress, amount: u256) { + self.balance_of.write(to, self.balance_of.read(to) + amount); + self.total_supply.write(self.total_supply.read() + amount); + } + + fn _burn(ref self: ContractState, from: ContractAddress, amount: u256) { + self.balance_of.write(from, self.balance_of.read(from) - amount); + self.total_supply.write(self.total_supply.read() - amount); + } + + fn _update(ref self: ContractState, reserve0: u256, reserve1: u256) { + self.reserve0.write(reserve0); + self.reserve1.write(reserve1); + } + + #[inline(always)] + fn select_token(self: @ContractState, token: ContractAddress) -> bool { + assert( + token == self.token0.read().contract_address + || token == self.token1.read().contract_address, + 'invalid token' + ); + token == self.token0.read().contract_address + } + + #[inline(always)] + fn min(x: u256, y: u256) -> u256 { + if (x <= y) { + x + } else { + y + } + } + } + + #[external(v0)] + #[generate_trait] + impl ConstantAmm of IConstantAmm { + fn swap(ref self: ContractState, token_in: ContractAddress, amount_in: u256) -> u256 { + assert(amount_in > 0, 'amount in = 0'); + let is_token0: bool = self.select_token(token_in); + + let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = ( + self.token0.read(), self.token1.read() + ); + let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read()); + let ( + token_in, token_out, reserve_in, reserve_out + ): (IERC20Dispatcher, IERC20Dispatcher, u256, u256) = + if (is_token0) { + (token0, token1, reserve0, reserve1) + } else { + (token1, token0, reserve1, reserve0) + }; + + let caller = get_caller_address(); + let this = get_contract_address(); + token_in.transfer_from(caller, this, amount_in); + + // How much dy for dx? + // xy = k + // (x + dx)(y - dy) = k + // y - dy = k / (x + dx) + // y - k / (x + dx) = dy + // y - xy / (x + dx) = dy + // (yx + ydx - xy) / (x + dx) = dy + // ydx / (x + dx) = dy + // 0.3% fee + + let amount_in_with_fee = (amount_in * 997) / 1000; + let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee); + + token_out.transfer(caller, amount_out); + + self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this)); + amount_out + } + + fn add_liquidity(ref self: ContractState, amount0: u256, amount1: u256) -> u256 { + let caller = get_caller_address(); + let this = get_contract_address(); + let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = ( + self.token0.read(), self.token1.read() + ); + + token0.transfer_from(caller, this, amount0); + token1.transfer_from(caller, this, amount1); + + // How much dx, dy to add? + // + // xy = k + // (x + dx)(y + dy) = k' + // + // No price change, before and after adding liquidity + // x / y = (x + dx) / (y + dy) + // + // x(y + dy) = y(x + dx) + // x * dy = y * dx + // + // x / y = dx / dy + // dy = y / x * dx + let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read()); + if (reserve0 > 0 || reserve1 > 0) { + assert(reserve0 * amount1 == reserve1 * amount0, 'x / y != dx / dy'); + } + + // How much shares to mint? + // + // f(x, y) = value of liquidity + // We will define f(x, y) = sqrt(xy) + // + // L0 = f(x, y) + // L1 = f(x + dx, y + dy) + // T = total shares + // s = shares to mint + // + // Total shares should increase proportional to increase in liquidity + // L1 / L0 = (T + s) / T + // + // L1 * T = L0 * (T + s) + // + // (L1 - L0) * T / L0 = s + + // Claim + // (L1 - L0) / L0 = dx / x = dy / y + // + // Proof + // --- Equation 1 --- + // (L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy) + // + // dx / dy = x / y so replace dy = dx * y / x + // + // --- Equation 2 --- + // Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy) + // + // Multiply by sqrt(x) / sqrt(x) + // Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y) + // = (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(y)sqrt(x^2)) + // sqrt(y) on top and bottom cancels out + // + // --- Equation 3 --- + // Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(x^2) + // = (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2) + // = ((x + dx) - x) / x + // = dx / x + // Since dx / dy = x / y, + // dx / x = dy / y + // + // Finally + // (L1 - L0) / L0 = dx / x = dy / y + let total_supply = self.total_supply.read(); + let shares = if (total_supply == 0) { + u256_sqrt(amount0 * amount1).into() + } else { + PrivateFunctions::min( + amount0 * total_supply / reserve0, amount1 * total_supply / reserve1 + ) + }; + assert(shares > 0, 'shares = 0'); + self._mint(caller, shares); + + self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this)); + shares + } + + fn remove_liquidity(ref self: ContractState, shares: u256) -> (u256, u256) { + let caller = get_caller_address(); + let this = get_contract_address(); + let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = ( + self.token0.read(), self.token1.read() + ); + + // Claim + // dx, dy = amount of liquidity to remove + // dx = s / T * x + // dy = s / T * y + // + // Proof + // Let's find dx, dy such that + // v / L = s / T + // + // where + // v = f(dx, dy) = sqrt(dxdy) + // L = total liquidity = sqrt(xy) + // s = shares + // T = total supply + // + // --- Equation 1 --- + // v = s / T * L + // sqrt(dxdy) = s / T * sqrt(xy) + // + // Amount of liquidity to remove must not change price so + // dx / dy = x / y + // + // replace dy = dx * y / x + // sqrt(dxdy) = sqrt(dx * dx * y / x) = dx * sqrt(y / x) + // + // Divide both sides of Equation 1 with sqrt(y / x) + // dx = s / T * sqrt(xy) / sqrt(y / x) + // = s / T * sqrt(x^2) = s / T * x + // + // Likewise + // dy = s / T * y + + // bal0 >= reserve0 + // bal1 >= reserve1 + let (bal0, bal1): (u256, u256) = (token0.balance_of(this), token1.balance_of(this)); + + let total_supply = self.total_supply.read(); + let (amount0, amount1): (u256, u256) = ( + (shares * bal0) / total_supply, (shares * bal1) / total_supply + ); + assert(amount0 > 0 && amount1 > 0, 'amount0 or amount1 = 0'); + + self._burn(caller, shares); + self._update(bal0 - amount0, bal1 - amount1); + + token0.transfer(caller, amount0); + token1.transfer(caller, amount1); + (amount0, amount1) + } + } +} +// TODO: Add tests + diff --git a/listings/ch02-applications/constant_amm/src/lib.cairo b/listings/ch02-applications/constant_amm/src/lib.cairo new file mode 100644 index 00000000..0394f6be --- /dev/null +++ b/listings/ch02-applications/constant_amm/src/lib.cairo @@ -0,0 +1 @@ +mod constant_amm; From 45e9f2d3fb80faac007793504672576740c42800 Mon Sep 17 00:00:00 2001 From: julio4 Date: Wed, 4 Oct 2023 15:54:16 +0900 Subject: [PATCH 2/8] feat: const product amm tests (wip) --- .../ch02-applications/constant_amm/Scarb.toml | 8 - .../constant_amm/src/lib.cairo | 1 - .../.gitignore | 0 .../constant_product_amm/Scarb.toml | 9 ++ .../src/constant_product_amm.cairo} | 141 +++++++++++++++--- .../constant_product_amm/src/lib.cairo | 1 + 6 files changed, 131 insertions(+), 29 deletions(-) delete mode 100644 listings/ch02-applications/constant_amm/Scarb.toml delete mode 100644 listings/ch02-applications/constant_amm/src/lib.cairo rename listings/ch02-applications/{constant_amm => constant_product_amm}/.gitignore (100%) create mode 100644 listings/ch02-applications/constant_product_amm/Scarb.toml rename listings/ch02-applications/{constant_amm/src/constant_amm.cairo => constant_product_amm/src/constant_product_amm.cairo} (66%) create mode 100644 listings/ch02-applications/constant_product_amm/src/lib.cairo diff --git a/listings/ch02-applications/constant_amm/Scarb.toml b/listings/ch02-applications/constant_amm/Scarb.toml deleted file mode 100644 index c6ed5cbc..00000000 --- a/listings/ch02-applications/constant_amm/Scarb.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "constant_amm" -version = "0.1.0" - -[dependencies] -starknet = ">=2.2.0" - -[[target.starknet-contract]] diff --git a/listings/ch02-applications/constant_amm/src/lib.cairo b/listings/ch02-applications/constant_amm/src/lib.cairo deleted file mode 100644 index 0394f6be..00000000 --- a/listings/ch02-applications/constant_amm/src/lib.cairo +++ /dev/null @@ -1 +0,0 @@ -mod constant_amm; diff --git a/listings/ch02-applications/constant_amm/.gitignore b/listings/ch02-applications/constant_product_amm/.gitignore similarity index 100% rename from listings/ch02-applications/constant_amm/.gitignore rename to listings/ch02-applications/constant_product_amm/.gitignore diff --git a/listings/ch02-applications/constant_product_amm/Scarb.toml b/listings/ch02-applications/constant_product_amm/Scarb.toml new file mode 100644 index 00000000..840d36ec --- /dev/null +++ b/listings/ch02-applications/constant_product_amm/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "constant_product_amm" +version = "0.1.0" + +[dependencies] +starknet = ">=2.2.0" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } + +[[target.starknet-contract]] diff --git a/listings/ch02-applications/constant_amm/src/constant_amm.cairo b/listings/ch02-applications/constant_product_amm/src/constant_product_amm.cairo similarity index 66% rename from listings/ch02-applications/constant_amm/src/constant_amm.cairo rename to listings/ch02-applications/constant_product_amm/src/constant_product_amm.cairo index 3a06b421..28efa56c 100644 --- a/listings/ch02-applications/constant_amm/src/constant_amm.cairo +++ b/listings/ch02-applications/constant_product_amm/src/constant_product_amm.cairo @@ -1,29 +1,19 @@ +// ANCHOR: ConstantProductAmmContract use starknet::ContractAddress; -// In order to make contract calls within our Vault, -// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher. #[starknet::interface] -trait IERC20 { - fn name(self: @TContractState) -> felt252; - fn symbol(self: @TContractState) -> felt252; - fn decimals(self: @TContractState) -> u8; - fn total_supply(self: @TContractState) -> u256; - fn balance_of(self: @TContractState, account: ContractAddress) -> u256; - fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; +trait IConstantProductAmm { + fn swap(ref self: TContractState, token_in: ContractAddress, amount_in: u256) -> u256; + fn add_liquidity(ref self: TContractState, amount0: u256, amount1: u256) -> u256; + fn remove_liquidity(ref self: TContractState, shares: u256) -> (u256, u256); } #[starknet::contract] -mod ConstantAmm { - use super::{IERC20Dispatcher, IERC20DispatcherTrait}; +mod ConstantProductAmm { + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::{ContractAddress, get_caller_address, get_contract_address}; use integer::u256_sqrt; - #[storage] struct Storage { token0: IERC20Dispatcher, @@ -78,8 +68,7 @@ mod ConstantAmm { } #[external(v0)] - #[generate_trait] - impl ConstantAmm of IConstantAmm { + impl ConstantProductAmm of super::IConstantProductAmm { fn swap(ref self: ContractState, token_in: ContractAddress, amount_in: u256) -> u256 { assert(amount_in > 0, 'amount in = 0'); let is_token0: bool = self.select_token(token_in); @@ -143,6 +132,7 @@ mod ConstantAmm { // // x / y = dx / dy // dy = y / x * dx + let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read()); if (reserve0 > 0 || reserve1 > 0) { assert(reserve0 * amount1 == reserve1 * amount0, 'x / y != dx / dy'); @@ -192,6 +182,7 @@ mod ConstantAmm { // // Finally // (L1 - L0) / L0 = dx / x = dy / y + let total_supply = self.total_supply.read(); let shares = if (total_supply == 0) { u256_sqrt(amount0 * amount1).into() @@ -265,5 +256,115 @@ mod ConstantAmm { } } } -// TODO: Add tests +// ANCHOR_END: StoreArrayContract + +#[cfg(test)] +mod tests { + use core::traits::TryInto; + use openzeppelin::token::erc20::{ + ERC20, interface::IERC20Dispatcher, interface::IERC20DispatcherTrait + }; + use openzeppelin::utils::serde::SerializedAppend; + use openzeppelin::tests::utils; + + use super::{ + ConstantProductAmm, IConstantProductAmmDispatcher, IConstantProductAmmDispatcherTrait + }; + use starknet::{ + deploy_syscall, ContractAddress, get_caller_address, get_contract_address, + contract_address_const + }; + use starknet::testing::set_contract_address; + use starknet::class_hash::Felt252TryIntoClassHash; + + use debug::PrintTrait; + + const BANK: felt252 = 0x123; + const INITIAL_SUPPLY: u256 = 10_000; + + #[derive(Drop, Copy)] + struct Deployment { + contract: IConstantProductAmmDispatcher, + contract_address: ContractAddress, + token0: IERC20Dispatcher, + token1: IERC20Dispatcher + } + + fn deploy_erc20( + name: felt252, symbol: felt252, recipient: ContractAddress, initial_supply: u256 + ) -> (ContractAddress, IERC20Dispatcher) { + let mut calldata = array![]; + calldata.append_serde(name); + calldata.append_serde(symbol); + calldata.append_serde(initial_supply); + calldata.append_serde(recipient); + + let address = utils::deploy(ERC20::TEST_CLASS_HASH, calldata); + (address, IERC20Dispatcher { contract_address: address }) + } + + fn setup() -> Deployment { + let recipient: ContractAddress = BANK.try_into().unwrap(); + let (token0_address, token0) = deploy_erc20('Token0', 'T0', recipient, INITIAL_SUPPLY); + let (token1_address, token1) = deploy_erc20('Token1', 'T1', recipient, INITIAL_SUPPLY); + + let mut calldata = array![]; + calldata.append_serde(token0_address); + calldata.append_serde(token1_address); + + let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); + Deployment { + contract: IConstantProductAmmDispatcher { contract_address }, + contract_address, + token0, + token1 + } + } + + fn add_liquidity(deploy: Deployment, amount: u256) -> u256 { + assert(amount <= INITIAL_SUPPLY, 'amount > INITIAL_SUPPLY'); + + let provider: ContractAddress = BANK.try_into().unwrap(); + set_contract_address(provider); + + deploy.token0.approve(deploy.contract_address, amount); + deploy.token1.approve(deploy.contract_address, amount); + deploy.contract.add_liquidity(amount, amount) + } + + #[test] + #[available_gas(20000000)] + fn should_deploy() { + let deploy = setup(); + let bank: ContractAddress = BANK.try_into().unwrap(); + + assert(deploy.token0.balance_of(bank) == INITIAL_SUPPLY, 'Wrong balance token0'); + assert(deploy.token1.balance_of(bank) == INITIAL_SUPPLY, 'Wrong balance token1'); + } + + #[test] + #[available_gas(20000000)] + fn should_add_liquidity() { + let deploy = setup(); + let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); + + let provider: ContractAddress = BANK.try_into().unwrap(); + assert(deploy.token0.balance_of(provider) == INITIAL_SUPPLY / 2, 'Wrong balance token0'); + assert(deploy.token1.balance_of(provider) == INITIAL_SUPPLY / 2, 'Wrong balance token1'); + assert(shares > 0, 'Wrong shares'); + } + + #[test] + #[available_gas(20000000)] + fn should_remove_liquidity() { + let deploy = setup(); + let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); + let provider: ContractAddress = BANK.try_into().unwrap(); + + deploy.contract.remove_liquidity(shares); + + assert(deploy.token0.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token0'); + assert(deploy.token1.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token1'); + } +} diff --git a/listings/ch02-applications/constant_product_amm/src/lib.cairo b/listings/ch02-applications/constant_product_amm/src/lib.cairo new file mode 100644 index 00000000..f0d02bf5 --- /dev/null +++ b/listings/ch02-applications/constant_product_amm/src/lib.cairo @@ -0,0 +1 @@ +mod constant_product_amm; From abe9662d12d55fce6ce5cab255dbeab7808f76ae Mon Sep 17 00:00:00 2001 From: julio4 Date: Thu, 5 Oct 2023 21:43:49 +0900 Subject: [PATCH 3/8] feat: Constant Product Amm --- .../constant_product_amm/.gitignore | 0 .../constant_product_amm/Scarb.toml | 0 .../src/constant_product_amm.cairo | 37 ++++++++++++++++--- .../constant_product_amm/src/lib.cairo | 0 src/SUMMARY.md | 1 + src/ch01/constant-product-amm.md | 9 +++++ 6 files changed, 42 insertions(+), 5 deletions(-) rename listings/{ch02-applications => ch01-applications}/constant_product_amm/.gitignore (100%) rename listings/{ch02-applications => ch01-applications}/constant_product_amm/Scarb.toml (100%) rename listings/{ch02-applications => ch01-applications}/constant_product_amm/src/constant_product_amm.cairo (89%) rename listings/{ch02-applications => ch01-applications}/constant_product_amm/src/lib.cairo (100%) create mode 100644 src/ch01/constant-product-amm.md diff --git a/listings/ch02-applications/constant_product_amm/.gitignore b/listings/ch01-applications/constant_product_amm/.gitignore similarity index 100% rename from listings/ch02-applications/constant_product_amm/.gitignore rename to listings/ch01-applications/constant_product_amm/.gitignore diff --git a/listings/ch02-applications/constant_product_amm/Scarb.toml b/listings/ch01-applications/constant_product_amm/Scarb.toml similarity index 100% rename from listings/ch02-applications/constant_product_amm/Scarb.toml rename to listings/ch01-applications/constant_product_amm/Scarb.toml diff --git a/listings/ch02-applications/constant_product_amm/src/constant_product_amm.cairo b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo similarity index 89% rename from listings/ch02-applications/constant_product_amm/src/constant_product_amm.cairo rename to listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo index 28efa56c..c9e904cd 100644 --- a/listings/ch02-applications/constant_product_amm/src/constant_product_amm.cairo +++ b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo @@ -256,7 +256,7 @@ mod ConstantProductAmm { } } } -// ANCHOR_END: StoreArrayContract +// ANCHOR_END: ConstantProductAmmContract #[cfg(test)] mod tests { @@ -285,7 +285,6 @@ mod tests { #[derive(Drop, Copy)] struct Deployment { contract: IConstantProductAmmDispatcher, - contract_address: ContractAddress, token0: IERC20Dispatcher, token1: IERC20Dispatcher } @@ -315,7 +314,6 @@ mod tests { let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); Deployment { contract: IConstantProductAmmDispatcher { contract_address }, - contract_address, token0, token1 } @@ -327,8 +325,8 @@ mod tests { let provider: ContractAddress = BANK.try_into().unwrap(); set_contract_address(provider); - deploy.token0.approve(deploy.contract_address, amount); - deploy.token1.approve(deploy.contract_address, amount); + deploy.token0.approve(deploy.contract.contract_address, amount); + deploy.token1.approve(deploy.contract.contract_address, amount); deploy.contract.add_liquidity(amount, amount) } @@ -367,4 +365,33 @@ mod tests { assert(deploy.token0.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token0'); assert(deploy.token1.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token1'); } + + #[test] + #[available_gas(20000000)] + fn should_swap() { + let deploy = setup(); + let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); + + let provider: ContractAddress = BANK.try_into().unwrap(); + let user = contract_address_const::<0x1>(); + + // Provider send some token0 to user + set_contract_address(provider); + let amount = deploy.token0.balance_of(provider) / 2; + deploy.token0.transfer(user, amount); + + // user swap for token1 using AMM liquidity + set_contract_address(user); + deploy.token0.approve(deploy.contract.contract_address, amount); + deploy.contract.swap(deploy.token0.contract_address, amount); + let amount_token1_received = deploy.token1.balance_of(user); + assert(amount_token1_received > 0, 'Swap: wrong balance token1'); + + // User can swap back token1 to token0 + // As each swap has a 0.3% fee, user will receive less token0 + deploy.token1.approve(deploy.contract.contract_address, amount_token1_received); + deploy.contract.swap(deploy.token1.contract_address, amount_token1_received); + let amount_token0_received = deploy.token0.balance_of(user); + assert(amount_token0_received < amount, 'Swap: wrong balance token0'); + } } diff --git a/listings/ch02-applications/constant_product_amm/src/lib.cairo b/listings/ch01-applications/constant_product_amm/src/lib.cairo similarity index 100% rename from listings/ch02-applications/constant_product_amm/src/lib.cairo rename to listings/ch01-applications/constant_product_amm/src/lib.cairo diff --git a/src/SUMMARY.md b/src/SUMMARY.md index e9075d93..c32252a2 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -34,6 +34,7 @@ Summary - [Upgradeable Contract](./ch01/upgradeable_contract.md) - [Defi Vault](./ch01/simple_vault.md) - [ERC20 Token](./ch01/erc20.md) + - [Constant Product AMM](./ch01/constant-product-amm.md) # Advanced concepts diff --git a/src/ch01/constant-product-amm.md b/src/ch01/constant-product-amm.md new file mode 100644 index 00000000..fffa3c9d --- /dev/null +++ b/src/ch01/constant-product-amm.md @@ -0,0 +1,9 @@ +# Constant Product AMM + +This is the Cairo adaptation of the [Solidity by example Constant Product AMM](https://solidity-by-example.org/defi/constant-product-amm/). + +```rust +{{#include ../listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo:ConstantProductAmmContract}} +``` + +Play with this contract in [Remix](https://remix.ethereum.org/?#activate=Starknet&url=https://github.com/NethermindEth/StarknetByExample/blob/main/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo). \ No newline at end of file From 07454d25a02e00f938a489c40c6e21c446759e23 Mon Sep 17 00:00:00 2001 From: julio4 Date: Thu, 5 Oct 2023 21:47:42 +0900 Subject: [PATCH 4/8] format: apply cairo-fmt --- .../constant_product_amm/src/constant_product_amm.cairo | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo index c9e904cd..4552316d 100644 --- a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo +++ b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo @@ -312,11 +312,7 @@ mod tests { calldata.append_serde(token1_address); let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); - Deployment { - contract: IConstantProductAmmDispatcher { contract_address }, - token0, - token1 - } + Deployment { contract: IConstantProductAmmDispatcher { contract_address }, token0, token1 } } fn add_liquidity(deploy: Deployment, amount: u256) -> u256 { From 4cdaa975b1735d7117d45a79fc1f12c2a621e756 Mon Sep 17 00:00:00 2001 From: julio4 Date: Fri, 6 Oct 2023 16:49:18 +0900 Subject: [PATCH 5/8] chore: move integration tests to separate file --- .../src/constant_product_amm.cairo | 134 ------------------ .../constant_product_amm/src/lib.cairo | 1 + .../constant_product_amm/src/tests.cairo | 133 +++++++++++++++++ 3 files changed, 134 insertions(+), 134 deletions(-) create mode 100644 listings/ch01-applications/constant_product_amm/src/tests.cairo diff --git a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo index 4552316d..0916b864 100644 --- a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo +++ b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo @@ -257,137 +257,3 @@ mod ConstantProductAmm { } } // ANCHOR_END: ConstantProductAmmContract - -#[cfg(test)] -mod tests { - use core::traits::TryInto; - use openzeppelin::token::erc20::{ - ERC20, interface::IERC20Dispatcher, interface::IERC20DispatcherTrait - }; - use openzeppelin::utils::serde::SerializedAppend; - use openzeppelin::tests::utils; - - use super::{ - ConstantProductAmm, IConstantProductAmmDispatcher, IConstantProductAmmDispatcherTrait - }; - use starknet::{ - deploy_syscall, ContractAddress, get_caller_address, get_contract_address, - contract_address_const - }; - use starknet::testing::set_contract_address; - use starknet::class_hash::Felt252TryIntoClassHash; - - use debug::PrintTrait; - - const BANK: felt252 = 0x123; - const INITIAL_SUPPLY: u256 = 10_000; - - #[derive(Drop, Copy)] - struct Deployment { - contract: IConstantProductAmmDispatcher, - token0: IERC20Dispatcher, - token1: IERC20Dispatcher - } - - fn deploy_erc20( - name: felt252, symbol: felt252, recipient: ContractAddress, initial_supply: u256 - ) -> (ContractAddress, IERC20Dispatcher) { - let mut calldata = array![]; - calldata.append_serde(name); - calldata.append_serde(symbol); - calldata.append_serde(initial_supply); - calldata.append_serde(recipient); - - let address = utils::deploy(ERC20::TEST_CLASS_HASH, calldata); - (address, IERC20Dispatcher { contract_address: address }) - } - - fn setup() -> Deployment { - let recipient: ContractAddress = BANK.try_into().unwrap(); - let (token0_address, token0) = deploy_erc20('Token0', 'T0', recipient, INITIAL_SUPPLY); - let (token1_address, token1) = deploy_erc20('Token1', 'T1', recipient, INITIAL_SUPPLY); - - let mut calldata = array![]; - calldata.append_serde(token0_address); - calldata.append_serde(token1_address); - - let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); - Deployment { contract: IConstantProductAmmDispatcher { contract_address }, token0, token1 } - } - - fn add_liquidity(deploy: Deployment, amount: u256) -> u256 { - assert(amount <= INITIAL_SUPPLY, 'amount > INITIAL_SUPPLY'); - - let provider: ContractAddress = BANK.try_into().unwrap(); - set_contract_address(provider); - - deploy.token0.approve(deploy.contract.contract_address, amount); - deploy.token1.approve(deploy.contract.contract_address, amount); - - deploy.contract.add_liquidity(amount, amount) - } - - #[test] - #[available_gas(20000000)] - fn should_deploy() { - let deploy = setup(); - let bank: ContractAddress = BANK.try_into().unwrap(); - - assert(deploy.token0.balance_of(bank) == INITIAL_SUPPLY, 'Wrong balance token0'); - assert(deploy.token1.balance_of(bank) == INITIAL_SUPPLY, 'Wrong balance token1'); - } - - #[test] - #[available_gas(20000000)] - fn should_add_liquidity() { - let deploy = setup(); - let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); - - let provider: ContractAddress = BANK.try_into().unwrap(); - assert(deploy.token0.balance_of(provider) == INITIAL_SUPPLY / 2, 'Wrong balance token0'); - assert(deploy.token1.balance_of(provider) == INITIAL_SUPPLY / 2, 'Wrong balance token1'); - assert(shares > 0, 'Wrong shares'); - } - - #[test] - #[available_gas(20000000)] - fn should_remove_liquidity() { - let deploy = setup(); - let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); - let provider: ContractAddress = BANK.try_into().unwrap(); - - deploy.contract.remove_liquidity(shares); - - assert(deploy.token0.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token0'); - assert(deploy.token1.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token1'); - } - - #[test] - #[available_gas(20000000)] - fn should_swap() { - let deploy = setup(); - let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); - - let provider: ContractAddress = BANK.try_into().unwrap(); - let user = contract_address_const::<0x1>(); - - // Provider send some token0 to user - set_contract_address(provider); - let amount = deploy.token0.balance_of(provider) / 2; - deploy.token0.transfer(user, amount); - - // user swap for token1 using AMM liquidity - set_contract_address(user); - deploy.token0.approve(deploy.contract.contract_address, amount); - deploy.contract.swap(deploy.token0.contract_address, amount); - let amount_token1_received = deploy.token1.balance_of(user); - assert(amount_token1_received > 0, 'Swap: wrong balance token1'); - - // User can swap back token1 to token0 - // As each swap has a 0.3% fee, user will receive less token0 - deploy.token1.approve(deploy.contract.contract_address, amount_token1_received); - deploy.contract.swap(deploy.token1.contract_address, amount_token1_received); - let amount_token0_received = deploy.token0.balance_of(user); - assert(amount_token0_received < amount, 'Swap: wrong balance token0'); - } -} diff --git a/listings/ch01-applications/constant_product_amm/src/lib.cairo b/listings/ch01-applications/constant_product_amm/src/lib.cairo index f0d02bf5..78295f2f 100644 --- a/listings/ch01-applications/constant_product_amm/src/lib.cairo +++ b/listings/ch01-applications/constant_product_amm/src/lib.cairo @@ -1 +1,2 @@ mod constant_product_amm; +mod tests; diff --git a/listings/ch01-applications/constant_product_amm/src/tests.cairo b/listings/ch01-applications/constant_product_amm/src/tests.cairo new file mode 100644 index 00000000..7286e7f4 --- /dev/null +++ b/listings/ch01-applications/constant_product_amm/src/tests.cairo @@ -0,0 +1,133 @@ +#[cfg(test)] +mod tests { + use core::traits::TryInto; + use openzeppelin::token::erc20::{ + ERC20, interface::IERC20Dispatcher, interface::IERC20DispatcherTrait + }; + use openzeppelin::utils::serde::SerializedAppend; + use openzeppelin::tests::utils; + + use super::{ + ConstantProductAmm, IConstantProductAmmDispatcher, IConstantProductAmmDispatcherTrait + }; + use starknet::{ + deploy_syscall, ContractAddress, get_caller_address, get_contract_address, + contract_address_const + }; + use starknet::testing::set_contract_address; + use starknet::class_hash::Felt252TryIntoClassHash; + + use debug::PrintTrait; + + const BANK: felt252 = 0x123; + const INITIAL_SUPPLY: u256 = 10_000; + + #[derive(Drop, Copy)] + struct Deployment { + contract: IConstantProductAmmDispatcher, + token0: IERC20Dispatcher, + token1: IERC20Dispatcher + } + + fn deploy_erc20( + name: felt252, symbol: felt252, recipient: ContractAddress, initial_supply: u256 + ) -> (ContractAddress, IERC20Dispatcher) { + let mut calldata = array![]; + calldata.append_serde(name); + calldata.append_serde(symbol); + calldata.append_serde(initial_supply); + calldata.append_serde(recipient); + + let address = utils::deploy(ERC20::TEST_CLASS_HASH, calldata); + (address, IERC20Dispatcher { contract_address: address }) + } + + fn setup() -> Deployment { + let recipient: ContractAddress = BANK.try_into().unwrap(); + let (token0_address, token0) = deploy_erc20('Token0', 'T0', recipient, INITIAL_SUPPLY); + let (token1_address, token1) = deploy_erc20('Token1', 'T1', recipient, INITIAL_SUPPLY); + + let mut calldata = array![]; + calldata.append_serde(token0_address); + calldata.append_serde(token1_address); + + let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); + Deployment { contract: IConstantProductAmmDispatcher { contract_address }, token0, token1 } + } + + fn add_liquidity(deploy: Deployment, amount: u256) -> u256 { + assert(amount <= INITIAL_SUPPLY, 'amount > INITIAL_SUPPLY'); + + let provider: ContractAddress = BANK.try_into().unwrap(); + set_contract_address(provider); + + deploy.token0.approve(deploy.contract.contract_address, amount); + deploy.token1.approve(deploy.contract.contract_address, amount); + + deploy.contract.add_liquidity(amount, amount) + } + + #[test] + #[available_gas(20000000)] + fn should_deploy() { + let deploy = setup(); + let bank: ContractAddress = BANK.try_into().unwrap(); + + assert(deploy.token0.balance_of(bank) == INITIAL_SUPPLY, 'Wrong balance token0'); + assert(deploy.token1.balance_of(bank) == INITIAL_SUPPLY, 'Wrong balance token1'); + } + + #[test] + #[available_gas(20000000)] + fn should_add_liquidity() { + let deploy = setup(); + let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); + + let provider: ContractAddress = BANK.try_into().unwrap(); + assert(deploy.token0.balance_of(provider) == INITIAL_SUPPLY / 2, 'Wrong balance token0'); + assert(deploy.token1.balance_of(provider) == INITIAL_SUPPLY / 2, 'Wrong balance token1'); + assert(shares > 0, 'Wrong shares'); + } + + #[test] + #[available_gas(20000000)] + fn should_remove_liquidity() { + let deploy = setup(); + let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); + let provider: ContractAddress = BANK.try_into().unwrap(); + + deploy.contract.remove_liquidity(shares); + + assert(deploy.token0.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token0'); + assert(deploy.token1.balance_of(provider) == INITIAL_SUPPLY, 'Wrong balance token1'); + } + + #[test] + #[available_gas(20000000)] + fn should_swap() { + let deploy = setup(); + let shares = add_liquidity(deploy, INITIAL_SUPPLY / 2); + + let provider: ContractAddress = BANK.try_into().unwrap(); + let user = contract_address_const::<0x1>(); + + // Provider send some token0 to user + set_contract_address(provider); + let amount = deploy.token0.balance_of(provider) / 2; + deploy.token0.transfer(user, amount); + + // user swap for token1 using AMM liquidity + set_contract_address(user); + deploy.token0.approve(deploy.contract.contract_address, amount); + deploy.contract.swap(deploy.token0.contract_address, amount); + let amount_token1_received = deploy.token1.balance_of(user); + assert(amount_token1_received > 0, 'Swap: wrong balance token1'); + + // User can swap back token1 to token0 + // As each swap has a 0.3% fee, user will receive less token0 + deploy.token1.approve(deploy.contract.contract_address, amount_token1_received); + deploy.contract.swap(deploy.token1.contract_address, amount_token1_received); + let amount_token0_received = deploy.token0.balance_of(user); + assert(amount_token0_received < amount, 'Swap: wrong balance token0'); + } +} From 4bf9dd5542b6bcda542b6086a6e1238625fc9a6f Mon Sep 17 00:00:00 2001 From: julio4 Date: Tue, 10 Oct 2023 18:02:13 +0900 Subject: [PATCH 6/8] feat: update to 2.3.0 + support custom fee --- .../ch01-applications/constant_product_amm/Scarb.toml | 2 +- .../src/constant_product_amm.cairo | 10 ++++++---- .../constant_product_amm/src/lib.cairo | 2 ++ .../constant_product_amm/src/tests.cairo | 6 +++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/listings/ch01-applications/constant_product_amm/Scarb.toml b/listings/ch01-applications/constant_product_amm/Scarb.toml index 840d36ec..94c4c902 100644 --- a/listings/ch01-applications/constant_product_amm/Scarb.toml +++ b/listings/ch01-applications/constant_product_amm/Scarb.toml @@ -3,7 +3,7 @@ name = "constant_product_amm" version = "0.1.0" [dependencies] -starknet = ">=2.2.0" +starknet = ">=2.3.0-rc0" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } [[target.starknet-contract]] diff --git a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo index 0916b864..a90efbc8 100644 --- a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo +++ b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo @@ -21,11 +21,14 @@ mod ConstantProductAmm { reserve0: u256, reserve1: u256, total_supply: u256, - balance_of: LegacyMap:: + balance_of: LegacyMap::, + fee: u256, } #[constructor] - fn constructor(ref self: ContractState, token0: ContractAddress, token1: ContractAddress) { + fn constructor(ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u256) { + assert(fee <= 1000, 'fee > 1000') + self.fee.write(fee); self.token0.write(IERC20Dispatcher { contract_address: token0 }); self.token1.write(IERC20Dispatcher { contract_address: token1 }); } @@ -98,9 +101,8 @@ mod ConstantProductAmm { // y - xy / (x + dx) = dy // (yx + ydx - xy) / (x + dx) = dy // ydx / (x + dx) = dy - // 0.3% fee - let amount_in_with_fee = (amount_in * 997) / 1000; + let amount_in_with_fee = (amount_in * (1000 - fee)) / 1000; let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee); token_out.transfer(caller, amount_out); diff --git a/listings/ch01-applications/constant_product_amm/src/lib.cairo b/listings/ch01-applications/constant_product_amm/src/lib.cairo index 78295f2f..d538d2c7 100644 --- a/listings/ch01-applications/constant_product_amm/src/lib.cairo +++ b/listings/ch01-applications/constant_product_amm/src/lib.cairo @@ -1,2 +1,4 @@ mod constant_product_amm; + +#[cfg(test)] mod tests; diff --git a/listings/ch01-applications/constant_product_amm/src/tests.cairo b/listings/ch01-applications/constant_product_amm/src/tests.cairo index 7286e7f4..e0ae300e 100644 --- a/listings/ch01-applications/constant_product_amm/src/tests.cairo +++ b/listings/ch01-applications/constant_product_amm/src/tests.cairo @@ -1,4 +1,3 @@ -#[cfg(test)] mod tests { use core::traits::TryInto; use openzeppelin::token::erc20::{ @@ -17,8 +16,6 @@ mod tests { use starknet::testing::set_contract_address; use starknet::class_hash::Felt252TryIntoClassHash; - use debug::PrintTrait; - const BANK: felt252 = 0x123; const INITIAL_SUPPLY: u256 = 10_000; @@ -51,6 +48,9 @@ mod tests { calldata.append_serde(token0_address); calldata.append_serde(token1_address); + // 0.3% fee + calldata.append_serde(3) + let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); Deployment { contract: IConstantProductAmmDispatcher { contract_address }, token0, token1 } } From e34e949c42e9ff2b580a7cd0dcdb2c214e593eb9 Mon Sep 17 00:00:00 2001 From: julio4 Date: Wed, 25 Oct 2023 09:49:29 +0900 Subject: [PATCH 7/8] fix: migrate to scarb lockfile --- .../constant_product_amm/Scarb.lock | 14 ++++++++++++++ .../constant_product_amm/Scarb.toml | 2 +- .../src/constant_product_amm.cairo | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 listings/ch01-applications/constant_product_amm/Scarb.lock diff --git a/listings/ch01-applications/constant_product_amm/Scarb.lock b/listings/ch01-applications/constant_product_amm/Scarb.lock new file mode 100644 index 00000000..28148050 --- /dev/null +++ b/listings/ch01-applications/constant_product_amm/Scarb.lock @@ -0,0 +1,14 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "constant_product_amm" +version = "0.1.0" +dependencies = [ + "openzeppelin", +] + +[[package]] +name = "openzeppelin" +version = "0.7.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.7.0#61a2505fe0c0f19b5de2b3f8dedf421ba2cff657" diff --git a/listings/ch01-applications/constant_product_amm/Scarb.toml b/listings/ch01-applications/constant_product_amm/Scarb.toml index 94c4c902..720aae7e 100644 --- a/listings/ch01-applications/constant_product_amm/Scarb.toml +++ b/listings/ch01-applications/constant_product_amm/Scarb.toml @@ -3,7 +3,7 @@ name = "constant_product_amm" version = "0.1.0" [dependencies] -starknet = ">=2.3.0-rc0" +starknet = "2.2.0" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } [[target.starknet-contract]] diff --git a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo index a90efbc8..6974ac66 100644 --- a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo +++ b/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo @@ -27,7 +27,7 @@ mod ConstantProductAmm { #[constructor] fn constructor(ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u256) { - assert(fee <= 1000, 'fee > 1000') + assert(fee <= 1000, 'fee > 1000'); self.fee.write(fee); self.token0.write(IERC20Dispatcher { contract_address: token0 }); self.token1.write(IERC20Dispatcher { contract_address: token1 }); @@ -102,7 +102,7 @@ mod ConstantProductAmm { // (yx + ydx - xy) / (x + dx) = dy // ydx / (x + dx) = dy - let amount_in_with_fee = (amount_in * (1000 - fee)) / 1000; + let amount_in_with_fee = (amount_in * (1000 - self.fee.read())) / 1000; let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee); token_out.transfer(caller, amount_out); From 80d41572928bdfa90f13cbfa33d0ed4ee576b913 Mon Sep 17 00:00:00 2001 From: julio4 Date: Fri, 3 Nov 2023 17:56:49 +0900 Subject: [PATCH 8/8] chore: bump oz-contracts to 0.8.0-beta.0 --- .../constant_product_amm/Scarb.lock | 4 +-- .../constant_product_amm/Scarb.toml | 4 +-- ...tant_product_amm.cairo => contracts.cairo} | 21 +++++++++++----- .../constant_product_amm/src/lib.cairo | 2 +- .../constant_product_amm/src/tests.cairo | 25 +++++++++++++------ 5 files changed, 37 insertions(+), 19 deletions(-) rename listings/ch01-applications/constant_product_amm/src/{constant_product_amm.cairo => contracts.cairo} (95%) diff --git a/listings/ch01-applications/constant_product_amm/Scarb.lock b/listings/ch01-applications/constant_product_amm/Scarb.lock index 28148050..c06b045c 100644 --- a/listings/ch01-applications/constant_product_amm/Scarb.lock +++ b/listings/ch01-applications/constant_product_amm/Scarb.lock @@ -10,5 +10,5 @@ dependencies = [ [[package]] name = "openzeppelin" -version = "0.7.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.7.0#61a2505fe0c0f19b5de2b3f8dedf421ba2cff657" +version = "0.8.0-beta.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.8.0-beta.0#4c95981a7178e43730f152c85a92336dc6a22d62" diff --git a/listings/ch01-applications/constant_product_amm/Scarb.toml b/listings/ch01-applications/constant_product_amm/Scarb.toml index 720aae7e..2cdd0e03 100644 --- a/listings/ch01-applications/constant_product_amm/Scarb.toml +++ b/listings/ch01-applications/constant_product_amm/Scarb.toml @@ -3,7 +3,7 @@ name = "constant_product_amm" version = "0.1.0" [dependencies] -starknet = "2.2.0" -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } +starknet = ">=2.3.0" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.0-beta.0" } [[target.starknet-contract]] diff --git a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo b/listings/ch01-applications/constant_product_amm/src/contracts.cairo similarity index 95% rename from listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo rename to listings/ch01-applications/constant_product_amm/src/contracts.cairo index 6974ac66..5bc4843b 100644 --- a/listings/ch01-applications/constant_product_amm/src/constant_product_amm.cairo +++ b/listings/ch01-applications/constant_product_amm/src/contracts.cairo @@ -10,8 +10,11 @@ trait IConstantProductAmm { #[starknet::contract] mod ConstantProductAmm { + use core::traits::Into; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use starknet::{ + ContractAddress, get_caller_address, get_contract_address, contract_address_const + }; use integer::u256_sqrt; #[storage] @@ -22,15 +25,19 @@ mod ConstantProductAmm { reserve1: u256, total_supply: u256, balance_of: LegacyMap::, - fee: u256, + // Fee 0 - 1000 (0% - 100%, 1 decimal places) + // E.g. 3 = 0.3% + fee: u16, } #[constructor] - fn constructor(ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u256) { - assert(fee <= 1000, 'fee > 1000'); - self.fee.write(fee); + fn constructor( + ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u16 + ) { + // assert(fee <= 1000, 'fee > 1000'); self.token0.write(IERC20Dispatcher { contract_address: token0 }); self.token1.write(IERC20Dispatcher { contract_address: token1 }); + self.fee.write(fee); } #[generate_trait] @@ -102,7 +109,7 @@ mod ConstantProductAmm { // (yx + ydx - xy) / (x + dx) = dy // ydx / (x + dx) = dy - let amount_in_with_fee = (amount_in * (1000 - self.fee.read())) / 1000; + let amount_in_with_fee = (amount_in * (1000 - self.fee.read().into()) / 1000); let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee); token_out.transfer(caller, amount_out); @@ -259,3 +266,5 @@ mod ConstantProductAmm { } } // ANCHOR_END: ConstantProductAmmContract + + diff --git a/listings/ch01-applications/constant_product_amm/src/lib.cairo b/listings/ch01-applications/constant_product_amm/src/lib.cairo index d538d2c7..0e88faff 100644 --- a/listings/ch01-applications/constant_product_amm/src/lib.cairo +++ b/listings/ch01-applications/constant_product_amm/src/lib.cairo @@ -1,4 +1,4 @@ -mod constant_product_amm; +mod contracts; #[cfg(test)] mod tests; diff --git a/listings/ch01-applications/constant_product_amm/src/tests.cairo b/listings/ch01-applications/constant_product_amm/src/tests.cairo index e0ae300e..e41e52f1 100644 --- a/listings/ch01-applications/constant_product_amm/src/tests.cairo +++ b/listings/ch01-applications/constant_product_amm/src/tests.cairo @@ -1,4 +1,5 @@ mod tests { + use core::option::OptionTrait; use core::traits::TryInto; use openzeppelin::token::erc20::{ ERC20, interface::IERC20Dispatcher, interface::IERC20DispatcherTrait @@ -6,7 +7,7 @@ mod tests { use openzeppelin::utils::serde::SerializedAppend; use openzeppelin::tests::utils; - use super::{ + use constant_product_amm::contracts::{ ConstantProductAmm, IConstantProductAmmDispatcher, IConstantProductAmmDispatcherTrait }; use starknet::{ @@ -16,6 +17,8 @@ mod tests { use starknet::testing::set_contract_address; use starknet::class_hash::Felt252TryIntoClassHash; + use debug::PrintTrait; + const BANK: felt252 = 0x123; const INITIAL_SUPPLY: u256 = 10_000; @@ -44,14 +47,20 @@ mod tests { let (token0_address, token0) = deploy_erc20('Token0', 'T0', recipient, INITIAL_SUPPLY); let (token1_address, token1) = deploy_erc20('Token1', 'T1', recipient, INITIAL_SUPPLY); - let mut calldata = array![]; - calldata.append_serde(token0_address); - calldata.append_serde(token1_address); - // 0.3% fee - calldata.append_serde(3) - - let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); + let fee: u16 = 3; + + let mut calldata: Array:: = array![]; + calldata.append(token0_address.into()); + calldata.append(token1_address.into()); + calldata.append(fee.into()); + + let (contract_address, _) = starknet::deploy_syscall( + ConstantProductAmm::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap(); + // Or with OpenZeppelin helper: + // let contract_address = utils::deploy(ConstantProductAmm::TEST_CLASS_HASH, calldata); Deployment { contract: IConstantProductAmmDispatcher { contract_address }, token0, token1 } }