From eed6b61ab41aecd1a049163f9e32d59d81dbc89a Mon Sep 17 00:00:00 2001 From: akiraonstarknet Date: Thu, 23 Oct 2025 15:00:19 +0800 Subject: [PATCH 1/2] diff to checkout before mig --- .gitignore | 4 +- .../aum_provider_4626/aum_provider_4626.cairo | 81 +++ .../aum_provider_4626/errors.cairo | 9 + .../aum_provider_4626/interface.cairo | 10 + .../src/aum_provider/base_aum_provider.cairo | 52 ++ packages/vault/src/aum_provider/errors.cairo | 9 + .../vault/src/aum_provider/interface.cairo | 9 + packages/vault/src/lib.cairo | 12 + .../vault/src/redeem_request/errors.cairo | 4 + .../src/redeem_request/redeem_request.cairo | 124 +++- .../vault/src/test/mock_erc721_receiver.cairo | 60 ++ .../vault/src/test/units/redeem_request.cairo | 16 +- packages/vault/src/test/units/vault.cairo | 275 +++++---- packages/vault/src/test/utils.cairo | 9 + packages/vault/src/vault/errors.cairo | 4 + packages/vault/src/vault/interface.cairo | 6 +- packages/vault/src/vault/vault.cairo | 218 ++++++- .../base_decoder_and_sanitizer.cairo | 13 +- .../decoders_and_sanitizers/interface.cairo | 9 +- .../multiply_decoder_and_sanitizer.cairo | 4 +- .../simple_decoder_and_sanitizer.cairo | 13 + .../interface.cairo | 15 + ...knet_vault_kit_decoder_and_sanitizer.cairo | 41 ++ .../interface.cairo | 12 + .../uncap_decoder_and_sanitizer.cairo | 37 ++ .../vesu_decoder_and_sanitizer.cairo | 6 +- .../vesu_v2_decoder_and_sanitizer.cairo | 10 +- ...su_v2_specific_decoder_and_sanitizer.cairo | 46 ++ .../src/integration_interfaces/vesu.cairo | 396 ------------ .../src/integration_interfaces/vesu_v1.cairo | 23 + .../src/integration_interfaces/vesu_v2.cairo | 28 + packages/vault_allocator/src/lib.cairo | 32 +- .../vault_allocator/src/manager/errors.cairo | 12 - .../src/manager/interface.cairo | 10 - .../vault_allocator/src/manager/manager.cairo | 115 +--- .../src/merkle_tree/base.cairo | 319 ++++++++++ .../src/merkle_tree/integrations/avnu.cairo | 72 +++ .../merkle_tree/integrations/erc4626.cairo | 114 ++++ .../starknet_vault_kit_strategies.cairo | 102 ++++ .../merkle_tree/integrations/vesu_v1.cairo | 91 +++ .../merkle_tree/integrations/vesu_v2.cairo | 95 +++ .../registery.cairo} | 9 + .../avnu_middleware/avnu_middleware.cairo | 146 +++-- .../middlewares/avnu_middleware/errors.cairo | 6 +- .../avnu_middleware/interface.cairo | 9 +- .../vault_allocator/src/mocks/flashloan.cairo | 139 ----- .../vault_allocator/src/mocks/vault.cairo | 179 ++++++ .../periphery/price_router/price_router.cairo | 3 +- .../periphery/price_router_vesu/errors.cairo | 11 + .../price_router_vesu/interface.cairo | 15 + .../price_router_vesu/price_router_vesu.cairo | 93 +++ .../vault_allocator/src/test/creator.cairo | 41 +- .../src/test/integrations/avnu.cairo | 25 +- .../integrations/vault_bring_liquidity.cairo | 135 ++++ .../src/test/integrations/vesu_v1.cairo | 56 +- .../leveraged_loop_staked_ether.cairo | 272 --------- .../test/scenarios/stable_carry_loop.cairo | 73 ++- .../src/test/units/manager.cairo | 412 +------------ packages/vault_allocator/src/test/utils.cairo | 576 +----------------- .../src/vault_allocator/vault_allocator.cairo | 59 +- 60 files changed, 2541 insertions(+), 2235 deletions(-) create mode 100644 packages/vault/src/aum_provider/aum_provider_4626/aum_provider_4626.cairo create mode 100644 packages/vault/src/aum_provider/aum_provider_4626/errors.cairo create mode 100644 packages/vault/src/aum_provider/aum_provider_4626/interface.cairo create mode 100644 packages/vault/src/aum_provider/base_aum_provider.cairo create mode 100644 packages/vault/src/aum_provider/errors.cairo create mode 100644 packages/vault/src/aum_provider/interface.cairo create mode 100644 packages/vault/src/test/mock_erc721_receiver.cairo create mode 100644 packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/interface.cairo create mode 100644 packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo create mode 100644 packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/interface.cairo create mode 100644 packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/uncap_decoder_and_sanitizer.cairo create mode 100644 packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_specific_decoder_and_sanitizer.cairo delete mode 100644 packages/vault_allocator/src/integration_interfaces/vesu.cairo create mode 100644 packages/vault_allocator/src/integration_interfaces/vesu_v1.cairo create mode 100644 packages/vault_allocator/src/integration_interfaces/vesu_v2.cairo create mode 100644 packages/vault_allocator/src/merkle_tree/base.cairo create mode 100644 packages/vault_allocator/src/merkle_tree/integrations/avnu.cairo create mode 100644 packages/vault_allocator/src/merkle_tree/integrations/erc4626.cairo create mode 100644 packages/vault_allocator/src/merkle_tree/integrations/starknet_vault_kit_strategies.cairo create mode 100644 packages/vault_allocator/src/merkle_tree/integrations/vesu_v1.cairo create mode 100644 packages/vault_allocator/src/merkle_tree/integrations/vesu_v2.cairo rename packages/vault_allocator/src/{test/register.cairo => merkle_tree/registery.cairo} (87%) delete mode 100644 packages/vault_allocator/src/mocks/flashloan.cairo create mode 100644 packages/vault_allocator/src/mocks/vault.cairo create mode 100644 packages/vault_allocator/src/periphery/price_router_vesu/errors.cairo create mode 100644 packages/vault_allocator/src/periphery/price_router_vesu/interface.cairo create mode 100644 packages/vault_allocator/src/periphery/price_router_vesu/price_router_vesu.cairo create mode 100644 packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo delete mode 100644 packages/vault_allocator/src/test/scenarios/leveraged_loop_staked_ether.cairo diff --git a/.gitignore b/.gitignore index 14690868..9bce706d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ target .snfoundry_cache -.env \ No newline at end of file +.env +my_scripts +scripts \ No newline at end of file diff --git a/packages/vault/src/aum_provider/aum_provider_4626/aum_provider_4626.cairo b/packages/vault/src/aum_provider/aum_provider_4626/aum_provider_4626.cairo new file mode 100644 index 00000000..88bb8011 --- /dev/null +++ b/packages/vault/src/aum_provider/aum_provider_4626/aum_provider_4626.cairo @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::contract] +pub mod AumProvider4626 { + use core::num::traits::Zero; + use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::interfaces::erc4626::{IERC4626Dispatcher, IERC4626DispatcherTrait}; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use vault::aum_provider::aum_provider_4626::errors::Errors; + use vault::aum_provider::aum_provider_4626::interface::IAumProvider4626; + use vault::aum_provider::base_aum_provider::BaseAumProviderComponent; + use vault::vault::interface::IVaultDispatcherTrait; + component!( + path: BaseAumProviderComponent, storage: base_aum_provider, event: BaseAumProviderEvent, + ); + + #[abi(embed_v0)] + impl BaseAumProviderImpl = + BaseAumProviderComponent::BaseAumProviderImpl; + impl BaseAumProviderInternalImpl = BaseAumProviderComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + base_aum_provider: BaseAumProviderComponent::Storage, + strategy4626: IERC4626Dispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + BaseAumProviderEvent: BaseAumProviderComponent::Event, + } + + #[abi(embed_v0)] + impl AumProvider4626Impl of IAumProvider4626 { + fn get_strategy_4626(self: @ContractState) -> ContractAddress { + self.strategy4626.read().contract_address + } + } + + #[constructor] + fn constructor(ref self: ContractState, vault: ContractAddress, strategy4626: ContractAddress) { + self.base_aum_provider.initializer(vault); + if (strategy4626.is_zero()) { + Errors::invalid_strategy4626_address(); + } + + self.strategy4626.write(IERC4626Dispatcher { contract_address: strategy4626 }); + } + + // --- AUM Trait Implementation --- + impl AumTrait of BaseAumProviderComponent::AumTrait { + fn get_aum(self: @BaseAumProviderComponent::ComponentState) -> u256 { + let contract_state = self.get_contract(); + let strategy = contract_state.strategy4626.read(); + let vault_allocator_address = contract_state + .base_aum_provider + .vault + .read() + .vault_allocator(); + + // .vault.vault_allocator(); + let underlying_asset = strategy.asset(); + let underlying_dispatcher = ERC20ABIDispatcher { contract_address: underlying_asset }; + let underlying_balance = underlying_dispatcher.balance_of(vault_allocator_address); + let strategy_shares = ERC20ABIDispatcher { contract_address: strategy.contract_address } + .balance_of(vault_allocator_address); + let strategy_assets = if strategy_shares > 0 { + strategy.convert_to_assets(strategy_shares) + } else { + 0 + }; + + underlying_balance + strategy_assets + } + } +} diff --git a/packages/vault/src/aum_provider/aum_provider_4626/errors.cairo b/packages/vault/src/aum_provider/aum_provider_4626/errors.cairo new file mode 100644 index 00000000..916972c5 --- /dev/null +++ b/packages/vault/src/aum_provider/aum_provider_4626/errors.cairo @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +pub mod Errors { + pub fn invalid_strategy4626_address() { + panic!("Invalid strategy4626 address"); + } +} diff --git a/packages/vault/src/aum_provider/aum_provider_4626/interface.cairo b/packages/vault/src/aum_provider/aum_provider_4626/interface.cairo new file mode 100644 index 00000000..c20e957a --- /dev/null +++ b/packages/vault/src/aum_provider/aum_provider_4626/interface.cairo @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IAumProvider4626 { + fn get_strategy_4626(self: @TContractState) -> ContractAddress; +} \ No newline at end of file diff --git a/packages/vault/src/aum_provider/base_aum_provider.cairo b/packages/vault/src/aum_provider/base_aum_provider.cairo new file mode 100644 index 00000000..6ebf107d --- /dev/null +++ b/packages/vault/src/aum_provider/base_aum_provider.cairo @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::component] +pub mod BaseAumProviderComponent { + use core::num::traits::Zero; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use vault::aum_provider::errors::Errors; + use vault::aum_provider::interface::IBaseAumProvider; + use vault::vault::interface::{IVaultDispatcher, IVaultDispatcherTrait}; + #[storage] + pub struct Storage { + pub vault: IVaultDispatcher, + } + + #[event] + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub enum Event {} + + + pub trait AumTrait> { + fn get_aum(self: @ComponentState) -> u256; + } + + + #[embeddable_as(BaseAumProviderImpl)] + impl BaseAumProvider< + TContractState, +HasComponent, impl Aum: AumTrait, + > of IBaseAumProvider> { + fn aum(self: @ComponentState) -> u256 { + Aum::get_aum(self) + } + + fn report(ref self: ComponentState) { + self.vault.read().report(Aum::get_aum(@self)); + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, + > of InternalTrait { + fn initializer(ref self: ComponentState, vault: ContractAddress) { + if (vault.is_zero()) { + Errors::invalid_vault_address(); + } + self.vault.write(IVaultDispatcher { contract_address: vault }); + } + } +} diff --git a/packages/vault/src/aum_provider/errors.cairo b/packages/vault/src/aum_provider/errors.cairo new file mode 100644 index 00000000..a7c84ac7 --- /dev/null +++ b/packages/vault/src/aum_provider/errors.cairo @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +pub mod Errors { + pub fn invalid_vault_address() { + panic!("Invalid vault address"); + } +} diff --git a/packages/vault/src/aum_provider/interface.cairo b/packages/vault/src/aum_provider/interface.cairo new file mode 100644 index 00000000..6f218f13 --- /dev/null +++ b/packages/vault/src/aum_provider/interface.cairo @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::interface] +pub trait IBaseAumProvider { + fn aum(self: @T) -> u256; + fn report(ref self: T); +} diff --git a/packages/vault/src/lib.cairo b/packages/vault/src/lib.cairo index 9b6d63a5..742d79fc 100644 --- a/packages/vault/src/lib.cairo +++ b/packages/vault/src/lib.cairo @@ -8,6 +8,17 @@ pub mod vault { pub mod vault; } +pub mod aum_provider { + pub mod base_aum_provider; + pub mod errors; + pub mod interface; + pub mod aum_provider_4626 { + pub mod aum_provider_4626; + pub mod errors; + pub mod interface; + } +} + pub mod redeem_request { pub mod errors; @@ -17,6 +28,7 @@ pub mod redeem_request { #[cfg(test)] pub mod test { + pub mod mock_erc721_receiver; pub mod utils; pub mod units { pub mod redeem_request; diff --git a/packages/vault/src/redeem_request/errors.cairo b/packages/vault/src/redeem_request/errors.cairo index 3798485f..693e6bf0 100644 --- a/packages/vault/src/redeem_request/errors.cairo +++ b/packages/vault/src/redeem_request/errors.cairo @@ -10,4 +10,8 @@ pub mod Errors { pub fn not_vault_owner() { panic!("Caller is not vault owner"); } + + pub fn not_implemented() { + panic!("Not implemented"); + } } diff --git a/packages/vault/src/redeem_request/redeem_request.cairo b/packages/vault/src/redeem_request/redeem_request.cairo index a30d2bab..d44fae23 100644 --- a/packages/vault/src/redeem_request/redeem_request.cairo +++ b/packages/vault/src/redeem_request/redeem_request.cairo @@ -7,8 +7,15 @@ mod RedeemRequest { use openzeppelin::interfaces::accesscontrol::{ IAccessControlDispatcher, IAccessControlDispatcherTrait, }; + use openzeppelin::interfaces::accounts::ISRC6_ID; + use openzeppelin::interfaces::introspection::{ISRC5Dispatcher, ISRC5DispatcherTrait}; + use openzeppelin::interfaces::token::erc721::{ + ERC721ABI, IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID, + }; use openzeppelin::interfaces::upgrades::IUpgradeable; use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::ERC721Component::ERC721MixinImpl; + use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent; use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; use starknet::storage::{ @@ -20,12 +27,17 @@ mod RedeemRequest { use vault::redeem_request::interface::{IRedeemRequest, RedeemRequestInfo}; use vault::vault::vault::Vault::OWNER_ROLE; + component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!( + path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent, + ); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); #[abi(embed_v0)] - impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; + impl ERC721EnumerableImpl = + ERC721EnumerableComponent::ERC721EnumerableImpl; impl ERC721InternalImpl = ERC721Component::InternalImpl; impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; @@ -37,6 +49,8 @@ mod RedeemRequest { #[substorage(v0)] erc721: ERC721Component::Storage, #[substorage(v0)] + erc721_enumerable: ERC721EnumerableComponent::Storage, + #[substorage(v0)] upgradeable: UpgradeableComponent::Storage, id_len: u256, id_to_info: Map, @@ -49,6 +63,8 @@ mod RedeemRequest { #[flat] ERC721Event: ERC721Component::Event, #[flat] + ERC721EnumerableEvent: ERC721EnumerableComponent::Event, + #[flat] SRC5Event: SRC5Component::Event, #[flat] UpgradeableEvent: UpgradeableComponent::Event, @@ -61,6 +77,96 @@ mod RedeemRequest { self.vault.write(vault); } + #[abi(embed_v0)] + impl ERC721ABIImpl of ERC721ABI { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + ERC721MixinImpl::balance_of(self, account) + } + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + ERC721MixinImpl::owner_of(self, token_id) + } + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span, + ) { + ERC721MixinImpl::safe_transfer_from(ref self, from, to, token_id, data); + } + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ) { + Errors::not_implemented(); + } + fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + ERC721MixinImpl::approve(ref self, to, token_id); + } + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool, + ) { + ERC721MixinImpl::set_approval_for_all(ref self, operator, approved); + } + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + ERC721MixinImpl::get_approved(self, token_id) + } + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress, + ) -> bool { + ERC721MixinImpl::is_approved_for_all(self, owner, operator) + } + fn name(self: @ContractState) -> ByteArray { + ERC721MixinImpl::name(self) + } + fn symbol(self: @ContractState) -> ByteArray { + ERC721MixinImpl::symbol(self) + } + fn token_uri(self: @ContractState, token_id: u256) -> ByteArray { + ERC721MixinImpl::token_uri(self, token_id) + } + + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + ERC721MixinImpl::balance_of(self, account) + } + fn ownerOf(self: @ContractState, tokenId: u256) -> ContractAddress { + ERC721MixinImpl::owner_of(self, tokenId) + } + fn safeTransferFrom( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + data: Span, + ) { + ERC721MixinImpl::safe_transfer_from(ref self, from, to, tokenId, data); + } + fn transferFrom( + ref self: ContractState, from: ContractAddress, to: ContractAddress, tokenId: u256, + ) { + Errors::not_implemented(); + } + + fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { + ERC721MixinImpl::set_approval_for_all(ref self, operator, approved); + } + fn getApproved(self: @ContractState, tokenId: u256) -> ContractAddress { + ERC721MixinImpl::get_approved(self, tokenId) + } + fn isApprovedForAll( + self: @ContractState, owner: ContractAddress, operator: ContractAddress, + ) -> bool { + ERC721MixinImpl::is_approved_for_all(self, owner, operator) + } + + fn tokenURI(self: @ContractState, tokenId: u256) -> ByteArray { + ERC721MixinImpl::token_uri(self, tokenId) + } + + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + ERC721MixinImpl::supports_interface(self, interface_id) + } + } + #[abi(embed_v0)] impl RedeemRequestImpl of IRedeemRequest { // ───────────────────────────────────────────────────────────────────── @@ -88,7 +194,7 @@ mod RedeemRequest { ) -> u256 { self._assert_vault(); let id = self.id_len.read(); - self.erc721.mint(to, id); + self.erc721.safe_mint(to, id, array![].span()); self.id_to_info.write(id, redeem_request_info); self.id_len.write(id + 1); id @@ -125,4 +231,18 @@ mod RedeemRequest { } } } + fn _check_on_erc721_received( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span, + ) -> bool { + let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; + + if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { + IERC721ReceiverDispatcher { contract_address: to } + .on_erc721_received( + get_caller_address(), from, token_id, data, + ) == IERC721_RECEIVER_ID + } else { + src5_dispatcher.supports_interface(ISRC6_ID) + } + } } diff --git a/packages/vault/src/test/mock_erc721_receiver.cairo b/packages/vault/src/test/mock_erc721_receiver.cairo new file mode 100644 index 00000000..d488b832 --- /dev/null +++ b/packages/vault/src/test/mock_erc721_receiver.cairo @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::contract] +pub mod MockERC721Receiver { + use openzeppelin::interfaces::erc721::{ERC721ReceiverMixin, IERC721_RECEIVER_ID}; + use openzeppelin::introspection::src5::SRC5Component; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[storage] + struct Storage { + #[substorage(v0)] + src5: SRC5Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + SRC5Event: SRC5Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.src5.register_interface(IERC721_RECEIVER_ID); + } + + impl SRC5Impl = SRC5Component::SRC5Impl; + impl SRC5InternalImpl = SRC5Component::InternalImpl; + + #[abi(embed_v0)] + impl ERC721ReceiverMixinImpl of ERC721ReceiverMixin { + fn on_erc721_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span, + ) -> felt252 { + IERC721_RECEIVER_ID + } + + fn onERC721Received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + data: Span, + ) -> felt252 { + IERC721_RECEIVER_ID + } + + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + self.src5.supports_interface(interface_id) + } + } +} diff --git a/packages/vault/src/test/units/redeem_request.cairo b/packages/vault/src/test/units/redeem_request.cairo index 01e11b49..c4d26343 100644 --- a/packages/vault/src/test/units/redeem_request.cairo +++ b/packages/vault/src/test/units/redeem_request.cairo @@ -13,7 +13,7 @@ use vault::redeem_request::interface::{ }; use vault::test::utils::{ DUMMY_ADDRESS, OTHER_DUMMY_ADDRESS, OWNER, cheat_caller_address_once, deploy_counter, - deploy_erc20_mock, deploy_redeem_request, deploy_vault, + deploy_erc20_mock, deploy_erc721_receiver_at, deploy_redeem_request, deploy_vault, }; use vault::vault::vault::Vault; use vault_allocator::mocks::counter::{ICounterDispatcher, ICounterDispatcherTrait}; @@ -22,6 +22,8 @@ fn set_up() -> (ContractAddress, IRedeemRequestDispatcher) { let underlying_assets = deploy_erc20_mock(); let vault = deploy_vault(underlying_assets); let redeem_request = deploy_redeem_request(vault.contract_address); + deploy_erc721_receiver_at(DUMMY_ADDRESS()); + deploy_erc721_receiver_at(OTHER_DUMMY_ADDRESS()); (vault.contract_address, redeem_request) } @@ -267,7 +269,8 @@ fn test_erc721_functionality() { assert(erc721_dispatcher.get_approved(id) == OTHER_DUMMY_ADDRESS(), 'Approved incorrect'); cheat_caller_address_once(redeem_request.contract_address, OTHER_DUMMY_ADDRESS()); - erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id); + erc721_dispatcher + .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id, array![].span()); assert(erc721_dispatcher.owner_of(id) == OTHER_DUMMY_ADDRESS(), 'Owner after transfer'); assert(erc721_dispatcher.balance_of(DUMMY_ADDRESS()) == 0, 'Balance after transfer'); @@ -299,10 +302,12 @@ fn test_set_approval_for_all() { ); cheat_caller_address_once(redeem_request.contract_address, OTHER_DUMMY_ADDRESS()); - erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1); + erc721_dispatcher + .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1, array![].span()); cheat_caller_address_once(redeem_request.contract_address, OTHER_DUMMY_ADDRESS()); - erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_2); + erc721_dispatcher + .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_2, array![].span()); assert(erc721_dispatcher.balance_of(DUMMY_ADDRESS()) == 0, 'Original owner balance'); assert(erc721_dispatcher.balance_of(OTHER_DUMMY_ADDRESS()) == 2, 'New owner balance'); @@ -329,7 +334,8 @@ fn test_complex_scenario_mint_burn_transfer() { assert(redeem_request.id_len() == 2, 'Initial ID length'); cheat_caller_address_once(redeem_request.contract_address, DUMMY_ADDRESS()); - erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1); + erc721_dispatcher + .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1, array![].span()); assert(erc721_dispatcher.owner_of(id_1) == OTHER_DUMMY_ADDRESS(), 'ID 1 new owner'); assert(erc721_dispatcher.balance_of(DUMMY_ADDRESS()) == 1, 'Balance after transfer'); diff --git a/packages/vault/src/test/units/vault.cairo b/packages/vault/src/test/units/vault.cairo index b0052e9e..291cc308 100644 --- a/packages/vault/src/test/units/vault.cairo +++ b/packages/vault/src/test/units/vault.cairo @@ -2,7 +2,7 @@ // Copyright (c) 2025 Starknet Vault Kit // Licensed under the MIT License. See LICENSE file for details. -use core::num::traits::Zero; +use core::num::traits::{Bounded, Zero}; use openzeppelin::interfaces::accesscontrol::{ IAccessControlDispatcher, IAccessControlDispatcherTrait, }; @@ -25,8 +25,8 @@ use vault::redeem_request::interface::{IRedeemRequestDispatcher, IRedeemRequestD use vault::test::utils::{ DUMMY_ADDRESS, FEES_RECIPIENT, MANAGEMENT_FEES, MAX_DELTA, ORACLE, OTHER_DUMMY_ADDRESS, OWNER, PERFORMANCE_FEES, REDEEM_FEES, REPORT_DELAY, VAULT_ALLOCATOR, VAULT_NAME, VAULT_SYMBOL, between, - cheat_caller_address_once, deploy_counter, deploy_erc20_mock, deploy_redeem_request, - deploy_vault, + cheat_caller_address_once, deploy_counter, deploy_erc20_mock, deploy_erc721_receiver_at, + deploy_redeem_request, deploy_vault, }; use vault::vault::interface::{IVaultDispatcher, IVaultDispatcherTrait}; use vault::vault::vault::Vault; @@ -41,6 +41,10 @@ fn set_up() -> (ContractAddress, IVaultDispatcher, IRedeemRequestDispatcher) { vault.register_redeem_request(redeem_request.contract_address); cheat_caller_address_once(vault.contract_address, OWNER()); vault.register_vault_allocator(VAULT_ALLOCATOR()); + deploy_erc721_receiver_at(DUMMY_ADDRESS()); + deploy_erc721_receiver_at(OTHER_DUMMY_ADDRESS()); + deploy_erc721_receiver_at(OWNER()); + (underlying_assets, vault, redeem_request) } @@ -761,80 +765,6 @@ fn test_request_redeem_exact_max_ok() { ], ); } - -#[test] -fn test_request_redeem_fee_exempt_when_owner_is_fees_recipient() { - let (underlying, vault, redeem_request) = set_up(); - - cheat_caller_address_once(vault.contract_address, OWNER()); - vault.set_fees_config(FEES_RECIPIENT(), REDEEM_FEES(), MANAGEMENT_FEES(), PERFORMANCE_FEES()); - - let deposit_amount = Vault::WAD; - let erc20_dispatcher = ERC20ABIDispatcher { contract_address: underlying }; - - cheat_caller_address_once(underlying, OWNER()); - erc20_dispatcher.transfer(FEES_RECIPIENT(), deposit_amount); - - cheat_caller_address_once(underlying, FEES_RECIPIENT()); - erc20_dispatcher.approve(vault.contract_address, deposit_amount); - - let erc4626_dispatcher = IERC4626Dispatcher { contract_address: vault.contract_address }; - cheat_caller_address_once(vault.contract_address, FEES_RECIPIENT()); - let shares = erc4626_dispatcher.deposit(deposit_amount, FEES_RECIPIENT()); - - let total_supply_before = ERC20ABIDispatcher { contract_address: vault.contract_address } - .total_supply(); - let epoch = vault.epoch(); - let redeem_nominal_before = vault.redeem_nominal(epoch); - - let mut spy = spy_events(); - cheat_caller_address_once(vault.contract_address, FEES_RECIPIENT()); - let id = vault.request_redeem(shares, DUMMY_ADDRESS(), FEES_RECIPIENT()); - - let expected_assets = shares; - let total_supply_after = ERC20ABIDispatcher { contract_address: vault.contract_address } - .total_supply(); - - assert(total_supply_after == total_supply_before - shares, 'TotalSupply incorrect'); - assert( - vault.redeem_nominal(epoch) == redeem_nominal_before + expected_assets, - 'Redeem - nominal incorrect', - ); - assert( - ERC20ABIDispatcher { contract_address: vault.contract_address } - .balance_of(FEES_RECIPIENT()) == 0, - 'Fees recipient balance', - ); - - let id_info = redeem_request.id_to_info(id); - assert(id_info.epoch == epoch, 'Epoch not set correctly'); - assert(id_info.nominal == expected_assets, 'Nominal not set correctly'); - - let erc721_dispatcher = ERC721ABIDispatcher { - contract_address: redeem_request.contract_address, - }; - assert(erc721_dispatcher.owner_of(id) == DUMMY_ADDRESS(), 'Owner not set correctly'); - - spy - .assert_emitted( - @array![ - ( - vault.contract_address, - Vault::Event::RedeemRequested( - Vault::RedeemRequested { - owner: FEES_RECIPIENT(), - receiver: DUMMY_ADDRESS(), - shares, - assets: expected_assets, - id, - epoch, - }, - ), - ), - ], - ); -} #[test] fn test_request_redeem_third_party_spender_uses_allowance_and_decreases_it() { let (underlying, vault, _) = set_up(); @@ -1559,7 +1489,7 @@ fn setup_report_simple_deposit_epoch_0() -> ( let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_buffer + expected_aum - (expected_management_fees_assets) + 1, + expected_buffer + expected_aum - (expected_management_fees_assets + 0) + 1, Rounding::Floor, ); @@ -1663,16 +1593,17 @@ fn setup_report_simple_deposit_with_profit_epoch_1() -> ( * MANAGEMENT_FEES() * REPORT_DELAY().into()) / (Vault::WAD * Vault::YEAR.into()); + let net_profit_after_mgmt = profit_amount - expected_management_fees_assets; + let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; - - let net_profit_after_mgmt = profit_amount - expected_management_fees_assets; - let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; let performance_fee_shares = math::u256_mul_div( expected_performance_fee_assets, expected_total_supply + 1, @@ -1776,7 +1707,7 @@ fn setup_report_simple_deposit_with_loss_epoch_1() -> ( let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - (expected_total_assets - expected_management_fees_assets) + 1, + (expected_total_assets - (expected_management_fees_assets + 0)) + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; @@ -1893,19 +1824,21 @@ fn setup_report_simple_redeem_unhandled_with_profit_epoch_1() -> ( let expected_total_assets = liqudity_after - expected_redeem_assets_after_cut_epoch_1; + let management_fee_assets_for_shareholders = expected_management_fees_assets + - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); + + let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; + let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; + let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; - - let management_fee_assets_for_shareholders = expected_management_fees_assets - - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); - - let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; - let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; let performance_fee_shares = math::u256_mul_div( expected_performance_fee_assets, expected_total_supply + 1, @@ -2044,19 +1977,21 @@ fn setup_report_simple_redeem_matched_with_profit_epoch_1() -> ( let expected_total_assets = liqudity_after - expected_redeem_assets_after_cut_epoch_1; + let management_fee_assets_for_shareholders = expected_management_fees_assets + - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); + + let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; + let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; + let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; - - let management_fee_assets_for_shareholders = expected_management_fees_assets - - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); - - let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; - let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; let performance_fee_shares = math::u256_mul_div( expected_performance_fee_assets, expected_total_supply + 1, @@ -2190,19 +2125,21 @@ fn setup_report_simple_redeem_handled_with_bring_liquidity_with_profit_epoch_1() let expected_total_assets = liqudity_after - expected_redeem_assets_after_cut_epoch_1; + let management_fee_assets_for_shareholders = expected_management_fees_assets + - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); + + let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; + let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; + let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; - - let management_fee_assets_for_shareholders = expected_management_fees_assets - - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); - - let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; - let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; let performance_fee_shares = math::u256_mul_div( expected_performance_fee_assets, expected_total_supply + 1, @@ -2336,19 +2273,21 @@ fn setup_report_simple_redeem_not_handled_with_bring_liquidity_with_profit_epoch let expected_total_assets = liqudity_after - expected_redeem_assets_after_cut_epoch_1; + let management_fee_assets_for_shareholders = expected_management_fees_assets + - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); + + let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; + let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; + let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; - - let management_fee_assets_for_shareholders = expected_management_fees_assets - - (expected_nominal - expected_redeem_assets_after_cut_epoch_1); - - let net_profit_after_mgmt = profit_amount - management_fee_assets_for_shareholders; - let expected_performance_fee_assets = PERFORMANCE_FEES() * net_profit_after_mgmt / Vault::WAD; let performance_fee_shares = math::u256_mul_div( expected_performance_fee_assets, expected_total_supply + 1, @@ -2486,11 +2425,14 @@ fn setup_report_simple_redeem_unhandled_with_loss_epoch_1() -> ( - cut; let expected_total_assets = liqudity_after - expected_redeem_assets_after_cut_epoch_1; + let expected_performance_fee_assets = 0; let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; @@ -2635,11 +2577,14 @@ fn setup_report_simple_redeem_unhandled_not_enough_buffer_with_loss_epoch_1() -> - cut; let expected_total_assets = liqudity_after - expected_redeem_assets_after_cut_epoch_1; + let expected_performance_fee_assets = 0; let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; @@ -2820,11 +2765,14 @@ fn setup_report_simple_redeem_unhandled_not_enough_buffer_with_loss_epoch_2_hand let expected_total_assets = liqudity_after - (expected_redeem_assets_after_cut_epoch_1 + expected_redeem_assets_after_cut_epoch_2); + let expected_performance_fee_assets = 0; let management_fee_shares = math::u256_mul_div( expected_management_fees_assets, expected_total_supply + 1, - expected_total_assets - (expected_management_fees_assets) + 1, + expected_total_assets + - (expected_management_fees_assets + expected_performance_fee_assets) + + 1, Rounding::Floor, ); expected_total_supply = expected_total_supply + management_fee_shares; @@ -2878,3 +2826,102 @@ fn setup_report_simple_redeem_unhandled_not_enough_buffer_with_loss_epoch_2_hand fn test_report_simple_redeem_unhandled_not_enough_buffer_with_loss_epoch_2_handled_epoch_1() { setup_report_simple_redeem_unhandled_not_enough_buffer_with_loss_epoch_2_handled_epoch_1(); } + +#[test] +fn test_deposit_limit() { + let (underlying, vault, _) = set_up(); + let erc4626_dispatcher = IERC4626Dispatcher { contract_address: vault.contract_address }; + let deposit_cap = Vault::WAD * 100; + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_deposit_limit(deposit_cap); + assert(vault.get_deposit_limit() == deposit_cap, 'Deposit limit not set'); + assert( + erc4626_dispatcher.max_deposit(DUMMY_ADDRESS()) == deposit_cap, 'Initial max deposit wrong', + ); + let first_deposit = Vault::WAD * 30; + cheat_caller_address_once(underlying, OWNER()); + ERC20ABIDispatcher { contract_address: underlying }.transfer(DUMMY_ADDRESS(), first_deposit); + cheat_caller_address_once(underlying, DUMMY_ADDRESS()); + ERC20ABIDispatcher { contract_address: underlying } + .approve(vault.contract_address, first_deposit); + cheat_caller_address_once(vault.contract_address, DUMMY_ADDRESS()); + erc4626_dispatcher.deposit(first_deposit, DUMMY_ADDRESS()); + let expected_remaining = deposit_cap - first_deposit; + assert( + erc4626_dispatcher.max_deposit(DUMMY_ADDRESS()) == expected_remaining, + 'Max deposit not reduced', + ); + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_deposit_limit(Bounded::MAX); + assert(vault.get_deposit_limit() == Bounded::MAX, 'Unlimited not set'); + assert( + erc4626_dispatcher.max_deposit(DUMMY_ADDRESS()) == Bounded::MAX, + 'Max deposit not unlimited', + ); +} + + +#[test] +#[should_panic(expected: ('Caller is missing role',))] +fn test_set_deposit_limit_unauthorized() { + let (_, vault, _) = set_up(); + + cheat_caller_address_once(vault.contract_address, DUMMY_ADDRESS()); + vault.set_deposit_limit(Vault::WAD); +} + + +#[test] +fn test_deposit_with_limit() { + let (underlying, vault, _) = set_up(); + let erc4626_dispatcher = IERC4626Dispatcher { contract_address: vault.contract_address }; + + let deposit_limit = Vault::WAD; + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_deposit_limit(deposit_limit); + + cheat_caller_address_once(underlying, OWNER()); + ERC20ABIDispatcher { contract_address: underlying }.transfer(DUMMY_ADDRESS(), Vault::WAD * 2); + cheat_caller_address_once(underlying, DUMMY_ADDRESS()); + ERC20ABIDispatcher { contract_address: underlying } + .approve(vault.contract_address, Vault::WAD * 2); + + cheat_caller_address_once(vault.contract_address, DUMMY_ADDRESS()); + erc4626_dispatcher.deposit(deposit_limit, DUMMY_ADDRESS()); + + assert( + ERC20ABIDispatcher { contract_address: vault.contract_address } + .balance_of(DUMMY_ADDRESS()) > 0, + 'Deposit failed', + ); +} + +#[test] +#[should_panic(expected: 'ERC4626: exceeds max deposit')] +fn test_deposit_exceeds_limit() { + let (underlying, vault, _) = set_up(); + let erc4626_dispatcher = IERC4626Dispatcher { contract_address: vault.contract_address }; + + let deposit_limit = Vault::WAD; + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_deposit_limit(deposit_limit); + + cheat_caller_address_once(underlying, OWNER()); + ERC20ABIDispatcher { contract_address: underlying }.transfer(DUMMY_ADDRESS(), Vault::WAD * 2); + cheat_caller_address_once(underlying, DUMMY_ADDRESS()); + ERC20ABIDispatcher { contract_address: underlying } + .approve(vault.contract_address, Vault::WAD * 2); + + cheat_caller_address_once(vault.contract_address, DUMMY_ADDRESS()); + erc4626_dispatcher.deposit(deposit_limit + 1, DUMMY_ADDRESS()); +} + +#[test] +#[should_panic(expected: "Caller is not the vault allocator")] +fn test_bring_liquidity_unauthorized() { + let (_underlying, vault, _) = set_up(); + + // Try to call bring_liquidity from unauthorized address + cheat_caller_address_once(vault.contract_address, DUMMY_ADDRESS()); + vault.bring_liquidity(1000); +} diff --git a/packages/vault/src/test/utils.cairo b/packages/vault/src/test/utils.cairo index bd3dc487..37a570ff 100644 --- a/packages/vault/src/test/utils.cairo +++ b/packages/vault/src/test/utils.cairo @@ -93,6 +93,15 @@ pub fn deploy_vault(underlying_asset: ContractAddress) -> IVaultDispatcher { IVaultDispatcher { contract_address: vault_allocator_address } } +pub fn deploy_erc721_receiver_at(targetAddress: ContractAddress) -> ContractAddress { + let vault_erc721_receiver = declare("MockERC721Receiver").unwrap().contract_class(); + let mut calldata = ArrayTrait::new(); + let (vault_erc721_receiver_address, _) = vault_erc721_receiver + .deploy_at(@calldata, targetAddress) + .unwrap(); + vault_erc721_receiver_address +} + pub fn deploy_redeem_request(vault: ContractAddress) -> IRedeemRequestDispatcher { let redeem_request = declare("RedeemRequest").unwrap().contract_class(); let mut calldata = ArrayTrait::new(); diff --git a/packages/vault/src/vault/errors.cairo b/packages/vault/src/vault/errors.cairo index 01ee5f24..4d9d9bf9 100644 --- a/packages/vault/src/vault/errors.cairo +++ b/packages/vault/src/vault/errors.cairo @@ -66,4 +66,8 @@ pub mod Errors { pub fn invalid_new_aum(new_aum: u256) { panic!("Invalid new AUM: {}", new_aum); } + + pub fn caller_is_not_vault_allocator() { + panic!("Caller is not the vault allocator"); + } } diff --git a/packages/vault/src/vault/interface.cairo b/packages/vault/src/vault/interface.cairo index 5e7d9295..c5f9d151 100644 --- a/packages/vault/src/vault/interface.cairo +++ b/packages/vault/src/vault/interface.cairo @@ -4,7 +4,6 @@ // Standard library imports use starknet::ContractAddress; -use vault::redeem_request::interface::RedeemRequestInfo; #[starknet::interface] pub trait IVault { @@ -46,5 +45,10 @@ pub trait IVault { fn last_report_timestamp(self: @TContractState) -> u64; fn max_delta(self: @TContractState) -> u256; fn due_assets_from_id(self: @TContractState, id: u256) -> u256; + fn due_assets_from_owner(self: @TContractState, owner: ContractAddress) -> u256; + + // Limit configuration functions + fn set_deposit_limit(ref self: TContractState, limit: u256); + fn get_deposit_limit(self: @TContractState) -> u256; } diff --git a/packages/vault/src/vault/vault.cairo b/packages/vault/src/vault/vault.cairo index af0030e1..48f3f048 100644 --- a/packages/vault/src/vault/vault.cairo +++ b/packages/vault/src/vault/vault.cairo @@ -18,20 +18,21 @@ // ═══════════════════════════════════════════════════════════════════════════════════════════════════ #[starknet::contract] -pub mod Vault { - use core::num::traits::Zero; +pub mod VaultMigration { + use core::num::traits::{Bounded, Zero}; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::interfaces::erc20::{ ERC20ABIDispatcher, ERC20ABIDispatcherTrait, IERC20Metadata, }; - use openzeppelin::interfaces::erc721::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait}; + use openzeppelin::interfaces::erc721::{ + ERC721ABIDispatcher, ERC721ABIDispatcherTrait, IERC721EnumerableDispatcher, + IERC721EnumerableDispatcherTrait, + }; use openzeppelin::interfaces::upgrades::IUpgradeable; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::security::pausable::PausableComponent; use openzeppelin::token::erc20::extensions::erc4626::ERC4626Component::Fee; - use openzeppelin::token::erc20::extensions::erc4626::{ - DefaultConfig, ERC4626Component, ERC4626DefaultNoFees, ERC4626DefaultNoLimits, - }; + use openzeppelin::token::erc20::extensions::erc4626::{DefaultConfig, ERC4626Component}; use openzeppelin::token::erc20::{ DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl, }; @@ -84,6 +85,93 @@ pub mod Vault { impl ERC4626Impl = ERC4626Component::ERC4626Impl; impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + + impl ERC4626FeesImpl of ERC4626Component::FeeConfigTrait { + fn calculate_deposit_fee( + self: @ERC4626Component::ComponentState, assets: u256, shares: u256, + ) -> Option { + Option::None + } + + fn calculate_mint_fee( + self: @ERC4626Component::ComponentState, assets: u256, shares: u256, + ) -> Option { + Option::None + } + + fn calculate_withdraw_fee( + self: @ERC4626Component::ComponentState, assets: u256, shares: u256, + ) -> Option { + let contract_state = self.get_contract(); + let fee_shares = contract_state._calculate_fee_shares(shares); + Option::Some(Fee::Shares(fee_shares)) + } + + fn calculate_redeem_fee( + self: @ERC4626Component::ComponentState, assets: u256, shares: u256, + ) -> Option { + let contract_state = self.get_contract(); + let fee_shares = contract_state._calculate_fee_shares(shares); + Option::Some(Fee::Shares(fee_shares)) + } + } + + + // --- Custom ERC4626 Limits Implementation --- + // Custom implementation of deposit/withdraw limits + // Uses max u256 as sentinel value for "unlimited" + impl ERC4626LimitConfigImpl of ERC4626Component::LimitConfigTrait { + /// The max deposit allowed based on cap. + /// Returns remaining capacity: cap - total_assets + /// Returns None for no limit (when limit equals max u256) + fn deposit_limit( + self: @ERC4626Component::ComponentState, receiver: ContractAddress, + ) -> Option { + let contract_state = self.get_contract(); + let limit = contract_state.deposit_limit.read(); + if limit == Bounded::MAX { + Option::None + } else { + let total_assets = self.get_total_assets(); + if total_assets >= limit { + Option::Some(0) + } else { + Option::Some(limit - total_assets) + } + } + } + + /// The max mint allowed based on cap. + /// Returns shares equivalent of remaining deposit capacity + /// Returns None for no limit (when limit equals max u256) + fn mint_limit( + self: @ERC4626Component::ComponentState, receiver: ContractAddress, + ) -> Option { + let deposit_limit_opt = self.deposit_limit(receiver); + match deposit_limit_opt { + Option::None => Option::None, + Option::Some(deposit_remaining) => { + Option::Some(self._convert_to_shares(deposit_remaining, Rounding::Floor)) + }, + } + } + + /// The max withdraw allowed. + /// Not implemented - withdrawals are disabled in this vault + fn withdraw_limit( + self: @ERC4626Component::ComponentState, owner: ContractAddress, + ) -> Option { + Option::None + } + + /// The max redeem allowed. + fn redeem_limit( + self: @ERC4626Component::ComponentState, owner: ContractAddress, + ) -> Option { + Option::None + } + } + // --- ERC20 Implementation --- // Share token functionality with standard and camelCase interfaces #[abi(embed_v0)] @@ -142,7 +230,10 @@ pub mod Vault { management_fees: u256, // Annual management fee rate (in WAD) performance_fees: u256, // Performance fee on profits (in WAD) // --- Redemption System --- - redeem_request: IRedeemRequestDispatcher // NFT contract for tracking redemption requests + redeem_request: IRedeemRequestDispatcher, // NFT contract for tracking redemption requests + // --- ERC4626 Limits --- + // Note: max u256 means unlimited, any other value (including 0) sets a specific limit + deposit_limit: u256 // Maximum deposit amount } // --- Events --- @@ -150,16 +241,23 @@ pub mod Vault { #[derive(Drop, starknet::Event)] pub enum Event { // Component events + #[flat] ERC20Event: ERC20Component::Event, + #[flat] ERC4626Event: ERC4626Component::Event, + #[flat] SRC5Event: SRC5Component::Event, + #[flat] AccessControlEvent: AccessControlComponent::Event, + #[flat] UpgradeableEvent: UpgradeableComponent::Event, + #[flat] PausableEvent: PausableComponent::Event, // Vault-specific events RedeemRequested: RedeemRequested, // Emitted when a redemption is requested RedeemClaimed: RedeemClaimed, // Emitted when a redemption is claimed - Report: Report // Emitted when oracle reports new AUM + Report: Report, // Emitted when oracle reports new AUM + BringLiquidity: BringLiquidity // Emitted when liquidity is brought back from allocators } /// Event emitted when a user requests a redemption @@ -194,6 +292,16 @@ pub mod Vault { pub performance_fee_shares: u256 // Performance fee shares minted } + /// Event emitted when liquidity is brought back from allocators + #[derive(Drop, starknet::Event)] + pub struct BringLiquidity { + pub caller: ContractAddress, // Address that initiated the liquidity transfer + pub amount: u256, // Amount of assets brought back to vault + pub new_buffer: u256, // New buffer amount after the transfer + pub new_aum: u256, // New AUM amount after the transfer + pub epoch: u256 // Epoch when the liquidity was brought back + } + /// Initialize the vault with configuration parameters /// Sets up all components, roles, and fee structure #[constructor] @@ -240,6 +348,10 @@ pub mod Vault { // Initialize timestamp for fee calculations self.last_report_timestamp.write(get_block_timestamp()); + // Initialize limits to max u256 (unlimited) + // max u256 is used as sentinel value for "no limit" + let max_limit: u256 = Bounded::MAX; + self.deposit_limit.write(max_limit); self .emit( Report { @@ -329,7 +441,8 @@ pub mod Vault { shares: u256, fee: Option, ) { - Errors::not_implemented(); // Withdrawals disabled - use request_redeem instead + let mut contract_state = self.get_contract_mut(); + contract_state.pausable.assert_not_paused(); } @@ -343,7 +456,10 @@ pub mod Vault { assets: u256, shares: u256, fee: Option, - ) {} + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.buffer.write(contract_state.buffer.read() - assets); + } /// Hook executed before transferring assets and minting shares during deposit @@ -498,19 +614,14 @@ pub mod Vault { } // Calculate and collect redemption fees - let fees_recipient = self.fees_recipient.read(); - let redeem_fees = if (owner == fees_recipient) { - 0 - } else { - self.redeem_fees.read() - }; - let fee_shares = (shares * redeem_fees) - / WAD; // Fee calculation: shares * fee_rate / 1e18 + let fee_shares = self._calculate_fee_shares(shares); if (fee_shares.is_non_zero()) { self .erc20 - .update(owner, fees_recipient, fee_shares); // Transfer fee shares to recipient + .update( + owner, self.fees_recipient.read(), fee_shares, + ); // Transfer fee shares to recipient } let remaining_shares = shares - fee_shares; // Shares after fee deduction @@ -712,10 +823,10 @@ pub mod Vault { Errors::vault_allocator_not_set(); } // Deploy all remaining buffer to allocator - ERC20ABIDispatcher { contract_address: self.erc4626.asset() } - .transfer(alloc, remaining_buffer); - self.aum.write(new_aum + remaining_buffer); // Update AUM to include deployed assets - self.buffer.write(0); // Buffer is now empty + // ERC20ABIDispatcher { contract_address: self.erc4626.asset() } + // .transfer(alloc, remaining_buffer); + self.aum.write(new_aum); // Update AUM to include deployed assets + self.buffer.write(remaining_buffer); // Buffer is now empty } else { self.aum.write(new_aum); // Keep buffer for pending redemptions self.buffer.write(remaining_buffer); @@ -736,7 +847,7 @@ pub mod Vault { let management_fee_shares = math::u256_mul_div( management_fee_assets, total_supply + 1, - (total_assets - management_fee_assets) + 1, + (total_assets - (management_fee_assets + performance_fee_assets)) + 1, Rounding::Floor, ); if (management_fee_shares.is_non_zero()) { @@ -775,10 +886,24 @@ pub mod Vault { fn bring_liquidity( ref self: ContractState, amount: u256, ) { // Amount of assets to bring back + let caller = get_caller_address(); + // Only the registered vault allocator can bring liquidity + if (caller != self.vault_allocator.read()) { + Errors::caller_is_not_vault_allocator(); + } ERC20ABIDispatcher { contract_address: self.erc4626.asset() } - .transfer_from(get_caller_address(), starknet::get_contract_address(), amount); - self.buffer.write(self.buffer.read() + amount); // Increase buffer - self.aum.write(self.aum.read() - amount); // Decrease deployed AUM + .transfer_from(caller, starknet::get_contract_address(), amount); + let new_buffer = self.buffer.read() + amount; // Calculate new buffer + let new_aum = self.aum.read() - amount; // Calculate new AUM + self.buffer.write(new_buffer); // Increase buffer + self.aum.write(new_aum); // Decrease deployed AUM + + self + .emit( + BringLiquidity { + caller, amount, new_buffer, new_aum, epoch: self.epoch.read(), + }, + ); } // --- State Getter Functions --- @@ -860,6 +985,40 @@ pub mod Vault { self.max_delta.read() } + // --- Limit Configuration Functions --- + + /// Set the deposit limit (max u256 for unlimited, any other value including 0 for specific + /// limit) + /// Only callable by owner + fn set_deposit_limit(ref self: ContractState, limit: u256) { + self.access_control.assert_only_role(OWNER_ROLE); + self.deposit_limit.write(limit); + } + + /// Get the current deposit limit (max u256 means unlimited) + fn get_deposit_limit(self: @ContractState) -> u256 { + self.deposit_limit.read() + } + + fn due_assets_from_owner(self: @ContractState, owner: ContractAddress) -> u256 { + let balance = ERC721ABIDispatcher { + contract_address: self.redeem_request.read().contract_address, + } + .balance_of(owner); + let mut total_due_assets = 0; + for i in 0..balance { + total_due_assets += self + .due_assets_from_id( + IERC721EnumerableDispatcher { + contract_address: self.redeem_request.read().contract_address, + } + .token_of_owner_by_index(owner, i), + ); + } + total_due_assets + } + + fn due_assets_from_id(self: @ContractState, id: u256) -> u256 { let redeem_request_info = self.redeem_request.read().id_to_info(id); let redeem_request_nominal = redeem_request_info @@ -966,5 +1125,10 @@ pub mod Vault { } total_redeem_assets } + + + fn _calculate_fee_shares(self: @ContractState, shares: u256) -> u256 { + (shares * self.redeem_fees.read()) / WAD + } } } diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/base_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/base_decoder_and_sanitizer.cairo index 63304d20..62cc34bb 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/base_decoder_and_sanitizer.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/base_decoder_and_sanitizer.cairo @@ -28,18 +28,9 @@ pub mod BaseDecoderAndSanitizerComponent { serialized_struct.span() } - fn flash_loan( - self: @ComponentState, - receiver: ContractAddress, - asset: ContractAddress, - amount: u256, - is_legacy: bool, - data: Span, - ) -> Span { + + fn bring_liquidity(self: @ComponentState, amount: u256) -> Span { let mut serialized_struct: Array = ArrayTrait::new(); - receiver.serialize(ref serialized_struct); - asset.serialize(ref serialized_struct); - is_legacy.serialize(ref serialized_struct); serialized_struct.span() } } diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/interface.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/interface.cairo index 7314c955..e5f266e1 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/interface.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/interface.cairo @@ -7,12 +7,5 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IBaseDecoderAndSanitizer { fn approve(self: @T, spender: ContractAddress, amount: u256) -> Span; - fn flash_loan( - self: @T, - receiver: ContractAddress, - asset: ContractAddress, - amount: u256, - is_legacy: bool, - data: Span, - ) -> Span; + fn bring_liquidity(self: @T, amount: u256) -> Span; } diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/multiply_decoder_and_sanitizer/multiply_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/multiply_decoder_and_sanitizer/multiply_decoder_and_sanitizer.cairo index 6d4a8ff6..f096cd45 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/multiply_decoder_and_sanitizer/multiply_decoder_and_sanitizer.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/multiply_decoder_and_sanitizer/multiply_decoder_and_sanitizer.cairo @@ -16,8 +16,8 @@ pub mod MultiplyDecoderAndSanitizerComponent { #[derive(Drop, Debug, PartialEq, starknet::Event)] pub enum Event {} - #[embeddable_as(VesuDecoderAndSanitizerImpl)] - impl VesuDecoderAndSanitizer< + #[embeddable_as(MultiplyDecoderAndSanitizerImpl)] + impl MultiplyDecoderAndSanitizer< TContractState, +HasComponent, > of IMultiplyDecoderAndSanitizer> { fn modify_lever( diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo index 857e206a..fda00195 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo @@ -7,6 +7,7 @@ pub mod SimpleDecoderAndSanitizer { use vault_allocator::decoders_and_sanitizers::avnu_exchange_decoder_and_sanitizer::avnu_exchange_decoder_and_sanitizer::AvnuExchangeDecoderAndSanitizerComponent; use vault_allocator::decoders_and_sanitizers::base_decoder_and_sanitizer::BaseDecoderAndSanitizerComponent; use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent; + use vault_allocator::decoders_and_sanitizers::starknet_vault_kit_decoder_and_sanitizer::starknet_vault_kit_decoder_and_sanitizer::StarknetVaultKitDecoderAndSanitizerComponent; use vault_allocator::decoders_and_sanitizers::vesu_decoder_and_sanitizer::vesu_decoder_and_sanitizer::VesuDecoderAndSanitizerComponent; component!( @@ -20,6 +21,12 @@ pub mod SimpleDecoderAndSanitizer { event: Erc4626DecoderAndSanitizerEvent, ); + component!( + path: StarknetVaultKitDecoderAndSanitizerComponent, + storage: starknet_vault_kit_decoder_and_sanitizer, + event: StarknetVaultKitDecoderAndSanitizerEvent, + ); + component!( path: VesuDecoderAndSanitizerComponent, storage: vesu_decoder_and_sanitizer, @@ -32,6 +39,7 @@ pub mod SimpleDecoderAndSanitizer { event: AvnuExchangeDecoderAndSanitizerEvent, ); + #[abi(embed_v0)] impl BaseDecoderAndSanitizerImpl = BaseDecoderAndSanitizerComponent::BaseDecoderAndSanitizerImpl; @@ -50,6 +58,7 @@ pub mod SimpleDecoderAndSanitizer { ContractState, >; + #[storage] pub struct Storage { #[substorage(v0)] @@ -60,6 +69,8 @@ pub mod SimpleDecoderAndSanitizer { pub vesu_decoder_and_sanitizer: VesuDecoderAndSanitizerComponent::Storage, #[substorage(v0)] pub avnu_exchange_decoder_and_sanitizer: AvnuExchangeDecoderAndSanitizerComponent::Storage, + #[substorage(v0)] + pub starknet_vault_kit_decoder_and_sanitizer: StarknetVaultKitDecoderAndSanitizerComponent::Storage, } #[event] @@ -73,5 +84,7 @@ pub mod SimpleDecoderAndSanitizer { VesuDecoderAndSanitizerEvent: VesuDecoderAndSanitizerComponent::Event, #[flat] AvnuExchangeDecoderAndSanitizerEvent: AvnuExchangeDecoderAndSanitizerComponent::Event, + #[flat] + StarknetVaultKitDecoderAndSanitizerEvent: StarknetVaultKitDecoderAndSanitizerComponent::Event, } } diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/interface.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/interface.cairo new file mode 100644 index 00000000..837d592b --- /dev/null +++ b/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/interface.cairo @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IStarknetVaultKitDecoderAndSanitizer { + fn request_redeem( + self: @T, shares: u256, receiver: ContractAddress, owner: ContractAddress, + ) -> Span; + + fn claim_redeem(self: @T, id: u256) -> Span; +} + diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo new file mode 100644 index 00000000..e5ecbc3d --- /dev/null +++ b/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::component] +pub mod StarknetVaultKitDecoderAndSanitizerComponent { + use starknet::ContractAddress; + use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent; + use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent::Erc4626DecoderAndSanitizerImpl; + use vault_allocator::decoders_and_sanitizers::starknet_vault_kit_decoder_and_sanitizer::interface::IStarknetVaultKitDecoderAndSanitizer; + #[storage] + pub struct Storage {} + + #[event] + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub enum Event {} + + #[embeddable_as(VesuDecoderAndSanitizerImpl)] + impl StarknetVaultKitDecoderAndSanitizer< + TContractState, + +HasComponent, + +Erc4626DecoderAndSanitizerComponent::HasComponent, + > of IStarknetVaultKitDecoderAndSanitizer> { + fn request_redeem( + self: @ComponentState, + shares: u256, + receiver: ContractAddress, + owner: ContractAddress, + ) -> Span { + let mut serialized_struct: Array = ArrayTrait::new(); + receiver.serialize(ref serialized_struct); + owner.serialize(ref serialized_struct); + serialized_struct.span() + } + + fn claim_redeem(self: @ComponentState, id: u256) -> Span { + let mut serialized_struct: Array = ArrayTrait::new(); + serialized_struct.span() + } + } +} diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/interface.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/interface.cairo new file mode 100644 index 00000000..b86d7bac --- /dev/null +++ b/packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/interface.cairo @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IUnCapDecoderAndSanitizer { + fn provide_to_sp(self: @T, top_up: u256, do_claim: bool) -> Span; + fn withdraw_from_sp(self: @T, amount: u256, do_claim: bool) -> Span; +} + diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/uncap_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/uncap_decoder_and_sanitizer.cairo new file mode 100644 index 00000000..581fa73e --- /dev/null +++ b/packages/vault_allocator/src/decoders_and_sanitizers/uncap_decoder_and_sanitizer/uncap_decoder_and_sanitizer.cairo @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::component] +pub mod UnCapDecoderAndSanitizerComponent { + use starknet::ContractAddress; + use vault_allocator::decoders_and_sanitizers::uncap_decoder_and_sanitizer::interface::IUnCapDecoderAndSanitizer; + + #[storage] + pub struct Storage { + pub vault_allocator: ContractAddress, + } + + #[event] + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub enum Event {} + + #[embeddable_as(UnCapDecoderAndSanitizerImpl)] + impl UnCapDecoderAndSanitizer< + TContractState, +HasComponent, + > of IUnCapDecoderAndSanitizer> { + fn provide_to_sp( + self: @ComponentState, top_up: u256, do_claim: bool, + ) -> Span { + let mut serialized_struct: Array = ArrayTrait::new(); + serialized_struct.span() + } + + fn withdraw_from_sp( + self: @ComponentState, amount: u256, do_claim: bool, + ) -> Span { + let mut serialized_struct: Array = ArrayTrait::new(); + serialized_struct.span() + } + } +} diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/vesu_decoder_and_sanitizer/vesu_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/vesu_decoder_and_sanitizer/vesu_decoder_and_sanitizer.cairo index 91747a65..5dc1e578 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/vesu_decoder_and_sanitizer/vesu_decoder_and_sanitizer.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/vesu_decoder_and_sanitizer/vesu_decoder_and_sanitizer.cairo @@ -5,8 +5,6 @@ #[starknet::component] pub mod VesuDecoderAndSanitizerComponent { use vault_allocator::decoders_and_sanitizers::decoder_custom_types::ModifyPositionParams; - use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent; - use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent::Erc4626DecoderAndSanitizerImpl; use vault_allocator::decoders_and_sanitizers::vesu_decoder_and_sanitizer::interface::IVesuDecoderAndSanitizer; #[storage] @@ -18,9 +16,7 @@ pub mod VesuDecoderAndSanitizerComponent { #[embeddable_as(VesuDecoderAndSanitizerImpl)] impl VesuDecoderAndSanitizer< - TContractState, - +HasComponent, - +Erc4626DecoderAndSanitizerComponent::HasComponent, + TContractState, +HasComponent, > of IVesuDecoderAndSanitizer> { fn modify_position( self: @ComponentState, params: ModifyPositionParams, diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_decoder_and_sanitizer/vesu_v2_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_decoder_and_sanitizer/vesu_v2_decoder_and_sanitizer.cairo index 996c50a1..f4d10f31 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_decoder_and_sanitizer/vesu_v2_decoder_and_sanitizer.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_decoder_and_sanitizer/vesu_v2_decoder_and_sanitizer.cairo @@ -5,8 +5,6 @@ #[starknet::component] pub mod VesuV2DecoderAndSanitizerComponent { use vault_allocator::decoders_and_sanitizers::decoder_custom_types::ModifyPositionParamsV2; - use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent; - use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent::Erc4626DecoderAndSanitizerImpl; use vault_allocator::decoders_and_sanitizers::vesu_v2_decoder_and_sanitizer::interface::IVesuV2DecoderAndSanitizer; #[storage] @@ -16,11 +14,9 @@ pub mod VesuV2DecoderAndSanitizerComponent { #[derive(Drop, Debug, PartialEq, starknet::Event)] pub enum Event {} - #[embeddable_as(VesuDecoderAndSanitizerImpl)] - impl VesuDecoderAndSanitizer< - TContractState, - +HasComponent, - +Erc4626DecoderAndSanitizerComponent::HasComponent, + #[embeddable_as(VesuV2DecoderAndSanitizerImpl)] + impl VesuV2DecoderAndSanitizer< + TContractState, +HasComponent, > of IVesuV2DecoderAndSanitizer> { fn modify_position( self: @ComponentState, params: ModifyPositionParamsV2, diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_specific_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_specific_decoder_and_sanitizer.cairo new file mode 100644 index 00000000..3e36004d --- /dev/null +++ b/packages/vault_allocator/src/decoders_and_sanitizers/vesu_v2_specific_decoder_and_sanitizer.cairo @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::contract] +pub mod VesuV2SpecificDecoderAndSanitizer { + use vault_allocator::decoders_and_sanitizers::base_decoder_and_sanitizer::BaseDecoderAndSanitizerComponent; + use vault_allocator::decoders_and_sanitizers::vesu_v2_decoder_and_sanitizer::vesu_v2_decoder_and_sanitizer::VesuV2DecoderAndSanitizerComponent; + + component!( + path: BaseDecoderAndSanitizerComponent, + storage: base_decoder_and_sanitizer, + event: BaseDecoderAndSanitizerEvent, + ); + + component!( + path: VesuV2DecoderAndSanitizerComponent, + storage: vesu_v2_decoder_and_sanitizer, + event: VesuV2DecoderAndSanitizerEvent, + ); + + #[abi(embed_v0)] + impl BaseDecoderAndSanitizerImpl = + BaseDecoderAndSanitizerComponent::BaseDecoderAndSanitizerImpl; + + #[abi(embed_v0)] + impl VesuV2DecoderAndSanitizerImpl = + VesuV2DecoderAndSanitizerComponent::VesuV2DecoderAndSanitizerImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub base_decoder_and_sanitizer: BaseDecoderAndSanitizerComponent::Storage, + #[substorage(v0)] + pub vesu_v2_decoder_and_sanitizer: VesuV2DecoderAndSanitizerComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + BaseDecoderAndSanitizerEvent: BaseDecoderAndSanitizerComponent::Event, + #[flat] + VesuV2DecoderAndSanitizerEvent: VesuV2DecoderAndSanitizerComponent::Event, + } +} diff --git a/packages/vault_allocator/src/integration_interfaces/vesu.cairo b/packages/vault_allocator/src/integration_interfaces/vesu.cairo deleted file mode 100644 index 73aaccd4..00000000 --- a/packages/vault_allocator/src/integration_interfaces/vesu.cairo +++ /dev/null @@ -1,396 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2025 Starknet Vault Kit -// Licensed under the MIT License. See LICENSE file for details. - -use starknet::ContractAddress; - -#[starknet::interface] -pub trait ISingletonV2< - TContractState, -> { // fn creator_nonce(self: @TContractState, creator: ContractAddress) -> felt252; - fn extension(self: @TContractState, pool_id: felt252) -> ContractAddress; - // fn asset_config_unsafe( - // self: @TContractState, pool_id: felt252, asset: ContractAddress, - // ) -> (AssetConfig, u256); - // fn asset_config( - // ref self: TContractState, pool_id: felt252, asset: ContractAddress, - // ) -> (AssetConfig, u256); - // fn ltv_config( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // ) -> LTVConfig; - // fn position_unsafe( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // ) -> (Position, u256, u256); - // fn position( - // ref self: TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // ) -> (Position, u256, u256); - // fn check_collateralization_unsafe( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // ) -> (bool, u256, u256); - // fn check_collateralization( - // ref self: TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // ) -> (bool, u256, u256); - // fn rate_accumulator_unsafe( - // self: @TContractState, pool_id: felt252, asset: ContractAddress, - // ) -> u256; - // fn rate_accumulator(ref self: TContractState, pool_id: felt252, asset: ContractAddress) -> - // u256; - // fn utilization_unsafe(self: @TContractState, pool_id: felt252, asset: ContractAddress) -> - // u256; - // fn utilization(ref self: TContractState, pool_id: felt252, asset: ContractAddress) -> u256; - // fn delegation( - // self: @TContractState, - // pool_id: felt252, - // delegator: ContractAddress, - // delegatee: ContractAddress, - // ) -> bool; - // fn calculate_pool_id( - // self: @TContractState, caller_address: ContractAddress, nonce: felt252, - // ) -> felt252; - // fn calculate_debt( - // self: @TContractState, nominal_debt: i257, rate_accumulator: u256, asset_scale: u256, - // ) -> u256; - // fn calculate_nominal_debt( - // self: @TContractState, debt: i257, rate_accumulator: u256, asset_scale: u256, - // ) -> u256; - // fn calculate_collateral_shares_unsafe( - // self: @TContractState, pool_id: felt252, asset: ContractAddress, collateral: i257, - // ) -> u256; - // fn calculate_collateral_shares( - // ref self: TContractState, pool_id: felt252, asset: ContractAddress, collateral: i257, - // ) -> u256; - // fn calculate_collateral_unsafe( - // self: @TContractState, pool_id: felt252, asset: ContractAddress, collateral_shares: i257, - // ) -> u256; - // fn calculate_collateral( - // ref self: TContractState, pool_id: felt252, asset: ContractAddress, collateral_shares: - // i257, - // ) -> u256; - // fn deconstruct_collateral_amount_unsafe( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // collateral: Amount, - // ) -> (i257, i257); - // fn deconstruct_collateral_amount( - // ref self: TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // collateral: Amount, - // ) -> (i257, i257); - // fn deconstruct_debt_amount_unsafe( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // debt: Amount, - // ) -> (i257, i257); - // fn deconstruct_debt_amount( - // ref self: TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // debt: Amount, - // ) -> (i257, i257); - // fn context_unsafe( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // ) -> Context; - // fn context( - // ref self: TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // user: ContractAddress, - // ) -> Context; - // fn create_pool( - // ref self: TContractState, - // asset_params: Span, - // ltv_params: Span, - // extension: ContractAddress, - // ) -> felt252; - // fn modify_position( - // ref self: TContractState, params: ModifyPositionParams, - // ) -> UpdatePositionResponse; - // fn liquidate_position( - // ref self: TContractState, params: LiquidatePositionParams, - // ) -> UpdatePositionResponse; - fn flash_loan( - ref self: TContractState, - receiver: ContractAddress, - asset: ContractAddress, - amount: u256, - is_legacy: bool, - data: Span, - ); - // fn modify_delegation( -// ref self: TContractState, pool_id: felt252, delegatee: ContractAddress, delegation: bool, -// ); -// fn donate_to_reserve( -// ref self: TContractState, pool_id: felt252, asset: ContractAddress, amount: u256, -// ); -// fn retrieve_from_reserve( -// ref self: TContractState, -// pool_id: felt252, -// asset: ContractAddress, -// receiver: ContractAddress, -// amount: u256, -// ); -// fn set_asset_config(ref self: TContractState, pool_id: felt252, params: AssetParams); -// fn set_ltv_config( -// ref self: TContractState, -// pool_id: felt252, -// collateral_asset: ContractAddress, -// debt_asset: ContractAddress, -// ltv_config: LTVConfig, -// ); -// fn set_asset_parameter( -// ref self: TContractState, -// pool_id: felt252, -// asset: ContractAddress, -// parameter: felt252, -// value: u256, -// ); -// fn set_extension(ref self: TContractState, pool_id: felt252, extension: ContractAddress); -// fn set_extension_whitelist( -// ref self: TContractState, extension: ContractAddress, approved: bool, -// ); -// fn claim_fee_shares(ref self: TContractState, pool_id: felt252, asset: ContractAddress); - - // fn migrate_pool( -// ref self: TContractState, -// pool_id: felt252, -// extension: ContractAddress, -// creator: ContractAddress, -// asset_configs: Span<(ContractAddress, AssetConfig)>, -// ltv_configs: Span<(ContractAddress, ContractAddress, LTVConfig)>, -// ); -// fn migrate_position( -// ref self: TContractState, -// pool_id: felt252, -// collateral_asset: ContractAddress, -// debt_asset: ContractAddress, -// from: ContractAddress, -// to: ContractAddress, -// ); -// fn set_migrator(ref self: TContractState, migrator: ContractAddress); - - // fn upgrade_name(self: @TContractState) -> felt252; -// fn upgrade(ref self: TContractState, new_implementation: ClassHash); -} - -#[starknet::interface] -pub trait IDefaultExtensionPOV2< - TContractState, -> { // fn pool_name(self: @TContractState, pool_id: felt252) -> felt252; - // fn pool_owner(self: @TContractState, pool_id: felt252) -> ContractAddress; - // fn shutdown_mode_agent(self: @TContractState, pool_id: felt252) -> ContractAddress; - // fn pragma_oracle(self: @TContractState) -> ContractAddress; - // fn pragma_summary(self: @TContractState) -> ContractAddress; - // fn oracle_config( - // self: @TContractState, pool_id: felt252, asset: ContractAddress, - // ) -> OracleConfig; - // fn fee_config(self: @TContractState, pool_id: felt252) -> FeeConfig; - // fn debt_caps( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // ) -> u256; - // fn interest_rate_config( - // self: @TContractState, pool_id: felt252, asset: ContractAddress, - // ) -> InterestRateConfig; - // fn liquidation_config( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // ) -> LiquidationConfig; - // fn shutdown_config(self: @TContractState, pool_id: felt252) -> ShutdownConfig; - // fn shutdown_ltv_config( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // ) -> LTVConfig; - // fn shutdown_status( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // ) -> ShutdownStatus; - // fn pairs( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // ) -> Pair; - // fn violation_timestamp_for_pair( - // self: @TContractState, - // pool_id: felt252, - // collateral_asset: ContractAddress, - // debt_asset: ContractAddress, - // ) -> u64; - // fn violation_timestamp_count( - // self: @TContractState, pool_id: felt252, violation_timestamp: u64, - // ) -> u128; - // fn oldest_violation_timestamp(self: @TContractState, pool_id: felt252) -> u64; - // fn next_violation_timestamp( - // self: @TContractState, pool_id: felt252, violation_timestamp: u64, - // ) -> u64; - fn v_token_for_collateral_asset( - self: @TContractState, pool_id: felt252, collateral_asset: ContractAddress, - ) -> ContractAddress; - // fn collateral_asset_for_v_token( -// self: @TContractState, pool_id: felt252, v_token: ContractAddress, -// ) -> ContractAddress; -// fn create_pool( -// ref self: TContractState, -// name: felt252, -// asset_params: Span, -// v_token_params: Span, -// ltv_params: Span, -// interest_rate_configs: Span, -// pragma_oracle_params: Span, -// liquidation_params: Span, -// debt_caps: Span, -// shutdown_params: ShutdownParams, -// fee_params: FeeParams, -// owner: ContractAddress, -// ) -> felt252; -// fn add_asset( -// ref self: TContractState, -// pool_id: felt252, -// asset_params: AssetParams, -// v_token_params: VTokenParams, -// interest_rate_config: InterestRateConfig, -// pragma_oracle_params: PragmaOracleParams, -// ); -// fn set_asset_parameter( -// ref self: TContractState, -// pool_id: felt252, -// asset: ContractAddress, -// parameter: felt252, -// value: u256, -// ); -// fn set_debt_cap( -// ref self: TContractState, -// pool_id: felt252, -// collateral_asset: ContractAddress, -// debt_asset: ContractAddress, -// debt_cap: u256, -// ); -// fn set_interest_rate_parameter( -// ref self: TContractState, -// pool_id: felt252, -// asset: ContractAddress, -// parameter: felt252, -// value: u256, -// ); -// fn set_oracle_parameter( -// ref self: TContractState, -// pool_id: felt252, -// asset: ContractAddress, -// parameter: felt252, -// value: felt252, -// ); -// fn set_liquidation_config( -// ref self: TContractState, -// pool_id: felt252, -// collateral_asset: ContractAddress, -// debt_asset: ContractAddress, -// liquidation_config: LiquidationConfig, -// ); -// fn set_ltv_config( -// ref self: TContractState, -// pool_id: felt252, -// collateral_asset: ContractAddress, -// debt_asset: ContractAddress, -// ltv_config: LTVConfig, -// ); -// fn set_shutdown_config( -// ref self: TContractState, pool_id: felt252, shutdown_config: ShutdownConfig, -// ); -// fn set_shutdown_ltv_config( -// ref self: TContractState, -// pool_id: felt252, -// collateral_asset: ContractAddress, -// debt_asset: ContractAddress, -// shutdown_ltv_config: LTVConfig, -// ); -// fn set_shutdown_mode(ref self: TContractState, pool_id: felt252, shutdown_mode: -// ShutdownMode); -// fn set_pool_owner(ref self: TContractState, pool_id: felt252, owner: ContractAddress); -// fn set_shutdown_mode_agent( -// ref self: TContractState, pool_id: felt252, shutdown_mode_agent: ContractAddress, -// ); -// fn update_shutdown_status( -// ref self: TContractState, -// pool_id: felt252, -// collateral_asset: ContractAddress, -// debt_asset: ContractAddress, -// ) -> ShutdownMode; -// fn set_fee_config(ref self: TContractState, pool_id: felt252, fee_config: FeeConfig); -// fn claim_fees(ref self: TContractState, pool_id: felt252, collateral_asset: ContractAddress); - - // fn migrate_pool( -// ref self: TContractState, -// pool_id: felt252, -// name: felt252, -// v_token_configs: Span<(felt252, felt252, ContractAddress, ContractAddress)>, -// interest_rate_configs: Span<(ContractAddress, InterestRateConfig)>, -// pragma_oracle_configs: Span<(ContractAddress, OracleConfig)>, -// liquidation_configs: Span<(ContractAddress, ContractAddress, LiquidationConfig)>, -// pairs: Span<(ContractAddress, ContractAddress, Pair)>, -// debt_caps: Span<(ContractAddress, ContractAddress, u256)>, -// shutdown_ltv_configs: Span<(ContractAddress, ContractAddress, LTVConfig)>, -// shutdown_config: ShutdownConfig, -// fee_config: FeeConfig, -// owner: ContractAddress, -// ); -// fn set_migrator(ref self: TContractState, migrator: ContractAddress); - - // fn set_extension_utils_class_hash(ref self: TContractState, extension: felt252); -// // Upgrade -// fn upgrade_name(self: @TContractState) -> felt252; -// fn upgrade(ref self: TContractState, new_implementation: ClassHash); -} - -#[starknet::interface] -pub trait IFlashloanReceiver { - fn on_flash_loan( - ref self: TContractState, - sender: ContractAddress, - asset: ContractAddress, - amount: u256, - data: Span, - ); -} diff --git a/packages/vault_allocator/src/integration_interfaces/vesu_v1.cairo b/packages/vault_allocator/src/integration_interfaces/vesu_v1.cairo new file mode 100644 index 00000000..ffe96a60 --- /dev/null +++ b/packages/vault_allocator/src/integration_interfaces/vesu_v1.cairo @@ -0,0 +1,23 @@ +use starknet::ContractAddress; + +#[derive(PartialEq, Copy, Drop, Serde)] +pub struct Position { + collateral_shares: u256, + nominal_debt: u256, +} + +#[starknet::interface] +pub trait IV1Token { + fn pool_id(self: @TContractState) -> felt252; +} + +#[starknet::interface] +pub trait ISingletonV2 { + fn position( + ref self: TContractState, + pool_id: felt252, + collateral_asset: ContractAddress, + debt_asset: ContractAddress, + user: ContractAddress, + ) -> (Position, u256, u256); +} diff --git a/packages/vault_allocator/src/integration_interfaces/vesu_v2.cairo b/packages/vault_allocator/src/integration_interfaces/vesu_v2.cairo new file mode 100644 index 00000000..2ce1275c --- /dev/null +++ b/packages/vault_allocator/src/integration_interfaces/vesu_v2.cairo @@ -0,0 +1,28 @@ +use starknet::ContractAddress; +use vault_allocator::integration_interfaces::vesu_v1::Position; + +#[starknet::interface] +pub trait IV2Token { + fn pool_contract(self: @TContractState) -> ContractAddress; +} + +#[starknet::interface] +pub trait IPool { + fn position( + self: @TContractState, + collateral_asset: ContractAddress, + debt_asset: ContractAddress, + user: ContractAddress, + ) -> (Position, u256, u256); +} + + +#[starknet::interface] +pub trait IOracle { + fn price(self: @TContractState, asset: ContractAddress) -> AssetPrice; +} +#[derive(PartialEq, Copy, Drop, Serde, Default)] +pub struct AssetPrice { + pub value: u256, + pub is_valid: bool, +} diff --git a/packages/vault_allocator/src/lib.cairo b/packages/vault_allocator/src/lib.cairo index 00dfd9ae..5701d8b5 100644 --- a/packages/vault_allocator/src/lib.cairo +++ b/packages/vault_allocator/src/lib.cairo @@ -17,7 +17,8 @@ pub mod manager { pub mod integration_interfaces { pub mod avnu; pub mod pragma; - pub mod vesu; + pub mod vesu_v1; + pub mod vesu_v2; } pub mod periphery { @@ -26,6 +27,11 @@ pub mod periphery { pub mod interface; pub mod price_router; } + pub mod price_router_vesu { + pub mod errors; + pub mod interface; + pub mod price_router_vesu; + } } pub mod middlewares { @@ -41,6 +47,7 @@ pub mod decoders_and_sanitizers { pub mod decoder_custom_types; pub mod interface; pub mod simple_decoder_and_sanitizer; + pub mod vesu_v2_specific_decoder_and_sanitizer; pub mod avnu_exchange_decoder_and_sanitizer { pub mod avnu_exchange_decoder_and_sanitizer; pub mod interface; @@ -53,7 +60,10 @@ pub mod decoders_and_sanitizers { pub mod interface; pub mod vesu_decoder_and_sanitizer; } - + pub mod starknet_vault_kit_decoder_and_sanitizer { + pub mod interface; + pub mod starknet_vault_kit_decoder_and_sanitizer; + } pub mod vesu_v2_decoder_and_sanitizer { pub mod interface; pub mod vesu_v2_decoder_and_sanitizer; @@ -69,13 +79,12 @@ pub mod mocks { pub mod counter; pub mod erc20; pub mod erc4626; - pub mod flashloan; + pub mod vault; } #[cfg(test)] pub mod test { // pub mod creator; - pub mod register; pub mod utils; pub mod units { pub mod manager; @@ -83,12 +92,23 @@ pub mod test { } pub mod integrations { pub mod avnu; + pub mod vault_bring_liquidity; pub mod vesu_v1; } - pub mod scenarios { - pub mod leveraged_loop_staked_ether; pub mod stable_carry_loop; } } + +pub mod merkle_tree { + pub mod base; + pub mod registery; + pub mod integrations { + pub mod avnu; + pub mod erc4626; + pub mod starknet_vault_kit_strategies; + pub mod vesu_v1; + pub mod vesu_v2; + } +} diff --git a/packages/vault_allocator/src/manager/errors.cairo b/packages/vault_allocator/src/manager/errors.cairo index c232c949..8eb9aa4c 100644 --- a/packages/vault_allocator/src/manager/errors.cairo +++ b/packages/vault_allocator/src/manager/errors.cairo @@ -18,16 +18,4 @@ pub mod Errors { pub fn not_vault_allocator() { panic!("Not vault allocator"); } - - pub fn flash_loan_not_executed() { - panic!("Flash loan not executed"); - } - - pub fn not_vesu_singleton() { - panic!("Not vesu singleton"); - } - - pub fn bad_flash_loan_intent_hash() { - panic!("Bad flash loan intent hash"); - } } diff --git a/packages/vault_allocator/src/manager/interface.cairo b/packages/vault_allocator/src/manager/interface.cairo index 21e59cda..e5546961 100644 --- a/packages/vault_allocator/src/manager/interface.cairo +++ b/packages/vault_allocator/src/manager/interface.cairo @@ -6,7 +6,6 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IManager { - fn vesu_singleton(self: @T) -> ContractAddress; fn vault_allocator(self: @T) -> ContractAddress; fn set_manage_root(ref self: T, target: ContractAddress, root: felt252); fn manage_root(self: @T, target: ContractAddress) -> felt252; @@ -20,14 +19,5 @@ pub trait IManager { selectors: Span, calldatas: Span>, ); - - fn flash_loan( - ref self: T, - recipient: ContractAddress, - asset: ContractAddress, - amount: u256, - is_legacy: bool, - data: Span, - ); } diff --git a/packages/vault_allocator/src/manager/manager.cairo b/packages/vault_allocator/src/manager/manager.cairo index 1deeb830..3cea20b3 100644 --- a/packages/vault_allocator/src/manager/manager.cairo +++ b/packages/vault_allocator/src/manager/manager.cairo @@ -8,10 +8,8 @@ pub mod Manager { pub const OWNER_ROLE: felt252 = selector!("OWNER_ROLE"); pub const PAUSER_ROLE: felt252 = selector!("PAUSER_ROLE"); use core::hash::HashStateTrait; - use core::num::traits::Zero; use core::pedersen::PedersenTrait; use openzeppelin::access::accesscontrol::AccessControlComponent; - use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use openzeppelin::interfaces::upgrades::IUpgradeable; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::merkle_tree::merkle_proof; @@ -22,14 +20,9 @@ pub mod Manager { Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess, }; - use starknet::{ContractAddress, get_caller_address, get_contract_address}; - use vault_allocator::integration_interfaces::vesu::{ - IFlashloanReceiver, ISingletonV2Dispatcher, ISingletonV2DispatcherTrait, - }; + use starknet::{ContractAddress, get_caller_address}; use vault_allocator::manager::errors::Errors; - use vault_allocator::manager::interface::{ - IManager, IManagerDispatcher, IManagerDispatcherTrait, - }; + use vault_allocator::manager::interface::IManager; use vault_allocator::vault_allocator::interface::{ IVaultAllocatorDispatcher, IVaultAllocatorDispatcherTrait, }; @@ -50,28 +43,26 @@ pub mod Manager { #[substorage(v0)] pausable: PausableComponent::Storage, vault_allocator: IVaultAllocatorDispatcher, - vesu_singleton: ISingletonV2Dispatcher, manage_root: Map, - flash_loan_intent_hash: felt252, - performing_flash_loan: bool, } #[event] #[derive(Drop, starknet::Event)] pub enum Event { + #[flat] SRC5Event: SRC5Component::Event, + #[flat] AccessControlEvent: AccessControlComponent::Event, + #[flat] UpgradeableEvent: UpgradeableComponent::Event, + #[flat] PausableEvent: PausableComponent::Event, } #[constructor] fn constructor( - ref self: ContractState, - owner: ContractAddress, - vault_allocator: ContractAddress, - vesu_singleton: ContractAddress, + ref self: ContractState, owner: ContractAddress, vault_allocator: ContractAddress, ) { self.access_control.initializer(); self.access_control.set_role_admin(OWNER_ROLE, OWNER_ROLE); @@ -79,7 +70,6 @@ pub mod Manager { self.access_control._grant_role(OWNER_ROLE, owner); self.access_control._grant_role(PAUSER_ROLE, owner); self.vault_allocator.write(IVaultAllocatorDispatcher { contract_address: vault_allocator }); - self.vesu_singleton.write(ISingletonV2Dispatcher { contract_address: vesu_singleton }); } @@ -101,67 +91,9 @@ pub mod Manager { } } - #[abi(embed_v0)] - impl ManagerFlashloanReceiverImpl of IFlashloanReceiver { - fn on_flash_loan( - ref self: ContractState, - sender: ContractAddress, - asset: ContractAddress, - amount: u256, - data: Span, - ) { - let vesu_singleton = self.vesu_singleton.read().contract_address; - if (get_caller_address() != vesu_singleton) { - Errors::not_vesu_singleton(); - } - let intent_hash = self._get_flash_loan_intent_hash_from_span(data); - if (intent_hash != self.flash_loan_intent_hash.read()) { - Errors::bad_flash_loan_intent_hash(); - } - self.flash_loan_intent_hash.write(0); - - let vault_allocator = self.vault_allocator.read(); - let asset_dispatcher = ERC20ABIDispatcher { contract_address: asset }; - - asset_dispatcher.transfer(vault_allocator.contract_address, amount); - - let mut data = data.clone(); - let (proofs, decoder_and_sanitizers, targets, selectors, calldatas) = Serde::< - ( - Span>, - Span, - Span, - Span, - Span>, - ), - >::deserialize(ref data) - .unwrap(); - - let this = get_contract_address(); - IManagerDispatcher { contract_address: this } - .manage_vault_with_merkle_verification( - proofs, decoder_and_sanitizers, targets, selectors, calldatas, - ); - - let mut calldata = ArrayTrait::new(); - this.serialize(ref calldata); - amount.serialize(ref calldata); - - vault_allocator - .manage( - Call { to: asset, selector: selector!("transfer"), calldata: calldata.span() }, - ); - asset_dispatcher.approve(vesu_singleton, amount); - } - } - #[abi(embed_v0)] impl ManagerImpl of IManager { - fn vesu_singleton(self: @ContractState) -> ContractAddress { - self.vesu_singleton.read().contract_address - } - fn vault_allocator(self: @ContractState) -> ContractAddress { self.vault_allocator.read().contract_address } @@ -220,26 +152,6 @@ pub mod Manager { .manage(Call { to: target, selector: selector, calldata: calldata }); } } - - fn flash_loan( - ref self: ContractState, - recipient: ContractAddress, - asset: ContractAddress, - amount: u256, - is_legacy: bool, - data: Span, - ) { - if (get_caller_address() != self.vault_allocator.read().contract_address) { - Errors::not_vault_allocator(); - } - self.flash_loan_intent_hash.write(self._get_flash_loan_intent_hash_from_span(data)); - self.performing_flash_loan.write(true); - self.vesu_singleton.read().flash_loan(recipient, asset, amount, is_legacy, data); - self.performing_flash_loan.write(false); - if (self.flash_loan_intent_hash.read().is_non_zero()) { - Errors::flash_loan_not_executed(); - } - } } @@ -305,18 +217,5 @@ pub mod Manager { let leaf_hash = state.finalize(); merkle_proof::verify_pedersen(proof, root, leaf_hash) } - - fn _get_flash_loan_intent_hash_from_span( - self: @ContractState, data: Span, - ) -> felt252 { - let mut serialized_struct: Array = ArrayTrait::new(); - data.serialize(ref serialized_struct); - let first_element = serialized_struct.pop_front().unwrap(); - let mut state = PedersenTrait::new(first_element); - while let Some(value) = serialized_struct.pop_front() { - state = state.update(value); - } - state.finalize() - } } } diff --git a/packages/vault_allocator/src/merkle_tree/base.cairo b/packages/vault_allocator/src/merkle_tree/base.cairo new file mode 100644 index 00000000..7e0d681f --- /dev/null +++ b/packages/vault_allocator/src/merkle_tree/base.cairo @@ -0,0 +1,319 @@ +use core::hash::HashStateTrait; +use core::num::traits::Zero; +use core::pedersen::PedersenTrait; +use openzeppelin::interfaces::erc4626::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; +use openzeppelin::merkle_tree::hashes::PedersenCHasher; +use starknet::ContractAddress; +use starknet::syscalls::call_contract_syscall; + + +#[derive(PartialEq, Drop, Serde, Debug, Clone)] +pub struct ManageLeaf { + pub decoder_and_sanitizer: ContractAddress, + pub target: ContractAddress, + pub selector: felt252, + pub argument_addresses: Span, + pub description: ByteArray, +} + + +pub fn get_leaf_hash(leaf: ManageLeaf) -> felt252 { + let mut serialized_struct: Array = ArrayTrait::new(); + leaf.decoder_and_sanitizer.serialize(ref serialized_struct); + leaf.target.serialize(ref serialized_struct); + leaf.selector.serialize(ref serialized_struct); + leaf.argument_addresses.serialize(ref serialized_struct); + let first_element = serialized_struct.pop_front().unwrap(); + let mut state = PedersenTrait::new(first_element); + while let Some(value) = serialized_struct.pop_front() { + state = state.update(value); + } + state.finalize() +} + +pub fn generate_merkle_tree(manage_leafs: Span) -> Array> { + let mut first_layer = ArrayTrait::new(); + let leafs_length = manage_leafs.len(); + for i in 0..leafs_length { + first_layer.append(get_leaf_hash(manage_leafs.at(i).clone())); + } + let mut leafs = ArrayTrait::new(); + leafs.append(first_layer); + _build_tree(leafs) +} + +pub fn _build_tree(merkle_tree_in: Array>) -> Array> { + let merkle_tree_in_length = merkle_tree_in.len(); + + let mut current_layer_index = merkle_tree_in_length - 1; + let current_layer_length = merkle_tree_in[current_layer_index].len(); + let mut next_layer_length = 0; + if (current_layer_length % 2 != 0) { + next_layer_length = (current_layer_length + 1) / 2; + } else { + next_layer_length = current_layer_length / 2; + } + let mut current_layer = ArrayTrait::new(); + let mut count = 0; + let mut i = 0; + while i < current_layer_length { + current_layer + .append( + PedersenCHasher::commutative_hash( + *merkle_tree_in[current_layer_index].at(i), + *merkle_tree_in[current_layer_index].at(i + 1), + ), + ); + count += 1; + i += 2; + } + + let mut merkle_tree_out = merkle_tree_in.clone(); + merkle_tree_out.append(current_layer); + + if (next_layer_length > 1) { + _build_tree(merkle_tree_out) + } else { + merkle_tree_out + } +} + +fn _next_power_of_two(x: u256) -> u256 { + let mut power = 1_u256; + while power < x { + power = power * 2_u256; + } + power +} + + +pub fn _pad_leafs_to_power_of_two(ref leafs: Array, ref leaf_index: u256) { + let target_len = if leaf_index < 4_u256 { + 4_u256 + } else { + _next_power_of_two(leaf_index) + }; + let padding_needed = target_len - leaf_index; + + let mut i: u256 = 0_u256; + while i < padding_needed { + leafs + .append( + ManageLeaf { + decoder_and_sanitizer: Zero::zero(), + target: Zero::zero(), + selector: Zero::zero(), + argument_addresses: ArrayTrait::new().span(), + description: "", + }, + ); + leaf_index += 1_u256; + i += 1_u256; + } +} + + +pub fn _get_proofs_using_tree( + leafs: Array, tree: Array>, +) -> Array> { + let mut proofs = ArrayTrait::new(); + for i in 0..leafs.len() { + let leaf = leafs.at(i); + let mut serialized_struct: Array = ArrayTrait::new(); + leaf.decoder_and_sanitizer.serialize(ref serialized_struct); + leaf.target.serialize(ref serialized_struct); + leaf.selector.serialize(ref serialized_struct); + leaf.argument_addresses.serialize(ref serialized_struct); + let first_element = serialized_struct.pop_front().unwrap(); + let mut state = PedersenTrait::new(first_element); + + while let Some(value) = serialized_struct.pop_front() { + state = state.update(value); + } + let leaf_hash = state.finalize(); + let proof = _generate_proof(leaf_hash, tree.clone()); + proofs.append(proof); + } + proofs +} + +pub fn _generate_proof(mut leaf: felt252, tree: Array>) -> Span { + let tree_length = tree.len(); + let mut proof = ArrayTrait::new(); + for i in 0..tree_length - 1 { + let tree_current_layer = tree.at(i); + let tree_current_layer_length = tree_current_layer.len(); + for j in 0..tree_current_layer_length { + if leaf == *tree_current_layer.at(j) { + let element_to_append = if j % 2 == 0 { + *tree_current_layer.at(j + 1) + } else { + *tree_current_layer.at(j - 1) + }; + leaf = PedersenCHasher::commutative_hash(leaf, element_to_append); + proof.append(element_to_append); + break; + } else { + assert(j != tree_current_layer_length - 1, 'leaf not found in tree'); + } + } + } + proof.span() +} + + +pub fn _add_vault_allocator_leafs( + ref leafs: Array, + ref leaf_index: u256, + vault_allocator: ContractAddress, + decoder_and_sanitizer: ContractAddress, + vault: ContractAddress, +) { + let underlying_asset = ERC4626ABIDispatcher { contract_address: vault }.asset(); + + // Approvals + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: underlying_asset, + selector: selector!("approve"), + argument_addresses: array![vault.into()].span(), + description: "Approve" + + " " + + get_symbol(vault) + + " " + + "to spend" + + " " + + get_symbol(underlying_asset), + }, + ); + leaf_index += 1; + + // Bring liquidity + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: vault, + selector: selector!("bring_liquidity"), + argument_addresses: array![].span(), + description: "Bring liquidity" + " " + get_symbol(vault), + }, + ); + leaf_index += 1; +} + + +pub fn _add_vesu_flash_loan_leafs( + ref leafs: Array, + ref leaf_index: u256, + vault: ContractAddress, + decoder_and_sanitizer: ContractAddress, + manager: ContractAddress, + asset: ContractAddress, + is_legacy: bool, +) { + let mut argument_addresses = ArrayTrait::new(); + manager.serialize(ref argument_addresses); + asset.serialize(ref argument_addresses); + is_legacy.serialize(ref argument_addresses); + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: manager, + selector: selector!("flash_loan"), + argument_addresses: argument_addresses.span(), + description: "Flash loan" + " " + get_symbol(asset), + }, + ); + leaf_index += 1; +} + + +pub fn _generate_trading_pairs_from_unique_assets( + ref unique_assets: Array, +) -> Array<(ContractAddress, ContractAddress)> { + let mut trading_pairs: Array<(ContractAddress, ContractAddress)> = ArrayTrait::new(); + loop { + match unique_assets.pop_front() { + Option::Some(asset) => { + for other_asset in @unique_assets { + trading_pairs.append((asset, *other_asset)) + } + }, + Option::None(_) => { break; }, + }; + } + trading_pairs +} + + +pub fn _append_unique_assets( + ref array_of_unique_assets: Array, + assets: Span, + consider_underlying: bool, +) { + for asset in assets { + let mut asset_to_consider = *asset; + if (consider_underlying) { + asset_to_consider = ERC4626ABIDispatcher { contract_address: *asset }.asset(); + } + if (!_contains_address(array_of_unique_assets.span(), asset_to_consider)) { + array_of_unique_assets.append(asset_to_consider); + } + } +} + + +pub fn _contains_address(span: Span, addr: ContractAddress) -> bool { + let mut i = 0; + while i < span.len() { + if *span.at(i) == addr { + return true; + } + i += 1; + } + false +} + + +pub fn get_symbol(contract_address: ContractAddress) -> ByteArray { + let ret_data = call_contract_syscall(contract_address, selector!("symbol"), array![].span()); + match ret_data { + Ok(res) => { + let res_len: u32 = res.len(); + if (res_len == 1) { + let symbol_felt = *res.at(0); + let mut symbol_byte_array: ByteArray = ""; + symbol_byte_array.append_word(symbol_felt, bytes_in_felt(symbol_felt)); + symbol_byte_array + } else { + let mut res_span = res; + Serde::::deserialize(ref res_span).unwrap() + } + }, + Err(revert_reason) => { panic!("revert_reason: {:?}", revert_reason); }, + } +} + + +fn bytes_in_felt(word: felt252) -> usize { + if word == 0 { + return 0; + } + let x: u256 = word.try_into().unwrap(); + + let mut p: u256 = 1_u256; + let mut bytes: usize = 0; + + while p <= x && bytes < 31 { + p = p * 256_u256; + bytes += 1; + } + + bytes +} diff --git a/packages/vault_allocator/src/merkle_tree/integrations/avnu.cairo b/packages/vault_allocator/src/merkle_tree/integrations/avnu.cairo new file mode 100644 index 00000000..f0182752 --- /dev/null +++ b/packages/vault_allocator/src/merkle_tree/integrations/avnu.cairo @@ -0,0 +1,72 @@ +use starknet::ContractAddress; +use vault_allocator::merkle_tree::base::{ManageLeaf, _contains_address, get_symbol}; + + +#[derive(PartialEq, Drop, Serde, Debug, Clone, starknet::Store)] +pub struct AvnuConfig { + pub sell_token: ContractAddress, + pub buy_token: ContractAddress, +} + + +pub fn _add_avnu_leafs( + ref leafs: Array, + ref leaf_index: u256, + vault_allocator: ContractAddress, + decoder_and_sanitizer: ContractAddress, + router: ContractAddress, + avnu_configs: Span, +) { + let mut seen_sells: Array = ArrayTrait::new(); + + for i in 0..avnu_configs.len() { + let avnu_config_elem = avnu_configs.at(i); + let sell_token_address = *avnu_config_elem.sell_token; + let buy_token_address = *avnu_config_elem.buy_token; + + if !_contains_address(seen_sells.span(), sell_token_address) { + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: sell_token_address, + selector: selector!("approve"), + argument_addresses: array![router.into()].span(), + description: "Approve" + + " " + + "avnu_router" + + " " + + "to spend" + + " " + + get_symbol(sell_token_address), + }, + ); + leaf_index += 1; + seen_sells.append(sell_token_address); + } + + // swap leaf à chaque paire + let mut argument_addresses = ArrayTrait::new(); + sell_token_address.serialize(ref argument_addresses); + buy_token_address.serialize(ref argument_addresses); + vault_allocator.serialize(ref argument_addresses); + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: router, + selector: selector!("multi_route_swap"), + argument_addresses: argument_addresses.span(), + description: "Multi route swap" + + " " + + get_symbol(sell_token_address) + + " " + + "for" + + " " + + get_symbol(buy_token_address), + }, + ); + leaf_index += 1; + } +} diff --git a/packages/vault_allocator/src/merkle_tree/integrations/erc4626.cairo b/packages/vault_allocator/src/merkle_tree/integrations/erc4626.cairo new file mode 100644 index 00000000..48917eb4 --- /dev/null +++ b/packages/vault_allocator/src/merkle_tree/integrations/erc4626.cairo @@ -0,0 +1,114 @@ +use openzeppelin::interfaces::erc4626::{IERC4626Dispatcher, IERC4626DispatcherTrait}; +use starknet::ContractAddress; +use vault_allocator::merkle_tree::base::{ManageLeaf, get_symbol}; + + +pub fn _add_erc4626_leafs( + ref leafs: Array, + ref leaf_index: u256, + vault_allocator: ContractAddress, + decoder_and_sanitizer: ContractAddress, + erc4626: ContractAddress, +) { + let erc4626_erc4646_disp = IERC4626Dispatcher { contract_address: erc4626 }; + let asset = erc4626_erc4646_disp.asset(); + + // Approvals + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: asset, + selector: selector!("approve"), + argument_addresses: array![erc4626.into()].span(), + description: "Approve" + + " " + + get_symbol(erc4626) + + " " + + "to spend" + + " " + + get_symbol(asset), + }, + ); + leaf_index += 1; + + // Deposits + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: erc4626, + selector: selector!("deposit"), + argument_addresses: array![vault_allocator.into()].span(), + description: "Deposit" + + " " + + get_symbol(asset) + + " " + + "for" + + " " + + get_symbol(erc4626), + }, + ); + leaf_index += 1; + + // Withdrawals + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: erc4626, + selector: selector!("withdraw"), + argument_addresses: array![vault_allocator.into(), vault_allocator.into()].span(), + description: "Withdraw" + + " " + + get_symbol(asset) + + " " + + "from" + + " " + + get_symbol(erc4626), + }, + ); + leaf_index += 1; + + // Minting + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: erc4626, + selector: selector!("mint"), + argument_addresses: array![vault_allocator.into()].span(), + description: "Mint" + + " " + + get_symbol(erc4626) + + " " + + "from" + + " " + + get_symbol(asset), + }, + ); + leaf_index += 1; + + // Redeeming + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: erc4626, + selector: selector!("redeem"), + argument_addresses: array![vault_allocator.into(), vault_allocator.into()].span(), + description: "Redeem" + + " " + + get_symbol(erc4626) + + " " + + "for" + + " " + + get_symbol(asset), + }, + ); + leaf_index += 1; +} diff --git a/packages/vault_allocator/src/merkle_tree/integrations/starknet_vault_kit_strategies.cairo b/packages/vault_allocator/src/merkle_tree/integrations/starknet_vault_kit_strategies.cairo new file mode 100644 index 00000000..4b3fa276 --- /dev/null +++ b/packages/vault_allocator/src/merkle_tree/integrations/starknet_vault_kit_strategies.cairo @@ -0,0 +1,102 @@ +use openzeppelin::interfaces::erc4626::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; +use starknet::ContractAddress; +use vault_allocator::merkle_tree::base::{ManageLeaf, get_symbol}; + + +pub fn _add_starknet_vault_kit_strategies( + ref leafs: Array, + ref leaf_index: u256, + vault_allocator: ContractAddress, + decoder_and_sanitizer: ContractAddress, + starknet_vault_kit_strategy: ContractAddress, +) { + let starknet_vault_kit_strategy_erc4646_disp = ERC4626ABIDispatcher { + contract_address: starknet_vault_kit_strategy, + }; + let asset = starknet_vault_kit_strategy_erc4646_disp.asset(); + + // Approvals + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: asset, + selector: selector!("approve"), + argument_addresses: array![starknet_vault_kit_strategy.into()].span(), + description: "Approve" + + " " + + get_symbol(starknet_vault_kit_strategy) + + " " + + "to spend" + + " " + + get_symbol(asset), + }, + ); + leaf_index += 1; + + // Deposits + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: starknet_vault_kit_strategy, + selector: selector!("deposit"), + argument_addresses: array![vault_allocator.into()].span(), + description: "Deposit" + + " " + + get_symbol(asset) + + " " + + "for" + + " " + + get_symbol(starknet_vault_kit_strategy), + }, + ); + leaf_index += 1; + + // Minting + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: starknet_vault_kit_strategy, + selector: selector!("mint"), + argument_addresses: array![vault_allocator.into()].span(), + description: "Mint" + + " " + + get_symbol(starknet_vault_kit_strategy) + + " " + + "from" + + " " + + get_symbol(starknet_vault_kit_strategy), + }, + ); + leaf_index += 1; + + // Request Redeen + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: starknet_vault_kit_strategy, + selector: selector!("request_redeem"), + argument_addresses: array![vault_allocator.into(), vault_allocator.into()].span(), + description: "Request Redeem" + " " + get_symbol(starknet_vault_kit_strategy), + }, + ); + leaf_index += 1; + + // Claim Redeem + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: starknet_vault_kit_strategy, + selector: selector!("claim_redeem"), + argument_addresses: array![].span(), + description: "Claim Redeem" + " " + get_symbol(starknet_vault_kit_strategy), + }, + ); + leaf_index += 1; +} diff --git a/packages/vault_allocator/src/merkle_tree/integrations/vesu_v1.cairo b/packages/vault_allocator/src/merkle_tree/integrations/vesu_v1.cairo new file mode 100644 index 00000000..01c1d1d6 --- /dev/null +++ b/packages/vault_allocator/src/merkle_tree/integrations/vesu_v1.cairo @@ -0,0 +1,91 @@ +use core::to_byte_array::FormatAsByteArray; +use starknet::ContractAddress; +use vault_allocator::merkle_tree::base::{ManageLeaf, get_symbol}; +use vault_allocator::merkle_tree::registery::VESU_SINGLETON; + +#[derive(PartialEq, Drop, Serde, Debug, Clone)] +pub struct VesuV1Config { + pub pool_id: felt252, + pub collateral_asset: ContractAddress, + pub debt_assets: Span, +} + +pub fn _add_vesu_v1_leafs( + ref leafs: Array, + ref leaf_index: u256, + vault_allocator: ContractAddress, + decoder_and_sanitizer: ContractAddress, + vesu_v1_configs: Span, +) { + for i in 0..vesu_v1_configs.len() { + let vesu_v1_config_elem = vesu_v1_configs.at(i); + let pool_id = *vesu_v1_config_elem.pool_id; + let collateral_asset = *vesu_v1_config_elem.collateral_asset; + let debt_assets = *vesu_v1_config_elem.debt_assets; + let mut pool_id_str: ByteArray = FormatAsByteArray::format_as_byte_array(@pool_id, 16); + + // APPROVAL of collateral asset to the singleton + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: collateral_asset, + selector: selector!("approve"), + argument_addresses: array![VESU_SINGLETON().into()].span(), + description: "Approve" + + " " + + "singleton" + + "_" + + pool_id_str.clone() + + " " + + "to spend" + + " " + + get_symbol(collateral_asset), + }, + ); + leaf_index += 1; + + for j in 0..debt_assets.len() { + let debt_asset_elem = *debt_assets.at(j); + + // MODIFY POSITION + let mut argument_addresses_modify_position = ArrayTrait::new(); + + // pool_id + pool_id.serialize(ref argument_addresses_modify_position); + + // collateral_asset + collateral_asset.serialize(ref argument_addresses_modify_position); + + // debt_asset + debt_asset_elem.serialize(ref argument_addresses_modify_position); + + // user + vault_allocator.serialize(ref argument_addresses_modify_position); + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: VESU_SINGLETON(), + selector: selector!("modify_position"), + argument_addresses: argument_addresses_modify_position.span(), + description: "Modify position" + + " " + + "extension_pid" + + "_" + + pool_id_str.clone() + + " " + + "with collateral" + + " " + + get_symbol(collateral_asset) + + " " + + "and debt" + + " " + + get_symbol(debt_asset_elem), + }, + ); + leaf_index += 1; + } + } +} diff --git a/packages/vault_allocator/src/merkle_tree/integrations/vesu_v2.cairo b/packages/vault_allocator/src/merkle_tree/integrations/vesu_v2.cairo new file mode 100644 index 00000000..1335e298 --- /dev/null +++ b/packages/vault_allocator/src/merkle_tree/integrations/vesu_v2.cairo @@ -0,0 +1,95 @@ +use core::to_byte_array::FormatAsByteArray; +use starknet::ContractAddress; +use vault_allocator::merkle_tree::base::{ManageLeaf, get_symbol}; + + +#[derive(PartialEq, Drop, Serde, Debug, Clone)] +pub struct VesuV2Config { + pub pool_contract: ContractAddress, + pub collateral_asset: ContractAddress, + pub debt_assets: Span, +} + + +pub fn _add_vesu_v2_leafs( + ref leafs: Array, + ref leaf_index: u256, + vault_allocator: ContractAddress, + decoder_and_sanitizer: ContractAddress, + vesu_v2_configs: Span, +) { + for i in 0..vesu_v2_configs.len() { + let vesu_v2_config_elem = vesu_v2_configs.at(i); + let pool_contract = *vesu_v2_config_elem.pool_contract; + let collateral_asset = *vesu_v2_config_elem.collateral_asset; + let debt_assets = *vesu_v2_config_elem.debt_assets; + let pool_contract_felt: felt252 = pool_contract.into(); + let mut pool_contract_str: ByteArray = FormatAsByteArray::format_as_byte_array( + @pool_contract_felt, 16, + ); + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: collateral_asset, + selector: selector!("approve"), + argument_addresses: array![pool_contract.into()].span(), + description: "Approve" + + " " + + "pool contract" + + "_" + + pool_contract_str.clone() + + " " + + "to spend" + + " " + + get_symbol(collateral_asset), + }, + ); + leaf_index += 1; + + // debt mode + let debt_assets_len = debt_assets.len(); + for j in 0..debt_assets_len { + // APPROVAL of collateral asset to the pool contract + + let debt_asset = *debt_assets.at(j); + + // MODIFY POSITION + let mut argument_addresses_modify_position = ArrayTrait::new(); + + // collateral_asset + collateral_asset.serialize(ref argument_addresses_modify_position); + + // debt_asset + debt_asset.serialize(ref argument_addresses_modify_position); + + // user + vault_allocator.serialize(ref argument_addresses_modify_position); + + leafs + .append( + ManageLeaf { + decoder_and_sanitizer, + target: pool_contract, + selector: selector!("modify_position"), + argument_addresses: argument_addresses_modify_position.span(), + description: "Modify position" + + " " + + "extension_pid" + + "_" + + pool_contract_str.clone() + + " " + + "with collateral" + + " " + + get_symbol(collateral_asset) + + " " + + "and debt" + + " " + + get_symbol(debt_asset), + }, + ); + leaf_index += 1; + } + } +} diff --git a/packages/vault_allocator/src/test/register.cairo b/packages/vault_allocator/src/merkle_tree/registery.cairo similarity index 87% rename from packages/vault_allocator/src/test/register.cairo rename to packages/vault_allocator/src/merkle_tree/registery.cairo index 5a8cf769..2e385a05 100644 --- a/packages/vault_allocator/src/test/register.cairo +++ b/packages/vault_allocator/src/merkle_tree/registery.cairo @@ -43,6 +43,15 @@ pub fn VESU_SINGLETON() -> ContractAddress { pub const GENESIS_POOL_ID: felt252 = 2198503327643286920898110335698706244522220458610657370981979460625005526824; +pub fn VESU_GENESIS_POOL_V_TOKEN_WSTETH() -> ContractAddress { + 0x7cb1a46709214b94f51655be696a4ff6f9bdbbb6edb19418b6a55d190536048.try_into().unwrap() +} + +pub fn VESU_GENESIS_POOL_V_TOKEN_USDT() -> ContractAddress { + 0x40e480d202b47eb9335c31fc328ecda216231425dae74f87d1a97e6e7901dce.try_into().unwrap() +} + + // PRAGMA pub fn PRAGMA() -> ContractAddress { diff --git a/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo b/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo index 6272c225..13ee69ab 100644 --- a/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo +++ b/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo @@ -4,20 +4,20 @@ #[starknet::contract] pub mod AvnuMiddleware { - const BPS_SCALE: u256 = 10_000_u256; + const BPS_SCALE: u16 = 10_000; use core::num::traits::Zero; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::interfaces::upgrades::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; use openzeppelin::utils::math; - use starknet::storage::{ - Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, - StoragePointerWriteAccess, - }; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; use vault_allocator::decoders_and_sanitizers::decoder_custom_types::Route; use vault_allocator::integration_interfaces::avnu::{ IAvnuExchangeDispatcher, IAvnuExchangeDispatcherTrait, }; + use vault_allocator::merkle_tree::registery::AVNU_ROUTER; use vault_allocator::middlewares::avnu_middleware::errors::Errors; use vault_allocator::middlewares::avnu_middleware::interface::IAvnuMiddleware; use vault_allocator::periphery::price_router::interface::{ @@ -26,27 +26,27 @@ pub mod AvnuMiddleware { component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); #[abi(embed_v0)] impl OwnableImpl = OwnableComponent::OwnableImpl; - impl InternalImpl = OwnableComponent::InternalImpl; - - #[derive(Copy, Drop, Serde, starknet::Store)] - struct Config { - period: u64, - allowed_calls_per_period: u64, - } + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; #[storage] struct Storage { #[substorage(v0)] ownable: OwnableComponent::Storage, - avnu_router: IAvnuExchangeDispatcher, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, price_router: IPriceRouterDispatcher, - slippage_tolerance_bps: u256, - config: Option, - call_count: Map<(ContractAddress, u64), u64>, + vault_allocator: ContractAddress, + slippage: u16, + period: u64, + allowed_calls_per_period: u64, + current_window_id: u64, + window_call_count: u64, } #[event] @@ -54,17 +54,14 @@ pub mod AvnuMiddleware { pub enum Event { #[flat] OwnableEvent: OwnableComponent::Event, - SlippageUpdated: SlippageUpdated, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, ConfigUpdated: ConfigUpdated, } - #[derive(Drop, starknet::Event)] - pub struct SlippageUpdated { - pub slippage: u256, - } - #[derive(Drop, starknet::Event)] struct ConfigUpdated { + pub slippage: u16, period: u64, allowed_calls_per_period: u64, } @@ -74,52 +71,52 @@ pub mod AvnuMiddleware { fn constructor( ref self: ContractState, owner: ContractAddress, - avnu_router: ContractAddress, + vault_allocator: ContractAddress, price_router: ContractAddress, - initial_slippage_bps: u256, + slippage: u16, + period: u64, + allowed_calls_per_period: u64, ) { self.ownable.initializer(owner); - self.avnu_router.write(IAvnuExchangeDispatcher { contract_address: avnu_router }); self.price_router.write(IPriceRouterDispatcher { contract_address: price_router }); - self.slippage_tolerance_bps.write(initial_slippage_bps); + self.vault_allocator.write(vault_allocator); + self._set_config(slippage, period, allowed_calls_per_period) } + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } #[abi(embed_v0)] impl AvnuMiddlewareViewImpl of IAvnuMiddleware { fn avnu_router(self: @ContractState) -> ContractAddress { - self.avnu_router.read().contract_address + AVNU_ROUTER() } fn price_router(self: @ContractState) -> ContractAddress { self.price_router.read().contract_address } - fn slippage_tolerance_bps(self: @ContractState) -> u256 { - self.slippage_tolerance_bps.read() + fn vault_allocator(self: @ContractState) -> ContractAddress { + self.vault_allocator.read() } - fn set_config(ref self: ContractState, period: u64, allowed_calls_per_period: u64) { - self.ownable.assert_only_owner(); - if (period.is_zero()) { - Errors::period_zero(); - } - if (allowed_calls_per_period.is_zero()) { - Errors::allowed_calls_per_period_zero(); - } - self.config.write(Option::Some(Config { period, allowed_calls_per_period })); - self.emit(ConfigUpdated { period, allowed_calls_per_period }); + fn config(self: @ContractState) -> (u16, u64, u64) { + (self.slippage.read(), self.period.read(), self.allowed_calls_per_period.read()) } - - fn set_slippage_tolerance_bps(ref self: ContractState, new_slippage_bps: u256) { + fn set_config( + ref self: ContractState, slippage: u16, period: u64, allowed_calls_per_period: u64, + ) { self.ownable.assert_only_owner(); - if (new_slippage_bps >= BPS_SCALE) { - Errors::slippage_exceeds_max(new_slippage_bps); - } - self.slippage_tolerance_bps.write(new_slippage_bps); - self.emit(SlippageUpdated { slippage: new_slippage_bps }); + self._set_config(slippage, period, allowed_calls_per_period); + self.emit(ConfigUpdated { slippage, period, allowed_calls_per_period }); } + fn multi_route_swap( ref self: ContractState, sell_token_address: ContractAddress, @@ -147,7 +144,7 @@ pub mod AvnuMiddleware { } let sell = ERC20ABIDispatcher { contract_address: sell_token_address }; let buy = ERC20ABIDispatcher { contract_address: buy_token_address }; - let avnu = self.avnu_router.read(); + let avnu = IAvnuExchangeDispatcher { contract_address: AVNU_ROUTER() }; sell.transfer_from(caller, this, sell_token_amount); sell.approve(avnu.contract_address, sell_token_amount); let quote_out = self @@ -157,8 +154,8 @@ pub mod AvnuMiddleware { let computed_min = math::u256_mul_div( quote_out, - BPS_SCALE - self.slippage_tolerance_bps.read(), - BPS_SCALE, + (BPS_SCALE - self.slippage.read()).into(), + BPS_SCALE.into(), math::Rounding::Ceil, ); @@ -194,19 +191,46 @@ pub mod AvnuMiddleware { #[generate_trait] pub impl InternalFunctions of InternalFunctionsTrait { - fn enforce_rate_limit(ref self: ContractState, vault: ContractAddress) { - if (self.config.read().is_some()) { - let cfg = self.config.read().unwrap(); - let ts: u64 = get_block_timestamp(); - let slot = ts % cfg.period; - let key = (vault, slot); - let current = self.call_count.read(key); - let next = current + 1; - if (next > cfg.allowed_calls_per_period) { - Errors::rate_limit_exceeded(next, cfg.allowed_calls_per_period); - } - self.call_count.write(key, next); + fn enforce_rate_limit(ref self: ContractState, caller: ContractAddress) { + if (caller != self.vault_allocator.read()) { + Errors::caller_not_vault_allocator(); } + + let period = self.period.read(); + let ts: u64 = get_block_timestamp(); + let window_id: u64 = ts / period; + + if (window_id != self.current_window_id.read()) { + self.current_window_id.write(window_id); + self.window_call_count.write(0); + } + + let current = self.window_call_count.read(); + let next = current + 1; + let allowed = self.allowed_calls_per_period.read(); + + if (next > allowed) { + Errors::rate_limit_exceeded(next, allowed); + } + self.window_call_count.write(next); + } + + fn _set_config(ref self: ContractState, slippage: u16, period: u64, allowed: u64) { + if (slippage >= BPS_SCALE) { + Errors::slippage_exceeds_max(slippage); + } + if (period.is_zero()) { + Errors::period_zero(); + } + if (allowed.is_zero()) { + Errors::allowed_calls_per_period_zero(); + } + + self.slippage.write(slippage); + self.period.write(period); + self.allowed_calls_per_period.write(allowed); + self.current_window_id.write(0); + self.window_call_count.write(0); } } } diff --git a/packages/vault_allocator/src/middlewares/avnu_middleware/errors.cairo b/packages/vault_allocator/src/middlewares/avnu_middleware/errors.cairo index 1c90966a..cef66fda 100644 --- a/packages/vault_allocator/src/middlewares/avnu_middleware/errors.cairo +++ b/packages/vault_allocator/src/middlewares/avnu_middleware/errors.cairo @@ -7,7 +7,7 @@ pub mod Errors { panic!("Insufficient output: {} < {}", out, min); } - pub fn slippage_exceeds_max(slippage: u256) { + pub fn slippage_exceeds_max(slippage: u16) { panic!("Slippage exceeds max: {}", slippage); } @@ -22,4 +22,8 @@ pub mod Errors { pub fn allowed_calls_per_period_zero() { panic!("Allowed calls per period is zero"); } + + pub fn caller_not_vault_allocator() { + panic!("Caller not vault allocator"); + } } diff --git a/packages/vault_allocator/src/middlewares/avnu_middleware/interface.cairo b/packages/vault_allocator/src/middlewares/avnu_middleware/interface.cairo index 9fbb8bb5..7b424486 100644 --- a/packages/vault_allocator/src/middlewares/avnu_middleware/interface.cairo +++ b/packages/vault_allocator/src/middlewares/avnu_middleware/interface.cairo @@ -9,11 +9,9 @@ use vault_allocator::decoders_and_sanitizers::decoder_custom_types::Route; pub trait IAvnuMiddleware { fn avnu_router(self: @T) -> ContractAddress; fn price_router(self: @T) -> ContractAddress; - fn slippage_tolerance_bps(self: @T) -> u256; - - fn set_config(ref self: T, period: u64, allowed_calls_per_period: u64); - - + fn vault_allocator(self: @T) -> ContractAddress; + fn config(self: @T) -> (u16, u64, u64); + fn set_config(ref self: T, slippage: u16, period: u64, allowed_calls_per_period: u64); fn multi_route_swap( ref self: T, sell_token_address: ContractAddress, @@ -26,5 +24,4 @@ pub trait IAvnuMiddleware { integrator_fee_recipient: ContractAddress, routes: Array, ) -> u256; - fn set_slippage_tolerance_bps(ref self: T, new_slippage_bps: u256); } diff --git a/packages/vault_allocator/src/mocks/flashloan.cairo b/packages/vault_allocator/src/mocks/flashloan.cairo deleted file mode 100644 index cc7ce590..00000000 --- a/packages/vault_allocator/src/mocks/flashloan.cairo +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2025 Starknet Vault Kit -// Licensed under the MIT License. See LICENSE file for details. -use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; -use starknet::{ContractAddress, get_contract_address}; - -fn transfer_asset( - asset: ContractAddress, - sender: ContractAddress, - to: ContractAddress, - amount: u256, - is_legacy: bool, -) { - let erc20 = ERC20ABIDispatcher { contract_address: asset }; - if sender == get_contract_address() { - assert!(erc20.transfer(to, amount), "transfer-failed"); - } else if is_legacy { - assert!(erc20.transferFrom(sender, to, amount), "transferFrom-failed"); - } else { - assert!(erc20.transfer_from(sender, to, amount), "transfer-from-failed"); - } -} - -#[starknet::contract] -pub mod FlashLoanSingletonMock { - use core::num::traits::Zero; - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - use starknet::{ContractAddress, get_caller_address}; - use vault_allocator::integration_interfaces::vesu::{ - IFlashloanReceiverDispatcher, IFlashloanReceiverDispatcherTrait, - }; - use super::{get_contract_address, transfer_asset}; - - - #[storage] - struct Storage { - do_nothing: bool, - i_did_something: bool, - do_wrong_callback: bool, - } - - #[constructor] - fn constructor(ref self: ContractState) {} - - #[abi(embed_v0)] - impl FlashLoanSingletonMockImpl of super::IFlashLoanSingletonMock { - fn do_wrong_callback(self: @ContractState) -> bool { - self.do_wrong_callback.read() - } - - fn set_do_wrong_callback(ref self: ContractState, do_wrong_callback: bool) { - self.do_wrong_callback.write(do_wrong_callback); - } - - fn flash_loan( - ref self: ContractState, - receiver: ContractAddress, - asset: ContractAddress, - amount: u256, - is_legacy: bool, - data: Span, - ) { - if (!self.do_nothing.read()) { - if (!self.do_wrong_callback.read()) { - transfer_asset(asset, get_contract_address(), receiver, amount, is_legacy); - IFlashloanReceiverDispatcher { contract_address: receiver } - .on_flash_loan(get_caller_address(), asset, amount, data); - transfer_asset(asset, receiver, get_contract_address(), amount, is_legacy); - } else { - let mut flash_loan_data_proofs: Array> = ArrayTrait::new(); - flash_loan_data_proofs.append(array![Zero::zero()].span()); - let mut flash_loan_data_decoder_and_sanitizer: Array = - ArrayTrait::new(); - flash_loan_data_decoder_and_sanitizer.append(Zero::zero()); - let mut flash_loan_data_target: Array = ArrayTrait::new(); - flash_loan_data_target.append(Zero::zero()); - let mut flash_loan_data_selector: Array = ArrayTrait::new(); - flash_loan_data_selector.append(Zero::zero()); - let mut flash_loan_data_calldata: Array> = ArrayTrait::new(); - flash_loan_data_calldata.append(array![Zero::zero()].span()); - let mut serialized_flash_loan_data = ArrayTrait::new(); - ( - flash_loan_data_proofs.span(), - flash_loan_data_decoder_and_sanitizer.span(), - flash_loan_data_target.span(), - flash_loan_data_selector.span(), - flash_loan_data_calldata.span(), - ) - .serialize(ref serialized_flash_loan_data); - - transfer_asset(asset, get_contract_address(), receiver, amount, is_legacy); - IFlashloanReceiverDispatcher { contract_address: receiver } - .on_flash_loan( - get_caller_address(), asset, amount, serialized_flash_loan_data.span(), - ); - transfer_asset(asset, receiver, get_contract_address(), amount, is_legacy); - } - } - } - - fn approve(ref self: ContractState, token: ContractAddress, amount: u256) { - transfer_asset(token, get_caller_address(), get_contract_address(), amount, true); - transfer_asset(token, get_contract_address(), get_caller_address(), amount, true); - self.i_did_something.write(true) - } - - fn i_did_something(self: @ContractState) -> bool { - self.i_did_something.read() - } - - fn do_nothing(self: @ContractState) -> bool { - self.do_nothing.read() - } - - fn set_do_nothing(ref self: ContractState, do_nothing: bool) { - self.do_nothing.write(do_nothing); - } - } -} - -#[starknet::interface] -pub trait IFlashLoanSingletonMock { - fn flash_loan( - ref self: TContractState, - receiver: ContractAddress, - asset: ContractAddress, - amount: u256, - is_legacy: bool, - data: Span, - ); - - fn approve(ref self: TContractState, token: ContractAddress, amount: u256); - - fn i_did_something(self: @TContractState) -> bool; - fn do_nothing(self: @TContractState) -> bool; - fn do_wrong_callback(self: @TContractState) -> bool; - fn set_do_nothing(ref self: TContractState, do_nothing: bool); - fn set_do_wrong_callback(ref self: TContractState, do_wrong_callback: bool); -} diff --git a/packages/vault_allocator/src/mocks/vault.cairo b/packages/vault_allocator/src/mocks/vault.cairo new file mode 100644 index 00000000..05a5aeb8 --- /dev/null +++ b/packages/vault_allocator/src/mocks/vault.cairo @@ -0,0 +1,179 @@ +#[starknet::contract] +pub mod MockVault { + use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::token::erc20::extensions::erc4626::{ + DefaultConfig, ERC4626Component, ERC4626DefaultNoFees, ERC4626DefaultNoLimits, + }; + use openzeppelin::token::erc20::{ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl, + }; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; + + // --- OpenZeppelin Component Integrations --- + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + + // --- ERC4626 Implementation --- + #[abi(embed_v0)] + impl ERC4626Impl = ERC4626Component::ERC4626Impl; + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + + // --- ERC20 Implementation --- + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + // --- ERC20 Metadata Implementation --- + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + + #[storage] + pub struct Storage { + // --- Component Storage --- + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + erc4626: ERC4626Component::Storage, + // --- Vault State --- + buffer: u256, // Assets available in vault for instant redemption + aum: u256 // Assets under management (deployed to allocators) + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ERC20Event: ERC20Component::Event, + ERC4626Event: ERC4626Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, underlying_asset: ContractAddress) { + self.erc20.initializer("MockVault", "MVAULT"); + self.erc4626.initializer(underlying_asset); + } + + + /// Implementation of asset management for ERC4626 compatibility + impl MockVaultAssetsManagementImpl of ERC4626Component::AssetsManagementTrait { + fn get_total_assets(self: @ERC4626Component::ComponentState) -> u256 { + let contract_state = self.get_contract(); + contract_state.buffer.read() + contract_state.aum.read() + } + + fn transfer_assets_in( + ref self: ERC4626Component::ComponentState, + from: ContractAddress, + assets: u256, + ) { + let this = starknet::get_contract_address(); + let asset_dispatcher = ERC20ABIDispatcher { + contract_address: self.ERC4626_asset.read(), + }; + assert( + asset_dispatcher.transfer_from(from, this, assets), + ERC4626Component::Errors::TOKEN_TRANSFER_FAILED, + ); + } + + fn transfer_assets_out( + ref self: ERC4626Component::ComponentState, + to: ContractAddress, + assets: u256, + ) { + let asset_dispatcher = ERC20ABIDispatcher { + contract_address: self.ERC4626_asset.read(), + }; + assert( + asset_dispatcher.transfer(to, assets), + ERC4626Component::Errors::TOKEN_TRANSFER_FAILED, + ); + } + } + + pub impl MockVaultHooksImpl of ERC4626Component::ERC4626HooksTrait { + fn before_withdraw( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256, + fee: Option, + ) {} + + fn after_withdraw( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256, + fee: Option, + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.buffer.write(contract_state.buffer.read() - assets); + } + + fn before_deposit( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256, + fee: Option, + ) {} + + fn after_deposit( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256, + fee: Option, + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.buffer.write(contract_state.buffer.read() + assets); + } + } + + #[abi(embed_v0)] + impl MockVaultImpl of MockVaultTrait { + /// Bring assets back from allocators to vault buffer + fn bring_liquidity(ref self: ContractState, amount: u256) { + ERC20ABIDispatcher { contract_address: self.erc4626.asset() } + .transfer_from(get_caller_address(), starknet::get_contract_address(), amount); + self.buffer.write(self.buffer.read() + amount); + self.aum.write(self.aum.read() - amount); + } + + /// Get current buffer amount + fn buffer(self: @ContractState) -> u256 { + self.buffer.read() + } + + /// Get current assets under management + fn aum(self: @ContractState) -> u256 { + self.aum.read() + } + + /// Set buffer for testing purposes + fn set_buffer(ref self: ContractState, amount: u256) { + self.buffer.write(amount); + } + + /// Set AUM for testing purposes + fn set_aum(ref self: ContractState, amount: u256) { + self.aum.write(amount); + } + } + + #[starknet::interface] + pub trait MockVaultTrait { + fn bring_liquidity(ref self: TContractState, amount: u256); + fn buffer(self: @TContractState) -> u256; + fn aum(self: @TContractState) -> u256; + fn set_buffer(ref self: TContractState, amount: u256); + fn set_aum(ref self: TContractState, amount: u256); + } +} diff --git a/packages/vault_allocator/src/periphery/price_router/price_router.cairo b/packages/vault_allocator/src/periphery/price_router/price_router.cairo index 6acff3e3..46928f55 100644 --- a/packages/vault_allocator/src/periphery/price_router/price_router.cairo +++ b/packages/vault_allocator/src/periphery/price_router/price_router.cairo @@ -7,6 +7,7 @@ pub mod PriceRouter { use core::num::traits::{Pow, Zero}; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::utils::math; use starknet::ContractAddress; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, @@ -89,7 +90,7 @@ pub mod PriceRouter { let num: u256 = amount * base_price * scale_quote; let den: u256 = quote_price * scale_base; - num / den + math::u256_mul_div(num, 1, den, math::Rounding::Ceil) } fn asset_to_id(self: @ContractState, asset: ContractAddress) -> felt252 { self.asset_to_id.read(asset) diff --git a/packages/vault_allocator/src/periphery/price_router_vesu/errors.cairo b/packages/vault_allocator/src/periphery/price_router_vesu/errors.cairo new file mode 100644 index 00000000..0be8a340 --- /dev/null +++ b/packages/vault_allocator/src/periphery/price_router_vesu/errors.cairo @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +pub mod Errors { + use starknet::ContractAddress; + + pub fn invalid_price(asset: ContractAddress) { + panic!("Invalid price for asset: {:?}", asset); + } +} diff --git a/packages/vault_allocator/src/periphery/price_router_vesu/interface.cairo b/packages/vault_allocator/src/periphery/price_router_vesu/interface.cairo new file mode 100644 index 00000000..775165cf --- /dev/null +++ b/packages/vault_allocator/src/periphery/price_router_vesu/interface.cairo @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +use starknet::ContractAddress; + + +#[starknet::interface] +pub trait IPriceRouter { + fn get_value( + self: @T, base_asset: ContractAddress, amount: u256, quote_asset: ContractAddress, + ) -> u256; + fn vesu_oracle_contract(self: @T) -> ContractAddress; +} + diff --git a/packages/vault_allocator/src/periphery/price_router_vesu/price_router_vesu.cairo b/packages/vault_allocator/src/periphery/price_router_vesu/price_router_vesu.cairo new file mode 100644 index 00000000..4651b32c --- /dev/null +++ b/packages/vault_allocator/src/periphery/price_router_vesu/price_router_vesu.cairo @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +#[starknet::contract] +pub mod PriceRouter { + use core::num::traits::Pow; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::utils::math; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use vault_allocator::integration_interfaces::vesu_v2::{ + IOracleDispatcher, IOracleDispatcherTrait, + }; + use vault_allocator::periphery::price_router_vesu::errors::Errors; + use vault_allocator::periphery::price_router_vesu::interface::IPriceRouter; + + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl InternalImpl = OwnableComponent::InternalImpl; + + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + vesu_oracle: IOracleDispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress, vesu_oracle: ContractAddress) { + self.ownable.initializer(owner); + self.vesu_oracle.write(IOracleDispatcher { contract_address: vesu_oracle }); + } + + + #[abi(embed_v0)] + impl PriceRouterImpl of IPriceRouter { + fn get_value( + self: @ContractState, + base_asset: ContractAddress, + amount: u256, + quote_asset: ContractAddress, + ) -> u256 { + if base_asset == quote_asset { + return amount; + } + + let base_resp = self + .vesu_oracle + .read() + .price(base_asset); // USD price with its own decimals + if !base_resp.is_valid { + Errors::invalid_price(base_asset); + } + let quote_resp = self.vesu_oracle.read().price(quote_asset); + if !quote_resp.is_valid { + Errors::invalid_price(quote_asset); + } + + let base_price: u256 = base_resp.value; + let quote_price: u256 = quote_resp.value; + + let asset_decimals_base: u8 = ERC20ABIDispatcher { contract_address: base_asset } + .decimals(); + let asset_decimals_quote: u8 = ERC20ABIDispatcher { contract_address: quote_asset } + .decimals(); + + let scale_base: u256 = 10_u256.pow(asset_decimals_base.into()); + let scale_quote: u256 = 10_u256.pow(asset_decimals_quote.into()); + + let num: u256 = amount * base_price * scale_quote; + let den: u256 = quote_price * scale_base; + math::u256_mul_div(num, 1, den, math::Rounding::Ceil) + } + + fn vesu_oracle_contract(self: @ContractState) -> ContractAddress { + self.vesu_oracle.read().contract_address + } + } +} diff --git a/packages/vault_allocator/src/test/creator.cairo b/packages/vault_allocator/src/test/creator.cairo index b40b4796..9f01c5c0 100644 --- a/packages/vault_allocator/src/test/creator.cairo +++ b/packages/vault_allocator/src/test/creator.cairo @@ -3,13 +3,15 @@ // Licensed under the MIT License. See LICENSE file for details. use alexandria_math::i257::I257Impl; use starknet::ContractAddress; -use vault_allocator::test::register::{ETH, GENESIS_POOL_ID, wstETH}; -use vault_allocator::test::utils::{ - ManageLeaf, _add_avnu_leafs, _add_vesu_flash_loan_leafs, _add_vesu_leafs, - _pad_leafs_to_power_of_two, generate_merkle_tree, get_leaf_hash, +use vault_allocator::merkle_tree::base::{ + ManageLeaf, _pad_leafs_to_power_of_two, generate_merkle_tree, get_leaf_hash, }; +use vault_allocator::merkle_tree::integrations::avnu::{AvnuConfig, _add_avnu_leafs}; +use vault_allocator::merkle_tree::integrations::vesu_v1::{VesuV1Config, _add_vesu_v1_leafs}; +use vault_allocator::merkle_tree::registery::{ETH, GENESIS_POOL_ID, wstETH}; use super::utils::DUMMY_ADDRESS; + #[derive(PartialEq, Drop, Serde, Debug, Clone)] pub struct ManageLeafAdditionalData { pub decoder_and_sanitizer: ContractAddress, @@ -34,39 +36,30 @@ fn test_creator() { let router = DUMMY_ADDRESS(); // INTEGRATIONS - let flash_loan_asset = wstETH(); - let is_legacy = false; - _add_vesu_flash_loan_leafs( - ref leafs, - ref leaf_index, - vault_allocator, - decoder_and_sanitizer, - manager, - flash_loan_asset, - is_legacy, - ); let pool_id = GENESIS_POOL_ID; - let mut assets_to_supply = ArrayTrait::new(); - assets_to_supply.append(wstETH()); - let mut assets_to_borrow_per_assets_to_supply = ArrayTrait::new(); - assets_to_borrow_per_assets_to_supply.append(array![ETH()].span()); - _add_vesu_leafs( + _add_vesu_v1_leafs( ref leafs, ref leaf_index, vault_allocator, decoder_and_sanitizer, - pool_id, - assets_to_supply.span(), - assets_to_borrow_per_assets_to_supply.span(), + array![ + VesuV1Config { pool_id, collateral_asset: wstETH(), debt_assets: array![ETH()].span() }, + ] + .span(), ); let mut pairs_to_swap = ArrayTrait::new(); pairs_to_swap.append((ETH(), wstETH())); _add_avnu_leafs( - ref leafs, ref leaf_index, vault_allocator, decoder_and_sanitizer, router, pairs_to_swap, + ref leafs, + ref leaf_index, + vault_allocator, + decoder_and_sanitizer, + router, + array![AvnuConfig { sell_token: ETH(), buy_token: wstETH() }].span(), ); let leaf_used = leafs.len(); diff --git a/packages/vault_allocator/src/test/integrations/avnu.cairo b/packages/vault_allocator/src/test/integrations/avnu.cairo index 4e8b69a0..57360edc 100644 --- a/packages/vault_allocator/src/test/integrations/avnu.cairo +++ b/packages/vault_allocator/src/test/integrations/avnu.cairo @@ -9,27 +9,30 @@ use snforge_std::{map_entry_address, store}; use starknet::ContractAddress; use vault_allocator::decoders_and_sanitizers::decoder_custom_types::Route; use vault_allocator::manager::interface::IManagerDispatcherTrait; -use vault_allocator::middlewares::avnu_middleware::interface::{ - IAvnuMiddlewareDispatcher, IAvnuMiddlewareDispatcherTrait, +use vault_allocator::merkle_tree::base::{ + ManageLeaf, _get_proofs_using_tree, _pad_leafs_to_power_of_two, generate_merkle_tree, }; -use vault_allocator::test::register::{ETH, VESU_SINGLETON, wstETH}; +use vault_allocator::merkle_tree::integrations::avnu::{AvnuConfig, _add_avnu_leafs}; +use vault_allocator::merkle_tree::registery::{ETH, wstETH}; use vault_allocator::test::utils::{ - ManageLeaf, OWNER, STRATEGIST, WAD, _add_avnu_leafs, _get_proofs_using_tree, - _pad_leafs_to_power_of_two, cheat_caller_address_once, deploy_avnu_middleware, deploy_manager, + OWNER, STRATEGIST, WAD, cheat_caller_address_once, deploy_avnu_middleware, deploy_manager, deploy_price_router, deploy_simple_decoder_and_sanitizer, deploy_vault_allocator, - generate_merkle_tree, initialize_price_router, + initialize_price_router, }; use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; + #[fork("AVNU")] #[test] fn test_manage_vault_with_merkle_verification_avnu() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); let price_router = deploy_price_router(); initialize_price_router(price_router); - let avnu_middleware = deploy_avnu_middleware(price_router); + let avnu_middleware = deploy_avnu_middleware( + vault_allocator.contract_address, price_router, 100, 100000, 1000000, + ); // 1% slippage let mut leafs: Array = ArrayTrait::new(); let mut leaf_index: u256 = 0; @@ -40,7 +43,7 @@ fn test_manage_vault_with_merkle_verification_avnu() { vault_allocator.contract_address, simple_decoder_and_sanitizer, avnu_middleware, - array![(wstETH(), ETH())], + array![AvnuConfig { sell_token: wstETH(), buy_token: ETH() }].span(), ); _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); @@ -143,10 +146,6 @@ fn test_manage_vault_with_merkle_verification_avnu() { manage_leafs.append(leafs.at(0).clone()); manage_leafs.append(leafs.at(1).clone()); - cheat_caller_address_once(avnu_middleware, OWNER()); - IAvnuMiddlewareDispatcher { contract_address: avnu_middleware } - .set_slippage_tolerance_bps(100); // 1% slippage - let manage_proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); cheat_caller_address_once(manager.contract_address, STRATEGIST()); diff --git a/packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo b/packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo new file mode 100644 index 00000000..c2c846f8 --- /dev/null +++ b/packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Starknet Vault Kit +// Licensed under the MIT License. See LICENSE file for details. + +use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use snforge_std::{map_entry_address, store}; +use vault_allocator::manager::interface::IManagerDispatcherTrait; +use vault_allocator::merkle_tree::base::{ + ManageLeaf, _add_vault_allocator_leafs, _get_proofs_using_tree, _pad_leafs_to_power_of_two, + generate_merkle_tree, +}; +use vault_allocator::mocks::vault::MockVault::MockVaultTraitDispatcherTrait; +use vault_allocator::test::utils::{ + OWNER, STRATEGIST, WAD, cheat_caller_address_once, deploy_erc20_mock, deploy_manager, + deploy_mock_vault, deploy_simple_decoder_and_sanitizer, deploy_vault_allocator, +}; +use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; + +#[test] +fn test_manage_vault_with_merkle_verification_bring_liquidity() { + let vault_allocator = deploy_vault_allocator(); + let manager = deploy_manager(vault_allocator); + let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); + + let underlying_token = deploy_erc20_mock(); + let mock_vault = deploy_mock_vault(underlying_token); + + let mut leafs: Array = ArrayTrait::new(); + let mut leaf_index: u256 = 0; + + _add_vault_allocator_leafs( + ref leafs, + ref leaf_index, + vault_allocator.contract_address, + simple_decoder_and_sanitizer, + mock_vault.contract_address, + ); + + _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); + + let tree = generate_merkle_tree(leafs.span()); + let root = *tree.at(tree.len() - 1).at(0); + + cheat_caller_address_once(vault_allocator.contract_address, OWNER()); + vault_allocator.set_manager(manager.contract_address); + + cheat_caller_address_once(manager.contract_address, OWNER()); + manager.set_manage_root(STRATEGIST(), root); + + // Set initial balance and liquidity for mock vault + let initial_liquidity: u256 = 10 * WAD; + let initial_buffer: u256 = 5 * WAD; + + // Add underlying token balance to vault allocator + let mut cheat_calldata = ArrayTrait::new(); + initial_liquidity.serialize(ref cheat_calldata); + store( + underlying_token, + map_entry_address( + selector!("ERC20_balances"), array![vault_allocator.contract_address.into()].span(), + ), + cheat_calldata.span(), + ); + + // Set initial buffer and liquidity in mock vault + cheat_caller_address_once(mock_vault.contract_address, OWNER()); + mock_vault.set_buffer(initial_buffer); + + cheat_caller_address_once(mock_vault.contract_address, OWNER()); + mock_vault.set_aum(initial_liquidity); + + let underlying_disp = ERC20ABIDispatcher { contract_address: underlying_token }; + assert( + underlying_disp.balance_of(vault_allocator.contract_address) == initial_liquidity, + 'underlying balance incorrect', + ); + + // Prepare to call bring_liquidity + let mut array_of_decoders_and_sanitizers = ArrayTrait::new(); + array_of_decoders_and_sanitizers.append(simple_decoder_and_sanitizer); + array_of_decoders_and_sanitizers.append(simple_decoder_and_sanitizer); + + let mut array_of_targets = ArrayTrait::new(); + array_of_targets.append(underlying_token); + array_of_targets.append(mock_vault.contract_address); + + let mut array_of_selectors = ArrayTrait::new(); + array_of_selectors.append(selector!("approve")); + array_of_selectors.append(selector!("bring_liquidity")); + + let mut array_of_calldatas = ArrayTrait::new(); + + let bring_liquidity_amount: u256 = 2 * WAD; + + // Approval calldata + let mut array_of_calldata_approve: Array = ArrayTrait::new(); + mock_vault.contract_address.serialize(ref array_of_calldata_approve); + bring_liquidity_amount.serialize(ref array_of_calldata_approve); + array_of_calldatas.append(array_of_calldata_approve.span()); + + // Bring liquidity calldata (empty for this function) + let mut array_of_calldata_bring_liquidity: Array = ArrayTrait::new(); + bring_liquidity_amount.serialize(ref array_of_calldata_bring_liquidity); + array_of_calldatas.append(array_of_calldata_bring_liquidity.span()); + + let mut manage_leafs: Array = ArrayTrait::new(); + manage_leafs.append(leafs.at(0).clone()); + manage_leafs.append(leafs.at(1).clone()); + + let manage_proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); + + cheat_caller_address_once(manager.contract_address, STRATEGIST()); + manager + .manage_vault_with_merkle_verification( + manage_proofs.span(), + array_of_decoders_and_sanitizers.span(), + array_of_targets.span(), + array_of_selectors.span(), + array_of_calldatas.span(), + ); + + // Verify that bring_liquidity was called successfully + // The mock vault should have received some underlying tokens and updated its state + let new_liquidity = underlying_disp.balance_of(vault_allocator.contract_address); + assert( + new_liquidity == initial_liquidity - bring_liquidity_amount, 'tokens should be transferred', + ); + + // Check that the vault has increased liquidity + let new_buffer = mock_vault.buffer(); + let new_aum = mock_vault.aum(); + + assert(new_buffer == initial_buffer + bring_liquidity_amount, 'buffer should increase'); + assert(new_aum == initial_liquidity - bring_liquidity_amount, 'aum should decrease'); +} diff --git a/packages/vault_allocator/src/test/integrations/vesu_v1.cairo b/packages/vault_allocator/src/test/integrations/vesu_v1.cairo index b58c05cf..4e72a03c 100644 --- a/packages/vault_allocator/src/test/integrations/vesu_v1.cairo +++ b/packages/vault_allocator/src/test/integrations/vesu_v1.cairo @@ -9,16 +9,18 @@ use snforge_std::{map_entry_address, store}; use vault_allocator::decoders_and_sanitizers::decoder_custom_types::{ Amount, AmountDenomination, AmountType, }; -use vault_allocator::integration_interfaces::vesu::{ - IDefaultExtensionPOV2Dispatcher, IDefaultExtensionPOV2DispatcherTrait, ISingletonV2Dispatcher, - ISingletonV2DispatcherTrait, -}; use vault_allocator::manager::interface::IManagerDispatcherTrait; -use vault_allocator::test::register::{ETH, GENESIS_POOL_ID, VESU_SINGLETON, wstETH}; +use vault_allocator::merkle_tree::base::{ + ManageLeaf, _get_proofs_using_tree, _pad_leafs_to_power_of_two, generate_merkle_tree, +}; +use vault_allocator::merkle_tree::integrations::erc4626::_add_erc4626_leafs; +use vault_allocator::merkle_tree::integrations::vesu_v1::{VesuV1Config, _add_vesu_v1_leafs}; +use vault_allocator::merkle_tree::registery::{ + ETH, GENESIS_POOL_ID, VESU_GENESIS_POOL_V_TOKEN_WSTETH, VESU_SINGLETON, wstETH, +}; use vault_allocator::test::utils::{ - ManageLeaf, OWNER, STRATEGIST, WAD, _add_vesu_leafs, _get_proofs_using_tree, - _pad_leafs_to_power_of_two, cheat_caller_address_once, deploy_manager, - deploy_simple_decoder_and_sanitizer, deploy_vault_allocator, generate_merkle_tree, + OWNER, STRATEGIST, WAD, cheat_caller_address_once, deploy_manager, + deploy_simple_decoder_and_sanitizer, deploy_vault_allocator, }; use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; @@ -26,20 +28,18 @@ use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; #[test] fn test_manage_vault_with_merkle_verification_earn_mode() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); let mut leafs: Array = ArrayTrait::new(); let mut leaf_index: u256 = 0; - _add_vesu_leafs( + _add_erc4626_leafs( ref leafs, ref leaf_index, vault_allocator.contract_address, simple_decoder_and_sanitizer, - GENESIS_POOL_ID, - array![wstETH()].span(), - array![array![ETH()].span()].span(), + VESU_GENESIS_POOL_V_TOKEN_WSTETH(), ); _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); @@ -78,11 +78,7 @@ fn test_manage_vault_with_merkle_verification_earn_mode() { let mut array_of_targets = ArrayTrait::new(); array_of_targets.append(wstETH()); - let v_token = IDefaultExtensionPOV2Dispatcher { - contract_address: ISingletonV2Dispatcher { contract_address: VESU_SINGLETON() } - .extension(GENESIS_POOL_ID), - } - .v_token_for_collateral_asset(GENESIS_POOL_ID, wstETH()); + let v_token = VESU_GENESIS_POOL_V_TOKEN_WSTETH(); array_of_targets.append(v_token); let mut array_of_selectors = ArrayTrait::new(); @@ -277,20 +273,25 @@ fn test_manage_vault_with_merkle_verification_earn_mode() { #[test] fn test_manage_vault_with_merkle_verification_debt_mode() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); let mut leafs: Array = ArrayTrait::new(); let mut leaf_index: u256 = 0; - _add_vesu_leafs( + _add_vesu_v1_leafs( ref leafs, ref leaf_index, vault_allocator.contract_address, simple_decoder_and_sanitizer, - GENESIS_POOL_ID, - array![wstETH()].span(), - array![array![ETH()].span()].span(), + array![ + VesuV1Config { + pool_id: GENESIS_POOL_ID, + collateral_asset: wstETH(), + debt_assets: array![ETH()].span(), + }, + ] + .span(), ); _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); @@ -322,10 +323,7 @@ fn test_manage_vault_with_merkle_verification_debt_mode() { // first scenario is depositing wsteth to vesu genesis pool, transfer the position and borrow // ETH let deposit_amount: u256 = WAD; - let extension = ISingletonV2Dispatcher { contract_address: VESU_SINGLETON() } - .extension(GENESIS_POOL_ID); - let v_token = IDefaultExtensionPOV2Dispatcher { contract_address: extension } - .v_token_for_collateral_asset(GENESIS_POOL_ID, wstETH()); + let debt_amount: u256 = WAD / 40; // 2.5% of the deposit let mut array_of_decoders_and_sanitizers = ArrayTrait::new(); @@ -385,8 +383,8 @@ fn test_manage_vault_with_merkle_verification_debt_mode() { array_of_calldatas.append(array_of_calldata_modify_position.span()); let mut manage_leafs: Array = ArrayTrait::new(); - manage_leafs.append(leafs.at(5).clone()); - manage_leafs.append(leafs.at(6).clone()); + manage_leafs.append(leafs.at(0).clone()); + manage_leafs.append(leafs.at(1).clone()); let manage_proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); cheat_caller_address_once(manager.contract_address, STRATEGIST()); diff --git a/packages/vault_allocator/src/test/scenarios/leveraged_loop_staked_ether.cairo b/packages/vault_allocator/src/test/scenarios/leveraged_loop_staked_ether.cairo deleted file mode 100644 index 5fef2aa9..00000000 --- a/packages/vault_allocator/src/test/scenarios/leveraged_loop_staked_ether.cairo +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2025 Starknet Vault Kit -// Licensed under the MIT License. See LICENSE file for details. -use alexandria_math::i257::{I257Impl, I257Trait}; -use core::num::traits::Zero; -use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; -use snforge_std::{map_entry_address, store}; -use starknet::ContractAddress; -use vault_allocator::decoders_and_sanitizers::decoder_custom_types::{ - Amount, AmountDenomination, AmountType, Route, -}; -use vault_allocator::manager::interface::IManagerDispatcherTrait; -use vault_allocator::test::register::{ETH, GENESIS_POOL_ID, VESU_SINGLETON, wstETH}; -use vault_allocator::test::utils::{ - ManageLeaf, OWNER, STRATEGIST, WAD, _add_avnu_leafs, _add_vesu_flash_loan_leafs, - _add_vesu_leafs, _get_proofs_using_tree, _pad_leafs_to_power_of_two, cheat_caller_address_once, - deploy_avnu_middleware, deploy_manager, deploy_price_router, - deploy_simple_decoder_and_sanitizer, deploy_vault_allocator, generate_merkle_tree, - initialize_price_router, -}; -use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; - -#[fork("SLLSE")] -#[test] -fn test_leveraged_loop_staked_ether() { - let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); - let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); - let price_router = deploy_price_router(); - initialize_price_router(price_router); - let avnu_middleware = deploy_avnu_middleware(price_router); - - let mut leafs: Array = ArrayTrait::new(); - let mut leaf_index: u256 = 0; - - let collateral_asset = wstETH(); - - _add_vesu_flash_loan_leafs( - ref leafs, - ref leaf_index, - vault_allocator.contract_address, - simple_decoder_and_sanitizer, - manager.contract_address, - collateral_asset, - false, - ); - - _add_vesu_leafs( - ref leafs, - ref leaf_index, - vault_allocator.contract_address, - simple_decoder_and_sanitizer, - GENESIS_POOL_ID, - array![wstETH()].span(), - array![array![ETH()].span()].span(), - ); - - _add_avnu_leafs( - ref leafs, - ref leaf_index, - vault_allocator.contract_address, - simple_decoder_and_sanitizer, - avnu_middleware, - array![(ETH(), wstETH())], - ); - - _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); - - let tree = generate_merkle_tree(leafs.span()); - let root = *tree.at(tree.len() - 1).at(0); - cheat_caller_address_once(vault_allocator.contract_address, OWNER()); - vault_allocator.set_manager(manager.contract_address); - - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(STRATEGIST(), root); - - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(manager.contract_address, root); - - // config - let collateral_asset = wstETH(); - let initial_collateral_balance: u256 = WAD; - let collateral_to_flash_loan: u256 = WAD; - let debt_asset = ETH(); - let allowed_slippage: u256 = 10; // 0.1% - let required_debt_amount_to_refund_flash_loan = 1208100829164930048 - + 1208100829164930048 * allowed_slippage / 10000; - - let mut cheat_calldata = ArrayTrait::new(); - initial_collateral_balance.serialize(ref cheat_calldata); - store( - collateral_asset, - map_entry_address( - selector!("ERC20_balances"), array![vault_allocator.contract_address.into()].span(), - ), - cheat_calldata.span(), - ); - - let underlying_disp = ERC20ABIDispatcher { contract_address: collateral_asset }; - assert( - underlying_disp.balance_of(vault_allocator.contract_address) == initial_collateral_balance, - 'wsteth balance is not correct', - ); - - let mut array_of_decoders_and_sanitizers = ArrayTrait::new(); - array_of_decoders_and_sanitizers.append(simple_decoder_and_sanitizer); - - let mut array_of_targets = ArrayTrait::new(); - array_of_targets.append(manager.contract_address); - - let mut array_of_selectors = ArrayTrait::new(); - array_of_selectors.append(selector!("flash_loan")); - - let mut array_of_calldatas = ArrayTrait::new(); - let mut array_of_calldatas_flash_loan = ArrayTrait::new(); - manager.contract_address.serialize(ref array_of_calldatas_flash_loan); - collateral_asset.serialize(ref array_of_calldatas_flash_loan); - collateral_to_flash_loan.serialize(ref array_of_calldatas_flash_loan); - false.serialize(ref array_of_calldatas_flash_loan); - - let mut flash_loan_data_decoder_and_sanitizer: Array = ArrayTrait::new(); - flash_loan_data_decoder_and_sanitizer.append(simple_decoder_and_sanitizer); - flash_loan_data_decoder_and_sanitizer.append(simple_decoder_and_sanitizer); - flash_loan_data_decoder_and_sanitizer.append(simple_decoder_and_sanitizer); - flash_loan_data_decoder_and_sanitizer.append(simple_decoder_and_sanitizer); - - let mut flash_loan_data_target: Array = ArrayTrait::new(); - flash_loan_data_target.append(collateral_asset); - flash_loan_data_target.append(VESU_SINGLETON()); - flash_loan_data_target.append(debt_asset); - flash_loan_data_target.append(avnu_middleware); - - let mut flash_loan_data_selector: Array = ArrayTrait::new(); - flash_loan_data_selector.append(selector!("approve")); - flash_loan_data_selector.append(selector!("modify_position")); - flash_loan_data_selector.append(selector!("approve")); - flash_loan_data_selector.append(selector!("multi_route_swap")); - - let mut flash_loan_data_calldata: Array> = ArrayTrait::new(); - - // approve wsteth initial collateral + flash loan amount - let mut flash_loan_data_calldata_approve = ArrayTrait::new(); - VESU_SINGLETON().serialize(ref flash_loan_data_calldata_approve); - (initial_collateral_balance + collateral_to_flash_loan) - .serialize(ref flash_loan_data_calldata_approve); - flash_loan_data_calldata.append(flash_loan_data_calldata_approve.span()); - - // modify position supplying wsteth initial collateral + flash loan amount and borrowing eth - // equivalent to refund flashloan - let mut flash_loan_data_calldata_modify_position = ArrayTrait::new(); - GENESIS_POOL_ID.serialize(ref flash_loan_data_calldata_modify_position); - wstETH().serialize(ref flash_loan_data_calldata_modify_position); - ETH().serialize(ref flash_loan_data_calldata_modify_position); - vault_allocator.contract_address.serialize(ref flash_loan_data_calldata_modify_position); - - let value_for_collateral_modify_position = I257Trait::new( - initial_collateral_balance + collateral_to_flash_loan, false, - ); - let collateral_modify_position: Amount = Amount { - amount_type: AmountType::Delta, - denomination: AmountDenomination::Assets, - value: value_for_collateral_modify_position, - }; - collateral_modify_position.serialize(ref flash_loan_data_calldata_modify_position); - - let value_for_debt_modify_position = I257Trait::new( - required_debt_amount_to_refund_flash_loan, false, - ); - let debt_modify_position: Amount = Amount { - amount_type: AmountType::Delta, - denomination: AmountDenomination::Assets, - value: value_for_debt_modify_position, - }; - debt_modify_position.serialize(ref flash_loan_data_calldata_modify_position); - - let data: Span = array![].span(); - data.serialize(ref flash_loan_data_calldata_modify_position); - - flash_loan_data_calldata.append(flash_loan_data_calldata_modify_position.span()); - - // approve debt asset to avnu middleware - let mut flash_loan_data_calldata_approve_debt_asset: Array = ArrayTrait::new(); - avnu_middleware.serialize(ref flash_loan_data_calldata_approve_debt_asset); - required_debt_amount_to_refund_flash_loan - .serialize(ref flash_loan_data_calldata_approve_debt_asset); - flash_loan_data_calldata.append(flash_loan_data_calldata_approve_debt_asset.span()); - - // multi route swap: sell debt asset to avnu middleware for collateral to refund flashloan - let mut array_of_calldata_multi_route_swap: Array = ArrayTrait::new(); - debt_asset.serialize(ref array_of_calldata_multi_route_swap); - required_debt_amount_to_refund_flash_loan.serialize(ref array_of_calldata_multi_route_swap); - collateral_asset.serialize(ref array_of_calldata_multi_route_swap); - // buy token amount is 0 because we are selling - let buy_token_amount: u256 = Zero::zero(); - buy_token_amount.serialize(ref array_of_calldata_multi_route_swap); - // buy_token_min_amount is set to 0 because we are protected by price router whatever - let buy_token_min_amount: u256 = Zero::zero(); - buy_token_min_amount.serialize(ref array_of_calldata_multi_route_swap); - vault_allocator.contract_address.serialize(ref array_of_calldata_multi_route_swap); - let integrator_fee_amount_bps: u128 = Zero::zero(); - integrator_fee_amount_bps.serialize(ref array_of_calldata_multi_route_swap); - let integrator_fee_recipient: ContractAddress = Zero::zero(); - integrator_fee_recipient.serialize(ref array_of_calldata_multi_route_swap); - - let mut routes: Array = ArrayTrait::new(); - // hardcode route to ekubo wsteth/eth - let mut additional_swap_params: Array = ArrayTrait::new(); - collateral_asset.serialize(ref additional_swap_params); - debt_asset.serialize(ref additional_swap_params); - - let fee: u128 = 0x68db8bac710cb4000000000000000; - fee.serialize(ref additional_swap_params); - let tick_spacing: u128 = 0xc8; - tick_spacing.serialize(ref additional_swap_params); - let extension: ContractAddress = Zero::zero(); - extension.serialize(ref additional_swap_params); - let sqrt_ratio_distance: felt252 = 0x290d5f61e20000000000000000000; - sqrt_ratio_distance.serialize(ref additional_swap_params); - - routes - .append( - Route { - sell_token: debt_asset, - buy_token: collateral_asset, - exchange_address: 0x5dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b - .try_into() - .unwrap(), // ekubo - percent: 1000000000000, - additional_swap_params, - }, - ); - - routes.serialize(ref array_of_calldata_multi_route_swap); - - flash_loan_data_calldata.append(array_of_calldata_multi_route_swap.span()); - - let mut flash_loan_manager_leafs: Array = ArrayTrait::new(); - flash_loan_manager_leafs.append(leafs.at(6).clone()); - flash_loan_manager_leafs.append(leafs.at(7).clone()); - flash_loan_manager_leafs.append(leafs.at(8).clone()); - flash_loan_manager_leafs.append(leafs.at(9).clone()); - - let mut flash_loan_proofs = _get_proofs_using_tree(flash_loan_manager_leafs, tree.clone()); - - let mut serialized_flash_loan_data = ArrayTrait::new(); - ( - flash_loan_proofs.span(), - flash_loan_data_decoder_and_sanitizer.span(), - flash_loan_data_target.span(), - flash_loan_data_selector.span(), - flash_loan_data_calldata.span(), - ) - .serialize(ref serialized_flash_loan_data); - - serialized_flash_loan_data.span().serialize(ref array_of_calldatas_flash_loan); - array_of_calldatas.append(array_of_calldatas_flash_loan.span()); - - let mut manage_leafs: Array = ArrayTrait::new(); - manage_leafs.append(leafs.at(0).clone()); - - let proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); - - cheat_caller_address_once(manager.contract_address, STRATEGIST()); - manager - .manage_vault_with_merkle_verification( - proofs.span(), - array_of_decoders_and_sanitizers.span(), - array_of_targets.span(), - array_of_selectors.span(), - array_of_calldatas.span(), - ); -} diff --git a/packages/vault_allocator/src/test/scenarios/stable_carry_loop.cairo b/packages/vault_allocator/src/test/scenarios/stable_carry_loop.cairo index 89924332..47e6f9f9 100644 --- a/packages/vault_allocator/src/test/scenarios/stable_carry_loop.cairo +++ b/packages/vault_allocator/src/test/scenarios/stable_carry_loop.cairo @@ -9,20 +9,20 @@ use starknet::ContractAddress; use vault_allocator::decoders_and_sanitizers::decoder_custom_types::{ Amount, AmountDenomination, AmountType, Route, }; -use vault_allocator::integration_interfaces::vesu::{ - IDefaultExtensionPOV2Dispatcher, IDefaultExtensionPOV2DispatcherTrait, ISingletonV2Dispatcher, - ISingletonV2DispatcherTrait, -}; use vault_allocator::manager::interface::IManagerDispatcherTrait; -use vault_allocator::middlewares::avnu_middleware::interface::{ - IAvnuMiddlewareDispatcher, IAvnuMiddlewareDispatcherTrait, +use vault_allocator::merkle_tree::base::{ + ManageLeaf, _get_proofs_using_tree, _pad_leafs_to_power_of_two, generate_merkle_tree, +}; +use vault_allocator::merkle_tree::integrations::avnu::{AvnuConfig, _add_avnu_leafs}; +use vault_allocator::merkle_tree::integrations::erc4626::_add_erc4626_leafs; +use vault_allocator::merkle_tree::integrations::vesu_v1::{VesuV1Config, _add_vesu_v1_leafs}; +use vault_allocator::merkle_tree::registery::{ + ETH, GENESIS_POOL_ID, USDC, USDT, VESU_GENESIS_POOL_V_TOKEN_USDT, VESU_SINGLETON, }; -use vault_allocator::test::register::{ETH, GENESIS_POOL_ID, USDC, USDT, VESU_SINGLETON}; use vault_allocator::test::utils::{ - ManageLeaf, OWNER, STRATEGIST, WAD, _add_avnu_leafs, _add_vesu_leafs, _get_proofs_using_tree, - _pad_leafs_to_power_of_two, cheat_caller_address_once, deploy_avnu_middleware, deploy_manager, + OWNER, STRATEGIST, WAD, cheat_caller_address_once, deploy_avnu_middleware, deploy_manager, deploy_price_router, deploy_simple_decoder_and_sanitizer, deploy_vault_allocator, - generate_merkle_tree, initialize_price_router, + initialize_price_router, }; use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; @@ -30,22 +30,40 @@ use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; #[test] fn test_stable_carry_loop() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); let price_router = deploy_price_router(); initialize_price_router(price_router); - let avnu_middleware = deploy_avnu_middleware(price_router); + let avnu_middleware = deploy_avnu_middleware( + vault_allocator.contract_address, price_router, 100, 100000, 1000000, + ); let mut leafs: Array = ArrayTrait::new(); let mut leaf_index: u256 = 0; - _add_vesu_leafs( + + // add vesu v1 leafs + _add_vesu_v1_leafs( + ref leafs, + ref leaf_index, + vault_allocator.contract_address, + simple_decoder_and_sanitizer, + array![ + VesuV1Config { + pool_id: GENESIS_POOL_ID, + collateral_asset: ETH(), + debt_assets: array![USDC()].span(), + }, + ] + .span(), + ); + + // add erc4626 leafs for usdt v-token + _add_erc4626_leafs( ref leafs, ref leaf_index, vault_allocator.contract_address, simple_decoder_and_sanitizer, - GENESIS_POOL_ID, - array![ETH(), USDT()].span(), - array![array![USDC()].span(), array![].span()].span(), + VESU_GENESIS_POOL_V_TOKEN_USDT(), ); _add_avnu_leafs( @@ -54,7 +72,7 @@ fn test_stable_carry_loop() { vault_allocator.contract_address, simple_decoder_and_sanitizer, avnu_middleware, - array![(USDC(), USDT())], + array![AvnuConfig { sell_token: USDC(), buy_token: USDT() }].span(), ); _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); @@ -109,12 +127,7 @@ fn test_stable_carry_loop() { array_of_targets.append(USDC()); array_of_targets.append(avnu_middleware); array_of_targets.append(USDT()); - let v_token = IDefaultExtensionPOV2Dispatcher { - contract_address: ISingletonV2Dispatcher { contract_address: VESU_SINGLETON() } - .extension(GENESIS_POOL_ID), - } - .v_token_for_collateral_asset(GENESIS_POOL_ID, USDT()); - array_of_targets.append(v_token); + array_of_targets.append(VESU_GENESIS_POOL_V_TOKEN_USDT()); let mut array_of_selectors = ArrayTrait::new(); array_of_selectors.append(selector!("approve")); @@ -214,7 +227,7 @@ fn test_stable_carry_loop() { // approve usdt to the v-token let mut array_of_calldata_approve_vtoken: Array = ArrayTrait::new(); - v_token.serialize(ref array_of_calldata_approve_vtoken); + VESU_GENESIS_POOL_V_TOKEN_USDT().serialize(ref array_of_calldata_approve_vtoken); earn_amount_from_swap.serialize(ref array_of_calldata_approve_vtoken); array_of_calldatas.append(array_of_calldata_approve_vtoken.span()); @@ -225,16 +238,12 @@ fn test_stable_carry_loop() { array_of_calldatas.append(array_of_calldata_deposit.span()); let mut manage_leafs: Array = ArrayTrait::new(); - manage_leafs.append(leafs.at(5).clone()); - manage_leafs.append(leafs.at(6).clone()); - manage_leafs.append(leafs.at(12).clone()); - manage_leafs.append(leafs.at(13).clone()); + manage_leafs.append(leafs.at(0).clone()); + manage_leafs.append(leafs.at(1).clone()); manage_leafs.append(leafs.at(7).clone()); manage_leafs.append(leafs.at(8).clone()); - - cheat_caller_address_once(avnu_middleware, OWNER()); - IAvnuMiddlewareDispatcher { contract_address: avnu_middleware } - .set_slippage_tolerance_bps(100); // 1% slippage + manage_leafs.append(leafs.at(2).clone()); + manage_leafs.append(leafs.at(3).clone()); let manage_proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); diff --git a/packages/vault_allocator/src/test/units/manager.cairo b/packages/vault_allocator/src/test/units/manager.cairo index 3df3f8ee..4fc4c653 100644 --- a/packages/vault_allocator/src/test/units/manager.cairo +++ b/packages/vault_allocator/src/test/units/manager.cairo @@ -8,30 +8,24 @@ use openzeppelin::interfaces::accesscontrol::{ use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use openzeppelin::interfaces::security::pausable::{IPausableDispatcher, IPausableDispatcherTrait}; use openzeppelin::interfaces::upgrades::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; -use starknet::ContractAddress; -use vault_allocator::integration_interfaces::vesu::{ - IFlashloanReceiverDispatcher, IFlashloanReceiverDispatcherTrait, -}; use vault_allocator::manager::interface::{IManagerDispatcher, IManagerDispatcherTrait}; use vault_allocator::manager::manager::Manager::{OWNER_ROLE, PAUSER_ROLE}; -use vault_allocator::mocks::counter::{ICounterDispatcher, ICounterDispatcherTrait}; -use vault_allocator::mocks::flashloan::{ - IFlashLoanSingletonMockDispatcher, IFlashLoanSingletonMockDispatcherTrait, +use vault_allocator::merkle_tree::base::{ + ManageLeaf, _get_proofs_using_tree, _pad_leafs_to_power_of_two, generate_merkle_tree, }; -use vault_allocator::test::register::VESU_SINGLETON; +use vault_allocator::merkle_tree::integrations::erc4626::_add_erc4626_leafs; +use vault_allocator::mocks::counter::{ICounterDispatcher, ICounterDispatcherTrait}; use vault_allocator::test::utils::{ - DUMMY_ADDRESS, ManageLeaf, OWNER, STRATEGIST, WAD, _add_erc4626_leafs, - _add_vesu_flash_loan_leafs, _get_proofs_using_tree, _pad_leafs_to_power_of_two, - cheat_caller_address_once, deploy_counter, deploy_erc20_mock, deploy_erc4626_mock, - deploy_flashloan_mock, deploy_manager, deploy_simple_decoder_and_sanitizer, - deploy_vault_allocator, generate_merkle_tree, + DUMMY_ADDRESS, OWNER, STRATEGIST, WAD, cheat_caller_address_once, deploy_counter, + deploy_erc20_mock, deploy_erc4626_mock, deploy_manager, deploy_simple_decoder_and_sanitizer, + deploy_vault_allocator, }; use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcherTrait; #[test] fn test_constructor() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let access_control_dispatcher = IAccessControlDispatcher { contract_address: manager.contract_address, }; @@ -44,7 +38,6 @@ fn test_constructor() { let has_role = access_control_dispatcher.has_role(PAUSER_ROLE, OWNER()); assert(has_role, 'Pauser is not set correctly'); - assert(manager.vesu_singleton() == VESU_SINGLETON(), ' singleton is not set correctly'); assert( manager.vault_allocator() == vault_allocator.contract_address, 'allocator is not set correctly', @@ -55,7 +48,7 @@ fn test_constructor() { #[should_panic(expected: ('Caller is missing role',))] fn test_upgrade_not_owner() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let (_, counter_class_hash) = deploy_counter(); IUpgradeableDispatcher { contract_address: manager.contract_address } .upgrade(counter_class_hash); @@ -64,7 +57,7 @@ fn test_upgrade_not_owner() { #[test] fn test_upgrade() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let (_, counter_class_hash) = deploy_counter(); cheat_caller_address_once(manager.contract_address, OWNER()); IUpgradeableDispatcher { contract_address: manager.contract_address } @@ -75,7 +68,7 @@ fn test_upgrade() { #[test] fn test_set_manage_root() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let target = 0x123.try_into().unwrap(); let root = 0x456; @@ -91,7 +84,7 @@ fn test_set_manage_root() { #[should_panic(expected: ('Caller is missing role',))] fn test_set_manage_root_not_owner() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let target = 0x123.try_into().unwrap(); let root = 0x456; @@ -102,7 +95,7 @@ fn test_set_manage_root_not_owner() { #[test] fn test_pause() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let manager_dispatcher = IManagerDispatcher { contract_address: manager.contract_address }; let pausable_dispatcher = IPausableDispatcher { contract_address: manager.contract_address }; @@ -118,14 +111,14 @@ fn test_pause() { #[should_panic(expected: ('Caller is missing role',))] fn test_pause_not_pauser() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); manager.pause(); } #[test] fn test_unpause() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let manager_dispatcher = IManagerDispatcher { contract_address: manager.contract_address }; let pausable_dispatcher = IPausableDispatcher { contract_address: manager.contract_address }; @@ -143,7 +136,7 @@ fn test_unpause() { #[should_panic(expected: ('Caller is missing role',))] fn test_unpause_not_owner() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); cheat_caller_address_once(manager.contract_address, OWNER()); manager.pause(); manager.unpause(); @@ -153,7 +146,7 @@ fn test_unpause_not_owner() { #[should_panic(expected: "Inconsistent lengths")] fn test_manage_vault_with_merkle_verification_inconsistent_lengths_1() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); manager .manage_vault_with_merkle_verification( array![array![].span()].span(), @@ -168,7 +161,7 @@ fn test_manage_vault_with_merkle_verification_inconsistent_lengths_1() { #[should_panic(expected: "Inconsistent lengths")] fn test_manage_vault_with_merkle_verification_inconsistent_lengths_2() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); manager .manage_vault_with_merkle_verification( array![array![].span()].span(), @@ -183,7 +176,7 @@ fn test_manage_vault_with_merkle_verification_inconsistent_lengths_2() { #[should_panic(expected: "Inconsistent lengths")] fn test_manage_vault_with_merkle_verification_inconsistent_lengths_3() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); manager .manage_vault_with_merkle_verification( @@ -199,7 +192,7 @@ fn test_manage_vault_with_merkle_verification_inconsistent_lengths_3() { #[should_panic(expected: "Inconsistent lengths")] fn test_manage_vault_with_merkle_verification_inconsistent_lengths_4() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); manager .manage_vault_with_merkle_verification( @@ -215,7 +208,7 @@ fn test_manage_vault_with_merkle_verification_inconsistent_lengths_4() { #[should_panic(expected: "Invalid manage proof")] fn test_manage_vault_with_merkle_verification_invalid_proof() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); let underlying = deploy_erc20_mock(); let erc4626 = deploy_erc4626_mock(underlying); @@ -272,7 +265,7 @@ fn test_manage_vault_with_merkle_verification_invalid_proof() { #[test] fn test_manage_vault_with_merkle_verification_valid_proof() { let vault_allocator = deploy_vault_allocator(); - let manager = deploy_manager(vault_allocator, VESU_SINGLETON()); + let manager = deploy_manager(vault_allocator); let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); let underlying = deploy_erc20_mock(); let erc4626 = deploy_erc4626_mock(underlying); @@ -351,364 +344,3 @@ fn test_manage_vault_with_merkle_verification_valid_proof() { assert(underlying_balance == WAD * 9, 'Balance wrong'); } -#[test] -#[should_panic(expected: "Not vault allocator")] -fn test_flash_loan_not_vault_allocator() { - let vault_allocator = deploy_vault_allocator(); - let flashloan_mock = deploy_flashloan_mock(); - let manager = deploy_manager(vault_allocator, flashloan_mock); - manager - .flash_loan( - manager.contract_address, manager.contract_address, WAD, false, array!['0xdead'].span(), - ); -} - -#[test] -#[should_panic(expected: "Not vesu singleton")] -fn test_on_flash_loan_not_vesu_singleton() { - let vault_allocator = deploy_vault_allocator(); - let flashloan_mock = deploy_flashloan_mock(); - let manager = deploy_manager(vault_allocator, flashloan_mock); - let flash_loan_recipient_dispatcher = IFlashloanReceiverDispatcher { - contract_address: manager.contract_address, - }; - flash_loan_recipient_dispatcher - .on_flash_loan(DUMMY_ADDRESS(), DUMMY_ADDRESS(), WAD, array!['0xdead'].span()); -} - -#[test] -#[should_panic(expected: "Flash loan not executed")] -fn test_flash_loan_not_executed() { - let vault_allocator = deploy_vault_allocator(); - let flashloan_mock = deploy_flashloan_mock(); - let underlying = deploy_erc20_mock(); - let underlying_disp = ERC20ABIDispatcher { contract_address: underlying }; - cheat_caller_address_once(underlying, OWNER()); - underlying_disp.transfer(flashloan_mock, WAD * 10); - let manager = deploy_manager(vault_allocator, flashloan_mock); - let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); - - let mut leafs: Array = ArrayTrait::new(); - let mut leaf_index: u256 = 0; - - _add_vesu_flash_loan_leafs( - ref leafs, - ref leaf_index, - vault_allocator.contract_address, - simple_decoder_and_sanitizer, - manager.contract_address, - underlying, - false, - ); - - _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); - - let tree = generate_merkle_tree(leafs.span()); - - let root = *tree.at(tree.len() - 1).at(0); - cheat_caller_address_once(vault_allocator.contract_address, OWNER()); - vault_allocator.set_manager(manager.contract_address); - - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(STRATEGIST(), root); - - // Since the manager calls to itself to fulfill the flashloan, we need to set its root. - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(manager.contract_address, root); - - let mut array_of_decoders_and_sanitizers = ArrayTrait::new(); - array_of_decoders_and_sanitizers.append(simple_decoder_and_sanitizer); - - let mut array_of_targets = ArrayTrait::new(); - array_of_targets.append(manager.contract_address); - - let mut array_of_selectors = ArrayTrait::new(); - array_of_selectors.append(selector!("flash_loan")); - - let mut array_of_calldatas = ArrayTrait::new(); - let mut array_of_calldatas_flash_loan = ArrayTrait::new(); - manager.contract_address.serialize(ref array_of_calldatas_flash_loan); - underlying.serialize(ref array_of_calldatas_flash_loan); - WAD.serialize(ref array_of_calldatas_flash_loan); - false.serialize(ref array_of_calldatas_flash_loan); - - let mut flash_loan_data_proofs: Array> = ArrayTrait::new(); - let mut flash_loan_data_decoder_and_sanitizer: Array = ArrayTrait::new(); - let mut flash_loan_data_target: Array = ArrayTrait::new(); - let mut flash_loan_data_selector: Array = ArrayTrait::new(); - let mut flash_loan_data_calldata: Array> = ArrayTrait::new(); - let mut serialized_flash_loan_data = ArrayTrait::new(); - ( - flash_loan_data_proofs.span(), - flash_loan_data_decoder_and_sanitizer.span(), - flash_loan_data_target.span(), - flash_loan_data_selector.span(), - flash_loan_data_calldata.span(), - ) - .serialize(ref serialized_flash_loan_data); - - serialized_flash_loan_data.span().serialize(ref array_of_calldatas_flash_loan); - array_of_calldatas.append(array_of_calldatas_flash_loan.span()); - - let mut manage_leafs: Array = ArrayTrait::new(); - manage_leafs.append(leafs.at(0).clone()); - - let proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); - - IFlashLoanSingletonMockDispatcher { contract_address: flashloan_mock }.set_do_nothing(true); - - cheat_caller_address_once(manager.contract_address, STRATEGIST()); - manager - .manage_vault_with_merkle_verification( - proofs.span(), - array_of_decoders_and_sanitizers.span(), - array_of_targets.span(), - array_of_selectors.span(), - array_of_calldatas.span(), - ); -} - -#[test] -#[should_panic(expected: "Bad flash loan intent hash")] -fn test_flash_loan_bad_flash_loan_intent_hash() { - let vault_allocator = deploy_vault_allocator(); - let flashloan_mock = deploy_flashloan_mock(); - let underlying = deploy_erc20_mock(); - let underlying_disp = ERC20ABIDispatcher { contract_address: underlying }; - cheat_caller_address_once(underlying, OWNER()); - underlying_disp.transfer(flashloan_mock, WAD * 10); - let manager = deploy_manager(vault_allocator, flashloan_mock); - let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); - - let mut leafs: Array = ArrayTrait::new(); - let mut leaf_index: u256 = 0; - - _add_vesu_flash_loan_leafs( - ref leafs, - ref leaf_index, - vault_allocator.contract_address, - simple_decoder_and_sanitizer, - manager.contract_address, - underlying, - false, - ); - - _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); - - let tree = generate_merkle_tree(leafs.span()); - - let root = *tree.at(tree.len() - 1).at(0); - cheat_caller_address_once(vault_allocator.contract_address, OWNER()); - vault_allocator.set_manager(manager.contract_address); - - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(STRATEGIST(), root); - - // Since the manager calls to itself to fulfill the flashloan, we need to set its root. - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(manager.contract_address, root); - - let mut array_of_decoders_and_sanitizers = ArrayTrait::new(); - array_of_decoders_and_sanitizers.append(simple_decoder_and_sanitizer); - - let mut array_of_targets = ArrayTrait::new(); - array_of_targets.append(manager.contract_address); - - let mut array_of_selectors = ArrayTrait::new(); - array_of_selectors.append(selector!("flash_loan")); - - let mut array_of_calldatas = ArrayTrait::new(); - let mut array_of_calldatas_flash_loan = ArrayTrait::new(); - manager.contract_address.serialize(ref array_of_calldatas_flash_loan); - underlying.serialize(ref array_of_calldatas_flash_loan); - WAD.serialize(ref array_of_calldatas_flash_loan); - false.serialize(ref array_of_calldatas_flash_loan); - - let mut flash_loan_data_proofs: Array> = ArrayTrait::new(); - let mut flash_loan_data_decoder_and_sanitizer: Array = ArrayTrait::new(); - let mut flash_loan_data_target: Array = ArrayTrait::new(); - let mut flash_loan_data_selector: Array = ArrayTrait::new(); - let mut flash_loan_data_calldata: Array> = ArrayTrait::new(); - let mut serialized_flash_loan_data = ArrayTrait::new(); - ( - flash_loan_data_proofs.span(), - flash_loan_data_decoder_and_sanitizer.span(), - flash_loan_data_target.span(), - flash_loan_data_selector.span(), - flash_loan_data_calldata.span(), - ) - .serialize(ref serialized_flash_loan_data); - - serialized_flash_loan_data.span().serialize(ref array_of_calldatas_flash_loan); - array_of_calldatas.append(array_of_calldatas_flash_loan.span()); - - let mut manage_leafs: Array = ArrayTrait::new(); - manage_leafs.append(leafs.at(0).clone()); - - let proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); - - IFlashLoanSingletonMockDispatcher { contract_address: flashloan_mock } - .set_do_wrong_callback(true); - - cheat_caller_address_once(manager.contract_address, STRATEGIST()); - manager - .manage_vault_with_merkle_verification( - proofs.span(), - array_of_decoders_and_sanitizers.span(), - array_of_targets.span(), - array_of_selectors.span(), - array_of_calldatas.span(), - ); -} - -#[test] -fn test_flash_loan_bad_flash_loan() { - let vault_allocator = deploy_vault_allocator(); - let flashloan_mock = deploy_flashloan_mock(); - let underlying = deploy_erc20_mock(); - let underlying_disp = ERC20ABIDispatcher { contract_address: underlying }; - cheat_caller_address_once(underlying, OWNER()); - underlying_disp.transfer(flashloan_mock, WAD * 10); - let manager = deploy_manager(vault_allocator, flashloan_mock); - let simple_decoder_and_sanitizer = deploy_simple_decoder_and_sanitizer(); - - let mut leafs: Array = ArrayTrait::new(); - let mut leaf_index: u256 = 0; - - _add_vesu_flash_loan_leafs( - ref leafs, - ref leaf_index, - vault_allocator.contract_address, - simple_decoder_and_sanitizer, - manager.contract_address, - underlying, - false, - ); - - let amount_flash_loan = WAD; - - // approve the mock flashloan token to spend underlying - - let mut argument_addresses_approve = ArrayTrait::new(); - flashloan_mock.serialize(ref argument_addresses_approve); - leafs - .append( - ManageLeaf { - decoder_and_sanitizer: simple_decoder_and_sanitizer, - target: underlying, - selector: selector!("approve"), - argument_addresses: argument_addresses_approve.span(), - description: "", - }, - ); - - leaf_index += 1; - - // do something function called approve from the mock flashloan - let mut argument_addresses_fake_approve_func = ArrayTrait::new(); - underlying.serialize(ref argument_addresses_fake_approve_func); - leafs - .append( - ManageLeaf { - decoder_and_sanitizer: simple_decoder_and_sanitizer, - target: flashloan_mock, - selector: selector!("approve"), - argument_addresses: argument_addresses_fake_approve_func.span(), - description: "Approve", - }, - ); - leaf_index += 1; - - _pad_leafs_to_power_of_two(ref leafs, ref leaf_index); - - let tree = generate_merkle_tree(leafs.span()); - - let root = *tree.at(tree.len() - 1).at(0); - cheat_caller_address_once(vault_allocator.contract_address, OWNER()); - vault_allocator.set_manager(manager.contract_address); - - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(STRATEGIST(), root); - - // Since the manager calls to itself to fulfill the flashloan, we need to set its root. - cheat_caller_address_once(manager.contract_address, OWNER()); - manager.set_manage_root(manager.contract_address, root); - - let mut array_of_decoders_and_sanitizers = ArrayTrait::new(); - array_of_decoders_and_sanitizers.append(simple_decoder_and_sanitizer); - - let mut array_of_targets = ArrayTrait::new(); - array_of_targets.append(manager.contract_address); - - let mut array_of_selectors = ArrayTrait::new(); - array_of_selectors.append(selector!("flash_loan")); - - let mut array_of_calldatas = ArrayTrait::new(); - let mut array_of_calldatas_flash_loan = ArrayTrait::new(); - manager.contract_address.serialize(ref array_of_calldatas_flash_loan); - underlying.serialize(ref array_of_calldatas_flash_loan); - WAD.serialize(ref array_of_calldatas_flash_loan); - false.serialize(ref array_of_calldatas_flash_loan); - - /// construct the flash loan data to trigger do_something from the flashloan mock - let mut flash_loan_data_decoder_and_sanitizer: Array = ArrayTrait::new(); - flash_loan_data_decoder_and_sanitizer.append(simple_decoder_and_sanitizer); - flash_loan_data_decoder_and_sanitizer.append(simple_decoder_and_sanitizer); - - let mut flash_loan_data_target: Array = ArrayTrait::new(); - flash_loan_data_target.append(underlying); - flash_loan_data_target.append(flashloan_mock); - - let mut flash_loan_data_selector: Array = ArrayTrait::new(); - flash_loan_data_selector.append(selector!("approve")); - flash_loan_data_selector.append(selector!("approve")); - - let mut flash_loan_data_calldata: Array> = ArrayTrait::new(); - let mut flash_loan_data_calldata_approve = ArrayTrait::new(); - flashloan_mock.serialize(ref flash_loan_data_calldata_approve); - amount_flash_loan.serialize(ref flash_loan_data_calldata_approve); - - let mut flash_loan_data_calldata_approve_fake = ArrayTrait::new(); - underlying.serialize(ref flash_loan_data_calldata_approve_fake); - amount_flash_loan.serialize(ref flash_loan_data_calldata_approve_fake); - flash_loan_data_calldata.append(flash_loan_data_calldata_approve.span()); - flash_loan_data_calldata.append(flash_loan_data_calldata_approve_fake.span()); - - let mut flash_loan_manager_leafs: Array = ArrayTrait::new(); - flash_loan_manager_leafs.append(leafs.at(1).clone()); - flash_loan_manager_leafs.append(leafs.at(2).clone()); - - let mut flash_loan_proofs = _get_proofs_using_tree(flash_loan_manager_leafs, tree.clone()); - - let mut serialized_flash_loan_data = ArrayTrait::new(); - ( - flash_loan_proofs.span(), - flash_loan_data_decoder_and_sanitizer.span(), - flash_loan_data_target.span(), - flash_loan_data_selector.span(), - flash_loan_data_calldata.span(), - ) - .serialize(ref serialized_flash_loan_data); - - serialized_flash_loan_data.span().serialize(ref array_of_calldatas_flash_loan); - array_of_calldatas.append(array_of_calldatas_flash_loan.span()); - - let mut manage_leafs: Array = ArrayTrait::new(); - manage_leafs.append(leafs.at(0).clone()); - - let proofs = _get_proofs_using_tree(manage_leafs, tree.clone()); - - cheat_caller_address_once(manager.contract_address, STRATEGIST()); - manager - .manage_vault_with_merkle_verification( - proofs.span(), - array_of_decoders_and_sanitizers.span(), - array_of_targets.span(), - array_of_selectors.span(), - array_of_calldatas.span(), - ); - assert( - IFlashLoanSingletonMockDispatcher { contract_address: flashloan_mock }.i_did_something(), - 'i_did_something is not true', - ); -} diff --git a/packages/vault_allocator/src/test/utils.cairo b/packages/vault_allocator/src/test/utils.cairo index d000d071..050deec2 100644 --- a/packages/vault_allocator/src/test/utils.cairo +++ b/packages/vault_allocator/src/test/utils.cairo @@ -2,32 +2,22 @@ // Copyright (c) 2025 Starknet Vault Kit // Licensed under the MIT License. See LICENSE file for details. -use core::hash::HashStateTrait; -use core::num::traits::Zero; -use core::pedersen::PedersenTrait; -use openzeppelin::interfaces::erc4626::{IERC4626Dispatcher, IERC4626DispatcherTrait}; use openzeppelin::merkle_tree::hashes::PedersenCHasher; use snforge_std::{CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare}; -use starknet::syscalls::call_contract_syscall; use starknet::{ClassHash, ContractAddress}; -use vault_allocator::integration_interfaces::vesu::{ - IDefaultExtensionPOV2Dispatcher, IDefaultExtensionPOV2DispatcherTrait, ISingletonV2Dispatcher, - ISingletonV2DispatcherTrait, -}; use vault_allocator::manager::interface::IManagerDispatcher; +use vault_allocator::merkle_tree::registery::{ + DAI, DAI_PRAGMA_ID, ETH, ETH_PRAGMA_ID, PRAGMA, STRK, STRK_PRAGMA_ID, USDC, USDC_PRAGMA_ID, + USDT, USDT_PRAGMA_ID, WBTC, WBTC_PRAGMA_ID, wstETH, wstETH_PRAGMA_ID, +}; use vault_allocator::mocks::counter::ICounterDispatcher; +use vault_allocator::mocks::vault::MockVault::MockVaultTraitDispatcher; use vault_allocator::periphery::price_router::interface::{ IPriceRouterDispatcher, IPriceRouterDispatcherTrait, }; -use vault_allocator::test::register::{ - AVNU_ROUTER, DAI, DAI_PRAGMA_ID, ETH, ETH_PRAGMA_ID, PRAGMA, STRK, STRK_PRAGMA_ID, USDC, - USDC_PRAGMA_ID, USDT, USDT_PRAGMA_ID, VESU_SINGLETON, WBTC, WBTC_PRAGMA_ID, wstETH, - wstETH_PRAGMA_ID, -}; use vault_allocator::vault_allocator::interface::IVaultAllocatorDispatcher; pub const WAD: u256 = 1_000_000_000_000_000_000; pub const INITIAL_SLIPPAGE_BPS: u256 = 100; // 1% -use core::to_byte_array::FormatAsByteArray; pub fn OWNER() -> ContractAddress { @@ -55,14 +45,11 @@ pub fn deploy_vault_allocator() -> IVaultAllocatorDispatcher { IVaultAllocatorDispatcher { contract_address: vault_allocator_address } } -pub fn deploy_manager( - vault_allocator: IVaultAllocatorDispatcher, vesu_singleton: ContractAddress, -) -> IManagerDispatcher { +pub fn deploy_manager(vault_allocator: IVaultAllocatorDispatcher) -> IManagerDispatcher { let manager = declare("Manager").unwrap().contract_class(); let mut calldata = ArrayTrait::new(); OWNER().serialize(ref calldata); vault_allocator.contract_address.serialize(ref calldata); - vesu_singleton.serialize(ref calldata); let (manager_address, _) = manager.deploy(@calldata).unwrap(); IManagerDispatcher { contract_address: manager_address } } @@ -76,12 +63,6 @@ pub fn deploy_counter() -> (ICounterDispatcher, ClassHash) { (ICounterDispatcher { contract_address: counter_address }, *counter.class_hash) } -pub fn deploy_flashloan_mock() -> ContractAddress { - let flashloan = declare("FlashLoanSingletonMock").unwrap().contract_class(); - let mut calldata = ArrayTrait::new(); - let (flashloan_address, _) = flashloan.deploy(@calldata).unwrap(); - flashloan_address -} pub fn deploy_erc4626_mock(underlying: ContractAddress) -> ContractAddress { let erc4626 = declare("Erc4626Mock").unwrap().contract_class(); @@ -101,6 +82,14 @@ pub fn deploy_erc20_mock() -> ContractAddress { erc20_address } +pub fn deploy_mock_vault(underlying: ContractAddress) -> MockVaultTraitDispatcher { + let mock_vault = declare("MockVault").unwrap().contract_class(); + let mut calldata = ArrayTrait::new(); + underlying.serialize(ref calldata); + let (mock_vault_address, _) = mock_vault.deploy(@calldata).unwrap(); + MockVaultTraitDispatcher { contract_address: mock_vault_address } +} + pub fn deploy_simple_decoder_and_sanitizer() -> ContractAddress { let simple_decoder_and_sanitizer = declare("SimpleDecoderAndSanitizer") .unwrap() @@ -145,13 +134,21 @@ pub fn initialize_price_router(price_router: ContractAddress) { price_router.set_asset_to_id(DAI(), DAI_PRAGMA_ID()); } -pub fn deploy_avnu_middleware(price_router: ContractAddress) -> ContractAddress { +pub fn deploy_avnu_middleware( + vault_allocator: ContractAddress, + price_router: ContractAddress, + slippage: u16, + period: u64, + allowed_calls_per_period: u64, +) -> ContractAddress { let avnu_middleware = declare("AvnuMiddleware").unwrap().contract_class(); let mut calldata = ArrayTrait::new(); OWNER().serialize(ref calldata); - AVNU_ROUTER().serialize(ref calldata); + vault_allocator.serialize(ref calldata); price_router.serialize(ref calldata); - INITIAL_SLIPPAGE_BPS.serialize(ref calldata); + slippage.serialize(ref calldata); + period.serialize(ref calldata); + allowed_calls_per_period.serialize(ref calldata); let (avnu_middleware_address, _) = avnu_middleware.deploy(@calldata).unwrap(); avnu_middleware_address } @@ -163,526 +160,3 @@ pub fn cheat_caller_address_once( cheat_caller_address(:contract_address, :caller_address, span: CheatSpan::TargetCalls(1)); } - -#[derive(PartialEq, Drop, Serde, Debug, Clone)] -pub struct ManageLeaf { - pub decoder_and_sanitizer: ContractAddress, - pub target: ContractAddress, - pub selector: felt252, - pub argument_addresses: Span, - pub description: ByteArray, -} - -pub fn get_leaf_hash(leaf: ManageLeaf) -> felt252 { - let mut serialized_struct: Array = ArrayTrait::new(); - leaf.decoder_and_sanitizer.serialize(ref serialized_struct); - leaf.target.serialize(ref serialized_struct); - leaf.selector.serialize(ref serialized_struct); - leaf.argument_addresses.serialize(ref serialized_struct); - let first_element = serialized_struct.pop_front().unwrap(); - let mut state = PedersenTrait::new(first_element); - while let Some(value) = serialized_struct.pop_front() { - state = state.update(value); - } - state.finalize() -} - -pub fn generate_merkle_tree(manage_leafs: Span) -> Array> { - let mut first_layer = ArrayTrait::new(); - let leafs_length = manage_leafs.len(); - for i in 0..leafs_length { - first_layer.append(get_leaf_hash(manage_leafs.at(i).clone())); - } - let mut leafs = ArrayTrait::new(); - leafs.append(first_layer); - _build_tree(leafs) -} - -pub fn _build_tree(merkle_tree_in: Array>) -> Array> { - let merkle_tree_in_length = merkle_tree_in.len(); - - let mut current_layer_index = merkle_tree_in_length - 1; - let current_layer_length = merkle_tree_in[current_layer_index].len(); - let mut next_layer_length = 0; - if (current_layer_length % 2 != 0) { - next_layer_length = (current_layer_length + 1) / 2; - } else { - next_layer_length = current_layer_length / 2; - } - let mut current_layer = ArrayTrait::new(); - let mut count = 0; - let mut i = 0; - while i < current_layer_length { - current_layer - .append( - PedersenCHasher::commutative_hash( - *merkle_tree_in[current_layer_index].at(i), - *merkle_tree_in[current_layer_index].at(i + 1), - ), - ); - count += 1; - i += 2; - } - - let mut merkle_tree_out = merkle_tree_in.clone(); - merkle_tree_out.append(current_layer); - - if (next_layer_length > 1) { - _build_tree(merkle_tree_out) - } else { - merkle_tree_out - } -} - -fn _next_power_of_two(x: u256) -> u256 { - let mut power = 1_u256; - while power < x { - power = power * 2_u256; - } - power -} -// pub fn _pad_leafs_to_power_of_two(ref leafs: Array, ref leaf_index: u256) { -// let next_power = _next_power_of_two(leaf_index); -// let padding_needed = next_power - leaf_index; - -// let default_leaf = ManageLeaf { -// decoder_and_sanitizer: Zero::zero(), -// target: Zero::zero(), -// selector: Zero::zero(), -// argument_addresses: ArrayTrait::new().span(), -// }; - -// let mut i = 0; -// while i < padding_needed { -// leafs.append(default_leaf); -// leaf_index += 1; -// i += 1; -// } -// } - -pub fn _pad_leafs_to_power_of_two(ref leafs: Array, ref leaf_index: u256) { - let target_len = if leaf_index < 4_u256 { - 4_u256 - } else { - _next_power_of_two(leaf_index) - }; - let padding_needed = target_len - leaf_index; - - let mut i: u256 = 0_u256; - while i < padding_needed { - leafs - .append( - ManageLeaf { - decoder_and_sanitizer: Zero::zero(), - target: Zero::zero(), - selector: Zero::zero(), - argument_addresses: ArrayTrait::new().span(), - description: "", - }, - ); - leaf_index += 1_u256; - i += 1_u256; - } -} - - -pub fn _get_proofs_using_tree( - leafs: Array, tree: Array>, -) -> Array> { - let mut proofs = ArrayTrait::new(); - for i in 0..leafs.len() { - let leaf = leafs.at(i); - let mut serialized_struct: Array = ArrayTrait::new(); - leaf.decoder_and_sanitizer.serialize(ref serialized_struct); - leaf.target.serialize(ref serialized_struct); - leaf.selector.serialize(ref serialized_struct); - leaf.argument_addresses.serialize(ref serialized_struct); - let first_element = serialized_struct.pop_front().unwrap(); - let mut state = PedersenTrait::new(first_element); - - while let Some(value) = serialized_struct.pop_front() { - state = state.update(value); - } - let leaf_hash = state.finalize(); - let proof = _generate_proof(leaf_hash, tree.clone()); - proofs.append(proof); - } - proofs -} - -pub fn _generate_proof(mut leaf: felt252, tree: Array>) -> Span { - let tree_length = tree.len(); - let mut proof = ArrayTrait::new(); - for i in 0..tree_length - 1 { - let tree_current_layer = tree.at(i); - let tree_current_layer_length = tree_current_layer.len(); - for j in 0..tree_current_layer_length { - if leaf == *tree_current_layer.at(j) { - let element_to_append = if j % 2 == 0 { - *tree_current_layer.at(j + 1) - } else { - *tree_current_layer.at(j - 1) - }; - leaf = PedersenCHasher::commutative_hash(leaf, element_to_append); - proof.append(element_to_append); - break; - } else { - assert(j != tree_current_layer_length - 1, 'leaf not found in tree'); - } - } - } - proof.span() -} - - -// ========================================= ERC4626 ========================================= - -pub fn _add_erc4626_leafs( - ref leafs: Array, - ref leaf_index: u256, - vault: ContractAddress, - decoder_and_sanitizer: ContractAddress, - erc4626: ContractAddress, -) { - let erc4626_erc4646_disp = IERC4626Dispatcher { contract_address: erc4626 }; - let asset = erc4626_erc4646_disp.asset(); - - // Approvals - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: asset, - selector: selector!("approve"), - argument_addresses: array![erc4626.into()].span(), - description: "Approve" - + " " - + get_symbol(erc4626) - + " " - + "to spend" - + " " - + get_symbol(asset), - }, - ); - leaf_index += 1; - - // Deposits - - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: erc4626, - selector: selector!("deposit"), - argument_addresses: array![vault.into()].span(), - description: "Deposit" - + " " - + get_symbol(asset) - + " " - + "for" - + " " - + get_symbol(erc4626), - }, - ); - leaf_index += 1; - - // Withdrawals - - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: erc4626, - selector: selector!("withdraw"), - argument_addresses: array![vault.into(), vault.into()].span(), - description: "Withdraw" - + " " - + get_symbol(asset) - + " " - + "from" - + " " - + get_symbol(erc4626), - }, - ); - leaf_index += 1; - - // Minting - - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: erc4626, - selector: selector!("mint"), - argument_addresses: array![vault.into()].span(), - description: "Mint" - + " " - + get_symbol(erc4626) - + " " - + "from" - + " " - + get_symbol(asset), - }, - ); - leaf_index += 1; - - // Redeeming - - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: erc4626, - selector: selector!("redeem"), - argument_addresses: array![vault.into(), vault.into()].span(), - description: "Redeem" - + " " - + get_symbol(erc4626) - + " " - + "for" - + " " - + get_symbol(asset), - }, - ); - leaf_index += 1; -} - - -// ========================================= VESU ========================================= - -pub fn _add_vesu_leafs( - ref leafs: Array, - ref leaf_index: u256, - vault: ContractAddress, - decoder_and_sanitizer: ContractAddress, - pool_id: felt252, - collateral_assets: Span, - debt_assets_per_collateral_asset: Span>, -) { - assert(collateral_assets.len() == debt_assets_per_collateral_asset.len(), 'inconsistent len'); - let singleton = ISingletonV2Dispatcher { contract_address: VESU_SINGLETON() }; - let pool_extension = singleton.extension(pool_id); - assert(pool_extension != Zero::zero(), 'pool extension not found'); - let pool_extension = IDefaultExtensionPOV2Dispatcher { contract_address: pool_extension }; - - for i in 0..collateral_assets.len() { - let collateral_asset = *collateral_assets.at(i); - let debt_assets = *debt_assets_per_collateral_asset.at(i); - - let v_token = pool_extension.v_token_for_collateral_asset(pool_id, collateral_asset); - assert(v_token != Zero::zero(), 'v token not found'); - - // earn mode - _add_erc4626_leafs(ref leafs, ref leaf_index, vault, decoder_and_sanitizer, v_token); - - // debt mode - let debt_assets_len = debt_assets.len(); - if debt_assets_len > 0 { - for j in 0..debt_assets_len { - let mut pool_id_str: ByteArray = FormatAsByteArray::format_as_byte_array( - @pool_id, 16, - ); - - // APPROVAL of collateral asset to the singleton - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: collateral_asset, - selector: selector!("approve"), - argument_addresses: array![VESU_SINGLETON().into()].span(), - description: "Approve" - + " " - + "singleton" - + "_" - + pool_id_str.clone() - + " " - + "to spend" - + " " - + get_symbol(collateral_asset), - }, - ); - leaf_index += 1; - - let debt_asset = *debt_assets.at(j); - - // MODIFY POSITION - let mut argument_addresses_modify_position = ArrayTrait::new(); - - // pool_id - pool_id.serialize(ref argument_addresses_modify_position); - - // collateral_asset - collateral_asset.serialize(ref argument_addresses_modify_position); - - // debt_asset - debt_asset.serialize(ref argument_addresses_modify_position); - - // user - vault.serialize(ref argument_addresses_modify_position); - - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: singleton.contract_address, - selector: selector!("modify_position"), - argument_addresses: argument_addresses_modify_position.span(), - description: "Modify position" - + " " - + "extension_pid" - + "_" - + pool_id_str - + " " - + "with collateral" - + " " - + get_symbol(collateral_asset) - + " " - + "and debt" - + " " - + get_symbol(debt_asset), - }, - ); - leaf_index += 1; - } - } - } -} - - -pub fn _add_vesu_flash_loan_leafs( - ref leafs: Array, - ref leaf_index: u256, - vault: ContractAddress, - decoder_and_sanitizer: ContractAddress, - manager: ContractAddress, - asset: ContractAddress, - is_legacy: bool, -) { - let mut argument_addresses = ArrayTrait::new(); - manager.serialize(ref argument_addresses); - asset.serialize(ref argument_addresses); - is_legacy.serialize(ref argument_addresses); - - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: manager, - selector: selector!("flash_loan"), - argument_addresses: argument_addresses.span(), - description: "Flash loan" + " " + get_symbol(asset), - }, - ); - leaf_index += 1; -} - -// ========================================= AVNU ========================================= -pub fn _add_avnu_leafs( - ref leafs: Array, - ref leaf_index: u256, - vault: ContractAddress, - decoder_and_sanitizer: ContractAddress, - router: ContractAddress, - sell_and_buy_token_address: Array<(ContractAddress, ContractAddress)>, -) { - let mut seen_sells: Array = ArrayTrait::new(); - - for i in 0..sell_and_buy_token_address.len() { - let (sell_token_address, buy_token_address) = *sell_and_buy_token_address.at(i); - - if !_contains_address(seen_sells.span(), sell_token_address) { - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: sell_token_address, - selector: selector!("approve"), - argument_addresses: array![router.into()].span(), - description: "Approve" - + " " - + "avnu_router" - + " " - + "to spend" - + " " - + get_symbol(sell_token_address), - }, - ); - leaf_index += 1; - seen_sells.append(sell_token_address); - } - - // swap leaf à chaque paire - let mut argument_addresses = ArrayTrait::new(); - sell_token_address.serialize(ref argument_addresses); - buy_token_address.serialize(ref argument_addresses); - vault.serialize(ref argument_addresses); - - leafs - .append( - ManageLeaf { - decoder_and_sanitizer, - target: router, - selector: selector!("multi_route_swap"), - argument_addresses: argument_addresses.span(), - description: "Multi route swap" - + " " - + get_symbol(sell_token_address) - + " " - + "for" - + " " - + get_symbol(buy_token_address), - }, - ); - leaf_index += 1; - } -} - -fn _contains_address(span: Span, addr: ContractAddress) -> bool { - let mut i = 0; - while i < span.len() { - if *span.at(i) == addr { - return true; - } - i += 1; - } - false -} - - -fn get_symbol(contract_address: ContractAddress) -> ByteArray { - let ret_data = call_contract_syscall(contract_address, selector!("symbol"), array![].span()); - match ret_data { - Ok(res) => { - let res_len: u32 = res.len(); - if (res_len == 1) { - let symbol_felt = *res.at(0); - let mut symbol_byte_array: ByteArray = ""; - symbol_byte_array.append_word(symbol_felt, bytes_in_felt(symbol_felt)); - symbol_byte_array - } else { - let mut res_span = res; - Serde::::deserialize(ref res_span).unwrap() - } - }, - Err(revert_reason) => { panic!("revert_reason: {:?}", revert_reason); }, - } -} - - -fn bytes_in_felt(word: felt252) -> usize { - if word == 0 { - return 0; - } - let x: u256 = word.try_into().unwrap(); - - let mut p: u256 = 1_u256; - let mut bytes: usize = 0; - - while p <= x && bytes < 31 { - p = p * 256_u256; - bytes += 1; - } - - bytes -} diff --git a/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo b/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo index 54be9608..4557ad71 100644 --- a/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo +++ b/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo @@ -2,10 +2,20 @@ // Copyright (c) 2025 Starknet Vault Kit // Licensed under the MIT License. See LICENSE file for details. +#[starknet::interface] +pub trait IVaultMigration { + fn bring_liquidity(ref self: TContractState, amount: u256); +} + + #[starknet::contract] -pub mod VaultAllocator { +pub mod VaultAllocatorMigration { use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::interfaces::erc4626::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; + use openzeppelin::interfaces::erc721::{ERC721ReceiverMixin, IERC721_RECEIVER_ID}; use openzeppelin::interfaces::upgrades::IUpgradeable; + use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; use starknet::account::Call; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; @@ -13,12 +23,16 @@ pub mod VaultAllocator { use starknet::{ContractAddress, SyscallResultTrait, get_caller_address}; use vault_allocator::vault_allocator::errors::Errors; use vault_allocator::vault_allocator::interface::IVaultAllocator; + use super::{IVaultMigrationDispatcher, IVaultMigrationDispatcherTrait}; + component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); #[storage] struct Storage { + #[substorage(v0)] + src5: SRC5Component::Storage, #[substorage(v0)] ownable: OwnableComponent::Storage, #[substorage(v0)] @@ -29,7 +43,11 @@ pub mod VaultAllocator { #[event] #[derive(Drop, starknet::Event)] pub enum Event { + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] OwnableEvent: OwnableComponent::Event, + #[flat] UpgradeableEvent: UpgradeableComponent::Event, CallPerformed: CallPerformed, } @@ -55,6 +73,9 @@ pub mod VaultAllocator { impl OwnableInternalImpl = OwnableComponent::InternalImpl; impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + impl SRC5Impl = SRC5Component::SRC5Impl; + impl SRC5InternalImpl = SRC5Component::InternalImpl; + #[abi(embed_v0)] impl UpgradeableImpl of IUpgradeable { fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { @@ -92,6 +113,40 @@ pub mod VaultAllocator { } } + #[abi(embed_v0)] + fn bring_liquidity(ref self: ContractState, vault: ContractAddress, amount: u256) { + self.ownable.assert_only_owner(); + let underlying_asset = ERC4626ABIDispatcher { contract_address: vault }.asset(); + ERC20ABIDispatcher { contract_address: underlying_asset }.approve(vault, amount); + IVaultMigrationDispatcher { contract_address: vault }.bring_liquidity(amount); + } + + #[abi(embed_v0)] + impl ERC721ReceiverMixinImpl of ERC721ReceiverMixin { + fn on_erc721_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span, + ) -> felt252 { + IERC721_RECEIVER_ID + } + fn onERC721Received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + data: Span, + ) -> felt252 { + IERC721_RECEIVER_ID + } + + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + self.src5.supports_interface(interface_id) + } + } + #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { @@ -107,7 +162,9 @@ pub mod VaultAllocator { selector: felt252, calldata: Span, ) -> Span { + self.src5.register_interface(IERC721_RECEIVER_ID); let result = call_contract_syscall(to, selector, calldata).unwrap_syscall(); + self.src5.deregister_interface(IERC721_RECEIVER_ID); self.emit(CallPerformed { to, selector, calldata, result }); result } From e8e1f51d5ab4f049800960e90d2d08c0f201458d Mon Sep 17 00:00:00 2001 From: akiraonstarknet Date: Thu, 23 Oct 2025 15:03:53 +0800 Subject: [PATCH 2/2] actual audited diff --- packages/vault/src/lib.cairo | 1 - .../vault/src/redeem_request/errors.cairo | 4 - .../src/redeem_request/redeem_request.cairo | 115 +--------------- .../vault/src/test/mock_erc721_receiver.cairo | 60 --------- .../vault/src/test/units/redeem_request.cairo | 16 +-- packages/vault/src/test/units/vault.cairo | 124 +++++++++++++++++- packages/vault/src/test/utils.cairo | 9 -- packages/vault/src/vault/interface.cairo | 4 +- packages/vault/src/vault/vault.cairo | 124 ++++++++---------- .../simple_decoder_and_sanitizer.cairo | 15 +++ ...knet_vault_kit_decoder_and_sanitizer.cairo | 1 + .../vault_allocator/src/manager/manager.cairo | 4 - .../avnu_middleware/avnu_middleware.cairo | 5 +- .../integrations/vault_bring_liquidity.cairo | 1 + .../src/vault_allocator/vault_allocator.cairo | 59 +-------- 15 files changed, 205 insertions(+), 337 deletions(-) delete mode 100644 packages/vault/src/test/mock_erc721_receiver.cairo diff --git a/packages/vault/src/lib.cairo b/packages/vault/src/lib.cairo index 742d79fc..12aa3603 100644 --- a/packages/vault/src/lib.cairo +++ b/packages/vault/src/lib.cairo @@ -28,7 +28,6 @@ pub mod redeem_request { #[cfg(test)] pub mod test { - pub mod mock_erc721_receiver; pub mod utils; pub mod units { pub mod redeem_request; diff --git a/packages/vault/src/redeem_request/errors.cairo b/packages/vault/src/redeem_request/errors.cairo index 693e6bf0..3798485f 100644 --- a/packages/vault/src/redeem_request/errors.cairo +++ b/packages/vault/src/redeem_request/errors.cairo @@ -10,8 +10,4 @@ pub mod Errors { pub fn not_vault_owner() { panic!("Caller is not vault owner"); } - - pub fn not_implemented() { - panic!("Not implemented"); - } } diff --git a/packages/vault/src/redeem_request/redeem_request.cairo b/packages/vault/src/redeem_request/redeem_request.cairo index d44fae23..acabc1e0 100644 --- a/packages/vault/src/redeem_request/redeem_request.cairo +++ b/packages/vault/src/redeem_request/redeem_request.cairo @@ -7,14 +7,8 @@ mod RedeemRequest { use openzeppelin::interfaces::accesscontrol::{ IAccessControlDispatcher, IAccessControlDispatcherTrait, }; - use openzeppelin::interfaces::accounts::ISRC6_ID; - use openzeppelin::interfaces::introspection::{ISRC5Dispatcher, ISRC5DispatcherTrait}; - use openzeppelin::interfaces::token::erc721::{ - ERC721ABI, IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID, - }; use openzeppelin::interfaces::upgrades::IUpgradeable; use openzeppelin::introspection::src5::SRC5Component; - use openzeppelin::token::erc721::ERC721Component::ERC721MixinImpl; use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent; use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; @@ -27,7 +21,6 @@ mod RedeemRequest { use vault::redeem_request::interface::{IRedeemRequest, RedeemRequestInfo}; use vault::vault::vault::Vault::OWNER_ROLE; - component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: ERC721Component, storage: erc721, event: ERC721Event); component!( @@ -35,6 +28,8 @@ mod RedeemRequest { ); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; #[abi(embed_v0)] impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl; @@ -77,96 +72,6 @@ mod RedeemRequest { self.vault.write(vault); } - #[abi(embed_v0)] - impl ERC721ABIImpl of ERC721ABI { - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - ERC721MixinImpl::balance_of(self, account) - } - fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { - ERC721MixinImpl::owner_of(self, token_id) - } - fn safe_transfer_from( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span, - ) { - ERC721MixinImpl::safe_transfer_from(ref self, from, to, token_id, data); - } - fn transfer_from( - ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, - ) { - Errors::not_implemented(); - } - fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { - ERC721MixinImpl::approve(ref self, to, token_id); - } - fn set_approval_for_all( - ref self: ContractState, operator: ContractAddress, approved: bool, - ) { - ERC721MixinImpl::set_approval_for_all(ref self, operator, approved); - } - fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { - ERC721MixinImpl::get_approved(self, token_id) - } - fn is_approved_for_all( - self: @ContractState, owner: ContractAddress, operator: ContractAddress, - ) -> bool { - ERC721MixinImpl::is_approved_for_all(self, owner, operator) - } - fn name(self: @ContractState) -> ByteArray { - ERC721MixinImpl::name(self) - } - fn symbol(self: @ContractState) -> ByteArray { - ERC721MixinImpl::symbol(self) - } - fn token_uri(self: @ContractState, token_id: u256) -> ByteArray { - ERC721MixinImpl::token_uri(self, token_id) - } - - fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { - ERC721MixinImpl::balance_of(self, account) - } - fn ownerOf(self: @ContractState, tokenId: u256) -> ContractAddress { - ERC721MixinImpl::owner_of(self, tokenId) - } - fn safeTransferFrom( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - tokenId: u256, - data: Span, - ) { - ERC721MixinImpl::safe_transfer_from(ref self, from, to, tokenId, data); - } - fn transferFrom( - ref self: ContractState, from: ContractAddress, to: ContractAddress, tokenId: u256, - ) { - Errors::not_implemented(); - } - - fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { - ERC721MixinImpl::set_approval_for_all(ref self, operator, approved); - } - fn getApproved(self: @ContractState, tokenId: u256) -> ContractAddress { - ERC721MixinImpl::get_approved(self, tokenId) - } - fn isApprovedForAll( - self: @ContractState, owner: ContractAddress, operator: ContractAddress, - ) -> bool { - ERC721MixinImpl::is_approved_for_all(self, owner, operator) - } - - fn tokenURI(self: @ContractState, tokenId: u256) -> ByteArray { - ERC721MixinImpl::token_uri(self, tokenId) - } - - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - ERC721MixinImpl::supports_interface(self, interface_id) - } - } - #[abi(embed_v0)] impl RedeemRequestImpl of IRedeemRequest { // ───────────────────────────────────────────────────────────────────── @@ -194,7 +99,7 @@ mod RedeemRequest { ) -> u256 { self._assert_vault(); let id = self.id_len.read(); - self.erc721.safe_mint(to, id, array![].span()); + self.erc721.mint(to, id); self.id_to_info.write(id, redeem_request_info); self.id_len.write(id + 1); id @@ -231,18 +136,4 @@ mod RedeemRequest { } } } - fn _check_on_erc721_received( - from: ContractAddress, to: ContractAddress, token_id: u256, data: Span, - ) -> bool { - let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; - - if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { - IERC721ReceiverDispatcher { contract_address: to } - .on_erc721_received( - get_caller_address(), from, token_id, data, - ) == IERC721_RECEIVER_ID - } else { - src5_dispatcher.supports_interface(ISRC6_ID) - } - } } diff --git a/packages/vault/src/test/mock_erc721_receiver.cairo b/packages/vault/src/test/mock_erc721_receiver.cairo deleted file mode 100644 index d488b832..00000000 --- a/packages/vault/src/test/mock_erc721_receiver.cairo +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2025 Starknet Vault Kit -// Licensed under the MIT License. See LICENSE file for details. - -#[starknet::contract] -pub mod MockERC721Receiver { - use openzeppelin::interfaces::erc721::{ERC721ReceiverMixin, IERC721_RECEIVER_ID}; - use openzeppelin::introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[storage] - struct Storage { - #[substorage(v0)] - src5: SRC5Component::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - pub enum Event { - #[flat] - SRC5Event: SRC5Component::Event, - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.src5.register_interface(IERC721_RECEIVER_ID); - } - - impl SRC5Impl = SRC5Component::SRC5Impl; - impl SRC5InternalImpl = SRC5Component::InternalImpl; - - #[abi(embed_v0)] - impl ERC721ReceiverMixinImpl of ERC721ReceiverMixin { - fn on_erc721_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span, - ) -> felt252 { - IERC721_RECEIVER_ID - } - - fn onERC721Received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span, - ) -> felt252 { - IERC721_RECEIVER_ID - } - - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - self.src5.supports_interface(interface_id) - } - } -} diff --git a/packages/vault/src/test/units/redeem_request.cairo b/packages/vault/src/test/units/redeem_request.cairo index c4d26343..01e11b49 100644 --- a/packages/vault/src/test/units/redeem_request.cairo +++ b/packages/vault/src/test/units/redeem_request.cairo @@ -13,7 +13,7 @@ use vault::redeem_request::interface::{ }; use vault::test::utils::{ DUMMY_ADDRESS, OTHER_DUMMY_ADDRESS, OWNER, cheat_caller_address_once, deploy_counter, - deploy_erc20_mock, deploy_erc721_receiver_at, deploy_redeem_request, deploy_vault, + deploy_erc20_mock, deploy_redeem_request, deploy_vault, }; use vault::vault::vault::Vault; use vault_allocator::mocks::counter::{ICounterDispatcher, ICounterDispatcherTrait}; @@ -22,8 +22,6 @@ fn set_up() -> (ContractAddress, IRedeemRequestDispatcher) { let underlying_assets = deploy_erc20_mock(); let vault = deploy_vault(underlying_assets); let redeem_request = deploy_redeem_request(vault.contract_address); - deploy_erc721_receiver_at(DUMMY_ADDRESS()); - deploy_erc721_receiver_at(OTHER_DUMMY_ADDRESS()); (vault.contract_address, redeem_request) } @@ -269,8 +267,7 @@ fn test_erc721_functionality() { assert(erc721_dispatcher.get_approved(id) == OTHER_DUMMY_ADDRESS(), 'Approved incorrect'); cheat_caller_address_once(redeem_request.contract_address, OTHER_DUMMY_ADDRESS()); - erc721_dispatcher - .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id, array![].span()); + erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id); assert(erc721_dispatcher.owner_of(id) == OTHER_DUMMY_ADDRESS(), 'Owner after transfer'); assert(erc721_dispatcher.balance_of(DUMMY_ADDRESS()) == 0, 'Balance after transfer'); @@ -302,12 +299,10 @@ fn test_set_approval_for_all() { ); cheat_caller_address_once(redeem_request.contract_address, OTHER_DUMMY_ADDRESS()); - erc721_dispatcher - .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1, array![].span()); + erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1); cheat_caller_address_once(redeem_request.contract_address, OTHER_DUMMY_ADDRESS()); - erc721_dispatcher - .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_2, array![].span()); + erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_2); assert(erc721_dispatcher.balance_of(DUMMY_ADDRESS()) == 0, 'Original owner balance'); assert(erc721_dispatcher.balance_of(OTHER_DUMMY_ADDRESS()) == 2, 'New owner balance'); @@ -334,8 +329,7 @@ fn test_complex_scenario_mint_burn_transfer() { assert(redeem_request.id_len() == 2, 'Initial ID length'); cheat_caller_address_once(redeem_request.contract_address, DUMMY_ADDRESS()); - erc721_dispatcher - .safe_transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1, array![].span()); + erc721_dispatcher.transfer_from(DUMMY_ADDRESS(), OTHER_DUMMY_ADDRESS(), id_1); assert(erc721_dispatcher.owner_of(id_1) == OTHER_DUMMY_ADDRESS(), 'ID 1 new owner'); assert(erc721_dispatcher.balance_of(DUMMY_ADDRESS()) == 1, 'Balance after transfer'); diff --git a/packages/vault/src/test/units/vault.cairo b/packages/vault/src/test/units/vault.cairo index 291cc308..c02ad023 100644 --- a/packages/vault/src/test/units/vault.cairo +++ b/packages/vault/src/test/units/vault.cairo @@ -25,8 +25,8 @@ use vault::redeem_request::interface::{IRedeemRequestDispatcher, IRedeemRequestD use vault::test::utils::{ DUMMY_ADDRESS, FEES_RECIPIENT, MANAGEMENT_FEES, MAX_DELTA, ORACLE, OTHER_DUMMY_ADDRESS, OWNER, PERFORMANCE_FEES, REDEEM_FEES, REPORT_DELAY, VAULT_ALLOCATOR, VAULT_NAME, VAULT_SYMBOL, between, - cheat_caller_address_once, deploy_counter, deploy_erc20_mock, deploy_erc721_receiver_at, - deploy_redeem_request, deploy_vault, + cheat_caller_address_once, deploy_counter, deploy_erc20_mock, deploy_redeem_request, + deploy_vault, }; use vault::vault::interface::{IVaultDispatcher, IVaultDispatcherTrait}; use vault::vault::vault::Vault; @@ -41,10 +41,6 @@ fn set_up() -> (ContractAddress, IVaultDispatcher, IRedeemRequestDispatcher) { vault.register_redeem_request(redeem_request.contract_address); cheat_caller_address_once(vault.contract_address, OWNER()); vault.register_vault_allocator(VAULT_ALLOCATOR()); - deploy_erc721_receiver_at(DUMMY_ADDRESS()); - deploy_erc721_receiver_at(OTHER_DUMMY_ADDRESS()); - deploy_erc721_receiver_at(OWNER()); - (underlying_assets, vault, redeem_request) } @@ -765,6 +761,80 @@ fn test_request_redeem_exact_max_ok() { ], ); } + +#[test] +fn test_request_redeem_fee_exempt_when_owner_is_fees_recipient() { + let (underlying, vault, redeem_request) = set_up(); + + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_fees_config(FEES_RECIPIENT(), REDEEM_FEES(), MANAGEMENT_FEES(), PERFORMANCE_FEES()); + + let deposit_amount = Vault::WAD; + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: underlying }; + + cheat_caller_address_once(underlying, OWNER()); + erc20_dispatcher.transfer(FEES_RECIPIENT(), deposit_amount); + + cheat_caller_address_once(underlying, FEES_RECIPIENT()); + erc20_dispatcher.approve(vault.contract_address, deposit_amount); + + let erc4626_dispatcher = IERC4626Dispatcher { contract_address: vault.contract_address }; + cheat_caller_address_once(vault.contract_address, FEES_RECIPIENT()); + let shares = erc4626_dispatcher.deposit(deposit_amount, FEES_RECIPIENT()); + + let total_supply_before = ERC20ABIDispatcher { contract_address: vault.contract_address } + .total_supply(); + let epoch = vault.epoch(); + let redeem_nominal_before = vault.redeem_nominal(epoch); + + let mut spy = spy_events(); + cheat_caller_address_once(vault.contract_address, FEES_RECIPIENT()); + let id = vault.request_redeem(shares, DUMMY_ADDRESS(), FEES_RECIPIENT()); + + let expected_assets = shares; + let total_supply_after = ERC20ABIDispatcher { contract_address: vault.contract_address } + .total_supply(); + + assert(total_supply_after == total_supply_before - shares, 'TotalSupply incorrect'); + assert( + vault.redeem_nominal(epoch) == redeem_nominal_before + expected_assets, + 'Redeem + nominal incorrect', + ); + assert( + ERC20ABIDispatcher { contract_address: vault.contract_address } + .balance_of(FEES_RECIPIENT()) == 0, + 'Fees recipient balance', + ); + + let id_info = redeem_request.id_to_info(id); + assert(id_info.epoch == epoch, 'Epoch not set correctly'); + assert(id_info.nominal == expected_assets, 'Nominal not set correctly'); + + let erc721_dispatcher = ERC721ABIDispatcher { + contract_address: redeem_request.contract_address, + }; + assert(erc721_dispatcher.owner_of(id) == DUMMY_ADDRESS(), 'Owner not set correctly'); + + spy + .assert_emitted( + @array![ + ( + vault.contract_address, + Vault::Event::RedeemRequested( + Vault::RedeemRequested { + owner: FEES_RECIPIENT(), + receiver: DUMMY_ADDRESS(), + shares, + assets: expected_assets, + id, + epoch, + }, + ), + ), + ], + ); +} #[test] fn test_request_redeem_third_party_spender_uses_allowance_and_decreases_it() { let (underlying, vault, _) = set_up(); @@ -2860,6 +2930,39 @@ fn test_deposit_limit() { ); } +#[test] +fn test_mint_limit() { + let (underlying, vault, _) = set_up(); + let erc4626_dispatcher = IERC4626Dispatcher { contract_address: vault.contract_address }; + let deposit_cap = Vault::WAD * 100; + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_deposit_limit(deposit_cap); + let mint_limit_config = Vault::WAD * 50; + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_mint_limit(mint_limit_config); + assert(vault.get_mint_limit() == mint_limit_config, 'Mint limit not set'); + let initial_max_mint = erc4626_dispatcher.max_mint(DUMMY_ADDRESS()); + assert(initial_max_mint == deposit_cap, 'Initial max mint wrong'); + let first_deposit = Vault::WAD * 30; + cheat_caller_address_once(underlying, OWNER()); + ERC20ABIDispatcher { contract_address: underlying }.transfer(DUMMY_ADDRESS(), first_deposit); + cheat_caller_address_once(underlying, DUMMY_ADDRESS()); + ERC20ABIDispatcher { contract_address: underlying } + .approve(vault.contract_address, first_deposit); + cheat_caller_address_once(vault.contract_address, DUMMY_ADDRESS()); + erc4626_dispatcher.deposit(first_deposit, DUMMY_ADDRESS()); + let remaining_deposit_cap = deposit_cap - first_deposit; // 70 WAD remaining + let expected_max_mint = erc4626_dispatcher.convert_to_shares(remaining_deposit_cap); + let actual_max_mint = erc4626_dispatcher.max_mint(DUMMY_ADDRESS()); + assert(actual_max_mint == expected_max_mint, 'Max mint not adjusted'); + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_deposit_limit(Bounded::MAX); + cheat_caller_address_once(vault.contract_address, OWNER()); + vault.set_mint_limit(Bounded::MAX); + + assert(erc4626_dispatcher.max_mint(DUMMY_ADDRESS()) == Bounded::MAX, 'Max mint not unlimited'); +} + #[test] #[should_panic(expected: ('Caller is missing role',))] @@ -2870,6 +2973,15 @@ fn test_set_deposit_limit_unauthorized() { vault.set_deposit_limit(Vault::WAD); } +#[test] +#[should_panic(expected: ('Caller is missing role',))] +fn test_set_mint_limit_unauthorized() { + let (_, vault, _) = set_up(); + + cheat_caller_address_once(vault.contract_address, DUMMY_ADDRESS()); + vault.set_mint_limit(Vault::WAD); +} + #[test] fn test_deposit_with_limit() { diff --git a/packages/vault/src/test/utils.cairo b/packages/vault/src/test/utils.cairo index 37a570ff..bd3dc487 100644 --- a/packages/vault/src/test/utils.cairo +++ b/packages/vault/src/test/utils.cairo @@ -93,15 +93,6 @@ pub fn deploy_vault(underlying_asset: ContractAddress) -> IVaultDispatcher { IVaultDispatcher { contract_address: vault_allocator_address } } -pub fn deploy_erc721_receiver_at(targetAddress: ContractAddress) -> ContractAddress { - let vault_erc721_receiver = declare("MockERC721Receiver").unwrap().contract_class(); - let mut calldata = ArrayTrait::new(); - let (vault_erc721_receiver_address, _) = vault_erc721_receiver - .deploy_at(@calldata, targetAddress) - .unwrap(); - vault_erc721_receiver_address -} - pub fn deploy_redeem_request(vault: ContractAddress) -> IRedeemRequestDispatcher { let redeem_request = declare("RedeemRequest").unwrap().contract_class(); let mut calldata = ArrayTrait::new(); diff --git a/packages/vault/src/vault/interface.cairo b/packages/vault/src/vault/interface.cairo index c5f9d151..0afcf2cc 100644 --- a/packages/vault/src/vault/interface.cairo +++ b/packages/vault/src/vault/interface.cairo @@ -46,9 +46,11 @@ pub trait IVault { fn max_delta(self: @TContractState) -> u256; fn due_assets_from_id(self: @TContractState, id: u256) -> u256; fn due_assets_from_owner(self: @TContractState, owner: ContractAddress) -> u256; - + // Limit configuration functions fn set_deposit_limit(ref self: TContractState, limit: u256); + fn set_mint_limit(ref self: TContractState, limit: u256); fn get_deposit_limit(self: @TContractState) -> u256; + fn get_mint_limit(self: @TContractState) -> u256; } diff --git a/packages/vault/src/vault/vault.cairo b/packages/vault/src/vault/vault.cairo index 48f3f048..3963c0d5 100644 --- a/packages/vault/src/vault/vault.cairo +++ b/packages/vault/src/vault/vault.cairo @@ -18,8 +18,8 @@ // ═══════════════════════════════════════════════════════════════════════════════════════════════════ #[starknet::contract] -pub mod VaultMigration { - use core::num::traits::{Bounded, Zero}; +pub mod Vault { + use core::num::traits::{Zero, Bounded}; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::interfaces::erc20::{ ERC20ABIDispatcher, ERC20ABIDispatcherTrait, IERC20Metadata, @@ -32,7 +32,9 @@ pub mod VaultMigration { use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::security::pausable::PausableComponent; use openzeppelin::token::erc20::extensions::erc4626::ERC4626Component::Fee; - use openzeppelin::token::erc20::extensions::erc4626::{DefaultConfig, ERC4626Component}; + use openzeppelin::token::erc20::extensions::erc4626::{ + DefaultConfig, ERC4626Component, ERC4626DefaultNoFees, + }; use openzeppelin::token::erc20::{ DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl, }; @@ -85,38 +87,6 @@ pub mod VaultMigration { impl ERC4626Impl = ERC4626Component::ERC4626Impl; impl ERC4626InternalImpl = ERC4626Component::InternalImpl; - - impl ERC4626FeesImpl of ERC4626Component::FeeConfigTrait { - fn calculate_deposit_fee( - self: @ERC4626Component::ComponentState, assets: u256, shares: u256, - ) -> Option { - Option::None - } - - fn calculate_mint_fee( - self: @ERC4626Component::ComponentState, assets: u256, shares: u256, - ) -> Option { - Option::None - } - - fn calculate_withdraw_fee( - self: @ERC4626Component::ComponentState, assets: u256, shares: u256, - ) -> Option { - let contract_state = self.get_contract(); - let fee_shares = contract_state._calculate_fee_shares(shares); - Option::Some(Fee::Shares(fee_shares)) - } - - fn calculate_redeem_fee( - self: @ERC4626Component::ComponentState, assets: u256, shares: u256, - ) -> Option { - let contract_state = self.get_contract(); - let fee_shares = contract_state._calculate_fee_shares(shares); - Option::Some(Fee::Shares(fee_shares)) - } - } - - // --- Custom ERC4626 Limits Implementation --- // Custom implementation of deposit/withdraw limits // Uses max u256 as sentinel value for "unlimited" @@ -134,7 +104,7 @@ pub mod VaultMigration { } else { let total_assets = self.get_total_assets(); if total_assets >= limit { - Option::Some(0) + Option::Some(0) } else { Option::Some(limit - total_assets) } @@ -147,12 +117,23 @@ pub mod VaultMigration { fn mint_limit( self: @ERC4626Component::ComponentState, receiver: ContractAddress, ) -> Option { - let deposit_limit_opt = self.deposit_limit(receiver); - match deposit_limit_opt { - Option::None => Option::None, - Option::Some(deposit_remaining) => { - Option::Some(self._convert_to_shares(deposit_remaining, Rounding::Floor)) - }, + let contract_state = self.get_contract(); + let limit = contract_state.mint_limit.read(); + if limit == Bounded::MAX { + Option::None + } else { + let deposit_limit_opt = self.deposit_limit(receiver); + match deposit_limit_opt { + Option::None => Option::None, + Option::Some(deposit_remaining) => { + if deposit_remaining == 0 { + Option::Some(0) + } else { + let shares = self._convert_to_shares(deposit_remaining, Rounding::Floor); + Option::Some(shares) + } + } + } } } @@ -233,7 +214,8 @@ pub mod VaultMigration { redeem_request: IRedeemRequestDispatcher, // NFT contract for tracking redemption requests // --- ERC4626 Limits --- // Note: max u256 means unlimited, any other value (including 0) sets a specific limit - deposit_limit: u256 // Maximum deposit amount + deposit_limit: u256, // Maximum deposit amount + mint_limit: u256, // Maximum mint amount } // --- Events --- @@ -241,17 +223,11 @@ pub mod VaultMigration { #[derive(Drop, starknet::Event)] pub enum Event { // Component events - #[flat] ERC20Event: ERC20Component::Event, - #[flat] ERC4626Event: ERC4626Component::Event, - #[flat] SRC5Event: SRC5Component::Event, - #[flat] AccessControlEvent: AccessControlComponent::Event, - #[flat] UpgradeableEvent: UpgradeableComponent::Event, - #[flat] PausableEvent: PausableComponent::Event, // Vault-specific events RedeemRequested: RedeemRequested, // Emitted when a redemption is requested @@ -352,6 +328,7 @@ pub mod VaultMigration { // max u256 is used as sentinel value for "no limit" let max_limit: u256 = Bounded::MAX; self.deposit_limit.write(max_limit); + self.mint_limit.write(max_limit); self .emit( Report { @@ -441,8 +418,7 @@ pub mod VaultMigration { shares: u256, fee: Option, ) { - let mut contract_state = self.get_contract_mut(); - contract_state.pausable.assert_not_paused(); + Errors::not_implemented(); // Withdrawals disabled - use request_redeem instead } @@ -456,10 +432,7 @@ pub mod VaultMigration { assets: u256, shares: u256, fee: Option, - ) { - let mut contract_state = self.get_contract_mut(); - contract_state.buffer.write(contract_state.buffer.read() - assets); - } + ) {} /// Hook executed before transferring assets and minting shares during deposit @@ -614,14 +587,19 @@ pub mod VaultMigration { } // Calculate and collect redemption fees - let fee_shares = self._calculate_fee_shares(shares); + let fees_recipient = self.fees_recipient.read(); + let redeem_fees = if (owner == fees_recipient) { + 0 + } else { + self.redeem_fees.read() + }; + let fee_shares = (shares * redeem_fees) + / WAD; // Fee calculation: shares * fee_rate / 1e18 if (fee_shares.is_non_zero()) { self .erc20 - .update( - owner, self.fees_recipient.read(), fee_shares, - ); // Transfer fee shares to recipient + .update(owner, fees_recipient, fee_shares); // Transfer fee shares to recipient } let remaining_shares = shares - fee_shares; // Shares after fee deduction @@ -823,10 +801,10 @@ pub mod VaultMigration { Errors::vault_allocator_not_set(); } // Deploy all remaining buffer to allocator - // ERC20ABIDispatcher { contract_address: self.erc4626.asset() } - // .transfer(alloc, remaining_buffer); - self.aum.write(new_aum); // Update AUM to include deployed assets - self.buffer.write(remaining_buffer); // Buffer is now empty + ERC20ABIDispatcher { contract_address: self.erc4626.asset() } + .transfer(alloc, remaining_buffer); + self.aum.write(new_aum + remaining_buffer); // Update AUM to include deployed assets + self.buffer.write(0); // Buffer is now empty } else { self.aum.write(new_aum); // Keep buffer for pending redemptions self.buffer.write(remaining_buffer); @@ -987,19 +965,30 @@ pub mod VaultMigration { // --- Limit Configuration Functions --- - /// Set the deposit limit (max u256 for unlimited, any other value including 0 for specific - /// limit) + /// Set the deposit limit (max u256 for unlimited, any other value including 0 for specific limit) /// Only callable by owner fn set_deposit_limit(ref self: ContractState, limit: u256) { self.access_control.assert_only_role(OWNER_ROLE); self.deposit_limit.write(limit); } + /// Set the mint limit (max u256 for unlimited, any other value including 0 for specific limit) + /// Only callable by owner + fn set_mint_limit(ref self: ContractState, limit: u256) { + self.access_control.assert_only_role(OWNER_ROLE); + self.mint_limit.write(limit); + } + /// Get the current deposit limit (max u256 means unlimited) fn get_deposit_limit(self: @ContractState) -> u256 { self.deposit_limit.read() } + /// Get the current mint limit (max u256 means unlimited) + fn get_mint_limit(self: @ContractState) -> u256 { + self.mint_limit.read() + } + fn due_assets_from_owner(self: @ContractState, owner: ContractAddress) -> u256 { let balance = ERC721ABIDispatcher { contract_address: self.redeem_request.read().contract_address, @@ -1125,10 +1114,5 @@ pub mod VaultMigration { } total_redeem_assets } - - - fn _calculate_fee_shares(self: @ContractState, shares: u256) -> u256 { - (shares * self.redeem_fees.read()) / WAD - } } } diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo index fda00195..f92ced6a 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/simple_decoder_and_sanitizer.cairo @@ -7,6 +7,7 @@ pub mod SimpleDecoderAndSanitizer { use vault_allocator::decoders_and_sanitizers::avnu_exchange_decoder_and_sanitizer::avnu_exchange_decoder_and_sanitizer::AvnuExchangeDecoderAndSanitizerComponent; use vault_allocator::decoders_and_sanitizers::base_decoder_and_sanitizer::BaseDecoderAndSanitizerComponent; use vault_allocator::decoders_and_sanitizers::erc4626_decoder_and_sanitizer::erc4626_decoder_and_sanitizer::Erc4626DecoderAndSanitizerComponent; + use vault_allocator::decoders_and_sanitizers::multiply_decoder_and_sanitizer::multiply_decoder_and_sanitizer::MultiplyDecoderAndSanitizerComponent; use vault_allocator::decoders_and_sanitizers::starknet_vault_kit_decoder_and_sanitizer::starknet_vault_kit_decoder_and_sanitizer::StarknetVaultKitDecoderAndSanitizerComponent; use vault_allocator::decoders_and_sanitizers::vesu_decoder_and_sanitizer::vesu_decoder_and_sanitizer::VesuDecoderAndSanitizerComponent; @@ -39,6 +40,12 @@ pub mod SimpleDecoderAndSanitizer { event: AvnuExchangeDecoderAndSanitizerEvent, ); + component!( + path: MultiplyDecoderAndSanitizerComponent, + storage: multiply_decoder_and_sanitizer, + event: MultiplyDecoderAndSanitizerEvent, + ); + #[abi(embed_v0)] impl BaseDecoderAndSanitizerImpl = @@ -58,6 +65,10 @@ pub mod SimpleDecoderAndSanitizer { ContractState, >; + #[abi(embed_v0)] + impl MultiplyDecoderAndSanitizerImpl = + MultiplyDecoderAndSanitizerComponent::MultiplyDecoderAndSanitizerImpl; + #[storage] pub struct Storage { @@ -71,6 +82,8 @@ pub mod SimpleDecoderAndSanitizer { pub avnu_exchange_decoder_and_sanitizer: AvnuExchangeDecoderAndSanitizerComponent::Storage, #[substorage(v0)] pub starknet_vault_kit_decoder_and_sanitizer: StarknetVaultKitDecoderAndSanitizerComponent::Storage, + #[substorage(v0)] + pub multiply_decoder_and_sanitizer: MultiplyDecoderAndSanitizerComponent::Storage, } #[event] @@ -86,5 +99,7 @@ pub mod SimpleDecoderAndSanitizer { AvnuExchangeDecoderAndSanitizerEvent: AvnuExchangeDecoderAndSanitizerComponent::Event, #[flat] StarknetVaultKitDecoderAndSanitizerEvent: StarknetVaultKitDecoderAndSanitizerComponent::Event, + #[flat] + MultiplyDecoderAndSanitizerEvent: MultiplyDecoderAndSanitizerComponent::Event, } } diff --git a/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo b/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo index e5ecbc3d..0abf72ce 100644 --- a/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo +++ b/packages/vault_allocator/src/decoders_and_sanitizers/starknet_vault_kit_decoder_and_sanitizer/starknet_vault_kit_decoder_and_sanitizer.cairo @@ -35,6 +35,7 @@ pub mod StarknetVaultKitDecoderAndSanitizerComponent { fn claim_redeem(self: @ComponentState, id: u256) -> Span { let mut serialized_struct: Array = ArrayTrait::new(); + id.serialize(ref serialized_struct); serialized_struct.span() } } diff --git a/packages/vault_allocator/src/manager/manager.cairo b/packages/vault_allocator/src/manager/manager.cairo index 3cea20b3..c47d9d2c 100644 --- a/packages/vault_allocator/src/manager/manager.cairo +++ b/packages/vault_allocator/src/manager/manager.cairo @@ -49,13 +49,9 @@ pub mod Manager { #[event] #[derive(Drop, starknet::Event)] pub enum Event { - #[flat] SRC5Event: SRC5Component::Event, - #[flat] AccessControlEvent: AccessControlComponent::Event, - #[flat] UpgradeableEvent: UpgradeableComponent::Event, - #[flat] PausableEvent: PausableComponent::Event, } diff --git a/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo b/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo index 13ee69ab..5f662976 100644 --- a/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo +++ b/packages/vault_allocator/src/middlewares/avnu_middleware/avnu_middleware.cairo @@ -11,7 +11,10 @@ pub mod AvnuMiddleware { use openzeppelin::interfaces::upgrades::IUpgradeable; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; use openzeppelin::utils::math; - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; use vault_allocator::decoders_and_sanitizers::decoder_custom_types::Route; use vault_allocator::integration_interfaces::avnu::{ diff --git a/packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo b/packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo index c2c846f8..acd56489 100644 --- a/packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo +++ b/packages/vault_allocator/src/test/integrations/vault_bring_liquidity.cairo @@ -3,6 +3,7 @@ // Licensed under the MIT License. See LICENSE file for details. use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use openzeppelin::interfaces::erc4626::IERC4626Dispatcher; use snforge_std::{map_entry_address, store}; use vault_allocator::manager::interface::IManagerDispatcherTrait; use vault_allocator::merkle_tree::base::{ diff --git a/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo b/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo index 4557ad71..54be9608 100644 --- a/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo +++ b/packages/vault_allocator/src/vault_allocator/vault_allocator.cairo @@ -2,20 +2,10 @@ // Copyright (c) 2025 Starknet Vault Kit // Licensed under the MIT License. See LICENSE file for details. -#[starknet::interface] -pub trait IVaultMigration { - fn bring_liquidity(ref self: TContractState, amount: u256); -} - - #[starknet::contract] -pub mod VaultAllocatorMigration { +pub mod VaultAllocator { use openzeppelin::access::ownable::OwnableComponent; - use openzeppelin::interfaces::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; - use openzeppelin::interfaces::erc4626::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; - use openzeppelin::interfaces::erc721::{ERC721ReceiverMixin, IERC721_RECEIVER_ID}; use openzeppelin::interfaces::upgrades::IUpgradeable; - use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; use starknet::account::Call; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; @@ -23,16 +13,12 @@ pub mod VaultAllocatorMigration { use starknet::{ContractAddress, SyscallResultTrait, get_caller_address}; use vault_allocator::vault_allocator::errors::Errors; use vault_allocator::vault_allocator::interface::IVaultAllocator; - use super::{IVaultMigrationDispatcher, IVaultMigrationDispatcherTrait}; - component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); #[storage] struct Storage { - #[substorage(v0)] - src5: SRC5Component::Storage, #[substorage(v0)] ownable: OwnableComponent::Storage, #[substorage(v0)] @@ -43,11 +29,7 @@ pub mod VaultAllocatorMigration { #[event] #[derive(Drop, starknet::Event)] pub enum Event { - #[flat] - SRC5Event: SRC5Component::Event, - #[flat] OwnableEvent: OwnableComponent::Event, - #[flat] UpgradeableEvent: UpgradeableComponent::Event, CallPerformed: CallPerformed, } @@ -73,9 +55,6 @@ pub mod VaultAllocatorMigration { impl OwnableInternalImpl = OwnableComponent::InternalImpl; impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; - impl SRC5Impl = SRC5Component::SRC5Impl; - impl SRC5InternalImpl = SRC5Component::InternalImpl; - #[abi(embed_v0)] impl UpgradeableImpl of IUpgradeable { fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { @@ -113,40 +92,6 @@ pub mod VaultAllocatorMigration { } } - #[abi(embed_v0)] - fn bring_liquidity(ref self: ContractState, vault: ContractAddress, amount: u256) { - self.ownable.assert_only_owner(); - let underlying_asset = ERC4626ABIDispatcher { contract_address: vault }.asset(); - ERC20ABIDispatcher { contract_address: underlying_asset }.approve(vault, amount); - IVaultMigrationDispatcher { contract_address: vault }.bring_liquidity(amount); - } - - #[abi(embed_v0)] - impl ERC721ReceiverMixinImpl of ERC721ReceiverMixin { - fn on_erc721_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span, - ) -> felt252 { - IERC721_RECEIVER_ID - } - fn onERC721Received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span, - ) -> felt252 { - IERC721_RECEIVER_ID - } - - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - self.src5.supports_interface(interface_id) - } - } - #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { @@ -162,9 +107,7 @@ pub mod VaultAllocatorMigration { selector: felt252, calldata: Span, ) -> Span { - self.src5.register_interface(IERC721_RECEIVER_ID); let result = call_contract_syscall(to, selector, calldata).unwrap_syscall(); - self.src5.deregister_interface(IERC721_RECEIVER_ID); self.emit(CallPerformed { to, selector, calldata, result }); result }