Skip to content

Commit

Permalink
feat: conversion rate price for tokenized vaults (#132)
Browse files Browse the repository at this point in the history
* feat: conversion rate pricing impl

* feat: mock erc4626 + tests

* feat: `delete` operation

* feat: additional tests + corrections

* fix: revert decimals
  • Loading branch information
JordyRo1 authored Dec 6, 2024
1 parent d4480ce commit a4a6cdb
Show file tree
Hide file tree
Showing 6 changed files with 458 additions and 6 deletions.
5 changes: 5 additions & 0 deletions pragma-oracle/src/entry/entry.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion pragma-oracle/src/entry/structs.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,11 @@ struct PragmaPricesResponse {
expiration_timestamp: Option<u64>,
}

#[derive(Serde, Drop, Copy, starknet::Store)]
#[derive(Serde, Drop, Copy, starknet::Store, PartialEq)]
enum AggregationMode {
Median: (),
Mean: (),
ConversionRate,
Error: (),
}

Expand Down Expand Up @@ -306,6 +307,7 @@ impl AggregationModeIntoU8 of TryInto<AggregationMode, u8> {
match self {
AggregationMode::Median(()) => Option::Some(0_u8),
AggregationMode::Mean(()) => Option::Some(1_u8),
AggregationMode::ConversionRate => Option::Some(2_u8),
AggregationMode::Error(()) => Option::None(()),
}
}
Expand All @@ -316,6 +318,8 @@ impl u8IntoAggregationMode of Into<u8, AggregationMode> {
AggregationMode::Median(())
} else if self == 1_u8 {
AggregationMode::Mean(())
} else if self == 2_u8 {
AggregationMode::ConversionRate
} else {
AggregationMode::Error(())
}
Expand Down
186 changes: 186 additions & 0 deletions pragma-oracle/src/erc4626/erc4626.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Forked from https://github.com/0xEniotna/ERC4626/blob/main/src/erc4626/interface.cairo
// Mock contract to be used for TESTING PURPOSE ONLY
use starknet::ContractAddress;

#[starknet::interface]
trait IERC4626<TContractState> {
// Metadata (match implementation)
fn name(self: @TContractState) -> felt252;
fn symbol(self: @TContractState) -> felt252;
fn decimals(self: @TContractState) -> u8;

// ERC20-like methods (match implementation)
fn total_supply(self: @TContractState) -> u256;
fn balance_of(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 transfer_from(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;

// Remove camelCase methods as they're not in the implementation

// Additional ERC4626 methods (match implementation)
fn asset(self: @TContractState) -> ContractAddress;
fn total_assets(self: @TContractState) -> u256;
fn convert_to_shares(self: @TContractState, assets: u256) -> u256;
fn convert_to_assets(self: @TContractState, shares: u256) -> u256;
fn max_deposit(self: @TContractState, receiver: ContractAddress) -> u256;
fn preview_deposit(self: @TContractState, assets: u256) -> u256;
fn deposit(ref self: TContractState, assets: u256, receiver: ContractAddress) -> u256;
fn max_mint(self: @TContractState, receiver: ContractAddress) -> u256;
fn preview_mint(self: @TContractState, shares: u256) -> u256;
fn mint(ref self: TContractState, shares: u256, receiver: ContractAddress) -> u256;
fn max_withdraw(self: @TContractState, owner: ContractAddress) -> u256;
fn preview_withdraw(self: @TContractState, assets: u256) -> u256;
fn withdraw(
ref self: TContractState, assets: u256, receiver: ContractAddress, owner: ContractAddress
) -> u256;
fn max_redeem(self: @TContractState, owner: ContractAddress) -> u256;
fn preview_redeem(self: @TContractState, shares: u256) -> u256;
fn redeem(
ref self: TContractState, shares: u256, receiver: ContractAddress, owner: ContractAddress
) -> u256;
}

#[starknet::contract]
mod ERC4626 {
use super::IERC4626;
use starknet::get_caller_address;
use starknet::get_contract_address;
use starknet::ContractAddress;
use starknet::contract_address::ContractAddressZeroable;
use zeroable::Zeroable;
use traits::Into;
use traits::TryInto;
use option::OptionTrait;
use integer::BoundedInt;
use debug::PrintTrait;

#[storage]
struct Storage {}

#[external(v0)]
impl ERC4626Impl of IERC4626<ContractState> {
////////////////////////////////
// ERC20 implementation
////////////////////////////////

fn name(self: @ContractState) -> felt252 {
0
}

fn symbol(self: @ContractState) -> felt252 {
0
}

fn decimals(self: @ContractState) -> u8 {
0
}

fn total_supply(self: @ContractState) -> u256 {
0
}

fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
0
}

fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress
) -> u256 {
0
}

fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
true
}

fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) -> bool {
true
}

fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool {
true
}

////////////////////////////////
// ERC4626-specific implementation
////////////////////////////////

fn asset(self: @ContractState) -> ContractAddress {
0.try_into().unwrap()
}

fn total_assets(self: @ContractState) -> u256 {
0
}

fn convert_to_shares(self: @ContractState, assets: u256) -> u256 {
0
}

fn convert_to_assets(self: @ContractState, shares: u256) -> u256 {
0
}

fn max_deposit(self: @ContractState, receiver: ContractAddress) -> u256 {
0
}

fn preview_deposit(self: @ContractState, assets: u256) -> u256 {
0
}

fn deposit(ref self: ContractState, assets: u256, receiver: ContractAddress) -> u256 {
0
}

fn max_mint(self: @ContractState, receiver: ContractAddress) -> u256 {
BoundedInt::<u256>::max()
}

fn preview_mint(self: @ContractState, shares: u256) -> u256 {
// TESTING
1002465544733197129
}

fn mint(ref self: ContractState, shares: u256, receiver: ContractAddress) -> u256 {
0
}

fn max_withdraw(self: @ContractState, owner: ContractAddress) -> u256 {
0
}

fn preview_withdraw(self: @ContractState, assets: u256) -> u256 {
0
}

fn withdraw(
ref self: ContractState, assets: u256, receiver: ContractAddress, owner: ContractAddress
) -> u256 {
0
}

fn max_redeem(self: @ContractState, owner: ContractAddress) -> u256 {
0
}

fn preview_redeem(self: @ContractState, shares: u256) -> u256 {
0
}

fn redeem(
ref self: ContractState, shares: u256, receiver: ContractAddress, owner: ContractAddress
) -> u256 {
0
}
}
}
3 changes: 3 additions & 0 deletions pragma-oracle/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ mod utils {
mod bitwise;
mod strings;
}
mod erc4626 {
mod erc4626;
}
mod operations {
mod sorting {
mod merge_sort;
Expand Down
75 changes: 70 additions & 5 deletions pragma-oracle/src/oracle/oracle.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ trait IOracleABI<TContractState> {
fn add_currency(ref self: TContractState, new_currency: Currency);
fn update_currency(ref self: TContractState, currency_id: felt252, currency: Currency);
fn get_currency(self: @TContractState, currency_id: felt252) -> Currency;
fn get_tokenized_vaults(self: @TContractState, token: felt252) -> ContractAddress;
fn update_pair(ref self: TContractState, pair_id: felt252, pair: Pair);
fn add_pair(ref self: TContractState, new_pair: Pair);
fn get_pair(self: @TContractState, pair_id: felt252) -> Pair;
Expand All @@ -101,6 +102,9 @@ trait IOracleABI<TContractState> {
);
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);
}

Expand Down Expand Up @@ -192,11 +196,13 @@ mod Oracle {
IPublisherRegistryABIDispatcher, IPublisherRegistryABIDispatcherTrait
};
use starknet::{get_block_timestamp, Felt252TryIntoContractAddress};
use pragma::erc4626::erc4626::{IERC4626Dispatcher, IERC4626DispatcherTrait};

use cmp::{max, min};
use option::OptionTrait;
const BACKWARD_TIMESTAMP_BUFFER: u64 = 3600; // 1 hour
const FORWARD_TIMESTAMP_BUFFER: u64 = 420; // 7 minutes
const ONE_E18: u256 = 1000000000000000000;


#[storage]
Expand Down Expand Up @@ -235,6 +241,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>
}


Expand Down Expand Up @@ -514,18 +522,57 @@ 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 != starknet::contract_address_const::<0>(),
'No pool address for given token'
);
let pool = IERC4626Dispatcher { contract_address: pool_address };

// Compute adjusted price
// `preview_mint` takes as argument an e18 and returns an e18
// We operate under u256 to avoid overflow
let price: u256 = response.price.into() * pool.preview_mint(ONE_E18) / ONE_E18;

// The conversion should not fail because we scaled the price to response.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
Expand Down Expand Up @@ -927,6 +974,10 @@ mod Oracle {
Ownable::OwnableImpl::owner(@state)
}

fn get_tokenized_vaults(self: @ContractState, token: felt252) -> ContractAddress {
self.tokenized_vault.read((token, 'STRK'))
}


// @notice retrieve the last checkpoint before a given timestamp
// @param data_type: an enum of DataType (e.g : DataType::SpotEntry(ASSET_ID) or DataType::FutureEntry((ASSSET_ID, expiration_timestamp)))
Expand Down Expand Up @@ -1771,6 +1822,20 @@ 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');
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
Expand Down
Loading

0 comments on commit a4a6cdb

Please sign in to comment.