From 5dc1d096843faf1ff0cbca4cf6fde0034e588378 Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 30 Oct 2024 14:10:24 +0100 Subject: [PATCH 01/18] Methods for extra depositing and withdrawing added --- src/deposit.cairo | 53 +++++++++++++++++++++++++++++++++++++++++++- src/interfaces.cairo | 8 ++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 9ab0a8fd..dd90bfa1 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -153,7 +153,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 { @@ -379,6 +379,57 @@ mod Deposit { ); // TODO: Add transfer to the Treasury } + + /// Makes a deposit into open zkLend position to control stability + /// + /// # Panics + /// `is_position_open` variable is set to false + /// `amount` is equal to zero + /// + /// # Parameters + /// `token`: ContractAddress - token address to withdraw from zkLend + /// `amount`: TokenAmount - amount to withdraw + fn extra_deposit(ref self: ContractState, token: ContractAddress, amount: TokenAmount) { + assert(self.is_position_open.read(), 'Open position not exists'); + assert(amount != 0, 'Deposit amount is zero'); + ERC20ABIDispatcher { contract_address: token } + .transferFrom( + get_tx_info().unbox().account_contract_address, get_contract_address(), amount + ); + self.zk_market.read().deposit(token, amount.try_into().unwrap()); + } + + /// Withdraws tokens from zkLend if looped tokens are repaid + /// + /// # Panics + /// address of account that started the transaction is not equal to `owner` storage variable + /// if trying to withdraw from open position, so `is_position_open` is set to true + /// + /// # Parameters + /// `token`: TokenAddress - token address to withdraw from zkLend + /// `amount`: TokenAmount - amount to withdraw. Pass `0` to withdraw all + fn withdraw(ref self: ContractState, token: ContractAddress, amount: TokenAmount) { + assert( + get_tx_info().unbox().account_contract_address == self.owner.read(), + 'Caller is not the owner' + ); + assert(!self.is_position_open.read(), 'Tokens are locked'); + let zk_market = self.zk_market.read(); + if amount == 0 { + zk_market.withdraw_all(token); + ERC20ABIDispatcher { contract_address: token } + .transfer( + self.owner.read(), + ERC20ABIDispatcher { + contract_address: zk_market.get_reserve_data(token).z_token_address + } + .balanceOf(get_contract_address()) + ); + } else { + self.zk_market.read().withdraw(token, amount.try_into().unwrap()); + ERC20ABIDispatcher { contract_address: token }.transfer(self.owner.read(), amount); + }; + } } #[abi(embed_v0)] diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 0dc3e61f..1ae081a9 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -1,5 +1,7 @@ use ekubo::types::keys::PoolKey; -use spotnet::types::{MarketReserveData, DepositData, Claim, EkuboSlippageLimits, TokenPrice}; +use spotnet::types::{ + MarketReserveData, DepositData, Claim, EkuboSlippageLimits, TokenPrice, TokenAmount +}; use starknet::ContractAddress; #[starknet::interface] @@ -28,6 +30,10 @@ pub trait IDeposit { proof: Span, airdrop_addr: ContractAddress ); + + fn extra_deposit(ref self: TContractState, token: ContractAddress, amount: TokenAmount); + + fn withdraw(ref self: TContractState, token: ContractAddress, amount: TokenAmount); } #[starknet::interface] From 224ac5c8592470ee9ac64963b540da37aaa1864b Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 30 Oct 2024 17:42:16 +0100 Subject: [PATCH 02/18] Tests for deposit and withdraw --- src/deposit.cairo | 17 ++- tests/test_loop.cairo | 261 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 267 insertions(+), 11 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index dd90bfa1..daa4cf76 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -21,7 +21,7 @@ mod Deposit { }; use starknet::{ - ContractAddress, get_contract_address, get_tx_info, event::EventEmitter, + ContractAddress, get_contract_address, get_caller_address, get_tx_info, event::EventEmitter, storage::{StoragePointerWriteAccess, StoragePointerReadAccess} }; @@ -392,11 +392,12 @@ mod Deposit { fn extra_deposit(ref self: ContractState, token: ContractAddress, amount: TokenAmount) { assert(self.is_position_open.read(), 'Open position not exists'); assert(amount != 0, 'Deposit amount is zero'); - ERC20ABIDispatcher { contract_address: token } - .transferFrom( - get_tx_info().unbox().account_contract_address, get_contract_address(), amount - ); - self.zk_market.read().deposit(token, amount.try_into().unwrap()); + let (zk_market, token_dispatcher) = ( + self.zk_market.read(), ERC20ABIDispatcher { contract_address: token } + ); + token_dispatcher.transferFrom(get_caller_address(), get_contract_address(), amount); + token_dispatcher.approve(zk_market.contract_address, amount); + zk_market.deposit(token, amount.try_into().unwrap()); } /// Withdraws tokens from zkLend if looped tokens are repaid @@ -420,9 +421,7 @@ mod Deposit { ERC20ABIDispatcher { contract_address: token } .transfer( self.owner.read(), - ERC20ABIDispatcher { - contract_address: zk_market.get_reserve_data(token).z_token_address - } + ERC20ABIDispatcher { contract_address: token } .balanceOf(get_contract_address()) ); } else { diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 07ba16e5..d8837d78 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -526,6 +526,265 @@ fn test_close_position_amounts_cleared() { ); } +#[test] +#[fork("MAINNET")] +fn test_extra_deposit_valid() { + 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, + get_slippage_limits(pool_key), + 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_caller_address(eth_addr, user); + ERC20ABIDispatcher { contract_address: eth_addr } + .approve(deposit_disp.contract_address, 10000000000000); + stop_cheat_caller_address(eth_addr); + + start_cheat_caller_address(deposit_disp.contract_address, user); + deposit_disp.extra_deposit(eth_addr, 10000000000000); + stop_cheat_caller_address(deposit_disp.contract_address); + + assert( + ERC20ABIDispatcher { + contract_address: zk_market.get_reserve_data(eth_addr).z_token_address + } + .balanceOf(deposit_disp.contract_address) != 0, + 'Tokens not deposited' + ); +} + +#[test] +#[fuzzer(runs: 10)] // TODO: Move to global config +#[fork("MAINNET")] +fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { + 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, + get_slippage_limits(pool_key), + 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_caller_address(usdc_addr, user); + ERC20ABIDispatcher { contract_address: usdc_addr } + .approve(deposit_disp.contract_address, extra_amount.into()); + stop_cheat_caller_address(usdc_addr); + let safe_deposit_disp = IDepositSafeDispatcher { + contract_address: deposit_disp.contract_address + }; + start_cheat_caller_address(deposit_disp.contract_address, user); + if let Result::Err(panic_data) = safe_deposit_disp + .extra_deposit(usdc_addr, extra_amount.into()) { + assert(*panic_data.at(0) == 'Deposit amount is zero', *panic_data.at(0)); + return; + } + stop_cheat_caller_address(deposit_disp.contract_address); + + 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 + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + assert( + ERC20ABIDispatcher { + contract_address: zk_market.get_reserve_data(usdc_addr).z_token_address + } + .balanceOf(deposit_disp.contract_address) == 0, + 'Not all withdrawn' + ); +} + +#[test] +#[fuzzer(runs: 10)] +#[fork("MAINNET")] +fn test_withdraw_valid_fuzz(amount: u32) { + 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, + get_slippage_limits(pool_key), + pool_price + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + let zk_market = IMarketTestingDispatcher { + contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() + }; + let eth_disp = ERC20ABIDispatcher { contract_address: eth_addr }; + start_cheat_caller_address(eth_addr, user); + eth_disp.approve(deposit_disp.contract_address, 10000000000000); + stop_cheat_caller_address(eth_addr); + + start_cheat_caller_address(deposit_disp.contract_address, user); + deposit_disp.extra_deposit(eth_addr, 10000000000000); + stop_cheat_caller_address(deposit_disp.contract_address); + + 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 + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + let z_eth_disp = ERC20ABIDispatcher { + contract_address: zk_market.get_reserve_data(eth_addr).z_token_address + }; + + let contract_pre_balance = z_eth_disp.balanceOf(deposit_disp.contract_address); + let to_withdraw_expected = if amount == 0 { + contract_pre_balance + } else { + amount.into() + }; + + let user_pre_balance = eth_disp.balanceOf(user); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp.withdraw(eth_addr, amount.into()); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + if amount == 0 { + assert(z_eth_disp.balanceOf(deposit_disp.contract_address) == 0, 'Wrong contract balance'); + } else { + // Z Token balance may increase, making equation not strict + assert( + contract_pre_balance + - z_eth_disp.balanceOf(deposit_disp.contract_address) <= amount.into(), + 'Wrong contract balance' + ); + }; + assert( + user_pre_balance + to_withdraw_expected == eth_disp.balanceOf(user), + 'Wrong amount withdrawn' + ); +} + #[test] #[fork(url: "http://127.0.0.1:5050", block_number: 834899)] fn test_claim_rewards() { @@ -613,8 +872,6 @@ fn test_claim_rewards() { 'Unexpected amount was rewarded' ); } -// TODO: Calculate interest rates to test behaviour after liquidation. - // #[test] // #[fork("MAINNET")] // fn test_full_liquidation() { From b937de7dd22253e6afdab77d9da68950875366ff Mon Sep 17 00:00:00 2001 From: faurdent Date: Thu, 31 Oct 2024 15:55:44 +0100 Subject: [PATCH 03/18] Using caller address when swap not called. New tests --- src/deposit.cairo | 16 +++--- tests/test_loop.cairo | 122 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 17 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index daa4cf76..e85f17a2 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -410,23 +410,19 @@ mod Deposit { /// `token`: TokenAddress - token address to withdraw from zkLend /// `amount`: TokenAmount - amount to withdraw. Pass `0` to withdraw all fn withdraw(ref self: ContractState, token: ContractAddress, amount: TokenAmount) { - assert( - get_tx_info().unbox().account_contract_address == self.owner.read(), - 'Caller is not the owner' - ); + assert(get_caller_address() == self.owner.read(), 'Caller is not the owner'); assert(!self.is_position_open.read(), 'Tokens are locked'); let zk_market = self.zk_market.read(); + let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; if amount == 0 { zk_market.withdraw_all(token); - ERC20ABIDispatcher { contract_address: token } + token_dispatcher .transfer( - self.owner.read(), - ERC20ABIDispatcher { contract_address: token } - .balanceOf(get_contract_address()) + self.owner.read(), token_dispatcher.balanceOf(get_contract_address()) ); } else { - self.zk_market.read().withdraw(token, amount.try_into().unwrap()); - ERC20ABIDispatcher { contract_address: token }.transfer(self.owner.read(), amount); + zk_market.withdraw(token, amount.try_into().unwrap()); + token_dispatcher.transfer(self.owner.read(), amount); }; } } diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index d8837d78..f8cd768c 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -422,11 +422,6 @@ 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 usdc_reserve = zk_market.get_reserve_data(usdc_addr); - let eth_reserve = zk_market.get_reserve_data(eth_addr); start_cheat_account_contract_address(deposit_disp.contract_address, user); start_cheat_block_timestamp( @@ -765,9 +760,9 @@ fn test_withdraw_valid_fuzz(amount: u32) { let user_pre_balance = eth_disp.balanceOf(user); - start_cheat_account_contract_address(deposit_disp.contract_address, user); + start_cheat_caller_address(deposit_disp.contract_address, user); deposit_disp.withdraw(eth_addr, amount.into()); - stop_cheat_account_contract_address(deposit_disp.contract_address); + stop_cheat_caller_address(deposit_disp.contract_address); if amount == 0 { assert(z_eth_disp.balanceOf(deposit_disp.contract_address) == 0, 'Wrong contract balance'); @@ -785,6 +780,119 @@ fn test_withdraw_valid_fuzz(amount: u32) { ); } +#[test] +#[should_panic(expected: 'Open position not exists')] +#[fork("MAINNET")] +fn test_extra_deposit_position_not_exists() { + let eth_addr: ContractAddress = + 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff + .try_into() + .unwrap(); + + 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, 100000000000); + deposit_disp.extra_deposit(eth_addr, 100000000000); + stop_cheat_caller_address(eth_addr); +} + +#[test] +#[should_panic(expected: 'Deposit amount is zero')] +#[fork("MAINNET")] +fn test_extra_deposit_position_zero_amount() { + 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_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); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp + .loop_liquidity( + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2 }, + pool_key, + get_slippage_limits(pool_key), + pool_price + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + start_cheat_caller_address(eth_addr.try_into().unwrap(), user); + deposit_disp.extra_deposit(eth_addr, 0); + stop_cheat_caller_address(eth_addr); +} + +#[test] +#[should_panic(expected: 'Tokens are locked')] +#[fork("MAINNET")] +fn test_withdraw_position_open() { + 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_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); + + start_cheat_account_contract_address(deposit_disp.contract_address, user); + deposit_disp + .loop_liquidity( + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2 }, + pool_key, + get_slippage_limits(pool_key), + pool_price + ); + stop_cheat_account_contract_address(deposit_disp.contract_address); + + start_cheat_caller_address(deposit_disp.contract_address, user); + deposit_disp.withdraw(eth_addr, 100000000000000); + stop_cheat_caller_address(deposit_disp.contract_address); +} + #[test] #[fork(url: "http://127.0.0.1:5050", block_number: 834899)] fn test_claim_rewards() { From 9ea2e59a6795e6bf081ba3cf3e2e33d18f66669a Mon Sep 17 00:00:00 2001 From: faurdent Date: Fri, 1 Nov 2024 11:16:10 +0100 Subject: [PATCH 04/18] Added borrow const to new tests --- tests/test_loop.cairo | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 14e2a370..354ff72f 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -559,7 +559,7 @@ fn test_extra_deposit_valid() { 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 @@ -628,7 +628,7 @@ fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { 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 @@ -715,7 +715,7 @@ fn test_withdraw_valid_fuzz(amount: u32) { 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 @@ -833,7 +833,7 @@ fn test_extra_deposit_position_zero_amount() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2 }, + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price @@ -879,7 +879,7 @@ fn test_withdraw_position_open() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2 }, + DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price From a3c724df4f51ca390a5ce1737535ff5eabedc650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Fri, 1 Nov 2024 14:47:31 +0100 Subject: [PATCH 05/18] Implement functionality, add happy case fork test --- Scarb.toml | 5 ++++ src/constants.cairo | 1 + src/deposit.cairo | 25 ++++++++++++---- tests/lib.cairo | 3 ++ tests/test_defispring.cairo | 57 +++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 tests/test_defispring.cairo diff --git a/Scarb.toml b/Scarb.toml index 0b9d8037..c82ad6ff 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -32,3 +32,8 @@ block_id.tag = "latest" name = "MAINNET" url = "http://127.0.0.1:5050" block_id.tag = "latest" + +[[tool.snforge.fork]] +name = "MAINNET_FIXED_BLOCK" +url = "http://127.0.0.1:5050" +block_id.number = "863242" \ No newline at end of file diff --git a/src/constants.cairo b/src/constants.cairo index 5268c154..6e45c63a 100644 --- a/src/constants.cairo +++ b/src/constants.cairo @@ -1 +1,2 @@ pub const ZK_SCALE_DECIMALS: u256 = 1000000000000000000000000000; +pub const STRK_ADDRESS: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; \ No newline at end of file diff --git a/src/deposit.cairo b/src/deposit.cairo index 5f721c72..9651f334 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -9,7 +9,7 @@ mod Deposit { use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use spotnet::{ - constants::ZK_SCALE_DECIMALS, + constants::{ZK_SCALE_DECIMALS, STRK_ADDRESS}, interfaces::{ IMarketDispatcher, IMarketDispatcherTrait, IAirdropDispatcher, IAirdropDispatcherTrait, IDeposit @@ -30,6 +30,7 @@ mod Deposit { owner: ContractAddress, ekubo_core: ICoreDispatcher, zk_market: IMarketDispatcher, + treasury: ContractAddress, is_position_open: bool } @@ -38,11 +39,13 @@ mod Deposit { ref self: ContractState, owner: ContractAddress, ekubo_core: ICoreDispatcher, - zk_market: IMarketDispatcher + zk_market: IMarketDispatcher, + treasury: ContractAddress ) { self.owner.write(owner); self.ekubo_core.write(ekubo_core); self.zk_market.write(zk_market); + self.treasury.write(treasury); } fn get_borrow_amount( @@ -355,6 +358,8 @@ mod Deposit { /// Claims STRK airdrop on ZKlend /// + /// Can be called by anyone, e.g. a keeper + /// /// # Panics /// `is_position_open` storage variable is set to false('Open position not exists') /// `proof` span is empty @@ -374,10 +379,22 @@ mod Deposit { IAirdropDispatcher { contract_address: airdrop_addr }.claim(claim_data, proof), 'Claim failed' ); - // TODO: Add transfer to the Treasury + + let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; + let zk_market = self.zk_market.read(); + let half_of_claim = claim_data.amount / 2; // u128 integer division, rounds down + + strk.transfer(self.treasury.read(), half_of_claim.into()); + + let remainder = claim_data.amount - half_of_claim; + strk.approve(zk_market.contract_address, remainder.into()); + zk_market.deposit(STRK_ADDRESS.try_into().unwrap(), remainder.into()); + zk_market.enable_collateral(STRK_ADDRESS.try_into().unwrap()); } /// Makes a deposit into open zkLend position to control stability + /// + /// Anyone can deposit theoretically /// /// # Panics /// `is_position_open` variable is set to false @@ -401,14 +418,12 @@ mod Deposit { /// /// # Panics /// address of account that started the transaction is not equal to `owner` storage variable - /// if trying to withdraw from open position, so `is_position_open` is set to true /// /// # Parameters /// `token`: TokenAddress - token address to withdraw from zkLend /// `amount`: TokenAmount - amount to withdraw. Pass `0` to withdraw all fn withdraw(ref self: ContractState, token: ContractAddress, amount: TokenAmount) { assert(get_caller_address() == self.owner.read(), 'Caller is not the owner'); - assert(!self.is_position_open.read(), 'Tokens are locked'); let zk_market = self.zk_market.read(); let token_dispatcher = ERC20ABIDispatcher { contract_address: token }; if amount == 0 { diff --git a/tests/lib.cairo b/tests/lib.cairo index 52b577c3..36d0c5e3 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -2,3 +2,6 @@ pub mod interfaces; #[cfg(test)] mod test_loop; + +#[cfg(test)] +mod test_defispring; \ No newline at end of file diff --git a/tests/test_defispring.cairo b/tests/test_defispring.cairo new file mode 100644 index 00000000..b096bcc4 --- /dev/null +++ b/tests/test_defispring.cairo @@ -0,0 +1,57 @@ +use snforge_std::{declare, DeclareResultTrait, ContractClassTrait, replace_bytecode, store}; +use starknet::ContractAddress; +use spotnet::interfaces::{ + IDepositDispatcher, IDepositDispatcherTrait +}; +use spotnet::types::Claim; +use spotnet::constants::STRK_ADDRESS; +use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + +#[test] +#[fork("MAINNET_FIXED_BLOCK")] +fn test_claim_as_keeper() { + let defispring_claim_contract: ContractAddress = 0x2d55d6f311413945595788818d4e89e151360a2c2c6b5270d5d0ed16475505f.try_into().unwrap(); + + let address_eligible_for_zklend_rewards: ContractAddress = 0x020281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af.try_into().unwrap(); + let contract = declare("Deposit").unwrap().contract_class(); + replace_bytecode(address_eligible_for_zklend_rewards, *contract.class_hash).unwrap(); + + // write the treasury address so we can check funds were sent + let hypothetical_treasury_address = 0x98765; + let storage_entry_for_treasury_address = array![hypothetical_treasury_address].span(); + store(address_eligible_for_zklend_rewards, selector!("treasury"), storage_entry_for_treasury_address); + let ZKLEND_MARKET = 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; + let storage_entry_for_zk_market = array![ZKLEND_MARKET].span(); + store(address_eligible_for_zklend_rewards, selector!("zk_market"), storage_entry_for_zk_market); + + let deposit_contract = IDepositDispatcher {contract_address: address_eligible_for_zklend_rewards}; + let proof = array![ + 0x43a677604d8a532f023b8a1480e39f0a4f95460a88eb978bf86cf2e6af4a505, + 0x69faedf42e0dccc605c8f5b773c58154bd51f1d807ce51d6a254b58379df414, + 0x75afcd7c6775bd043279c5adf5cfc8519175ddb640d9bab3a80d6216fc434f2, + 0x718d5326a3a934d067b4930ff2ffbc6dba50eb189ddecc50d559c74e30ce375, + 0x60842dbbced8d585d720c3efe1f99fb32da09f6334f3ef679dbfbc9b47fcf2b, + 0x7d22c5040360327dc761eb46959c15f888b68af777829d758150612ec13949c, + 0x406de13ffdac0138c360921e9e51bb7fdabe9770c750157e40e04909589c0e7, + 0x4f138575a80804622f8b92152ffaf19e634c63934348d13b92dd1eb91bfa3c, + 0x1ca16be8f87a5dc8cbdd781a8e2e37b047ccdc0552ea048f8cbc28b6e0e9621, + 0x6e5d1a64f19a0a541716f701413ae2bace2151b83da7b231ebb347c2be8272b, + 0xf1e537b49ce8629386bfab3e390bb5dee770e0c8a176115b82607f9d9fa441, + 0x517330339d2b79fff83a99bfa17d974267ff1a49fa517bda0ec6105130d15e1, + 0x17230a4e2cb1bc3fb6a83c165e9f8f719c5198065e02d4aac7c55b75ac92fc0, + 0x787a4ca028ae34239a07b4d023f4ef785c9aa934da05a0fd2ec1310dc1a6d83 + ]; + let claim: Claim = Claim {id: 11051, claimee: address_eligible_for_zklend_rewards, amount: 0x2a52c411698a729}; + // eligible for 0x2a52c411698a729 = 190607217296713513 fri (fri is lowest denominator of strk token) + // treasury should get 95303608648356756 which is half, rounded down + + + deposit_contract.claim_reward(claim, proof.span(), defispring_claim_contract); + + + let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; + let fri_in_treasury = strk.balance_of(hypothetical_treasury_address.try_into().unwrap()); + assert(fri_in_treasury == 95303608648356756, 'incorrect amount in treasury'); + let strk_left_in_contract = strk.balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); + assert!(strk_left_in_contract == 0, "strk left in contract after airdrop claim"); +} \ No newline at end of file From b55241ca211d7ad5926320c6d3f91eede554fc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Fri, 1 Nov 2024 14:56:40 +0100 Subject: [PATCH 06/18] Fix failing happy case test, forgot that eligible address has some STRK on it --- tests/test_defispring.cairo | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_defispring.cairo b/tests/test_defispring.cairo index b096bcc4..c52acbd0 100644 --- a/tests/test_defispring.cairo +++ b/tests/test_defispring.cairo @@ -10,12 +10,15 @@ use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatche #[test] #[fork("MAINNET_FIXED_BLOCK")] fn test_claim_as_keeper() { + let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; let defispring_claim_contract: ContractAddress = 0x2d55d6f311413945595788818d4e89e151360a2c2c6b5270d5d0ed16475505f.try_into().unwrap(); let address_eligible_for_zklend_rewards: ContractAddress = 0x020281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af.try_into().unwrap(); let contract = declare("Deposit").unwrap().contract_class(); replace_bytecode(address_eligible_for_zklend_rewards, *contract.class_hash).unwrap(); + let strk_balance_at_start = strk.balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); + // write the treasury address so we can check funds were sent let hypothetical_treasury_address = 0x98765; let storage_entry_for_treasury_address = array![hypothetical_treasury_address].span(); @@ -49,9 +52,8 @@ fn test_claim_as_keeper() { deposit_contract.claim_reward(claim, proof.span(), defispring_claim_contract); - let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; let fri_in_treasury = strk.balance_of(hypothetical_treasury_address.try_into().unwrap()); assert(fri_in_treasury == 95303608648356756, 'incorrect amount in treasury'); let strk_left_in_contract = strk.balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); - assert!(strk_left_in_contract == 0, "strk left in contract after airdrop claim"); + assert!(strk_left_in_contract == strk_balance_at_start, "strk left in contract after airdrop claim"); } \ No newline at end of file From ca0f41c32a29906cc404dd48981c74cea01033ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Sat, 2 Nov 2024 21:35:17 +0100 Subject: [PATCH 07/18] Fix typo --- src/deposit.cairo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 9651f334..51d5f491 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -136,8 +136,8 @@ mod Deposit { ekubo_limits: EkuboSlippageLimits, pool_price: TokenPrice ) { - let user_acount = get_tx_info().unbox().account_contract_address; - assert(user_acount == self.owner.read(), 'Caller is not the owner'); + let user_account = get_tx_info().unbox().account_contract_address; + assert(user_account == self.owner.read(), 'Caller is not the owner'); assert(!self.is_position_open.read(), 'Open position already exists'); let DepositData { token, amount, multiplier, borrow_const } = deposit_data; assert(borrow_const > 0 && borrow_const < 100, 'Cannot calculate borrow amount'); @@ -149,10 +149,10 @@ mod Deposit { let curr_contract_address = get_contract_address(); assert( - token_dispatcher.allowance(user_acount, curr_contract_address) >= amount, + token_dispatcher.allowance(user_account, curr_contract_address) >= amount, 'Approved amount insufficient' ); - assert(token_dispatcher.balanceOf(user_acount) >= amount, 'Insufficient balance'); + assert(token_dispatcher.balanceOf(user_account) >= amount, 'Insufficient balance'); let zk_market = self.zk_market.read(); From 9c7ec0240d379f46e656ee10a20c2d74cd4306ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Mon, 4 Nov 2024 11:06:52 +0100 Subject: [PATCH 08/18] Add withdrawal claim+withdraw test --- tests/test_defispring.cairo | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/test_defispring.cairo b/tests/test_defispring.cairo index c52acbd0..d8d3742e 100644 --- a/tests/test_defispring.cairo +++ b/tests/test_defispring.cairo @@ -1,4 +1,4 @@ -use snforge_std::{declare, DeclareResultTrait, ContractClassTrait, replace_bytecode, store}; +use snforge_std::{declare, DeclareResultTrait, replace_bytecode, store, cheat_caller_address, CheatSpan}; use starknet::ContractAddress; use spotnet::interfaces::{ IDepositDispatcher, IDepositDispatcherTrait @@ -7,13 +7,16 @@ use spotnet::types::Claim; use spotnet::constants::STRK_ADDRESS; use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +const ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS: felt252 = 0x020281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af; +const HYPOTHETICAL_OWNER_ADDR: felt252 = 0x56789; + #[test] #[fork("MAINNET_FIXED_BLOCK")] fn test_claim_as_keeper() { let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; let defispring_claim_contract: ContractAddress = 0x2d55d6f311413945595788818d4e89e151360a2c2c6b5270d5d0ed16475505f.try_into().unwrap(); - let address_eligible_for_zklend_rewards: ContractAddress = 0x020281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af.try_into().unwrap(); + let address_eligible_for_zklend_rewards: ContractAddress = ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS.try_into().unwrap(); let contract = declare("Deposit").unwrap().contract_class(); replace_bytecode(address_eligible_for_zklend_rewards, *contract.class_hash).unwrap(); @@ -46,14 +49,33 @@ fn test_claim_as_keeper() { ]; let claim: Claim = Claim {id: 11051, claimee: address_eligible_for_zklend_rewards, amount: 0x2a52c411698a729}; // eligible for 0x2a52c411698a729 = 190607217296713513 fri (fri is lowest denominator of strk token) - // treasury should get 95303608648356756 which is half, rounded down + // treasury should get 152485773837370811 which is 80 % deposit_contract.claim_reward(claim, proof.span(), defispring_claim_contract); let fri_in_treasury = strk.balance_of(hypothetical_treasury_address.try_into().unwrap()); - assert(fri_in_treasury == 95303608648356756, 'incorrect amount in treasury'); + println!("Fri in treasury: {}", fri_in_treasury); + assert(fri_in_treasury == 152485773837370811, 'incorrect amount in treasury'); let strk_left_in_contract = strk.balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); assert!(strk_left_in_contract == strk_balance_at_start, "strk left in contract after airdrop claim"); +} + +#[test] +#[fork("MAINNET_FIXED_BLOCK")] +fn test_claim_and_withdraw() { + test_claim_as_keeper(); + let address_eligible_for_zklend_rewards: ContractAddress = ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS.try_into().unwrap(); + let deposit_contract = IDepositDispatcher {contract_address: address_eligible_for_zklend_rewards}; + let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; + let hypothetical_owner_address: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let storage_entry_for_hypothetical_owner= array![HYPOTHETICAL_OWNER_ADDR].span(); + store(address_eligible_for_zklend_rewards, selector!("owner"), storage_entry_for_hypothetical_owner); + + cheat_caller_address(address_eligible_for_zklend_rewards, hypothetical_owner_address, CheatSpan::TargetCalls(1)); + deposit_contract.withdraw(STRK_ADDRESS.try_into().unwrap(), 0); //passing 0 to withdraw all + + assert(strk.balance_of(hypothetical_owner_address) != 0, 'no strk sent on to user'); + } \ No newline at end of file From e9ed14ad0b1821e9b8d7fb9e9dba606d0d6cfb7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Mon, 4 Nov 2024 11:07:01 +0100 Subject: [PATCH 09/18] Update to send 80 % to treasury --- src/deposit.cairo | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 51d5f491..79c0c320 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -360,6 +360,9 @@ mod Deposit { /// /// Can be called by anyone, e.g. a keeper /// + /// If the treasury address is zero, the funds are not sent to treasury to avoid burning them. + /// This does mean that a sophisticated user could deploy their own contract with a zero treasury address to avoid sending on fees. + /// /// # Panics /// `is_position_open` storage variable is set to false('Open position not exists') /// `proof` span is empty @@ -382,11 +385,17 @@ mod Deposit { let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; let zk_market = self.zk_market.read(); - let half_of_claim = claim_data.amount / 2; // u128 integer division, rounds down - - strk.transfer(self.treasury.read(), half_of_claim.into()); + let part_for_treasury = claim_data.amount - claim_data.amount / 5; // u128 integer division, rounds down + + let treasury_addr = self.treasury.read(); + let remainder = if(treasury_addr.into() == 0) { // Zeroable not publicly accessible in this Cairo version AFAIK + strk.transfer(treasury_addr, part_for_treasury.into()); + claim_data.amount - part_for_treasury + } else { + claim_data.amount + }; - let remainder = claim_data.amount - half_of_claim; + strk.approve(zk_market.contract_address, remainder.into()); zk_market.deposit(STRK_ADDRESS.try_into().unwrap(), remainder.into()); zk_market.enable_collateral(STRK_ADDRESS.try_into().unwrap()); From e28a8a47043bd249233fd97c87881035ea6fdb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Mon, 4 Nov 2024 11:07:22 +0100 Subject: [PATCH 10/18] Update RPC URL --- Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scarb.toml b/Scarb.toml index c82ad6ff..9a6e6187 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -25,7 +25,7 @@ sort-module-level-items = true [[tool.snforge.fork]] name = "SEPOLIA" -url = "http://178.32.172.148:6062/v0_7" +url = "http://51.195.57.196:6062/v0_7" block_id.tag = "latest" [[tool.snforge.fork]] From 013258e67c5d49277f453f6d9daa16cb149b6cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Mon, 4 Nov 2024 12:28:53 +0100 Subject: [PATCH 11/18] Fix; tests pass now --- src/deposit.cairo | 2 +- tests/test_defispring.cairo | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 79c0c320..7c862233 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -388,7 +388,7 @@ mod Deposit { let part_for_treasury = claim_data.amount - claim_data.amount / 5; // u128 integer division, rounds down let treasury_addr = self.treasury.read(); - let remainder = if(treasury_addr.into() == 0) { // Zeroable not publicly accessible in this Cairo version AFAIK + let remainder = if(treasury_addr.into() != 0) { // Zeroable not publicly accessible in this Cairo version AFAIK strk.transfer(treasury_addr, part_for_treasury.into()); claim_data.amount - part_for_treasury } else { diff --git a/tests/test_defispring.cairo b/tests/test_defispring.cairo index d8d3742e..d37f0fae 100644 --- a/tests/test_defispring.cairo +++ b/tests/test_defispring.cairo @@ -56,7 +56,6 @@ fn test_claim_as_keeper() { let fri_in_treasury = strk.balance_of(hypothetical_treasury_address.try_into().unwrap()); - println!("Fri in treasury: {}", fri_in_treasury); assert(fri_in_treasury == 152485773837370811, 'incorrect amount in treasury'); let strk_left_in_contract = strk.balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); assert!(strk_left_in_contract == strk_balance_at_start, "strk left in contract after airdrop claim"); From c2afea850bae6875855d3e6950e470ece08bd56a Mon Sep 17 00:00:00 2001 From: faurdent Date: Mon, 4 Nov 2024 14:31:55 +0100 Subject: [PATCH 12/18] Deploy in tests fixed. Fuzzer config moved to Scarb.toml --- Scarb.toml | 9 ++- tests/test_loop.cairo | 181 +----------------------------------------- 2 files changed, 10 insertions(+), 180 deletions(-) diff --git a/Scarb.toml b/Scarb.toml index 9a6e6187..b1519265 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -23,6 +23,9 @@ snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag [tool.fmt] sort-module-level-items = true +[tool.snforge] +fuzzer_runs = 10 + [[tool.snforge.fork]] name = "SEPOLIA" url = "http://51.195.57.196:6062/v0_7" @@ -30,10 +33,10 @@ block_id.tag = "latest" [[tool.snforge.fork]] name = "MAINNET" -url = "http://127.0.0.1:5050" +url = "http://51.195.57.196:6060/v0_7" block_id.tag = "latest" [[tool.snforge.fork]] name = "MAINNET_FIXED_BLOCK" -url = "http://127.0.0.1:5050" -block_id.number = "863242" \ No newline at end of file +url = "http://51.195.57.196:6060/v0_7" +block_id.number = "863242" diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 354ff72f..4b4c3cd9 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -19,7 +19,7 @@ use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; use spotnet::interfaces::{ IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, IDepositDispatcherTrait }; -use spotnet::types::{DepositData, Claim, EkuboSlippageLimits}; +use spotnet::types::{DepositData, EkuboSlippageLimits}; use starknet::{ContractAddress, get_block_timestamp}; @@ -34,6 +34,8 @@ mod contracts { pub const PRAGMA_ADDRESS: felt252 = 0x02a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; + + pub const TREASURY_ADDRESS: felt252 = 0x123; // Mock Address } mod tokens { @@ -46,7 +48,7 @@ fn deploy_deposit_contract(user: ContractAddress) -> ContractAddress { let (deposit_address, _) = deposit_contract .deploy( @array![ - user.try_into().unwrap(), contracts::EKUBO_CORE_MAINNET, contracts::ZKLEND_MARKET + user.try_into().unwrap(), contracts::EKUBO_CORE_MAINNET, contracts::ZKLEND_MARKET, contracts::TREASURY_ADDRESS ] ) .expect('Deploy failed'); @@ -79,8 +81,6 @@ fn get_slippage_limits(pool_key: PoolKey) -> EkuboSlippageLimits { EkuboSlippageLimits { lower: sqrt_ratio - tolerance, upper: sqrt_ratio + tolerance } } -// TODO: Add tests for asserts. - #[test] #[fork("MAINNET")] fn test_loop_eth_valid() { @@ -122,7 +122,6 @@ fn test_loop_eth_valid() { } #[test] -#[fuzzer(runs: 10)] #[feature("safe_dispatcher")] #[fork("MAINNET")] fn test_loop_eth_fuzz(amount: u64) { @@ -318,7 +317,6 @@ fn test_loop_position_exists() { } #[test] -#[fuzzer(runs: 10)] #[feature("safe_dispatcher")] #[fork("MAINNET")] fn test_loop_position_exists_fuzz(amount: u64) { @@ -588,7 +586,6 @@ fn test_extra_deposit_valid() { } #[test] -#[fuzzer(runs: 10)] // TODO: Move to global config #[fork("MAINNET")] fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { let usdc_addr: ContractAddress = @@ -675,7 +672,6 @@ fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { } #[test] -#[fuzzer(runs: 10)] #[fork("MAINNET")] fn test_withdraw_valid_fuzz(amount: u32) { let usdc_addr: ContractAddress = @@ -846,7 +842,6 @@ fn test_extra_deposit_position_zero_amount() { } #[test] -#[should_panic(expected: 'Tokens are locked')] #[fork("MAINNET")] fn test_withdraw_position_open() { let usdc_addr: ContractAddress = @@ -890,171 +885,3 @@ fn test_withdraw_position_open() { deposit_disp.withdraw(eth_addr, 100000000000000); stop_cheat_caller_address(deposit_disp.contract_address); } - -#[test] -#[fork("MAINNET")] -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 proof = [ - 0x5e3a0851fc0f58fc98964fa4aeccfae6170f87540fb6c98c6b1a95ff8619235, - 0x2d6dc4884ec2fe892aeb92c661de4bd41a833603f309a2ca4079647f8bfc2d1, - 0x73a528ae05f2995cfecb744ffd6a9dd8e4697fb5fd65a6ee8bcdf3f7dba924d, - 0x6d4d65e6140674f5aa8aad2da4110faec2aba7a4b80080468a5068b5d8bfa55, - 0x58d3abc98b3aad194393f72f3be4f71636b2af39272e1ac4cae588ac5569e95, - 0x2d45d9ea573b7e830e182feaaf67fb9296080cdf8877ad787e8cd13a219c187, - 0x7fdc8c1b9a2303764f692d208ba6725a57db5e542f92b4dffeaebd00b495bb9, - 0x321c1fb2c1b85728f374568de28e173a220d2e0efcc404c7a7cd39883b0c8f7, - 0x151cfa61a31b61e7e8dedb909c6dbb13259404d8f413796e1c285ef8908a18b, - 0x45cbbc33878e5728fc97fb00c722a5946087c6f2ac287a15fa5abfa407b4d7d, - 0x194a2043aff310ac94defa8f50d78d66d63076a521c5fd7e2420c9b10a66813, - 0x3877f5b4750b7cb26e211e2f867882680bf9a9542222971f048fb831e6f225a, - 0x5c4a8fbdc17983b19b841f490aa0531d274e9a42231d22be2dadfbce6cdf981, - 0x44692783f2e911b439cd018f3ba8c067ba5ab88bec4b35e2496b2a3b0f817ef - ].span(); - - 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 }; - 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, borrow_const: 98 }, - pool_key, - 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); - - deposit_disp.claim_reward(claim_data, proof, airdrop_addr); - - let final_balance = strk_disp.balanceOf(user); - // 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' - ); -} -// #[test] -// #[fork("MAINNET")] -// fn test_full_liquidation() { -// let usdc_addr: ContractAddress = -// 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 -// .try_into() -// .unwrap(); -// let eth_addr: ContractAddress = -// 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 -// .try_into() -// .unwrap(); -// let user: ContractAddress = -// 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 -// .try_into() -// .unwrap(); -// let liquidator: 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 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, 10000000000000000); -// 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: 10000000000000000, multiplier: 4 }, -// pool_key, -// pool_price, -// ); -// 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); -// 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 4163dafc96954d169b840542de076720b3e7727c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Mon, 4 Nov 2024 19:36:25 +0100 Subject: [PATCH 13/18] Fixup docs --- docs/spotnet.md | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/spotnet.md b/docs/spotnet.md index 24a9f880..a1f24d63 100644 --- a/docs/spotnet.md +++ b/docs/spotnet.md @@ -23,7 +23,7 @@ This method has next parameters: * `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(so for ex. 2400000000 USDC for ETH when depositing ETH). -It's flow can be described as follows: +Its flow can be described as follows: ``` assertions @@ -60,7 +60,7 @@ The method has next parameters: * `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: +Its flow can be described as follows: ``` assertions @@ -89,13 +89,45 @@ This method has next parameters: * `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 +Its flow can be described as follows: + ``` assertions -airdrop claim +claim tokens from airdrop contract + +if treasury address is non-zero { + calculate and transfer 80% to treasury +} + +approve zkLend to spend remaining tokens + +deposit remaining tokens into zkLend + +enable STRK as collateral +``` + +The method can be called by anyone (e.g., a keeper) to claim rewards. If the treasury address is set to zero when deploying the contract, all claimed rewards will be deposited into zkLend on behalf of the user instead of being split with the treasury. + + +### extra_deposit + +The `extra_deposit` method allows depositing additional tokens into an open zkLend position for increased stability. This method can be called by anyone, not just the position owner. + +Parameters: +* `token`: ContractAddress - Address of the token to deposit +* `amount`: TokenAmount - Amount of tokens to deposit + +It's flow can be described as follows: + +``` +assertions (position must be open, amount must be non-zero) + +transfer tokens from caller to contract + +approve zkLend to spend the tokens -transfer half of reward to the treasury +deposit tokens into zkLend position ``` ## Important types, events and constants @@ -138,3 +170,4 @@ struct PositionClosed { ### Constants * ZK_SCALE_DECIMALS is used for scaling down values obtained by multiplying on zklend collateral and borrow factors. +* STRK_ADDRESS is the same across Sepolia and Mainnet. From 7604c762b58bcc08052e24162082db07b1b898b5 Mon Sep 17 00:00:00 2001 From: faurdent Date: Mon, 4 Nov 2024 22:04:10 +0100 Subject: [PATCH 14/18] Reentrancy guard, upgradability, type fixed --- Scarb.lock | 48 +++++++--------- Scarb.toml | 4 +- src/constants.cairo | 3 +- src/deposit.cairo | 70 +++++++++++++++++------ tests/lib.cairo | 5 +- tests/test_defispring.cairo | 108 ++++++++++++++++++++++-------------- tests/test_loop.cairo | 25 ++++++--- 7 files changed, 164 insertions(+), 99 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 273095b8..9ce4f52a 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -11,58 +11,50 @@ name = "ekubo" version = "0.1.0" source = "git+https://github.com/ekuboprotocol/abis?rev=edb6de8#edb6de8c9baf515f1053bbab3d86825d54a63bc3" -[[package]] -name = "openzeppelin_access" -version = "0.17.0" -source = "registry+https://scarbs.xyz/" -checksum = "sha256:541bb8fdf1ad17fe0d275b00acb9f0d7f56ea5534741e21535ac3fda2c600281" -dependencies = [ - "openzeppelin_introspection", - "openzeppelin_utils", -] - [[package]] name = "openzeppelin_account" -version = "0.17.0" +version = "0.18.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:c4e11609fdd1f4c3d3004cd1468711bd2ea664739c9e59a4b270567fe4c23ee3" +checksum = "sha256:83e6571cac4c67049c8d0ab4e3c7ad146d582d7605e7354248835833e1d26c4a" dependencies = [ "openzeppelin_introspection", "openzeppelin_utils", ] [[package]] -name = "openzeppelin_governance" -version = "0.17.0" +name = "openzeppelin_introspection" +version = "0.18.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:b7e0142d88d69a8c367aea8c9dc7f659f27372551efc23f39a0cf71a189c1302" -dependencies = [ - "openzeppelin_access", - "openzeppelin_introspection", -] +checksum = "sha256:46c4cc6c95c9baa4c7d5cc0ed2bdaf334f46c25a8c92b3012829fff936e3042b" [[package]] -name = "openzeppelin_introspection" -version = "0.17.0" +name = "openzeppelin_security" +version = "0.18.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:892433a4a1ea0fc9cf7cdb01e06ddc2782182abcc188e4ea5dd480906d006cf8" +checksum = "sha256:1db3a41e02ed48806587981340ed01ee7d552c3ad52cb33a6d81c1ed5cba9ee0" [[package]] name = "openzeppelin_token" -version = "0.17.0" +version = "0.18.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:77997a7e217b69674c34b402dc0c7b2210540db66a56087572679c31896eaabb" +checksum = "sha256:eafbe13f6a0487ce212459e25a81ae07f340ba76208ad4616626eb2d25a9625e" dependencies = [ "openzeppelin_account", - "openzeppelin_governance", "openzeppelin_introspection", + "openzeppelin_utils", ] +[[package]] +name = "openzeppelin_upgrades" +version = "0.18.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:33c9d0865364fc18a5e7b471fe53c3b0f3e0aec56a94f435089638fad2a4a35b" + [[package]] name = "openzeppelin_utils" -version = "0.17.0" +version = "0.18.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:36d93e353f42fd6b824abcd8b4b51c3f5d02c893c5f886ae81403b0368aa5fde" +checksum = "sha256:725b212839f3eddc32791408609099c5e808c167ca0cf331d8c1d778b07a4e21" [[package]] name = "pragma_lib" @@ -88,7 +80,9 @@ version = "0.1.0" dependencies = [ "alexandria_math", "ekubo", + "openzeppelin_security", "openzeppelin_token", + "openzeppelin_upgrades", "pragma_lib", "snforge_std", ] diff --git a/Scarb.toml b/Scarb.toml index b1519265..ef538667 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -10,7 +10,9 @@ cairo-version = "2.8.2" 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" +openzeppelin_token = "0.18.0" +openzeppelin_security = "0.18.0" +openzeppelin_upgrades = "0.18.0" [dev-dependencies] pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib", tag = "2.8.2" } diff --git a/src/constants.cairo b/src/constants.cairo index 6e45c63a..8c28eaa6 100644 --- a/src/constants.cairo +++ b/src/constants.cairo @@ -1,2 +1,3 @@ pub const ZK_SCALE_DECIMALS: u256 = 1000000000000000000000000000; -pub const STRK_ADDRESS: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; \ No newline at end of file +pub const STRK_ADDRESS: felt252 = + 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; diff --git a/src/deposit.cairo b/src/deposit.cairo index 7c862233..f4e3fd90 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -7,7 +7,10 @@ mod Deposit { types::{i129::i129, keys::PoolKey} }; + use openzeppelin_security::ReentrancyGuardComponent; + use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin_upgrades::{UpgradeableComponent, interface::IUpgradeable}; use spotnet::{ constants::{ZK_SCALE_DECIMALS, STRK_ADDRESS}, interfaces::{ @@ -21,17 +24,29 @@ mod Deposit { }; use starknet::{ - ContractAddress, get_contract_address, get_caller_address, get_tx_info, event::EventEmitter, - storage::{StoragePointerWriteAccess, StoragePointerReadAccess} + ContractAddress, ClassHash, get_contract_address, get_caller_address, get_tx_info, + event::EventEmitter, storage::{StoragePointerWriteAccess, StoragePointerReadAccess} }; + component!( + path: ReentrancyGuardComponent, storage: reentrancy_guard, event: ReentrancyGuardEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl ReentrancyInternalImpl = ReentrancyGuardComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + #[storage] struct Storage { owner: ContractAddress, ekubo_core: ICoreDispatcher, zk_market: IMarketDispatcher, treasury: ContractAddress, - is_position_open: bool + is_position_open: bool, + #[substorage(v0)] + reentrancy_guard: ReentrancyGuardComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage } #[constructor] @@ -54,10 +69,10 @@ mod Deposit { decimals_difference: DecimalScale, total_borrowed: TokenAmount, borrow_const: u8 - ) -> felt252 { + ) -> u256 { 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.into()).try_into().unwrap() + ((amount_quote_token - total_borrowed) / 100_u256 * borrow_const.into()) } fn get_withdraw_amount( @@ -102,6 +117,10 @@ mod Deposit { enum Event { LiquidityLooped: LiquidityLooped, PositionClosed: PositionClosed, + #[flat] + ReentrancyGuardEvent: ReentrancyGuardComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event } #[generate_trait] @@ -146,7 +165,7 @@ mod Deposit { 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_account, curr_contract_address) >= amount, @@ -191,8 +210,8 @@ mod Deposit { total_borrowed, borrow_const ); - total_borrowed += to_borrow.into(); - zk_market.borrow(borrowing_token, to_borrow); + total_borrowed += to_borrow; + zk_market.borrow(borrowing_token, to_borrow.try_into().unwrap()); let params = SwapParameters { amount: i129 { mag: to_borrow.try_into().unwrap(), sign: false }, is_token1, @@ -359,10 +378,12 @@ mod Deposit { /// Claims STRK airdrop on ZKlend /// /// Can be called by anyone, e.g. a keeper - /// - /// If the treasury address is zero, the funds are not sent to treasury to avoid burning them. - /// This does mean that a sophisticated user could deploy their own contract with a zero treasury address to avoid sending on fees. - /// + /// + /// If the treasury address is zero, the funds are not sent to treasury to avoid burning + /// them. + /// This does mean that a sophisticated user could deploy their own contract with a zero + /// treasury address to avoid sending on fees. + /// /// # Panics /// `is_position_open` storage variable is set to false('Open position not exists') /// `proof` span is empty @@ -385,24 +406,25 @@ mod Deposit { let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; let zk_market = self.zk_market.read(); - let part_for_treasury = claim_data.amount - claim_data.amount / 5; // u128 integer division, rounds down + let part_for_treasury = claim_data.amount + - claim_data.amount / 5; // u128 integer division, rounds down let treasury_addr = self.treasury.read(); - let remainder = if(treasury_addr.into() != 0) { // Zeroable not publicly accessible in this Cairo version AFAIK - strk.transfer(treasury_addr, part_for_treasury.into()); - claim_data.amount - part_for_treasury + let remainder = if (treasury_addr + .into() != 0) { // Zeroable not publicly accessible in this Cairo version AFAIK + strk.transfer(treasury_addr, part_for_treasury.into()); + claim_data.amount - part_for_treasury } else { claim_data.amount }; - strk.approve(zk_market.contract_address, remainder.into()); zk_market.deposit(STRK_ADDRESS.try_into().unwrap(), remainder.into()); zk_market.enable_collateral(STRK_ADDRESS.try_into().unwrap()); } /// Makes a deposit into open zkLend position to control stability - /// + /// /// Anyone can deposit theoretically /// /// # Panics @@ -413,6 +435,7 @@ mod Deposit { /// `token`: ContractAddress - token address to withdraw from zkLend /// `amount`: TokenAmount - amount to withdraw fn extra_deposit(ref self: ContractState, token: ContractAddress, amount: TokenAmount) { + self.reentrancy_guard.start(); assert(self.is_position_open.read(), 'Open position not exists'); assert(amount != 0, 'Deposit amount is zero'); let (zk_market, token_dispatcher) = ( @@ -421,6 +444,7 @@ mod Deposit { token_dispatcher.transferFrom(get_caller_address(), get_contract_address(), amount); token_dispatcher.approve(zk_market.contract_address, amount); zk_market.deposit(token, amount.try_into().unwrap()); + self.reentrancy_guard.end(); } /// Withdraws tokens from zkLend if looped tokens are repaid @@ -470,4 +494,14 @@ mod Deposit { arr.span() } } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + // This function can only be called by the owner + assert(get_caller_address() == self.owner.read(), 'Caller is not the owner'); + + self.upgradeable.upgrade(new_class_hash); + } + } } diff --git a/tests/lib.cairo b/tests/lib.cairo index 36d0c5e3..864363b4 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -1,7 +1,6 @@ pub mod interfaces; #[cfg(test)] -mod test_loop; - +mod test_defispring; #[cfg(test)] -mod test_defispring; \ No newline at end of file +mod test_loop; diff --git a/tests/test_defispring.cairo b/tests/test_defispring.cairo index d37f0fae..8d47bfb7 100644 --- a/tests/test_defispring.cairo +++ b/tests/test_defispring.cairo @@ -1,80 +1,106 @@ -use snforge_std::{declare, DeclareResultTrait, replace_bytecode, store, cheat_caller_address, CheatSpan}; -use starknet::ContractAddress; -use spotnet::interfaces::{ - IDepositDispatcher, IDepositDispatcherTrait +use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use snforge_std::{ + declare, DeclareResultTrait, replace_bytecode, store, cheat_caller_address, CheatSpan }; -use spotnet::types::Claim; use spotnet::constants::STRK_ADDRESS; -use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use spotnet::interfaces::{IDepositDispatcher, IDepositDispatcherTrait}; +use spotnet::types::Claim; +use starknet::ContractAddress; -const ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS: felt252 = 0x020281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af; +const ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS: felt252 = + 0x020281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af; const HYPOTHETICAL_OWNER_ADDR: felt252 = 0x56789; #[test] #[fork("MAINNET_FIXED_BLOCK")] fn test_claim_as_keeper() { let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; - let defispring_claim_contract: ContractAddress = 0x2d55d6f311413945595788818d4e89e151360a2c2c6b5270d5d0ed16475505f.try_into().unwrap(); - - let address_eligible_for_zklend_rewards: ContractAddress = ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS.try_into().unwrap(); + let defispring_claim_contract: ContractAddress = + 0x2d55d6f311413945595788818d4e89e151360a2c2c6b5270d5d0ed16475505f + .try_into() + .unwrap(); + + let address_eligible_for_zklend_rewards: ContractAddress = ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS + .try_into() + .unwrap(); let contract = declare("Deposit").unwrap().contract_class(); replace_bytecode(address_eligible_for_zklend_rewards, *contract.class_hash).unwrap(); - let strk_balance_at_start = strk.balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); + let strk_balance_at_start = strk + .balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); // write the treasury address so we can check funds were sent let hypothetical_treasury_address = 0x98765; let storage_entry_for_treasury_address = array![hypothetical_treasury_address].span(); - store(address_eligible_for_zklend_rewards, selector!("treasury"), storage_entry_for_treasury_address); + store( + address_eligible_for_zklend_rewards, + selector!("treasury"), + storage_entry_for_treasury_address + ); let ZKLEND_MARKET = 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; let storage_entry_for_zk_market = array![ZKLEND_MARKET].span(); store(address_eligible_for_zklend_rewards, selector!("zk_market"), storage_entry_for_zk_market); - let deposit_contract = IDepositDispatcher {contract_address: address_eligible_for_zklend_rewards}; + let deposit_contract = IDepositDispatcher { + contract_address: address_eligible_for_zklend_rewards + }; let proof = array![ - 0x43a677604d8a532f023b8a1480e39f0a4f95460a88eb978bf86cf2e6af4a505, - 0x69faedf42e0dccc605c8f5b773c58154bd51f1d807ce51d6a254b58379df414, - 0x75afcd7c6775bd043279c5adf5cfc8519175ddb640d9bab3a80d6216fc434f2, - 0x718d5326a3a934d067b4930ff2ffbc6dba50eb189ddecc50d559c74e30ce375, - 0x60842dbbced8d585d720c3efe1f99fb32da09f6334f3ef679dbfbc9b47fcf2b, - 0x7d22c5040360327dc761eb46959c15f888b68af777829d758150612ec13949c, - 0x406de13ffdac0138c360921e9e51bb7fdabe9770c750157e40e04909589c0e7, - 0x4f138575a80804622f8b92152ffaf19e634c63934348d13b92dd1eb91bfa3c, - 0x1ca16be8f87a5dc8cbdd781a8e2e37b047ccdc0552ea048f8cbc28b6e0e9621, - 0x6e5d1a64f19a0a541716f701413ae2bace2151b83da7b231ebb347c2be8272b, - 0xf1e537b49ce8629386bfab3e390bb5dee770e0c8a176115b82607f9d9fa441, - 0x517330339d2b79fff83a99bfa17d974267ff1a49fa517bda0ec6105130d15e1, - 0x17230a4e2cb1bc3fb6a83c165e9f8f719c5198065e02d4aac7c55b75ac92fc0, - 0x787a4ca028ae34239a07b4d023f4ef785c9aa934da05a0fd2ec1310dc1a6d83 - ]; - let claim: Claim = Claim {id: 11051, claimee: address_eligible_for_zklend_rewards, amount: 0x2a52c411698a729}; - // eligible for 0x2a52c411698a729 = 190607217296713513 fri (fri is lowest denominator of strk token) + 0x43a677604d8a532f023b8a1480e39f0a4f95460a88eb978bf86cf2e6af4a505, + 0x69faedf42e0dccc605c8f5b773c58154bd51f1d807ce51d6a254b58379df414, + 0x75afcd7c6775bd043279c5adf5cfc8519175ddb640d9bab3a80d6216fc434f2, + 0x718d5326a3a934d067b4930ff2ffbc6dba50eb189ddecc50d559c74e30ce375, + 0x60842dbbced8d585d720c3efe1f99fb32da09f6334f3ef679dbfbc9b47fcf2b, + 0x7d22c5040360327dc761eb46959c15f888b68af777829d758150612ec13949c, + 0x406de13ffdac0138c360921e9e51bb7fdabe9770c750157e40e04909589c0e7, + 0x4f138575a80804622f8b92152ffaf19e634c63934348d13b92dd1eb91bfa3c, + 0x1ca16be8f87a5dc8cbdd781a8e2e37b047ccdc0552ea048f8cbc28b6e0e9621, + 0x6e5d1a64f19a0a541716f701413ae2bace2151b83da7b231ebb347c2be8272b, + 0xf1e537b49ce8629386bfab3e390bb5dee770e0c8a176115b82607f9d9fa441, + 0x517330339d2b79fff83a99bfa17d974267ff1a49fa517bda0ec6105130d15e1, + 0x17230a4e2cb1bc3fb6a83c165e9f8f719c5198065e02d4aac7c55b75ac92fc0, + 0x787a4ca028ae34239a07b4d023f4ef785c9aa934da05a0fd2ec1310dc1a6d83 + ]; + let claim: Claim = Claim { + id: 11051, claimee: address_eligible_for_zklend_rewards, amount: 0x2a52c411698a729 + }; + // eligible for 0x2a52c411698a729 = 190607217296713513 fri (fri is lowest denominator of strk + // token) // treasury should get 152485773837370811 which is 80 % - deposit_contract.claim_reward(claim, proof.span(), defispring_claim_contract); - let fri_in_treasury = strk.balance_of(hypothetical_treasury_address.try_into().unwrap()); assert(fri_in_treasury == 152485773837370811, 'incorrect amount in treasury'); - let strk_left_in_contract = strk.balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); - assert!(strk_left_in_contract == strk_balance_at_start, "strk left in contract after airdrop claim"); + let strk_left_in_contract = strk + .balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); + assert!( + strk_left_in_contract == strk_balance_at_start, "strk left in contract after airdrop claim" + ); } #[test] #[fork("MAINNET_FIXED_BLOCK")] fn test_claim_and_withdraw() { test_claim_as_keeper(); - let address_eligible_for_zklend_rewards: ContractAddress = ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS.try_into().unwrap(); - let deposit_contract = IDepositDispatcher {contract_address: address_eligible_for_zklend_rewards}; + let address_eligible_for_zklend_rewards: ContractAddress = ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS + .try_into() + .unwrap(); + let deposit_contract = IDepositDispatcher { + contract_address: address_eligible_for_zklend_rewards + }; let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; let hypothetical_owner_address: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); - let storage_entry_for_hypothetical_owner= array![HYPOTHETICAL_OWNER_ADDR].span(); - store(address_eligible_for_zklend_rewards, selector!("owner"), storage_entry_for_hypothetical_owner); + let storage_entry_for_hypothetical_owner = array![HYPOTHETICAL_OWNER_ADDR].span(); + store( + address_eligible_for_zklend_rewards, + selector!("owner"), + storage_entry_for_hypothetical_owner + ); - cheat_caller_address(address_eligible_for_zklend_rewards, hypothetical_owner_address, CheatSpan::TargetCalls(1)); + cheat_caller_address( + address_eligible_for_zklend_rewards, hypothetical_owner_address, CheatSpan::TargetCalls(1) + ); deposit_contract.withdraw(STRK_ADDRESS.try_into().unwrap(), 0); //passing 0 to withdraw all assert(strk.balance_of(hypothetical_owner_address) != 0, 'no strk sent on to user'); - -} \ No newline at end of file +} diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 4b4c3cd9..f2b18609 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -48,7 +48,10 @@ fn deploy_deposit_contract(user: ContractAddress) -> ContractAddress { let (deposit_address, _) = deposit_contract .deploy( @array![ - user.try_into().unwrap(), contracts::EKUBO_CORE_MAINNET, contracts::ZKLEND_MARKET, contracts::TREASURY_ADDRESS + user.try_into().unwrap(), + contracts::EKUBO_CORE_MAINNET, + contracts::ZKLEND_MARKET, + contracts::TREASURY_ADDRESS ] ) .expect('Deploy failed'); @@ -113,7 +116,9 @@ 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, borrow_const: 98 }, + DepositData { + token: eth_addr, amount: 685000000000000, multiplier: 4, borrow_const: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -211,7 +216,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, borrow_const: 98 }, + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -255,7 +260,7 @@ fn test_loop_unauthorized() { disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 10000000, multiplier: 4, borrow_const: 98 }, + DepositData { token: usdc_addr, amount: 10000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -301,14 +306,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, borrow_const: 98 }, + 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, borrow_const: 98 }, + DepositData { token: usdc_addr, amount: 60000000, multiplier: 4, borrow_const: 98 }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -829,7 +834,9 @@ fn test_extra_deposit_position_zero_amount() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 }, + DepositData { + token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -874,7 +881,9 @@ fn test_withdraw_position_open() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 }, + DepositData { + token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price From 725b4b7be552313c3ebf3169cfa490cb7a059f5e Mon Sep 17 00:00:00 2001 From: faurdent Date: Tue, 5 Nov 2024 17:02:57 +0100 Subject: [PATCH 15/18] Extra deposit updated, repay const and test case added --- docs/spotnet.md | 1 + src/deposit.cairo | 10 +++-- src/interfaces.cairo | 1 + src/types.cairo | 2 +- tests/test_defispring.cairo | 5 +-- tests/test_loop.cairo | 80 ++++++++++++++++++++++++++++++++++++- 6 files changed, 91 insertions(+), 8 deletions(-) diff --git a/docs/spotnet.md b/docs/spotnet.md index a1f24d63..e5d8302f 100644 --- a/docs/spotnet.md +++ b/docs/spotnet.md @@ -57,6 +57,7 @@ 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. +* `repay_const`: u8 - Sets how much to borrow from free amount. Parameter is used for dealing with price error on zklend or for pairs where debt interest rate accumulates faster than supply interest rate. * `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). diff --git a/src/deposit.cairo b/src/deposit.cairo index f4e3fd90..c2566d64 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -80,6 +80,7 @@ mod Deposit { total_debt: TokenAmount, collateral_factor: felt252, borrow_factor: felt252, + repay_const: u8, supply_token_price: TokenPrice, debt_token_price: TokenPrice, supply_decimals: DecimalScale, @@ -91,8 +92,7 @@ mod Deposit { * borrow_factor.into() / ZK_SCALE_DECIMALS) - total_debt.into(); - let withdraw_amount = free_amount * debt_token_price.into() / debt_decimals.into(); - withdraw_amount + free_amount * debt_token_price.into() / debt_decimals.into() * repay_const.into() / 100 } #[derive(starknet::Event, Drop)] @@ -264,6 +264,7 @@ mod Deposit { /// tokens. /// * `ekubo_limits`: EkuboSlippageLimits - Represents upper and lower sqrt_ratio values on /// Ekubo. Used to control slippage while swapping. + /// * `repay_const`: u8 - Sets how much to borrow from free amount. /// * `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( @@ -272,6 +273,7 @@ mod Deposit { debt_token: ContractAddress, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, + repay_const: u8, supply_price: TokenPrice, debt_price: TokenPrice ) { @@ -316,6 +318,7 @@ mod Deposit { debt, collateral_factor, borrow_factor, + repay_const, supply_price, debt_price, supply_decimals, @@ -407,7 +410,7 @@ mod Deposit { let strk = ERC20ABIDispatcher { contract_address: STRK_ADDRESS.try_into().unwrap() }; let zk_market = self.zk_market.read(); let part_for_treasury = claim_data.amount - - claim_data.amount / 5; // u128 integer division, rounds down + - claim_data.amount / 2; // u128 integer division, rounds down let treasury_addr = self.treasury.read(); let remainder = if (treasury_addr @@ -443,6 +446,7 @@ mod Deposit { ); token_dispatcher.transferFrom(get_caller_address(), get_contract_address(), amount); token_dispatcher.approve(zk_market.contract_address, amount); + zk_market.enable_collateral(token); zk_market.deposit(token, amount.try_into().unwrap()); self.reentrancy_guard.end(); } diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 1ae081a9..90a2d5bf 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -20,6 +20,7 @@ pub trait IDeposit { debt_token: ContractAddress, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, + repay_const: u8, supply_price: TokenPrice, debt_price: TokenPrice ); diff --git a/src/types.cairo b/src/types.cairo index eeb22cd8..2bd82e76 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -24,7 +24,7 @@ pub struct SwapData { pub struct DepositData { pub token: ContractAddress, pub amount: TokenAmount, - pub multiplier: u32, + pub multiplier: u8, pub borrow_const: u8 } diff --git a/tests/test_defispring.cairo b/tests/test_defispring.cairo index 8d47bfb7..98016f3e 100644 --- a/tests/test_defispring.cairo +++ b/tests/test_defispring.cairo @@ -65,12 +65,11 @@ fn test_claim_as_keeper() { }; // eligible for 0x2a52c411698a729 = 190607217296713513 fri (fri is lowest denominator of strk // token) - // treasury should get 152485773837370811 which is 80 % - + // treasury should get 95303608648356757 which is 50 % deposit_contract.claim_reward(claim, proof.span(), defispring_claim_contract); let fri_in_treasury = strk.balance_of(hypothetical_treasury_address.try_into().unwrap()); - assert(fri_in_treasury == 152485773837370811, 'incorrect amount in treasury'); + assert(fri_in_treasury == 95303608648356757, 'incorrect amount in treasury'); let strk_left_in_contract = strk .balance_of(address_eligible_for_zklend_rewards.try_into().unwrap()); assert!( diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index f2b18609..fcfccba1 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -438,6 +438,7 @@ fn test_close_position_usdc_valid_time_passed() { eth_addr, pool_key, get_slippage_limits(pool_key), + 100, pool_price, quote_token_price ); @@ -493,21 +494,96 @@ fn test_close_position_amounts_cleared() { get_slippage_limits(pool_key), pool_price ); + deposit_disp + .close_position( + usdc_addr, + eth_addr, + pool_key, + get_slippage_limits(pool_key), + 100, + pool_price, + quote_token_price + ); stop_cheat_account_contract_address(deposit_disp.contract_address); let zk_market = IMarketTestingDispatcher { contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() }; + + 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")] +fn test_close_position_partial_debt_utilization() { + 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 pool_price = get_asset_price_pragma('ETH/USD').into(); + + let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; + let decimals_sum_power: u128 = fast_power( + 10, + (ERC20ABIDispatcher { contract_address: usdc_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, 1000000000000000); + 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: 1000000000000000, multiplier: 4, borrow_const: 98 + }, + pool_key, + get_slippage_limits(pool_key), + pool_price + ); deposit_disp .close_position( - usdc_addr, eth_addr, + usdc_addr, pool_key, get_slippage_limits(pool_key), + 85, pool_price, quote_token_price ); stop_cheat_account_contract_address(deposit_disp.contract_address); + let zk_market = IMarketTestingDispatcher { + contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() + }; assert( zk_market.get_user_debt_for_token(deposit_disp.contract_address, eth_addr) == 0, @@ -662,6 +738,7 @@ fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { eth_addr, pool_key, get_slippage_limits(pool_key), + 100, pool_price, quote_token_price ); @@ -741,6 +818,7 @@ fn test_withdraw_valid_fuzz(amount: u32) { eth_addr, pool_key, get_slippage_limits(pool_key), + 100, pool_price, quote_token_price ); From bbd76e16b1fe0d1df3e42f87b9d43d09e29a96d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sojka?= Date: Wed, 6 Nov 2024 12:00:48 +0100 Subject: [PATCH 16/18] Polish docs --- docs/spotnet.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/spotnet.md b/docs/spotnet.md index e5d8302f..2c01ba51 100644 --- a/docs/spotnet.md +++ b/docs/spotnet.md @@ -23,6 +23,8 @@ This method has next parameters: * `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(so for ex. 2400000000 USDC for ETH when depositing ETH). +We can trust the passed values, such as pool_price, because this function can be only called by the owner of the contract. + Its flow can be described as follows: ``` @@ -108,7 +110,7 @@ deposit remaining tokens into zkLend enable STRK as collateral ``` -The method can be called by anyone (e.g., a keeper) to claim rewards. If the treasury address is set to zero when deploying the contract, all claimed rewards will be deposited into zkLend on behalf of the user instead of being split with the treasury. +The method can be called by anyone (e.g., a keeper) to claim rewards. If the treasury address is set to zero when deploying the contract, all claimed rewards will be deposited into zkLend on behalf of the user instead of being split with the treasury. This is intended behavior; sophisticated users wanting to bypass the functionality could deploy their modified contract anyway. This serves to avoid burning the STRK. ### extra_deposit From 1646095398580cffc9ccf4e5f222347423bd7b5d Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 6 Nov 2024 18:27:04 +0100 Subject: [PATCH 17/18] Parameter and field renamed. Scaling numbers for correct math. --- src/deposit.cairo | 58 ++++++++++++++++++++++++------------- src/interfaces.cairo | 2 +- src/types.cairo | 2 +- tests/test_loop.cairo | 67 +++++++++++++++++++++++++++++++------------ 4 files changed, 88 insertions(+), 41 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index c2566d64..5d493243 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -1,6 +1,7 @@ #[starknet::contract] mod Deposit { use alexandria_math::fast_power::fast_power; + use core::num::traits::Zero; use ekubo::{ interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait, ILocker, SwapParameters}, @@ -57,6 +58,7 @@ mod Deposit { zk_market: IMarketDispatcher, treasury: ContractAddress ) { + assert(owner.is_non_zero(), 'Owner address is zero'); self.owner.write(owner); self.ekubo_core.write(ekubo_core); self.zk_market.write(zk_market); @@ -66,13 +68,18 @@ mod Deposit { fn get_borrow_amount( borrow_capacity: TokenAmount, token_price: TokenPrice, - decimals_difference: DecimalScale, + deposit_token_decimals: DecimalScale, total_borrowed: TokenAmount, - borrow_const: u8 + borrow_portion_percent: u8 ) -> u256 { - 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.into()) + // Borrow capacity already scaled. + let amount_borrow_token = borrow_capacity + * token_price.into() + / deposit_token_decimals.into(); + ((amount_borrow_token - (total_borrowed * ZK_SCALE_DECIMALS)) + * borrow_portion_percent.into() + / 100) + / ZK_SCALE_DECIMALS } fn get_withdraw_amount( @@ -80,19 +87,22 @@ mod Deposit { total_debt: TokenAmount, collateral_factor: felt252, borrow_factor: felt252, - repay_const: u8, + borrow_portion_percent: u8, supply_token_price: TokenPrice, debt_token_price: TokenPrice, supply_decimals: DecimalScale, debt_decimals: DecimalScale ) -> u256 { - let deposited = (total_deposited * supply_token_price.into()).into() - / supply_decimals.into(); - let free_amount = ((deposited * collateral_factor.into() / ZK_SCALE_DECIMALS) + let deposited = ((total_deposited * ZK_SCALE_DECIMALS * 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(); - free_amount * debt_token_price.into() / debt_decimals.into() * repay_const.into() / 100 + / ZK_SCALE_DECIMALS)) + - (total_debt.into() * ZK_SCALE_DECIMALS); + ((free_amount * debt_token_price.into() / debt_decimals.into()) + * borrow_portion_percent.into() + / 100) + / ZK_SCALE_DECIMALS } #[derive(starknet::Event, Drop)] @@ -158,8 +168,11 @@ mod Deposit { let user_account = get_tx_info().unbox().account_contract_address; assert(user_account == self.owner.read(), 'Caller is not the owner'); assert(!self.is_position_open.read(), 'Open position already exists'); - let DepositData { token, amount, multiplier, borrow_const } = deposit_data; - assert(borrow_const > 0 && borrow_const < 100, 'Cannot calculate borrow amount'); + let DepositData { token, amount, multiplier, borrow_portion_percent } = deposit_data; + assert( + borrow_portion_percent > 0 && borrow_portion_percent < 100, + 'Cannot calculate borrow amount' + ); assert(multiplier < 6 && multiplier > 1, 'Multiplier not supported'); assert(amount != 0 && pool_price != 0, 'Parameters cannot be zero'); @@ -200,7 +213,10 @@ mod Deposit { let mut deposited = amount; while (amount + accumulated) / amount < multiplier.into() { - let borrow_capacity = ((deposited * collateral_factor / ZK_SCALE_DECIMALS) + let borrow_capacity = ((deposited + * ZK_SCALE_DECIMALS + * collateral_factor + / ZK_SCALE_DECIMALS) * borrow_factor / ZK_SCALE_DECIMALS); let to_borrow = get_borrow_amount( @@ -208,7 +224,7 @@ mod Deposit { pool_price, deposit_token_decimals.into(), total_borrowed, - borrow_const + borrow_portion_percent ); total_borrowed += to_borrow; zk_market.borrow(borrowing_token, to_borrow.try_into().unwrap()); @@ -273,7 +289,7 @@ mod Deposit { debt_token: ContractAddress, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, - repay_const: u8, + borrow_portion_percent: u8, supply_price: TokenPrice, debt_price: TokenPrice ) { @@ -318,7 +334,7 @@ mod Deposit { debt, collateral_factor, borrow_factor, - repay_const, + borrow_portion_percent, supply_price, debt_price, supply_decimals, @@ -326,9 +342,11 @@ mod Deposit { ); zk_market.withdraw(supply_token, withdraw_amount.try_into().unwrap()); - let params = if (debt > withdraw_amount + let params = if (debt > (withdraw_amount + * ZK_SCALE_DECIMALS * supply_price.into() - / supply_decimals.into()) { + / supply_decimals.into()) + / ZK_SCALE_DECIMALS) { SwapParameters { amount: i129 { mag: withdraw_amount.try_into().unwrap(), sign: false }, is_token1: is_token1_repay_swap, diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 90a2d5bf..67fe85e8 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -20,7 +20,7 @@ pub trait IDeposit { debt_token: ContractAddress, pool_key: PoolKey, ekubo_limits: EkuboSlippageLimits, - repay_const: u8, + borrow_portion_percent: u8, supply_price: TokenPrice, debt_price: TokenPrice ); diff --git a/src/types.cairo b/src/types.cairo index 2bd82e76..e263cc0f 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -25,7 +25,7 @@ pub struct DepositData { pub token: ContractAddress, pub amount: TokenAmount, pub multiplier: u8, - pub borrow_const: u8 + pub borrow_portion_percent: u8 } #[derive(Copy, Drop, Serde)] diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index fcfccba1..b7d1ade3 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -16,6 +16,7 @@ use snforge_std::cheatcodes::execution_info::caller_address::{ start_cheat_caller_address, stop_cheat_caller_address }; use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; +use spotnet::constants::ZK_SCALE_DECIMALS; use spotnet::interfaces::{ IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, IDepositDispatcherTrait }; @@ -117,7 +118,7 @@ fn test_loop_eth_valid() { deposit_disp .loop_liquidity( DepositData { - token: eth_addr, amount: 685000000000000, multiplier: 4, borrow_const: 98 + token: eth_addr, amount: 685000000000000, multiplier: 4, borrow_portion_percent: 98 }, pool_key, get_slippage_limits(pool_key), @@ -160,7 +161,9 @@ 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, borrow_const: 98 }, + DepositData { + token: eth_addr, amount: amount.into(), multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -206,7 +209,11 @@ fn test_loop_usdc_valid() { (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 pool_price = (1 + * ZK_SCALE_DECIMALS + * decimals_sum_power.into() + / get_asset_price_pragma('ETH/USD').into()) + / ZK_SCALE_DECIMALS; let deposit_disp = get_deposit_dispatcher(user); start_cheat_caller_address(usdc_addr.try_into().unwrap(), user); @@ -216,10 +223,12 @@ 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, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 60000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), - pool_price.into() + pool_price.try_into().unwrap() ); stop_cheat_account_contract_address(deposit_disp.contract_address); } @@ -260,7 +269,9 @@ fn test_loop_unauthorized() { disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 10000000, multiplier: 4, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 10000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -306,14 +317,18 @@ 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, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 60000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price.into() ); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 60000000, multiplier: 4, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 60000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price.into() @@ -356,7 +371,9 @@ fn test_loop_position_exists_fuzz(amount: u64) { if let Result::Err(_) = deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: amount.into(), multiplier: 4, borrow_const: 98 }, + DepositData { + token: eth_addr, amount: amount.into(), multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -365,7 +382,9 @@ fn test_loop_position_exists_fuzz(amount: u64) { }; match deposit_disp .loop_liquidity( - DepositData { token: eth_addr, amount: amount.into(), multiplier: 4, borrow_const: 98 }, + DepositData { + token: eth_addr, amount: amount.into(), multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -420,7 +439,9 @@ 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, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -489,7 +510,9 @@ 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, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -564,7 +587,7 @@ fn test_close_position_partial_debt_utilization() { deposit_disp .loop_liquidity( DepositData { - token: eth_addr, amount: 1000000000000000, multiplier: 4, borrow_const: 98 + token: eth_addr, amount: 1000000000000000, multiplier: 4, borrow_portion_percent: 98 }, pool_key, get_slippage_limits(pool_key), @@ -576,7 +599,7 @@ fn test_close_position_partial_debt_utilization() { usdc_addr, pool_key, get_slippage_limits(pool_key), - 85, + 99, pool_price, quote_token_price ); @@ -638,7 +661,9 @@ fn test_extra_deposit_valid() { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -706,7 +731,9 @@ fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -793,7 +820,9 @@ fn test_withdraw_valid_fuzz(amount: u32) { start_cheat_account_contract_address(deposit_disp.contract_address, user); deposit_disp .loop_liquidity( - DepositData { token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_const: 98 }, + DepositData { + token: usdc_addr, amount: 1000000000, multiplier: 4, borrow_portion_percent: 98 + }, pool_key, get_slippage_limits(pool_key), pool_price @@ -913,7 +942,7 @@ fn test_extra_deposit_position_zero_amount() { deposit_disp .loop_liquidity( DepositData { - token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 + token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_portion_percent: 98 }, pool_key, get_slippage_limits(pool_key), @@ -960,7 +989,7 @@ fn test_withdraw_position_open() { deposit_disp .loop_liquidity( DepositData { - token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_const: 98 + token: eth_addr, amount: 685000000000000, multiplier: 2, borrow_portion_percent: 98 }, pool_key, get_slippage_limits(pool_key), From 4c7fc1e257322e3378795896cfcc6df22601fb93 Mon Sep 17 00:00:00 2001 From: faurdent Date: Wed, 6 Nov 2024 18:37:11 +0100 Subject: [PATCH 18/18] Unneeded parts removed --- src/deposit.cairo | 3 +-- src/types.cairo | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/deposit.cairo b/src/deposit.cairo index 5d493243..6209aac6 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -93,8 +93,7 @@ mod Deposit { supply_decimals: DecimalScale, debt_decimals: DecimalScale ) -> u256 { - let deposited = ((total_deposited * ZK_SCALE_DECIMALS * supply_token_price.into()).into() - / supply_decimals.into()); + let deposited = ((total_deposited * ZK_SCALE_DECIMALS * supply_token_price.into()) / supply_decimals.into()); let free_amount = (((deposited * collateral_factor.into() / ZK_SCALE_DECIMALS) * borrow_factor.into() / ZK_SCALE_DECIMALS)) diff --git a/src/types.cairo b/src/types.cairo index e263cc0f..be93bc5d 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -1,6 +1,3 @@ -// 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;