diff --git a/Makefile b/Makefile index 7ca7305..29dbfd7 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ format: ## Format the code -------Deployer Scripts-------: deploy: ## Run the deployment script for core contracts - @cd deploy-scripts && RPC=$(RPC) SECRET=$(SECRET) cargo run deploy + @forc build && cd deploy-scripts && RPC=$(RPC) SECRET=$(SECRET) cargo run deploy add-asset: ## Run the script to add assets to the protocol @cd deploy-scripts && RPC=$(RPC) SECRET=$(SECRET) cargo run add-asset diff --git a/contracts/oracle-contract/src/main.sw b/contracts/oracle-contract/src/main.sw index a34588f..059ccda 100644 --- a/contracts/oracle-contract/src/main.sw +++ b/contracts/oracle-contract/src/main.sw @@ -28,6 +28,7 @@ use libraries::{ oracle_interface::RedstoneCore, oracle_interface::{ Oracle, + RedstoneConfig, }, }; use std::{block::timestamp, constants::ZERO_B256,}; @@ -42,16 +43,12 @@ configurable { PYTH: ContractId = ContractId::zero(), /// Price feed to query PYTH_PRICE_ID: PriceFeedId = ZERO_B256, - /// Contract Address - REDSTONE: ContractId = ContractId::zero(), - /// Price feed to query - REDSTONE_PRICE_ID: u256 = u256::min(), - /// Precision of value returned by Redstone - REDSTONE_PRECISION: u32 = 9, /// Decimal representation of the asset in the Fuel network FUEL_DECIMAL_REPRESENTATION: u32 = 9, /// Timeout in seconds DEBUG: bool = false, + /// Initializer + INITIALIZER: Identity = Identity::Address(Address::zero()), } // Timeout period for considering oracle data as stale (4 hours in seconds) const TIMEOUT: u64 = 14400; @@ -66,11 +63,20 @@ storage { }, // Used for simulating different timestamps during testing debug_timestamp: u64 = 0, + redstone_config: Option = None, } impl Oracle for Contract { #[storage(read, write)] fn get_price() -> u64 { + // Step 1: Query the Pyth oracle (primary source) + let mut pyth_price = abi(PythCore, PYTH.bits()).price_unsafe(PYTH_PRICE_ID); + pyth_price = pyth_price_with_fuel_vm_precision_adjustment(pyth_price, FUEL_DECIMAL_REPRESENTATION); + let redstone_config = storage.redstone_config.read(); + // If Redstone is not configured, return the latest Pyth price regardless of staleness and confidence + if redstone_config.is_none() { + return pyth_price.price; + } // Determine the current timestamp based on debug mode let current_time = match DEBUG { true => storage.debug_timestamp.read(), @@ -78,17 +84,16 @@ impl Oracle for Contract { }; // Read the last stored valid price let last_price = storage.last_good_price.read(); - // Step 1: Query the Pyth oracle (primary source) - let mut pyth_price = abi(PythCore, PYTH.bits()).price_unsafe(PYTH_PRICE_ID); - pyth_price = pyth_price_with_fuel_vm_precision_adjustment(pyth_price, FUEL_DECIMAL_REPRESENTATION); // Check if Pyth data is stale or outside confidence if is_pyth_price_stale_or_outside_confidence(pyth_price, current_time) { // Step 2: Pyth is stale or outside confidence, query Redstone oracle (fallback source) + let config = redstone_config.unwrap(); + let mut feed = Vec::with_capacity(1); - feed.push(REDSTONE_PRICE_ID); + feed.push(config.price_id); // Fuel Bug workaround: trait coherence - let id = REDSTONE.bits(); + let id = config.contract_id.bits(); let redstone = abi(RedstoneCore, id); let redstone_prices = redstone.read_prices(feed); let redstone_timestamp = redstone.read_timestamp(); @@ -96,7 +101,7 @@ impl Oracle for Contract { // By default redstone uses 8 decimal precision so it is generally safe to cast down let redstone_price = convert_precision_u256_and_downcast( redstone_price_u64, - adjust_exponent(REDSTONE_PRECISION, FUEL_DECIMAL_REPRESENTATION), + adjust_exponent(config.precision, FUEL_DECIMAL_REPRESENTATION), ); // Check if Redstone data is also stale if current_time > redstone_timestamp + TIMEOUT { @@ -147,6 +152,23 @@ impl Oracle for Contract { require(DEBUG, "ORACLE: Debug is not enabled"); storage.debug_timestamp.write(timestamp); } + + #[storage(read, write)] + fn set_redstone_config(config: RedstoneConfig) { + require( + msg_sender() + .unwrap() == INITIALIZER, + "ORACLE: Only initializer can set Redstone config", + ); + require( + storage + .redstone_config + .read() + .is_none(), + "ORACLE: Redstone config already set", + ); + storage.redstone_config.write(Some(config)); + } } // Assets in the fuel VM can have a different decimal representation // This function adjusts the price to align with the decimal representation of the Fuel VM diff --git a/contracts/oracle-contract/tests/authorization.rs b/contracts/oracle-contract/tests/authorization.rs new file mode 100644 index 0000000..11f5214 --- /dev/null +++ b/contracts/oracle-contract/tests/authorization.rs @@ -0,0 +1,127 @@ +use fuels::{prelude::*, types::Identity}; +use test_utils::{ + interfaces::{ + oracle::{oracle_abi, Oracle, RedstoneConfig}, + pyth_oracle::{pyth_oracle_abi, Price, PythCore, DEFAULT_PYTH_PRICE_ID}, + }, + setup::common::{deploy_mock_pyth_oracle, deploy_mock_redstone_oracle, deploy_oracle}, +}; + +async fn setup() -> ( + Oracle, + PythCore, + WalletUnlocked, + WalletUnlocked, +) { + let mut wallets = launch_custom_provider_and_get_wallets( + WalletsConfig::new(Some(3), Some(1), Some(1_000_000_000)), + None, + None, + ) + .await + .unwrap(); + + let deployer_wallet = wallets.pop().unwrap(); + let attacker_wallet = wallets.pop().unwrap(); + + let pyth = deploy_mock_pyth_oracle(&deployer_wallet).await; + + let oracle = deploy_oracle( + &deployer_wallet, + pyth.contract_id().into(), + DEFAULT_PYTH_PRICE_ID, + 9, // Default Fuel VM decimals + true, + Identity::Address(deployer_wallet.address().into()), + ) + .await; + + (oracle, pyth, deployer_wallet, attacker_wallet) +} + +#[tokio::test] +async fn test_set_redstone_config_authorization() { + let (oracle, _, deployer_wallet, attacker_wallet) = setup().await; + let redstone = deploy_mock_redstone_oracle(&deployer_wallet).await; + // Test 1: Authorized set_redstone_config + let redstone_config = RedstoneConfig { + contract_id: ContractId::from([1u8; 32]), + price_id: [2u8; 32].into(), + precision: 6, + }; + + let result = oracle_abi::set_redstone_config(&oracle, &redstone, redstone_config.clone()).await; + assert!( + result.is_ok(), + "Authorized user should be able to set Redstone config" + ); + + // Test 2: Unauthorized set_redstone_config + let oracle_attacker = Oracle::new(oracle.contract_id().clone(), attacker_wallet.clone()); + let result = + oracle_abi::set_redstone_config(&oracle_attacker, &redstone, redstone_config.clone()).await; + + assert!( + result.is_err(), + "Unauthorized user should not be able to set Redstone config" + ); + if let Err(error) = result { + assert!( + error + .to_string() + .contains("Only initializer can set Redstone config"), + "Unexpected error message: {}", + error + ); + } + + // Test 3: Attempt to set Redstone config again (should fail) + let result = oracle_abi::set_redstone_config(&oracle, &redstone, redstone_config.clone()).await; + assert!(result.is_err(), "Setting Redstone config twice should fail"); + if let Err(error) = result { + assert!( + error.to_string().contains("Redstone config already set"), + "Unexpected error message: {}", + error + ); + } +} + +#[tokio::test] +async fn test_get_price_pyth_only() { + let (oracle, pyth, _, _) = setup().await; + + // Set a price in Pyth + let pyth_price = 1000 * 1_000_000_000; // $1000 with 9 decimal places + let pyth_timestamp = 1234567890; + + oracle_abi::set_debug_timestamp(&oracle, pyth_timestamp).await; + pyth_oracle_abi::update_price_feeds( + &pyth, + vec![( + DEFAULT_PYTH_PRICE_ID, + Price { + confidence: 0, + exponent: 9, + price: pyth_price, + publish_time: pyth_timestamp, + }, + )], + ) + .await; + + // Get price from Oracle (should return Pyth price) + let price = oracle_abi::get_price(&oracle, &pyth, &None).await.value; + assert_eq!(price, pyth_price, "Oracle should return Pyth price"); + + // Set Pyth price as stale + let stale_timestamp = pyth_timestamp + 14401; // TIMEOUT + 1 + oracle_abi::set_debug_timestamp(&oracle, stale_timestamp).await; + + // Get price from Oracle (should return last good price) + let price = oracle_abi::get_price(&oracle, &pyth, &None).await.value; + assert_eq!( + price, pyth_price, + "Oracle should return last good price when Pyth is stale" + ); +} diff --git a/contracts/oracle-contract/tests/harness.rs b/contracts/oracle-contract/tests/harness.rs index 1f28b47..2a052f7 100644 --- a/contracts/oracle-contract/tests/harness.rs +++ b/contracts/oracle-contract/tests/harness.rs @@ -1,9 +1,9 @@ -use fuels::prelude::*; use fuels::types::U256; +use fuels::{prelude::*, types::Identity}; use test_utils::{ data_structures::PRECISION, interfaces::{ - oracle::{oracle_abi, Oracle, ORACLE_TIMEOUT}, + oracle::{oracle_abi, Oracle, RedstoneConfig, ORACLE_TIMEOUT}, pyth_oracle::{ pyth_oracle_abi, pyth_price_feed_with_time, PythCore, DEFAULT_PYTH_PRICE_ID, PYTH_TIMESTAMP, @@ -20,10 +20,11 @@ const DEFAULT_FUEL_VM_DECIMALS: u32 = 9; async fn setup( redstone_precision: u32, fuel_vm_decimals: u32, + initialize_redstone: bool, ) -> ( Oracle, PythCore, - RedstoneCore, + Option>, ) { let block_time = 1u32; // seconds let config = NodeConfig { @@ -43,20 +44,34 @@ async fn setup( let wallet = wallets.pop().unwrap(); let pyth = deploy_mock_pyth_oracle(&wallet).await; - let redstone = deploy_mock_redstone_oracle(&wallet).await; + let oracle = deploy_oracle( &wallet, pyth.contract_id().into(), DEFAULT_PYTH_PRICE_ID, - redstone.contract_id().into(), - redstone_precision, - DEFAULT_REDSTONE_PRICE_ID, fuel_vm_decimals, true, + Identity::Address(wallet.address().into()), ) .await; + if initialize_redstone { + let redstone = deploy_mock_redstone_oracle(&wallet).await; + + oracle_abi::set_redstone_config( + &oracle, + &redstone, + RedstoneConfig { + contract_id: redstone.contract_id().into(), + price_id: DEFAULT_REDSTONE_PRICE_ID, + precision: redstone_precision, + }, + ) + .await; + + return (oracle, pyth, Some(redstone)); + } - (oracle, pyth, redstone) + (oracle, pyth, None) } fn redstone_feed(price: u64) -> Vec<(U256, U256)> { @@ -86,7 +101,7 @@ mod tests { #[tokio::test] async fn price() { let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let expected_price = convert_precision(1 * PRECISION, PYTH_PRECISION.into()); oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; @@ -96,24 +111,30 @@ mod tests { ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.unwrap().clone())) + .await + .value; assert_eq!(expected_price, price); } #[tokio::test] async fn fallback_to_last_price() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let expected_price = convert_precision(1 * PRECISION, PYTH_PRECISION.into()); + let redstone = redstone_wrapped.unwrap(); + oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; pyth_oracle_abi::update_price_feeds( &pyth, pyth_price_feed_with_time(1, PYTH_TIMESTAMP, PYTH_PRECISION.into()), ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price); @@ -122,7 +143,9 @@ mod tests { pyth_price_feed_with_time(2, PYTH_TIMESTAMP - 1, PYTH_PRECISION.into()), ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price); } @@ -133,8 +156,10 @@ mod tests { #[tokio::test] async fn live_redstone() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; + + let redstone = redstone_wrapped.unwrap(); let expected_price_pyth = convert_precision(1 * PRECISION, PYTH_PRECISION.into()); let expected_price_redstone = convert_precision(3 * PRECISION, REDSTONE_PRECISION.into()); @@ -145,7 +170,9 @@ mod tests { pyth_price_feed_with_time(1, PYTH_TIMESTAMP, PYTH_PRECISION.into()), ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price_pyth, price); @@ -158,7 +185,9 @@ mod tests { .await; redstone_oracle_abi::write_prices(&redstone, redstone_feed(3)).await; redstone_oracle_abi::set_timestamp(&redstone, PYTH_TIMESTAMP + 1).await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price_redstone, price); } @@ -175,17 +204,21 @@ mod tests { #[tokio::test] async fn price() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let expected_price = convert_precision(1 * PRECISION, PYTH_PRECISION.into()); + let redstone = redstone_wrapped.unwrap(); + oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; pyth_oracle_abi::update_price_feeds( &pyth, pyth_price_feed_with_time(1, PYTH_TIMESTAMP, PYTH_PRECISION.into()), ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price); @@ -199,24 +232,30 @@ mod tests { .await; redstone_oracle_abi::write_prices(&redstone, redstone_feed(3)).await; redstone_oracle_abi::set_timestamp(&redstone, PYTH_TIMESTAMP).await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price * 2, price); } #[tokio::test] async fn fallback_last_price() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let expected_price = convert_precision(1 * PRECISION, PYTH_PRECISION.into()); + let redstone = redstone_wrapped.unwrap(); + oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; pyth_oracle_abi::update_price_feeds( &pyth, pyth_price_feed_with_time(1, PYTH_TIMESTAMP, PYTH_PRECISION.into()), ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price); @@ -231,7 +270,9 @@ mod tests { redstone_oracle_abi::write_prices(&redstone, redstone_feed(3)).await; redstone_oracle_abi::set_timestamp(&redstone, PYTH_TIMESTAMP).await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price); } @@ -242,20 +283,24 @@ mod tests { #[tokio::test] async fn price() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let expected_price_pyth = convert_precision(1 * PRECISION, PYTH_PRECISION.into()); let expected_price_redstone = convert_precision(3 * PRECISION, REDSTONE_PRECISION.into()); + let redstone = redstone_wrapped.unwrap(); + oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; pyth_oracle_abi::update_price_feeds( &pyth, pyth_price_feed_with_time(1, PYTH_TIMESTAMP, PYTH_PRECISION.into()), ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price_pyth, price); @@ -269,7 +314,9 @@ mod tests { .await; redstone_oracle_abi::write_prices(&redstone, redstone_feed(3)).await; redstone_oracle_abi::set_timestamp(&redstone, PYTH_TIMESTAMP + 1).await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; let redstone_price = redstone_oracle_abi::read_prices( &redstone, @@ -288,17 +335,21 @@ mod tests { #[tokio::test] async fn fallback_last_price() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let expected_price = convert_precision(1 * PRECISION, PYTH_PRECISION.into()); + let redstone = redstone_wrapped.unwrap(); + oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; pyth_oracle_abi::update_price_feeds( &pyth, pyth_price_feed_with_time(1, PYTH_TIMESTAMP, PYTH_PRECISION.into()), ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price); @@ -312,7 +363,9 @@ mod tests { .await; redstone_oracle_abi::write_prices(&redstone, redstone_feed(3)).await; redstone_oracle_abi::set_timestamp(&redstone, PYTH_TIMESTAMP).await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price); } @@ -327,11 +380,13 @@ mod tests { #[tokio::test] async fn price_within_confidence() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let price = 1000 * PRECISION; let confidence = price / 100; // 1% confidence, which is within the 4% threshold + let redstone = redstone_wrapped.unwrap(); + oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; pyth_oracle_abi::update_price_feeds( &pyth, @@ -343,15 +398,19 @@ mod tests { ), ) .await; - let result = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let result = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(convert_precision(price, PYTH_PRECISION.into()), result); } #[tokio::test] async fn price_outside_confidence_good_redstone() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; + + let redstone = redstone_wrapped.unwrap(); let pyth_price = 100 * PRECISION; let pyth_confidence = pyth_price / 20; // 5% confidence, which is outside the 4% threshold let redstone_price = 105 * PRECISION; @@ -371,7 +430,9 @@ mod tests { .await; redstone_oracle_abi::set_timestamp(&redstone, PYTH_TIMESTAMP).await; - let result = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let result = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!( convert_precision(redstone_price, REDSTONE_PRECISION.into()), @@ -386,7 +447,10 @@ mod tests { #[tokio::test] async fn test_with_fuel_vm_decimals_8() { let fuel_vm_decimals = 8; - let (oracle, pyth, redstone) = setup(REDSTONE_PRECISION, fuel_vm_decimals).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, fuel_vm_decimals, true).await; + + let redstone = redstone_wrapped.unwrap(); // Set a price of $5000 for 1 unit of the asset let pyth_price = 5000; @@ -399,7 +463,9 @@ mod tests { ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; // Expected price calculation: // 1. Convert Pyth price to 8 decimal precision: 5000 * 10^8 = 500_000_000_000 @@ -412,7 +478,10 @@ mod tests { #[tokio::test] async fn test_with_fuel_vm_decimals_12() { let fuel_vm_decimals = 12; - let (oracle, pyth, redstone) = setup(REDSTONE_PRECISION, fuel_vm_decimals).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, fuel_vm_decimals, true).await; + + let redstone = redstone_wrapped.unwrap(); // Set a price of $5000 for 1 unit of the asset let pyth_price = 5000; @@ -425,7 +494,9 @@ mod tests { ) .await; - let price = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; // Expected price calculation: // 1. Convert Pyth price to 12 decimal precision: 5000 * 10^12 = 5_000_000_000_000_000 @@ -443,10 +514,12 @@ mod tests { #[tokio::test] async fn test_pyth_price_adjustment_different_exponents() { - let (oracle, pyth, redstone) = - setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS).await; + let (oracle, pyth, redstone_wrapped) = + setup(REDSTONE_PRECISION, DEFAULT_FUEL_VM_DECIMALS, true).await; let expected_price = 1 * PRECISION; // $1.00 with 9 decimal places + let redstone = redstone_wrapped.unwrap(); + // Test with exponent 9 oracle_abi::set_debug_timestamp(&oracle, PYTH_TIMESTAMP).await; pyth_oracle_abi::update_price_feeds( @@ -454,7 +527,9 @@ mod tests { pyth_price_feed_with_confidence(1_000_000_000, PYTH_TIMESTAMP, 0, 9), ) .await; - let price_exp_9 = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price_exp_9 = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price_exp_9); // Test with exponent 6 @@ -464,7 +539,9 @@ mod tests { pyth_price_feed_with_confidence(1_000_000, PYTH_TIMESTAMP + 1, 0, 6), ) .await; - let price_exp_6 = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price_exp_6 = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price_exp_6); // Test with exponent 12 @@ -474,7 +551,9 @@ mod tests { pyth_price_feed_with_confidence(1_000_000_000_000, PYTH_TIMESTAMP + 2, 0, 12), ) .await; - let price_exp_12 = oracle_abi::get_price(&oracle, &pyth, &redstone).await.value; + let price_exp_12 = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())) + .await + .value; assert_eq!(expected_price, price_exp_12); // Assert that all prices are equal diff --git a/deploy-scripts/src/add_asset.rs b/deploy-scripts/src/add_asset.rs index d8bcc2e..5039b8d 100644 --- a/deploy-scripts/src/add_asset.rs +++ b/deploy-scripts/src/add_asset.rs @@ -79,7 +79,7 @@ fn write_asset_contracts_to_file(asset_contracts: Vec) { let current_price = oracle_abi::get_price( &asset_contracts.oracle, &asset_contracts.mock_pyth_oracle, - &asset_contracts.mock_redstone_oracle, + &Some(asset_contracts.mock_redstone_oracle.clone()), ) .await .value; diff --git a/deploy-scripts/src/deploy.rs b/deploy-scripts/src/deploy.rs index ba6b1e8..0690ff2 100644 --- a/deploy-scripts/src/deploy.rs +++ b/deploy-scripts/src/deploy.rs @@ -79,12 +79,12 @@ pub mod deployment { let json = json!({ "borrow_operations": contracts.borrow_operations.contract_id().to_string(), "usdf": contracts.usdf.contract_id().to_string(), - "usdf_asset_id": contracts.usdf_asset_id.to_string(), + "usdf_asset_id": format!("0x{}", contracts.usdf_asset_id.to_string()), "stability_pool": contracts.stability_pool.contract_id().to_string(), "protocol_manager": contracts.protocol_manager.contract_id().to_string(), "fpt_staking": contracts.fpt_staking.contract_id().to_string(), "fpt_token": contracts.fpt_token.contract_id().to_string(), - "fpt_asset_id": contracts.fpt_asset_id.to_string(), + "fpt_asset_id": format!("0x{}", contracts.fpt_asset_id.to_string()), "community_issuance": contracts.community_issuance.contract_id().to_string(), "coll_surplus_pool": contracts.coll_surplus_pool.contract_id().to_string(), "default_pool": contracts.default_pool.contract_id().to_string(), @@ -98,7 +98,7 @@ pub mod deployment { "oracle": asset_contracts.oracle.contract_id().to_string(), "trove_manager": asset_contracts.trove_manager.contract_id().to_string(), "asset_contract": asset_contracts.asset.contract_id().to_string(), - "asset_id": asset_contracts.asset_id.to_string(), + "asset_id": format!("0x{}", asset_contracts.asset_id.to_string()), }) }).collect::>() }); diff --git a/libraries/src/oracle_interface.sw b/libraries/src/oracle_interface.sw index 8df9e53..9285b29 100644 --- a/libraries/src/oracle_interface.sw +++ b/libraries/src/oracle_interface.sw @@ -9,6 +9,9 @@ abi Oracle { // Testing workaround #[storage(write)] fn set_debug_timestamp(timestamp: u64); + + #[storage(read, write)] + fn set_redstone_config(config: RedstoneConfig); } pub struct Price { @@ -16,6 +19,15 @@ pub struct Price { pub time: u64, } +pub struct RedstoneConfig { + /// Contract address + pub contract_id: ContractId, + /// Price feed ID + pub price_id: u256, + /// Precision + pub precision: u32, +} + impl Price { pub fn new(price: u64, time: u64) -> Self { Self { diff --git a/test-utils/src/interfaces/oracle.rs b/test-utils/src/interfaces/oracle.rs index f9067d1..247673d 100644 --- a/test-utils/src/interfaces/oracle.rs +++ b/test-utils/src/interfaces/oracle.rs @@ -13,18 +13,29 @@ pub const ORACLE_TIMEOUT: u64 = 14400; pub mod oracle_abi { use super::*; - use fuels::prelude::{Account, TxPolicies}; + use fuels::{ + prelude::{Account, TxPolicies}, + programs::calls::ContractDependency, + types::errors::Error, + }; pub async fn get_price( oracle: &Oracle, pyth: &PythCore, - redstone: &RedstoneCore, + redstone: &Option>, ) -> CallResponse { let tx_params = TxPolicies::default().with_tip(1); + + let mut with_contracts: Vec<&dyn ContractDependency> = Vec::new(); + with_contracts.push(pyth); + if let Some(redstone) = redstone { + with_contracts.push(redstone); + } + oracle .methods() .get_price() - .with_contracts(&[pyth, redstone]) + .with_contracts(&with_contracts) .with_tx_policies(tx_params) .call() .await @@ -42,4 +53,20 @@ pub mod oracle_abi { .await .unwrap(); } + + pub async fn set_redstone_config( + oracle: &Oracle, + redstone: &RedstoneCore, + config: RedstoneConfig, + ) -> Result, Error> { + let tx_params = TxPolicies::default().with_tip(1); + + oracle + .methods() + .set_redstone_config(config) + .with_contracts(&[redstone]) + .with_tx_policies(tx_params) + .call() + .await + } } diff --git a/test-utils/src/setup.rs b/test-utils/src/setup.rs index fcd0e09..7fa4aa2 100644 --- a/test-utils/src/setup.rs +++ b/test-utils/src/setup.rs @@ -590,11 +590,9 @@ pub mod common { wallet: &WalletUnlocked, pyth: ContractId, pyth_price_id: Bits256, - redstone: ContractId, - redstone_precison: u32, - redstone_price_id: U256, fuel_vm_decimals: u32, debug: bool, + initializer: Identity, ) -> Oracle { let mut rng = rand::thread_rng(); let salt = rng.gen::<[u8; 32]>(); @@ -605,15 +603,11 @@ pub mod common { .unwrap() .with_PYTH_PRICE_ID(pyth_price_id) .unwrap() - .with_REDSTONE(redstone) - .unwrap() - .with_REDSTONE_PRICE_ID(redstone_price_id) - .unwrap() .with_DEBUG(debug) .unwrap() - .with_REDSTONE_PRECISION(redstone_precison) - .unwrap() .with_FUEL_DECIMAL_REPRESENTATION(fuel_vm_decimals) + .unwrap() + .with_INITIALIZER(initializer) .unwrap(); let id = Contract::load_from( @@ -751,11 +745,9 @@ pub mod common { &wallet, contracts.pyth_oracle, contracts.pyth_price_id, - contracts.redstone_oracle, - contracts.redstone_precision, - contracts.redstone_price_id, contracts.fuel_vm_decimals, false, + Identity::Address(wallet.address().into()), ) .await; @@ -799,11 +791,9 @@ pub mod common { &wallet, pyth.contract_id().into(), pyth_price_id, - redstone.contract_id().into(), - 9, - redstone_price_id, 9, true, + Identity::Address(wallet.address().into()), ) .await; pb.inc(); @@ -871,11 +861,9 @@ pub mod common { wallet, pyth.contract_id().into(), DEFAULT_PYTH_PRICE_ID, - redstone.contract_id().into(), - 9, - DEFAULT_REDSTONE_PRICE_ID, 9, true, + Identity::Address(wallet.address().into()), ) .await; let trove_manager = deploy_trove_manager_contract(wallet).await; diff --git a/test-utils/src/testing_query_manual.rs b/test-utils/src/testing_query_manual.rs index 6909cd3..36020ea 100644 --- a/test-utils/src/testing_query_manual.rs +++ b/test-utils/src/testing_query_manual.rs @@ -46,7 +46,7 @@ pub async fn testing_query() { let pyth = PythCore::new(id.clone(), wallet.clone()); let redstone = RedstoneCore::new(id, wallet.clone()); - let res = oracle_abi::get_price(&oracle, &pyth, &redstone).await; + let res = oracle_abi::get_price(&oracle, &pyth, &Some(redstone.clone())).await; println!("Result: {:#?}", res.value);