diff --git a/pragma-oracle/src/entry/entry.cairo b/pragma-oracle/src/entry/entry.cairo index d59b859..c6309fe 100644 --- a/pragma-oracle/src/entry/entry.cairo +++ b/pragma-oracle/src/entry/entry.cairo @@ -44,6 +44,11 @@ mod Entry { let value: u128 = entries_mean(entries); value }, + AggregationMode::ConversionRate => { + panic_with_felt252('No agg for conversion rate'); + // No aggregation needed for conversion rate + 0 + }, AggregationMode::Error(()) => { panic_with_felt252('Wrong aggregation mode'); 0 diff --git a/pragma-oracle/src/entry/structs.cairo b/pragma-oracle/src/entry/structs.cairo index 35d99e9..0772864 100644 --- a/pragma-oracle/src/entry/structs.cairo +++ b/pragma-oracle/src/entry/structs.cairo @@ -180,10 +180,11 @@ struct PragmaPricesResponse { expiration_timestamp: Option, } -#[derive(Serde, Drop, Copy, starknet::Store)] +#[derive(Serde, Drop, Copy, starknet::Store, PartialEq)] enum AggregationMode { Median: (), Mean: (), + ConversionRate, Error: (), } @@ -306,6 +307,7 @@ impl AggregationModeIntoU8 of TryInto { match self { AggregationMode::Median(()) => Option::Some(0_u8), AggregationMode::Mean(()) => Option::Some(1_u8), + AggregationMode::ConversionRate => Option::Some(2_u8), AggregationMode::Error(()) => Option::None(()), } } @@ -316,6 +318,8 @@ impl u8IntoAggregationMode of Into { AggregationMode::Median(()) } else if self == 1_u8 { AggregationMode::Mean(()) + } else if self == 2_u8 { + AggregationMode::ConversionRate } else { AggregationMode::Error(()) } diff --git a/pragma-oracle/src/erc4626/erc4626.cairo b/pragma-oracle/src/erc4626/erc4626.cairo new file mode 100644 index 0000000..ee0e381 --- /dev/null +++ b/pragma-oracle/src/erc4626/erc4626.cairo @@ -0,0 +1,116 @@ +// Forked from https://github.com/0xEniotna/ERC4626/blob/main/src/erc4626/interface.cairo +use starknet::ContractAddress; + +#[starknet::interface] +trait IERC4626 { + // ************************************ + // * Metadata + // ************************************ + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + + // ************************************ + // * snake_case + // ************************************ + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + // ************************************ + // * camelCase + // ************************************ + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + + // ************************************ + // * Additional functions + // ************************************ + fn asset(self: @TState) -> starknet::ContractAddress; + fn convert_to_assets(self: @TState, shares: u256) -> u256; + fn convert_to_shares(self: @TState, assets: u256) -> u256; + fn deposit(ref self: TState, assets: u256, receiver: starknet::ContractAddress) -> u256; + fn max_deposit(self: @TState, address: starknet::ContractAddress) -> u256; + fn max_mint(self: @TState, receiver: starknet::ContractAddress) -> u256; + fn max_redeem(self: @TState, owner: starknet::ContractAddress) -> u256; + fn max_withdraw(self: @TState, owner: starknet::ContractAddress) -> u256; + fn mint(ref self: TState, shares: u256, receiver: starknet::ContractAddress) -> u256; + fn preview_deposit(self: @TState, assets: u256) -> u256; + fn preview_mint(self: @TState, shares: u256) -> u256; + fn preview_redeem(self: @TState, shares: u256) -> u256; + fn preview_withdraw(self: @TState, assets: u256) -> u256; + fn redeem( + ref self: TState, + shares: u256, + receiver: starknet::ContractAddress, + owner: starknet::ContractAddress + ) -> u256; + fn total_assets(self: @TState) -> u256; + fn withdraw( + ref self: TState, + assets: u256, + receiver: starknet::ContractAddress, + owner: starknet::ContractAddress + ) -> u256; +} + + +#[starknet::interface] +trait IERC4626Metadata { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; +} + +#[starknet::interface] +trait IERC4626Camel { + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; +} + +#[starknet::interface] +trait IERC4626Snake { + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::interface] +trait IERC4626Additional { + fn asset(self: @TState) -> ContractAddress; + fn convert_to_assets(self: @TState, shares: u256) -> u256; + fn convert_to_shares(self: @TState, assets: u256) -> u256; + fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + fn max_deposit(self: @TState, address: ContractAddress) -> u256; + fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + fn preview_deposit(self: @TState, assets: u256) -> u256; + fn preview_mint(self: @TState, shares: u256) -> u256; + fn preview_redeem(self: @TState, shares: u256) -> u256; + fn preview_withdraw(self: @TState, assets: u256) -> u256; + fn redeem( + ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn total_assets(self: @TState) -> u256; + fn withdraw( + ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; +} diff --git a/pragma-oracle/src/lib.cairo b/pragma-oracle/src/lib.cairo index c719521..e2a1c35 100644 --- a/pragma-oracle/src/lib.cairo +++ b/pragma-oracle/src/lib.cairo @@ -9,6 +9,9 @@ mod utils { mod bitwise; mod strings; } +mod erc4626 { + mod erc4626; +} mod operations { mod sorting { mod merge_sort; diff --git a/pragma-oracle/src/oracle/oracle.cairo b/pragma-oracle/src/oracle/oracle.cairo index 1f238b1..5b3db33 100644 --- a/pragma-oracle/src/oracle/oracle.cairo +++ b/pragma-oracle/src/oracle/oracle.cairo @@ -101,6 +101,9 @@ trait IOracleABI { ); fn remove_source(ref self: TContractState, source: felt252, data_type: DataType) -> bool; fn set_sources_threshold(ref self: TContractState, threshold: u32); + fn register_tokenized_vault( + ref self: TContractState, token: felt252, token_address: ContractAddress + ); fn upgrade(ref self: TContractState, impl_hash: ClassHash); } @@ -192,6 +195,7 @@ mod Oracle { IPublisherRegistryABIDispatcher, IPublisherRegistryABIDispatcherTrait }; use starknet::{get_block_timestamp, Felt252TryIntoContractAddress}; + use pragma::erc4626::erc4626::{IERC4626Dispatcher, IERC4626DispatcherTrait}; use cmp::{max, min}; use option::OptionTrait; @@ -235,6 +239,8 @@ mod Oracle { //oracle_checkpoint_index, legacyMap between (pair_id, (SPOT/FUTURES/OPTIONS), expiration_timestamp (0 for SPOT)) and the index of the last checkpoint oracle_checkpoint_index: LegacyMap::<(felt252, felt252, u64, u8), u64>, oracle_sources_threshold_storage: u32, + // registry containing registered tokenized vaults and the corresponding address + tokenized_vault: LegacyMap<(felt252, felt252), ContractAddress> } @@ -514,18 +520,56 @@ mod Oracle { } // @notice aggregate all the entries for a given data type, with a given aggregation mode + // @notice The aggregation mode `ConversionRate` is set for tokenized vault, with pricing made based on STRK price and associated conversion rate fetched + // @notice onchain. // @param data_type: an enum of DataType (e.g : DataType::SpotEntry(ASSET_ID) or DataType::FutureEntry((ASSSET_ID, expiration_timestamp))) // @param aggregation_mode: the aggregation method to be used (e.g. AggregationMode::Median(())) // @returns a PragmaPricesResponse, a structure providing the main information for an asset (see entry/structs for details) fn get_data( self: @ContractState, data_type: DataType, aggregation_mode: AggregationMode ) -> PragmaPricesResponse { - let sources = IOracleABI::get_all_sources(self, data_type); - let prices_response: PragmaPricesResponse = IOracleABI::get_data_for_sources( - self, data_type, aggregation_mode, sources - ); + if aggregation_mode == AggregationMode::ConversionRate { + // Query median for STRK/USD + let sources = IOracleABI::get_all_sources(self, DataType::SpotEntry('STRK/USD')); + let response: PragmaPricesResponse = IOracleABI::get_data_for_sources( + self, DataType::SpotEntry('STRK/USD'), AggregationMode::Median(()), sources + ); - prices_response + // Extract base asset + let asset: felt252 = match data_type { + DataType::SpotEntry(asset) => asset, + DataType::FutureEntry(_) => panic_with_felt252('Set only for Spot entries'), + DataType::GenericEntry(_) => panic_with_felt252('Set only for Spot entries'), + }; + + // Get base currency and pool + let base_asset: felt252 = self.get_pair(asset).base_currency_id; + assert(base_asset != 0, 'Asset not registered'); + let pool_address: ContractAddress = self.tokenized_vault.read((base_asset, 'STRK')); + assert( + pool_address != contract_address_const::<0>(), 'No pool address for given token' + ); + let pool = IERC4626Dispatcher { contract_address: pool_address }; + + // Compute adjusted price + let price: u256 = response.price.into() + * pool.preview_mint(1000000000000000000) + / 1000000000000000000; + + // The conversion should not fail because we scaled the price to 8 decimals + let converted_price: u128 = price.try_into().expect('Conversion should not fail'); + assert(converted_price != 0, 'Price conversion failed'); + PragmaPricesResponse { + price: converted_price, + decimals: response.decimals, + last_updated_timestamp: response.last_updated_timestamp, + num_sources_aggregated: response.num_sources_aggregated, + expiration_timestamp: response.expiration_timestamp + } + } else { + let sources = IOracleABI::get_all_sources(self, data_type); + IOracleABI::get_data_for_sources(self, data_type, aggregation_mode, sources) + } } // @notice aggregate all the entries for a given data type and given sources, with a given aggregation mode @@ -1771,6 +1815,22 @@ mod Oracle { } } + // @notice register a new tokenized vault into the regisry (priced with STRK) + // @dev Callable only by the owner + // @dev the token must be registered as currency and pair in the oracle registry + // @dev We reserve the owner of the contract the right to overwrite an existing token address + // @param token The token to register + // @param token_address Token address to register + fn register_tokenized_vault( + ref self: ContractState, token: felt252, token_address: ContractAddress + ) { + OracleInternal::assert_only_admin(); + assert(token != 0, 'Token cannot be 0'); + assert(token_address != contract_address_const::<0>(), 'Token address cannot be 0'); + self.tokenized_vault.write((token, 'STRK'), token_address) + } + + // @notice set a new checkpoint for a given data type and and aggregation mode // @param data_type: an enum of DataType (e.g : DataType::SpotEntry(ASSET_ID) or DataType::FutureEntry((ASSSET_ID, expiration_timestamp))) // @param aggregation_mode: the aggregation method to be used