Skip to content

Commit

Permalink
feat: pragma price sanity and staleness check (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
xJonathanLEI authored Sep 20, 2023
1 parent dce146e commit 10dfb3d
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 6 deletions.
36 changes: 32 additions & 4 deletions src/oracles/pragma_oracle_adapter.cairo
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
mod errors {
const INVALID_TIMESTAMP: felt252 = 'PRAGMA_INVALID_TIMESTAMP';
const STALED_PRICE: felt252 = 'PRAGMA_STALED_PRICE';
const ZERO_PRICE: felt252 = 'PRAGMA_ZERO_PRICE';
}

#[starknet::contract]
mod PragmaOracleAdapter {
use traits::Into;
use integer::u64_checked_sub;
use option::OptionTrait;
use traits::{Into, TryInto};

use starknet::ContractAddress;
use starknet::{ContractAddress, get_block_timestamp};

// Hack to simulate the `crate` keyword
use super::super::super as crate;
Expand All @@ -13,20 +21,24 @@ mod PragmaOracleAdapter {
};
use crate::libraries::{pow, safe_math};

use super::errors;

// These two consts MUST be the same.
const TARGET_DECIMALS: felt252 = 8;
const TARGET_DECIMALS_U256: u256 = 8;

#[storage]
struct Storage {
oracle: ContractAddress,
pair: felt252
pair: felt252,
timeout: u64
}

#[constructor]
fn constructor(ref self: ContractState, oracle: ContractAddress, pair: felt252) {
fn constructor(ref self: ContractState, oracle: ContractAddress, pair: felt252, timeout: u64) {
self.oracle.write(oracle);
self.pair.write(pair);
self.timeout.write(timeout);
}

#[external(v0)]
Expand All @@ -47,6 +59,22 @@ mod PragmaOracleAdapter {
let median = IPragmaOracleDispatcher {
contract_address: oracle_addr
}.get_spot_median(pair_key);
assert(median.price != 0, errors::ZERO_PRICE);

// Block times are usually behind real world time by a bit. It's possible that the reported
// last updated timestamp is in the (very near) future.
let block_time: u64 = get_block_timestamp();
let last_updated_timestamp: u64 = median
.last_updated_timestamp
.try_into()
.expect(errors::INVALID_TIMESTAMP);

let time_elasped: u64 = match u64_checked_sub(block_time, last_updated_timestamp) {
Option::Some(value) => value,
Option::None => 0,
};
let timeout = self.timeout.read();
assert(time_elasped <= timeout, errors::STALED_PRICE);

let scaled_price = scale_price(median.price, median.decimals);
PriceWithUpdateTime { price: scaled_price, update_time: median.last_updated_timestamp }
Expand Down
32 changes: 30 additions & 2 deletions tests/deploy.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ use starknet::ContractAddress;
use starknet::syscalls::deploy_syscall;

use zklend::interfaces::{
IInterestRateModelDispatcher, IMarketDispatcher, ITestContractDispatcher, IZTokenDispatcher
IInterestRateModelDispatcher, IMarketDispatcher, IPriceOracleSourceDispatcher,
ITestContractDispatcher, IZTokenDispatcher
};
use zklend::irms::default_interest_rate_model::DefaultInterestRateModel;
use zklend::market::Market;
use zklend::oracles::pragma_oracle_adapter::PragmaOracleAdapter;
use zklend::z_token::ZToken;

use tests::mock;
use tests::mock::{
IAccountDispatcher, IERC20Dispatcher, IFlashLoanHandlerDispatcher, IMockMarketDispatcher,
IMockPriceOracleDispatcher
IMockPragmaOracleDispatcher, IMockPriceOracleDispatcher
};

fn deploy_account() -> IAccountDispatcher {
Expand Down Expand Up @@ -65,6 +67,18 @@ fn deploy_mock_price_oracle() -> IMockPriceOracleDispatcher {
IMockPriceOracleDispatcher { contract_address }
}

fn deploy_mock_pragma_oracle() -> IMockPragmaOracleDispatcher {
let (contract_address, _) = deploy_syscall(
mock::mock_pragma_oracle::MockPragmaOracle::TEST_CLASS_HASH.try_into().unwrap(),
0,
Default::default().span(),
false
)
.unwrap();

IMockPragmaOracleDispatcher { contract_address }
}

fn deploy_mock_market() -> IMockMarketDispatcher {
let (contract_address, _) = deploy_syscall(
mock::mock_market::MockMarket::TEST_CLASS_HASH.try_into().unwrap(),
Expand All @@ -89,6 +103,20 @@ fn deploy_flash_loan_handler() -> IFlashLoanHandlerDispatcher {
IFlashLoanHandlerDispatcher { contract_address }
}

fn deploy_pragma_oracle_adapter(
oracle: ContractAddress, pair: felt252, timeout: felt252
) -> IPriceOracleSourceDispatcher {
let (contract_address, _) = deploy_syscall(
PragmaOracleAdapter::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![oracle.into(), pair, timeout].span(),
false
)
.unwrap();

IPriceOracleSourceDispatcher { contract_address }
}

fn deploy_default_interest_rate_model(
slope_0: felt252, slope_1: felt252, y_intercept: felt252, optimal_rate: felt252
) -> IInterestRateModelDispatcher {
Expand Down
3 changes: 3 additions & 0 deletions tests/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#[cfg(test)]
mod default_interest_rate_model;

#[cfg(test)]
mod pragma_oracle_adapter;

#[cfg(test)]
mod z_token;

Expand Down
18 changes: 18 additions & 0 deletions tests/mock.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ mod mock_market;

mod mock_price_oracle;

mod mock_pragma_oracle;

mod flash_loan_handler;

mod erc20;
Expand Down Expand Up @@ -185,6 +187,22 @@ trait IMockPriceOracle<TContractState> {
);
}

#[starknet::interface]
trait IMockPragmaOracle<TContractState> {
//
// External
//

fn set_price(
ref self: TContractState,
pair_id: felt252,
price: felt252,
decimals: felt252,
last_updated_timestamp: felt252,
num_sources_aggregated: felt252
);
}

#[starknet::interface]
trait IFlashLoanHandler<TContractState> {
//
Expand Down
47 changes: 47 additions & 0 deletions tests/mock/mock_pragma_oracle.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#[starknet::contract]
mod MockPragmaOracle {
use starknet::ContractAddress;

use zklend::interfaces::{IPragmaOracle, PragmaOracleSpotMedian};

use super::super::IMockPragmaOracle;

#[storage]
struct Storage {
pair_id: felt252,
price: felt252,
decimals: felt252,
last_updated_timestamp: felt252,
num_sources_aggregated: felt252
}

#[external(v0)]
impl IPragmaOracleImpl of IPragmaOracle<ContractState> {
fn get_spot_median(self: @ContractState, pair_id: felt252) -> PragmaOracleSpotMedian {
PragmaOracleSpotMedian {
price: self.price.read(),
decimals: self.decimals.read(),
last_updated_timestamp: self.last_updated_timestamp.read(),
num_sources_aggregated: self.num_sources_aggregated.read()
}
}
}

#[external(v0)]
impl IMockPragmaOracleImpl of IMockPragmaOracle<ContractState> {
fn set_price(
ref self: ContractState,
pair_id: felt252,
price: felt252,
decimals: felt252,
last_updated_timestamp: felt252,
num_sources_aggregated: felt252
) {
self.pair_id.write(pair_id);
self.price.write(price);
self.decimals.write(decimals);
self.last_updated_timestamp.write(last_updated_timestamp);
self.num_sources_aggregated.write(num_sources_aggregated);
}
}
}
42 changes: 42 additions & 0 deletions tests/pragma_oracle_adapter.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use test::test_utils::assert_eq;

use zklend::interfaces::IPriceOracleSourceDispatcherTrait;

use tests::deploy;
use tests::mock::IMockPragmaOracleDispatcherTrait;

#[test]
#[available_gas(30000000)]
fn test_not_staled_price() {
let mock_pragma_oracle = deploy::deploy_mock_pragma_oracle();
let pragma_oracle_adpater = deploy::deploy_pragma_oracle_adapter(
mock_pragma_oracle.contract_address, 'BTC/USD', 500
);

// Set last update timestamp to 100
mock_pragma_oracle.set_price('BTC/USD', 10000_00000000, 8, 100, 5);

// Current block time is 0. It's okay for the updated time to be in the future.
pragma_oracle_adpater.get_price();

// It's still acceptable when the time elasped equals timeout.
starknet::testing::set_block_timestamp(600);
pragma_oracle_adpater.get_price();
}

#[test]
#[available_gas(30000000)]
#[should_panic(expected: ('PRAGMA_STALED_PRICE', 'ENTRYPOINT_FAILED'))]
fn test_staled_price() {
let mock_pragma_oracle = deploy::deploy_mock_pragma_oracle();
let pragma_oracle_adpater = deploy::deploy_pragma_oracle_adapter(
mock_pragma_oracle.contract_address, 'BTC/USD', 500
);

// Set last update timestamp to 100
mock_pragma_oracle.set_price('BTC/USD', 10000_00000000, 8, 100, 5);

// One second over timeout will be rejected.
starknet::testing::set_block_timestamp(601);
pragma_oracle_adpater.get_price();
}

0 comments on commit 10dfb3d

Please sign in to comment.