diff --git a/pragma-oracle/src/erc4626/erc4626.cairo b/pragma-oracle/src/erc4626/erc4626.cairo index ee0e381b..5bbfee96 100644 --- a/pragma-oracle/src/erc4626/erc4626.cairo +++ b/pragma-oracle/src/erc4626/erc4626.cairo @@ -1,116 +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 { - // ************************************ - // * 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; +trait IERC4626 { + // 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: 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 + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 ) -> bool; + fn approve(ref self: TContractState, spender: 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; + // 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: TState, - assets: u256, - receiver: starknet::ContractAddress, - owner: starknet::ContractAddress + 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; -#[starknet::interface] -trait IERC4626Metadata { - fn name(self: @TState) -> felt252; - fn symbol(self: @TState) -> felt252; - fn decimals(self: @TState) -> u8; -} + #[storage] + struct Storage {} -#[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; -} + #[external(v0)] + impl ERC4626Impl of IERC4626 { + //////////////////////////////// + // ERC20 implementation + //////////////////////////////// -#[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; -} + fn name(self: @ContractState) -> felt252 { + 0 + } -#[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; + 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::::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 + } + } } diff --git a/pragma-oracle/src/oracle/oracle.cairo b/pragma-oracle/src/oracle/oracle.cairo index 5b3db331..9e5999ca 100644 --- a/pragma-oracle/src/oracle/oracle.cairo +++ b/pragma-oracle/src/oracle/oracle.cairo @@ -90,6 +90,7 @@ trait IOracleABI { 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; @@ -547,16 +548,19 @@ mod Oracle { 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' + 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(1000000000000000000) / 1000000000000000000; - // The conversion should not fail because we scaled the price to 8 decimals + // 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 { @@ -971,6 +975,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))) @@ -1826,7 +1834,10 @@ mod Oracle { ) { OracleInternal::assert_only_admin(); assert(token != 0, 'Token cannot be 0'); - assert(token_address != contract_address_const::<0>(), 'Token address cannot be 0'); + assert( + token_address != starknet::contract_address_const::<0>(), + 'Token address cannot be 0' + ); self.tokenized_vault.write((token, 'STRK'), token_address) } diff --git a/pragma-oracle/src/tests/test_oracle.cairo b/pragma-oracle/src/tests/test_oracle.cairo index c4082769..4f0a2a6b 100644 --- a/pragma-oracle/src/tests/test_oracle.cairo +++ b/pragma-oracle/src/tests/test_oracle.cairo @@ -11,6 +11,7 @@ use starknet::class_hash::class_hash_const; use traits::Into; use traits::TryInto; use pragma::oracle::oracle::Oracle; +use pragma::erc4626::erc4626::ERC4626; use pragma::oracle::oracle::{IOracleABIDispatcher, IOracleABIDispatcherTrait}; use pragma::publisher_registry::publisher_registry::{ IPublisherRegistryABIDispatcher, IPublisherRegistryABIDispatcherTrait @@ -319,6 +320,14 @@ fn setup() -> (IPublisherRegistryABIDispatcher, IOracleABIDispatcher) { (publisher_registry, oracle) } + +fn deploy_erc4626() -> ContractAddress { + let (erc4626_address, _) = deploy_syscall( + ERC4626::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), true + ) + .unwrap_syscall(); + return erc4626_address; +} #[test] #[available_gas(200000000000000)] fn test_get_decimals() { @@ -1371,3 +1380,77 @@ fn test_update_pair() { assert(pair.quote_currency_id == 111, 'wrong recorded pair'); assert(pair.base_currency_id == 12345, 'wrong recorded pair'); } + + +#[test] +#[available_gas(20000000000000)] +fn test_register_tokenized_vault() { + let (publisher_registry, oracle) = setup(); + let token: felt252 = 'xStrk'; + let token_address: ContractAddress = contract_address_const::<0x12345566>(); + let admin = contract_address_const::<0x123456789>(); + set_contract_address(admin); + oracle.register_tokenized_vault(token, token_address); + assert(oracle.get_tokenized_vaults(token) == token_address, 'Failed to register token'); +} + + +#[test] +#[available_gas(20000000000000)] +#[should_panic(expected: ('Admin: unauthorized', 'ENTRYPOINT_FAILED'))] +fn test_register_tokenized_vault_panics_if_not_owner() { + let (publisher_registry, oracle) = setup(); + let token: felt252 = 'xStrk'; + let token_address: ContractAddress = contract_address_const::<0x12345566>(); + set_contract_address(contract_address_const::<0x12>()); + oracle.register_tokenized_vault(token, token_address); +} + + +#[test] +#[available_gas(20000000000000)] +fn test_get_conversion_rate_price() { + let now = 100000; + let (publisher_registry, oracle) = setup(); + let admin = contract_address_const::<0x123456789>(); + set_contract_address(admin); + oracle + .add_currency( + Currency { + id: 'STRK', + decimals: 8, + is_abstract_currency: false, + starknet_address: 0.try_into().unwrap(), + ethereum_address: 0.try_into().unwrap(), + } + ); + oracle + .add_currency( + Currency { + id: 'xSTRK', + decimals: 8, + is_abstract_currency: false, + starknet_address: 0.try_into().unwrap(), + ethereum_address: 0.try_into().unwrap(), + } + ); + oracle.add_pair(Pair { id: 'STRK/USD', base_currency_id: 'STRK', quote_currency_id: 'USD', }); + oracle.add_pair(Pair { id: 'xSTRK/USD', base_currency_id: 'xSTRK', quote_currency_id: 'USD', }); + oracle + .publish_data( + PossibleEntries::Spot( + SpotEntry { + base: BaseEntry { timestamp: now, source: 2, publisher: 1 }, + pair_id: 'STRK/USD', + price: 68250000, + volume: 0 + } + ) + ); + let erc4626 = deploy_erc4626(); + oracle.register_tokenized_vault('xSTRK', erc4626); + let res = oracle.get_data(DataType::SpotEntry('xSTRK/USD'), AggregationMode::ConversionRate); + assert( + res.price == (68250000 * 1002465544733197129) / 1000000000000000000, 'Computation failed' + ); +}