diff --git a/Scarb.toml b/Scarb.toml index 5b44ce4..ff82ae5 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -5,11 +5,14 @@ description = "Code for Carmine Option AMM" cairo-version = "2.2.0" homepage = "https://github.com/CarmineOptions/protocol-cairo1" +[cairo] +sierra-replace-ids = false [dependencies] cubit = { git = "https://github.com/chepelau/cubit.git", branch = "main" } -snforge_std = { git = "https://github.com/tensojka/starknet-foundry.git", branch = "patch-1"} +snforge_std = { path = "/workspaces/protocol-cairo1/starknet-foundry/snforge_std"} openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } +alexandria_sorting = { git = "https://github.com/keep-starknet-strange/alexandria", rev = "27fbf5b" } starknet = ">=2.2.0" [[target.starknet-contract]] diff --git a/src/ilhedge.cairo b/src/ilhedge.cairo new file mode 100644 index 0000000..a3de074 --- /dev/null +++ b/src/ilhedge.cairo @@ -0,0 +1,7 @@ +mod amm_curve; +mod carmine; +mod constants; +mod contract; +mod erc20; +mod hedging; +mod helpers; diff --git a/src/ilhedge/amm_curve.cairo b/src/ilhedge/amm_curve.cairo new file mode 100644 index 0000000..480a360 --- /dev/null +++ b/src/ilhedge/amm_curve.cairo @@ -0,0 +1,97 @@ +use carmine_protocol::ilhedge::helpers::convert_from_int_to_Fixed; + +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +use debug::PrintTrait; + +// Computes the portfolio value if it moved from current (fetched from Empiric) to the specific strike +// Notional is in ETH, it's the amount of ETH that needs to be hedged. +// Also converts the excess to the hedge result asset +// Result is USDC in case of puts, ETH in case of calls. +fn compute_portfolio_value(curr_price: Fixed, notional: u128, calls: bool, strike: Fixed) -> Fixed { + let x = convert_from_int_to_Fixed(notional, 18); // in ETH + let y = x * curr_price; + let k = x * y; + + // price = y / x + // k = x * y + // 1500 = 3000 / 2 + let y_at_strike = k.sqrt() * strike.sqrt(); + let x_at_strike = k.sqrt() / strike.sqrt(); // actually sqrt is a hint so basically free. + convert_excess(x_at_strike, y_at_strike, x, strike, curr_price, calls) +} + +use carmine_protocol::ilhedge::helpers::percent; +#[test] +#[available_gas(3000000)] +fn test_compute_portfolio_value() { + // k = 1500, initial price 1500. + // price being considered 1700. + let ONEETH = 1000000000000000000; + let res = compute_portfolio_value( + FixedTrait::from_unscaled_felt(1500), ONEETH, true, FixedTrait::from_unscaled_felt(1700) + ); + assert(res < FixedTrait::ONE(), 'loss must happen due to IL'); + assert(res > percent(95), 'loss weirdly high'); + + // k = 1500, initial price 1500. + // price being considered 1300. + let res = compute_portfolio_value( + FixedTrait::from_unscaled_felt(1500), ONEETH, false, FixedTrait::from_unscaled_felt(1300) + ); + assert(res < FixedTrait::from_unscaled_felt(1500), 'loss must happen'); + assert(res > FixedTrait::from_unscaled_felt(1492), 'loss too high'); + + // repro attempt + let res = compute_portfolio_value( + FixedTrait::from_unscaled_felt(1650), ONEETH, true, FixedTrait::from_unscaled_felt(1800) + ); + assert(res < FixedTrait::ONE(), 'loss must happen due to IL'); + assert(res > percent(97), 'loss weirdly high'); +} + +// converts the excess to the hedge result asset (calls -> convert to eth) +// ensures the call asset / put assset (based on calls bool) is equal to notional (or equivalent amount in puts) +// returns amount of asset that isn't fixed +fn convert_excess( + call_asset: Fixed, + put_asset: Fixed, + notional: Fixed, + strike: Fixed, + entry_price: Fixed, + calls: bool +) -> Fixed { + if calls { + assert(strike > entry_price, 'certainly calls?'); + assert(call_asset < notional, 'hedging at odd strikes, warning'); + let extra_put_asset = if ((notional * entry_price) > put_asset) { // TODO understand + (notional * entry_price) - put_asset + } else { + put_asset - (notional * entry_price) + }; + + (extra_put_asset / strike) + call_asset + } else { // DEBUG CURRENTLY HERE + assert(strike < entry_price, 'certainly puts?'); + let extra_call_asset = if (call_asset > notional) { // I don't fucking get this. + call_asset - notional + } else { + notional - call_asset + }; + (extra_call_asset * strike) + put_asset + } +} + +#[test] +#[available_gas(3000000)] +fn test_convert_excess() { + let x_at_strike = FixedTrait::from_felt(0x10c7ebc96a119c8bd); // 1.0488088481662097 + let y_at_strike = FixedTrait::from_felt(0x6253699028cfb2bd398); // 1573.2132722467607 + let x = FixedTrait::from_felt(0x100000000000000000); // 1 + let strike = FixedTrait::from_felt(0x5dc0000000000000000); // 1500 + let curr_price = FixedTrait::from_felt(0x6720000000000000000); // 1650 + let calls = false; + let res = convert_excess(x_at_strike, y_at_strike, x, strike, curr_price, calls); + 'res'.print(); + res.print(); // 0x66e6d320524ee400704 = 1646.426544496075 +} diff --git a/src/ilhedge/carmine.cairo b/src/ilhedge/carmine.cairo new file mode 100644 index 0000000..6e0aa8f --- /dev/null +++ b/src/ilhedge/carmine.cairo @@ -0,0 +1,312 @@ +// Module responsible for interactions with the AMM + +use option::OptionTrait; +use traits::TryInto; +use array::ArrayTrait; +use traits::Into; + +use starknet::{ContractAddress, get_block_timestamp}; + +use carmine_protocol::ilhedge::constants::{TOKEN_ETH_ADDRESS, TOKEN_USDC_ADDRESS}; +use carmine_protocol::ilhedge::helpers::FixedHelpersTrait; + +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + + +fn buy_option(strike: Fixed, notional: u128, expiry: u64, calls: bool, amm_address: ContractAddress) { + let optiontype = if (calls) { + 0 + } else { + 1 + }; + IAMMDispatcher { contract_address: amm_address } + .trade_open( + optiontype, + FixedHelpersTrait::to_legacyMath(strike), + expiry.into(), + 0, + notional.into(), + TOKEN_USDC_ADDRESS.try_into().unwrap(), // testnet + TOKEN_ETH_ADDRESS.try_into().unwrap(), + (notional / 5).into(), + (get_block_timestamp() + 42).into() + ); +} + +use debug::PrintTrait; + +fn price_option(strike: Fixed, notional: u128, expiry: u64, calls: bool, amm_address: ContractAddress) -> u128 { + let optiontype = if (calls) { + 0 + } else { + 1 + }; + let lpt_addr_felt: felt252 = if (calls) { // testnet + 0x03b176f8e5b4c9227b660e49e97f2d9d1756f96e5878420ad4accd301dd0cc17 + } else { + 0x30fe5d12635ed696483a824eca301392b3f529e06133b42784750503a24972 + }; + 'pricing option, maturity:'.print(); + expiry.print(); + 'notional:'.print(); + notional.print(); + 'strike:'.print(); + FixedHelpersTrait::to_legacyMath(strike).print(); + let option = LegacyOption { + option_side: 0, + maturity: expiry.into(), + strike_price: FixedHelpersTrait::to_legacyMath(strike), + quote_token_address: TOKEN_USDC_ADDRESS.try_into().unwrap(), + base_token_address: TOKEN_ETH_ADDRESS.try_into().unwrap(), // testnet + option_type: optiontype + }; + let (before_fees, after_fees) = IAMMDispatcher { + contract_address: amm_address + } + .get_total_premia(option, lpt_addr_felt.try_into().unwrap(), notional.into(), 0); + 'call to get_total_premia fin'.print(); + after_fees.try_into().unwrap() +} + +fn available_strikes( + expiry: u64, quote_token_addr: ContractAddress, base_token_addr: ContractAddress, calls: bool +) -> Array { + // TODO implement + if (calls) { + let mut res = ArrayTrait::::new(); + res.append(FixedTrait::from_unscaled_felt(1700)); + res.append(FixedTrait::from_unscaled_felt(1800)); + res.append(FixedTrait::from_unscaled_felt(1900)); + res.append(FixedTrait::from_unscaled_felt(2000)); + res + } else { + let mut res = ArrayTrait::::new(); + res.append(FixedTrait::from_unscaled_felt(1300)); + res.append(FixedTrait::from_unscaled_felt(1400)); + res.append(FixedTrait::from_unscaled_felt(1500)); + res.append(FixedTrait::from_unscaled_felt(1600)); + res + } +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct LegacyOption { + option_side: OptionSide, + maturity: felt252, + strike_price: LegacyStrike, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType +} + +type LegacyStrike = Math64x61_; +type Math64x61_ = felt252; // legacy, for AMM trait definition +type OptionSide = felt252; +type OptionType = felt252; + +#[starknet::interface] +trait IAMM { + fn trade_open( + ref self: TContractState, + option_type: OptionType, + strike_price: Math64x61_, + maturity: felt252, + option_side: OptionSide, + option_size: felt252, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + limit_total_premia: Math64x61_, + tx_deadline: felt252, + ) -> Math64x61_; + fn trade_close( + ref self: TContractState, + option_type: OptionType, + strike_price: Math64x61_, + maturity: felt252, + option_side: OptionSide, + option_size: felt252, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + limit_total_premia: Math64x61_, + tx_deadline: felt252, + ) -> Math64x61_; + fn trade_settle( + ref self: TContractState, + option_type: OptionType, + strike_price: Math64x61_, + maturity: felt252, + option_side: OptionSide, + option_size: felt252, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + ); + fn is_option_available( + self: @TContractState, + lptoken_address: ContractAddress, + option_side: OptionSide, + strike_price: Math64x61_, + maturity: felt252, + ) -> felt252; + fn set_trading_halt(ref self: TContractState, new_status: felt252); + fn get_trading_halt(self: @TContractState) -> felt252; + fn add_lptoken( + ref self: TContractState, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + lptoken_address: ContractAddress, + pooled_token_addr: ContractAddress, + volatility_adjustment_speed: Math64x61_, + max_lpool_bal: u256, + ); + fn add_option( + ref self: TContractState, + option_side: OptionSide, + maturity: felt252, + strike_price: Math64x61_, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + lptoken_address: ContractAddress, + option_token_address_: ContractAddress, + initial_volatility: Math64x61_, + ); + fn get_option_token_address( + self: @TContractState, + lptoken_address: ContractAddress, + option_side: OptionSide, + maturity: felt252, + strike_price: Math64x61_, + ) -> ContractAddress; + fn get_lptokens_for_underlying( + ref self: TContractState, pooled_token_addr: ContractAddress, underlying_amt: u256, + ) -> u256; + fn get_underlying_for_lptokens( + self: @TContractState, pooled_token_addr: ContractAddress, lpt_amt: u256 + ) -> u256; + fn get_available_lptoken_addresses(self: @TContractState, order_i: felt252) -> ContractAddress; + fn get_all_options(self: @TContractState, lptoken_address: ContractAddress) -> Array; + fn get_all_non_expired_options_with_premia( + self: @TContractState, lptoken_address: ContractAddress + ) -> Array; + fn get_option_with_position_of_user( + self: @TContractState, user_address: ContractAddress + ) -> Array; + fn get_all_lptoken_addresses(self: @TContractState,) -> Array; + fn get_value_of_pool_position( + self: @TContractState, lptoken_address: ContractAddress + ) -> Math64x61_; + // fn get_value_of_position( + // option: Option, + // position_size: Math64x61_, + // option_type: OptionType, + // current_volatility: Math64x61_, + // ) -> Math64x61_; + // fn get_all_poolinfo() -> Array; + // fn get_option_info_from_addresses( + // lptoken_address: ContractAddress, option_token_address: ContractAddress, + // ) -> Option; + // fn get_user_pool_infos(user: ContractAddress) -> Array; + fn deposit_liquidity( + ref self: TContractState, + pooled_token_addr: ContractAddress, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + amount: u256, + ); + fn withdraw_liquidity( + ref self: TContractState, + pooled_token_addr: ContractAddress, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + lp_token_amount: u256, + ); + fn get_unlocked_capital(self: @TContractState, lptoken_address: ContractAddress) -> u256; + fn expire_option_token_for_pool( + ref self: TContractState, + lptoken_address: ContractAddress, + option_side: OptionSide, + strike_price: Math64x61_, + maturity: felt252, + ); + fn getAdmin(self: @TContractState); + fn set_max_option_size_percent_of_voladjspd( + ref self: TContractState, max_opt_size_as_perc_of_vol_adjspd: felt252 + ); + fn get_max_option_size_percent_of_voladjspd(self: @TContractState) -> felt252; + fn get_lpool_balance(self: @TContractState, lptoken_address: ContractAddress) -> u256; + fn get_max_lpool_balance(self: @TContractState, pooled_token_addr: ContractAddress) -> u256; + fn set_max_lpool_balance( + ref self: TContractState, pooled_token_addr: ContractAddress, max_lpool_bal: u256 + ); + fn get_pool_locked_capital(self: @TContractState, lptoken_address: ContractAddress) -> u256; + // fn get_available_options(lptoken_address: ContractAddress, order_i: felt252) -> Option; + fn get_available_options_usable_index( + self: @TContractState, lptoken_address: ContractAddress, starting_index: felt252 + ) -> felt252; + fn get_lptoken_address_for_given_option( + self: @TContractState, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + ) -> ContractAddress; + //fn get_pool_definition_from_lptoken_address(lptoken_addres: ContractAddress) -> Pool; + fn get_option_type(self: @TContractState, lptoken_address: ContractAddress) -> OptionType; + fn get_pool_volatility_separate( + self: @TContractState, + lptoken_address: ContractAddress, + maturity: felt252, + strike_price: Math64x61_, + ) -> Math64x61_; + fn get_underlying_token_address( + self: @TContractState, lptoken_address: ContractAddress + ) -> ContractAddress; + fn get_available_lptoken_addresses_usable_index( + self: @TContractState, starting_index: felt252 + ) -> felt252; + fn get_pool_volatility_adjustment_speed( + self: @TContractState, lptoken_address: ContractAddress + ) -> Math64x61_; + fn set_pool_volatility_adjustment_speed_external( + ref self: TContractState, lptoken_address: ContractAddress, new_speed: Math64x61_, + ); + fn get_pool_volatility( + self: @TContractState, lptoken_address: ContractAddress, maturity: felt252 + ) -> Math64x61_; + fn get_pool_volatility_auto( + self: @TContractState, + lptoken_address: ContractAddress, + maturity: felt252, + strike_price: Math64x61_, + ) -> Math64x61_; + fn get_option_position( + self: @TContractState, + lptoken_address: ContractAddress, + option_side: OptionSide, + maturity: felt252, + strike_price: Math64x61_ + ) -> felt252; + fn get_total_premia( + self: @TContractState, + option: LegacyOption, + lptoken_address: ContractAddress, + position_size: u256, + is_closing: felt252, + ) -> (Math64x61_, Math64x61_); // before_fees, including_fees + fn black_scholes( + self: @TContractState, + sigma: felt252, + time_till_maturity_annualized: felt252, + strike_price: felt252, + underlying_price: felt252, + risk_free_rate_annualized: felt252, + is_for_trade: felt252, // bool + ) -> (felt252, felt252); + fn empiric_median_price(self: @TContractState, key: felt252) -> Math64x61_; + fn initializer(ref self: TContractState, proxy_admin: ContractAddress); + fn upgrade(ref self: TContractState, new_implementation: felt252); + fn setAdmin(ref self: TContractState, address: felt252); + fn getImplementationHash(self: @TContractState,) -> felt252; +} diff --git a/src/ilhedge/constants.cairo b/src/ilhedge/constants.cairo new file mode 100644 index 0000000..1827976 --- /dev/null +++ b/src/ilhedge/constants.cairo @@ -0,0 +1,6 @@ +const TOKEN_ETH_ADDRESS: felt252 = + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; // mainnet & testnet. base token in case of eth/usdc +const TOKEN_USDC_ADDRESS: felt252 = + 0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426; // testnet + +// testnet c0, not used: const AMM_ADDR: felt252 = 0x042a7d485171a01b8c38b6b37e0092f0f096e9d3f945c50c77799171916f5a54; diff --git a/src/ilhedge/contract.cairo b/src/ilhedge/contract.cairo new file mode 100644 index 0000000..6fdeb45 --- /dev/null +++ b/src/ilhedge/contract.cairo @@ -0,0 +1,218 @@ +use core::array::SpanTrait; +use starknet::ContractAddress; +use starknet::ClassHash; + +#[starknet::interface] +trait IILHedge { + fn hedge( + ref self: TContractState, + notional: u128, + quote_token_addr: ContractAddress, + base_token_addr: ContractAddress, + expiry: u64 + ); + fn price_hedge( + self: @TContractState, + notional: u128, + quote_token_addr: ContractAddress, + base_token_addr: ContractAddress, + expiry: u64 + ) -> (u128, u128); + fn upgrade(ref self: TContractState, impl_hash: ClassHash); + fn get_pragma_price(self: @TContractState) -> u128 ; +} + +#[starknet::contract] +mod ILHedge { + use array::{ArrayTrait, SpanTrait}; + use option::OptionTrait; + use traits::{Into, TryInto}; + + use starknet::ContractAddress; + use starknet::ClassHash; + use starknet::{get_caller_address, get_contract_address}; + use starknet::syscalls::{replace_class_syscall}; + + use cubit::f128::types::fixed::{Fixed, FixedTrait}; + + use carmine_protocol::ilhedge::amm_curve::compute_portfolio_value; + use carmine_protocol::ilhedge::constants::{TOKEN_ETH_ADDRESS}; + use carmine_protocol::ilhedge::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use carmine_protocol::ilhedge::hedging::{ + iterate_strike_prices, buy_options_at_strike_to_hedge_at, + price_options_at_strike_to_hedge_at + }; + use carmine_protocol::ilhedge::pragma::Pragma::get_pragma_median_price; + use carmine_protocol::ilhedge::helpers::{convert_from_Fixed_to_int, convert_from_int_to_Fixed}; + + #[storage] + struct Storage { + amm_address: ContractAddress + } + + #[constructor] + fn constructor(ref self: ContractState, amm_address: ContractAddress) { + self.amm_address.write(amm_address); + } + + use cubit::f128; + #[external(v0)] + impl ILHedge of super::IILHedge { + fn get_pragma_price(self: @ContractState) -> u128 { + let base_token_addr: ContractAddress = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7.try_into().unwrap(); + let quote_token_addr: ContractAddress = 0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426.try_into().unwrap(); + get_pragma_median_price(quote_token_addr, base_token_addr).mag + } + + fn hedge( + ref self: ContractState, + notional: u128, + quote_token_addr: ContractAddress, + base_token_addr: ContractAddress, + expiry: u64 + ) { + let pricing: (u128, u128) = ILHedge::price_hedge( + @self, notional, quote_token_addr, base_token_addr, expiry + ); + let (cost_quote, cost_base) = pricing; + let eth = IERC20Dispatcher { contract_address: TOKEN_ETH_ADDRESS.try_into().unwrap() }; + eth.transferFrom(get_caller_address(), get_contract_address(), cost_base.into()); + eth.approve(self.amm_address.read(), cost_base.into()); // approve AMM to spend + let curr_price = get_pragma_median_price(quote_token_addr, base_token_addr); + // iterate available strike prices and get them into pairs of (bought strike, at which strike one should be hedged) + let mut strikes_calls = iterate_strike_prices( + curr_price, quote_token_addr, base_token_addr, expiry, true + ); + //let mut strikes_puts = iterate_strike_prices( + // quote_token_addr, base_token_addr, expiry, false + //); + + let mut already_hedged: Fixed = FixedTrait::ZERO(); + + loop { + match strikes_calls.pop_front() { + Option::Some(strike_pair) => { + let (tobuy, tohedge) = *strike_pair; + // compute how much portf value would be at each hedged strike + // converts the excess to the hedge result asset (calls -> convert to eth) + // for each strike + let portf_val_calls = compute_portfolio_value( + curr_price, notional, true, tohedge + ); // value of second asset is precisely as much as user put in, expecting conversion + assert(portf_val_calls > FixedTrait::ZERO(), 'portf val calls < 0?'); + let notional_fixed = convert_from_int_to_Fixed(notional, 18); + let amount_to_hedge = notional_fixed + - portf_val_calls; // difference between converted and leftover amounts is how much one should be hedging against + // buy this much of previous strike price (fst in iterate_strike_prices()) + buy_options_at_strike_to_hedge_at( + tobuy, + tohedge, + amount_to_hedge, + expiry, + quote_token_addr, + base_token_addr, + self.amm_address.read(), + true + ); + }, + Option::None(()) => { + break; + } + }; + }; + } + + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { + let caller: ContractAddress = get_caller_address(); + let owner: ContractAddress = + 0x001dd8e12b10592676E109C85d6050bdc1E17adf1be0573a089E081C3c260eD9 + .try_into() + .unwrap(); + assert(owner == caller, 'invalid caller'); + replace_class_syscall(impl_hash); + } + + fn price_hedge( + self: @ContractState, + notional: u128, + quote_token_addr: ContractAddress, + base_token_addr: ContractAddress, + expiry: u64 + ) -> (u128, u128) { + let curr_price = get_pragma_median_price(quote_token_addr, base_token_addr); + + // iterate available strike prices and get them into pairs of (bought strike, at which strike one should be hedged) + let mut strikes_calls = iterate_strike_prices( + curr_price, quote_token_addr, base_token_addr, expiry, true + ); + let mut strikes_puts = iterate_strike_prices( + curr_price, quote_token_addr, base_token_addr, expiry, false + ); + + let mut already_hedged: Fixed = FixedTrait::ZERO(); + let mut cost_quote = 0; + let mut cost_base = 0; + + loop { + match strikes_calls.pop_front() { + Option::Some(strike_pair) => { + let (tobuy, tohedge) = *strike_pair; + // compute how much portf value would be at each hedged strike + // converts the excess to the hedge result asset (calls -> convert to eth) + // for each strike + let portf_val_calls = compute_portfolio_value( + curr_price, notional, true, tohedge + ); // value of second asset is precisely as much as user put in, expecting conversion + assert(portf_val_calls > FixedTrait::ZERO(), 'portf val calls < 0?'); + assert(portf_val_calls.sign == false, 'portf val neg??'); + let notional_fixed = convert_from_int_to_Fixed( + notional, 18 + ); // difference between converted and premia amounts is how much one should be hedging against + //assert((notional_fixed - portf_val_calls) > already_hedged, "amounttohedge neg??"); // can't compile with it for some reason?? + let amount_to_hedge = (notional_fixed - portf_val_calls) - already_hedged; + already_hedged += amount_to_hedge; + cost_base += + price_options_at_strike_to_hedge_at( + tobuy, tohedge, amount_to_hedge, expiry, self.amm_address.read(), true + ); + }, + Option::None(()) => { + break; + } + }; + }; + loop { + match strikes_puts.pop_front() { + Option::Some(strike_pair) => { + let (tobuy, tohedge) = *strike_pair; + // compute how much portf value would be at each hedged strike + // converts the excess to the hedge result asset (calls -> convert to eth) + // for each strike + let portf_val_puts = compute_portfolio_value( + curr_price, notional, false, tohedge + ); // value of second asset is precisely as much as user put in, expecting conversion + assert(portf_val_puts > FixedTrait::ZERO(), 'portf val puts < 0?'); + assert( + portf_val_puts < (convert_from_int_to_Fixed(notional, 18) * curr_price), + 'some loss expected' + ); // portf_val_puts is in USDC + assert(portf_val_puts.sign == false, 'portf val neg??'); + let notional_fixed = convert_from_int_to_Fixed( + notional, 6 + ); // difference between converted and premia amounts is how much one should be hedging against + let amount_to_hedge = notional_fixed + - portf_val_puts; // in USDC, with decimals + cost_quote += + price_options_at_strike_to_hedge_at( + tobuy, tohedge, amount_to_hedge, expiry, self.amm_address.read(), false + ); + }, + Option::None(()) => { + break; + } + }; + }; + (cost_quote, cost_base) + } + } +} diff --git a/src/ilhedge/erc20.cairo b/src/ilhedge/erc20.cairo new file mode 100644 index 0000000..d70de75 --- /dev/null +++ b/src/ilhedge/erc20.cairo @@ -0,0 +1,18 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IERC20 { + fn name(self: @TContractState) -> felt252; + fn symbol(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; + fn totalSupply(self: @TContractState) -> u256; + fn mint(ref self: TContractState, to: ContractAddress, amount: u256); + fn burn(ref self: TContractState, account: ContractAddress, amount: u256); + fn balanceOf(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn transferFrom( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; +} diff --git a/src/ilhedge/hedging.cairo b/src/ilhedge/hedging.cairo new file mode 100644 index 0000000..5732da2 --- /dev/null +++ b/src/ilhedge/hedging.cairo @@ -0,0 +1,193 @@ +use core::traits::Into; +use array::{ArrayTrait, SpanTrait}; + +use alexandria_sorting::merge_sort::merge; +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +use carmine_protocol::ilhedge::carmine::{available_strikes, buy_option, price_option}; +use carmine_protocol::ilhedge::helpers::{convert_from_Fixed_to_int, reverse}; + +use starknet::ContractAddress; + + +fn iterate_strike_prices( + curr_price: Fixed, + quote_token_addr: ContractAddress, + base_token_addr: ContractAddress, + expiry: u64, + calls: bool +) -> Span<(Fixed, Fixed)> { + let MAX_HEDGE_CALLS: Fixed = FixedTrait::from_unscaled_felt(2400); + let MAX_HEDGE_PUTS: Fixed = FixedTrait::from_unscaled_felt(800); + + let mut strike_prices_arr = available_strikes(expiry, quote_token_addr, base_token_addr, calls); + let mut res = ArrayTrait::<(Fixed, Fixed)>::new(); + let mut strike_prices = merge(strike_prices_arr).span(); + if (!calls) { + strike_prices = reverse(strike_prices); + } + let mut i = 0; + loop { + if (i + 1 == strike_prices.len()) { + res + .append( + (*strike_prices.at(i), if (calls) { + MAX_HEDGE_CALLS + } else { + MAX_HEDGE_PUTS + }) + ); + break; + } + // If both strikes are above (in case of puts) or below (in case of calls) current price, no point – so throw away. + // This is handled by the other type of the options. + let tobuy = *strike_prices.at(i); + let tohedge = *strike_prices.at(i + 1); + if (calls && (tobuy > curr_price || tohedge > curr_price)) { + let pair: (Fixed, Fixed) = (tobuy, tohedge); + res.append(pair); + } else if (!calls && (tobuy < curr_price || tohedge < curr_price)) { + let pair: (Fixed, Fixed) = (tobuy, tohedge); + res.append(pair); + } + + i += 1; + }; + res.span() +} + + +use carmine_protocol::ilhedge::constants::{TOKEN_ETH_ADDRESS, TOKEN_USDC_ADDRESS}; +use traits::TryInto; +use option::OptionTrait; +use debug::PrintTrait; +#[test] +#[available_gas(3000000)] +fn test_iterate_strike_prices_calls() { + let res = iterate_strike_prices( + FixedTrait::from_unscaled_felt(1650), + TOKEN_USDC_ADDRESS.try_into().unwrap(), + TOKEN_ETH_ADDRESS.try_into().unwrap(), + 1693526399, + true + ); + assert(res.len() == 4, 'len?'); + let (a, b) = res.at(0); + assert(*a.mag == 1700 * 18446744073709551616, 1); + assert(*b.mag == 1800 * 18446744073709551616, 2); + let (a, b) = res.at(1); + assert(*a.mag == 1800 * 18446744073709551616, 3); + assert(*b.mag == 1900 * 18446744073709551616, 4); + let (a, b) = res.at(2); + assert(*a.mag == 1900 * 18446744073709551616, 5); + assert(*b.mag == 2000 * 18446744073709551616, 6); + let (a, b) = res.at(3); + assert(*a.mag == 2000 * 18446744073709551616, 7); + assert(*b.mag == 2400 * 18446744073709551616, 8); +} + +#[test] +#[available_gas(3000000)] +fn test_iterate_strike_prices_puts() { + let res = iterate_strike_prices( + FixedTrait::from_unscaled_felt(1650), + TOKEN_USDC_ADDRESS.try_into().unwrap(), + TOKEN_ETH_ADDRESS.try_into().unwrap(), + 1693526399, + false + ); + assert(res.len() == 4, 'len 2?'); + let (a, b) = res.at(0); + assert(*a.mag == 1600 * 18446744073709551616, 1); + assert(*b.mag == 1500 * 18446744073709551616, 2); + let (a, b) = res.at(1); + assert(*a.mag == 1500 * 18446744073709551616, 3); + assert(*b.mag == 1400 * 18446744073709551616, 4); + let (a, b) = res.at(2); + assert(*a.mag == 1400 * 18446744073709551616, 5); + assert(*b.mag == 1300 * 18446744073709551616, 6); + let (a, b) = res.at(3); + assert(*a.mag == 1300 * 18446744073709551616, 5); + assert(*b.mag == 800 * 18446744073709551616, 6); +} + +// Calculates how much to buy at buystrike to get specified payoff at hedgestrike. Payoff is in quote token for puts, base token for calls. +// Put asset is expected to have 6 decimals. +fn how_many_options_at_strike_to_hedge_at( + to_buy_strike: Fixed, to_hedge_strike: Fixed, payoff: Fixed, calls: bool +) -> u128 { + if (calls) { + assert(to_hedge_strike > to_buy_strike, 'tohedge<=tobuy'); + let res = payoff / (to_hedge_strike - to_buy_strike); + convert_from_Fixed_to_int(res, 18) + } else { + assert(to_hedge_strike < to_buy_strike, 'tohedge>=tobuy'); + let res = payoff / (to_buy_strike - to_hedge_strike); + convert_from_Fixed_to_int(res, 18) + } +} + + +// Calculates how much to buy at buystrike to get specified payoff at hedgestrike. +// And buys via carmine module. +fn buy_options_at_strike_to_hedge_at( + to_buy_strike: Fixed, + to_hedge_strike: Fixed, + payoff: Fixed, + expiry: u64, + quote_token_addr: ContractAddress, + base_token_addr: ContractAddress, + amm_address: ContractAddress, + calls: bool, +) { + let notional = how_many_options_at_strike_to_hedge_at( + to_buy_strike, to_hedge_strike, payoff, calls + ); + buy_option(to_buy_strike, notional, expiry, calls, amm_address); +} + + +fn price_options_at_strike_to_hedge_at( + to_buy_strike: Fixed, to_hedge_strike: Fixed, payoff: Fixed, expiry: u64, amm_address: ContractAddress, calls: bool +) -> u128 { + let notional = how_many_options_at_strike_to_hedge_at( + to_buy_strike, to_hedge_strike, payoff, calls + ); + //notional + price_option(to_buy_strike, notional, expiry, calls, amm_address) +} + + +#[test] +#[available_gas(3000000)] +fn test_how_many_options_at_strike_to_hedge_at() { + let res = how_many_options_at_strike_to_hedge_at( + FixedTrait::from_unscaled_felt(1000), + FixedTrait::from_unscaled_felt(1100), + FixedTrait::ONE(), + true + ); + assert(res > 99 * 100000000000000 && res < 101 * 100000000000000, 'rescalls?'); + + let res = how_many_options_at_strike_to_hedge_at( + FixedTrait::from_unscaled_felt(1000), + FixedTrait::from_unscaled_felt(900), + FixedTrait::ONE(), + false + ); + assert(res > 99 * 100000000000000 && res < 101 * 1000000000000000, 'resputs?'); + + // repro attempt + let res = how_many_options_at_strike_to_hedge_at( + FixedTrait::from_unscaled_felt(1700), + FixedTrait::from_unscaled_felt(1800), + FixedTrait::from_felt(0xb93a99eda0819d), + true + ); + //res.print(); + // result is 100000000000000170703 => 100.00000000000017, what the fuck?? + assert( + res < 1000000000000000000, 'buying way too many options' + ); //0xb93a99eda0819d => 0.002, should be buying less than that in options... + assert(res < 2826368885168429, 'buying > payoff amt'); +} diff --git a/src/ilhedge/helpers.cairo b/src/ilhedge/helpers.cairo new file mode 100644 index 0000000..b268833 --- /dev/null +++ b/src/ilhedge/helpers.cairo @@ -0,0 +1,116 @@ +use core::array::SpanTrait; +use core::traits::TryInto; +use traits::Into; +use option::OptionTrait; + +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +fn pow(a: u128, b: u128) -> u128 { + let mut x: u128 = a; + let mut n = b; + + if n == 0 { + return 1; + } + + let mut y = 1; + let two = integer::u128_as_non_zero(2); + + 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 +} + +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. + // TODO test that it indeed overflows. + + let denom: u128 = pow(5, decimals.into()); + let numer: u128 = pow(2, 64 - decimals.into()); + + let res: u128 = (value * numer) / denom; + + FixedTrait::from_felt(res.into()) +} + +#[test] +#[available_gas(3000000)] +fn test_convert_from_int_to_Fixed() { + assert(convert_from_int_to_Fixed(1000000000000000000, 18) == FixedTrait::ONE(), 'oneeth!!'); +} + +fn convert_from_Fixed_to_int(value: Fixed, decimals: u8) -> u128 { + assert(value.sign == false, 'cant convert -val to uint'); + + (value.mag * pow(5, decimals.into())) / pow(2, (64 - decimals).into()) +} + +#[test] +#[available_gas(3000000)] +fn test_convert_from_Fixed_to_int() { + let oneeth = convert_from_Fixed_to_int(FixedTrait::ONE(), 18); + assert(oneeth == 1000000000000000000, 'oneeth?'); +} + +type Math64x61_ = felt252; + +trait FixedHelpersTrait { + fn assert_nn_not_zero(self: Fixed, msg: felt252); + fn assert_nn(self: Fixed, errmsg: felt252); + fn to_legacyMath(self: Fixed) -> Math64x61_; + fn from_legacyMath(num: Math64x61_) -> Fixed; +} + +impl FixedHelpersImpl of FixedHelpersTrait { + fn assert_nn_not_zero(self: Fixed, msg: felt252) { + assert(self > FixedTrait::ZERO(), msg); + } + + fn assert_nn(self: Fixed, errmsg: felt252) { + assert(self >= FixedTrait::ZERO(), errmsg) + } + + fn to_legacyMath(self: Fixed) -> Math64x61_ { + // TODO: Find better way to do this, this is just wrong + // Fixed is 8 times the old math + let new: felt252 = (self / FixedTrait::from_unscaled_felt(8)).into(); + new + } + + fn from_legacyMath(num: Math64x61_) -> Fixed { + // 2**61 is 8 times smaller than 2**64 + // so we can just multiply old legacy math number by 8 to get cubit + FixedTrait::from_felt(num * 8) + } +} + +fn percent>(inp: T) -> Fixed { + FixedTrait::from_unscaled_felt(inp.into()) / FixedTrait::from_unscaled_felt(100) +} + +use array::ArrayTrait; +fn reverse(inp: Span) -> Span { + let mut res = ArrayTrait::::new(); + let mut i = inp.len() - 1; + loop { + if (i == 0) { + res.append(*(inp.at(i))); + break; + } + res.append(*(inp.at(i))); + i -= 1; + }; + res.span() +} diff --git a/src/ilhedge/lib.cairo b/src/ilhedge/lib.cairo new file mode 100644 index 0000000..0c3be18 --- /dev/null +++ b/src/ilhedge/lib.cairo @@ -0,0 +1,8 @@ +mod amm_curve; +mod carmine; +mod contract; +mod constants; +mod erc20; +mod hedging; +mod pragma; +mod helpers; diff --git a/src/lib.cairo b/src/lib.cairo index c3f4414..63c703c 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,6 +1,7 @@ -mod traits; -mod types; mod amm_core; +mod ilhedge; mod testing; mod tokens; +mod traits; +mod types; mod utils; diff --git a/tests/ilhedge/test_ilhedge.cairo b/tests/ilhedge/test_ilhedge.cairo new file mode 100644 index 0000000..b8659db --- /dev/null +++ b/tests/ilhedge/test_ilhedge.cairo @@ -0,0 +1,8 @@ +use carmine_protocol::testing::setup::deploy_setup; + +#[test] +fn test_ilhedge() { + let (ctx, dsps) = deploy_setup(); + + +} \ No newline at end of file