Skip to content

Commit

Permalink
Making redstone oracle optional
Browse files Browse the repository at this point in the history
If there is no redstone price feed, we want to default to using the last pyth price
- tests
- authorization tests
  • Loading branch information
diyahir committed Oct 21, 2024
1 parent fdd71d1 commit 7e8cb03
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 89 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 34 additions & 12 deletions contracts/oracle-contract/src/main.sw
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use libraries::{
oracle_interface::RedstoneCore,
oracle_interface::{
Oracle,
RedstoneConfig,
},
};
use std::{block::timestamp, constants::ZERO_B256,};
Expand All @@ -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;
Expand All @@ -66,37 +63,45 @@ storage {
},
// Used for simulating different timestamps during testing
debug_timestamp: u64 = 0,
redstone_config: Option<RedstoneConfig> = 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(),
false => timestamp(),
};
// 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();
let redstone_price_u64 = redstone_prices.get(0).unwrap();
// 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 {
Expand Down Expand Up @@ -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
Expand Down
127 changes: 127 additions & 0 deletions contracts/oracle-contract/tests/authorization.rs
Original file line number Diff line number Diff line change
@@ -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<WalletUnlocked>,
PythCore<WalletUnlocked>,
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"
);
}
Loading

0 comments on commit 7e8cb03

Please sign in to comment.