From b3a9e016e4eaa0e38ccfc797a64ab04a43dc40e9 Mon Sep 17 00:00:00 2001 From: faurdent Date: Sun, 20 Oct 2024 13:21:55 +0200 Subject: [PATCH 01/44] Swap method moved to internal trait --- src/interfaces.cairo | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 7b51c404..9f4e0b05 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -1,11 +1,9 @@ use ekubo::types::keys::PoolKey; -use spotnet::types::{MarketReserveData, SwapData, SwapResult, DepositData}; +use spotnet::types::{MarketReserveData, DepositData}; use starknet::{ContractAddress}; #[starknet::interface] pub trait IDeposit { - fn swap(ref self: TContractState, swap_data: SwapData) -> SwapResult; - fn loop_liquidity( ref self: TContractState, deposit_data: DepositData, From d994326297a4ee9da34965415f4b99f38016fe22 Mon Sep 17 00:00:00 2001 From: faurdent Date: Sun, 20 Oct 2024 13:22:57 +0200 Subject: [PATCH 02/44] Docstrings added, swap removed from public interface --- src/deposit.cairo | 33 +++++++- tests/test_loop.cairo | 187 +++--------------------------------------- 2 files changed, 43 insertions(+), 177 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 86cf755d..7ae62b8a 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -85,14 +85,29 @@ mod Deposit { PositionClosed: PositionClosed } - #[abi(embed_v0)] - impl Deposit of IDeposit { + #[generate_trait] + impl InternalImpl of InternalTrait { fn swap(ref self: ContractState, swap_data: SwapData) -> SwapResult { ekubo::components::shared_locker::call_core_with_callback( self.ekubo_core.read(), @swap_data ) } + } + #[abi(embed_v0)] + impl Deposit of IDeposit { + /// Loops collateral token on ZKlend. + /// + /// # Panics + /// + /// `is_position_open` storage variable is set to true('Open position already exists') + /// `amount` field of `deposit_data` is `0` or `pool_price` is `0` + /// address of the caller is not equal to `owner` storage address + /// + /// # Paraemters + /// * `deposit_data` - Object of which stores main deposit information. + /// * `pool_key` - Ekubo type which represents data about pool. + /// * `pool_price` - Price of `deposit` token in terms of `debt` token in Ekubo pool. fn loop_liquidity( ref self: ContractState, deposit_data: DepositData, @@ -177,6 +192,20 @@ mod Deposit { ); } + /// Closes position on ZKlend. + /// + /// # Panics + /// + /// `is_position_open` storage variable is set to false('Open position not exists') + /// `supply_price` or `debt_price` is `0` + /// address of the caller is not equal to `owner` storage address + /// + /// # Paraemters + /// * `supply_token`: ContractAddress - Address of the token used as collateral. + /// * `debt_token`: ContractAddress - Address of the token used as borrowing. + /// * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping tokens. + /// * `supply_price`: felt252 - Price of `supply` token in terms of `debt` token in Ekubo pool. + /// * `debt_price`: felt252 - Price of `debt` token in terms of `supply` token in Ekubo pool. fn close_position( ref self: ContractState, supply_token: ContractAddress, diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index a9d8e52d..3ccb5793 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -8,9 +8,9 @@ use snforge_std::cheatcodes::execution_info::caller_address::{ }; use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; use spotnet::interfaces::{IDepositDispatcher, IDepositDispatcherTrait}; -use spotnet::types::{SwapData, DepositData}; +use spotnet::types::{DepositData}; -use starknet::{ContractAddress}; +use starknet::{ContractAddress, get_caller_address}; pub const EKUBO_CORE_MAINNET: felt252 = 0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b; @@ -24,128 +24,10 @@ fn deploy_user_contract(user: ContractAddress) -> IDepositDispatcher { IDepositDispatcher {contract_address: deposit_address} } -#[test] -#[fork("MAINNET")] -fn test_quote_for_base_mainnet() { - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); - let strk_addr: ContractAddress = - 0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - - let pool_key = PoolKey { - token0: strk_addr, - token1: eth_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() - }; - let params = SwapParameters { - amount: i129 { mag: 10000000000000, sign: false }, - is_token1: false, - sqrt_ratio_limit: 18446748437148339061, - skip_ahead: 0 - }; - let token_disp = IERC20Dispatcher { - contract_address: if params.is_token1 { - eth_addr - } else { - strk_addr - } - }; - let deposit_dispatcher = deploy_user_contract(user); - let disp = IERC20Dispatcher { contract_address: strk_addr }; - println!("My bal ETH: {}", token_disp.balanceOf(user)); - println!("My bal STRK: {}", disp.balanceOf(user)); - - start_cheat_caller_address(token_disp.contract_address, user); - token_disp.transfer(deposit_dispatcher.contract_address.try_into().unwrap(), params.amount.mag.into()); - stop_cheat_caller_address(token_disp.contract_address); - - let res = deposit_dispatcher.swap(SwapData { params: params, pool_key, caller: user }); - assert(res.delta.amount0.mag == params.amount.mag, 'Amount not swapped'); -} - -#[test] -#[fork("MAINNET")] -fn test_both_directions_mainnet() { - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - - let pool_key = PoolKey { - token0: eth_addr, - token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() - }; - let params = SwapParameters { - amount: i129 { mag: 1000000, sign: false }, - is_token1: true, - sqrt_ratio_limit: 6277100250585753475930931601400621808602321654880405518632, - skip_ahead: 0 - }; - let token_disp = IERC20Dispatcher { - contract_address: if params.is_token1 { - usdc_addr - } else { - eth_addr - } - }; - let deposit_disp = deploy_user_contract(user); - let disp = IERC20Dispatcher { contract_address: eth_addr }; - println!("My bal USDC: {}", token_disp.balanceOf(user)); - println!("My bal ETH: {}", disp.balanceOf(user)); - start_cheat_caller_address(token_disp.contract_address, user); - token_disp.transfer(deposit_disp.contract_address.try_into().unwrap(), params.amount.mag.into()); - stop_cheat_caller_address(token_disp.contract_address); - - - let res = deposit_disp.swap(SwapData { params: params, pool_key, caller: user }); - println!("Swapped: {}", res.delta.amount0.mag); - - let params2 = SwapParameters { - amount: i129 { mag: res.delta.amount0.mag, sign: false }, - is_token1: false, - sqrt_ratio_limit: 18446748437148339061, - skip_ahead: 0 - }; - - let token_disp = IERC20Dispatcher { - contract_address: if params2.is_token1 { - usdc_addr - } else { - eth_addr - } - }; - let deposit_disp = deploy_user_contract(user); - start_cheat_caller_address(token_disp.contract_address, user); - token_disp.transfer(deposit_disp.contract_address.try_into().unwrap(), params2.amount.mag.into()); - stop_cheat_caller_address(token_disp.contract_address); - - let res = deposit_disp.swap(SwapData { params: params2, pool_key, caller: user }); - println!("My bal USDC: {}", IERC20Dispatcher { contract_address: usdc_addr }.balanceOf(user)); - println!("Swapped: {}", res.delta.amount0.mag); -} - #[test] #[fork("MAINNET")] fn test_loop_base_token_zklend() { + // println!("2: {amount}"); let usdc_addr: ContractAddress = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 .try_into() @@ -154,12 +36,10 @@ fn test_loop_base_token_zklend() { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 + let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); - // let deposit_contract = 0.try_into().unwrap(); - let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, @@ -167,16 +47,17 @@ fn test_loop_base_token_zklend() { tick_spacing: 1000, extension: 0.try_into().unwrap() }; - let pool_price = 2400000000; + let pool_price = 2600000000; let token_disp = IERC20Dispatcher { contract_address: eth_addr }; let deposit_disp = deploy_user_contract(user); start_cheat_caller_address(eth_addr.try_into().unwrap(), user); - token_disp.approve(deposit_disp.contract_address, 685000000000000); + // token_disp.approve(deposit_disp.contract_address, amount.into()); + token_disp.approve(deposit_disp.contract_address, 68500000000000); stop_cheat_caller_address(eth_addr); - // println!("There 0"); + deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2 }, + DepositData { token: eth_addr, amount: 68500000000000, multiplier: 4 }, pool_key, pool_price ); @@ -204,17 +85,17 @@ fn test_loop_quote_token_zklend() { tick_spacing: 1000, extension: 0.try_into().unwrap() }; - let pool_price = 410000000000000; + let pool_price = 370000000000000; let token_disp = IERC20Dispatcher { contract_address: usdc_addr }; let disp = deploy_user_contract(user); start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); - token_disp.approve(disp.contract_address, 10000000); + token_disp.approve(disp.contract_address, 60000000); stop_cheat_caller_address(usdc_addr); disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 10000000, multiplier: 3 }, + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, pool_key, pool_price ); @@ -382,48 +263,4 @@ fn test_loop_dai() { // deposit_disp.close_position(usdc_addr, eth_addr, pool_key, supply_price, debt_price); // } -//// Implement looping through STRK and other tokens -// #[test] -// #[fork("MAINNET")] -// fn test_loop_eth_strk_token_zklend() { -// let strk_addr: ContractAddress = -// 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d -// .try_into() -// .unwrap(); -// let eth_addr: ContractAddress = -// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 -// .try_into() -// .unwrap(); -// let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 -// .try_into() -// .unwrap(); - -// let swapper = deploy_core(EKUBO_CORE_MAINNET); -// let pool_key = PoolKey { -// token0: strk_addr, -// token1: eth_addr, -// fee: 0x28f5c28f5c28f5c28f5c28f5c28f5c2, -// tick_spacing: 19802, -// extension: 0.try_into().unwrap() -// }; -// let pool_price = 6000000000000000000000; -// let token_disp = IERC20Dispatcher { contract_address: eth_addr }; -// // 41600000000000000000 -// // 84674241142648157000 -// start_cheat_caller_address(eth_addr.try_into().unwrap(), user); -// token_disp.approve(swapper.contract_address, 10000000000000000); -// stop_cheat_caller_address(eth_addr); - -// let disp = ICoreDispatcher { contract_address: swapper.contract_address }; -// disp -// .loop_liquidity( -// DepositData { -// token: eth_addr, amount: 10000000000000000, multiplier: 3 -// }, // For now multiplier is a number of iterations -// pool_key, -// pool_price, -// user -// ); -// } - From 97e9c24794f51e3bcb5323845dc2e03d8944efe5 Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 22 Oct 2024 00:05:30 +0200 Subject: [PATCH 03/44] New assertions --- src/deposit.cairo | 67 +++++++++++++++++++++++++++---------------- src/interfaces.cairo | 3 +- tests/test_loop.cairo | 26 +++++++---------- 3 files changed, 55 insertions(+), 41 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 7ae62b8a..f2595f6a 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -37,7 +37,10 @@ mod Deposit { } fn get_borrow_amount( - borrow_capacity: felt252, token_price: felt252, decimals_difference: felt252, total_borrowed: felt252 + borrow_capacity: felt252, + token_price: felt252, + decimals_difference: felt252, + total_borrowed: felt252 ) -> felt252 { let borrow_const = 80; let amount_base_token = token_price * borrow_capacity; @@ -97,32 +100,48 @@ mod Deposit { #[abi(embed_v0)] impl Deposit of IDeposit { /// Loops collateral token on ZKlend. - /// + /// /// # Panics - /// + /// /// `is_position_open` storage variable is set to true('Open position already exists') /// `amount` field of `deposit_data` is `0` or `pool_price` is `0` /// address of the caller is not equal to `owner` storage address - /// + /// /// # Paraemters /// * `deposit_data` - Object of which stores main deposit information. /// * `pool_key` - Ekubo type which represents data about pool. /// * `pool_price` - Price of `deposit` token in terms of `debt` token in Ekubo pool. + /// * `usdc_price` - Price of `deposit` token in USDC. fn loop_liquidity( ref self: ContractState, deposit_data: DepositData, pool_key: PoolKey, - pool_price: felt252 + pool_price: felt252, + usdc_price: u256 ) { - assert(get_caller_address() == self.owner.read(), 'Caller is not an owner'); + let caller = get_caller_address(); + assert(caller == self.owner.read(), 'Caller is not an owner'); assert(!self.is_position_open.read(), 'Open position already exists'); let DepositData { token, amount, multiplier } = deposit_data; - assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); + let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; + let deposit_token_decimals = fast_power(10_u128, token_dispatcher.decimals().into()); + assert(multiplier < 5, 'Multiplier not supported'); + assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); + assert( + amount * usdc_price / deposit_token_decimals.into() > 1000000, + 'Loop amount is too small' + ); + assert( + token_dispatcher.allowance(caller, get_contract_address()) >= amount, + 'Approved amount incuficient' + ); + assert(token_dispatcher.balanceOf(caller) >= amount, 'Insufficient balance'); + let (EKUBO_LOWER_SQRT_LIMIT, EKUBO_UPPER_SQRT_LIMIT) = ( 18446748437148339061, 6277100250585753475930931601400621808602321654880405518632 ); - let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; + let zk_market = self.zk_market.read(); let is_token1 = token == pool_key.token0; let (borrowing_token, sqrt_limit) = if is_token1 { @@ -139,19 +158,17 @@ mod Deposit { zk_market.enable_collateral(token); - let deposit_token_decimals = fast_power( - 10_u128, token_dispatcher.decimals().into() - ); token_dispatcher.approve(zk_market.contract_address, amount); zk_market.deposit(token, amount.try_into().expect('Overflow')); - let mut deposited = amount; - let mut total_borrowed = 0; - let mut accumulated = 0; + let (mut total_borrowed, mut accumulated, mut deposited) = (0, 0, amount); while (amount + accumulated) / amount < multiplier.into() { let borrow_capacity = (deposited * collateral_factor / ZK_SCALE_DECIMALS); let to_borrow = get_borrow_amount( - borrow_capacity.try_into().unwrap(), pool_price, deposit_token_decimals.into(), total_borrowed + borrow_capacity.try_into().unwrap(), + pool_price, + deposit_token_decimals.into(), + total_borrowed ); total_borrowed += to_borrow; zk_market.borrow(borrowing_token, to_borrow); @@ -193,19 +210,22 @@ mod Deposit { } /// Closes position on ZKlend. - /// + /// /// # Panics - /// + /// /// `is_position_open` storage variable is set to false('Open position not exists') /// `supply_price` or `debt_price` is `0` /// address of the caller is not equal to `owner` storage address - /// + /// /// # Paraemters /// * `supply_token`: ContractAddress - Address of the token used as collateral. /// * `debt_token`: ContractAddress - Address of the token used as borrowing. - /// * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping tokens. - /// * `supply_price`: felt252 - Price of `supply` token in terms of `debt` token in Ekubo pool. - /// * `debt_price`: felt252 - Price of `debt` token in terms of `supply` token in Ekubo pool. + /// * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping + /// tokens. + /// * `supply_price`: felt252 - Price of `supply` token in terms of `debt` token in Ekubo + /// pool. + /// * `debt_price`: felt252 - Price of `debt` token in terms of `supply` token in Ekubo + /// pool. fn close_position( ref self: ContractState, supply_token: ContractAddress, @@ -294,10 +314,7 @@ mod Deposit { self .emit( PositionClosed { - deposit_token: supply_token, - debt_token, - repaid_amount, - withdrawn_amount + deposit_token: supply_token, debt_token, repaid_amount, withdrawn_amount } ); } diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 9f4e0b05..988e3fb3 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -8,7 +8,8 @@ pub trait IDeposit { ref self: TContractState, deposit_data: DepositData, pool_key: PoolKey, - pool_price: felt252 + pool_price: felt252, + usdc_price: u256 ); fn close_position( diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 3ccb5793..cd5111ec 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -20,8 +20,10 @@ pub const ZKLEND_MARKET: felt252 = fn deploy_user_contract(user: ContractAddress) -> IDepositDispatcher { let deposit_contract = declare("Deposit").unwrap().contract_class(); - let (deposit_address, _) = deposit_contract.deploy(@array![user.try_into().unwrap(), EKUBO_CORE_MAINNET, ZKLEND_MARKET]).expect('Deploy failed'); - IDepositDispatcher {contract_address: deposit_address} + let (deposit_address, _) = deposit_contract + .deploy(@array![user.try_into().unwrap(), EKUBO_CORE_MAINNET, ZKLEND_MARKET]) + .expect('Deploy failed'); + IDepositDispatcher { contract_address: deposit_address } } #[test] @@ -92,12 +94,9 @@ fn test_loop_quote_token_zklend() { token_disp.approve(disp.contract_address, 60000000); stop_cheat_caller_address(usdc_addr); - disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, - pool_key, - pool_price + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, pool_key, pool_price ); } @@ -130,9 +129,7 @@ fn test_loop_unauthorized() { start_cheat_caller_address(disp.contract_address, user); disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, - pool_key, - pool_price + DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, pool_key, pool_price ); stop_cheat_caller_address(disp.contract_address); } @@ -140,7 +137,6 @@ fn test_loop_unauthorized() { #[test] #[fork("MAINNET")] fn test_close_position_base_token() { - let usdc_addr: ContractAddress = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 .try_into() @@ -184,9 +180,9 @@ fn test_close_position_base_token() { #[fork("MAINNET")] fn test_loop_dai() { let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); let dai_addr: ContractAddress = 0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3 .try_into() @@ -218,7 +214,6 @@ fn test_loop_dai() { ); println!("Balance after: {}", token_disp.balanceOf(user)); } - // #[test] // #[fork("MAINNET")] // fn test_close_position_quote_token() { @@ -233,7 +228,8 @@ fn test_loop_dai() { // 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 // .try_into() // .unwrap(); -// let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 +// let user: ContractAddress = +// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 // .try_into() // .unwrap(); // let deposit_contract = 0.try_into().unwrap(); From f69b44cb24c60a28cab997178463f99134da268c Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 22 Oct 2024 23:45:38 +0200 Subject: [PATCH 04/44] Pragma added for testing --- Scarb.lock | 6 ++++++ Scarb.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Scarb.lock b/Scarb.lock index 25b9bd12..7dc1f50d 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -64,6 +64,11 @@ version = "0.17.0" source = "registry+https://scarbs.xyz/" checksum = "sha256:36d93e353f42fd6b824abcd8b4b51c3f5d02c893c5f886ae81403b0368aa5fde" +[[package]] +name = "pragma_lib" +version = "1.0.0" +source = "git+https://github.com/astraly-labs/pragma-lib?tag=2.8.2#86d7ccdc15b349b8b48d9796fc8464c947bea6e1" + [[package]] name = "snforge_scarb_plugin" version = "0.1.0" @@ -84,5 +89,6 @@ dependencies = [ "alexandria_math", "ekubo", "openzeppelin_token", + "pragma_lib", "snforge_std", ] diff --git a/Scarb.toml b/Scarb.toml index 1703737f..5015bcff 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -13,6 +13,7 @@ alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.g openzeppelin_token = "0.17.0" [dev-dependencies] +pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib", tag = "2.8.2" } snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.30.0" } [lib] @@ -29,6 +30,5 @@ block_id.tag = "latest" [[tool.snforge.fork]] name = "MAINNET" -# url = "https://starknet-mainnet.public.blastapi.io/rpc/v0_7" url = "http://127.0.0.1:5050" block_id.tag = "latest" From 7349a40dfdf728a2c73167e25f18092806c6f040 Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 22 Oct 2024 23:46:47 +0200 Subject: [PATCH 05/44] More validations for contract stability. Owner check reworked --- src/deposit.cairo | 24 +++++++++++++----------- src/interfaces.cairo | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index f2595f6a..3fc4c888 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -14,7 +14,7 @@ mod Deposit { use starknet::event::EventEmitter; use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess}; - use starknet::{ContractAddress, get_contract_address, get_caller_address}; + use starknet::{ContractAddress, get_contract_address, get_caller_address, get_tx_info}; #[storage] struct Storage { @@ -37,8 +37,8 @@ mod Deposit { } fn get_borrow_amount( - borrow_capacity: felt252, - token_price: felt252, + borrow_capacity: u256, + token_price: u256, decimals_difference: felt252, total_borrowed: felt252 ) -> felt252 { @@ -116,11 +116,11 @@ mod Deposit { ref self: ContractState, deposit_data: DepositData, pool_key: PoolKey, - pool_price: felt252, + pool_price: u256, usdc_price: u256 ) { - let caller = get_caller_address(); - assert(caller == self.owner.read(), 'Caller is not an owner'); + let user_acount = get_tx_info().unbox().account_contract_address; + assert(user_acount == self.owner.read(), 'Caller is not an owner'); assert(!self.is_position_open.read(), 'Open position already exists'); let DepositData { token, amount, multiplier } = deposit_data; let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; @@ -129,14 +129,16 @@ mod Deposit { assert(multiplier < 5, 'Multiplier not supported'); assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); assert( - amount * usdc_price / deposit_token_decimals.into() > 1000000, + amount * usdc_price / deposit_token_decimals.into() >= 1000000, 'Loop amount is too small' - ); + ); // User needs to have at least 1 USDC, so rounded down amount is OK. + + let curr_contract_address = get_contract_address(); assert( - token_dispatcher.allowance(caller, get_contract_address()) >= amount, + token_dispatcher.allowance(user_acount, curr_contract_address) >= amount, 'Approved amount incuficient' ); - assert(token_dispatcher.balanceOf(caller) >= amount, 'Insufficient balance'); + assert(token_dispatcher.balanceOf(user_acount) >= amount, 'Insufficient balance'); let (EKUBO_LOWER_SQRT_LIMIT, EKUBO_UPPER_SQRT_LIMIT) = ( 18446748437148339061, 6277100250585753475930931601400621808602321654880405518632 @@ -149,7 +151,6 @@ mod Deposit { } else { (pool_key.token0, EKUBO_LOWER_SQRT_LIMIT) }; - let curr_contract_address = get_contract_address(); token_dispatcher.transferFrom(self.owner.read(), curr_contract_address, amount); let reserve_data = zk_market.get_reserve_data(token); @@ -191,6 +192,7 @@ mod Deposit { deposited += amount_swapped.into(); accumulated += amount_swapped.into(); }; + self.is_position_open.write(true); self .emit( diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 988e3fb3..78beff74 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -8,7 +8,7 @@ pub trait IDeposit { ref self: TContractState, deposit_data: DepositData, pool_key: PoolKey, - pool_price: felt252, + pool_price: u256, usdc_price: u256 ); From 3660f96b413d55496c4238c702e91124ff4923f1 Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 22 Oct 2024 23:47:18 +0200 Subject: [PATCH 06/44] First fuzz test for looping --- tests/test_loop.cairo | 340 ++++++++++++++++++++++++++---------------- 1 file changed, 213 insertions(+), 127 deletions(-) diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index cd5111ec..14bd9e13 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -3,13 +3,22 @@ use ekubo::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use ekubo::types::i129::{i129}; use ekubo::types::keys::{PoolKey}; +use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; +use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; +use snforge_std::cheatcodes::execution_info::account_contract_address::{ + start_cheat_account_contract_address, stop_cheat_account_contract_address +}; + use snforge_std::cheatcodes::execution_info::caller_address::{ start_cheat_caller_address, stop_cheat_caller_address }; use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; -use spotnet::interfaces::{IDepositDispatcher, IDepositDispatcherTrait}; +use spotnet::interfaces::{ + IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, IDepositDispatcherTrait +}; use spotnet::types::{DepositData}; +use starknet::syscalls::{deploy_syscall, get_execution_info_syscall}; use starknet::{ContractAddress, get_caller_address}; pub const EKUBO_CORE_MAINNET: felt252 = @@ -18,6 +27,9 @@ pub const EKUBO_CORE_MAINNET: felt252 = pub const ZKLEND_MARKET: felt252 = 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; +pub const PRAGMA_ADDRESS: felt252 = + 0x02a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; + fn deploy_user_contract(user: ContractAddress) -> IDepositDispatcher { let deposit_contract = declare("Deposit").unwrap().contract_class(); let (deposit_address, _) = deposit_contract @@ -26,10 +38,18 @@ fn deploy_user_contract(user: ContractAddress) -> IDepositDispatcher { IDepositDispatcher { contract_address: deposit_address } } +fn get_asset_price_pragma(pair: felt252) -> u128 { + let oracle_dispatcher = IPragmaABIDispatcher { + contract_address: PRAGMA_ADDRESS.try_into().unwrap() + }; + let output: PragmaPricesResponse = oracle_dispatcher + .get_data(DataType::SpotEntry(pair), AggregationMode::Median(())); + output.price / 100 // Make 6 decimals wide instead of 8. +} + #[test] #[fork("MAINNET")] -fn test_loop_base_token_zklend() { - // println!("2: {amount}"); +fn test_loop_eth_valid() { let usdc_addr: ContractAddress = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 .try_into() @@ -49,94 +69,29 @@ fn test_loop_base_token_zklend() { tick_spacing: 1000, extension: 0.try_into().unwrap() }; - let pool_price = 2600000000; + let pool_price = get_asset_price_pragma('ETH/USD').into(); let token_disp = IERC20Dispatcher { contract_address: eth_addr }; let deposit_disp = deploy_user_contract(user); start_cheat_caller_address(eth_addr.try_into().unwrap(), user); - // token_disp.approve(deposit_disp.contract_address, amount.into()); - token_disp.approve(deposit_disp.contract_address, 68500000000000); + token_disp.approve(deposit_disp.contract_address, 685000000000000); stop_cheat_caller_address(eth_addr); + start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 68500000000000, multiplier: 4 }, + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4 }, pool_key, + pool_price, pool_price ); + stop_cheat_account_contract_address(deposit_disp.contract_address); } #[test] +#[feature("safe_dispatcher")] +#[fuzzer(runs: 10)] #[fork("MAINNET")] -fn test_loop_quote_token_zklend() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); - - let pool_key = PoolKey { - token0: eth_addr, - token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() - }; - let pool_price = 370000000000000; - let token_disp = IERC20Dispatcher { contract_address: usdc_addr }; - let disp = deploy_user_contract(user); - start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); - token_disp.approve(disp.contract_address, 60000000); - stop_cheat_caller_address(usdc_addr); - - disp - .loop_liquidity( - DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, pool_key, pool_price - ); -} - -#[test] -#[should_panic(expected: ('Caller is not the owner',))] -#[fork("MAINNET")] -fn test_loop_unauthorized() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); - - let pool_key = PoolKey { - token0: eth_addr, - token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() - }; - let pool_price = 410000000000000; - - let disp = deploy_user_contract(0x1223.try_into().unwrap()); - start_cheat_caller_address(disp.contract_address, user); - disp - .loop_liquidity( - DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, pool_key, pool_price - ); - stop_cheat_caller_address(disp.contract_address); -} - -#[test] -#[fork("MAINNET")] -fn test_close_position_base_token() { +fn test_loop_eth_fuzz(amount: u64) { let usdc_addr: ContractAddress = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 .try_into() @@ -145,11 +100,9 @@ fn test_close_position_base_token() { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 + let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); - - // let deposit_contract = disp.deploy_user_contract(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, @@ -157,63 +110,196 @@ fn test_close_position_base_token() { tick_spacing: 1000, extension: 0.try_into().unwrap() }; - let pool_price = 2400000000; - let quote_token_price = 410182369224320; + let pool_price = get_asset_price_pragma('ETH/USD').into(); let token_disp = IERC20Dispatcher { contract_address: eth_addr }; let deposit_disp = deploy_user_contract(user); - println!("Balance before: {}", token_disp.balanceOf(user)); + let deposit_disp = IDepositSafeDispatcher { contract_address: deposit_disp.contract_address }; start_cheat_caller_address(eth_addr.try_into().unwrap(), user); - token_disp.approve(deposit_disp.contract_address, 685000000000000); + token_disp.approve(deposit_disp.contract_address, amount.into()); stop_cheat_caller_address(eth_addr); - deposit_disp + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + match deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 3 }, + DepositData { token: eth_addr, amount: amount.into(), multiplier: 4 }, pool_key, + pool_price, pool_price - ); - println!("Balance mid: {}", token_disp.balanceOf(user)); - deposit_disp.close_position(eth_addr, usdc_addr, pool_key, pool_price, quote_token_price); - println!("Balance after: {}", token_disp.balanceOf(user)); + ) { + Result::Ok(_) => (), + Result::Err(panic_data) => { + let message = *panic_data.at(0); + assert( + message == 'Parameters cannot be zero' + || message == 'Loop amount is too small' + || message == 'Approved amount incuficient' + || message == 'Insufficient balance', + message + ); // Acceptable panics which can be triggered by fuzzers' values + } + }; + stop_cheat_account_contract_address(deposit_disp.contract_address); } -#[test] -#[fork("MAINNET")] -fn test_loop_dai() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let dai_addr: ContractAddress = - 0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3 - .try_into() - .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); +// #[test] +// #[fork("MAINNET")] +// fn test_loop_quote_token_zklend() { +// let usdc_addr: ContractAddress = +// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 +// .try_into() +// .unwrap(); +// let eth_addr: ContractAddress = +// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 +// .try_into() +// .unwrap(); +// let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 +// .try_into() +// .unwrap(); - let pool_key = PoolKey { - token0: dai_addr, - token1: usdc_addr, - fee: 0xa7c6a3aab97597fb9b6713851eb8, - tick_spacing: 10, - extension: 0.try_into().unwrap() - }; - let pool_price = 960904; - let quote_token_price = 1033974330230609190; - let token_disp = IERC20Dispatcher { contract_address: dai_addr }; - let deposit_disp = deploy_user_contract(user); - println!("Balance before: {}", token_disp.balanceOf(user)); - start_cheat_caller_address(dai_addr.try_into().unwrap(), user); - token_disp.approve(deposit_disp.contract_address, 6850000000000000000); - stop_cheat_caller_address(dai_addr); - deposit_disp - .loop_liquidity( - DepositData { token: dai_addr, amount: 6850000000000000000, multiplier: 2 }, - pool_key, - pool_price - ); - println!("Balance after: {}", token_disp.balanceOf(user)); -} +// let pool_key = PoolKey { +// token0: eth_addr, +// token1: usdc_addr, +// fee: 170141183460469235273462165868118016, +// tick_spacing: 1000, +// extension: 0.try_into().unwrap() +// }; +// let pool_price = 370000000000000; +// let token_disp = IERC20Dispatcher { contract_address: usdc_addr }; +// let disp = deploy_user_contract(user); +// start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); +// token_disp.approve(disp.contract_address, 60000000); +// stop_cheat_caller_address(usdc_addr); + +// disp +// .loop_liquidity( +// DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, +// pool_key, +// pool_price, +// 1000000 +// ); +// } + +// #[test] +// #[should_panic(expected: ('Caller is not the owner',))] +// #[fork("MAINNET")] +// fn test_loop_unauthorized() { +// let usdc_addr: ContractAddress = +// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 +// .try_into() +// .unwrap(); +// let eth_addr: ContractAddress = +// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 +// .try_into() +// .unwrap(); +// let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 +// .try_into() +// .unwrap(); + +// let pool_key = PoolKey { +// token0: eth_addr, +// token1: usdc_addr, +// fee: 170141183460469235273462165868118016, +// tick_spacing: 1000, +// extension: 0.try_into().unwrap() +// }; +// let pool_price = 410000000000000; + +// let disp = deploy_user_contract(0x1223.try_into().unwrap()); +// start_cheat_caller_address(disp.contract_address, user); +// disp +// .loop_liquidity( +// DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, +// pool_key, +// pool_price, +// 1000000 +// ); +// stop_cheat_caller_address(disp.contract_address); +// } + +// #[test] +// #[fork("MAINNET")] +// fn test_close_position_base_token() { +// let usdc_addr: ContractAddress = +// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 +// .try_into() +// .unwrap(); +// let eth_addr: ContractAddress = +// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 +// .try_into() +// .unwrap(); +// let user: ContractAddress = +// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 +// .try_into() +// .unwrap(); + +// // let deposit_contract = disp.deploy_user_contract(); +// let pool_key = PoolKey { +// token0: eth_addr, +// token1: usdc_addr, +// fee: 170141183460469235273462165868118016, +// tick_spacing: 1000, +// extension: 0.try_into().unwrap() +// }; +// let pool_price = 2400000000; +// let quote_token_price = 410182369224320; +// let token_disp = IERC20Dispatcher { contract_address: eth_addr }; +// let deposit_disp = deploy_user_contract(user); +// println!("Balance before: {}", token_disp.balanceOf(user)); +// start_cheat_caller_address(eth_addr.try_into().unwrap(), user); +// token_disp.approve(deposit_disp.contract_address, 685000000000000); +// stop_cheat_caller_address(eth_addr); +// deposit_disp +// .loop_liquidity( +// DepositData { token: eth_addr, amount: 685000000000000, multiplier: 3 }, +// pool_key, +// pool_price, +// pool_price +// ); +// println!("Balance mid: {}", token_disp.balanceOf(user)); +// deposit_disp.close_position(eth_addr, usdc_addr, pool_key, pool_price, quote_token_price); +// println!("Balance after: {}", token_disp.balanceOf(user)); +// } + +// #[test] +// #[fork("MAINNET")] +// fn test_loop_dai() { +// let usdc_addr: ContractAddress = +// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 +// .try_into() +// .unwrap(); +// let dai_addr: ContractAddress = +// 0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3 +// .try_into() +// .unwrap(); +// let user: ContractAddress = +// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 +// .try_into() +// .unwrap(); + +// let pool_key = PoolKey { +// token0: dai_addr, +// token1: usdc_addr, +// fee: 0xa7c6a3aab97597fb9b6713851eb8, +// tick_spacing: 10, +// extension: 0.try_into().unwrap() +// }; +// let pool_price = 960904; +// let quote_token_price = 1033974330230609190; +// let token_disp = IERC20Dispatcher { contract_address: dai_addr }; +// let deposit_disp = deploy_user_contract(user); +// println!("Balance before: {}", token_disp.balanceOf(user)); +// start_cheat_caller_address(dai_addr.try_into().unwrap(), user); +// token_disp.approve(deposit_disp.contract_address, 6850000000000000000); +// stop_cheat_caller_address(dai_addr); +// deposit_disp +// .loop_liquidity( +// DepositData { token: dai_addr, amount: 6850000000000000000, multiplier: 2 }, +// pool_key, +// pool_price, +// 980000 +// ); +// println!("Balance after: {}", token_disp.balanceOf(user)); +// } // #[test] // #[fork("MAINNET")] // fn test_close_position_quote_token() { From bb8c80ba149d80f2573ba1dd8e80edbf3c57b5f7 Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 23 Oct 2024 16:03:29 +0200 Subject: [PATCH 07/44] New tests for liquidity loop --- src/deposit.cairo | 4 +- tests/test_loop.cairo | 336 ++++++++++++++++++++++++++++++------------ 2 files changed, 247 insertions(+), 93 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 3fc4c888..00a470b7 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -120,13 +120,13 @@ mod Deposit { usdc_price: u256 ) { let user_acount = get_tx_info().unbox().account_contract_address; - assert(user_acount == self.owner.read(), 'Caller is not an owner'); + assert(user_acount == self.owner.read(), 'Caller is not the owner'); assert(!self.is_position_open.read(), 'Open position already exists'); let DepositData { token, amount, multiplier } = deposit_data; let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; let deposit_token_decimals = fast_power(10_u128, token_dispatcher.decimals().into()); - assert(multiplier < 5, 'Multiplier not supported'); + assert(multiplier < 5 && multiplier > 1, 'Multiplier not supported'); assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); assert( amount * usdc_price / deposit_token_decimals.into() >= 1000000, diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 14bd9e13..4fa830d2 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -1,14 +1,17 @@ -use ekubo::interfaces::core::{SwapParameters}; +use core::panic_with_felt252; + +use alexandria_math::fast_power::fast_power; +use ekubo::interfaces::core::{SwapParameters, ICoreDispatcher, ICoreDispatcherTrait}; use ekubo::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use ekubo::types::i129::{i129}; use ekubo::types::keys::{PoolKey}; +use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; use snforge_std::cheatcodes::execution_info::account_contract_address::{ start_cheat_account_contract_address, stop_cheat_account_contract_address }; - use snforge_std::cheatcodes::execution_info::caller_address::{ start_cheat_caller_address, stop_cheat_caller_address }; @@ -21,32 +24,60 @@ use spotnet::types::{DepositData}; use starknet::syscalls::{deploy_syscall, get_execution_info_syscall}; use starknet::{ContractAddress, get_caller_address}; -pub const EKUBO_CORE_MAINNET: felt252 = - 0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b; +mod contracts { + pub const EKUBO_CORE_MAINNET: felt252 = + 0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b; -pub const ZKLEND_MARKET: felt252 = - 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; + pub const ZKLEND_MARKET: felt252 = + 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; -pub const PRAGMA_ADDRESS: felt252 = - 0x02a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; + pub const PRAGMA_ADDRESS: felt252 = + 0x02a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; +} -fn deploy_user_contract(user: ContractAddress) -> IDepositDispatcher { +mod tokens { + pub const ETH: felt252 = 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; + pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; +} + +fn deploy_deposit_contract(user: ContractAddress) -> ContractAddress { let deposit_contract = declare("Deposit").unwrap().contract_class(); let (deposit_address, _) = deposit_contract - .deploy(@array![user.try_into().unwrap(), EKUBO_CORE_MAINNET, ZKLEND_MARKET]) + .deploy( + @array![ + user.try_into().unwrap(), contracts::EKUBO_CORE_MAINNET, contracts::ZKLEND_MARKET + ] + ) .expect('Deploy failed'); - IDepositDispatcher { contract_address: deposit_address } + deposit_address +} + +fn get_deposit_dispatcher(user: ContractAddress) -> IDepositDispatcher { + IDepositDispatcher { contract_address: deploy_deposit_contract(user) } +} + +fn get_safe_deposit_dispatcher(user: ContractAddress) -> IDepositSafeDispatcher { + IDepositSafeDispatcher { contract_address: deploy_deposit_contract(user) } } fn get_asset_price_pragma(pair: felt252) -> u128 { let oracle_dispatcher = IPragmaABIDispatcher { - contract_address: PRAGMA_ADDRESS.try_into().unwrap() + contract_address: contracts::PRAGMA_ADDRESS.try_into().unwrap() }; let output: PragmaPricesResponse = oracle_dispatcher .get_data(DataType::SpotEntry(pair), AggregationMode::Median(())); output.price / 100 // Make 6 decimals wide instead of 8. } +// fn get_token_addresses(pair: felt252) -> (ContractAddress, ContractAddress) { +// match pair { +// 'ETH/USDC' => (tokens::ETH.try_into().unwrap(), tokens::USDC.try_into().unwrap()), +// _ => (0.try_into().unwrap(), 1.try_into().unwrap()), +// } +// } + +// TODO: Add tests for asserts. + #[test] #[fork("MAINNET")] fn test_loop_eth_valid() { @@ -70,8 +101,8 @@ fn test_loop_eth_valid() { extension: 0.try_into().unwrap() }; let pool_price = get_asset_price_pragma('ETH/USD').into(); - let token_disp = IERC20Dispatcher { contract_address: eth_addr }; - let deposit_disp = deploy_user_contract(user); + let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; + let deposit_disp = get_deposit_dispatcher(user); start_cheat_caller_address(eth_addr.try_into().unwrap(), user); token_disp.approve(deposit_disp.contract_address, 685000000000000); stop_cheat_caller_address(eth_addr); @@ -88,8 +119,8 @@ fn test_loop_eth_valid() { } #[test] -#[feature("safe_dispatcher")] #[fuzzer(runs: 10)] +#[feature("safe_dispatcher")] #[fork("MAINNET")] fn test_loop_eth_fuzz(amount: u64) { let usdc_addr: ContractAddress = @@ -110,24 +141,23 @@ fn test_loop_eth_fuzz(amount: u64) { tick_spacing: 1000, extension: 0.try_into().unwrap() }; + let pool_price = get_asset_price_pragma('ETH/USD').into(); - let token_disp = IERC20Dispatcher { contract_address: eth_addr }; - let deposit_disp = deploy_user_contract(user); - let deposit_disp = IDepositSafeDispatcher { contract_address: deposit_disp.contract_address }; + let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; + let deposit_disp = get_safe_deposit_dispatcher(user); + start_cheat_caller_address(eth_addr.try_into().unwrap(), user); token_disp.approve(deposit_disp.contract_address, amount.into()); stop_cheat_caller_address(eth_addr); start_cheat_account_contract_address(deposit_disp.contract_address, user); - match deposit_disp + if let Result::Err(panic_data) = deposit_disp .loop_liquidity( DepositData { token: eth_addr, amount: amount.into(), multiplier: 4 }, pool_key, pool_price, pool_price ) { - Result::Ok(_) => (), - Result::Err(panic_data) => { let message = *panic_data.at(0); assert( message == 'Parameters cannot be zero' @@ -136,85 +166,209 @@ fn test_loop_eth_fuzz(amount: u64) { || message == 'Insufficient balance', message ); // Acceptable panics which can be triggered by fuzzers' values - } }; stop_cheat_account_contract_address(deposit_disp.contract_address); } -// #[test] -// #[fork("MAINNET")] -// fn test_loop_quote_token_zklend() { -// let usdc_addr: ContractAddress = -// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 -// .try_into() -// .unwrap(); -// let eth_addr: ContractAddress = -// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 -// .try_into() -// .unwrap(); -// let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 -// .try_into() -// .unwrap(); +#[test] +#[fork("MAINNET")] +fn test_loop_usdc_valid() { + let usdc_addr: ContractAddress = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 + .try_into() + .unwrap(); -// let pool_key = PoolKey { -// token0: eth_addr, -// token1: usdc_addr, -// fee: 170141183460469235273462165868118016, -// tick_spacing: 1000, -// extension: 0.try_into().unwrap() -// }; -// let pool_price = 370000000000000; -// let token_disp = IERC20Dispatcher { contract_address: usdc_addr }; -// let disp = deploy_user_contract(user); -// start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); -// token_disp.approve(disp.contract_address, 60000000); -// stop_cheat_caller_address(usdc_addr); + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: 170141183460469235273462165868118016, + tick_spacing: 1000, + extension: 0.try_into().unwrap() + }; -// disp -// .loop_liquidity( -// DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, -// pool_key, -// pool_price, -// 1000000 -// ); -// } + let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; + let decimals_sum_power: u128 = fast_power( + 10, + (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + token_disp.decimals()) + .into() + ); + let pool_price = 1 * decimals_sum_power.into() / get_asset_price_pragma('ETH/USD'); + let deposit_disp = get_deposit_dispatcher(user); + start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); + token_disp.approve(deposit_disp.contract_address, 60000000); + stop_cheat_caller_address(usdc_addr); -// #[test] -// #[should_panic(expected: ('Caller is not the owner',))] -// #[fork("MAINNET")] -// fn test_loop_unauthorized() { -// let usdc_addr: ContractAddress = -// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 -// .try_into() -// .unwrap(); -// let eth_addr: ContractAddress = -// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 -// .try_into() -// .unwrap(); -// let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 -// .try_into() -// .unwrap(); + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp + .loop_liquidity( + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, + pool_key, + pool_price.into(), + 1000000 + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); +} -// let pool_key = PoolKey { -// token0: eth_addr, -// token1: usdc_addr, -// fee: 170141183460469235273462165868118016, -// tick_spacing: 1000, -// extension: 0.try_into().unwrap() -// }; -// let pool_price = 410000000000000; +#[test] +#[should_panic(expected: 'Caller is not the owner')] +#[fork("MAINNET")] +fn test_loop_unauthorized() { + let usdc_addr: ContractAddress = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 + .try_into() + .unwrap(); -// let disp = deploy_user_contract(0x1223.try_into().unwrap()); -// start_cheat_caller_address(disp.contract_address, user); -// disp -// .loop_liquidity( -// DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, -// pool_key, -// pool_price, -// 1000000 -// ); -// stop_cheat_caller_address(disp.contract_address); -// } + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: 170141183460469235273462165868118016, + tick_spacing: 1000, + extension: 0.try_into().unwrap() + }; + + let decimals_sum_power: u128 = fast_power( + 10, + (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + + ERC20ABIDispatcher { contract_address: usdc_addr }.decimals()) + .into() + ); + let pool_price = 1 * decimals_sum_power.into() / get_asset_price_pragma('ETH/USD'); + + let disp = get_deposit_dispatcher(user); + + disp + .loop_liquidity( + DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, + pool_key, + pool_price.into(), + 1000000 + ); +} + +#[test] +#[should_panic(expected: 'Open position already exists')] +#[fork("MAINNET")] +fn test_loop_position_exists() { + let usdc_addr: ContractAddress = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 + .try_into() + .unwrap(); + + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: 170141183460469235273462165868118016, + tick_spacing: 1000, + extension: 0.try_into().unwrap() + }; + + let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; + let decimals_sum_power: u128 = fast_power( + 10, + (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + token_disp.decimals()) + .into() + ); + let pool_price = 1 * decimals_sum_power.into() / get_asset_price_pragma('ETH/USD'); + let deposit_disp = get_deposit_dispatcher(user); + start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); + token_disp.approve(deposit_disp.contract_address, 60000000); + stop_cheat_caller_address(usdc_addr); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp + .loop_liquidity( + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, + pool_key, + pool_price.into(), + 1000000 + ); + deposit_disp + .loop_liquidity( + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, + pool_key, + pool_price.into(), + 1000000 + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); +} + +#[test] +#[fuzzer(runs: 10)] +#[feature("safe_dispatcher")] +#[fork("MAINNET")] +fn test_loop_position_exists_fuzz(amount: u64) { + let usdc_addr: ContractAddress = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + .try_into() + .unwrap(); + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: 170141183460469235273462165868118016, + tick_spacing: 1000, + extension: 0.try_into().unwrap() + }; + + let pool_price = get_asset_price_pragma('ETH/USD').into(); + let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; + let deposit_disp = get_safe_deposit_dispatcher(user); + + start_cheat_caller_address(eth_addr.try_into().unwrap(), user); + token_disp.approve(deposit_disp.contract_address, amount.into()); + stop_cheat_caller_address(eth_addr); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + + if let Result::Err(_) = deposit_disp + .loop_liquidity( + DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, + pool_key, + pool_price, + pool_price + ) { + return; + }; + match deposit_disp.loop_liquidity( + DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, + pool_key, + pool_price, + pool_price + ) { + Result::Ok(_) => panic_with_felt252('Not panicked with position open'), + Result::Err(panic_data) => assert(*panic_data.at(0) == 'Open position already exists', *panic_data.at(0)) + }; + stop_cheat_account_contract_address(deposit_disp.contract_address); +} + +fn test_ // #[test] // #[fork("MAINNET")] From f331bce2dbbcb841d7c99e4fc2ddfa7f1a11af08 Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 23 Oct 2024 23:50:38 +0200 Subject: [PATCH 08/44] Dependencies updated --- Scarb.lock | 8 ++++---- Scarb.toml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 7dc1f50d..273095b8 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -71,13 +71,13 @@ source = "git+https://github.com/astraly-labs/pragma-lib?tag=2.8.2#86d7ccdc15b34 [[package]] name = "snforge_scarb_plugin" -version = "0.1.0" -source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.30.0#196f06b251926697c3d66800f2a93ae595e76496" +version = "0.32.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.32.0#3817c903b640201c72e743b9bbe70a97149828a2" [[package]] name = "snforge_std" -version = "0.30.0" -source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.30.0#196f06b251926697c3d66800f2a93ae595e76496" +version = "0.32.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.32.0#3817c903b640201c72e743b9bbe70a97149828a2" dependencies = [ "snforge_scarb_plugin", ] diff --git a/Scarb.toml b/Scarb.toml index 5015bcff..da783adb 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -2,19 +2,19 @@ name = "spotnet" version = "0.1.0" edition = "2024_07" -cairo-version = "2.8.2" +cairo-version = "2.8.4" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -starknet = "2.8.2" +starknet = "2.8.4" ekubo = { git = "https://github.com/ekuboprotocol/abis", rev = "edb6de8" } alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev = "8208871" } openzeppelin_token = "0.17.0" [dev-dependencies] pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib", tag = "2.8.2" } -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.30.0" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.32.0" } [lib] From 8fd1a54a83e6402b538e212d862cc4d11f2cfbed Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 23 Oct 2024 23:52:59 +0200 Subject: [PATCH 09/44] Use input amount computing on Ekubo to not loose token on last swap --- src/deposit.cairo | 73 +++++++++++++++++++++++--------------------- src/interfaces.cairo | 4 +-- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 00a470b7..6301b79e 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -14,7 +14,7 @@ mod Deposit { use starknet::event::EventEmitter; use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess}; - use starknet::{ContractAddress, get_contract_address, get_caller_address, get_tx_info}; + use starknet::{ContractAddress, get_contract_address, get_tx_info}; #[storage] struct Storage { @@ -42,26 +42,26 @@ mod Deposit { decimals_difference: felt252, total_borrowed: felt252 ) -> felt252 { - let borrow_const = 80; + let borrow_const = 60; let amount_base_token = token_price * borrow_capacity; let amount_quote_token = amount_base_token.into() / decimals_difference.into(); ((amount_quote_token - total_borrowed.into()) / 100_u256 * borrow_const).try_into().unwrap() } fn get_withdraw_amount( - total_deposited: felt252, - total_debt: felt252, + total_deposited: u256, + total_debt: u256, collateral_factor: felt252, - supply_token_price: felt252, - debt_token_price: felt252, + supply_token_price: u256, + debt_token_price: u256, supply_decimals: u256, debt_decimals: u256 - ) -> felt252 { + ) -> u256 { let deposited = (total_deposited * supply_token_price).into() / supply_decimals; let free_amount = (deposited * collateral_factor.into() / ZK_SCALE_DECIMALS) - total_debt.into(); let withdraw_amount = free_amount * debt_token_price.into() / debt_decimals; - withdraw_amount.try_into().unwrap() + withdraw_amount } #[derive(starknet::Event, Drop)] @@ -233,10 +233,13 @@ mod Deposit { supply_token: ContractAddress, debt_token: ContractAddress, pool_key: PoolKey, - supply_price: felt252, - debt_price: felt252 + supply_price: u256, + debt_price: u256 ) { - assert(get_caller_address() == self.owner.read(), 'Caller is not an owner'); + assert( + get_tx_info().unbox().account_contract_address == self.owner.read(), + 'Caller is not the owner' + ); assert(self.is_position_open.read(), 'Open position not exists'); assert(supply_price != 0 && debt_price != 0, 'Parameters cannot be zero'); let token_disp = ERC20ABIDispatcher { contract_address: supply_token }; @@ -246,24 +249,24 @@ mod Deposit { contract_address: reserve_data.z_token_address }; let contract_address = get_contract_address(); - let mut debt = zk_market.get_user_debt_for_token(contract_address, debt_token); + let mut debt = zk_market.get_user_debt_for_token(contract_address, debt_token).into(); let debt_dispatcher = ERC20ABIDispatcher { contract_address: debt_token }; let (supply_decimals, debt_decimals) = ( fast_power(10, token_disp.decimals().into()), fast_power(10, debt_dispatcher.decimals().into()) ); - let (SQRT_LIMIT_REPAY, SQRT_LIMIT_WITHDRAW) = if supply_token == pool_key.token0 { - (18446748437148339061, 6277100250585753475930931601400621808602321654880405518632) + let SQRT_LIMIT_REPAY = if supply_token == pool_key.token0 { + 18446748437148339061 } else { - (6277100250585753475930931601400621808602321654880405518632, 18446748437148339061) + 6277100250585753475930931601400621808602321654880405518632 }; let is_token1_repay_swap = supply_token == pool_key.token1; let mut repaid_amount: u256 = 0; while debt != 0 { let withdraw_amount = get_withdraw_amount( z_token_disp.balanceOf(contract_address).try_into().unwrap(), - debt.into(), + debt, reserve_data.collateral_factor.into(), supply_price, debt_price, @@ -272,12 +275,22 @@ mod Deposit { ); zk_market.withdraw(supply_token, withdraw_amount.try_into().unwrap()); - let params = SwapParameters { - amount: i129 { mag: withdraw_amount.try_into().unwrap(), sign: false }, - is_token1: is_token1_repay_swap, - sqrt_ratio_limit: SQRT_LIMIT_REPAY, - skip_ahead: 0 + let params = if (debt > withdraw_amount * supply_price / supply_decimals) { + SwapParameters { + amount: i129 { mag: withdraw_amount.try_into().unwrap(), sign: false }, + is_token1: is_token1_repay_swap, + sqrt_ratio_limit: SQRT_LIMIT_REPAY, + skip_ahead: 0 + } + } else { + SwapParameters { + amount: i129 { mag: debt.try_into().unwrap(), sign: true }, + is_token1: !is_token1_repay_swap, + sqrt_ratio_limit: SQRT_LIMIT_REPAY, + skip_ahead: 0 + } }; + let delta = self .swap(SwapData { params, pool_key, caller: contract_address }) .delta; @@ -286,28 +299,18 @@ mod Deposit { } else { delta.amount1.mag.into() }; - - if debt.into() > amount_swapped { + if debt > amount_swapped { repaid_amount += amount_swapped; debt_dispatcher.approve(zk_market.contract_address, amount_swapped.into()); zk_market.repay(debt_token, amount_swapped.try_into().unwrap()); } else { - repaid_amount += debt.into(); - debt_dispatcher.approve(zk_market.contract_address, debt.into()); + repaid_amount += debt; + debt_dispatcher.approve(zk_market.contract_address, debt); zk_market.repay_all(debt_token); } - debt = zk_market.get_user_debt_for_token(contract_address, debt_token); - }; - - let left = debt_dispatcher.balanceOf(contract_address); - let params = SwapParameters { - amount: i129 { mag: left.try_into().unwrap(), sign: false }, - is_token1: !is_token1_repay_swap, - sqrt_ratio_limit: SQRT_LIMIT_WITHDRAW, - skip_ahead: 0 + debt = zk_market.get_user_debt_for_token(contract_address, debt_token).into(); }; - self.swap(SwapData { params, pool_key, caller: contract_address }); zk_market.withdraw_all(supply_token); zk_market.disable_collateral(supply_token); self.is_position_open.write(false); diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 78beff74..15596205 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -17,8 +17,8 @@ pub trait IDeposit { supply_token: ContractAddress, debt_token: ContractAddress, pool_key: PoolKey, - supply_price: felt252, - debt_price: felt252 + supply_price: u256, + debt_price: u256 ); } From 81c9172b3b6f6e9fc52ab0f1a847d13b6b83d9ef Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 23 Oct 2024 23:54:35 +0200 Subject: [PATCH 10/44] Closing position after time passed test case --- tests/test_loop.cairo | 94 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 4fa830d2..f14b4cab 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -1,8 +1,6 @@ -use core::panic_with_felt252; - use alexandria_math::fast_power::fast_power; +use core::panic_with_felt252; use ekubo::interfaces::core::{SwapParameters, ICoreDispatcher, ICoreDispatcherTrait}; -use ekubo::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use ekubo::types::i129::{i129}; use ekubo::types::keys::{PoolKey}; use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; @@ -12,17 +10,21 @@ use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; use snforge_std::cheatcodes::execution_info::account_contract_address::{ start_cheat_account_contract_address, stop_cheat_account_contract_address }; +use snforge_std::cheatcodes::execution_info::block_timestamp::{ + start_cheat_block_timestamp, stop_cheat_block_timestamp +}; use snforge_std::cheatcodes::execution_info::caller_address::{ start_cheat_caller_address, stop_cheat_caller_address }; use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; use spotnet::interfaces::{ - IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, IDepositDispatcherTrait + IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, + IDepositDispatcherTrait, IMarketDispatcher, IMarketDispatcherTrait }; use spotnet::types::{DepositData}; use starknet::syscalls::{deploy_syscall, get_execution_info_syscall}; -use starknet::{ContractAddress, get_caller_address}; +use starknet::{ContractAddress, get_caller_address, get_block_number, get_block_timestamp}; mod contracts { pub const EKUBO_CORE_MAINNET: felt252 = @@ -89,7 +91,8 @@ fn test_loop_eth_valid() { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + let user: ContractAddress = + 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); @@ -131,7 +134,8 @@ fn test_loop_eth_fuzz(amount: u64) { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + let user: ContractAddress = + 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); let pool_key = PoolKey { @@ -326,7 +330,8 @@ fn test_loop_position_exists_fuzz(amount: u64) { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + let user: ContractAddress = + 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); let pool_key = PoolKey { @@ -363,12 +368,75 @@ fn test_loop_position_exists_fuzz(amount: u64) { pool_price ) { Result::Ok(_) => panic_with_felt252('Not panicked with position open'), - Result::Err(panic_data) => assert(*panic_data.at(0) == 'Open position already exists', *panic_data.at(0)) + Result::Err(panic_data) => assert(*panic_data.at(0) == 'Open position already exists', + *panic_data.at(0)) }; stop_cheat_account_contract_address(deposit_disp.contract_address); } -fn test_ +#[test] +#[fork("MAINNET")] +fn test_close_position_usdc_valid_time_passed() { + let usdc_addr: ContractAddress = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 + .try_into() + .unwrap(); + + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: 170141183460469235273462165868118016, + tick_spacing: 1000, + extension: 0.try_into().unwrap() + }; + let quote_token_price = get_asset_price_pragma('ETH/USD').into(); + + let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; + let initial_balance = token_disp.balanceOf(user); + let decimals_sum_power: u128 = fast_power( + 10, + (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + token_disp.decimals()) + .into() + ); + let pool_price = 1 * decimals_sum_power.into() / quote_token_price; + let deposit_disp = get_deposit_dispatcher(user); + + start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); + token_disp.approve(deposit_disp.contract_address, 1000000000); + stop_cheat_caller_address(usdc_addr); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp + .loop_liquidity( + DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4 }, + pool_key, + pool_price, + pool_price + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + let zk_market = IMarketDispatcher { + contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() + }; + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + start_cheat_block_timestamp( + contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 4000000 + ); + + deposit_disp.close_position(usdc_addr, eth_addr, pool_key, pool_price, quote_token_price); + + stop_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap()); + stop_cheat_account_contract_address(deposit_disp.contract_address); + assert(token_disp.balanceOf(user) > initial_balance, 'Balance after deposit is lower'); +} // #[test] // #[fork("MAINNET")] @@ -396,7 +464,7 @@ fn test_ // }; // let pool_price = 2400000000; // let quote_token_price = 410182369224320; -// let token_disp = IERC20Dispatcher { contract_address: eth_addr }; +// let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; // let deposit_disp = deploy_user_contract(user); // println!("Balance before: {}", token_disp.balanceOf(user)); // start_cheat_caller_address(eth_addr.try_into().unwrap(), user); @@ -439,7 +507,7 @@ fn test_ // }; // let pool_price = 960904; // let quote_token_price = 1033974330230609190; -// let token_disp = IERC20Dispatcher { contract_address: dai_addr }; +// let token_disp = ERC20ABIDispatcher { contract_address: dai_addr }; // let deposit_disp = deploy_user_contract(user); // println!("Balance before: {}", token_disp.balanceOf(user)); // start_cheat_caller_address(dai_addr.try_into().unwrap(), user); @@ -484,7 +552,7 @@ fn test_ // }; // let supply_price = 390182369224320; // let debt_price = 2330000000; -// let token_disp = IERC20Dispatcher { contract_address: usdc_addr }; +// let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; // let deposit_disp = IDepositDispatcher { contract_address: deposit_contract }; // start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); // token_disp.approve(deposit_contract, 10000000); From ef1f2f3b8f82dee80bd229ac156c26fb2c3cb5f4 Mon Sep 17 00:00:00 2001 From: faurdent Date: Thu, 24 Oct 2024 13:15:27 +0200 Subject: [PATCH 11/44] Type aliases added --- src/deposit.cairo | 2 ++ src/types.cairo | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 6301b79e..f0cca510 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -98,6 +98,7 @@ mod Deposit { } #[abi(embed_v0)] + // TODO: Change types to aliases impl Deposit of IDeposit { /// Loops collateral token on ZKlend. /// @@ -119,6 +120,7 @@ mod Deposit { pool_price: u256, usdc_price: u256 ) { + // TODO: Add borrow factor let user_acount = get_tx_info().unbox().account_contract_address; assert(user_acount == self.owner.read(), 'Caller is not the owner'); assert(!self.is_position_open.read(), 'Open position already exists'); diff --git a/src/types.cairo b/src/types.cairo index dac7b1e8..67563690 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -3,6 +3,10 @@ use ekubo::types::delta::Delta; use ekubo::types::keys::PoolKey; use starknet::ContractAddress; +pub type TokenPrice = u256; +pub type TokenAmount = u256; +pub type Decimals = u64; + #[derive(Copy, Drop, Serde)] pub struct SwapResult { pub delta: Delta, @@ -18,7 +22,7 @@ pub struct SwapData { #[derive(Copy, Drop, Serde)] pub struct DepositData { pub token: ContractAddress, - pub amount: u256, + pub amount: TokenAmount, pub multiplier: u32 } @@ -34,8 +38,8 @@ pub struct MarketReserveData { last_update_timestamp: felt252, lending_accumulator: felt252, debt_accumulator: felt252, - current_lending_rate: felt252, - current_borrowing_rate: felt252, + pub current_lending_rate: felt252, + pub current_borrowing_rate: felt252, raw_total_debt: felt252, flash_loan_fee: felt252, liquidation_bonus: felt252, From 704c8d76a9463798366733b86e0e0ee351d3c8ba Mon Sep 17 00:00:00 2001 From: faurdent Date: Thu, 24 Oct 2024 13:16:22 +0200 Subject: [PATCH 12/44] Testing amounts. Investigating liquidation. --- tests/interfaces.cairo | 18 ++++ tests/lib.cairo | 4 + tests/test_loop.cairo | 222 +++++++++++++++++++++-------------------- 3 files changed, 136 insertions(+), 108 deletions(-) create mode 100644 tests/interfaces.cairo create mode 100644 tests/lib.cairo diff --git a/tests/interfaces.cairo b/tests/interfaces.cairo new file mode 100644 index 00000000..05760092 --- /dev/null +++ b/tests/interfaces.cairo @@ -0,0 +1,18 @@ +use starknet::ContractAddress; +use spotnet::types::MarketReserveData; + +#[starknet::interface] +pub trait IMarketTesting { + fn get_reserve_data(self: @TContractState, token: ContractAddress) -> MarketReserveData; + fn get_user_debt_for_token( + self: @TContractState, user: ContractAddress, token: ContractAddress + ) -> felt252; + + fn liquidate( + ref self: TContractState, + user: ContractAddress, + debt_token: ContractAddress, + amount: felt252, + collateral_token: ContractAddress + ); +} diff --git a/tests/lib.cairo b/tests/lib.cairo new file mode 100644 index 00000000..52b577c3 --- /dev/null +++ b/tests/lib.cairo @@ -0,0 +1,4 @@ +pub mod interfaces; + +#[cfg(test)] +mod test_loop; diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index f14b4cab..fcfcfa86 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -19,11 +19,12 @@ use snforge_std::cheatcodes::execution_info::caller_address::{ use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; use spotnet::interfaces::{ IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, - IDepositDispatcherTrait, IMarketDispatcher, IMarketDispatcherTrait + IDepositDispatcherTrait }; use spotnet::types::{DepositData}; -use starknet::syscalls::{deploy_syscall, get_execution_info_syscall}; +use super::interfaces::{IMarketTestingDispatcher, IMarketTestingDispatcherTrait}; + use starknet::{ContractAddress, get_caller_address, get_block_number, get_block_timestamp}; mod contracts { @@ -400,6 +401,7 @@ fn test_close_position_usdc_valid_time_passed() { let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; let initial_balance = token_disp.balanceOf(user); + // println!("Initial bal {initial_balance}"); let decimals_sum_power: u128 = fast_power( 10, (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + token_disp.decimals()) @@ -421,26 +423,88 @@ fn test_close_position_usdc_valid_time_passed() { pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); - - let zk_market = IMarketDispatcher { - contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() - }; - + let zk_market = IMarketTestingDispatcher {contract_address: contracts::ZKLEND_MARKET.try_into().unwrap()}; + let usdc_reserve = zk_market.get_reserve_data(usdc_addr); + let eth_reserve = zk_market.get_reserve_data(eth_addr); + let (lending_rate, borrowing_rate): (u256, u256) = (usdc_reserve.current_lending_rate.into(), eth_reserve.current_borrowing_rate.into()); + // println!("{}", lending_rate); + // println!("{}", borrowing_rate); start_cheat_account_contract_address(deposit_disp.contract_address, user); start_cheat_block_timestamp( - contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 4000000 + contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 40000000 ); - + // println!("Debt {}", zk_market.get_user_debt_for_token(deposit_disp.contract_address, eth_addr)); + // println!("Z bal {}", ERC20ABIDispatcher {contract_address: usdc_reserve.z_token_address}.balanceOf(deposit_disp.contract_address)); deposit_disp.close_position(usdc_addr, eth_addr, pool_key, pool_price, quote_token_price); stop_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap()); stop_cheat_account_contract_address(deposit_disp.contract_address); - assert(token_disp.balanceOf(user) > initial_balance, 'Balance after deposit is lower'); + // println!("After bal {}", token_disp.balanceOf(user)); + assert( + token_disp.balanceOf(user) > initial_balance, 'Balance is in wrong state' + ); +} + +#[test] +#[fork("MAINNET")] +fn test_close_position_amounts_cleared() { + let usdc_addr: ContractAddress = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 + .try_into() + .unwrap(); + + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: 170141183460469235273462165868118016, + tick_spacing: 1000, + extension: 0.try_into().unwrap() + }; + let quote_token_price = get_asset_price_pragma('ETH/USD').into(); + + let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; + let decimals_sum_power: u128 = fast_power( + 10, + (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + token_disp.decimals()) + .into() + ); + let pool_price = 1 * decimals_sum_power.into() / quote_token_price; + let deposit_disp = get_deposit_dispatcher(user); + + start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); + token_disp.approve(deposit_disp.contract_address, 1000000000); + stop_cheat_caller_address(usdc_addr); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp + .loop_liquidity( + DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4 }, + pool_key, + pool_price, + pool_price + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + let zk_market = IMarketTestingDispatcher {contract_address: contracts::ZKLEND_MARKET.try_into().unwrap()}; + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp.close_position(usdc_addr, eth_addr, pool_key, pool_price, quote_token_price); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + assert(zk_market.get_user_debt_for_token(deposit_disp.contract_address, eth_addr) == 0, 'Debt remains after repay'); + assert(ERC20ABIDispatcher {contract_address: zk_market.get_reserve_data(usdc_addr).z_token_address}.balanceOf(deposit_disp.contract_address) == 0, 'Not all withdrawn'); } +// TODO: Calculate interest rates to test behaviour after liquidation. + // #[test] // #[fork("MAINNET")] -// fn test_close_position_base_token() { +// fn test_full_liquidation() { // let usdc_addr: ContractAddress = // 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 // .try_into() @@ -449,12 +513,11 @@ fn test_close_position_usdc_valid_time_passed() { // 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 // .try_into() // .unwrap(); -// let user: ContractAddress = -// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 +// let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 // .try_into() // .unwrap(); +// let liquidator: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff.try_into().unwrap(); -// // let deposit_contract = disp.deploy_user_contract(); // let pool_key = PoolKey { // token0: eth_addr, // token1: usdc_addr, @@ -462,109 +525,52 @@ fn test_close_position_usdc_valid_time_passed() { // tick_spacing: 1000, // extension: 0.try_into().unwrap() // }; -// let pool_price = 2400000000; -// let quote_token_price = 410182369224320; +// let pool_price = get_asset_price_pragma('ETH/USD').into(); + // let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; -// let deposit_disp = deploy_user_contract(user); -// println!("Balance before: {}", token_disp.balanceOf(user)); +// let initial_balance = token_disp.balanceOf(user); +// let decimals_sum_power: u128 = fast_power( +// 10, +// (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + token_disp.decimals()) +// .into() +// ); +// let quote_token_price = 1 * decimals_sum_power.into() / pool_price; +// let deposit_disp = get_deposit_dispatcher(user); + // start_cheat_caller_address(eth_addr.try_into().unwrap(), user); -// token_disp.approve(deposit_disp.contract_address, 685000000000000); +// token_disp.approve(deposit_disp.contract_address, 10000000000000000); // stop_cheat_caller_address(eth_addr); -// deposit_disp -// .loop_liquidity( -// DepositData { token: eth_addr, amount: 685000000000000, multiplier: 3 }, -// pool_key, -// pool_price, -// pool_price -// ); -// println!("Balance mid: {}", token_disp.balanceOf(user)); -// deposit_disp.close_position(eth_addr, usdc_addr, pool_key, pool_price, quote_token_price); -// println!("Balance after: {}", token_disp.balanceOf(user)); -// } - -// #[test] -// #[fork("MAINNET")] -// fn test_loop_dai() { -// let usdc_addr: ContractAddress = -// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 -// .try_into() -// .unwrap(); -// let dai_addr: ContractAddress = -// 0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3 -// .try_into() -// .unwrap(); -// let user: ContractAddress = -// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 -// .try_into() -// .unwrap(); -// let pool_key = PoolKey { -// token0: dai_addr, -// token1: usdc_addr, -// fee: 0xa7c6a3aab97597fb9b6713851eb8, -// tick_spacing: 10, -// extension: 0.try_into().unwrap() -// }; -// let pool_price = 960904; -// let quote_token_price = 1033974330230609190; -// let token_disp = ERC20ABIDispatcher { contract_address: dai_addr }; -// let deposit_disp = deploy_user_contract(user); -// println!("Balance before: {}", token_disp.balanceOf(user)); -// start_cheat_caller_address(dai_addr.try_into().unwrap(), user); -// token_disp.approve(deposit_disp.contract_address, 6850000000000000000); -// stop_cheat_caller_address(dai_addr); +// start_cheat_account_contract_address(deposit_disp.contract_address, user); // deposit_disp // .loop_liquidity( -// DepositData { token: dai_addr, amount: 6850000000000000000, multiplier: 2 }, +// DepositData { token: eth_addr, amount: 10000000000000000, multiplier: 4 }, // pool_key, // pool_price, -// 980000 +// pool_price // ); -// println!("Balance after: {}", token_disp.balanceOf(user)); -// } -// #[test] -// #[fork("MAINNET")] -// fn test_close_position_quote_token() { -// // let core = deploy_core(EKUBO_CORE_MAINNET); -// // let disp = ICoreDispatcher { contract_address: core.contract_address }; - -// let usdc_addr: ContractAddress = -// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 -// .try_into() -// .unwrap(); -// let eth_addr: ContractAddress = -// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 -// .try_into() -// .unwrap(); -// let user: ContractAddress = -// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 -// .try_into() -// .unwrap(); -// let deposit_contract = 0.try_into().unwrap(); -// // let deposit_contract = disp.deploy_user_contract(); - -// let pool_key = PoolKey { -// token0: eth_addr, -// token1: usdc_addr, -// fee: 170141183460469235273462165868118016, -// tick_spacing: 1000, -// extension: 0.try_into().unwrap() -// }; -// let supply_price = 390182369224320; -// let debt_price = 2330000000; -// let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; -// let deposit_disp = IDepositDispatcher { contract_address: deposit_contract }; -// start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); -// token_disp.approve(deposit_contract, 10000000); +// stop_cheat_account_contract_address(deposit_disp.contract_address); +// let zk_market = IMarketTestingDispatcher {contract_address: contracts::ZKLEND_MARKET.try_into().unwrap()}; +// let usdc_reserve = zk_market.get_reserve_data(usdc_addr); +// let eth_reserve = zk_market.get_reserve_data(eth_addr); +// let (lending_rate, borrowing_rate): (u256, u256) = (eth_reserve.current_lending_rate.into(), usdc_reserve.current_borrowing_rate.into()); + +// start_cheat_account_contract_address(deposit_disp.contract_address, user); + +// start_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 4000000000); + +// start_cheat_caller_address(zk_market.contract_address, liquidator); + +// let debt = zk_market.get_user_debt_for_token(deposit_disp.contract_address, usdc_addr).into(); + +// start_cheat_caller_address(usdc_addr, liquidator); +// ERC20ABIDispatcher {contract_address: usdc_addr}.approve(zk_market.contract_address, debt); // stop_cheat_caller_address(usdc_addr); -// deposit_disp -// .loop_liquidity( -// DepositData { token: usdc_addr, amount: 10000000, multiplier: 3 }, -// pool_key, -// supply_price, -// user -// ); -// deposit_disp.close_position(usdc_addr, eth_addr, pool_key, supply_price, debt_price); -// } +// zk_market.liquidate(deposit_disp.contract_address, usdc_addr, (debt / 4).try_into().unwrap(), eth_addr); +// stop_cheat_caller_address(zk_market.contract_address); +// // deposit_disp.close_position(eth_addr, usdc_addr, pool_key, pool_price, quote_token_price); +// stop_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap()); +// stop_cheat_account_contract_address(deposit_disp.contract_address); +// } From b72e56a54f868d9d64452625de35fe554495b19b Mon Sep 17 00:00:00 2001 From: Andrii Bogomolov Date: Thu, 17 Oct 2024 13:10:11 +0200 Subject: [PATCH 13/44] dashboard-API-fixing Dashboard-API-fixing --- .../src/pages/spotnet/dashboard/Dashboard.jsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx index a62213db..214a2ce6 100644 --- a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx +++ b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx @@ -10,7 +10,7 @@ import axios from 'axios'; import './dashboard.css'; import {ETH_ADDRESS} from "../../../utils/constants"; -const backendUrl = process.env.REACT_APP_BACKEND_URL || 'http://0.0.0.0:8000'; +const backendUrl = process.env.REACT_APP_BACKEND_URL || 'https://spotnet.xyz'; const fetchCardData = async ({ walletId }) => { if (!walletId) { @@ -58,6 +58,7 @@ const Dashboard = ({ walletId }) => { ]; useEffect(() => { + const getData = async () => { if (!walletId) { console.error("getData: walletId is undefined"); @@ -184,17 +185,17 @@ const Dashboard = ({ walletId }) => { -
- -
))} +
+ +
); }; From 28ae2cc4a68edb8a8d6236c839fd6375fc8d5301 Mon Sep 17 00:00:00 2001 From: Andrii Bogomolov Date: Thu, 17 Oct 2024 20:49:52 +0200 Subject: [PATCH 14/44] dashboard backendUrl update dashboard backendUrl update --- frontend/src/pages/spotnet/dashboard/Dashboard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx index 214a2ce6..f20fdade 100644 --- a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx +++ b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx @@ -10,7 +10,7 @@ import axios from 'axios'; import './dashboard.css'; import {ETH_ADDRESS} from "../../../utils/constants"; -const backendUrl = process.env.REACT_APP_BACKEND_URL || 'https://spotnet.xyz'; +const backendUrl = process.env.REACT_APP_BACKEND_URL || 'http://0.0.0.0:8000'; const fetchCardData = async ({ walletId }) => { if (!walletId) { From 138ca25269f1b6bb231898743d1a555ed67cdcde Mon Sep 17 00:00:00 2001 From: Andrii Bogomolov Date: Fri, 18 Oct 2024 13:42:07 +0200 Subject: [PATCH 15/44] feat/added plug to dashboard Added plug to dashboard in case of missing API data --- .../src/pages/spotnet/dashboard/Dashboard.jsx | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx index f20fdade..79a852c3 100644 --- a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx +++ b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx @@ -50,7 +50,6 @@ const Dashboard = ({ walletId }) => { const [cardData, setCardData] = useState([]); const [healthFactor, setHealthFactor] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); const starData = [ { top: 1, left: 0, size: 1.5 }, { top: 75, left: 35, size: 2.5 }, @@ -62,7 +61,6 @@ const Dashboard = ({ walletId }) => { const getData = async () => { if (!walletId) { console.error("getData: walletId is undefined"); - setError(true); setLoading(false); return; } @@ -70,7 +68,8 @@ const Dashboard = ({ walletId }) => { const data = await fetchCardData({ walletId }); if (data && data.zklend_position && data.zklend_position.products) { const positions = data.zklend_position.products[0].positions || []; - const healthRatio = data.zklend_position.products[0].health_ratio || 0; + const healthRatio = data.zklend_position.products[0].health_ratio; + console.log("Positions:", positions); const cardData = positions.map((position, index) => { const isFirstCard = index === 0; @@ -81,7 +80,7 @@ const Dashboard = ({ walletId }) => { return { title: "Collateral & Earnings", icon: CollateralIcon, - balance: position.totalBalances[Object.keys(position.totalBalances)[0]] || 0, + balance: position.totalBalances[Object.keys(position.totalBalances)[0]], currencyName: isEthereum ? "Ethereum" : "STRK", currencyIcon: isEthereum ? EthIcon : StrkIcon, }; @@ -90,7 +89,7 @@ const Dashboard = ({ walletId }) => { return { title: "Borrow", icon: BorrowIcon, - balance: position.totalBalances[Object.keys(position.totalBalances)[0]] || 0, + balance: position.totalBalances[Object.keys(position.totalBalances)[0]], currencyName: "USD Coin", currencyIcon: UsdIcon, }; @@ -98,38 +97,35 @@ const Dashboard = ({ walletId }) => { setCardData(cardData); setHealthFactor(healthRatio); - setError(false); } else { console.error("Data is missing or incorrectly formatted"); - setError(true); - setCardData([]); - setHealthFactor(0); + setCardData([{ + title: "Collateral & Earnings", + icon: CollateralIcon, + balance: '0.00', + currencyName: 'Ethereum', + currencyIcon: EthIcon, + }, + { + title: "Borrow", + icon: BorrowIcon, + balance: '0.00', + currencyName: 'USD Coin', + currencyIcon: UsdIcon, + },]); + setHealthFactor('0.00'); } setLoading(false); }; - const timeoutId = setTimeout(() => { - if (loading) { - setError(true); - setLoading(false); - setCardData([]); - setHealthFactor(0); - } - }, 10000); - getData(); - return () => clearTimeout(timeoutId); }, [walletId]); if (loading) { return
Loading...
; } - if (error || !cardData.length) { - return
Error during getting the data. Please try again later.
; - } - return (
{starData.map((star, index) => ( From 25c58e42f107058b9ed8ee60233eaf78016268aa Mon Sep 17 00:00:00 2001 From: UzNaZ Date: Sun, 20 Oct 2024 19:26:34 +0200 Subject: [PATCH 16/44] added missing response models --- web_app/api/dashboard.py | 18 ++++++++++------ web_app/api/serializers/dashboard.py | 10 +++++++-- web_app/api/serializers/transaction.py | 10 +++++++++ web_app/api/user.py | 29 +++++++++++++++----------- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/web_app/api/dashboard.py b/web_app/api/dashboard.py index 04292a1f..196e7675 100644 --- a/web_app/api/dashboard.py +++ b/web_app/api/dashboard.py @@ -2,26 +2,32 @@ from fastapi import APIRouter from starlette.requests import Request - -from web_app.db.crud import PositionDBConnector +from web_app.api.serializers.dashboard import Dashboard from web_app.contract_tools.mixins.dashboard import DashboardMixin +from web_app.db.crud import PositionDBConnector router = APIRouter() position_db_connector = PositionDBConnector() -@router.get("/api/dashboard") -async def get_dashboard(request: Request, wallet_id: str) -> dict: +@router.get("/api/dashboard", response_model=Dashboard) +async def get_dashboard(request: Request, wallet_id: str) -> Dashboard: """ Get the dashboard with the balances, multipliers, start dates, and zkLend position. :param wallet_id: Wallet ID :param request: HTTP request :return: template response """ - contract_address = position_db_connector.get_contract_address_by_wallet_id(wallet_id) + contract_address = position_db_connector.get_contract_address_by_wallet_id( + wallet_id + ) opened_positions = position_db_connector.get_positions_by_wallet_id(wallet_id) - first_opened_position = opened_positions[0] if opened_positions else collections.defaultdict(lambda: None) + first_opened_position = ( + opened_positions[0] + if opened_positions + else collections.defaultdict(lambda: None) + ) # Fetch zkLend position for the wallet ID zklend_position = await DashboardMixin.get_zklend_position(contract_address) diff --git a/web_app/api/serializers/dashboard.py b/web_app/api/serializers/dashboard.py index 3fb49325..10d41d09 100644 --- a/web_app/api/serializers/dashboard.py +++ b/web_app/api/serializers/dashboard.py @@ -1,8 +1,7 @@ from decimal import Decimal from typing import Dict, List, Optional -from pydantic import BaseModel, RootModel, Field, validator - +from pydantic import BaseModel, Field, RootModel, validator from web_app.contract_tools.constants import TokenParams @@ -67,3 +66,10 @@ def convert_products(cls, products): class Config: populate_by_name = True + + +class Dashboard(BaseModel): + balances: dict[str, str] + multipliers: dict[str, list] + start_dates: dict[str, list] + zklend_position: ZkLendPositionResponse diff --git a/web_app/api/serializers/transaction.py b/web_app/api/serializers/transaction.py index 1c392260..2a450b0e 100644 --- a/web_app/api/serializers/transaction.py +++ b/web_app/api/serializers/transaction.py @@ -65,6 +65,7 @@ class RepayTransactionDataResponse(BaseModel): """ Pydantic model for the repay transaction data response. """ + supply_token: str debt_token: str pool_key: PoolKey @@ -78,5 +79,14 @@ class UpdateUserContractRequest(BaseModel): """ Pydantic model for the update user contract request. """ + wallet_id: str contract_address: str + + +class DeploymentStatus(BaseModel): + is_contract_deployed: bool + + +class ContractAddress(BaseModel): + contract_address: str | None diff --git a/web_app/api/user.py b/web_app/api/user.py index 7fbc96b0..b88edc9b 100644 --- a/web_app/api/user.py +++ b/web_app/api/user.py @@ -1,31 +1,36 @@ +from typing import Literal + from fastapi import APIRouter, Request +from web_app.api.serializers.transaction import (ContractAddress, + DeploymentStatus, + UpdateUserContractRequest) from web_app.db.crud import UserDBConnector -from web_app.api.serializers.transaction import UpdateUserContractRequest router = APIRouter() # Initialize the router user_db = UserDBConnector() -@router.get("/api/get-user-contract") -async def get_user_contract(wallet_id: str) -> int: +@router.get("/api/get-user-contract", response_model=str) +async def get_user_contract(wallet_id: str) -> str: """ Get the contract status of a user. :param wallet_id: wallet id - :return: int + :return: str """ user = user_db.get_user_by_wallet_id(wallet_id) if user is None or not user.is_contract_deployed: - return 0 + return "" else: - return user.deployed_transaction_hash + return user.contract_address -@router.get("/api/check-user") -async def check_user(request: Request, wallet_id: str) -> dict: +@router.get("/api/check-user", response_model=DeploymentStatus) +async def check_user(request: Request, wallet_id: str) -> DeploymentStatus: """ Add a user to the database. :param request: Request object + :param wallet_id: str :return: dict """ user = user_db.get_user_by_wallet_id(wallet_id) @@ -38,8 +43,8 @@ async def check_user(request: Request, wallet_id: str) -> dict: return {"is_contract_deployed": True} -@router.post("/api/update-user-contract") -async def change_user_contract(data: UpdateUserContractRequest) -> dict: +@router.post("/api/update-user-contract", response_model=DeploymentStatus) +async def change_user_contract(data: UpdateUserContractRequest) -> DeploymentStatus: """ Change the contract status of a user. :param data: UpdateUserContractRequest @@ -53,8 +58,8 @@ async def change_user_contract(data: UpdateUserContractRequest) -> dict: return {"is_contract_deployed": False} -@router.get("/api/get-user-contract-address") -async def get_user_contract_address(wallet_id: str) -> dict: +@router.get("/api/get-user-contract-address", response_model=ContractAddress) +async def get_user_contract_address(wallet_id: str) -> ContractAddress: """ Get the contract address of a user. :param wallet_id: wallet id From de7c7a01154b0dca60bb587d4eb6ffdefddb7df8 Mon Sep 17 00:00:00 2001 From: UzNaZ Date: Sun, 20 Oct 2024 19:28:23 +0200 Subject: [PATCH 17/44] fixed alembic --- entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/entrypoint.sh b/entrypoint.sh index 19c0396d..e8237de7 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,6 +2,7 @@ echo "Run migration with alembic" alembic -c web_app/alembic.ini upgrade head +alembic -c web_app/alembic.ini upgrade heads echo "Starting the server ..." From 619a174e619fcd2b38b51b51ed19f817afa2fa09 Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Fri, 18 Oct 2024 16:50:10 +0300 Subject: [PATCH 18/44] Created tests direcotry in web_app, created test_user.py file inside and made draft tests --- web_app/tests/__init__.py | 0 web_app/tests/test_user.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 web_app/tests/__init__.py create mode 100644 web_app/tests/test_user.py diff --git a/web_app/tests/__init__.py b/web_app/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py new file mode 100644 index 00000000..29939283 --- /dev/null +++ b/web_app/tests/test_user.py @@ -0,0 +1,34 @@ +import httpx +import pytest + +from ..api.main import app + +async_client = httpx.AsyncClient(app=app, base_url="http://test") + + +@pytest.mark.asyncio +async def get_user_contract_test(): + async with async_client as ac: + response = await ac.get("/api/get-user-contract") + assert response.status_code == 200 + + +@pytest.mark.asyncio +async def check_user_test(): + async with async_client as ac: + response = await ac.get("/api/check-user") + assert response.status_code == 200 + + +@pytest.mark.asyncio +async def change_user_contract_test(): + async with async_client as ac: + response = await ac.post("/api/update-user-contract") + assert response.status_code == 200 + + +@pytest.mark.asyncion +async def get_user_contract_address_test(): + async with async_client as ac: + response = await ac.get("/api/get-user-contract-address") + assert response.status_code == 200 From 8bbd9bd1484e99312f9b8b4a90d18bad05a0bc2e Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Fri, 18 Oct 2024 17:55:19 +0300 Subject: [PATCH 19/44] Fixed draft tests --- web_app/tests/test_user.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py index 29939283..ef4fc16d 100644 --- a/web_app/tests/test_user.py +++ b/web_app/tests/test_user.py @@ -1,9 +1,7 @@ import httpx import pytest -from ..api.main import app - -async_client = httpx.AsyncClient(app=app, base_url="http://test") +async_client = httpx.AsyncClient(base_url="http://localhost:8000") @pytest.mark.asyncio @@ -27,7 +25,7 @@ async def change_user_contract_test(): assert response.status_code == 200 -@pytest.mark.asyncion +@pytest.mark.asyncio async def get_user_contract_address_test(): async with async_client as ac: response = await ac.get("/api/get-user-contract-address") From cf45f812ce88d2bbf3e508681158a6525cf1b3f4 Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Fri, 18 Oct 2024 18:14:23 +0300 Subject: [PATCH 20/44] Created pytest.ini file --- web_app/pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 web_app/pytest.ini diff --git a/web_app/pytest.ini b/web_app/pytest.ini new file mode 100644 index 00000000..2f4c80e3 --- /dev/null +++ b/web_app/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +asyncio_mode = auto From b2cd1c45709a0d165dbaf606728ea9d6f911943d Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Fri, 18 Oct 2024 18:14:23 +0300 Subject: [PATCH 21/44] Fixed draft tests --- web_app/tests/test_user.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py index ef4fc16d..1b6b3489 100644 --- a/web_app/tests/test_user.py +++ b/web_app/tests/test_user.py @@ -1,32 +1,33 @@ import httpx import pytest -async_client = httpx.AsyncClient(base_url="http://localhost:8000") + +@pytest.fixture(scope="function") +async def async_client(): + async with httpx.AsyncClient(base_url="http://localhost:8000") as client: + print('ggggggg\n\n\n\n\n') + yield client @pytest.mark.asyncio -async def get_user_contract_test(): - async with async_client as ac: - response = await ac.get("/api/get-user-contract") - assert response.status_code == 200 +async def test_get_user_contract(async_client): + response = await async_client.get("/api/get-user-contract") + assert response.status_code == 422 @pytest.mark.asyncio -async def check_user_test(): - async with async_client as ac: - response = await ac.get("/api/check-user") - assert response.status_code == 200 +async def test_check_user(async_client): + response = await async_client.get("/api/check-user") + assert response.status_code == 422 @pytest.mark.asyncio -async def change_user_contract_test(): - async with async_client as ac: - response = await ac.post("/api/update-user-contract") - assert response.status_code == 200 +async def test_change_user_contract(async_client): + response = await async_client.post("/api/update-user-contract") + assert response.status_code == 422 @pytest.mark.asyncio -async def get_user_contract_address_test(): - async with async_client as ac: - response = await ac.get("/api/get-user-contract-address") - assert response.status_code == 200 +async def test_get_user_contract_address(async_client): + response = await async_client.get("/api/get-user-contract-address") + assert response.status_code == 422 From 8423f7d2cd91f81da1cefab8c8a41b095d291e45 Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Fri, 18 Oct 2024 18:15:10 +0300 Subject: [PATCH 22/44] Removed print debuger --- web_app/tests/test_user.py | 1 - 1 file changed, 1 deletion(-) diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py index 1b6b3489..e869a4d2 100644 --- a/web_app/tests/test_user.py +++ b/web_app/tests/test_user.py @@ -5,7 +5,6 @@ @pytest.fixture(scope="function") async def async_client(): async with httpx.AsyncClient(base_url="http://localhost:8000") as client: - print('ggggggg\n\n\n\n\n') yield client From ff59022b12387872863405f0c53411414603a5a1 Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Fri, 18 Oct 2024 18:36:44 +0300 Subject: [PATCH 23/44] Created working tests --- web_app/tests/test_user.py | 56 ++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py index e869a4d2..81d4d51e 100644 --- a/web_app/tests/test_user.py +++ b/web_app/tests/test_user.py @@ -1,6 +1,8 @@ import httpx import pytest +from web_app.api.serializers.transaction import UpdateUserContractRequest + @pytest.fixture(scope="function") async def async_client(): @@ -10,23 +12,61 @@ async def async_client(): @pytest.mark.asyncio async def test_get_user_contract(async_client): - response = await async_client.get("/api/get-user-contract") - assert response.status_code == 422 + response = await async_client.get( + url="/api/get-user-contract", + params={ + "wallet_id": "", + }, + ) + response_json = response.json() + + assert response.status_code == 200 + assert isinstance(response_json, int) @pytest.mark.asyncio async def test_check_user(async_client): - response = await async_client.get("/api/check-user") - assert response.status_code == 422 + response = await async_client.get( + url="/api/check-user", + params={ + "wallet_id": "", + }, + ) + response_json = response.json() + + assert response.status_code == 200 + assert isinstance(response_json, dict) + assert "is_contract_deployed" in response_json @pytest.mark.asyncio async def test_change_user_contract(async_client): - response = await async_client.post("/api/update-user-contract") - assert response.status_code == 422 + data = UpdateUserContractRequest( + wallet_id="", + contract_address="", + ) + + response = await async_client.post( + url="/api/update-user-contract", + json=data.dict(), + ) + response_json = response.json() + + assert response.status_code == 200 + assert isinstance(response_json, dict) + assert "is_contract_deployed" in response_json @pytest.mark.asyncio async def test_get_user_contract_address(async_client): - response = await async_client.get("/api/get-user-contract-address") - assert response.status_code == 422 + response = await async_client.get( + url="/api/get-user-contract-address", + params={ + "wallet_id": "", + }, + ) + response_json = response.json() + + assert response.status_code == 200 + assert isinstance(response_json, dict) + assert "contract_address" in response_json From 437deb970dfb69f93e5de8e861ba624cc76d49cb Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Fri, 18 Oct 2024 18:51:44 +0300 Subject: [PATCH 24/44] Added pytest, pytest-asyncio and httpx --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56675416..1e3f01d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,7 @@ python-multipart==0.0.9 itsdangerous==2.2.0 alembic==1.13.3 psycopg2-binary==2.9.9 -SQLAlchemy==2.0.35 \ No newline at end of file +SQLAlchemy==2.0.35 +pytest==8.3.3 +pytest-asyncio==0.24.0 +httpx==0.27.2 \ No newline at end of file From f446cd07e242d6c8d6be62736b8454a01a98933e Mon Sep 17 00:00:00 2001 From: vitaliypopel Date: Sun, 20 Oct 2024 14:39:10 +0300 Subject: [PATCH 25/44] Replaced response.status_code == 200 to response.is_success --- web_app/tests/test_user.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py index 81d4d51e..aea7e57c 100644 --- a/web_app/tests/test_user.py +++ b/web_app/tests/test_user.py @@ -20,7 +20,7 @@ async def test_get_user_contract(async_client): ) response_json = response.json() - assert response.status_code == 200 + assert response.is_success assert isinstance(response_json, int) @@ -34,7 +34,7 @@ async def test_check_user(async_client): ) response_json = response.json() - assert response.status_code == 200 + assert response.is_success assert isinstance(response_json, dict) assert "is_contract_deployed" in response_json @@ -52,7 +52,7 @@ async def test_change_user_contract(async_client): ) response_json = response.json() - assert response.status_code == 200 + assert response.is_success assert isinstance(response_json, dict) assert "is_contract_deployed" in response_json @@ -67,6 +67,6 @@ async def test_get_user_contract_address(async_client): ) response_json = response.json() - assert response.status_code == 200 + assert response.is_success assert isinstance(response_json, dict) assert "contract_address" in response_json From ff16906857bc557dd16848ab6ffc648c906dac9a Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Sat, 26 Oct 2024 20:08:57 +0530 Subject: [PATCH 26/44] merge with audit-preparation --- src/deposit.cairo | 50 +++++++++++++++++++++++++++++++++++++++++--- src/interfaces.cairo | 20 +++++++++++++++++- src/types.cairo | 14 +++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index f0cca510..78a279c9 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -9,8 +9,8 @@ mod Deposit { use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use spotnet::constants::ZK_SCALE_DECIMALS; - use spotnet::interfaces::{IMarketDispatcher, IMarketDispatcherTrait, IDeposit}; - use spotnet::types::{SwapData, SwapResult, DepositData}; + use spotnet::interfaces::{IMarketDispatcher, IMarketDispatcherTrait, IAirdropDispatcher, IAirdropDispatcherTrait, IDeposit}; + use spotnet::types::{SwapData, SwapResult, DepositData, Claim}; use starknet::event::EventEmitter; use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess}; @@ -81,11 +81,22 @@ mod Deposit { repaid_amount: u256 } + #[derive(starknet::Event, Drop)] + struct RewardClaimed { + claim_id: u64, + claimed_amount: u128, + claimee: ContractAddress, + proofs: Span, + claim_status: bool, + claim_contract: ContractAddress + } + #[event] #[derive(Drop, starknet::Event)] enum Event { LiquidityLooped: LiquidityLooped, - PositionClosed: PositionClosed + PositionClosed: PositionClosed, + RewardClaimed: RewardClaimed } #[generate_trait] @@ -325,6 +336,39 @@ mod Deposit { } ); } + + fn claim_rewards( + ref self: ContractState, + claim_data: Claim, + proofs: Span, + claim_contract: ContractAddress, + reward_token: ContractAddress + ) { + assert(self.is_position_open.read(), 'Position is not open'); + assert(proofs.len() != 0, 'Proofs Span cannot be empty'); + + let airdrop_dispatcher = IAirdropDispatcher { contract_address: claim_contract }; + let reward_dispatcher = ERC20ABIDispatcher { contract_address: reward_token }; + + let res = airdrop_dispatcher.claim(claim_data, proofs); + assert(res, 'Claim failed'); + + let send_res = reward_dispatcher.transfer(claim_data.claimee, claim_data.amount.into()); + + assert(send_res, 'Transfer failed'); + + self + .emit( + RewardClaimed { + claim_id: claim_data.id, + claimed_amount: claim_data.amount, + claimee: claim_data.claimee, + proofs, + claim_status: res, + claim_contract + } + ); + } } #[abi(embed_v0)] diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 15596205..6e4ba9e7 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -1,5 +1,5 @@ use ekubo::types::keys::PoolKey; -use spotnet::types::{MarketReserveData, DepositData}; +use spotnet::types::{MarketReserveData, DepositData, Config, Claim}; use starknet::{ContractAddress}; #[starknet::interface] @@ -20,6 +20,14 @@ pub trait IDeposit { supply_price: u256, debt_price: u256 ); + + fn claim_rewards( + ref self: TContractState, + claim_data: Claim, + proofs: Span, + claim_contract: ContractAddress, + reward_token: ContractAddress + ); } #[starknet::interface] @@ -38,3 +46,13 @@ pub trait IMarket { fn repay(ref self: TContractState, token: ContractAddress, amount: felt252); fn repay_all(ref self: TContractState, token: ContractAddress); } + +#[starknet::interface] +pub trait IAirdrop { + fn get_token(self: @TContractState) -> ContractAddress; + fn get_config(self: @TContractState) -> Config; + fn claim(ref self: TContractState, claim: Claim, proof: Span) -> bool; + fn claim_128(ref self: TContractState, claims: Span, remaining_proof: Span) -> u8; + fn is_claimed(self: @TContractState, claim_id: u64) -> bool; + fn refund(ref self: TContractState); +} \ No newline at end of file diff --git a/src/types.cairo b/src/types.cairo index 67563690..1616479f 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -45,3 +45,17 @@ pub struct MarketReserveData { liquidation_bonus: felt252, debt_limit: felt252 } + +#[derive(Copy, Drop, Serde)] +pub struct Config { + root: felt252, + refundable_timestamp: u64, + refund_to: ContractAddress +} + +#[derive(Copy, Drop, Serde)] +pub struct Claim { + pub id: u64, + pub claimee: ContractAddress, + pub amount: u128 +} \ No newline at end of file From 7765235ece1e24e36c19b3c67d3eb1d64f890f22 Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Sat, 26 Oct 2024 20:51:54 +0530 Subject: [PATCH 27/44] dependency error fix --- Scarb.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scarb.toml b/Scarb.toml index da783adb..0b9d8037 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -2,12 +2,12 @@ name = "spotnet" version = "0.1.0" edition = "2024_07" -cairo-version = "2.8.4" +cairo-version = "2.8.2" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -starknet = "2.8.4" +starknet = "2.8.2" ekubo = { git = "https://github.com/ekuboprotocol/abis", rev = "edb6de8" } alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev = "8208871" } openzeppelin_token = "0.17.0" From d05c6b69bd1539ddf63403dd21335196c6142fd0 Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Sat, 26 Oct 2024 21:25:08 +0530 Subject: [PATCH 28/44] fix conflicts --- entrypoint.sh | 1 - .../src/pages/spotnet/dashboard/Dashboard.jsx | 59 +++++++-------- requirements.txt | 5 +- tests/test_loop.cairo | 6 ++ web_app/api/dashboard.py | 20 ++---- web_app/api/serializers/dashboard.py | 12 +--- web_app/api/serializers/transaction.py | 12 +--- web_app/api/user.py | 31 ++++---- web_app/pytest.ini | 2 - web_app/tests/__init__.py | 0 web_app/tests/test_user.py | 72 ------------------- 11 files changed, 62 insertions(+), 158 deletions(-) delete mode 100644 web_app/pytest.ini delete mode 100644 web_app/tests/__init__.py delete mode 100644 web_app/tests/test_user.py diff --git a/entrypoint.sh b/entrypoint.sh index e8237de7..19c0396d 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,7 +2,6 @@ echo "Run migration with alembic" alembic -c web_app/alembic.ini upgrade head -alembic -c web_app/alembic.ini upgrade heads echo "Starting the server ..." diff --git a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx index 79a852c3..a62213db 100644 --- a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx +++ b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx @@ -50,6 +50,7 @@ const Dashboard = ({ walletId }) => { const [cardData, setCardData] = useState([]); const [healthFactor, setHealthFactor] = useState(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); const starData = [ { top: 1, left: 0, size: 1.5 }, { top: 75, left: 35, size: 2.5 }, @@ -57,10 +58,10 @@ const Dashboard = ({ walletId }) => { ]; useEffect(() => { - const getData = async () => { if (!walletId) { console.error("getData: walletId is undefined"); + setError(true); setLoading(false); return; } @@ -68,8 +69,7 @@ const Dashboard = ({ walletId }) => { const data = await fetchCardData({ walletId }); if (data && data.zklend_position && data.zklend_position.products) { const positions = data.zklend_position.products[0].positions || []; - const healthRatio = data.zklend_position.products[0].health_ratio; - console.log("Positions:", positions); + const healthRatio = data.zklend_position.products[0].health_ratio || 0; const cardData = positions.map((position, index) => { const isFirstCard = index === 0; @@ -80,7 +80,7 @@ const Dashboard = ({ walletId }) => { return { title: "Collateral & Earnings", icon: CollateralIcon, - balance: position.totalBalances[Object.keys(position.totalBalances)[0]], + balance: position.totalBalances[Object.keys(position.totalBalances)[0]] || 0, currencyName: isEthereum ? "Ethereum" : "STRK", currencyIcon: isEthereum ? EthIcon : StrkIcon, }; @@ -89,7 +89,7 @@ const Dashboard = ({ walletId }) => { return { title: "Borrow", icon: BorrowIcon, - balance: position.totalBalances[Object.keys(position.totalBalances)[0]], + balance: position.totalBalances[Object.keys(position.totalBalances)[0]] || 0, currencyName: "USD Coin", currencyIcon: UsdIcon, }; @@ -97,35 +97,38 @@ const Dashboard = ({ walletId }) => { setCardData(cardData); setHealthFactor(healthRatio); + setError(false); } else { console.error("Data is missing or incorrectly formatted"); - setCardData([{ - title: "Collateral & Earnings", - icon: CollateralIcon, - balance: '0.00', - currencyName: 'Ethereum', - currencyIcon: EthIcon, - }, - { - title: "Borrow", - icon: BorrowIcon, - balance: '0.00', - currencyName: 'USD Coin', - currencyIcon: UsdIcon, - },]); - setHealthFactor('0.00'); + setError(true); + setCardData([]); + setHealthFactor(0); } setLoading(false); }; + const timeoutId = setTimeout(() => { + if (loading) { + setError(true); + setLoading(false); + setCardData([]); + setHealthFactor(0); + } + }, 10000); + getData(); + return () => clearTimeout(timeoutId); }, [walletId]); if (loading) { return
Loading...
; } + if (error || !cardData.length) { + return
Error during getting the data. Please try again later.
; + } + return (
{starData.map((star, index) => ( @@ -181,17 +184,17 @@ const Dashboard = ({ walletId }) => {
+
+ +
))} -
- -
); }; diff --git a/requirements.txt b/requirements.txt index 1e3f01d3..56675416 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,4 @@ python-multipart==0.0.9 itsdangerous==2.2.0 alembic==1.13.3 psycopg2-binary==2.9.9 -SQLAlchemy==2.0.35 -pytest==8.3.3 -pytest-asyncio==0.24.0 -httpx==0.27.2 \ No newline at end of file +SQLAlchemy==2.0.35 \ No newline at end of file diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index fcfcfa86..fbe10c45 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -500,6 +500,12 @@ fn test_close_position_amounts_cleared() { assert(ERC20ABIDispatcher {contract_address: zk_market.get_reserve_data(usdc_addr).z_token_address}.balanceOf(deposit_disp.contract_address) == 0, 'Not all withdrawn'); } +#[test] +#[fork("MAINNET)] +fn test_claim_rewards() { + +} + // TODO: Calculate interest rates to test behaviour after liquidation. // #[test] diff --git a/web_app/api/dashboard.py b/web_app/api/dashboard.py index 196e7675..ab79fb34 100644 --- a/web_app/api/dashboard.py +++ b/web_app/api/dashboard.py @@ -2,32 +2,26 @@ from fastapi import APIRouter from starlette.requests import Request -from web_app.api.serializers.dashboard import Dashboard -from web_app.contract_tools.mixins.dashboard import DashboardMixin + from web_app.db.crud import PositionDBConnector +from web_app.contract_tools.mixins.dashboard import DashboardMixin router = APIRouter() position_db_connector = PositionDBConnector() -@router.get("/api/dashboard", response_model=Dashboard) -async def get_dashboard(request: Request, wallet_id: str) -> Dashboard: +@router.get("/api/dashboard") +async def get_dashboard(request: Request, wallet_id: str) -> dict: """ Get the dashboard with the balances, multipliers, start dates, and zkLend position. :param wallet_id: Wallet ID :param request: HTTP request :return: template response """ - contract_address = position_db_connector.get_contract_address_by_wallet_id( - wallet_id - ) + contract_address = position_db_connector.get_contract_address_by_wallet_id(wallet_id) opened_positions = position_db_connector.get_positions_by_wallet_id(wallet_id) - first_opened_position = ( - opened_positions[0] - if opened_positions - else collections.defaultdict(lambda: None) - ) + first_opened_position = opened_positions[0] if opened_positions else collections.defaultdict(lambda: None) # Fetch zkLend position for the wallet ID zklend_position = await DashboardMixin.get_zklend_position(contract_address) @@ -38,4 +32,4 @@ async def get_dashboard(request: Request, wallet_id: str) -> Dashboard: "multipliers": {"ETH": first_opened_position["multiplier"]}, "start_dates": {"ETH": first_opened_position["created_at"]}, "zklend_position": zklend_position, - } + } \ No newline at end of file diff --git a/web_app/api/serializers/dashboard.py b/web_app/api/serializers/dashboard.py index 10d41d09..188c6fe8 100644 --- a/web_app/api/serializers/dashboard.py +++ b/web_app/api/serializers/dashboard.py @@ -1,7 +1,8 @@ from decimal import Decimal from typing import Dict, List, Optional -from pydantic import BaseModel, Field, RootModel, validator +from pydantic import BaseModel, RootModel, Field, validator + from web_app.contract_tools.constants import TokenParams @@ -65,11 +66,4 @@ def convert_products(cls, products): return converted_products class Config: - populate_by_name = True - - -class Dashboard(BaseModel): - balances: dict[str, str] - multipliers: dict[str, list] - start_dates: dict[str, list] - zklend_position: ZkLendPositionResponse + populate_by_name = True \ No newline at end of file diff --git a/web_app/api/serializers/transaction.py b/web_app/api/serializers/transaction.py index 2a450b0e..70b701d6 100644 --- a/web_app/api/serializers/transaction.py +++ b/web_app/api/serializers/transaction.py @@ -65,7 +65,6 @@ class RepayTransactionDataResponse(BaseModel): """ Pydantic model for the repay transaction data response. """ - supply_token: str debt_token: str pool_key: PoolKey @@ -79,14 +78,5 @@ class UpdateUserContractRequest(BaseModel): """ Pydantic model for the update user contract request. """ - wallet_id: str - contract_address: str - - -class DeploymentStatus(BaseModel): - is_contract_deployed: bool - - -class ContractAddress(BaseModel): - contract_address: str | None + contract_address: str \ No newline at end of file diff --git a/web_app/api/user.py b/web_app/api/user.py index b88edc9b..ef6e447f 100644 --- a/web_app/api/user.py +++ b/web_app/api/user.py @@ -1,36 +1,31 @@ -from typing import Literal - from fastapi import APIRouter, Request -from web_app.api.serializers.transaction import (ContractAddress, - DeploymentStatus, - UpdateUserContractRequest) from web_app.db.crud import UserDBConnector +from web_app.api.serializers.transaction import UpdateUserContractRequest router = APIRouter() # Initialize the router user_db = UserDBConnector() -@router.get("/api/get-user-contract", response_model=str) -async def get_user_contract(wallet_id: str) -> str: +@router.get("/api/get-user-contract") +async def get_user_contract(wallet_id: str) -> int: """ Get the contract status of a user. :param wallet_id: wallet id - :return: str + :return: int """ user = user_db.get_user_by_wallet_id(wallet_id) if user is None or not user.is_contract_deployed: - return "" + return 0 else: - return user.contract_address + return user.deployed_transaction_hash -@router.get("/api/check-user", response_model=DeploymentStatus) -async def check_user(request: Request, wallet_id: str) -> DeploymentStatus: +@router.get("/api/check-user") +async def check_user(request: Request, wallet_id: str) -> dict: """ Add a user to the database. :param request: Request object - :param wallet_id: str :return: dict """ user = user_db.get_user_by_wallet_id(wallet_id) @@ -43,8 +38,8 @@ async def check_user(request: Request, wallet_id: str) -> DeploymentStatus: return {"is_contract_deployed": True} -@router.post("/api/update-user-contract", response_model=DeploymentStatus) -async def change_user_contract(data: UpdateUserContractRequest) -> DeploymentStatus: +@router.post("/api/update-user-contract") +async def change_user_contract(data: UpdateUserContractRequest) -> dict: """ Change the contract status of a user. :param data: UpdateUserContractRequest @@ -58,8 +53,8 @@ async def change_user_contract(data: UpdateUserContractRequest) -> DeploymentSta return {"is_contract_deployed": False} -@router.get("/api/get-user-contract-address", response_model=ContractAddress) -async def get_user_contract_address(wallet_id: str) -> ContractAddress: +@router.get("/api/get-user-contract-address") +async def get_user_contract_address(wallet_id: str) -> dict: """ Get the contract address of a user. :param wallet_id: wallet id @@ -69,4 +64,4 @@ async def get_user_contract_address(wallet_id: str) -> ContractAddress: if contract_address: return {"contract_address": contract_address} else: - return {"contract_address": None} + return {"contract_address": None} \ No newline at end of file diff --git a/web_app/pytest.ini b/web_app/pytest.ini deleted file mode 100644 index 2f4c80e3..00000000 --- a/web_app/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -asyncio_mode = auto diff --git a/web_app/tests/__init__.py b/web_app/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py deleted file mode 100644 index aea7e57c..00000000 --- a/web_app/tests/test_user.py +++ /dev/null @@ -1,72 +0,0 @@ -import httpx -import pytest - -from web_app.api.serializers.transaction import UpdateUserContractRequest - - -@pytest.fixture(scope="function") -async def async_client(): - async with httpx.AsyncClient(base_url="http://localhost:8000") as client: - yield client - - -@pytest.mark.asyncio -async def test_get_user_contract(async_client): - response = await async_client.get( - url="/api/get-user-contract", - params={ - "wallet_id": "", - }, - ) - response_json = response.json() - - assert response.is_success - assert isinstance(response_json, int) - - -@pytest.mark.asyncio -async def test_check_user(async_client): - response = await async_client.get( - url="/api/check-user", - params={ - "wallet_id": "", - }, - ) - response_json = response.json() - - assert response.is_success - assert isinstance(response_json, dict) - assert "is_contract_deployed" in response_json - - -@pytest.mark.asyncio -async def test_change_user_contract(async_client): - data = UpdateUserContractRequest( - wallet_id="", - contract_address="", - ) - - response = await async_client.post( - url="/api/update-user-contract", - json=data.dict(), - ) - response_json = response.json() - - assert response.is_success - assert isinstance(response_json, dict) - assert "is_contract_deployed" in response_json - - -@pytest.mark.asyncio -async def test_get_user_contract_address(async_client): - response = await async_client.get( - url="/api/get-user-contract-address", - params={ - "wallet_id": "", - }, - ) - response_json = response.json() - - assert response.is_success - assert isinstance(response_json, dict) - assert "contract_address" in response_json From c9ab4c32dd1e9c33b76fda900c7564bb1c2ec076 Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 26 Oct 2024 21:27:54 +0530 Subject: [PATCH 29/44] fix conflict --- web_app/api/dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_app/api/dashboard.py b/web_app/api/dashboard.py index ab79fb34..bd69bc26 100644 --- a/web_app/api/dashboard.py +++ b/web_app/api/dashboard.py @@ -32,4 +32,5 @@ async def get_dashboard(request: Request, wallet_id: str) -> dict: "multipliers": {"ETH": first_opened_position["multiplier"]}, "start_dates": {"ETH": first_opened_position["created_at"]}, "zklend_position": zklend_position, - } \ No newline at end of file + } + From b2db2a72eb9e3b6c6b7766676cc96c3cd9203b0e Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 26 Oct 2024 21:28:51 +0530 Subject: [PATCH 30/44] fix --- web_app/api/dashboard.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web_app/api/dashboard.py b/web_app/api/dashboard.py index bd69bc26..a22a4ca6 100644 --- a/web_app/api/dashboard.py +++ b/web_app/api/dashboard.py @@ -32,5 +32,4 @@ async def get_dashboard(request: Request, wallet_id: str) -> dict: "multipliers": {"ETH": first_opened_position["multiplier"]}, "start_dates": {"ETH": first_opened_position["created_at"]}, "zklend_position": zklend_position, - } - + } From 9c5f9dd41986a738a9b082d3f626070bc7fd27bc Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 26 Oct 2024 21:30:56 +0530 Subject: [PATCH 31/44] fix conflict --- web_app/api/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_app/api/dashboard.py b/web_app/api/dashboard.py index a22a4ca6..04292a1f 100644 --- a/web_app/api/dashboard.py +++ b/web_app/api/dashboard.py @@ -32,4 +32,4 @@ async def get_dashboard(request: Request, wallet_id: str) -> dict: "multipliers": {"ETH": first_opened_position["multiplier"]}, "start_dates": {"ETH": first_opened_position["created_at"]}, "zklend_position": zklend_position, - } + } From 06c1ed47ff1078a4a6ff32066fb5928a7eda1c3e Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Sat, 26 Oct 2024 21:32:53 +0530 Subject: [PATCH 32/44] merge conflict --- web_app/api/serializers/dashboard.py | 2 +- web_app/api/serializers/transaction.py | 2 +- web_app/api/user.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web_app/api/serializers/dashboard.py b/web_app/api/serializers/dashboard.py index 188c6fe8..3fb49325 100644 --- a/web_app/api/serializers/dashboard.py +++ b/web_app/api/serializers/dashboard.py @@ -66,4 +66,4 @@ def convert_products(cls, products): return converted_products class Config: - populate_by_name = True \ No newline at end of file + populate_by_name = True diff --git a/web_app/api/serializers/transaction.py b/web_app/api/serializers/transaction.py index 70b701d6..1c392260 100644 --- a/web_app/api/serializers/transaction.py +++ b/web_app/api/serializers/transaction.py @@ -79,4 +79,4 @@ class UpdateUserContractRequest(BaseModel): Pydantic model for the update user contract request. """ wallet_id: str - contract_address: str \ No newline at end of file + contract_address: str diff --git a/web_app/api/user.py b/web_app/api/user.py index ef6e447f..7fbc96b0 100644 --- a/web_app/api/user.py +++ b/web_app/api/user.py @@ -64,4 +64,4 @@ async def get_user_contract_address(wallet_id: str) -> dict: if contract_address: return {"contract_address": contract_address} else: - return {"contract_address": None} \ No newline at end of file + return {"contract_address": None} From 0b68f307e83aeb1b35de436e9201a98f8a062a32 Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Mon, 28 Oct 2024 03:55:35 +0530 Subject: [PATCH 33/44] added tests --- src/deposit.cairo | 13 ++- src/interfaces.cairo | 7 +- src/types.cairo | 2 +- tests/interfaces.cairo | 4 +- tests/test_loop.cairo | 199 ++++++++++++++++++++++++++++++----------- 5 files changed, 161 insertions(+), 64 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 78a279c9..c9909144 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -9,7 +9,10 @@ mod Deposit { use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use spotnet::constants::ZK_SCALE_DECIMALS; - use spotnet::interfaces::{IMarketDispatcher, IMarketDispatcherTrait, IAirdropDispatcher, IAirdropDispatcherTrait, IDeposit}; + use spotnet::interfaces::{ + IMarketDispatcher, IMarketDispatcherTrait, IAirdropDispatcher, IAirdropDispatcherTrait, + IDeposit + }; use spotnet::types::{SwapData, SwapResult, DepositData, Claim}; use starknet::event::EventEmitter; @@ -342,21 +345,15 @@ mod Deposit { claim_data: Claim, proofs: Span, claim_contract: ContractAddress, - reward_token: ContractAddress ) { assert(self.is_position_open.read(), 'Position is not open'); assert(proofs.len() != 0, 'Proofs Span cannot be empty'); let airdrop_dispatcher = IAirdropDispatcher { contract_address: claim_contract }; - let reward_dispatcher = ERC20ABIDispatcher { contract_address: reward_token }; let res = airdrop_dispatcher.claim(claim_data, proofs); assert(res, 'Claim failed'); - let send_res = reward_dispatcher.transfer(claim_data.claimee, claim_data.amount.into()); - - assert(send_res, 'Transfer failed'); - self .emit( RewardClaimed { @@ -368,7 +365,7 @@ mod Deposit { claim_contract } ); - } + } } #[abi(embed_v0)] diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 6e4ba9e7..e29c31f6 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -26,7 +26,6 @@ pub trait IDeposit { claim_data: Claim, proofs: Span, claim_contract: ContractAddress, - reward_token: ContractAddress ); } @@ -52,7 +51,9 @@ pub trait IAirdrop { fn get_token(self: @TContractState) -> ContractAddress; fn get_config(self: @TContractState) -> Config; fn claim(ref self: TContractState, claim: Claim, proof: Span) -> bool; - fn claim_128(ref self: TContractState, claims: Span, remaining_proof: Span) -> u8; + fn claim_128( + ref self: TContractState, claims: Span, remaining_proof: Span + ) -> u8; fn is_claimed(self: @TContractState, claim_id: u64) -> bool; fn refund(ref self: TContractState); -} \ No newline at end of file +} diff --git a/src/types.cairo b/src/types.cairo index 1616479f..1cf8ffab 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -58,4 +58,4 @@ pub struct Claim { pub id: u64, pub claimee: ContractAddress, pub amount: u128 -} \ No newline at end of file +} diff --git a/tests/interfaces.cairo b/tests/interfaces.cairo index 05760092..71ef427a 100644 --- a/tests/interfaces.cairo +++ b/tests/interfaces.cairo @@ -1,5 +1,5 @@ -use starknet::ContractAddress; use spotnet::types::MarketReserveData; +use starknet::ContractAddress; #[starknet::interface] pub trait IMarketTesting { @@ -7,7 +7,7 @@ pub trait IMarketTesting { fn get_user_debt_for_token( self: @TContractState, user: ContractAddress, token: ContractAddress ) -> felt252; - + fn liquidate( ref self: TContractState, user: ContractAddress, diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index fbe10c45..b3cbde70 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -18,15 +18,14 @@ use snforge_std::cheatcodes::execution_info::caller_address::{ }; use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; use spotnet::interfaces::{ - IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, - IDepositDispatcherTrait + IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, IDepositDispatcherTrait }; -use spotnet::types::{DepositData}; - -use super::interfaces::{IMarketTestingDispatcher, IMarketTestingDispatcherTrait}; +use spotnet::types::{DepositData, Claim}; use starknet::{ContractAddress, get_caller_address, get_block_number, get_block_timestamp}; +use super::interfaces::{IMarketTestingDispatcher, IMarketTestingDispatcherTrait}; + mod contracts { pub const EKUBO_CORE_MAINNET: felt252 = 0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b; @@ -92,8 +91,7 @@ fn test_loop_eth_valid() { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = - 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); @@ -135,8 +133,7 @@ fn test_loop_eth_fuzz(amount: u64) { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = - 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); let pool_key = PoolKey { @@ -163,14 +160,14 @@ fn test_loop_eth_fuzz(amount: u64) { pool_price, pool_price ) { - let message = *panic_data.at(0); - assert( - message == 'Parameters cannot be zero' - || message == 'Loop amount is too small' - || message == 'Approved amount incuficient' - || message == 'Insufficient balance', - message - ); // Acceptable panics which can be triggered by fuzzers' values + let message = *panic_data.at(0); + assert( + message == 'Parameters cannot be zero' + || message == 'Loop amount is too small' + || message == 'Approved amount incuficient' + || message == 'Insufficient balance', + message + ); // Acceptable panics which can be triggered by fuzzers' values }; stop_cheat_account_contract_address(deposit_disp.contract_address); } @@ -331,8 +328,7 @@ fn test_loop_position_exists_fuzz(amount: u64) { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 .try_into() .unwrap(); - let user: ContractAddress = - 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff .try_into() .unwrap(); let pool_key = PoolKey { @@ -362,15 +358,17 @@ fn test_loop_position_exists_fuzz(amount: u64) { ) { return; }; - match deposit_disp.loop_liquidity( - DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, - pool_key, - pool_price, - pool_price - ) { + match deposit_disp + .loop_liquidity( + DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, + pool_key, + pool_price, + pool_price + ) { Result::Ok(_) => panic_with_felt252('Not panicked with position open'), - Result::Err(panic_data) => assert(*panic_data.at(0) == 'Open position already exists', - *panic_data.at(0)) + Result::Err(panic_data) => assert( + *panic_data.at(0) == 'Open position already exists', *panic_data.at(0) + ) }; stop_cheat_account_contract_address(deposit_disp.contract_address); } @@ -423,26 +421,30 @@ fn test_close_position_usdc_valid_time_passed() { pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); - let zk_market = IMarketTestingDispatcher {contract_address: contracts::ZKLEND_MARKET.try_into().unwrap()}; + let zk_market = IMarketTestingDispatcher { + contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() + }; let usdc_reserve = zk_market.get_reserve_data(usdc_addr); let eth_reserve = zk_market.get_reserve_data(eth_addr); - let (lending_rate, borrowing_rate): (u256, u256) = (usdc_reserve.current_lending_rate.into(), eth_reserve.current_borrowing_rate.into()); + let (lending_rate, borrowing_rate): (u256, u256) = ( + usdc_reserve.current_lending_rate.into(), eth_reserve.current_borrowing_rate.into() + ); // println!("{}", lending_rate); // println!("{}", borrowing_rate); start_cheat_account_contract_address(deposit_disp.contract_address, user); start_cheat_block_timestamp( contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 40000000 ); - // println!("Debt {}", zk_market.get_user_debt_for_token(deposit_disp.contract_address, eth_addr)); - // println!("Z bal {}", ERC20ABIDispatcher {contract_address: usdc_reserve.z_token_address}.balanceOf(deposit_disp.contract_address)); + // println!("Debt {}", zk_market.get_user_debt_for_token(deposit_disp.contract_address, + // eth_addr)); + // println!("Z bal {}", ERC20ABIDispatcher {contract_address: + // usdc_reserve.z_token_address}.balanceOf(deposit_disp.contract_address)); deposit_disp.close_position(usdc_addr, eth_addr, pool_key, pool_price, quote_token_price); stop_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap()); stop_cheat_account_contract_address(deposit_disp.contract_address); // println!("After bal {}", token_disp.balanceOf(user)); - assert( - token_disp.balanceOf(user) > initial_balance, 'Balance is in wrong state' - ); + assert(token_disp.balanceOf(user) > initial_balance, 'Balance is in wrong state'); } #[test] @@ -491,21 +493,110 @@ fn test_close_position_amounts_cleared() { pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); - let zk_market = IMarketTestingDispatcher {contract_address: contracts::ZKLEND_MARKET.try_into().unwrap()}; + let zk_market = IMarketTestingDispatcher { + contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() + }; start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp.close_position(usdc_addr, eth_addr, pool_key, pool_price, quote_token_price); stop_cheat_account_contract_address(deposit_disp.contract_address); - - assert(zk_market.get_user_debt_for_token(deposit_disp.contract_address, eth_addr) == 0, 'Debt remains after repay'); - assert(ERC20ABIDispatcher {contract_address: zk_market.get_reserve_data(usdc_addr).z_token_address}.balanceOf(deposit_disp.contract_address) == 0, 'Not all withdrawn'); + + assert( + zk_market.get_user_debt_for_token(deposit_disp.contract_address, eth_addr) == 0, + 'Debt remains after repay' + ); + assert( + ERC20ABIDispatcher { + contract_address: zk_market.get_reserve_data(usdc_addr).z_token_address + } + .balanceOf(deposit_disp.contract_address) == 0, + 'Not all withdrawn' + ); } #[test] -#[fork("MAINNET)] +#[fork("MAINNET", block_number: 834899)] fn test_claim_rewards() { - -} + let strk_addr: ContractAddress = + 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d + .try_into() + .unwrap(); + + let usdc_addr: ContractAddress = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 + .try_into() + .unwrap(); + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + + let airdrop_addr: ContractAddress = + 0x66cabe824da3ff583b967ce571d393e8b667b33415acc750397aa66b64a5a6c + .try_into() + .unwrap(); + + let user: ContractAddress = 0x20281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af + .try_into() + .unwrap(); + + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: 170141183460469235273462165868118016, + tick_spacing: 1000, + extension: 0.try_into().unwrap() + }; + + let claim_data = Claim { id: 10611, claimee: user, amount: 0x2d86724a27dc3e2 }; + + let proofs = [ + 0x5e3a0851fc0f58fc98964fa4aeccfae6170f87540fb6c98c6b1a95ff8619235, + 0x2d6dc4884ec2fe892aeb92c661de4bd41a833603f309a2ca4079647f8bfc2d1, + 0x73a528ae05f2995cfecb744ffd6a9dd8e4697fb5fd65a6ee8bcdf3f7dba924d, + 0x6d4d65e6140674f5aa8aad2da4110faec2aba7a4b80080468a5068b5d8bfa55, + 0x58d3abc98b3aad194393f72f3be4f71636b2af39272e1ac4cae588ac5569e95, + 0x2d45d9ea573b7e830e182feaaf67fb9296080cdf8877ad787e8cd13a219c187, + 0x7fdc8c1b9a2303764f692d208ba6725a57db5e542f92b4dffeaebd00b495bb9, + 0x321c1fb2c1b85728f374568de28e173a220d2e0efcc404c7a7cd39883b0c8f7, + 0x151cfa61a31b61e7e8dedb909c6dbb13259404d8f413796e1c285ef8908a18b, + 0x45cbbc33878e5728fc97fb00c722a5946087c6f2ac287a15fa5abfa407b4d7d, + 0x194a2043aff310ac94defa8f50d78d66d63076a521c5fd7e2420c9b10a66813, + 0x3877f5b4750b7cb26e211e2f867882680bf9a9542222971f048fb831e6f225a, + 0x5c4a8fbdc17983b19b841f490aa0531d274e9a42231d22be2dadfbce6cdf981, + 0x44692783f2e911b439cd018f3ba8c067ba5ab88bec4b35e2496b2a3b0f817ef + ].span(); + + let pool_price: u256 = get_asset_price_pragma('ETH/USD').into(); + + let strk_disp = ERC20ABIDispatcher { contract_address: strk_addr }; + let eth_disp = ERC20ABIDispatcher { contract_address: eth_addr }; + let deposit_disp = get_deposit_dispatcher(user); + start_cheat_caller_address(eth_addr.try_into().unwrap(), user); + eth_disp.approve(deposit_disp.contract_address, 685000000000000); + stop_cheat_caller_address(eth_addr); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp + .loop_liquidity( + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4 }, + pool_key, + pool_price, + pool_price + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + let initial_balance = strk_disp.balanceOf(user); + // println!("initial bal {}", initial_balance); + + deposit_disp + .claim_rewards(claim_data: claim_data, proofs: proofs, claim_contract: airdrop_addr,); + + let final_balance = strk_disp.balanceOf(user); + // println!("final bal {}", final_balance); + + assert(final_balance > initial_balance, 'Reward was not transferred'); +} // TODO: Calculate interest rates to test behaviour after liquidation. // #[test] @@ -519,10 +610,12 @@ fn test_claim_rewards() { // 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 // .try_into() // .unwrap(); -// let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 +// let user: ContractAddress = +// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 // .try_into() // .unwrap(); -// let liquidator: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff.try_into().unwrap(); +// let liquidator: ContractAddress = +// 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff.try_into().unwrap(); // let pool_key = PoolKey { // token0: eth_addr, @@ -556,23 +649,28 @@ fn test_claim_rewards() { // pool_price // ); // stop_cheat_account_contract_address(deposit_disp.contract_address); -// let zk_market = IMarketTestingDispatcher {contract_address: contracts::ZKLEND_MARKET.try_into().unwrap()}; +// let zk_market = IMarketTestingDispatcher {contract_address: +// contracts::ZKLEND_MARKET.try_into().unwrap()}; // let usdc_reserve = zk_market.get_reserve_data(usdc_addr); // let eth_reserve = zk_market.get_reserve_data(eth_addr); -// let (lending_rate, borrowing_rate): (u256, u256) = (eth_reserve.current_lending_rate.into(), usdc_reserve.current_borrowing_rate.into()); +// let (lending_rate, borrowing_rate): (u256, u256) = (eth_reserve.current_lending_rate.into(), +// usdc_reserve.current_borrowing_rate.into()); // start_cheat_account_contract_address(deposit_disp.contract_address, user); -// start_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 4000000000); - +// start_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap(), +// get_block_timestamp() + 4000000000); + // start_cheat_caller_address(zk_market.contract_address, liquidator); - -// let debt = zk_market.get_user_debt_for_token(deposit_disp.contract_address, usdc_addr).into(); + +// let debt = zk_market.get_user_debt_for_token(deposit_disp.contract_address, +// usdc_addr).into(); // start_cheat_caller_address(usdc_addr, liquidator); // ERC20ABIDispatcher {contract_address: usdc_addr}.approve(zk_market.contract_address, debt); // stop_cheat_caller_address(usdc_addr); -// zk_market.liquidate(deposit_disp.contract_address, usdc_addr, (debt / 4).try_into().unwrap(), eth_addr); +// zk_market.liquidate(deposit_disp.contract_address, usdc_addr, (debt / 4).try_into().unwrap(), +// eth_addr); // stop_cheat_caller_address(zk_market.contract_address); // // deposit_disp.close_position(eth_addr, usdc_addr, pool_key, pool_price, quote_token_price); @@ -580,3 +678,4 @@ fn test_claim_rewards() { // stop_cheat_account_contract_address(deposit_disp.contract_address); // } + From a4efff1da8a71adafc59a6013ecf3d53fa84afe6 Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Mon, 28 Oct 2024 04:27:45 +0530 Subject: [PATCH 34/44] minor changes --- src/deposit.cairo | 34 +++++++++------------------------- src/interfaces.cairo | 7 +------ tests/test_loop.cairo | 9 ++------- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index c9909144..1f5b5247 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -86,12 +86,7 @@ mod Deposit { #[derive(starknet::Event, Drop)] struct RewardClaimed { - claim_id: u64, - claimed_amount: u128, - claimee: ContractAddress, - proofs: Span, - claim_status: bool, - claim_contract: ContractAddress + claim_status: bool } #[event] @@ -340,31 +335,20 @@ mod Deposit { ); } - fn claim_rewards( - ref self: ContractState, - claim_data: Claim, - proofs: Span, - claim_contract: ContractAddress, - ) { + fn claim_rewards(ref self: ContractState, claim_data: Claim, proofs: Span,) { assert(self.is_position_open.read(), 'Position is not open'); assert(proofs.len() != 0, 'Proofs Span cannot be empty'); - let airdrop_dispatcher = IAirdropDispatcher { contract_address: claim_contract }; + let airdrop_addr: ContractAddress = + 0x66cabe824da3ff583b967ce571d393e8b667b33415acc750397aa66b64a5a6c + .try_into() + .unwrap(); + let airdrop_disp = IAirdropDispatcher { contract_address: airdrop_addr }; - let res = airdrop_dispatcher.claim(claim_data, proofs); + let res = airdrop_disp.claim(claim_data, proofs); assert(res, 'Claim failed'); - self - .emit( - RewardClaimed { - claim_id: claim_data.id, - claimed_amount: claim_data.amount, - claimee: claim_data.claimee, - proofs, - claim_status: res, - claim_contract - } - ); + self.emit(RewardClaimed { claim_status: res, }); } } diff --git a/src/interfaces.cairo b/src/interfaces.cairo index e29c31f6..2d57b163 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -21,12 +21,7 @@ pub trait IDeposit { debt_price: u256 ); - fn claim_rewards( - ref self: TContractState, - claim_data: Claim, - proofs: Span, - claim_contract: ContractAddress, - ); + fn claim_rewards(ref self: TContractState, claim_data: Claim, proofs: Span,); } #[starknet::interface] diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index b3cbde70..e5b28251 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -530,11 +530,6 @@ fn test_claim_rewards() { .try_into() .unwrap(); - let airdrop_addr: ContractAddress = - 0x66cabe824da3ff583b967ce571d393e8b667b33415acc750397aa66b64a5a6c - .try_into() - .unwrap(); - let user: ContractAddress = 0x20281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af .try_into() .unwrap(); @@ -589,8 +584,7 @@ fn test_claim_rewards() { let initial_balance = strk_disp.balanceOf(user); // println!("initial bal {}", initial_balance); - deposit_disp - .claim_rewards(claim_data: claim_data, proofs: proofs, claim_contract: airdrop_addr,); + deposit_disp.claim_rewards(claim_data: claim_data, proofs: proofs); let final_balance = strk_disp.balanceOf(user); // println!("final bal {}", final_balance); @@ -679,3 +673,4 @@ fn test_claim_rewards() { // stop_cheat_account_contract_address(deposit_disp.contract_address); // } + From 7db97d323f79937ada508633634b6c52d1b351fa Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Mon, 28 Oct 2024 14:10:43 +0530 Subject: [PATCH 35/44] additional assert to check reward amount --- tests/test_loop.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index e5b28251..7c1b5f39 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -590,6 +590,7 @@ fn test_claim_rewards() { // println!("final bal {}", final_balance); assert(final_balance > initial_balance, 'Reward was not transferred'); + assert(final_balance - initial_balance == claim_data.amount.into(), 'Unexpected amount was rewarded'); } // TODO: Calculate interest rates to test behaviour after liquidation. From ad02dd9df129e19351b56b6f909e54e5c0a1c772 Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Tue, 29 Oct 2024 00:04:42 +0530 Subject: [PATCH 36/44] requested changes --- src/deposit.cairo | 19 +++---------------- src/interfaces.cairo | 11 ++--------- src/types.cairo | 7 ------- tests/test_loop.cairo | 9 +++++++-- 4 files changed, 12 insertions(+), 34 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 1f5b5247..d991dca8 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -84,17 +84,11 @@ mod Deposit { repaid_amount: u256 } - #[derive(starknet::Event, Drop)] - struct RewardClaimed { - claim_status: bool - } - #[event] #[derive(Drop, starknet::Event)] enum Event { LiquidityLooped: LiquidityLooped, PositionClosed: PositionClosed, - RewardClaimed: RewardClaimed } #[generate_trait] @@ -335,20 +329,13 @@ mod Deposit { ); } - fn claim_rewards(ref self: ContractState, claim_data: Claim, proofs: Span,) { + fn claim_rewards(ref self: ContractState, claim_data: Claim, proof: Span, airdrop_addr: ContractAddress) { assert(self.is_position_open.read(), 'Position is not open'); - assert(proofs.len() != 0, 'Proofs Span cannot be empty'); + assert(proof.len() != 0, 'Proof Span cannot be empty'); - let airdrop_addr: ContractAddress = - 0x66cabe824da3ff583b967ce571d393e8b667b33415acc750397aa66b64a5a6c - .try_into() - .unwrap(); let airdrop_disp = IAirdropDispatcher { contract_address: airdrop_addr }; - let res = airdrop_disp.claim(claim_data, proofs); - assert(res, 'Claim failed'); - - self.emit(RewardClaimed { claim_status: res, }); + assert(airdrop_disp.claim(claim_data, proof), 'Claim failed'); } } diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 2d57b163..26f94c8e 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -1,5 +1,5 @@ use ekubo::types::keys::PoolKey; -use spotnet::types::{MarketReserveData, DepositData, Config, Claim}; +use spotnet::types::{MarketReserveData, DepositData, Claim}; use starknet::{ContractAddress}; #[starknet::interface] @@ -21,7 +21,7 @@ pub trait IDeposit { debt_price: u256 ); - fn claim_rewards(ref self: TContractState, claim_data: Claim, proofs: Span,); + fn claim_rewards(ref self: TContractState, claim_data: Claim, proof: Span, airdrop_addr: ContractAddress); } #[starknet::interface] @@ -43,12 +43,5 @@ pub trait IMarket { #[starknet::interface] pub trait IAirdrop { - fn get_token(self: @TContractState) -> ContractAddress; - fn get_config(self: @TContractState) -> Config; fn claim(ref self: TContractState, claim: Claim, proof: Span) -> bool; - fn claim_128( - ref self: TContractState, claims: Span, remaining_proof: Span - ) -> u8; - fn is_claimed(self: @TContractState, claim_id: u64) -> bool; - fn refund(ref self: TContractState); } diff --git a/src/types.cairo b/src/types.cairo index 1cf8ffab..d8d05e77 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -46,13 +46,6 @@ pub struct MarketReserveData { debt_limit: felt252 } -#[derive(Copy, Drop, Serde)] -pub struct Config { - root: felt252, - refundable_timestamp: u64, - refund_to: ContractAddress -} - #[derive(Copy, Drop, Serde)] pub struct Claim { pub id: u64, diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 7c1b5f39..0cf6ae0d 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -530,6 +530,11 @@ fn test_claim_rewards() { .try_into() .unwrap(); + let airdrop_addr: ContractAddress = + 0x66cabe824da3ff583b967ce571d393e8b667b33415acc750397aa66b64a5a6c + .try_into() + .unwrap(); + let user: ContractAddress = 0x20281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af .try_into() .unwrap(); @@ -544,7 +549,7 @@ fn test_claim_rewards() { let claim_data = Claim { id: 10611, claimee: user, amount: 0x2d86724a27dc3e2 }; - let proofs = [ + let proof = [ 0x5e3a0851fc0f58fc98964fa4aeccfae6170f87540fb6c98c6b1a95ff8619235, 0x2d6dc4884ec2fe892aeb92c661de4bd41a833603f309a2ca4079647f8bfc2d1, 0x73a528ae05f2995cfecb744ffd6a9dd8e4697fb5fd65a6ee8bcdf3f7dba924d, @@ -584,7 +589,7 @@ fn test_claim_rewards() { let initial_balance = strk_disp.balanceOf(user); // println!("initial bal {}", initial_balance); - deposit_disp.claim_rewards(claim_data: claim_data, proofs: proofs); + deposit_disp.claim_rewards(claim_data, proof, airdrop_addr); let final_balance = strk_disp.balanceOf(user); // println!("final bal {}", final_balance); From c9cedfd78ae977edd7c4f0ee889b15d73dcad555 Mon Sep 17 00:00:00 2001 From: SoarinSkySagar Date: Tue, 29 Oct 2024 00:07:46 +0530 Subject: [PATCH 37/44] scarb fmt --- src/deposit.cairo | 7 ++++++- src/interfaces.cairo | 7 ++++++- tests/test_loop.cairo | 11 +++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index d991dca8..ca981f02 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -329,7 +329,12 @@ mod Deposit { ); } - fn claim_rewards(ref self: ContractState, claim_data: Claim, proof: Span, airdrop_addr: ContractAddress) { + fn claim_rewards( + ref self: ContractState, + claim_data: Claim, + proof: Span, + airdrop_addr: ContractAddress + ) { assert(self.is_position_open.read(), 'Position is not open'); assert(proof.len() != 0, 'Proof Span cannot be empty'); diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 26f94c8e..ee5d87db 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -21,7 +21,12 @@ pub trait IDeposit { debt_price: u256 ); - fn claim_rewards(ref self: TContractState, claim_data: Claim, proof: Span, airdrop_addr: ContractAddress); + fn claim_rewards( + ref self: TContractState, + claim_data: Claim, + proof: Span, + airdrop_addr: ContractAddress + ); } #[starknet::interface] diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 0cf6ae0d..4786e21b 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -531,9 +531,9 @@ fn test_claim_rewards() { .unwrap(); let airdrop_addr: ContractAddress = - 0x66cabe824da3ff583b967ce571d393e8b667b33415acc750397aa66b64a5a6c - .try_into() - .unwrap(); + 0x66cabe824da3ff583b967ce571d393e8b667b33415acc750397aa66b64a5a6c + .try_into() + .unwrap(); let user: ContractAddress = 0x20281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af .try_into() @@ -595,7 +595,10 @@ fn test_claim_rewards() { // println!("final bal {}", final_balance); assert(final_balance > initial_balance, 'Reward was not transferred'); - assert(final_balance - initial_balance == claim_data.amount.into(), 'Unexpected amount was rewarded'); + assert( + final_balance - initial_balance == claim_data.amount.into(), + 'Unexpected amount was rewarded' + ); } // TODO: Calculate interest rates to test behaviour after liquidation. From 8b45606c4b0b71dc465dd8ea3400eb1c33a66e42 Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 29 Oct 2024 19:43:52 +0100 Subject: [PATCH 38/44] Ekubo slippage parameter added --- src/deposit.cairo | 39 ++++++++++++------------------- src/interfaces.cairo | 7 +++--- src/types.cairo | 20 +++++++++++----- tests/test_loop.cairo | 53 +++++++++++++++++++++---------------------- 4 files changed, 58 insertions(+), 61 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index ca981f02..324e509c 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -13,7 +13,7 @@ mod Deposit { IMarketDispatcher, IMarketDispatcherTrait, IAirdropDispatcher, IAirdropDispatcherTrait, IDeposit }; - use spotnet::types::{SwapData, SwapResult, DepositData, Claim}; + use spotnet::types::{SwapData, SwapResult, DepositData, Claim, EkuboSlippageLimits}; use starknet::event::EventEmitter; use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess}; @@ -111,17 +111,16 @@ mod Deposit { /// `amount` field of `deposit_data` is `0` or `pool_price` is `0` /// address of the caller is not equal to `owner` storage address /// - /// # Paraemters - /// * `deposit_data` - Object of which stores main deposit information. + /// # Parameters + /// * `deposit_data` - Object which stores main deposit information. /// * `pool_key` - Ekubo type which represents data about pool. /// * `pool_price` - Price of `deposit` token in terms of `debt` token in Ekubo pool. - /// * `usdc_price` - Price of `deposit` token in USDC. fn loop_liquidity( ref self: ContractState, deposit_data: DepositData, pool_key: PoolKey, - pool_price: u256, - usdc_price: u256 + ekubo_limits: EkuboSlippageLimits, + pool_price: u256 ) { // TODO: Add borrow factor let user_acount = get_tx_info().unbox().account_contract_address; @@ -133,10 +132,6 @@ mod Deposit { assert(multiplier < 5 && multiplier > 1, 'Multiplier not supported'); assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); - assert( - amount * usdc_price / deposit_token_decimals.into() >= 1000000, - 'Loop amount is too small' - ); // User needs to have at least 1 USDC, so rounded down amount is OK. let curr_contract_address = get_contract_address(); assert( @@ -145,22 +140,18 @@ mod Deposit { ); assert(token_dispatcher.balanceOf(user_acount) >= amount, 'Insufficient balance'); - let (EKUBO_LOWER_SQRT_LIMIT, EKUBO_UPPER_SQRT_LIMIT) = ( - 18446748437148339061, 6277100250585753475930931601400621808602321654880405518632 - ); - let zk_market = self.zk_market.read(); let is_token1 = token == pool_key.token0; let (borrowing_token, sqrt_limit) = if is_token1 { - (pool_key.token1, EKUBO_UPPER_SQRT_LIMIT) + (pool_key.token1, ekubo_limits.upper) } else { - (pool_key.token0, EKUBO_LOWER_SQRT_LIMIT) + (pool_key.token0, ekubo_limits.lower) }; token_dispatcher.transferFrom(self.owner.read(), curr_contract_address, amount); - let reserve_data = zk_market.get_reserve_data(token); + let (deposit_reserve_data, debt_reserve_data) = (zk_market.get_reserve_data(token), zk_market.get_reserve_data(borrowing_token)); - let collateral_factor = reserve_data.collateral_factor.into(); + let (collateral_factor, borrow_factor) = (deposit_reserve_data.collateral_factor.into(), debt_reserve_data.borrow_factor.into()); zk_market.enable_collateral(token); @@ -169,7 +160,7 @@ mod Deposit { let (mut total_borrowed, mut accumulated, mut deposited) = (0, 0, amount); while (amount + accumulated) / amount < multiplier.into() { - let borrow_capacity = (deposited * collateral_factor / ZK_SCALE_DECIMALS); + let borrow_capacity = ((deposited * collateral_factor / ZK_SCALE_DECIMALS) * borrow_factor / ZK_SCALE_DECIMALS); let to_borrow = get_borrow_amount( borrow_capacity.try_into().unwrap(), pool_price, @@ -204,7 +195,7 @@ mod Deposit { LiquidityLooped { initial_amount: amount, deposited: ERC20ABIDispatcher { - contract_address: reserve_data.z_token_address + contract_address: deposit_reserve_data.z_token_address } .balanceOf(get_contract_address()), token_deposit: token, @@ -238,9 +229,11 @@ mod Deposit { supply_token: ContractAddress, debt_token: ContractAddress, pool_key: PoolKey, + ekubo_limits: EkuboSlippageLimits, supply_price: u256, debt_price: u256 ) { + // TODO: Add ekubo limits to the docs assert( get_tx_info().unbox().account_contract_address == self.owner.read(), 'Caller is not the owner' @@ -261,11 +254,7 @@ mod Deposit { fast_power(10, token_disp.decimals().into()), fast_power(10, debt_dispatcher.decimals().into()) ); - let SQRT_LIMIT_REPAY = if supply_token == pool_key.token0 { - 18446748437148339061 - } else { - 6277100250585753475930931601400621808602321654880405518632 - }; + let SQRT_LIMIT_REPAY = if supply_token == pool_key.token0 { ekubo_limits.lower } else { ekubo_limits.upper }; let is_token1_repay_swap = supply_token == pool_key.token1; let mut repaid_amount: u256 = 0; while debt != 0 { diff --git a/src/interfaces.cairo b/src/interfaces.cairo index ee5d87db..56d03c63 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -1,5 +1,5 @@ use ekubo::types::keys::PoolKey; -use spotnet::types::{MarketReserveData, DepositData, Claim}; +use spotnet::types::{MarketReserveData, DepositData, Claim, EkuboSlippageLimits}; use starknet::{ContractAddress}; #[starknet::interface] @@ -8,8 +8,8 @@ pub trait IDeposit { ref self: TContractState, deposit_data: DepositData, pool_key: PoolKey, - pool_price: u256, - usdc_price: u256 + ekubo_limits: EkuboSlippageLimits, + pool_price: u256 ); fn close_position( @@ -17,6 +17,7 @@ pub trait IDeposit { supply_token: ContractAddress, debt_token: ContractAddress, pool_key: PoolKey, + ekubo_limits: EkuboSlippageLimits, supply_price: u256, debt_price: u256 ); diff --git a/src/types.cairo b/src/types.cairo index d8d05e77..836843cd 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -26,6 +26,19 @@ pub struct DepositData { pub multiplier: u32 } +#[derive(Copy, Drop, Serde)] +pub struct EkuboSlippageLimits { + pub lower: u256, + pub upper: u256 +} + +#[derive(Copy, Drop, Serde)] +pub struct Claim { + pub id: u64, + pub claimee: ContractAddress, + pub amount: u128 +} + #[derive(Drop, Serde, starknet::Store)] pub struct MarketReserveData { enabled: bool, @@ -46,9 +59,4 @@ pub struct MarketReserveData { debt_limit: felt252 } -#[derive(Copy, Drop, Serde)] -pub struct Claim { - pub id: u64, - pub claimee: ContractAddress, - pub amount: u128 -} + diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 4786e21b..955c05ff 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -20,7 +20,7 @@ use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; use spotnet::interfaces::{ IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, IDepositDispatcherTrait }; -use spotnet::types::{DepositData, Claim}; +use spotnet::types::{DepositData, Claim, EkuboSlippageLimits}; use starknet::{ContractAddress, get_caller_address, get_block_number, get_block_timestamp}; @@ -71,12 +71,12 @@ fn get_asset_price_pragma(pair: felt252) -> u128 { output.price / 100 // Make 6 decimals wide instead of 8. } -// fn get_token_addresses(pair: felt252) -> (ContractAddress, ContractAddress) { -// match pair { -// 'ETH/USDC' => (tokens::ETH.try_into().unwrap(), tokens::USDC.try_into().unwrap()), -// _ => (0.try_into().unwrap(), 1.try_into().unwrap()), -// } -// } +fn get_slippage_limits(pool_key: PoolKey) -> EkuboSlippageLimits { + let ekubo_core = ICoreDispatcher {contract_address: contracts::EKUBO_CORE_MAINNET.try_into().unwrap()}; + let sqrt_ratio = ekubo_core.get_pool_price(pool_key).sqrt_ratio; + let tolerance = sqrt_ratio * 5 / 100; + EkuboSlippageLimits {lower: sqrt_ratio - tolerance, upper: sqrt_ratio + tolerance} +} // TODO: Add tests for asserts. @@ -114,7 +114,7 @@ fn test_loop_eth_valid() { .loop_liquidity( DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4 }, pool_key, - pool_price, + get_slippage_limits(pool_key), pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); @@ -157,7 +157,7 @@ fn test_loop_eth_fuzz(amount: u64) { .loop_liquidity( DepositData { token: eth_addr, amount: amount.into(), multiplier: 4 }, pool_key, - pool_price, + get_slippage_limits(pool_key), pool_price ) { let message = *panic_data.at(0); @@ -212,8 +212,8 @@ fn test_loop_usdc_valid() { .loop_liquidity( DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, pool_key, - pool_price.into(), - 1000000 + get_slippage_limits(pool_key), + pool_price.into() ); stop_cheat_account_contract_address(deposit_disp.contract_address); } @@ -256,8 +256,8 @@ fn test_loop_unauthorized() { .loop_liquidity( DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, pool_key, - pool_price.into(), - 1000000 + get_slippage_limits(pool_key), + pool_price.into() ); } @@ -302,15 +302,15 @@ fn test_loop_position_exists() { .loop_liquidity( DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, pool_key, - pool_price.into(), - 1000000 + get_slippage_limits(pool_key), + pool_price.into() ); deposit_disp .loop_liquidity( DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, pool_key, - pool_price.into(), - 1000000 + get_slippage_limits(pool_key), + pool_price.into() ); stop_cheat_account_contract_address(deposit_disp.contract_address); } @@ -353,7 +353,7 @@ fn test_loop_position_exists_fuzz(amount: u64) { .loop_liquidity( DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, pool_key, - pool_price, + get_slippage_limits(pool_key), pool_price ) { return; @@ -362,7 +362,7 @@ fn test_loop_position_exists_fuzz(amount: u64) { .loop_liquidity( DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, pool_key, - pool_price, + get_slippage_limits(pool_key), pool_price ) { Result::Ok(_) => panic_with_felt252('Not panicked with position open'), @@ -417,7 +417,7 @@ fn test_close_position_usdc_valid_time_passed() { .loop_liquidity( DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4 }, pool_key, - pool_price, + get_slippage_limits(pool_key), pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); @@ -439,7 +439,7 @@ fn test_close_position_usdc_valid_time_passed() { // eth_addr)); // println!("Z bal {}", ERC20ABIDispatcher {contract_address: // usdc_reserve.z_token_address}.balanceOf(deposit_disp.contract_address)); - deposit_disp.close_position(usdc_addr, eth_addr, pool_key, pool_price, quote_token_price); + deposit_disp.close_position(usdc_addr, eth_addr, pool_key, get_slippage_limits(pool_key), pool_price, quote_token_price); stop_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap()); stop_cheat_account_contract_address(deposit_disp.contract_address); @@ -489,7 +489,7 @@ fn test_close_position_amounts_cleared() { .loop_liquidity( DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4 }, pool_key, - pool_price, + get_slippage_limits(pool_key), pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); @@ -497,7 +497,7 @@ fn test_close_position_amounts_cleared() { contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() }; start_cheat_account_contract_address(deposit_disp.contract_address, user); - deposit_disp.close_position(usdc_addr, eth_addr, pool_key, pool_price, quote_token_price); + deposit_disp.close_position(usdc_addr, eth_addr, pool_key, get_slippage_limits(pool_key), pool_price, quote_token_price); stop_cheat_account_contract_address(deposit_disp.contract_address); assert( @@ -514,7 +514,7 @@ fn test_close_position_amounts_cleared() { } #[test] -#[fork("MAINNET", block_number: 834899)] +#[fork(url: "http://127.0.0.1:5050", block_number: 834899)] fn test_claim_rewards() { let strk_addr: ContractAddress = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d @@ -581,11 +581,11 @@ fn test_claim_rewards() { .loop_liquidity( DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4 }, pool_key, - pool_price, + get_slippage_limits(pool_key), pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); - + let initial_balance = strk_disp.balanceOf(user); // println!("initial bal {}", initial_balance); @@ -649,7 +649,6 @@ fn test_claim_rewards() { // DepositData { token: eth_addr, amount: 10000000000000000, multiplier: 4 }, // pool_key, // pool_price, -// pool_price // ); // stop_cheat_account_contract_address(deposit_disp.contract_address); // let zk_market = IMarketTestingDispatcher {contract_address: From 9dce53a0856423d51d3a6fa8c900b5cf91b4f5c4 Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 29 Oct 2024 19:45:16 +0100 Subject: [PATCH 39/44] Break added to not add debt --- src/deposit.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/src/deposit.cairo b/src/deposit.cairo index 324e509c..7553674e 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -301,6 +301,7 @@ mod Deposit { repaid_amount += debt; debt_dispatcher.approve(zk_market.contract_address, debt); zk_market.repay_all(debt_token); + break; } debt = zk_market.get_user_debt_for_token(contract_address, debt_token).into(); From 412326ee96ca009a02c6f4758c9536e7a6417b83 Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 29 Oct 2024 22:39:40 +0100 Subject: [PATCH 40/44] Docs updated --- docs/spotnet.md | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/spotnet.md b/docs/spotnet.md index ae55ce56..b324e653 100644 --- a/docs/spotnet.md +++ b/docs/spotnet.md @@ -1,11 +1,11 @@ # Spotnet functionality documentation ## Overview -Spotnet is a dApp designed for increasing of initial collateral deposit deposit by utilizing lending +Spotnet is a dApp designed for increasing of initial collateral deposit by utilizing lending protocols and AMMs for borrowing tokens, swapping them and redepositing up to x4 of starting capital(as for now). ## Spotnet Smart Contract -Smart contract consists of a constructor and two methods for public use. +Smart contract consists of a constructor and three methods for public use. ### Constructor Constructor of our contract receives three parameters(ekubo_core and zk_market are serialized to Dispatcher types for starknet::interfaces): @@ -19,6 +19,7 @@ The `loop_liquidity` method is responsible for deposit of collateral token. For This method has next parameters: * `deposit_data`: DepositData - Object of internal type which stores main deposit information. * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping tokens. +* `ekubo_limits`: EkuboSlippageLimits - Object of internal type which represents upper and lower sqrt_ratio values on Ekubo. Used to control slippage while swapping. * `pool_price`: felt252 - Price of `deposit` token in terms of `debt` token in Ekubo pool(so for ex. 2400000000 USDC for ETH when depositing ETH). It's flow can be described as follows: @@ -53,8 +54,9 @@ The method has next parameters: * `supply_token`: ContractAddress - Address of the token used as collateral. * `debt_token`: ContractAddress - Address of the token used as borrowing. * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping tokens. -* `supply_price`: felt252 - Price of `supply` token in terms of `debt` token in Ekubo pool(so for ex. 2400000000 USDC for ETH). -* `debt_price`: felt252 - Price of `debt` token in terms of `supply` token in Ekubo pool(for ex. 410000000000000 ETH for USDC). +* `ekubo_limits`: EkuboSlippageLimits - Object of internal type which represents upper and lower sqrt_ratio values on Ekubo. Used to control slippage while swapping. +* `supply_price`: TokenPrice - Price of `supply` token in terms of `debt` token in Ekubo pool(so for ex. 2400000000 USDC for ETH). +* `debt_price`: TokenPrice - Price of `debt` token in terms of `supply` token in Ekubo pool(for ex. 410000000000000 ETH for USDC). It's flow can be described as follows: ``` @@ -71,17 +73,35 @@ while debt != 0 { repay } -swap back extra debt token +transfer tokens to user + +disable collateral token emit event ``` +### claim_reward +The `claim_reward` method claims airdrop reward accumulated by the contract that deposited into zkLend. Claim only possible if position is currently open. +This method has next parameters: +* `claim_data`: Claim - contains data about claim operation +* `proof`: Span - proof used to validate the claim +* `airdrop_addr`: ContractAddress - address of a contract responsible for claim + +Ir's flow can be described as follow +``` +assertions + +airdrop claim + +transfer half of reward to the treasury +``` + ## Important types, events and constants ### Types ``` struct DepositData { token: ContractAddress, - amount: u256, + amount: TokenAmount, multiplier: u32 } ``` @@ -89,10 +109,10 @@ struct DepositData { ### Events ``` struct LiquidityLooped { - initial_amount: u256, - deposited: u256, + initial_amount: TokenAmount, + deposited: TokenAmount, token_deposit: ContractAddress, - borrowed: u256, + borrowed: TokenAmount, token_borrowed: ContractAddress } ``` @@ -100,12 +120,10 @@ struct LiquidityLooped { struct PositionClosed { deposit_token: ContractAddress, debt_token: ContractAddress, - withdrawn_amount: u256, - repaid_amount: u256 + withdrawn_amount: TokenAmount, + repaid_amount: TokenAmount } ``` ### Constants -* For swaps the EKUBO_LOWER_SQRT_LIMIT and EKUBO_UPPER_SQRT_LIMIT constants used. They define how much price can be moved by swappng. For now it is set to limiting values (`18446748437148339061` and `6277100250585753475930931601400621808602321654880405518632` respectively). - -* ZK_SCALE_DECIMALS is used for scaling down values obtained by multiplying on zklend collateral and borrow factor. +* ZK_SCALE_DECIMALS is used for scaling down values obtained by multiplying on zklend collateral and borrow factors. From 2fd5ee3d32d3f3813a739399a7ef83d131bcb77f Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 29 Oct 2024 22:41:10 +0100 Subject: [PATCH 41/44] Proper types set. Imports optimized --- src/deposit.cairo | 173 +++++++++++++++++++++++++++--------------- src/interfaces.cairo | 12 +-- src/types.cairo | 10 +-- tests/test_loop.cairo | 45 +++++++---- 4 files changed, 150 insertions(+), 90 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 7553674e..9ab0a8fd 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -2,22 +2,28 @@ mod Deposit { use alexandria_math::fast_power::fast_power; - use ekubo::interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait, ILocker, SwapParameters}; - use ekubo::types::i129::i129; - use ekubo::types::keys::PoolKey; + use ekubo::{ + interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait, ILocker, SwapParameters}, + types::{i129::i129, keys::PoolKey} + }; use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; - use spotnet::constants::ZK_SCALE_DECIMALS; - - use spotnet::interfaces::{ - IMarketDispatcher, IMarketDispatcherTrait, IAirdropDispatcher, IAirdropDispatcherTrait, - IDeposit + use spotnet::{ + constants::ZK_SCALE_DECIMALS, + interfaces::{ + IMarketDispatcher, IMarketDispatcherTrait, IAirdropDispatcher, IAirdropDispatcherTrait, + IDeposit + }, + types::{ + SwapData, SwapResult, DepositData, Claim, EkuboSlippageLimits, TokenAmount, TokenPrice, + Decimals + } }; - use spotnet::types::{SwapData, SwapResult, DepositData, Claim, EkuboSlippageLimits}; - use starknet::event::EventEmitter; - use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess}; - use starknet::{ContractAddress, get_contract_address, get_tx_info}; + use starknet::{ + ContractAddress, get_contract_address, get_tx_info, event::EventEmitter, + storage::{StoragePointerWriteAccess, StoragePointerReadAccess} + }; #[storage] struct Storage { @@ -40,39 +46,43 @@ mod Deposit { } fn get_borrow_amount( - borrow_capacity: u256, - token_price: u256, - decimals_difference: felt252, - total_borrowed: felt252 + borrow_capacity: TokenAmount, + token_price: TokenPrice, + decimals_difference: Decimals, + total_borrowed: TokenAmount ) -> felt252 { let borrow_const = 60; - let amount_base_token = token_price * borrow_capacity; - let amount_quote_token = amount_base_token.into() / decimals_difference.into(); - ((amount_quote_token - total_borrowed.into()) / 100_u256 * borrow_const).try_into().unwrap() + let amount_base_token = token_price.into() * borrow_capacity; + let amount_quote_token = amount_base_token / decimals_difference.into(); + ((amount_quote_token - total_borrowed) / 100_u256 * borrow_const).try_into().unwrap() } fn get_withdraw_amount( - total_deposited: u256, - total_debt: u256, + total_deposited: TokenAmount, + total_debt: TokenAmount, collateral_factor: felt252, - supply_token_price: u256, - debt_token_price: u256, - supply_decimals: u256, - debt_decimals: u256 + borrow_factor: felt252, + supply_token_price: TokenPrice, + debt_token_price: TokenPrice, + supply_decimals: Decimals, + debt_decimals: Decimals ) -> u256 { - let deposited = (total_deposited * supply_token_price).into() / supply_decimals; - let free_amount = (deposited * collateral_factor.into() / ZK_SCALE_DECIMALS) + let deposited = (total_deposited * supply_token_price.into()).into() + / supply_decimals.into(); + let free_amount = ((deposited * collateral_factor.into() / ZK_SCALE_DECIMALS) + * borrow_factor.into() + / ZK_SCALE_DECIMALS) - total_debt.into(); - let withdraw_amount = free_amount * debt_token_price.into() / debt_decimals; + let withdraw_amount = free_amount * debt_token_price.into() / debt_decimals.into(); withdraw_amount } #[derive(starknet::Event, Drop)] struct LiquidityLooped { - initial_amount: u256, - deposited: u256, + initial_amount: TokenAmount, + deposited: TokenAmount, token_deposit: ContractAddress, - borrowed: u256, + borrowed: TokenAmount, token_borrowed: ContractAddress } @@ -80,8 +90,8 @@ mod Deposit { struct PositionClosed { deposit_token: ContractAddress, debt_token: ContractAddress, - withdrawn_amount: u256, - repaid_amount: u256 + withdrawn_amount: TokenAmount, + repaid_amount: TokenAmount } #[event] @@ -112,23 +122,25 @@ mod Deposit { /// address of the caller is not equal to `owner` storage address /// /// # Parameters - /// * `deposit_data` - Object which stores main deposit information. - /// * `pool_key` - Ekubo type which represents data about pool. - /// * `pool_price` - Price of `deposit` token in terms of `debt` token in Ekubo pool. + /// * `deposit_data`: DepositData - Object which stores main deposit information. + /// * `pool_key`: PoolKey - Ekubo type which represents data about pool. + /// * `ekubo_limits`: EkuboSlippageLimits - Represents upper and lower sqrt_ratio values on + /// Ekubo. Used to control slippage while swapping. + /// * `pool_price`: TokenPrice - Price of `deposit` token in terms of `debt` token in Ekubo + /// pool. fn loop_liquidity( ref self: ContractState, deposit_data: DepositData, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, - pool_price: u256 + pool_price: TokenPrice ) { - // TODO: Add borrow factor let user_acount = get_tx_info().unbox().account_contract_address; assert(user_acount == self.owner.read(), 'Caller is not the owner'); assert(!self.is_position_open.read(), 'Open position already exists'); let DepositData { token, amount, multiplier } = deposit_data; let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; - let deposit_token_decimals = fast_power(10_u128, token_dispatcher.decimals().into()); + let deposit_token_decimals = fast_power(10_u64, token_dispatcher.decimals().into()); assert(multiplier < 5 && multiplier > 1, 'Multiplier not supported'); assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); @@ -141,17 +153,22 @@ mod Deposit { assert(token_dispatcher.balanceOf(user_acount) >= amount, 'Insufficient balance'); let zk_market = self.zk_market.read(); - let is_token1 = token == pool_key.token0; - let (borrowing_token, sqrt_limit) = if is_token1 { - (pool_key.token1, ekubo_limits.upper) + // let is_token1 = token == pool_key.token0; + let (is_token1, borrowing_token, sqrt_limit) = if token == pool_key.token0 { + (true, pool_key.token1, ekubo_limits.upper) } else { - (pool_key.token0, ekubo_limits.lower) + (false, pool_key.token0, ekubo_limits.lower) }; token_dispatcher.transferFrom(self.owner.read(), curr_contract_address, amount); - let (deposit_reserve_data, debt_reserve_data) = (zk_market.get_reserve_data(token), zk_market.get_reserve_data(borrowing_token)); + let (deposit_reserve_data, debt_reserve_data) = ( + zk_market.get_reserve_data(token), zk_market.get_reserve_data(borrowing_token) + ); - let (collateral_factor, borrow_factor) = (deposit_reserve_data.collateral_factor.into(), debt_reserve_data.borrow_factor.into()); + let (collateral_factor, borrow_factor) = ( + deposit_reserve_data.collateral_factor.into(), + debt_reserve_data.borrow_factor.into() + ); zk_market.enable_collateral(token); @@ -160,14 +177,16 @@ mod Deposit { let (mut total_borrowed, mut accumulated, mut deposited) = (0, 0, amount); while (amount + accumulated) / amount < multiplier.into() { - let borrow_capacity = ((deposited * collateral_factor / ZK_SCALE_DECIMALS) * borrow_factor / ZK_SCALE_DECIMALS); + let borrow_capacity = ((deposited * collateral_factor / ZK_SCALE_DECIMALS) + * borrow_factor + / ZK_SCALE_DECIMALS); let to_borrow = get_borrow_amount( borrow_capacity.try_into().unwrap(), pool_price, deposit_token_decimals.into(), total_borrowed ); - total_borrowed += to_borrow; + total_borrowed += to_borrow.into(); zk_market.borrow(borrowing_token, to_borrow); let params = SwapParameters { amount: i129 { mag: to_borrow.try_into().unwrap(), sign: false }, @@ -220,9 +239,11 @@ mod Deposit { /// * `debt_token`: ContractAddress - Address of the token used as borrowing. /// * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping /// tokens. - /// * `supply_price`: felt252 - Price of `supply` token in terms of `debt` token in Ekubo + /// * `ekubo_limits`: EkuboSlippageLimits - Represents upper and lower sqrt_ratio values on + /// Ekubo. Used to control slippage while swapping. + /// * `supply_price`: TokenPrice - Price of `supply` token in terms of `debt` token in Ekubo /// pool. - /// * `debt_price`: felt252 - Price of `debt` token in terms of `supply` token in Ekubo + /// * `debt_price`: TokenPrice - Price of `debt` token in terms of `supply` token in Ekubo /// pool. fn close_position( ref self: ContractState, @@ -230,10 +251,9 @@ mod Deposit { debt_token: ContractAddress, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, - supply_price: u256, - debt_price: u256 + supply_price: TokenPrice, + debt_price: TokenPrice ) { - // TODO: Add ekubo limits to the docs assert( get_tx_info().unbox().account_contract_address == self.owner.read(), 'Caller is not the owner' @@ -242,9 +262,17 @@ mod Deposit { assert(supply_price != 0 && debt_price != 0, 'Parameters cannot be zero'); let token_disp = ERC20ABIDispatcher { contract_address: supply_token }; let zk_market = self.zk_market.read(); - let reserve_data = zk_market.get_reserve_data(supply_token); + let (deposit_reserve_data, debt_reserve_data) = ( + zk_market.get_reserve_data(supply_token), zk_market.get_reserve_data(debt_token) + ); + + let (collateral_factor, borrow_factor) = ( + deposit_reserve_data.collateral_factor.into(), + debt_reserve_data.borrow_factor.into() + ); + let z_token_disp = ERC20ABIDispatcher { - contract_address: reserve_data.z_token_address + contract_address: deposit_reserve_data.z_token_address }; let contract_address = get_contract_address(); let mut debt = zk_market.get_user_debt_for_token(contract_address, debt_token).into(); @@ -254,14 +282,19 @@ mod Deposit { fast_power(10, token_disp.decimals().into()), fast_power(10, debt_dispatcher.decimals().into()) ); - let SQRT_LIMIT_REPAY = if supply_token == pool_key.token0 { ekubo_limits.lower } else { ekubo_limits.upper }; + let SQRT_LIMIT_REPAY = if supply_token == pool_key.token0 { + ekubo_limits.lower + } else { + ekubo_limits.upper + }; let is_token1_repay_swap = supply_token == pool_key.token1; let mut repaid_amount: u256 = 0; while debt != 0 { let withdraw_amount = get_withdraw_amount( z_token_disp.balanceOf(contract_address).try_into().unwrap(), debt, - reserve_data.collateral_factor.into(), + collateral_factor, + borrow_factor, supply_price, debt_price, supply_decimals, @@ -269,7 +302,9 @@ mod Deposit { ); zk_market.withdraw(supply_token, withdraw_amount.try_into().unwrap()); - let params = if (debt > withdraw_amount * supply_price / supply_decimals) { + let params = if (debt > withdraw_amount + * supply_price.into() + / supply_decimals.into()) { SwapParameters { amount: i129 { mag: withdraw_amount.try_into().unwrap(), sign: false }, is_token1: is_token1_repay_swap, @@ -319,18 +354,30 @@ mod Deposit { ); } - fn claim_rewards( + /// Claims STRK airdrop on ZKlend + /// + /// # Panics + /// `is_position_open` storage variable is set to false('Open position not exists') + /// `proof` span is empty + /// Claim call failed + /// + /// # Parameters + /// `claim_data`: Claim - contains data about claim operation + /// `proof`: Span - proof used to validate the claim + /// `airdrop_addr`: ContractAddress - address of a contract responsible for claim + fn claim_reward( ref self: ContractState, claim_data: Claim, proof: Span, airdrop_addr: ContractAddress ) { - assert(self.is_position_open.read(), 'Position is not open'); + assert(self.is_position_open.read(), 'Open position not exists'); assert(proof.len() != 0, 'Proof Span cannot be empty'); - - let airdrop_disp = IAirdropDispatcher { contract_address: airdrop_addr }; - - assert(airdrop_disp.claim(claim_data, proof), 'Claim failed'); + assert( + IAirdropDispatcher { contract_address: airdrop_addr }.claim(claim_data, proof), + 'Claim failed' + ); + // TODO: Add transfer to the Treasury } } diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 56d03c63..0dc3e61f 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -1,6 +1,6 @@ use ekubo::types::keys::PoolKey; -use spotnet::types::{MarketReserveData, DepositData, Claim, EkuboSlippageLimits}; -use starknet::{ContractAddress}; +use spotnet::types::{MarketReserveData, DepositData, Claim, EkuboSlippageLimits, TokenPrice}; +use starknet::ContractAddress; #[starknet::interface] pub trait IDeposit { @@ -9,7 +9,7 @@ pub trait IDeposit { deposit_data: DepositData, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, - pool_price: u256 + pool_price: TokenPrice ); fn close_position( @@ -18,11 +18,11 @@ pub trait IDeposit { debt_token: ContractAddress, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, - supply_price: u256, - debt_price: u256 + supply_price: TokenPrice, + debt_price: TokenPrice ); - fn claim_rewards( + fn claim_reward( ref self: TContractState, claim_data: Claim, proof: Span, diff --git a/src/types.cairo b/src/types.cairo index 836843cd..e824beaa 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -1,9 +1,10 @@ -use ekubo::interfaces::core::SwapParameters; -use ekubo::types::delta::Delta; -use ekubo::types::keys::PoolKey; +// use ekubo::interfaces::core::SwapParameters; +// use ekubo::types::delta::Delta; +// use ekubo::types::keys::PoolKey; +use ekubo::{interfaces::core::SwapParameters, types::{delta::Delta, keys::PoolKey}}; use starknet::ContractAddress; -pub type TokenPrice = u256; +pub type TokenPrice = u128; pub type TokenAmount = u256; pub type Decimals = u64; @@ -59,4 +60,3 @@ pub struct MarketReserveData { debt_limit: felt252 } - diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 955c05ff..07ba16e5 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -1,7 +1,6 @@ use alexandria_math::fast_power::fast_power; use core::panic_with_felt252; -use ekubo::interfaces::core::{SwapParameters, ICoreDispatcher, ICoreDispatcherTrait}; -use ekubo::types::i129::{i129}; +use ekubo::interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait}; use ekubo::types::keys::{PoolKey}; use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; @@ -22,7 +21,7 @@ use spotnet::interfaces::{ }; use spotnet::types::{DepositData, Claim, EkuboSlippageLimits}; -use starknet::{ContractAddress, get_caller_address, get_block_number, get_block_timestamp}; +use starknet::{ContractAddress, get_block_timestamp}; use super::interfaces::{IMarketTestingDispatcher, IMarketTestingDispatcherTrait}; @@ -72,10 +71,12 @@ fn get_asset_price_pragma(pair: felt252) -> u128 { } fn get_slippage_limits(pool_key: PoolKey) -> EkuboSlippageLimits { - let ekubo_core = ICoreDispatcher {contract_address: contracts::EKUBO_CORE_MAINNET.try_into().unwrap()}; + let ekubo_core = ICoreDispatcher { + contract_address: contracts::EKUBO_CORE_MAINNET.try_into().unwrap() + }; let sqrt_ratio = ekubo_core.get_pool_price(pool_key).sqrt_ratio; let tolerance = sqrt_ratio * 5 / 100; - EkuboSlippageLimits {lower: sqrt_ratio - tolerance, upper: sqrt_ratio + tolerance} + EkuboSlippageLimits { lower: sqrt_ratio - tolerance, upper: sqrt_ratio + tolerance } } // TODO: Add tests for asserts. @@ -399,7 +400,7 @@ fn test_close_position_usdc_valid_time_passed() { let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; let initial_balance = token_disp.balanceOf(user); - // println!("Initial bal {initial_balance}"); + let decimals_sum_power: u128 = fast_power( 10, (ERC20ABIDispatcher { contract_address: eth_addr }.decimals() + token_disp.decimals()) @@ -426,11 +427,7 @@ fn test_close_position_usdc_valid_time_passed() { }; let usdc_reserve = zk_market.get_reserve_data(usdc_addr); let eth_reserve = zk_market.get_reserve_data(eth_addr); - let (lending_rate, borrowing_rate): (u256, u256) = ( - usdc_reserve.current_lending_rate.into(), eth_reserve.current_borrowing_rate.into() - ); - // println!("{}", lending_rate); - // println!("{}", borrowing_rate); + start_cheat_account_contract_address(deposit_disp.contract_address, user); start_cheat_block_timestamp( contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 40000000 @@ -439,7 +436,15 @@ fn test_close_position_usdc_valid_time_passed() { // eth_addr)); // println!("Z bal {}", ERC20ABIDispatcher {contract_address: // usdc_reserve.z_token_address}.balanceOf(deposit_disp.contract_address)); - deposit_disp.close_position(usdc_addr, eth_addr, pool_key, get_slippage_limits(pool_key), pool_price, quote_token_price); + deposit_disp + .close_position( + usdc_addr, + eth_addr, + pool_key, + get_slippage_limits(pool_key), + pool_price, + quote_token_price + ); stop_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap()); stop_cheat_account_contract_address(deposit_disp.contract_address); @@ -497,7 +502,15 @@ fn test_close_position_amounts_cleared() { contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() }; start_cheat_account_contract_address(deposit_disp.contract_address, user); - deposit_disp.close_position(usdc_addr, eth_addr, pool_key, get_slippage_limits(pool_key), pool_price, quote_token_price); + deposit_disp + .close_position( + usdc_addr, + eth_addr, + pool_key, + get_slippage_limits(pool_key), + pool_price, + quote_token_price + ); stop_cheat_account_contract_address(deposit_disp.contract_address); assert( @@ -566,7 +579,7 @@ fn test_claim_rewards() { 0x44692783f2e911b439cd018f3ba8c067ba5ab88bec4b35e2496b2a3b0f817ef ].span(); - let pool_price: u256 = get_asset_price_pragma('ETH/USD').into(); + let pool_price = get_asset_price_pragma('ETH/USD'); let strk_disp = ERC20ABIDispatcher { contract_address: strk_addr }; let eth_disp = ERC20ABIDispatcher { contract_address: eth_addr }; @@ -585,11 +598,11 @@ fn test_claim_rewards() { pool_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); - + let initial_balance = strk_disp.balanceOf(user); // println!("initial bal {}", initial_balance); - deposit_disp.claim_rewards(claim_data, proof, airdrop_addr); + deposit_disp.claim_reward(claim_data, proof, airdrop_addr); let final_balance = strk_disp.balanceOf(user); // println!("final bal {}", final_balance); From d691ec5209d1f7b5d71b00d07567a7d65b3c13b1 Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 29 Oct 2024 22:44:02 +0100 Subject: [PATCH 42/44] Add explanation on owner check in docs --- docs/spotnet.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/spotnet.md b/docs/spotnet.md index b324e653..8c41d241 100644 --- a/docs/spotnet.md +++ b/docs/spotnet.md @@ -16,6 +16,7 @@ Onwer address is an account address of the user who deploys it, only owner can d ### loop_liquidity The `loop_liquidity` method is responsible for deposit of collateral token. For now only one position can be opened, to create the new you need to close old one. +For validating the identity of the caller is used an address of account initiated a transaction instead of a caller for testing purposes. This method has next parameters: * `deposit_data`: DepositData - Object of internal type which stores main deposit information. * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping tokens. @@ -49,7 +50,8 @@ emit event ``` ### close_position -The `close_position` method is responsible for repaying debts and withdrawing all tokens from zklend. Can be called only if there is active position. +The `close_position` method is responsible for repaying debts and withdrawing all tokens from zklend. Can be called only if there is active position. For validating the +identity of the caller is used an address of account initiated a transaction instead of a caller for testing purposes. The method has next parameters: * `supply_token`: ContractAddress - Address of the token used as collateral. * `debt_token`: ContractAddress - Address of the token used as borrowing. From 259e497632a42a66335b1ce8fe09d7df16d4d07e Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 30 Oct 2024 17:53:05 +0100 Subject: [PATCH 43/44] Renamings, docs changes --- docs/spotnet.md | 6 +++--- src/deposit.cairo | 25 +++++++++++-------------- src/types.cairo | 2 +- tests/test_loop.cairo | 7 ++----- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/spotnet.md b/docs/spotnet.md index 8c41d241..f7d874eb 100644 --- a/docs/spotnet.md +++ b/docs/spotnet.md @@ -21,7 +21,7 @@ This method has next parameters: * `deposit_data`: DepositData - Object of internal type which stores main deposit information. * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping tokens. * `ekubo_limits`: EkuboSlippageLimits - Object of internal type which represents upper and lower sqrt_ratio values on Ekubo. Used to control slippage while swapping. -* `pool_price`: felt252 - Price of `deposit` token in terms of `debt` token in Ekubo pool(so for ex. 2400000000 USDC for ETH when depositing ETH). +* `pool_price`: felt252 - Price of `deposit` token in terms of `debt` token(so for ex. 2400000000 USDC for ETH when depositing ETH). It's flow can be described as follows: @@ -57,8 +57,8 @@ The method has next parameters: * `debt_token`: ContractAddress - Address of the token used as borrowing. * `pool_key`: PoolKey - Ekubo type for obtaining info about the pool and swapping tokens. * `ekubo_limits`: EkuboSlippageLimits - Object of internal type which represents upper and lower sqrt_ratio values on Ekubo. Used to control slippage while swapping. -* `supply_price`: TokenPrice - Price of `supply` token in terms of `debt` token in Ekubo pool(so for ex. 2400000000 USDC for ETH). -* `debt_price`: TokenPrice - Price of `debt` token in terms of `supply` token in Ekubo pool(for ex. 410000000000000 ETH for USDC). +* `supply_price`: TokenPrice - Price of `supply` token in terms of `debt` token(so for ex. 2400000000 USDC for ETH). +* `debt_price`: TokenPrice - Price of `debt` token in terms of `supply` token(for ex. 410000000000000 ETH for USDC). It's flow can be described as follows: ``` diff --git a/src/deposit.cairo b/src/deposit.cairo index 9ab0a8fd..5a5eb543 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -16,7 +16,7 @@ mod Deposit { }, types::{ SwapData, SwapResult, DepositData, Claim, EkuboSlippageLimits, TokenAmount, TokenPrice, - Decimals + DecimalScale } }; @@ -48,7 +48,7 @@ mod Deposit { fn get_borrow_amount( borrow_capacity: TokenAmount, token_price: TokenPrice, - decimals_difference: Decimals, + decimals_difference: DecimalScale, total_borrowed: TokenAmount ) -> felt252 { let borrow_const = 60; @@ -64,8 +64,8 @@ mod Deposit { borrow_factor: felt252, supply_token_price: TokenPrice, debt_token_price: TokenPrice, - supply_decimals: Decimals, - debt_decimals: Decimals + supply_decimals: DecimalScale, + debt_decimals: DecimalScale ) -> u256 { let deposited = (total_deposited * supply_token_price.into()).into() / supply_decimals.into(); @@ -126,8 +126,7 @@ mod Deposit { /// * `pool_key`: PoolKey - Ekubo type which represents data about pool. /// * `ekubo_limits`: EkuboSlippageLimits - Represents upper and lower sqrt_ratio values on /// Ekubo. Used to control slippage while swapping. - /// * `pool_price`: TokenPrice - Price of `deposit` token in terms of `debt` token in Ekubo - /// pool. + /// * `pool_price`: TokenPrice - Price of `deposit` token in terms of `debt` token. fn loop_liquidity( ref self: ContractState, deposit_data: DepositData, @@ -148,7 +147,7 @@ mod Deposit { let curr_contract_address = get_contract_address(); assert( token_dispatcher.allowance(user_acount, curr_contract_address) >= amount, - 'Approved amount incuficient' + 'Approved amount insufficient' ); assert(token_dispatcher.balanceOf(user_acount) >= amount, 'Insufficient balance'); @@ -174,7 +173,9 @@ mod Deposit { token_dispatcher.approve(zk_market.contract_address, amount); zk_market.deposit(token, amount.try_into().expect('Overflow')); - let (mut total_borrowed, mut accumulated, mut deposited) = (0, 0, amount); + let mut total_borrowed = 0; + let mut accumulated = 0; + let mut deposited = amount; while (amount + accumulated) / amount < multiplier.into() { let borrow_capacity = ((deposited * collateral_factor / ZK_SCALE_DECIMALS) @@ -241,10 +242,8 @@ mod Deposit { /// tokens. /// * `ekubo_limits`: EkuboSlippageLimits - Represents upper and lower sqrt_ratio values on /// Ekubo. Used to control slippage while swapping. - /// * `supply_price`: TokenPrice - Price of `supply` token in terms of `debt` token in Ekubo - /// pool. - /// * `debt_price`: TokenPrice - Price of `debt` token in terms of `supply` token in Ekubo - /// pool. + /// * `supply_price`: TokenPrice - Price of `supply` token in terms of `debt` token. + /// * `debt_price`: TokenPrice - Price of `debt` token in terms of `supply` token. fn close_position( ref self: ContractState, supply_token: ContractAddress, @@ -371,8 +370,6 @@ mod Deposit { proof: Span, airdrop_addr: ContractAddress ) { - assert(self.is_position_open.read(), 'Open position not exists'); - assert(proof.len() != 0, 'Proof Span cannot be empty'); assert( IAirdropDispatcher { contract_address: airdrop_addr }.claim(claim_data, proof), 'Claim failed' diff --git a/src/types.cairo b/src/types.cairo index e824beaa..1dec9eb4 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -6,7 +6,7 @@ use starknet::ContractAddress; pub type TokenPrice = u128; pub type TokenAmount = u256; -pub type Decimals = u64; +pub type DecimalScale = u64; #[derive(Copy, Drop, Serde)] pub struct SwapResult { diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 07ba16e5..76ba214d 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -432,10 +432,7 @@ fn test_close_position_usdc_valid_time_passed() { start_cheat_block_timestamp( contracts::ZKLEND_MARKET.try_into().unwrap(), get_block_timestamp() + 40000000 ); - // println!("Debt {}", zk_market.get_user_debt_for_token(deposit_disp.contract_address, - // eth_addr)); - // println!("Z bal {}", ERC20ABIDispatcher {contract_address: - // usdc_reserve.z_token_address}.balanceOf(deposit_disp.contract_address)); + deposit_disp .close_position( usdc_addr, @@ -448,7 +445,7 @@ fn test_close_position_usdc_valid_time_passed() { stop_cheat_block_timestamp(contracts::ZKLEND_MARKET.try_into().unwrap()); stop_cheat_account_contract_address(deposit_disp.contract_address); - // println!("After bal {}", token_disp.balanceOf(user)); + assert(token_disp.balanceOf(user) > initial_balance, 'Balance is in wrong state'); } From 0b7f0af76688cca9683c39a6d053f92740c6a439 Mon Sep 17 00:00:00 2001 From: faurdent Date: Thu, 31 Oct 2024 10:21:20 +0100 Subject: [PATCH 44/44] Borrow const moved to Deposit Data. Docs and tests updated --- docs/spotnet.md | 11 ++++++++++- src/deposit.cairo | 24 ++++++++++++------------ src/types.cairo | 3 ++- tests/test_loop.cairo | 25 +++++++++++++------------ 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/docs/spotnet.md b/docs/spotnet.md index f7d874eb..24a9f880 100644 --- a/docs/spotnet.md +++ b/docs/spotnet.md @@ -100,14 +100,23 @@ transfer half of reward to the treasury ## Important types, events and constants ### Types +#### DepositData +The main data about the loop to perform. The `amount` * `multiplier` value is minimal amount that will be deposited after the loop. +The `borrow_const` sets how much tokens will be borrowed from available amount(borrowing power). So if there is available 1 ETH to borrow and we passed 60%, it will borrow 0.6 ETH. +This will work up to 99% of available amount, howewer, for stability against slippage and prices difference on zkLend and our source it's better to not go higher than 98%. + ``` struct DepositData { token: ContractAddress, amount: TokenAmount, - multiplier: u32 + multiplier: u32, + borrow_const: u8 } ``` +#### EkuboSlippageLimits +This structure sets slippage limits for swapping on Ekubo. Maximal are `6277100250585753475930931601400621808602321654880405518632` for `upper` and `18446748437148339061` for `lower` as stated by [Ekubo docs](https://docs.ekubo.org/integration-guides/reference/error-codes#limit_mag). + ### Events ``` struct LiquidityLooped { diff --git a/src/deposit.cairo b/src/deposit.cairo index 5a5eb543..5ad8bdfc 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -49,12 +49,12 @@ mod Deposit { borrow_capacity: TokenAmount, token_price: TokenPrice, decimals_difference: DecimalScale, - total_borrowed: TokenAmount + total_borrowed: TokenAmount, + borrow_const: u8 ) -> felt252 { - let borrow_const = 60; let amount_base_token = token_price.into() * borrow_capacity; let amount_quote_token = amount_base_token / decimals_difference.into(); - ((amount_quote_token - total_borrowed) / 100_u256 * borrow_const).try_into().unwrap() + ((amount_quote_token - total_borrowed) / 100_u256 * borrow_const.into()).try_into().unwrap() } fn get_withdraw_amount( @@ -111,7 +111,6 @@ mod Deposit { } #[abi(embed_v0)] - // TODO: Change types to aliases impl Deposit of IDeposit { /// Loops collateral token on ZKlend. /// @@ -137,13 +136,14 @@ mod Deposit { let user_acount = get_tx_info().unbox().account_contract_address; assert(user_acount == self.owner.read(), 'Caller is not the owner'); assert(!self.is_position_open.read(), 'Open position already exists'); - let DepositData { token, amount, multiplier } = deposit_data; - let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; - let deposit_token_decimals = fast_power(10_u64, token_dispatcher.decimals().into()); - - assert(multiplier < 5 && multiplier > 1, 'Multiplier not supported'); + let DepositData { token, amount, multiplier, borrow_const } = deposit_data; + assert(borrow_const > 0 && borrow_const < 100, 'Cannot calculate borrow amount'); + assert(multiplier < 6 && multiplier > 1, 'Multiplier not supported'); assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); + let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; + let deposit_token_decimals = fast_power(10_u64, token_dispatcher.decimals().into()); + let curr_contract_address = get_contract_address(); assert( token_dispatcher.allowance(user_acount, curr_contract_address) >= amount, @@ -152,7 +152,7 @@ mod Deposit { assert(token_dispatcher.balanceOf(user_acount) >= amount, 'Insufficient balance'); let zk_market = self.zk_market.read(); - // let is_token1 = token == pool_key.token0; + let (is_token1, borrowing_token, sqrt_limit) = if token == pool_key.token0 { (true, pool_key.token1, ekubo_limits.upper) } else { @@ -185,7 +185,8 @@ mod Deposit { borrow_capacity.try_into().unwrap(), pool_price, deposit_token_decimals.into(), - total_borrowed + total_borrowed, + borrow_const ); total_borrowed += to_borrow.into(); zk_market.borrow(borrowing_token, to_borrow); @@ -208,7 +209,6 @@ mod Deposit { deposited += amount_swapped.into(); accumulated += amount_swapped.into(); }; - self.is_position_open.write(true); self .emit( diff --git a/src/types.cairo b/src/types.cairo index 1dec9eb4..eeb22cd8 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -24,7 +24,8 @@ pub struct SwapData { pub struct DepositData { pub token: ContractAddress, pub amount: TokenAmount, - pub multiplier: u32 + pub multiplier: u32, + pub borrow_const: u8 } #[derive(Copy, Drop, Serde)] diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 76ba214d..93e1c0c9 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -113,7 +113,7 @@ fn test_loop_eth_valid() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4 }, + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price @@ -156,7 +156,7 @@ fn test_loop_eth_fuzz(amount: u64) { start_cheat_account_contract_address(deposit_disp.contract_address, user); if let Result::Err(panic_data) = deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: amount.into(), multiplier: 4 }, + DepositData { token: eth_addr, amount: amount.into(), multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price @@ -203,6 +203,7 @@ fn test_loop_usdc_valid() { .into() ); let pool_price = 1 * decimals_sum_power.into() / get_asset_price_pragma('ETH/USD'); + let deposit_disp = get_deposit_dispatcher(user); start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); token_disp.approve(deposit_disp.contract_address, 60000000); @@ -211,7 +212,7 @@ fn test_loop_usdc_valid() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -255,7 +256,7 @@ fn test_loop_unauthorized() { disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 10000000, multiplier: 4 }, + DepositData { token: usdc_addr, amount: 10000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -301,14 +302,14 @@ fn test_loop_position_exists() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price.into() ); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 60000000, multiplier: 4 }, + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -352,7 +353,7 @@ fn test_loop_position_exists_fuzz(amount: u64) { if let Result::Err(_) = deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, + DepositData { token: eth_addr, amount: amount.into(), multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price @@ -361,7 +362,7 @@ fn test_loop_position_exists_fuzz(amount: u64) { }; match deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: amount.into(), multiplier: 2 }, + DepositData { token: eth_addr, amount: amount.into(), multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price @@ -416,7 +417,7 @@ fn test_close_position_usdc_valid_time_passed() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4 }, + DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price @@ -489,7 +490,7 @@ fn test_close_position_amounts_cleared() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4 }, + DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price @@ -524,7 +525,7 @@ fn test_close_position_amounts_cleared() { } #[test] -#[fork(url: "http://127.0.0.1:5050", block_number: 834899)] +#[fork("MAINNET")] fn test_claim_rewards() { let strk_addr: ContractAddress = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d @@ -589,7 +590,7 @@ fn test_claim_rewards() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4 }, + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price