From e3784564c942d32de286893ec8256135819ada3b Mon Sep 17 00:00:00 2001 From: Martin Domajnko <35891136+martines3000@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:46:08 +0200 Subject: [PATCH] feat: add `update_price_feeds_if_necessary` to market contract (#13) --- Cargo.lock | 1 + abis/market_abi/src/abi.sw | 19 +- abis/market_abi/src/structs.sw | 9 + contracts/market/src/main.sw | 48 +- .../tests/local_tests/functions/pause.rs | 37 +- .../market/tests/local_tests/main_test_uni.rs | 61 ++- .../main_test_uni_no_debug_mode.rs | 60 ++- contracts/pyth-mock/src/main.sw | 15 + libs/market_sdk/Cargo.toml | 1 + libs/market_sdk/src/market_utils.rs | 422 ++++++++++++++---- libs/pyth_mock_sdk/src/lib.rs | 17 +- 11 files changed, 541 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a5cec59..81294b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3657,6 +3657,7 @@ dependencies = [ "anyhow", "fuels", "market", + "pyth_sdk", "rand", "serde", "serde_json", diff --git a/abis/market_abi/src/abi.sw b/abis/market_abi/src/abi.sw index 7d04c273..4eff261a 100644 --- a/abis/market_abi/src/abi.sw +++ b/abis/market_abi/src/abi.sw @@ -41,8 +41,8 @@ abi Market { #[payable, storage(read, write)] fn supply_collateral(); // Payment is required: any collateral asset - #[storage(read, write)] - fn withdraw_collateral(asset: b256, amount: u256); + #[payable, storage(read, write)] + fn withdraw_collateral(asset: b256, amount: u256, price_data_update: PriceDataUpdate); #[storage(read)] fn get_user_collateral(address: Address, asset_id: b256) -> u256; @@ -55,8 +55,8 @@ abi Market { #[payable, storage(read, write)] fn supply_base(); // Payment is required: base asset (USDC) - #[storage(read, write)] - fn withdraw_base(amount: u256); + #[payable, storage(read, write)] + fn withdraw_base(amount: u256, price_data_update: PriceDataUpdate); #[storage(read)] fn get_user_supply_borrow(account: Address) -> (u256, u256); @@ -66,15 +66,15 @@ abi Market { // # 5. Liquidation management // Liquidates the user if there is insufficient collateral for the borrowing. - #[storage(read, write)] - fn absorb(accounts: Vec
); + #[payable, storage(read, write)] + fn absorb(accounts: Vec
, price_data_update: PriceDataUpdate); #[storage(read)] fn is_liquidatable(account: Address) -> bool; // # 6. Protocol collateral management #[payable, storage(read)] - fn buy_collateral(asset_id: b256, min_amount: u256, recipient: Address); // Payment is required: base asset (USDC) + fn buy_collateral(asset_id: b256, min_amount: u256, recipient: Address, price_data_update: PriceDataUpdate); // Payment is required: base asset (USDC) #[storage(read)] fn collateral_value_to_sell(asset_id: b256, collateral_amount: u256) -> u256; @@ -85,6 +85,7 @@ abi Market { // ## 7. Reserves management #[storage(read)] fn get_reserves() -> I256; + #[storage(read)] fn withdraw_reserves(to: Address, amount: u256); @@ -128,8 +129,8 @@ abi Market { fn update_fee(update_data: Vec) -> u64; #[payable, storage(read)] - fn update_price_feeds(update_fee: u64, update_data: Vec); - + fn update_price_feeds_if_necessary(price_data_update: PriceDataUpdate); + // ## 11. Changing market configuration #[storage(write, read)] fn update_market_configuration(configuration: MarketConfiguration); diff --git a/abis/market_abi/src/structs.sw b/abis/market_abi/src/structs.sw index 92f705b5..aba22999 100644 --- a/abis/market_abi/src/structs.sw +++ b/abis/market_abi/src/structs.sw @@ -2,6 +2,8 @@ library; use i256::I256; use std::constants::ZERO_B256; +use std::bytes::Bytes; +use pyth_interface::{data_structures::price::{PriceFeedId}}; pub const BASE_ACCRUAL_SCALE: u256 = 1_000_000; // 1e6 pub const BASE_INDEX_SCALE_15: u256 = 1_000_000_000_000_000; // 1e15 @@ -126,6 +128,13 @@ impl MarketBasics { } } +pub struct PriceDataUpdate { + pub update_fee: u64, + pub publish_times: Vec, + pub price_feed_ids: Vec, + pub update_data: Vec +} + pub enum Error { AlreadyInitialized: (), Paused: (), diff --git a/contracts/market/src/main.sw b/contracts/market/src/main.sw index 58aaa0c9..1e03ffda 100644 --- a/contracts/market/src/main.sw +++ b/contracts/market/src/main.sw @@ -138,7 +138,7 @@ impl Market for Contract { .collateral_configurations_keys .push(configuration.asset_id); - log(CollateralAssetAdded { asset_id: configuration.asset_id, configuration }); // TODO[Martin]: What is this event? + log(CollateralAssetAdded { asset_id: configuration.asset_id, configuration }); } // ## 2.2 Pause an existing collateral asset @@ -264,8 +264,9 @@ impl Market for Contract { // ### Parameters: // - `asset_id`: The asset ID of the collateral asset to be withdrawn // - `amount`: The amount of collateral to be withdrawn - #[storage(read, write)] - fn withdraw_collateral(asset_id: b256, amount: u256) { + // - `price_data_update`: The price data update struct to be used for updating the price feeds + #[payable, storage(read, write)] + fn withdraw_collateral(asset_id: b256, amount: u256, price_data_update: PriceDataUpdate) { // Get the caller's address and calculate the new user and total collateral let caller = msg_sender_address(); let user_collateral = storage.user_collateral.get((caller, asset_id)).try_read().unwrap_or(0) - amount; @@ -277,7 +278,10 @@ impl Market for Contract { .user_collateral .insert((caller, asset_id), user_collateral); - // Note: no accrue interest, BorrowCF < LiquidationCF covers small changes + // Update price data + update_price_feeds_if_necessary_internal(price_data_update); + + // Note: no accrue interest, BorrowCollateralFactor < LiquidationCollateralFactor covers small changes // Check if the user is borrow collateralized require(is_borrow_collateralized(caller), Error::NotCollateralized); @@ -367,8 +371,9 @@ impl Market for Contract { // ## 4.2 Withdraw base (borrowing if possible/necessary) // ### Parameters: // - `amount`: The amount of base asset to be withdrawn - #[storage(read, write)] - fn withdraw_base(amount: u256) { + // - `price_data_update`: The price data update struct to be used for updating the price feeds + #[payable, storage(read, write)] + fn withdraw_base(amount: u256, price_data_update: PriceDataUpdate) { // Only allow withdrawing if paused flag is not set require(!storage.pause_config.withdraw_paused.read(), Error::Paused); @@ -419,6 +424,9 @@ impl Market for Contract { Error::BorrowTooSmall, ); + // Update price data + update_price_feeds_if_necessary_internal(price_data_update); + // Check that the user is borrow collateralized require(is_borrow_collateralized(caller), Error::NotCollateralized); } @@ -486,14 +494,17 @@ impl Market for Contract { // - Absorb a list of underwater accounts onto the protocol balance sheet // ### Parameters: // - `accounts`: The list of underwater accounts to be absorbed - #[storage(read, write)] - fn absorb(accounts: Vec
) { + #[payable, storage(read, write)] + fn absorb(accounts: Vec
, price_data_update: PriceDataUpdate) { // Check that the pause flag is not set require(!storage.pause_config.absorb_paused.read(), Error::Paused); // Accrue interest accrue_internal(); + // Update price data + update_price_feeds_if_necessary_internal(price_data_update); + let mut index = 0; // Loop and absorb each account while index < accounts.len() { @@ -523,8 +534,9 @@ impl Market for Contract { // - `asset_id`: The asset ID of the collateral asset to be bought // - `min_amount`: The minimum amount of collateral to be bought // - `recipient`: The address of the recipient of the collateral + // - `price_data_update`: The price data update struct to be used for updating the price feeds #[payable, storage(read)] - fn buy_collateral(asset_id: b256, min_amount: u256, recipient: Address) { + fn buy_collateral(asset_id: b256, min_amount: u256, recipient: Address, price_data_update: PriceDataUpdate) { // Only allow buying collateral if paused flag is not set require(!storage.pause_config.buy_paused.read(), Error::Paused); let payment_amount: u256 = msg_amount().into(); @@ -548,6 +560,10 @@ impl Market for Contract { // Note: Re-entrancy can skip the reserves check above on a second buyCollateral call. let reserves = get_collateral_reserves_internal(asset_id); + // Update price data + // FIXME[Martin]: After Fuel responds either remove this or adapt + // update_price_feeds_if_necessary_internal(price_data_update); + // Calculate the quote for a collateral asset in exchange for an amount of the base asset let collateral_amount = quote_collateral_internal(asset_id, payment_amount); @@ -774,8 +790,8 @@ impl Market for Contract { } #[payable, storage(read)] - fn update_price_feeds(update_fee: u64, update_data: Vec) { - update_price_feeds_internal(update_fee, update_data) + fn update_price_feeds_if_necessary(price_data_update: PriceDataUpdate) { + update_price_feeds_if_necessary_internal(price_data_update) } // # 11. Changing market configuration @@ -822,17 +838,17 @@ fn update_fee_internal(update_data: Vec) -> u64 { } #[payable, storage(read)] -fn update_price_feeds_internal(update_fee: u64, update_data: Vec) { +fn update_price_feeds_if_necessary_internal(price_data_update: PriceDataUpdate) { let contract_id = storage.pyth_contract_id.read(); require(contract_id != ZERO_B256, Error::OracleContractIdNotSet); let oracle = abi(PythCore, contract_id); - oracle.update_price_feeds { - asset_id: FUEL_ETH_BASE_ASSET_ID, coins: update_fee + oracle.update_price_feeds_if_necessary { + asset_id: FUEL_ETH_BASE_ASSET_ID, coins: price_data_update.update_fee } - (update_data); + (price_data_update.price_feed_ids, price_data_update.publish_times, price_data_update.update_data); } - + // ## Timestamp getter // ### Description: diff --git a/contracts/market/tests/local_tests/functions/pause.rs b/contracts/market/tests/local_tests/functions/pause.rs index 882df9e2..8ed9eed4 100644 --- a/contracts/market/tests/local_tests/functions/pause.rs +++ b/contracts/market/tests/local_tests/functions/pause.rs @@ -1,4 +1,5 @@ use market::PauseConfiguration; +use market::PriceDataUpdate; use token_sdk::{TokenAsset, TokenContract}; use crate::utils::init_wallets; @@ -74,19 +75,22 @@ async fn pause_test() { // ==================== Set oracle prices ==================== let mut prices = Vec::new(); + let mut price_feed_ids = Vec::new(); let publish_time: u64 = Utc::now().timestamp().try_into().unwrap(); let confidence = 0; for asset in &assets { let price = asset.1.default_price * 10u64.pow(asset.1.price_feed_decimals); + price_feed_ids.push(asset.1.price_feed_id); + prices.push(( asset.1.price_feed_id, (price, asset.1.price_feed_decimals, publish_time, confidence), )) } - oracle.update_prices(prices).await.unwrap(); + oracle.update_prices(&prices).await.unwrap(); for asset in &assets { let price = oracle.price(asset.1.price_feed_id).await.unwrap().value; @@ -98,6 +102,13 @@ async fn pause_test() { ); } + let price_data_update = PriceDataUpdate { + update_fee: 1, + price_feed_ids: price_feed_ids, + publish_times: vec![publish_time; assets.len()], + update_data: oracle.create_update_data(&prices).await.unwrap(), + }; + // ================================================= // ==================== Step #0 ==================== // 👛 Wallet: Bob 🧛 @@ -173,7 +184,7 @@ async fn pause_test() { .with_account(&alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount) + .withdraw_base(&[&oracle.instance], amount, &price_data_update) .await .unwrap(); @@ -200,7 +211,15 @@ async fn pause_test() { res.confidence, ), )]); - oracle.update_prices(prices).await.unwrap(); + oracle.update_prices(&prices).await.unwrap(); + + // New `price_data_update` that will be used in the next steps + let price_data_update = PriceDataUpdate { + update_fee: 1, + price_feed_ids: vec![uni.price_feed_id], + publish_times: vec![Utc::now().timestamp().try_into().unwrap()], + update_data: oracle.create_update_data(&prices).await.unwrap(), + }; let res = oracle.price(uni.price_feed_id).await.unwrap().value; assert!(new_price == res.price); @@ -225,7 +244,7 @@ async fn pause_test() { .with_account(bob) .await .unwrap() - .absorb(&[&oracle.instance], vec![alice_address]) + .absorb(&[&oracle.instance], vec![alice_address], &price_data_update) .await .unwrap(); @@ -288,14 +307,13 @@ async fn pause_test() { uni.bits256, 1, bob_address, + &price_data_update, ) .await .unwrap(); market.debug_increment_timestamp().await.unwrap(); - // TODO claim_paused - // ================================================= // ==================== Step #6 ==================== // 👛 Wallet: Admin 🗿 @@ -304,7 +322,7 @@ async fn pause_test() { let price = oracle.price(uni.price_feed_id).await.unwrap().value; let amount = parse_units(5, uni.price_feed_decimals.into()); // 1 UNI = $5 oracle - .update_prices(Vec::from([( + .update_prices(&Vec::from([( uni.price_feed_id, ( amount, @@ -404,7 +422,7 @@ async fn pause_test() { .with_account(alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount) + .withdraw_base(&[&oracle.instance], amount, &price_data_update) .await .is_err(); assert!(res); @@ -419,7 +437,7 @@ async fn pause_test() { .with_account(bob) .await .unwrap() - .absorb(&[&oracle.instance], vec![alice_address]) + .absorb(&[&oracle.instance], vec![alice_address], &price_data_update) .await .is_err(); assert!(res); @@ -461,6 +479,7 @@ async fn pause_test() { uni.bits256, 1, bob_address, + &price_data_update, ) .await .is_err(); diff --git a/contracts/market/tests/local_tests/main_test_uni.rs b/contracts/market/tests/local_tests/main_test_uni.rs index 26c44111..0dc4abb2 100644 --- a/contracts/market/tests/local_tests/main_test_uni.rs +++ b/contracts/market/tests/local_tests/main_test_uni.rs @@ -1,6 +1,7 @@ use chrono::Utc; use fuels::prelude::ViewOnlyAccount; use fuels::types::{Address, Bits256, ContractId}; +use market::PriceDataUpdate; use market_sdk::{get_market_config, parse_units, MarketContract}; use pyth_mock_sdk::PythMockContract; use token_sdk::{TokenAsset, TokenContract}; @@ -82,6 +83,7 @@ async fn main_test() { // ==================== Set oracle prices ==================== let mut prices = Vec::new(); + let mut price_feed_ids = Vec::new(); let publish_time: u64 = Utc::now().timestamp().try_into().unwrap(); let confidence = 0; @@ -94,11 +96,13 @@ async fn main_test() { )) } - oracle.update_prices(prices).await.unwrap(); + oracle.update_prices(&prices).await.unwrap(); for asset in &assets { let price = oracle.price(asset.1.price_feed_id).await.unwrap().value; + price_feed_ids.push(asset.1.price_feed_id); + println!( "Price for {} = {}", asset.1.symbol, @@ -106,6 +110,13 @@ async fn main_test() { ); } + let price_data_update = PriceDataUpdate { + update_fee: 1, + price_feed_ids: price_feed_ids, + publish_times: vec![publish_time; assets.len()], + update_data: oracle.create_update_data(&prices).await.unwrap(), + }; + // ================================================= // ==================== Step #0 ==================== // 👛 Wallet: Bob 🧛 @@ -190,7 +201,7 @@ async fn main_test() { .with_account(&alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount) + .withdraw_base(&[&oracle.instance], amount, &price_data_update) .await .unwrap(); @@ -292,6 +303,7 @@ async fn main_test() { (amount - u128::from(parse_units(1, usdc.decimals))) .try_into() .unwrap(), + &price_data_update, ) .await .unwrap(); @@ -309,7 +321,11 @@ async fn main_test() { .with_account(alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], parse_units(2, usdc.decimals)) + .withdraw_base( + &[&oracle.instance], + parse_units(2, usdc.decimals), + &price_data_update, + ) .await .is_err(); assert!(res); @@ -343,7 +359,16 @@ async fn main_test() { res.confidence, ), )]); - oracle.update_prices(prices).await.unwrap(); + + oracle.update_prices(&prices).await.unwrap(); + + // New `price_data_update` that will be used in the next steps + let price_data_update = PriceDataUpdate { + update_fee: 1, + price_feed_ids: vec![uni.price_feed_id], + publish_times: vec![Utc::now().timestamp().try_into().unwrap()], + update_data: oracle.create_update_data(&prices).await.unwrap(), + }; println!( "🔻 UNI price drops: ${} -> ${}", @@ -376,7 +401,7 @@ async fn main_test() { .with_account(bob) .await .unwrap() - .absorb(&[&oracle.instance], vec![alice_address]) + .absorb(&[&oracle.instance], vec![alice_address], &price_data_update) .await .unwrap(); @@ -443,6 +468,7 @@ async fn main_test() { uni.bits256, 1, bob_address, + &price_data_update, ) .await .unwrap(); @@ -469,7 +495,11 @@ async fn main_test() { .with_account(bob) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount.try_into().unwrap()) + .withdraw_base( + &[&oracle.instance], + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); @@ -498,7 +528,11 @@ async fn main_test() { .with_account(chad) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount.try_into().unwrap()) + .withdraw_base( + &[&oracle.instance], + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); @@ -527,7 +561,11 @@ async fn main_test() { .with_account(alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount.try_into().unwrap()) + .withdraw_base( + &[&oracle.instance], + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); @@ -556,7 +594,12 @@ async fn main_test() { .with_account(chad) .await .unwrap() - .withdraw_collateral(&[&oracle.instance], uni.bits256, amount.try_into().unwrap()) + .withdraw_collateral( + &[&oracle.instance], + uni.bits256, + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); diff --git a/contracts/market/tests/local_tests/main_test_uni_no_debug_mode.rs b/contracts/market/tests/local_tests/main_test_uni_no_debug_mode.rs index b18aadc9..88e995ba 100644 --- a/contracts/market/tests/local_tests/main_test_uni_no_debug_mode.rs +++ b/contracts/market/tests/local_tests/main_test_uni_no_debug_mode.rs @@ -2,6 +2,7 @@ use crate::utils::{init_wallets, print_case_title}; use chrono::Utc; use fuels::prelude::ViewOnlyAccount; use fuels::types::{Address, Bits256, ContractId}; +use market::PriceDataUpdate; use market_sdk::{get_market_config, parse_units, MarketContract}; use pyth_mock_sdk::PythMockContract; use token_sdk::{TokenAsset, TokenContract}; @@ -79,6 +80,7 @@ async fn main_test_no_debug() { // ==================== Set oracle prices ==================== let mut prices = Vec::new(); + let mut price_feed_ids = Vec::new(); let publish_time: u64 = Utc::now().timestamp().try_into().unwrap(); let confidence = 0; @@ -91,11 +93,13 @@ async fn main_test_no_debug() { )) } - oracle.update_prices(prices).await.unwrap(); + oracle.update_prices(&prices).await.unwrap(); for asset in &assets { let price = oracle.price(asset.1.price_feed_id).await.unwrap().value; + price_feed_ids.push(asset.1.price_feed_id); + println!( "Price for {} = {}", asset.1.symbol, @@ -103,6 +107,13 @@ async fn main_test_no_debug() { ); } + let price_data_update = PriceDataUpdate { + update_fee: 1, + price_feed_ids: price_feed_ids, + publish_times: vec![publish_time; assets.len()], + update_data: oracle.create_update_data(&prices).await.unwrap(), + }; + // ================================================= // ==================== Step #0 ==================== // 👛 Wallet: Bob 🧛 @@ -186,7 +197,7 @@ async fn main_test_no_debug() { .with_account(&alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount) + .withdraw_base(&[&oracle.instance], amount, &price_data_update) .await .unwrap(); @@ -285,6 +296,7 @@ async fn main_test_no_debug() { (amount - u128::from(parse_units(1, usdc.decimals))) .try_into() .unwrap(), + &price_data_update, ) .await .unwrap(); @@ -301,7 +313,11 @@ async fn main_test_no_debug() { .with_account(alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], parse_units(2, usdc.decimals)) + .withdraw_base( + &[&oracle.instance], + parse_units(2, usdc.decimals), + &price_data_update, + ) .await .is_err(); assert!(res); @@ -334,7 +350,15 @@ async fn main_test_no_debug() { res.confidence, ), )]); - oracle.update_prices(prices).await.unwrap(); + oracle.update_prices(&prices).await.unwrap(); + + // New `price_data_update` that will be used in the next steps + let price_data_update = PriceDataUpdate { + update_fee: 1, + price_feed_ids: vec![uni.price_feed_id], + publish_times: vec![Utc::now().timestamp().try_into().unwrap()], + update_data: oracle.create_update_data(&prices).await.unwrap(), + }; println!( "🔻 UNI price drops: ${} -> ${}", @@ -365,7 +389,7 @@ async fn main_test_no_debug() { .with_account(bob) .await .unwrap() - .absorb(&[&oracle.instance], vec![alice_address]) + .absorb(&[&oracle.instance], vec![alice_address], &price_data_update) .await .unwrap(); @@ -431,6 +455,7 @@ async fn main_test_no_debug() { uni.bits256, 1, bob_address, + &price_data_update, ) .await .unwrap(); @@ -456,7 +481,11 @@ async fn main_test_no_debug() { .with_account(bob) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount.try_into().unwrap()) + .withdraw_base( + &[&oracle.instance], + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); @@ -484,7 +513,11 @@ async fn main_test_no_debug() { .with_account(chad) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount.try_into().unwrap()) + .withdraw_base( + &[&oracle.instance], + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); @@ -512,7 +545,11 @@ async fn main_test_no_debug() { .with_account(alice) .await .unwrap() - .withdraw_base(&[&oracle.instance], amount.try_into().unwrap()) + .withdraw_base( + &[&oracle.instance], + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); @@ -540,7 +577,12 @@ async fn main_test_no_debug() { .with_account(chad) .await .unwrap() - .withdraw_collateral(&[&oracle.instance], uni.bits256, amount.try_into().unwrap()) + .withdraw_collateral( + &[&oracle.instance], + uni.bits256, + amount.try_into().unwrap(), + &price_data_update, + ) .await .unwrap(); diff --git a/contracts/pyth-mock/src/main.sw b/contracts/pyth-mock/src/main.sw index bec0f4ea..d5e36fe6 100644 --- a/contracts/pyth-mock/src/main.sw +++ b/contracts/pyth-mock/src/main.sw @@ -99,7 +99,22 @@ impl PythCore for Contract { publish_times: Vec, update_data: Vec, ) { + // Currently implementation is the same as `update_price_feeds` let mut index = 0; + + // Bytes structure + // 0: price_feed_id -> b256 + // 1: price -> u64 + // 2: exponent -> u32 + // 3: publish_time -> u64 + // 4: confidence -> u64 + + while index < update_data.len() { + let payload = update_data.get(index).unwrap(); + let (price_feed_id, price) = decode_bytes(payload); + storage.latest_price_feed.insert(price_feed_id, price); + index += 1; + } } #[storage(read)] diff --git a/libs/market_sdk/Cargo.toml b/libs/market_sdk/Cargo.toml index 77dd0102..2086dcc5 100644 --- a/libs/market_sdk/Cargo.toml +++ b/libs/market_sdk/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" license = "Apache-2.0" [dependencies] +pyth_sdk = { workspace = true } market = { workspace = true } token_sdk = { workspace = true } fuels = { workspace = true } diff --git a/libs/market_sdk/src/market_utils.rs b/libs/market_sdk/src/market_utils.rs index c2f84a6f..8904cff3 100644 --- a/libs/market_sdk/src/market_utils.rs +++ b/libs/market_sdk/src/market_utils.rs @@ -12,7 +12,8 @@ use fuels::{ }, types::{ bech32::Bech32ContractId, transaction::TxPolicies, - transaction_builders::VariableOutputPolicy, Address, AssetId, Bits256, Bytes32, ContractId, + transaction_builders::VariableOutputPolicy, Address, AssetId, Bits256, Bytes, Bytes32, + ContractId, }, }; use rand::Rng; @@ -150,10 +151,20 @@ impl MarketContract { } // Contract methods - pub async fn activate_contract(&self, market_configuration: MarketConfiguration) -> anyhow::Result> { - Ok(self.instance.methods().activate_contract(market_configuration).call().await?) + // # 0. Activate contract + pub async fn activate_contract( + &self, + market_configuration: MarketConfiguration, + ) -> anyhow::Result> { + Ok(self + .instance + .methods() + .activate_contract(market_configuration) + .call() + .await?) } + // # 1. Debug functionality (for testing purposes) pub async fn debug_increment_timestamp(&self) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); @@ -166,78 +177,83 @@ impl MarketContract { .await?) } - pub async fn supply_base( + // # 2. Collateral asset management + pub async fn add_collateral_asset( &self, - base_asset_id: AssetId, - amount: u64, + config: &CollateralConfiguration, ) -> anyhow::Result> { - let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000); - let call_params = CallParameters::default() - .with_amount(amount) - .with_asset_id(base_asset_id); + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .supply_base() + .add_collateral_asset(config.clone()) .with_tx_policies(tx_policies) - .call_params(call_params)? .call() .await?) } - pub async fn add_collateral_asset( + pub async fn puase_collateral_asset( &self, - config: &CollateralConfiguration, + asset_id: AssetId, ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .add_collateral_asset(config.clone()) + .pause_collateral_asset(asset_id.into()) .with_tx_policies(tx_policies) .call() .await?) } - pub async fn withdraw_base( + pub async fn resume_collateral_asset( &self, - contract_ids: &[&dyn ContractDependency], - amount: u64, + asset_id: AssetId, ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .withdraw_base(amount.into()) - .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) - .with_contracts(contract_ids) + .resume_collateral_asset(asset_id.into()) .with_tx_policies(tx_policies) .call() .await?) } - pub async fn withdraw_collateral( + pub async fn update_collateral_asset( &self, - contract_ids: &[&dyn ContractDependency], - asset_id: Bits256, - amount: u64, + asset_id: AssetId, + config: &CollateralConfiguration, ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .withdraw_collateral(asset_id, amount.into()) + .update_collateral_asset(asset_id.into(), config.clone()) + .with_tx_policies(tx_policies) + .call() + .await?) + } + + pub async fn get_collateral_configurations( + &self, + ) -> anyhow::Result>> { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + Ok(self + .instance + .methods() + .get_collateral_configurations() .with_tx_policies(tx_policies) - .with_contracts(contract_ids) - .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) .call() .await?) } + // # 3. Collateral asset management (Supply and Withdrawal) pub async fn supply_collateral( &self, asset_id: AssetId, @@ -259,38 +275,38 @@ impl MarketContract { .await?) } - pub async fn get_user_collateral( + pub async fn withdraw_collateral( &self, - address: Address, + contract_ids: &[&dyn ContractDependency], asset_id: Bits256, - ) -> anyhow::Result { + amount: u64, + price_data_update: &PriceDataUpdate, + ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); - let res = self + Ok(self .instance .methods() - .get_user_collateral(address, asset_id) + .withdraw_collateral(asset_id, amount.into(), price_data_update.clone()) .with_tx_policies(tx_policies) + .with_contracts(contract_ids) + .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) .call() - .await? - .value; - - Ok(convert_u256_to_u128(res)) + .await?) } - pub async fn available_to_borrow( + pub async fn get_user_collateral( &self, - contract_ids: &[&dyn ContractDependency], address: Address, + asset_id: Bits256, ) -> anyhow::Result { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); let res = self .instance .methods() - .available_to_borrow(address) + .get_user_collateral(address, asset_id) .with_tx_policies(tx_policies) - .with_contracts(contract_ids) .call() .await? .value; @@ -298,97 +314,168 @@ impl MarketContract { Ok(convert_u256_to_u128(res)) } - pub async fn get_user_supply_borrow(&self, address: Address) -> anyhow::Result<(u128, u128)> { + pub async fn totals_collateral(&self, asset_id: Bits256) -> anyhow::Result { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); - let (supply, borrow) = self + let res = self .instance .methods() - .get_user_supply_borrow(address) + .totals_collateral(asset_id) .with_tx_policies(tx_policies) .call() .await? .value; - Ok((convert_u256_to_u128(supply), convert_u256_to_u128(borrow))) + Ok(convert_u256_to_u128(res)) } - pub async fn get_user_basic( + // # 4. Base asset management (Supply and Withdrawal) + pub async fn supply_base( &self, - address: Address, - ) -> anyhow::Result> { - let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + base_asset_id: AssetId, + amount: u64, + ) -> anyhow::Result> { + let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000); + let call_params = CallParameters::default() + .with_amount(amount) + .with_asset_id(base_asset_id); Ok(self .instance .methods() - .get_user_basic(address) + .supply_base() .with_tx_policies(tx_policies) + .call_params(call_params)? .call() .await?) } - pub async fn get_market_basics(&self) -> anyhow::Result> { + pub async fn withdraw_base( + &self, + contract_ids: &[&dyn ContractDependency], + amount: u64, + price_data_update: &PriceDataUpdate, + ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + let call_params = CallParameters::default().with_amount(price_data_update.update_fee); Ok(self .instance .methods() - .get_market_basics() + .withdraw_base(amount.into(), price_data_update.clone()) + .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) + .with_contracts(contract_ids) .with_tx_policies(tx_policies) + .call_params(call_params)? .call() .await?) } - pub async fn totals_collateral(&self, bits256: Bits256) -> anyhow::Result { + pub async fn get_user_supply_borrow(&self, address: Address) -> anyhow::Result<(u128, u128)> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); - let res = self + let (supply, borrow) = self .instance .methods() - .totals_collateral(bits256) + .get_user_supply_borrow(address) .with_tx_policies(tx_policies) .call() .await? .value; - Ok(convert_u256_to_u128(res)) + Ok((convert_u256_to_u128(supply), convert_u256_to_u128(borrow))) } - pub async fn get_utilization(&self) -> anyhow::Result { + pub async fn available_to_borrow( + &self, + contract_ids: &[&dyn ContractDependency], + address: Address, + ) -> anyhow::Result { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); let res = self .instance .methods() - .get_utilization() + .available_to_borrow(address) .with_tx_policies(tx_policies) + .with_contracts(contract_ids) .call() .await? .value; + Ok(convert_u256_to_u128(res)) } - pub async fn balance_of(&self, bits256: Bits256) -> anyhow::Result> { + // # 5. Liquidation management + pub async fn absorb( + &self, + contract_ids: &[&dyn ContractDependency], + addresses: Vec
, + price_data_update: &PriceDataUpdate, + ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + let call_params = CallParameters::default().with_amount(price_data_update.update_fee); Ok(self .instance .methods() - .balance_of(bits256) + .absorb(addresses, price_data_update.clone()) .with_tx_policies(tx_policies) + .with_contracts(contract_ids) + .call_params(call_params)? .call() .await?) } - pub async fn pause(&self, config: &PauseConfiguration) -> anyhow::Result> { + pub async fn is_liquidatable( + &self, + contract_ids: &[&dyn ContractDependency], + address: Address, + ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .pause(config.clone()) + .is_liquidatable(address) .with_tx_policies(tx_policies) + .with_contracts(contract_ids) + .call() + .await?) + } + + // # 6. Protocol collateral management + pub async fn buy_collateral( + &self, + contract_ids: &[&dyn ContractDependency], + base_asset_id: AssetId, + amount: u64, + asset_id: Bits256, + min_amount: u64, + recipient: Address, + price_data_update: &PriceDataUpdate, + ) -> anyhow::Result> { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + let call_params_base_asset = CallParameters::default() + .with_amount(amount) + .with_asset_id(base_asset_id); // Buy collateral with base asset + + let call_params = CallParameters::default().with_amount(price_data_update.update_fee); // Fee for price update + + Ok(self + .instance + .methods() + .buy_collateral( + asset_id, + min_amount.into(), + recipient, + price_data_update.clone(), + ) + .with_tx_policies(tx_policies) + .with_contracts(contract_ids) + .call_params(call_params_base_asset)? + .with_variable_output_policy(VariableOutputPolicy::Exactly(2)) .call() .await?) } @@ -414,63 +501,52 @@ impl MarketContract { Ok(convert_u256_to_u128(res)) } - pub async fn buy_collateral( + pub async fn quote_collateral( &self, contract_ids: &[&dyn ContractDependency], - base_asset_id: AssetId, - amount: u64, - asset_id: Bits256, - min_amount: u64, - recipient: Address, - ) -> anyhow::Result> { + asset_id: AssetId, + base_amount: u64, + ) -> anyhow::Result { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); - let call_params = CallParameters::default() - .with_amount(amount) - .with_asset_id(base_asset_id); - - Ok(self + let res = self .instance .methods() - .buy_collateral(asset_id, min_amount.into(), recipient) + .quote_collateral(asset_id.into(), base_amount.into()) .with_tx_policies(tx_policies) .with_contracts(contract_ids) - .call_params(call_params)? - .with_variable_output_policy(VariableOutputPolicy::Exactly(2)) .call() - .await?) + .await? + .value; + + Ok(convert_u256_to_u128(res)) } - pub async fn absorb( - &self, - contract_ids: &[&dyn ContractDependency], - addresses: Vec
, - ) -> anyhow::Result> { + // # 7. Reserves management + pub async fn get_reserves(&self) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .absorb(addresses) + .get_reserves() .with_tx_policies(tx_policies) - .with_contracts(contract_ids) .call() .await?) } - pub async fn is_liquidatable( + pub async fn withdraw_reserves( &self, - contract_ids: &[&dyn ContractDependency], - address: Address, - ) -> anyhow::Result> { + to: Address, + amount: u64, + ) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .is_liquidatable(address) + .withdraw_reserves(to, amount.into()) .with_tx_policies(tx_policies) - .with_contracts(contract_ids) .call() .await?) } @@ -490,18 +566,118 @@ impl MarketContract { .await?) } - pub async fn get_reserves(&self) -> anyhow::Result> { + // # 9. Pause management + pub async fn pause(&self, config: &PauseConfiguration) -> anyhow::Result> { let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); Ok(self .instance .methods() - .get_reserves() + .pause(config.clone()) + .with_tx_policies(tx_policies) + .call() + .await?) + } + + // # 10. Getters + pub async fn get_market_configuration( + &self, + ) -> anyhow::Result> { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + Ok(self + .instance + .methods() + .get_market_configuration() + .with_tx_policies(tx_policies) + .call() + .await?) + } + + pub async fn get_market_basics(&self) -> anyhow::Result> { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + Ok(self + .instance + .methods() + .get_market_basics() + .with_tx_policies(tx_policies) + .call() + .await?) + } + + pub async fn get_user_basic( + &self, + address: Address, + ) -> anyhow::Result> { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + Ok(self + .instance + .methods() + .get_user_basic(address) + .with_tx_policies(tx_policies) + .call() + .await?) + } + + pub async fn get_utilization(&self) -> anyhow::Result { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + let res = self + .instance + .methods() + .get_utilization() + .with_tx_policies(tx_policies) + .call() + .await? + .value; + Ok(convert_u256_to_u128(res)) + } + + pub async fn balance_of(&self, bits256: Bits256) -> anyhow::Result> { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + Ok(self + .instance + .methods() + .balance_of(bits256) .with_tx_policies(tx_policies) .call() .await?) } + pub async fn get_supply_rate(&self, utilization: u64) -> anyhow::Result { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + let value = self + .instance + .methods() + .get_supply_rate(utilization.into()) + .with_tx_policies(tx_policies) + .call() + .await? + .value; + + Ok(convert_u256_to_u128(value)) + } + + pub async fn get_borrow_rate(&self, utilization: u64) -> anyhow::Result { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + let value = self + .instance + .methods() + .get_borrow_rate(utilization.into()) + .with_tx_policies(tx_policies) + .call() + .await? + .value; + + Ok(convert_u256_to_u128(value)) + } + + // # 10. Pyth Oracle management pub async fn set_pyth_contract_id( &self, contract_id: ContractId, @@ -517,6 +693,66 @@ impl MarketContract { .await?) } + pub async fn get_price( + &self, + contract_ids: &[&dyn ContractDependency], + price_feed_id: Bits256, + ) -> anyhow::Result { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + let price = self + .instance + .methods() + .get_price(price_feed_id) + .with_contracts(contract_ids) + .with_tx_policies(tx_policies) + .call() + .await? + .value; + + Ok(price) + } + + pub async fn update_fee( + &self, + contract_ids: &[&dyn ContractDependency], + update_data: Vec, + ) -> anyhow::Result { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + let value = self + .instance + .methods() + .update_fee(update_data) + .with_contracts(contract_ids) + .with_tx_policies(tx_policies) + .call() + .await? + .value; + + Ok(value) + } + + pub async fn update_price_feeds_if_necessary( + &self, + contract_ids: &[&dyn ContractDependency], + price_data_update: &PriceDataUpdate, + ) -> anyhow::Result<()> { + let tx_policies = TxPolicies::default().with_script_gas_limit(DEFAULT_GAS_LIMIT); + + self.instance + .methods() + .update_price_feeds_if_necessary(price_data_update.clone()) + .with_contracts(contract_ids) + .with_tx_policies(tx_policies) + .call() + .await?; + + Ok(()) + } + + // # 11. Changing market configuration + pub async fn print_debug_state( &self, wallets: &Vec, diff --git a/libs/pyth_mock_sdk/src/lib.rs b/libs/pyth_mock_sdk/src/lib.rs index c68ddba0..17937c37 100644 --- a/libs/pyth_mock_sdk/src/lib.rs +++ b/libs/pyth_mock_sdk/src/lib.rs @@ -62,11 +62,10 @@ impl PythMockContract { .await?) } - // Vec<(PriceFeedId, Price)> - pub async fn update_prices( + pub async fn create_update_data( &self, - prices: Vec<(Bits256, (u64, u32, u64, u64))>, - ) -> anyhow::Result> { + prices: &Vec<(Bits256, (u64, u32, u64, u64))>, + ) -> anyhow::Result> { let mut update_data: Vec = Vec::new(); for (price_feed_id, (price, exponent, publish_time, confidence)) in prices { @@ -88,6 +87,16 @@ impl PythMockContract { }); } + Ok(update_data) + } + + // Vec<(PriceFeedId, Price)> + pub async fn update_prices( + &self, + prices: &Vec<(Bits256, (u64, u32, u64, u64))>, + ) -> anyhow::Result> { + let update_data: Vec = self.create_update_data(prices).await?; + Ok(self .instance .methods()