Skip to content

Commit

Permalink
feat: support chainlink via dual oracle setup (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
zklend-tech authored Aug 26, 2024
1 parent d1a959c commit 787f344
Show file tree
Hide file tree
Showing 11 changed files with 481 additions and 2 deletions.
2 changes: 2 additions & 0 deletions scripts/compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ compile zklend::market Market
compile zklend::z_token ZToken
compile zklend::default_price_oracle DefaultPriceOracle
compile zklend::irms::default_interest_rate_model DefaultInterestRateModel
compile zklend::oracles::dual_oracle_adapter DualOracleAdapter
compile zklend::oracles::chainlink_oracle_adapter ChainlinkOracleAdapter
compile zklend::oracles::pragma_oracle_adapter PragmaOracleAdapter

if [ -n "$USER_ID" ] && [ -n "$GROUP_ID" ]; then
Expand Down
22 changes: 22 additions & 0 deletions src/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ trait IPragmaOracle<TContractState> {
fn get_data_median(self: @TContractState, data_type: PragmaDataType) -> PragmaPricesResponse;
}

#[starknet::interface]
trait IChainlinkOracle<TContractState> {
fn latest_round_data(self: @TContractState) -> ChainlinkPricesResponse;

fn decimals(self: @TContractState) -> u8;
}

#[starknet::interface]
trait IERC20<TContractState> {
fn decimals(self: @TContractState) -> felt252;
Expand Down Expand Up @@ -349,3 +356,18 @@ struct PragmaPricesResponse {
num_sources_aggregated: u32,
expiration_timestamp: Option<u64>,
}

#[derive(Drop, Serde)]
struct ChainlinkPricesResponse {
/// The unique identifier of the data round.
round_id: felt252,
/// The actual data provided by the data feed, representing the latest price of an asset in the
/// case of a price feed.
answer: u128,
/// The block number at which the data was recorded on the blockchain.
block_num: u64,
/// The Unix timestamp indicating when the data round started.
started_at: u64,
/// The Unix timestamp indicating when the data was last updated.
updated_at: u64,
}
4 changes: 4 additions & 0 deletions src/oracles.cairo
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
mod dual_oracle_adapter;

mod pragma_oracle_adapter;

mod chainlink_oracle_adapter;
93 changes: 93 additions & 0 deletions src/oracles/chainlink_oracle_adapter.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
mod errors {
const STALED_PRICE: felt252 = 'CHAINLINK_STALED_PRICE';
const ZERO_PRICE: felt252 = 'CHAINLINK_ZERO_PRICE';
}

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

use starknet::{ContractAddress, get_block_timestamp};

// Hack to simulate the `crate` keyword
use super::super::super as crate;

use crate::interfaces::{
IChainlinkOracleDispatcher, IChainlinkOracleDispatcherTrait, IPriceOracleSource,
ChainlinkPricesResponse, PriceWithUpdateTime
};
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,
timeout: u64
}

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

#[abi(embed_v0)]
impl IPriceOracleSourceImpl of IPriceOracleSource<ContractState> {
fn get_price(self: @ContractState) -> felt252 {
get_data(self).price
}

fn get_price_with_time(self: @ContractState) -> PriceWithUpdateTime {
get_data(self)
}
}

fn get_data(self: @ContractState) -> PriceWithUpdateTime {
let oracle_addr = self.oracle.read();

let round_data = IChainlinkOracleDispatcher { contract_address: oracle_addr }
.latest_round_data();
assert(round_data.answer != 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 time_elasped: u64 = match u64_checked_sub(block_time, round_data.updated_at) {
Option::Some(value) => value,
Option::None => 0,
};
let timeout = self.timeout.read();
assert(time_elasped <= timeout, errors::STALED_PRICE);

let decimals = IChainlinkOracleDispatcher { contract_address: oracle_addr }.decimals();

let scaled_price = scale_price(round_data.answer.into(), decimals.into());
PriceWithUpdateTime { price: scaled_price, update_time: round_data.updated_at.into() }
}

fn scale_price(price: felt252, decimals: felt252) -> felt252 {
if decimals == TARGET_DECIMALS {
price
} else {
let should_scale_up = Into::<_, u256>::into(decimals) < TARGET_DECIMALS_U256;
if should_scale_up {
let multiplier = pow::ten_pow(TARGET_DECIMALS - decimals);
let scaled_price = safe_math::mul(price, multiplier);
scaled_price
} else {
let multiplier = pow::ten_pow(decimals - TARGET_DECIMALS);
let scaled_price = safe_math::div(price, multiplier);
scaled_price
}
}
}
}
91 changes: 91 additions & 0 deletions src/oracles/dual_oracle_adapter.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
mod errors {
const DIVERGING_UPSTREAMS: felt252 = 'DUAL_DIVERGING_UPSTREAMS';
}

/// An oracle implementation that implements `IPriceOracleSource` but is backed by two upstream
/// `IPriceOracleSource` oracles. The final output is the average price of the upstreams as long as
/// they don't diverge beyond a configured threshold; otherwise an error is thrown.
#[starknet::contract]
mod DualOracleAdapter {
use starknet::ContractAddress;

// Hack to simulate the `crate` keyword
use super::super::super as crate;

use crate::interfaces::{
IPriceOracleSource, IPriceOracleSourceDispatcher, IPriceOracleSourceDispatcherTrait,
PriceWithUpdateTime
};
use crate::libraries::safe_decimal_math;

use super::errors;

#[storage]
struct Storage {
upstream_0: ContractAddress,
upstream_1: ContractAddress,
threshold: felt252,
}

#[constructor]
fn constructor(
ref self: ContractState,
upstream_0: ContractAddress,
upstream_1: ContractAddress,
threshold: felt252
) {
self.upstream_0.write(upstream_0);
self.upstream_1.write(upstream_1);
self.threshold.write(threshold);
}

#[abi(embed_v0)]
impl IPriceOracleSourceImpl of IPriceOracleSource<ContractState> {
fn get_price(self: @ContractState) -> felt252 {
get_data(self).price
}

fn get_price_with_time(self: @ContractState) -> PriceWithUpdateTime {
get_data(self)
}
}

fn get_data(self: @ContractState) -> PriceWithUpdateTime {
// There is no need to scale the prices as all `IPriceOracleSource` implementations are
// guaranteed to return at target decimals.
let price_0 = IPriceOracleSourceDispatcher { contract_address: self.upstream_0.read() }
.get_price_with_time();
let price_1 = IPriceOracleSourceDispatcher { contract_address: self.upstream_1.read() }
.get_price_with_time();

let price_0_u256: u256 = price_0.price.into();
let price_1_u256: u256 = price_1.price.into();

let (lower_felt, lower, upper) = if price_0_u256 < price_1_u256 {
(price_0.price, price_0_u256, price_1_u256)
} else {
(price_1.price, price_1_u256, price_0_u256)
};

assert(
lower
+ Into::<
_, u256
>::into(safe_decimal_math::mul(lower_felt, self.threshold.read())) >= upper,
errors::DIVERGING_UPSTREAMS
);

PriceWithUpdateTime {
price: ((price_0_u256 + price_1_u256) / 2).try_into().unwrap(),
update_time: if Into::<
_, u256
>::into(price_0.update_time) < Into::<
_, u256
>::into(price_1.update_time) {
price_0.update_time
} else {
price_1.update_time
},
}
}
}
42 changes: 42 additions & 0 deletions tests/chainlink_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::IMockChainlinkOracleDispatcherTrait;

#[test]
#[available_gas(30000000)]
fn test_not_staled_price() {
let mock_chainlink_oracle = deploy::deploy_mock_chainlink_oracle();
let chainlink_oracle_adpater = deploy::deploy_chainlink_oracle_adapter(
mock_chainlink_oracle.contract_address, 500
);

// Set last update timestamp to 100
mock_chainlink_oracle.set_price(5, 10000_00000000, 1, 0, 100);

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

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

#[test]
#[available_gas(30000000)]
#[should_panic(expected: ('CHAINLINK_STALED_PRICE', 'ENTRYPOINT_FAILED'))]
fn test_staled_price() {
let mock_chainlink_oracle = deploy::deploy_mock_chainlink_oracle();
let chainlink_oracle_adpater = deploy::deploy_chainlink_oracle_adapter(
mock_chainlink_oracle.contract_address, 500
);

// Set last update timestamp to 100
mock_chainlink_oracle.set_price(5, 10000_00000000, 1, 0, 100);

// One second over timeout will be rejected.
starknet::testing::set_block_timestamp(601);
chainlink_oracle_adpater.get_price();
}
47 changes: 45 additions & 2 deletions tests/deploy.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ use zklend::interfaces::{
};
use zklend::irms::default_interest_rate_model::DefaultInterestRateModel;
use zklend::market::Market;
use zklend::oracles::chainlink_oracle_adapter::ChainlinkOracleAdapter;
use zklend::oracles::dual_oracle_adapter::DualOracleAdapter;
use zklend::oracles::pragma_oracle_adapter::PragmaOracleAdapter;
use zklend::z_token::ZToken;

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

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

fn deploy_mock_chainlink_oracle() -> IMockChainlinkOracleDispatcher {
let (contract_address, _) = deploy_syscall(
mock::mock_chainlink_oracle::MockChainlinkOracle::TEST_CLASS_HASH.try_into().unwrap(),
0,
Default::default().span(),
false
)
.unwrap();

IMockChainlinkOracleDispatcher { contract_address }
}

fn deploy_mock_pragma_oracle() -> IMockPragmaOracleDispatcher {
let (contract_address, _) = deploy_syscall(
mock::mock_pragma_oracle::MockPragmaOracle::TEST_CLASS_HASH.try_into().unwrap(),
Expand Down Expand Up @@ -103,6 +118,34 @@ fn deploy_flash_loan_handler() -> IFlashLoanHandlerDispatcher {
IFlashLoanHandlerDispatcher { contract_address }
}

fn deploy_dual_oracle_adapter(
upstream_0: ContractAddress, upstream_1: ContractAddress, threshold: felt252
) -> IPriceOracleSourceDispatcher {
let (contract_address, _) = deploy_syscall(
DualOracleAdapter::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![upstream_0.into(), upstream_1.into(), threshold].span(),
false
)
.unwrap();

IPriceOracleSourceDispatcher { contract_address }
}

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

IPriceOracleSourceDispatcher { contract_address }
}

fn deploy_pragma_oracle_adapter(
oracle: ContractAddress, pair: felt252, timeout: felt252
) -> IPriceOracleSourceDispatcher {
Expand Down
Loading

0 comments on commit 787f344

Please sign in to comment.