diff --git a/src/amm/helpers.cairo b/src/amm/helpers.cairo new file mode 100644 index 0000000..d05275c --- /dev/null +++ b/src/amm/helpers.cairo @@ -0,0 +1,32 @@ +use core::integer; +use integer::U128DivRem; + +fn pow(a: u128, b: u128) -> u128 { + let mut x: u128 = a; + let mut n = b; + + if n == 0 { + // 0**0 is undefined + assert(x > 0, 'Undefined pow action'); + + return 1; + } + + let mut y = 1; + + loop { + if n <= 1 { + break; + } + + let (div, rem) = integer::u128_safe_divmod(n, two); + + if rem == 1 { + y = x * y; + } + + x = x * x; + n = div; + }; + x * y +} \ No newline at end of file diff --git a/src/amm/pragma.cairo b/src/amm/pragma.cairo new file mode 100644 index 0000000..aed582c --- /dev/null +++ b/src/amm/pragma.cairo @@ -0,0 +1,308 @@ +// Copied from Carmine Options AMM + +mod Pragma { + use cubit::f128::types::fixed::{Fixed, FixedTrait}; + use option::OptionTrait; + use starknet::ContractAddress; + use starknet::contract_address::contract_address_const; + use starknet::get_block_timestamp; + use super::PragmaUtils::AggregationMode; + use super::PragmaUtils::Checkpoint; + use super::PragmaUtils::DataType; + use super::PragmaUtils::IOracleABIDispatcher; + use super::PragmaUtils::IOracleABIDispatcherTrait; + use super::PragmaUtils::PragmaPricesResponse; + + use super::PragmaUtils; + use super::super::helpers::pow; + use traits::{TryInto, Into}; + + type Timestamp = u64; + + const TOKEN_ETH_ADDRESS: felt252 = + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; + const TOKEN_USDC_ADDRESS: felt252 = + 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; + const TOKEN_STRK_ADDRESS: felt252 = + 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; + const TOKEN_WBTC_ADDRESS: felt252 = + 0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac; + + // Mainnet + const PRAGMA_ORACLE_ADDRESS: felt252 = + 0x2a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; // C1 version + + fn convert_from_int_to_Fixed(value: u128, decimals: u8) -> Fixed { + // Overflows (fails) when converting approx 1 million ETH, would need to use u256 for that, different code path needed. + + let denom = pow(5, decimals.into()); + let numer = pow(2, (64 - decimals).into()); + + let res = (value * numer) / denom; + + FixedTrait::from_felt(res.into()) + } + + + // @notice Returns Pragma key identifier for stablecoins + // @param quote_token_addr: Address of given stablecoin + // @return stablecoin_key: Stablecoin key identifier + fn _get_stablecoin_key(quote_token_addr: ContractAddress) -> Option { + if quote_token_addr == TOKEN_USDC_ADDRESS + .try_into() + .expect('Pragma/GSK - Failed to convert') { + Option::Some(PragmaUtils::PRAGMA_USDC_USD_KEY) + } else { + Option::None(()) + } + } + + // @notice Returns Pragma key identifier for spot pairs + // @param quote_token_addr: Address of quote token in given ticker + // @param base_token_addr: Address of base token in given ticker + // @return stablecoin_key: Spot pair key identifier + fn _get_ticker_key( + quote_token_addr: ContractAddress, base_token_addr: ContractAddress + ) -> felt252 { + if base_token_addr.into() == TOKEN_ETH_ADDRESS { + if quote_token_addr.into() == TOKEN_USDC_ADDRESS { + PragmaUtils::PRAGMA_ETH_USD_KEY + } else { + 0 + } + } else if base_token_addr.into() == TOKEN_WBTC_ADDRESS { + if quote_token_addr.into() == TOKEN_USDC_ADDRESS { + PragmaUtils::PRAGMA_WBTC_USD_KEY + } else { + 0 + } + } else if base_token_addr.into() == TOKEN_STRK_ADDRESS { + if quote_token_addr.into() == TOKEN_USDC_ADDRESS { + PragmaUtils::PRAGMA_STRK_USD_KEY + } else { + 0 + } + } else { + 0 + } + } + + // @notice Returns current Pragma median price for given key + // @dev This function does not account for stablecoin divergence + // @param key: Pragma key identifier + // @return median_price: Pragma current median price in Fixed + fn _get_pragma_median_price(key: felt252) -> Fixed { + let res: PragmaPricesResponse = IOracleABIDispatcher { + contract_address: PRAGMA_ORACLE_ADDRESS.try_into().expect('Pragma/_GPMP - Cant convert') + } + .get_data(DataType::SpotEntry(key), AggregationMode::Median(())); + + let curr_time = get_block_timestamp(); + let time_diff = if curr_time < res.last_updated_timestamp { + 0 + } else { + curr_time - res.last_updated_timestamp + }; + + assert(time_diff < 3600, 'Pragma/_GPMP - Price too old'); + + convert_from_int_to_Fixed( + res.price, res.decimals.try_into().expect('Pragma/_GPMP - decimals err') + ) + } + + // @notice Returns current Pragma median price for given key + // @dev This function accounts for stablecoin divergence + // @param quote_token_addr: Address of quote token in given ticker + // @param base_token_addr: Address of base token in given ticker + // @return median_price: Pragma current median price in Fixed + fn get_pragma_median_price( + quote_token_addr: ContractAddress, base_token_addr: ContractAddress, + ) -> Fixed { + // STRK/ETH gets special treatment + if base_token_addr.into() == TOKEN_ETH_ADDRESS + && quote_token_addr.into() == TOKEN_STRK_ADDRESS { + let eth_in_usd = _get_pragma_median_price(PragmaUtils::PRAGMA_ETH_USD_KEY); + let strk_in_usd = _get_pragma_median_price(PragmaUtils::PRAGMA_STRK_USD_KEY); + + let eth_in_strk = eth_in_usd / strk_in_usd; + + return eth_in_strk; + } else { + let key = _get_ticker_key(quote_token_addr, base_token_addr); + + let res = _get_pragma_median_price(key); + account_for_stablecoin_divergence(res, quote_token_addr, 0) + } + } + + + // @notice Returns terminal Pragma median price for given key + // @dev This function does not account for stablecoin divergence + // @param key: Pragma key identifier + // @param maturity: Timestamp for which to get the terminal price + // @return median_price: Pragma terminal median price in Fixed + fn _get_pragma_terminal_price(key: felt252, maturity: Timestamp) -> Fixed { + let (res, _) = IOracleABIDispatcher { + contract_address: PRAGMA_ORACLE_ADDRESS.try_into().expect('Pragma/_GPMP - Cant convert') + } + .get_last_checkpoint_before( + DataType::SpotEntry(key), maturity, AggregationMode::Median(()) + ); + + let decs = IOracleABIDispatcher { + contract_address: PRAGMA_ORACLE_ADDRESS.try_into().expect('Pragma/_GPMP - Cant convert') + } + .get_decimals(DataType::SpotEntry(key)); + + assert(decs > 0, 'Pragma/GPTP - decs zero'); + + let time_diff = maturity - res.timestamp; + + assert(time_diff < 7200, 'Pragma/GPTP - Term price old'); + assert(res.value > 0_u128, 'Pragma/GPTP - Price <= 0'); + convert_from_int_to_Fixed(res.value, decs.try_into().unwrap()) + } + + + // @notice Returns terminal Pragma median price for given key + // @dev This function accounts for stablecoin divergence + // @param quote_token_addr: Address of quote token in given ticker + // @param base_token_addr: Address of base token in given ticker + // @param maturity: Timestamp for which to get the terminal price + // @return median_price: Pragma terminal median price in Fixed + fn get_pragma_terminal_price( + quote_token_addr: ContractAddress, base_token_addr: ContractAddress, maturity: Timestamp + ) -> Fixed { + if base_token_addr.into() == TOKEN_ETH_ADDRESS + && quote_token_addr.into() == TOKEN_STRK_ADDRESS { + let eth_in_usd = _get_pragma_terminal_price(PragmaUtils::PRAGMA_ETH_USD_KEY, maturity); + let strk_in_usd = _get_pragma_terminal_price( + PragmaUtils::PRAGMA_STRK_USD_KEY, maturity + ); + + let eth_in_strk = eth_in_usd / strk_in_usd; + + return eth_in_strk; + } else { + let key = _get_ticker_key(quote_token_addr, base_token_addr); + let res = _get_pragma_terminal_price(key, maturity); + return account_for_stablecoin_divergence(res, quote_token_addr, maturity); + } + } + + // @notice Takes in current or terminal price and returns it after accounting for stablecoin divergence + // @param price: Current or terminal price, Fixed + // @param quote_token_addr: Address of quote token in given ticker + // @param maturity: Timestamp for which to get the terminal price if its used, "0" for spot price + // @return price: Price, accounted for stablecoin divergence + fn account_for_stablecoin_divergence( + price: Fixed, quote_token_addr: ContractAddress, maturity: Timestamp + ) -> Fixed { + let key = _get_stablecoin_key(quote_token_addr); + + match key { + Option::Some(key) => { + let stable_coin_price = if maturity == 0 { + _get_pragma_median_price(key) + } else { + _get_pragma_terminal_price(key, maturity) + }; + return price / stable_coin_price; + }, + // If key is zero, it means that quote_token isn't stablecoin(or at least one we use) + Option::None(_) => { return price; } + } + } + + // @notice Calls Pragma to set checkpoint + // @param key: Pragma key identifier + fn set_pragma_checkpoint(key: felt252) { + IOracleABIDispatcher { + contract_address: PRAGMA_ORACLE_ADDRESS.try_into().expect('Pragma/_GPMP - Cant convert') + } + .set_checkpoint(DataType::SpotEntry(key), AggregationMode::Median(())) + } + + // @notice Calls Pragma to set checkpoints we use + fn set_pragma_required_checkpoints() { + // Just add needed checkpoints here + set_pragma_checkpoint(PragmaUtils::PRAGMA_ETH_USD_KEY); + set_pragma_checkpoint(PragmaUtils::PRAGMA_USDC_USD_KEY); + set_pragma_checkpoint(PragmaUtils::PRAGMA_WBTC_USD_KEY); + set_pragma_checkpoint(PragmaUtils::PRAGMA_STRK_USD_KEY); + } +} + +///////////////////// +// Pragma Structs/Abi +///////////////////// + +pub(crate) mod PragmaUtils { + #[starknet::interface] + trait IOracleABI { + fn get_decimals(self: @TContractState, data_type: DataType) -> u32; + fn get_data( + self: @TContractState, data_type: DataType, aggregation_mode: AggregationMode + ) -> PragmaPricesResponse; + fn get_last_checkpoint_before( + self: @TContractState, + data_type: DataType, + timestamp: u64, + aggregation_mode: AggregationMode, + ) -> (Checkpoint, u64); + fn set_checkpoint( + ref self: TContractState, data_type: DataType, aggregation_mode: AggregationMode + ); + } + + #[derive(Drop, Copy, Serde)] + pub(crate) enum DataType { + SpotEntry: felt252, + FutureEntry: (felt252, u64), + GenericEntry: felt252, + } + + #[derive(Serde, Drop, Copy)] + pub(crate) struct PragmaPricesResponse { + pub price: u128, + pub decimals: u32, + pub last_updated_timestamp: u64, + pub num_sources_aggregated: u32, + pub expiration_timestamp: Option, + } + + #[derive(Serde, Drop)] + pub(crate) struct Checkpoint { + pub timestamp: u64, + pub value: u128, + pub aggregation_mode: AggregationMode, + pub num_sources_aggregated: u32, + } + + #[derive(Serde, Drop, Copy)] + enum AggregationMode { + Median: (), + Mean: (), + Error: (), + } + + // Pragma keys + // Spot + pub(crate) const PRAGMA_WBTC_USD_KEY: felt252 = 6287680677296296772; + pub(crate) const PRAGMA_ETH_USD_KEY: felt252 = 19514442401534788; + pub(crate) const PRAGMA_SOL_USD_KEY: felt252 = 23449611697214276; + pub(crate) const PRAGMA_AVAX_USD_KEY: felt252 = 4708022307469480772; + pub(crate) const PRAGMA_DOGE_USD_KEY: felt252 = 4922231280211678020; + pub(crate) const PRAGMA_SHIB_USD_KEY: felt252 = 6001127052081976132; + pub(crate) const PRAGMA_BNB_USD_KEY: felt252 = 18663394631832388; + pub(crate) const PRAGMA_ADA_USD_KEY: felt252 = 18370920243876676; + pub(crate) const PRAGMA_XRP_USD_KEY: felt252 = 24860302295520068; + pub(crate) const PRAGMA_MATIC_USD_KEY: felt252 = 1425106761739050242884; + pub(crate) const PRAGMA_STRK_USD_KEY: felt252 = 6004514686061859652; + + // Stablecoins + pub(crate) const PRAGMA_USDT_USD_KEY: felt252 = 6148333044652921668; + pub(crate) const PRAGMA_DAI_USD_KEY: felt252 = 19212080998863684; + pub(crate) const PRAGMA_USDC_USD_KEY: felt252 = 6148332971638477636; +} diff --git a/src/lib.cairo b/src/lib.cairo index 605cd6a..b75d644 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -8,3 +8,7 @@ mod traits; mod types; mod upgrades; pub mod vecarm; +pub mod amm { + mod pragma; + mod helpers; +} diff --git a/src/options.cairo b/src/options.cairo index 4d0f692..8aa5544 100644 --- a/src/options.cairo +++ b/src/options.cairo @@ -1,5 +1,8 @@ +use amm::pragma::get_pragma_median_price; use amm_governance::types::FutureOption; +use amm_governance::types::OptionType; use core::array::SpanTrait; +use cubit::f128::types::fixed::{Fixed, FixedTrait}; // Handles adding new options to the AMM and linking them to the liquidity pool. // I have chosen this perhaps rather complex type layout in expectation of generating the options soon – // – first generating FutureOption, then generating everything from Pragma data @@ -9,6 +12,7 @@ use core::array::SpanTrait; use starknet::contract_address::{ContractAddress}; + #[starknet::interface] trait IOptionDeployer { fn add_options( @@ -16,6 +20,19 @@ trait IOptionDeployer { ); } +#[derive(Copy, Drop, Serde)] +struct PreparedOption { + name_long: felt252, + name_short: felt252, + maturity: u64, + strike_price: Fixed, + option_type: OptionType, + lptoken_address: ContractAddress, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + initial_volatility: Fixed +} + #[starknet::contract] mod OptionDeployer { @@ -56,7 +73,7 @@ mod OptionDeployer { #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { fn add_option( - ref self: ContractState, amm_address: ContractAddress, option: @FutureOption + ref self: ContractState, amm_address: ContractAddress, option: @PreparedOption ) { let o = *option; @@ -111,17 +128,68 @@ mod OptionDeployer { o.initial_volatility ); } + + fn get_name(option: @FutureOption) -> (felt252, felt252) { + ('name long', 'namshort') + } + + fn get_rounding_unit( + quote_token_address: ContractAddress, base_token_address: ContractAddress + ) -> Fixed { + FixedTrait::ONE() * 1000 + } } #[abi(embed_v0)] impl OptionDeployer of super::IOptionDeployer { fn add_options( - ref self: ContractState, amm_address: ContractAddress, mut options: Span + ref self: ContractState, amm_address: ContractAddress, options: Span ) { - // TODO use block hash from block_hash syscall as salt // actually doable with the new syscall + let mut prepared_options = ArrayTrait::new(); + loop { match options.pop_front() { - Option::Some(option) => { self.add_option(amm_address, option); }, + Option::Some(option) => { + let spot_price = get_pragma_median_price( + *option.quote_token_address, *option.base_token_address + ); + let strike_price = spot_price + * (FixedTrait::ONE() + option.strike_price_offset); + let rounding_unit = self + .get_rounding_unit( + option.quote_token_address, option.base_token_address + ); + let strike_price_rounded = if (strike_price % rounding_unit) > 50 { + strike_price + (rounding_unit - strike_price % rounding_unit) + } else { + strike_price - (strike_price % rounding_unit) + }; + + let (name_long, name_short) = self.get_name(@option); + let prepared_option = super::PreparedOption { + name_long: name_long, + name_short: name_short, + maturity: *option.maturity, + strike_price: strike_price, + option_type: option.option_type, + lptoken_address: *option.lptoken_address, + quote_token_address: *option.quote_token_address, + base_token_address: *option.base_token_address, + initial_volatility: option.initial_volatility + }; + + prepared_options.append(prepared_option); + }, + Option::None(()) => { break (); }, + }; + }; + + let mut prepared_options_span = prepared_options.span(); + loop { + match prepared_options_span.pop_front() { + Option::Some(prepared_option) => { + self.add_option(amm_address, prepared_option); + }, Option::None(()) => { break (); }, }; } diff --git a/src/types.cairo b/src/types.cairo index 33e06d0..90e8414 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -4,13 +4,10 @@ use starknet::ContractAddress; pub type OptionSide = felt252; pub type OptionType = felt252; -// TODO add auto generation of FutureOption structs once string contacenation exists #[derive(Copy, Drop, Serde)] pub struct FutureOption { - pub name_long: felt252, - pub name_short: felt252, pub maturity: u64, - pub strike_price: Fixed, + pub strike_price_offset: Fixed, pub option_type: OptionType, pub lptoken_address: ContractAddress, pub quote_token_address: ContractAddress,