diff --git a/src/amm_core/amm.cairo b/src/amm_core/amm.cairo index 0cfe1c7..92f95f2 100644 --- a/src/amm_core/amm.cairo +++ b/src/amm_core/amm.cairo @@ -58,6 +58,7 @@ mod AMM { >, pool_definition_from_lptoken_address: LegacyMap, latest_oracle_price: LegacyMap::<(ContractAddress, ContractAddress), (Fixed, u64)>, + historical_oracle_price_cache: LegacyMap::<(ContractAddress, ContractAddress, u64), Fixed>, } fn emit_event, impl DropImpl: traits::Drop>( diff --git a/src/amm_core/oracles/agg.cairo b/src/amm_core/oracles/agg.cairo index 8df3581..68887ad 100644 --- a/src/amm_core/oracles/agg.cairo +++ b/src/amm_core/oracles/agg.cairo @@ -1,16 +1,20 @@ mod OracleAgg { use starknet::ContractAddress; use starknet::get_block_info; + use starknet::get_block_timestamp; - use cubit::f128::types::fixed::{Fixed}; + use cubit::f128::types::fixed::{Fixed, FixedTrait}; use carmine_protocol::types::basic::{Timestamp}; use carmine_protocol::amm_core::oracles::pragma::Pragma::{ get_pragma_median_price, get_pragma_terminal_price }; + use carmine_protocol::amm_core::oracles::chainlink::Chainlink; + use carmine_protocol::amm_core::state::State::{ - read_latest_oracle_price, write_latest_oracle_price + read_latest_oracle_price, write_latest_oracle_price, read_historical_oracle_price_cache, + write_historical_oracle_price_cache, }; // @notice Returns current spot price for given ticker (quote and base token) @@ -31,10 +35,20 @@ mod OracleAgg { if last_price_block_num == curr_block { last_price } else { - let price_pragma = get_pragma_median_price(quote_token_addr, base_token_addr); - write_latest_oracle_price(base_token_addr, quote_token_addr, price_pragma, curr_block); - - price_pragma + let price = + match Chainlink::get_chainlink_current_price(quote_token_addr, base_token_addr) { + // If there is a chainlink price, use that one + // Chainlink price fetch will fail only if the addresses are expected to have some price, + // but no prices were fetched + Option::Some(chainlink_price) => chainlink_price, + // If there is no chainlink price, try to use pragma (which will fail if the addresses are unknown) + // Pragma is used only for tokens that do not have chainlink source + Option::None(()) => get_pragma_median_price(quote_token_addr, base_token_addr) + }; + + write_latest_oracle_price(base_token_addr, quote_token_addr, price, curr_block); + + price } } @@ -45,8 +59,23 @@ mod OracleAgg { fn get_terminal_price( quote_token_addr: ContractAddress, base_token_addr: ContractAddress, maturity: Timestamp ) -> Fixed { - let price_pragma = get_pragma_terminal_price(quote_token_addr, base_token_addr, maturity); - price_pragma + let historical_price = read_historical_oracle_price_cache( + base_token_addr, quote_token_addr, maturity + ); + + if historical_price != FixedTrait::ZERO() { + return historical_price; + } else { + let price_pragma = get_pragma_terminal_price( + quote_token_addr, base_token_addr, maturity + ); + + write_historical_oracle_price_cache( + base_token_addr, quote_token_addr, maturity, price_pragma + ); + + price_pragma + } } } diff --git a/src/amm_core/oracles/chainlink.cairo b/src/amm_core/oracles/chainlink.cairo new file mode 100644 index 0000000..2804e35 --- /dev/null +++ b/src/amm_core/oracles/chainlink.cairo @@ -0,0 +1,117 @@ +mod Chainlink { + use starknet::ContractAddress; + + + use cubit::f128::types::fixed::Fixed; + use cubit::f128::types::fixed::FixedTrait; + + use carmine_protocol::amm_core::constants::TOKEN_USDC_ADDRESS; + use carmine_protocol::amm_core::constants::TOKEN_ETH_ADDRESS; + use carmine_protocol::amm_core::constants::TOKEN_WBTC_ADDRESS; + use carmine_protocol::amm_core::constants::TOKEN_STRK_ADDRESS; + + use carmine_protocol::amm_core::oracles::oracle_helpers::convert_from_int_to_Fixed; + + use super::ChainlinkUtils; + + fn get_chainlink_current_price( + quote_token_addr: ContractAddress, base_token_addr: ContractAddress, + ) -> Option { + let base_chainlink_proxy_address = ChainlinkUtils::get_chainlink_proxy_address( + base_token_addr + )?; + let quote_chainlink_proxy_address = ChainlinkUtils::get_chainlink_proxy_address( + quote_token_addr + )?; + + let base_price = ChainlinkUtils::get_current_price_from_proxy(base_chainlink_proxy_address); + let quote_price = ChainlinkUtils::get_current_price_from_proxy( + quote_chainlink_proxy_address + ); + + assert(base_price != 0, 'Unable to fetch base price'); + assert(quote_price != 0, 'Unable to fetch quote price'); + + let base_price_fixed = convert_from_int_to_Fixed( + base_price, ChainlinkUtils::CHAINLINK_DECIMALS + ); + let quote_price_fixed = convert_from_int_to_Fixed( + quote_price, ChainlinkUtils::CHAINLINK_DECIMALS + ); + + Option::Some(base_price_fixed / quote_price_fixed) + } +} + + +mod ChainlinkUtils { + use starknet::ContractAddress; + use starknet::get_block_timestamp; + use core::panic_with_felt252; + + use carmine_protocol::amm_core::constants::TOKEN_USDC_ADDRESS; + use carmine_protocol::amm_core::constants::TOKEN_ETH_ADDRESS; + use carmine_protocol::amm_core::constants::TOKEN_WBTC_ADDRESS; + use carmine_protocol::amm_core::constants::TOKEN_STRK_ADDRESS; + + + const CHAINLINK_DECIMALS: u8 = 8; + + #[derive(Copy, Drop, Serde, PartialEq, starknet::Store)] + struct Round { + round_id: felt252, + answer: u128, + block_num: u64, + started_at: u64, + updated_at: u64, + } + + #[starknet::interface] + trait IAggregatorProxy { + fn latest_round_data(self: @TContractState) -> Round; + fn round_data(self: @TContractState, round_id: felt252) -> Round; + fn description(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; + fn latest_answer(self: @TContractState) -> u128; + } + + mod Mainnet { + const CHAINLINK_WBTC_USD_ADDRESS: felt252 = + 0x6275040a2913e2fe1a20bead3feb40694920a7fea98e956b042e082b9e1adad; + const CHAINLINK_ETH_USD_ADDRESS: felt252 = + 0x6b2ef9b416ad0f996b2a8ac0dd771b1788196f51c96f5b000df2e47ac756d26; + const CHAINLINK_STRK_USD_ADDRESS: felt252 = + 0x76a0254cdadb59b86da3b5960bf8d73779cac88edc5ae587cab3cedf03226ec; + const CHAINLINK_USDC_USD_ADDRESS: felt252 = + 0x72495dbb867dd3c6373820694008f8a8bff7b41f7f7112245d687858b243470; + } + + fn get_current_price_from_proxy(proxy: ContractAddress) -> u128 { + let contract = IAggregatorProxyDispatcher { contract_address: proxy }; + let res = contract.latest_round_data(); + + let now = get_block_timestamp(); + assert(res.updated_at > now - 2 * 3600, 'Oracle data stale'); + + res.answer + } + + fn get_chainlink_proxy_address(token_address: ContractAddress) -> Option { + let address_felt: felt252 = token_address.into(); + + if address_felt == TOKEN_ETH_ADDRESS { + return Option::Some(Mainnet::CHAINLINK_ETH_USD_ADDRESS.try_into().unwrap()); + } + if address_felt == TOKEN_WBTC_ADDRESS { + return Option::Some(Mainnet::CHAINLINK_WBTC_USD_ADDRESS.try_into().unwrap()); + } + if address_felt == TOKEN_STRK_ADDRESS { + return Option::Some(Mainnet::CHAINLINK_STRK_USD_ADDRESS.try_into().unwrap()); + } + if address_felt == TOKEN_USDC_ADDRESS { + return Option::Some(Mainnet::CHAINLINK_USDC_USD_ADDRESS.try_into().unwrap()); + } + + Option::None(()) + } +} diff --git a/src/amm_core/state.cairo b/src/amm_core/state.cairo index 9cd0f56..2a15088 100644 --- a/src/amm_core/state.cairo +++ b/src/amm_core/state.cairo @@ -53,6 +53,8 @@ mod State { use carmine_protocol::amm_core::amm::AMM::latest_oracle_price; use carmine_protocol::amm_core::amm::AMM::latest_oracle_priceContractMemberStateTrait; + use carmine_protocol::amm_core::amm::AMM::historical_oracle_price_cache; + use carmine_protocol::amm_core::amm::AMM::historical_oracle_price_cacheContractMemberStateTrait; use carmine_protocol::amm_core::amm::AMM; @@ -558,6 +560,38 @@ mod State { .write((base_token_address, quote_token_address), (price, current_block)) } + // @notice Reads the historical oracle price for given token pair and maturity + // @param base_token_address: Address of the base token + // @param quote_token_address: Address of the quote token + // @param maturity: Maturity for which to fetch historical price + // @returns price: Historical price at maturity + fn read_historical_oracle_price_cache( + base_token_address: ContractAddress, + quote_token_address: ContractAddress, + maturity: Timestamp + ) -> Fixed { + let state = AMM::unsafe_new_contract_state(); + state + .historical_oracle_price_cache + .read((base_token_address, quote_token_address, maturity)) + } + + // @notice Writes the historical oracle price for given token pair and maturity + // @param base_token_address: Address of the base token + // @param quote_token_address: Address of the quote token + // @param maturity: Maturity for which to fetch historical price + // @param price: Price at maturity which'll be stored + fn write_historical_oracle_price_cache( + base_token_address: ContractAddress, + quote_token_address: ContractAddress, + maturity: Timestamp, + price: Fixed + ) { + let mut state = AMM::unsafe_new_contract_state(); + state + .historical_oracle_price_cache + .write((base_token_address, quote_token_address, maturity), price) + } // @notice Returns Option struct with addition info based on several option parameters // @param lptoken_address: Address of liquidity pool that corresponds to the option diff --git a/src/lib.cairo b/src/lib.cairo index c3cc3d9..dee9de7 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -11,6 +11,7 @@ mod amm_core { mod oracles { mod agg; mod pragma; + mod chainlink; mod oracle_helpers; } mod pricing {