-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support chainlink via dual oracle setup (#85)
- Loading branch information
1 parent
d1a959c
commit 787f344
Showing
11 changed files
with
481 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.