From a4fa66a4d192ba0ec436a70fe9da744bfa639daf Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Fri, 13 Sep 2024 04:17:43 +0800 Subject: [PATCH 1/6] added ERC721Enumerable component and integrated in HypErc721 --- .../token/components/erc721_enumerable.cairo | 221 ++++++++++++++++++ cairo/src/contracts/token/hyp_erc721.cairo | 17 +- cairo/src/lib.cairo | 1 + 3 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 cairo/src/contracts/token/components/erc721_enumerable.cairo diff --git a/cairo/src/contracts/token/components/erc721_enumerable.cairo b/cairo/src/contracts/token/components/erc721_enumerable.cairo new file mode 100644 index 0000000..4336539 --- /dev/null +++ b/cairo/src/contracts/token/components/erc721_enumerable.cairo @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 +// (token/erc721/extensions/erc721_enumerable/interface.cairo) + +use starknet::ContractAddress; + +pub const IERC721ENUMERABLE_ID: felt252 = + 0x16bc0f502eeaf65ce0b3acb5eea656e2f26979ce6750e8502a82f377e538c87; + +#[starknet::interface] +pub trait IERC721Enumerable { + fn total_supply(self: @TState) -> u256; + fn token_by_index(self: @TState, index: u256) -> u256; + fn token_of_owner_by_index(self: @TState, owner: ContractAddress, index: u256) -> u256; +} + +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 +// (token/erc721/extensions/erc721_enumerable/erc721_enumerable.cairo) + +/// # ERC721Enumerable Component +/// +/// Extension of ERC721 as defined in the EIP that adds enumerability of all the token ids in the +/// contract as well as all token ids owned by each account. +/// This extension allows contracts to publish their entire list of NFTs and make them discoverable. +/// +/// NOTE: Implementing ERC721Component is a requirement for this component to be implemented. +/// +/// WARNING: To properly track token ids, this extension requires that +/// the ERC721EnumerableComponent::before_update function is called after +/// every transfer, mint, or burn operation. +/// For this, the ERC721HooksTrait::before_update hook must be used. +#[starknet::component] +pub mod ERC721EnumerableComponent { + use core::num::traits::Zero; + use openzeppelin::token::erc721::ERC721Component::ERC721Impl; + use openzeppelin::token::erc721::ERC721Component::InternalImpl as ERC721InternalImpl; + use openzeppelin::token::erc721::ERC721Component; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; + use starknet::ContractAddress; + + #[storage] + pub struct Storage { + pub ERC721Enumerable_owned_tokens: LegacyMap<(ContractAddress, u256), u256>, + pub ERC721Enumerable_owned_tokens_index: LegacyMap, + pub ERC721Enumerable_all_tokens_len: u256, + pub ERC721Enumerable_all_tokens: LegacyMap, + pub ERC721Enumerable_all_tokens_index: LegacyMap + } + + pub mod Errors { + pub const OUT_OF_BOUNDS_INDEX: felt252 = 'ERC721Enum: out of bounds index'; + } + + #[embeddable_as(ERC721EnumerableImpl)] + impl ERC721Enumerable< + TContractState, + +HasComponent, + impl ERC721: ERC721Component::HasComponent, + +ERC721Component::ERC721HooksTrait, + +SRC5Component::HasComponent, + +Drop + > of super::IERC721Enumerable> { + /// Returns the total amount of tokens stored by the contract. + fn total_supply(self: @ComponentState) -> u256 { + self.ERC721Enumerable_all_tokens_len.read() + } + + /// Returns a token id at a given `index` of all the tokens stored by the contract. + /// Use along with `total_supply` to enumerate all tokens. + /// + /// Requirements: + /// + /// - `index` is less than the total token supply. + fn token_by_index(self: @ComponentState, index: u256) -> u256 { + assert(index < self.total_supply(), Errors::OUT_OF_BOUNDS_INDEX); + self.ERC721Enumerable_all_tokens.read(index) + } + + /// Returns the token id owned by `owner` at a given `index` of its token list. + /// Use along with `ERC721::balance_of` to enumerate all of `owner`'s tokens. + /// + /// Requirements: + /// + /// - `index` is less than `owner`'s token balance. + /// - `owner` is not the zero address. + fn token_of_owner_by_index( + self: @ComponentState, owner: ContractAddress, index: u256 + ) -> u256 { + let erc721_component = get_dep_component!(self, ERC721); + assert(index < erc721_component.balance_of(owner), Errors::OUT_OF_BOUNDS_INDEX); + self.ERC721Enumerable_owned_tokens.read((owner, index)) + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC721: ERC721Component::HasComponent, + +ERC721Component::ERC721HooksTrait, + impl SRC5: SRC5Component::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the contract by declaring support for the `IERC721Enumerable` + /// interface id. + fn initializer(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(super::IERC721ENUMERABLE_ID); + } + + /// Updates the ownership and token-tracking data structures. + /// + /// When a token is minted (or burned), `token_id` is added to (or removed from) + /// the token-tracking structures. + /// + /// When a token is transferred, minted, or burned, the ownership-tracking data structures + /// reflect the change in ownership of `token_id`. + /// + /// This must be added to the implementing contract's `ERC721HooksTrait::before_update` + /// hook. + fn before_update( + ref self: ComponentState, to: ContractAddress, token_id: u256 + ) { + let erc721_component = get_dep_component!(@self, ERC721); + let previous_owner = erc721_component._owner_of(token_id); + + if previous_owner.is_zero() { + self._add_token_to_all_tokens_enumeration(token_id); + } else if previous_owner != to { + self._remove_token_from_owner_enumeration(previous_owner, token_id); + } + + if to.is_zero() { + self._remove_token_from_all_tokens_enumeration(token_id); + } else if previous_owner != to { + self._add_token_to_owner_enumeration(to, token_id); + } + } + + /// Adds token to this extension's ownership-tracking data structures. + fn _add_token_to_owner_enumeration( + ref self: ComponentState, to: ContractAddress, token_id: u256 + ) { + let mut erc721_component = get_dep_component_mut!(ref self, ERC721); + let len = erc721_component.balance_of(to); + self.ERC721Enumerable_owned_tokens.write((to, len), token_id); + self.ERC721Enumerable_owned_tokens_index.write(token_id, len); + } + + /// Adds token to this extension's token-tracking data structures. + fn _add_token_to_all_tokens_enumeration( + ref self: ComponentState, token_id: u256 + ) { + let supply = self.total_supply(); + self.ERC721Enumerable_all_tokens_index.write(token_id, supply); + self.ERC721Enumerable_all_tokens.write(supply, token_id); + self.ERC721Enumerable_all_tokens_len.write(supply + 1); + } + + /// Removes a token from this extension's ownership-tracking data structures. + /// + /// This has 0(1) time complexity but alters the indexed order of owned-tokens by + /// swapping `token_id` and the index thereof with the last token id and the index + /// thereof. + fn _remove_token_from_owner_enumeration( + ref self: ComponentState, from: ContractAddress, token_id: u256 + ) { + let erc721_component = get_dep_component!(@self, ERC721); + let last_token_index = erc721_component.balance_of(from) - 1; + let this_token_index = self.ERC721Enumerable_owned_tokens_index.read(token_id); + + // To prevent a gap in the token indexing of `from`, we store the last token + // in the index of the token to delete and then remove the last slot (swap and pop). + // When `token_id` is the last token, the swap operation is unnecessary + if this_token_index != last_token_index { + let last_token_id = self + .ERC721Enumerable_owned_tokens + .read((from, last_token_index)); + // Set `token_id` index to point to the last token id + self.ERC721Enumerable_owned_tokens.write((from, this_token_index), last_token_id); + // Set the last token id index to point to `token_id`'s index position + self.ERC721Enumerable_owned_tokens_index.write(last_token_id, this_token_index); + } + + // Set the last token index and `token_id` to zero + self.ERC721Enumerable_owned_tokens.write((from, last_token_index), 0); + self.ERC721Enumerable_owned_tokens_index.write(token_id, 0); + } + + /// Removes `token_id` from this extension's token-tracking data structures. + /// + /// This has 0(1) time complexity but alters the indexed order by swapping + /// `token_id` and the index thereof with the last token id and the index thereof. + fn _remove_token_from_all_tokens_enumeration( + ref self: ComponentState, token_id: u256 + ) { + let last_token_index = self.total_supply() - 1; + let this_token_index = self.ERC721Enumerable_all_tokens_index.read(token_id); + let last_token_id = self.ERC721Enumerable_all_tokens.read(last_token_index); + + // Set last token index to zero + self.ERC721Enumerable_all_tokens.write(last_token_index, 0); + // Set `token_id` index to 0 + self.ERC721Enumerable_all_tokens_index.write(token_id, 0); + // Remove one from total supply + self.ERC721Enumerable_all_tokens_len.write(last_token_index); + + // When the token to delete is the last token, the swap operation is unnecessary. + // However, since this occurs rarely (when the last minted token is burnt), we still do + // the swap which avoids the additional expense of including an `if` statement + self.ERC721Enumerable_all_tokens_index.write(last_token_id, this_token_index); + self.ERC721Enumerable_all_tokens.write(this_token_index, last_token_id); + } + } +} diff --git a/cairo/src/contracts/token/hyp_erc721.cairo b/cairo/src/contracts/token/hyp_erc721.cairo index f5834ea..04e6e3d 100644 --- a/cairo/src/contracts/token/hyp_erc721.cairo +++ b/cairo/src/contracts/token/hyp_erc721.cairo @@ -27,6 +27,7 @@ pub mod HypErc721 { use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; use starknet::ContractAddress; + use hyperlane_starknet::contracts::token::components::erc721_enumerable::ERC721EnumerableComponent; // also needs {https://github.com/OpenZeppelin/cairo-contracts/blob/main/packages/token/src/erc721/extensions/erc721_enumerable/erc721_enumerable.cairo} component!(path: ERC721Component, storage: erc721, event: ERC721Event); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); @@ -37,12 +38,17 @@ pub mod HypErc721 { component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); component!(path: RouterComponent, storage: router, event: RouterEvent); component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); - + component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent); // ERC721 Mixin #[abi(embed_v0)] impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; impl ERC721InternalImpl = ERC721Component::InternalImpl; + // ERC721Enumerable + #[abi(embed_v0)] + impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl; + // impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl; + // Upgradeable impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; @@ -95,7 +101,9 @@ pub mod HypErc721 { #[substorage(v0)] router: RouterComponent::Storage, #[substorage(v0)] - gas_router: GasRouterComponent::Storage + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + erc721_enumerable: ERC721EnumerableComponent::Storage } #[event] @@ -118,7 +126,9 @@ pub mod HypErc721 { #[flat] RouterEvent: RouterComponent::Event, #[flat] - GasRouterEvent: GasRouterComponent::Event + GasRouterEvent: GasRouterComponent::Event, + #[flat] + ERC721EnumerableEvent: ERC721EnumerableComponent::Event } #[constructor] @@ -147,6 +157,7 @@ pub mod HypErc721 { /// /// * `new_class_hash` - The class hash of the new implementation. fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); self.upgradeable.upgrade(new_class_hash); } } diff --git a/cairo/src/lib.cairo b/cairo/src/lib.cairo index 506d9a8..14835f4 100644 --- a/cairo/src/lib.cairo +++ b/cairo/src/lib.cairo @@ -71,6 +71,7 @@ mod contracts { pub mod ixerc20_lockbox; } pub mod components { + pub mod erc721_enumerable; pub mod fast_token_router; pub mod hyp_erc20_collateral_component; pub mod hyp_erc20_component; From ffec292417817049fd1bdf00284937159d48f2bb Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Fri, 13 Sep 2024 05:00:07 +0800 Subject: [PATCH 2/6] integrated enumerable in hyp_erc721 --- .../contracts/token/components/erc721_enumerable.cairo | 4 ++-- cairo/src/contracts/token/hyp_erc721.cairo | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cairo/src/contracts/token/components/erc721_enumerable.cairo b/cairo/src/contracts/token/components/erc721_enumerable.cairo index 4336539..d4e3f2b 100644 --- a/cairo/src/contracts/token/components/erc721_enumerable.cairo +++ b/cairo/src/contracts/token/components/erc721_enumerable.cairo @@ -33,11 +33,11 @@ pub trait IERC721Enumerable { #[starknet::component] pub mod ERC721EnumerableComponent { use core::num::traits::Zero; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::token::erc721::ERC721Component::ERC721Impl; use openzeppelin::token::erc721::ERC721Component::InternalImpl as ERC721InternalImpl; use openzeppelin::token::erc721::ERC721Component; - use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; - use openzeppelin::introspection::src5::SRC5Component; use starknet::ContractAddress; #[storage] diff --git a/cairo/src/contracts/token/hyp_erc721.cairo b/cairo/src/contracts/token/hyp_erc721.cairo index 04e6e3d..8e41baa 100644 --- a/cairo/src/contracts/token/hyp_erc721.cairo +++ b/cairo/src/contracts/token/hyp_erc721.cairo @@ -14,6 +14,7 @@ pub mod HypErc721 { use hyperlane_starknet::contracts::client::gas_router_component::GasRouterComponent; use hyperlane_starknet::contracts::client::mailboxclient_component::MailboxclientComponent; use hyperlane_starknet::contracts::client::router_component::RouterComponent; + use hyperlane_starknet::contracts::token::components::erc721_enumerable::ERC721EnumerableComponent; use hyperlane_starknet::contracts::token::components::hyp_erc721_component::HypErc721Component; use hyperlane_starknet::contracts::token::components::token_message::TokenMessageTrait; use hyperlane_starknet::contracts::token::components::token_router::{ @@ -27,7 +28,6 @@ pub mod HypErc721 { use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; use starknet::ContractAddress; - use hyperlane_starknet::contracts::token::components::erc721_enumerable::ERC721EnumerableComponent; // also needs {https://github.com/OpenZeppelin/cairo-contracts/blob/main/packages/token/src/erc721/extensions/erc721_enumerable/erc721_enumerable.cairo} component!(path: ERC721Component, storage: erc721, event: ERC721Event); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); @@ -38,7 +38,9 @@ pub mod HypErc721 { component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); component!(path: RouterComponent, storage: router, event: RouterEvent); component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); - component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent); + component!( + path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent + ); // ERC721 Mixin #[abi(embed_v0)] impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; @@ -46,7 +48,8 @@ pub mod HypErc721 { // ERC721Enumerable #[abi(embed_v0)] - impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl; + impl ERC721EnumerableImpl = + ERC721EnumerableComponent::ERC721EnumerableImpl; // impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl; // Upgradeable From 7cdd2277569aa676a01bb0b3524a158d860cc3f5 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Fri, 13 Sep 2024 05:00:23 +0800 Subject: [PATCH 3/6] added erc721_uri_storage component --- .../token/components/erc721_uri_storage.cairo | 75 +++++++++++++++++++ cairo/src/lib.cairo | 1 + 2 files changed, 76 insertions(+) create mode 100644 cairo/src/contracts/token/components/erc721_uri_storage.cairo diff --git a/cairo/src/contracts/token/components/erc721_uri_storage.cairo b/cairo/src/contracts/token/components/erc721_uri_storage.cairo new file mode 100644 index 0000000..1cca252 --- /dev/null +++ b/cairo/src/contracts/token/components/erc721_uri_storage.cairo @@ -0,0 +1,75 @@ +#[starknet::interface] +pub trait IERC721URIStorage { + fn token_uri(self: @TContractState, token_id: u256) -> ByteArray; +} + +#[starknet::component] +pub mod ERC721URIStorageComponent { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::{ + ERC721Component, ERC721Component::InternalTrait as ERC721InternalTrait, + ERC721Component::ERC721HooksTrait, ERC721Component::ERC721MetadataImpl + }; + + #[storage] + struct Storage { + token_uris: LegacyMap + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + MetadataUpdate: MetadataUpdate, + } + + #[derive(Drop, starknet::Event)] + struct MetadataUpdate { + token_id: u256, + } + + #[embeddable_as(ERC721URIStorageImpl)] + impl ERC721URIStorage< + TContractState, + +HasComponent, + +Drop, + +SRC5Component::HasComponent, + +ERC721HooksTrait, + impl ERC721: ERC721Component::HasComponent, + > of super::IERC721URIStorage> { + fn token_uri(self: @ComponentState, token_id: u256) -> ByteArray { + let erc721_component = get_dep_component!(self, ERC721); + erc721_component._require_owned(token_id); + + let token_uri = self.token_uris.read(token_id); + let mut base = erc721_component._base_uri(); + + if base.len() == 0 { + return token_uri; + } + + if token_uri.len() > 0 { + base.append(@token_uri); + return base; + } + + erc721_component.token_uri(token_id) + } + } + + #[generate_trait] + impl ERC721URIStorageInternalImpl< + TContractState, + +HasComponent, + +Drop, + +SRC5Component::HasComponent, + +ERC721HooksTrait, + +ERC721Component::HasComponent, + > of InternalTrait { + fn _set_token_uri( + ref self: ComponentState, token_id: u256, token_uri: ByteArray + ) { + self.token_uris.write(token_id, token_uri); + self.emit(MetadataUpdate { token_id }); + } + } +} diff --git a/cairo/src/lib.cairo b/cairo/src/lib.cairo index 14835f4..1980df2 100644 --- a/cairo/src/lib.cairo +++ b/cairo/src/lib.cairo @@ -72,6 +72,7 @@ mod contracts { } pub mod components { pub mod erc721_enumerable; + pub mod erc721_uri_storage; pub mod fast_token_router; pub mod hyp_erc20_collateral_component; pub mod hyp_erc20_component; From 1ebed30a441c88081801f794fcb42415bb079fb1 Mon Sep 17 00:00:00 2001 From: Ege Caner <67071243+EgeCaner@users.noreply.github.com> Date: Fri, 13 Sep 2024 01:11:41 +0300 Subject: [PATCH 4/6] Hyp_Erc20_Collateral_Vault_Deposit tests (#110) * overrides for `_handle` * fmt * make test pass * fmt&silence failing test template * ERC4626 draft * ERC4626 implementations * getters * vault collateral rebase func * Fix initializer and embedded missing components * comment for upgradeable impls * remove outdated import * merge 'hyperlane/feat-token-extensions' into 'feat-token-extensions' * fix hyp_erc_721_setup * fmt * Removed TODOS for muldiv Round Down is default * erc721 test deployment fix * using self instead of ERC20MixinImpl * u256 encode * hyp_xerc20_test * xerc20 test * removed unused import * ERC4626 Mocks * hyp_erc20_collateral_vault_deposit_test * fmt --- .../contracts/mocks/erc4626_component.cairo | 484 ++++++++++++++++++ cairo/src/contracts/mocks/erc4626_mock.cairo | 46 ++ .../mocks/erc4626_yield_sharing_mock.cairo | 228 +++++++++ .../hyp_erc20_collateral_vault_deposit.cairo | 12 +- .../token/extensions/hyp_xerc20_lockbox.cairo | 1 - .../contracts/token/interfaces/ierc4626.cairo | 9 +- cairo/src/lib.cairo | 7 + cairo/src/tests/token/hyp_erc20/common.cairo | 8 +- .../token/hyp_erc20/hyp_xerc20_test.cairo | 15 +- ..._erc20_collateral_vault_deposit_test.cairo | 260 ++++++++++ 10 files changed, 1056 insertions(+), 14 deletions(-) create mode 100644 cairo/src/contracts/mocks/erc4626_component.cairo create mode 100644 cairo/src/contracts/mocks/erc4626_mock.cairo create mode 100644 cairo/src/contracts/mocks/erc4626_yield_sharing_mock.cairo create mode 100644 cairo/src/tests/token/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo diff --git a/cairo/src/contracts/mocks/erc4626_component.cairo b/cairo/src/contracts/mocks/erc4626_component.cairo new file mode 100644 index 0000000..e8026c3 --- /dev/null +++ b/cairo/src/contracts/mocks/erc4626_component.cairo @@ -0,0 +1,484 @@ +//! Modified from {https://github.com/0xHashstack/hashstack_contracts/blob/main/src/token/erc4626/erc4626component.cairo} +use starknet::ContractAddress; + +#[starknet::component] +pub mod ERC4626Component { + use core::integer::BoundedInt; + + use hyperlane_starknet::contracts::token::interfaces::ierc4626::{ + IERC4626, IERC4626Camel, IERC4626Metadata + }; + use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; + use openzeppelin::introspection::src5::{ + SRC5Component, SRC5Component::SRC5Impl, SRC5Component::InternalTrait as SRC5INternalTrait + }; + use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait; + use openzeppelin::token::erc20::interface::{ + IERC20, IERC20Metadata, ERC20ABIDispatcher, ERC20ABIDispatcherTrait, + }; + use openzeppelin::token::erc20::{ + ERC20Component, ERC20HooksEmptyImpl, ERC20Component::Errors as ERC20Errors + }; + use starknet::{ContractAddress, get_caller_address, get_contract_address}; + + #[storage] + struct Storage { + ERC4626_asset: ContractAddress, + ERC4626_underlying_decimals: u8, + ERC4626_offset: u8, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Deposit: Deposit, + Withdraw: Withdraw, + } + + #[derive(Drop, starknet::Event)] + struct Deposit { + #[key] + sender: ContractAddress, + #[key] + owner: ContractAddress, + assets: u256, + shares: u256 + } + + #[derive(Drop, starknet::Event)] + struct Withdraw { + #[key] + sender: ContractAddress, + #[key] + receiver: ContractAddress, + #[key] + owner: ContractAddress, + assets: u256, + shares: u256 + } + + mod Errors { + const EXCEEDED_MAX_DEPOSIT: felt252 = 'ERC4626: exceeded max deposit'; + const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeded max mint'; + const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeded max redeem'; + const EXCEEDED_MAX_WITHDRAW: felt252 = 'ERC4626: exceeded max withdraw'; + } + + pub trait ERC4626HooksTrait { + fn before_deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ); + fn after_deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ); + + fn before_withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ); + + fn after_withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ); + } + + #[embeddable_as(ERC4626Impl)] + pub impl ERC4626< + TContractState, + +HasComponent, + impl ERC20: ERC20Component::HasComponent, + +ERC4626HooksTrait, + +SRC5Component::HasComponent, + +Drop + > of IERC4626> { + fn name(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.name() + } + + fn symbol(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.symbol() + } + + fn decimals(self: @ComponentState) -> u8 { + self.ERC4626_underlying_decimals.read() + self.ERC4626_offset.read() + } + + fn total_supply(self: @ComponentState) -> u256 { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.total_supply() + } + + fn balance_of(self: @ComponentState, account: ContractAddress) -> u256 { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.balance_of(account) + } + + fn allowance( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.allowance(owner, spender) + } + + fn transfer( + ref self: ComponentState, recipient: ContractAddress, amount: u256 + ) -> bool { + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.transfer(recipient, amount) + } + + fn transfer_from( + ref self: ComponentState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.transfer_from(sender, recipient, amount) + } + + fn approve( + ref self: ComponentState, spender: ContractAddress, amount: u256 + ) -> bool { + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.approve(spender, amount) + } + + fn asset(self: @ComponentState) -> ContractAddress { + self.ERC4626_asset.read() + } + + fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares) + } + + fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets) + } + + fn deposit( + ref self: ComponentState, assets: u256, receiver: ContractAddress + ) -> u256 { + let caller = get_caller_address(); + let shares = self.preview_deposit(assets); + self._deposit(caller, receiver, assets, shares); + shares + } + + fn mint( + ref self: ComponentState, shares: u256, receiver: ContractAddress + ) -> u256 { + let caller = get_caller_address(); + let assets = self.preview_mint(shares); + self._deposit(caller, receiver, assets, shares); + assets + } + + fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets) + } + + fn preview_mint(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares) + } + + fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares) + } + + fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets) + } + + fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_mint(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_redeem(self: @ComponentState, owner: ContractAddress) -> u256 { + let erc20 = get_dep_component!(self, ERC20); + erc20.balance_of(owner) + } + + fn max_withdraw(self: @ComponentState, owner: ContractAddress) -> u256 { + let erc20 = get_dep_component!(self, ERC20); + let balance = erc20.balance_of(owner); + self._convert_to_assets(balance) + } + + fn redeem( + ref self: ComponentState, + shares: u256, + receiver: ContractAddress, + owner: ContractAddress + ) -> u256 { + let caller = get_caller_address(); + let assets = self.preview_redeem(shares); + self._withdraw(caller, receiver, owner, assets, shares); + assets + } + + fn total_assets(self: @ComponentState) -> u256 { + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.balance_of(get_contract_address()) + } + + fn withdraw( + ref self: ComponentState, + assets: u256, + receiver: ContractAddress, + owner: ContractAddress + ) -> u256 { + let caller = get_caller_address(); + let shares = self.preview_withdraw(assets); + self._withdraw(caller, receiver, owner, assets, shares); + + shares + } + } + + #[embeddable_as(ERC4626MetadataImpl)] + pub impl ERC4626Metadata< + TContractState, + +HasComponent, + impl ERC20: ERC20Component::HasComponent, + +SRC5Component::HasComponent, + +Drop + > of IERC4626Metadata> { + fn name(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.name() + } + fn symbol(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.symbol() + } + fn decimals(self: @ComponentState) -> u8 { + self.ERC4626_underlying_decimals.read() + self.ERC4626_offset.read() + } + } + + #[embeddable_as(ERC4626CamelImpl)] + pub impl ERC4626Camel< + TContractState, + +HasComponent, + +ERC20Component::HasComponent, + +SRC5Component::HasComponent, + +ERC4626HooksTrait, + +Drop + > of IERC4626Camel> { + fn totalSupply(self: @ComponentState) -> u256 { + self.total_supply() + } + fn balanceOf(self: @ComponentState, account: ContractAddress) -> u256 { + self.balance_of(account) + } + fn transferFrom( + ref self: ComponentState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.transfer_from(sender, recipient, amount) + } + + fn convertToAssets(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares) + } + + fn convertToShares(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets) + } + + fn previewDeposit(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets) + } + + fn previewMint(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares) + } + + fn previewRedeem(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares) + } + + fn previewWithdraw(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets) + } + + fn totalAssets(self: @ComponentState) -> u256 { + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.balanceOf(get_contract_address()) + } + + fn maxDeposit(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn maxMint(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn maxRedeem(self: @ComponentState, owner: ContractAddress) -> u256 { + self.max_redeem(owner) + } + + fn maxWithdraw(self: @ComponentState, owner: ContractAddress) -> u256 { + self.max_withdraw(owner) + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC20: ERC20Component::HasComponent, + +SRC5Component::HasComponent, + impl Hooks: ERC4626HooksTrait, + +Drop + > of InternalImplTrait { + fn initializer( + ref self: ComponentState, + asset: ContractAddress, + name: ByteArray, + symbol: ByteArray, + offset: u8 + ) { + let dispatcher = ERC20ABIDispatcher { contract_address: asset }; + self.ERC4626_offset.write(offset); + let decimals = dispatcher.decimals(); + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.initializer(name, symbol); + self.ERC4626_asset.write(asset); + self.ERC4626_underlying_decimals.write(decimals); + } + + fn _convert_to_assets(self: @ComponentState, shares: u256) -> u256 { + let supply: u256 = self.total_supply(); + if (supply == 0) { + shares + } else { + (shares * self.total_assets()) / supply + } + } + + fn _convert_to_shares(self: @ComponentState, assets: u256) -> u256 { + let supply: u256 = self.total_supply(); + if (assets == 0 || supply == 0) { + assets + } else { + (assets * supply) / self.total_assets() + } + } + + fn _deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) { + Hooks::before_deposit(ref self, caller, receiver, assets, shares); + + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.transfer_from(caller, get_contract_address(), assets); + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.mint(receiver, shares); + self.emit(Deposit { sender: caller, owner: receiver, assets, shares }); + + Hooks::after_deposit(ref self, caller, receiver, assets, shares); + } + + fn _withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) { + Hooks::before_withdraw(ref self, caller, receiver, owner, assets, shares); + + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + if (caller != owner) { + let erc20_comp = get_dep_component!(@self, ERC20); + let allowance = erc20_comp.allowance(owner, caller); + if (allowance != BoundedInt::max()) { + assert(allowance >= shares, ERC20Errors::APPROVE_FROM_ZERO); + erc20_comp_mut.ERC20_allowances.write((owner, caller), allowance - shares); + } + } + + erc20_comp_mut.burn(owner, shares); + + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.transfer(receiver, assets); + + self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); + + Hooks::before_withdraw(ref self, caller, receiver, owner, assets, shares); + } + + fn _decimals_offset(self: @ComponentState) -> u8 { + self.ERC4626_offset.read() + } + } +} + +pub impl ERC4626HooksEmptyImpl< + TContractState +> of ERC4626Component::ERC4626HooksTrait { + fn before_deposit( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) {} + fn after_deposit( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) {} + + fn before_withdraw( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) {} + fn after_withdraw( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) {} +} diff --git a/cairo/src/contracts/mocks/erc4626_mock.cairo b/cairo/src/contracts/mocks/erc4626_mock.cairo new file mode 100644 index 0000000..e6caf8e --- /dev/null +++ b/cairo/src/contracts/mocks/erc4626_mock.cairo @@ -0,0 +1,46 @@ +#[starknet::contract] +mod ERC4626Mock { + use hyperlane_starknet::contracts::mocks::erc4626_component::{ + ERC4626Component, ERC4626HooksEmptyImpl + }; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::{get_contract_address, ContractAddress}; + + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[abi(embed_v0)] + impl ERC4626Impl = ERC4626Component::ERC4626Impl; + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc4626: ERC4626Component::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC4626Event: ERC4626Component::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, asset: ContractAddress, name: ByteArray, symbol: ByteArray, + ) { + self.erc4626.initializer(asset, name, symbol, 0); + } +} diff --git a/cairo/src/contracts/mocks/erc4626_yield_sharing_mock.cairo b/cairo/src/contracts/mocks/erc4626_yield_sharing_mock.cairo new file mode 100644 index 0000000..54003fa --- /dev/null +++ b/cairo/src/contracts/mocks/erc4626_yield_sharing_mock.cairo @@ -0,0 +1,228 @@ +#[starknet::interface] +trait IERC4626YieldSharing { + fn set_fee(ref self: TContractState, new_fee: u256); + fn get_claimable_fees(self: @TContractState) -> u256; +} + +#[starknet::contract] +mod ERC4626YieldSharingMock { + use core::integer::BoundedInt; + use hyperlane_starknet::contracts::libs::math; + use hyperlane_starknet::contracts::mocks::erc4626_component::{ + ERC4626Component, ERC4626HooksEmptyImpl + }; + use hyperlane_starknet::contracts::token::interfaces::ierc4626::IERC4626; + use openzeppelin::access::ownable::{OwnableComponent}; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::{get_contract_address, get_caller_address, ContractAddress}; + + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + impl ERC4626Impl = ERC4626Component::ERC4626Impl; + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // E18 + const SCALE: u256 = 1_000_000_000_000_000_000; + + #[storage] + struct Storage { + fee: u256, + accumulated_fees: u256, + last_vault_balance: u256, + #[substorage(v0)] + erc4626: ERC4626Component::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC4626Event: ERC4626Component::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + asset: ContractAddress, + name: ByteArray, + symbol: ByteArray, + initial_fee: u256 + ) { + self.erc4626.initializer(asset, name, symbol, 0); + self.fee.write(initial_fee); + self.ownable.initializer(get_caller_address()); + } + + pub impl ERC4626YieldSharingImpl of super::IERC4626YieldSharing { + fn set_fee(ref self: ContractState, new_fee: u256) { + self.ownable.assert_only_owner(); + self.fee.write(new_fee); + } + + fn get_claimable_fees(self: @ContractState) -> u256 { + let new_vault_balance = IERC20Dispatcher { contract_address: self.erc4626.asset() } + .balance_of(get_contract_address()); + let last_vault_balance = self.last_vault_balance.read(); + if new_vault_balance <= last_vault_balance { + return self.accumulated_fees.read(); + } + + let new_yield = new_vault_balance - last_vault_balance; + let new_fees = math::mul_div(new_yield, self.fee.read(), SCALE); + + self.accumulated_fees.read() + new_fees + } + } + + pub impl ERC4626 of IERC4626 { + fn name(self: @ContractState) -> ByteArray { + self.erc4626.name() + } + + fn symbol(self: @ContractState) -> ByteArray { + self.erc4626.symbol() + } + + fn decimals(self: @ContractState) -> u8 { + self.erc4626.decimals() + } + + fn total_supply(self: @ContractState) -> u256 { + self.erc4626.total_supply() + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc4626.balance_of(account) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.erc4626.allowance(owner, spender) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + self.erc4626.transfer(recipient, amount) + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.erc4626.transfer_from(sender, recipient, amount) + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.erc4626.approve(spender, amount) + } + + fn asset(self: @ContractState) -> ContractAddress { + self.erc4626.asset() + } + + fn convert_to_assets(self: @ContractState, shares: u256) -> u256 { + self.erc4626.convert_to_assets(shares) + } + + fn convert_to_shares(self: @ContractState, assets: u256) -> u256 { + self.erc4626.convert_to_shares(assets) + } + // Overriden + fn deposit(ref self: ContractState, assets: u256, receiver: ContractAddress) -> u256 { + let last_vault_balance = self.last_vault_balance.read(); + self.last_vault_balance.write(last_vault_balance + assets); + self.erc4626.deposit(assets, receiver) + } + + fn mint(ref self: ContractState, shares: u256, receiver: ContractAddress) -> u256 { + self.erc4626.mint(shares, receiver) + } + + fn preview_deposit(self: @ContractState, assets: u256) -> u256 { + self.erc4626.preview_deposit(assets) + } + + fn preview_mint(self: @ContractState, shares: u256) -> u256 { + self.erc4626.preview_mint(shares) + } + + fn preview_redeem(self: @ContractState, shares: u256) -> u256 { + self.erc4626.preview_redeem(shares) + } + + fn preview_withdraw(self: @ContractState, assets: u256) -> u256 { + self.erc4626.preview_withdraw(assets) + } + + fn max_deposit(self: @ContractState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_mint(self: @ContractState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_redeem(self: @ContractState, owner: ContractAddress) -> u256 { + self.erc4626.max_redeem(owner) + } + + fn max_withdraw(self: @ContractState, owner: ContractAddress) -> u256 { + self.erc4626.max_withdraw(owner) + } + // Overriden + fn redeem( + ref self: ContractState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256 { + self._accrue_yield(); + self.erc4626.redeem(shares, receiver, owner) + } + // Overriden + fn total_assets(self: @ContractState) -> u256 { + self.erc4626.total_assets() - self.get_claimable_fees() + } + + fn withdraw( + ref self: ContractState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256 { + self.erc4626.withdraw(assets, receiver, owner) + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _accrue_yield(ref self: ContractState) { + let new_vault_balance = IERC20Dispatcher { contract_address: self.erc4626.asset() } + .balance_of(get_contract_address()); + let last_vault_balance = self.last_vault_balance.read(); + if new_vault_balance > last_vault_balance { + let new_yield = new_vault_balance - last_vault_balance; + let new_fees = math::mul_div(new_yield, self.fee.read(), SCALE); + let accumulated_fees = self.accumulated_fees.read(); + self.accumulated_fees.write(accumulated_fees + new_fees); + self.last_vault_balance.write(new_vault_balance); + } + } + } +} diff --git a/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo b/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo index e177632..67a6a47 100644 --- a/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo +++ b/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo @@ -119,18 +119,20 @@ pub mod HypERC20CollateralVaultDeposit { mailbox: ContractAddress, vault: ContractAddress, owner: ContractAddress, - hook: Option, - interchain_security_module: Option + hook: ContractAddress, + interchain_security_module: ContractAddress ) { self.ownable.initializer(owner); - self.mailbox.initialize(mailbox, hook, interchain_security_module); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); let vault_dispatcher = ERC4626ABIDispatcher { contract_address: vault }; let erc20 = vault_dispatcher.asset(); self.collateral.initialize(erc20); self.vault.write(vault_dispatcher); self.collateral.wrapped_token.read().approve(vault, BoundedInt::max()); } - + #[abi(embed_v0)] impl HypERC20CollateralVaultDepositImpl of super::IHypERC20CollateralVaultDeposit< ContractState > { @@ -195,7 +197,7 @@ pub mod HypERC20CollateralVaultDeposit { fn _withdraw_from_vault(ref self: ContractState, amount: u256, recipient: ContractAddress) { let asset_deposited = self.asset_deposited.read(); self.asset_deposited.write(asset_deposited - amount); - self.vault.read().withdraw(amount, recipient, starknet::get_caller_address()); + self.vault.read().withdraw(amount, recipient, starknet::get_contract_address()); } } } diff --git a/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo b/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo index 989dcfd..82fed52 100644 --- a/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo +++ b/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo @@ -72,7 +72,6 @@ pub mod HypXERC20Lockbox { // Upgradeable impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; - #[storage] struct Storage { #[substorage(v0)] diff --git a/cairo/src/contracts/token/interfaces/ierc4626.cairo b/cairo/src/contracts/token/interfaces/ierc4626.cairo index a09afac..84e2a1f 100644 --- a/cairo/src/contracts/token/interfaces/ierc4626.cairo +++ b/cairo/src/contracts/token/interfaces/ierc4626.cairo @@ -6,7 +6,7 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IERC4626 { // ************************************ - // * IERC4626 + // * IERC20 // ************************************ fn total_supply(self: @TState) -> u256; fn balance_of(self: @TState, account: ContractAddress) -> u256; @@ -16,7 +16,12 @@ pub trait IERC4626 { ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 ) -> bool; fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; - + // ************************************ + // * IERC20 metadata + // ************************************ + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; // ************************************ // * IERC4626 // ************************************ diff --git a/cairo/src/lib.cairo b/cairo/src/lib.cairo index 1980df2..8de9c45 100644 --- a/cairo/src/lib.cairo +++ b/cairo/src/lib.cairo @@ -27,6 +27,9 @@ mod contracts { } pub mod mocks { pub mod enumerable_map_holder; + pub mod erc4626_component; + pub mod erc4626_mock; + pub mod erc4626_yield_sharing_mock; pub mod fee_hook; pub mod fee_token; pub mod hook; @@ -143,6 +146,10 @@ mod tests { pub mod hyp_erc721_test; pub mod hyp_erc721_uri_storage_test; } + + pub mod vault_extensions { + pub mod hyp_erc20_collateral_vault_deposit_test; + } } pub mod libs { pub mod test_enumerable_map; diff --git a/cairo/src/tests/token/hyp_erc20/common.cairo b/cairo/src/tests/token/hyp_erc20/common.cairo index 60f30ca..9db0b1c 100644 --- a/cairo/src/tests/token/hyp_erc20/common.cairo +++ b/cairo/src/tests/token/hyp_erc20/common.cairo @@ -319,9 +319,11 @@ pub fn handle_local_transfer(setup: @Setup, transfer_amount: u256) { stop_prank(CheatTarget::One((*setup).local_token.contract_address)); } -pub fn mint_and_approve(setup: @Setup, amount: u256, account: ContractAddress) { - (*setup).primary_token.mint(account, amount); - (*setup).primary_token.approve(account, amount); +pub fn mint_and_approve( + setup: @Setup, amount: u256, mint_to: ContractAddress, approve_to: ContractAddress +) { + (*setup).primary_token.mint(mint_to, amount); + (*setup).primary_token.approve(approve_to, amount); } pub fn set_custom_gas_config(setup: @Setup) { diff --git a/cairo/src/tests/token/hyp_erc20/hyp_xerc20_test.cairo b/cairo/src/tests/token/hyp_erc20/hyp_xerc20_test.cairo index 72f656c..fde5568 100644 --- a/cairo/src/tests/token/hyp_erc20/hyp_xerc20_test.cairo +++ b/cairo/src/tests/token/hyp_erc20/hyp_xerc20_test.cairo @@ -40,12 +40,21 @@ fn setup_xerc20() -> Setup { setup.local_token = IHypERC20TestDispatcher { contract_address: local_token }; println!("HypXERC20: {:?}", local_token); - enroll_local_router(@setup); - enroll_remote_router(@setup); - + setup + .local_token + .enroll_remote_router( + DESTINATION, + Into::::into(setup.remote_token.contract_address).into() + ); setup.primary_token.transfer(local_token, 1000 * E18); setup.primary_token.transfer(ALICE(), 1000 * E18); setup + .remote_token + .enroll_remote_router( + ORIGIN, + Into::::into(setup.local_token.contract_address).into() + ); + setup } #[test] diff --git a/cairo/src/tests/token/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo b/cairo/src/tests/token/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo new file mode 100644 index 0000000..b954cfb --- /dev/null +++ b/cairo/src/tests/token/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo @@ -0,0 +1,260 @@ +use hyperlane_starknet::contracts::mocks::mock_mailbox::{ + IMockMailboxDispatcher, IMockMailboxDispatcherTrait +}; +use hyperlane_starknet::contracts::mocks::{ + test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait}, +}; +use hyperlane_starknet::contracts::token::extensions::hyp_erc20_collateral_vault_deposit::{ + IHypERC20CollateralVaultDepositDispatcher, IHypERC20CollateralVaultDepositDispatcherTrait +}; +use hyperlane_starknet::contracts::token::interfaces::ierc4626::{ + IERC4626Dispatcher, IERC4626DispatcherTrait +}; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::{declare, ContractClassTrait, CheatTarget, start_prank, stop_prank,}; +use starknet::ContractAddress; +use super::super::hyp_erc20::common::{ + Setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, TRANSFER_AMT, ALICE, BOB, E18, REQUIRED_VALUE, + DESTINATION, IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, setup, + perform_remote_transfer_and_gas, enroll_remote_router, enroll_local_router, + perform_remote_transfer, handle_local_transfer, mint_and_approve +}; + +const DUST_AMOUNT: u256 = 100_000_000_000; // E11 + +fn _transfer_roundtrip_and_increase_yields( + setup: @Setup, vault: ContractAddress, transfer_amount: u256, yield_amount: u256 +) { + // Transfer from Alice to Bob + start_prank(CheatTarget::One((*setup).primary_token.contract_address), ALICE()); + (*setup).primary_token.approve((*setup).local_token.contract_address, transfer_amount); + stop_prank(CheatTarget::One((*setup).primary_token.contract_address)); + perform_remote_transfer(setup, 0, transfer_amount); + // Increase vault balance, which will reduce share redeemed for the same amount + (*setup).primary_token.mint(vault, yield_amount); + start_prank(CheatTarget::One((*setup).remote_token.contract_address), BOB()); + (*setup) + .remote_token + .transfer_remote( + ORIGIN, + Into::::into(BOB()) + .into(), // orginal test has Bob here as well but not sure, should it be alice + transfer_amount, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One((*setup).remote_token.contract_address)); +} + +fn assert_approx_eq_abs(lhs: u256, rhs: u256, relaxation: u256) { + let diff = if lhs >= rhs { + lhs - rhs + } else { + rhs - lhs + }; + assert!(diff <= relaxation, "Values are not approximately equal"); +} + +fn setup_vault() -> (Setup, IERC4626Dispatcher, IHypERC20CollateralVaultDepositDispatcher) { + let mut setup = setup(); + let contract = declare("ERC4626Mock").unwrap(); + let mut calldata: Array = array![]; + setup.primary_token.contract_address.serialize(ref calldata); + let name: ByteArray = "Regular Vault"; + let symbol: ByteArray = "RV"; + name.serialize(ref calldata); + symbol.serialize(ref calldata); + let (vault, _) = contract.deploy(@calldata).unwrap(); + println!("VAULT: {:?}", vault); + + let contract = declare("HypERC20CollateralVaultDeposit").unwrap(); + let mut calldata: Array = array![]; + setup.local_mailbox.contract_address.serialize(ref calldata); + vault.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + setup.implementation.interchain_security_module().serialize(ref calldata); + let (implementation, _) = contract.deploy(@calldata).unwrap(); + println!("HypERC20CollateralVaultDeposit: {:?}", implementation); + setup.local_token = IHypERC20TestDispatcher { contract_address: implementation }; + setup + .local_token + .enroll_remote_router( + DESTINATION, + Into::::into(setup.remote_token.contract_address).into() + ); + + setup.remote_mailbox.set_default_hook(setup.noop_hook.contract_address); + setup.remote_mailbox.set_required_hook(setup.noop_hook.contract_address); + + setup.primary_token.transfer(ALICE(), 1000 * E18); + + setup + .remote_token + .enroll_remote_router( + ORIGIN, + Into::::into(setup.local_token.contract_address).into() + ); + ( + setup, + IERC4626Dispatcher { contract_address: vault }, + IHypERC20CollateralVaultDepositDispatcher { contract_address: implementation } + ) +} + +fn erc4626_vault_deposit_remote_transfer_deposits_into_vault( + mut transfer_amount: u256 +) -> (Setup, IERC4626Dispatcher, IHypERC20CollateralVaultDepositDispatcher) { + transfer_amount %= TOTAL_SUPPLY + 1; + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + mint_and_approve(@setup, transfer_amount, ALICE(), setup.local_token.contract_address); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + // Check vault shares balance before and after transfer + assert_eq!(vault.max_redeem(erc20_collateral_vault_deposit.contract_address), 0); + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), 0); + + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + setup.primary_token.approve(setup.local_token.contract_address, transfer_amount); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + perform_remote_transfer(@setup, 0, transfer_amount); + assert_approx_eq_abs( + vault.max_redeem(erc20_collateral_vault_deposit.contract_address), transfer_amount, 1 + ); + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), transfer_amount); + (setup, vault, erc20_collateral_vault_deposit) +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_deposits_into_vault(mut transfer_amount: u256) { + erc4626_vault_deposit_remote_transfer_deposits_into_vault(transfer_amount); +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_withdraws_from_vault(mut transfer_amount: u256) { + transfer_amount %= TOTAL_SUPPLY + 1; + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + mint_and_approve(@setup, transfer_amount, ALICE(), setup.local_token.contract_address); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, transfer_amount, DUST_AMOUNT + ); + // Check Alice's local token balance + let prev_balance = setup.local_token.balance_of(ALICE()); + handle_local_transfer(@setup, transfer_amount); + let after_balance = setup.local_token.balance_of(ALICE()); + assert_eq!(after_balance, prev_balance + transfer_amount); + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), 0); +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_withdraw_less_shares(mut reward_amount: u256) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + // Check Alice's local token balance + let prev_balance = setup.local_token.balance_of(ALICE()); + handle_local_transfer(@setup, TRANSFER_AMT); + let after_balance = setup.local_token.balance_of(ALICE()); + assert_eq!(after_balance, prev_balance + TRANSFER_AMT); + // Has leftover shares, but no assets deposited] + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), 0); + assert_gt!(vault.max_redeem(erc20_collateral_vault_deposit.contract_address), 0); +} + +#[test] +#[should_panic] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_sweep_revert_non_owner(mut reward_amount: u256) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + start_prank(CheatTarget::One(erc20_collateral_vault_deposit.contract_address), BOB()); + erc20_collateral_vault_deposit.sweep(); + stop_prank(CheatTarget::One(erc20_collateral_vault_deposit.contract_address)); +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_sweep_no_excess_shares( + mut transfer_amount: u256 +) { + let (mut setup, _, mut erc20_collateral_vault_deposit) = + erc4626_vault_deposit_remote_transfer_deposits_into_vault( + transfer_amount + ); + let owner = IOwnableDispatcher { + contract_address: erc20_collateral_vault_deposit.contract_address + } + .owner(); + let owner_balance_prev = setup.primary_token.balance_of(owner); + erc20_collateral_vault_deposit.sweep(); + let owner_balance_after = setup.primary_token.balance_of(owner); + assert_eq!(owner_balance_prev, owner_balance_after); +} + +#[test] +fn test_erc4626_vault_deposit_remote_transfer_sweep_excess_shares_12312(mut reward_amount: u256) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + handle_local_transfer(@setup, TRANSFER_AMT); + let owner = IOwnableDispatcher { + contract_address: erc20_collateral_vault_deposit.contract_address + } + .owner(); + let owner_balance_prev = setup.primary_token.balance_of(owner); + let excess_amount = vault.max_redeem(erc20_collateral_vault_deposit.contract_address); + erc20_collateral_vault_deposit.sweep(); + let owner_balance_after = setup.primary_token.balance_of(owner); + assert_gt!(owner_balance_after, owner_balance_prev + excess_amount); +} + +#[test] +fn test_erc4626_vault_deposit_remote_transfer_sweep_excess_shares_multiple_deposit( + mut reward_amount: u256 +) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + handle_local_transfer(@setup, TRANSFER_AMT); + + let owner = IOwnableDispatcher { + contract_address: erc20_collateral_vault_deposit.contract_address + } + .owner(); + let owner_balance_prev = setup.primary_token.balance_of(owner); + let excess_amount = vault.max_redeem(erc20_collateral_vault_deposit.contract_address); + // Deposit again for Alice + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + setup.primary_token.approve(setup.local_token.contract_address, TRANSFER_AMT); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + perform_remote_transfer(@setup, 0, TRANSFER_AMT); + // Sweep and check + erc20_collateral_vault_deposit.sweep(); + let owner_balance_after = setup.primary_token.balance_of(owner); + assert_gt!(owner_balance_after, owner_balance_prev + excess_amount); +} + +// NOTE: Not applicable on Starknet +fn test_benchmark_overhead_gas_usage() {} From 712459b68ec04d0dd0ba4c0711c88bafa20bfbb8 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Fri, 13 Sep 2024 14:29:50 +0800 Subject: [PATCH 5/6] added docs --- .../client/gas_router_component.cairo | 48 +++++ .../contracts/client/router_component.cairo | 82 ++++++++ cairo/src/contracts/libs/enumerable_map.cairo | 191 ++++++++++++++++++ .../token/components/erc721_uri_storage.cairo | 27 +++ .../token/components/fast_token_router.cairo | 34 ++++ .../hyp_erc20_collateral_component.cairo | 19 ++ .../components/hyp_erc20_component.cairo | 57 +++++- .../hyp_erc721_collateral_component.cairo | 32 +++ .../components/hyp_erc721_component.cairo | 32 +++ .../token/components/token_message.cairo | 46 +++++ .../token/components/token_router.cairo | 29 +++ .../token/extensions/fast_hyp_erc20.cairo | 19 ++ .../fast_hyp_erc20_collateral.cairo | 30 +++ .../hyp_erc20_collateral_vault_deposit.cairo | 37 ++++ .../token/extensions/hyp_erc20_vault.cairo | 90 +++++++++ .../hyp_erc20_vault_collateral.cairo | 75 +++++++ .../hyp_erc721_URI_collateral.cairo | 13 ++ .../token/extensions/hyp_fiat_token.cairo | 22 ++ .../token/extensions/hyp_xerc20.cairo | 22 ++ .../token/extensions/hyp_xerc20_lockbox.cairo | 42 ++++ 20 files changed, 944 insertions(+), 3 deletions(-) diff --git a/cairo/src/contracts/client/gas_router_component.cairo b/cairo/src/contracts/client/gas_router_component.cairo index d2c84a9..8af75b2 100644 --- a/cairo/src/contracts/client/gas_router_component.cairo +++ b/cairo/src/contracts/client/gas_router_component.cairo @@ -11,6 +11,25 @@ pub trait IGasRouter { fn quote_gas_payment(self: @TState, destination_domain: u32) -> u256; } +/// # Gas Router Component Module +/// +/// This module provides a gas management mechanism for routing messages across domains. +/// It allows setting gas limits for specific destinations and quoting gas payments for +/// message dispatches. +/// +/// ## Key Concepts +/// +/// - **Gas Configuration**: This module allows users to set gas limits for specific destination +/// domains, either individually or through an array of configurations. +/// +/// - **Message Dispatching**: Gas management is integrated with the message dispatch system, +/// enabling the module to quote gas payments for dispatching messages to other domains. +/// +/// - **Ownership-based Access Control**: The ability to set gas limits is restricted to the +/// contract owner. +/// +/// - **Component Composition**: The `GasRouterComponent` integrates with other components such as +/// `RouterComponent` for dispatching messages and `MailboxclientComponent` for mailbox interactions. #[starknet::component] pub mod GasRouterComponent { use alexandria_bytes::{Bytes, BytesTrait}; @@ -47,6 +66,22 @@ pub mod GasRouterComponent { impl Router: RouterComponent::HasComponent, impl Owner: OwnableComponent::HasComponent, > of super::IGasRouter> { + /// Sets the gas limit for a specified domain or applies an array of gas configurations. + /// + /// This function allows the contract owner to configure gas limits for one or multiple domains. + /// If an array of `GasRouterConfig` is provided via `gas_configs`, the function iterates over + /// the array and applies each configuration. If a specific `domain` and `gas` are provided, + /// the function sets the gas limit for that domain. + /// + /// # Arguments + /// + /// * `gas_configs` - An optional array of `GasRouterConfig`, each containing a `domain` and a `gas` value. + /// * `domain` - An optional `u32` representing the domain for which the gas limit is being set. + /// * `gas` - An optional `u256` representing the gas limit for the given domain. + /// + /// # Panics + /// + /// Panics if neither `gas_configs` nor a valid `domain` and `gas` are provided. fn set_destination_gas( ref self: ComponentState, gas_configs: Option>, @@ -78,6 +113,19 @@ pub mod GasRouterComponent { } } + /// Returns the quoted gas payment for dispatching a message to the specified domain. + /// + /// This function calculates and returns the gas payment required to dispatch a message + /// to a specified destination domain. It uses the router and mailbox components to compute + /// the necessary gas amount for the message dispatch. + /// + /// # Arguments + /// + /// * `destination_domain` - A `u32` representing the domain to which the message is being sent. + /// + /// # Returns + /// + /// A `u256` value representing the quoted gas payment. fn quote_gas_payment( self: @ComponentState, destination_domain: u32 ) -> u256 { diff --git a/cairo/src/contracts/client/router_component.cairo b/cairo/src/contracts/client/router_component.cairo index 29519af..c783038 100644 --- a/cairo/src/contracts/client/router_component.cairo +++ b/cairo/src/contracts/client/router_component.cairo @@ -12,6 +12,14 @@ pub trait IRouter { fn routers(self: @TState, domain: u32) -> u256; } +/// # Router Component Module +/// +/// This module implements a router component that manages the enrollment and +/// unenrollment of remote routers across various domains. It provides the +/// functionality for dispatching messages to remote routers and handling incoming +/// messages. The core functionality is split across traits, with the primary logic +/// provided by the `IRouter` trait and the additional internal mechanisms handled +/// by the `RouterComponent`. #[starknet::component] pub mod RouterComponent { use alexandria_bytes::Bytes; @@ -54,6 +62,15 @@ pub mod RouterComponent { +Drop, impl Hook: IMessageRecipientInternalHookTrait > of super::IRouter> { + /// Enrolls a remote router for the specified `domain`. + /// + /// This function requires ownership verification before proceeding. Once verified, + /// it calls the internal method `_enroll_remote_router` to complete the enrollment. + /// + /// # Arguments + /// + /// * `domain` - A `u32` representing the domain for which the router is being enrolled. + /// * `router` - A `u256` representing the address of the router to be enrolled. fn enroll_remote_router( ref self: ComponentState, domain: u32, router: u256 ) { @@ -62,6 +79,20 @@ pub mod RouterComponent { self._enroll_remote_router(domain, router); } + /// Enrolls multiple remote routers across multiple `domains`. + /// + /// This function requires ownership verification. It checks that the lengths of the + /// `domains` and `addresses` arrays are the same, then enrolls each router for its + /// corresponding domain using `_enroll_remote_router`. + /// + /// # Arguments + /// + /// * `domains` - An array of `u32` values representing the domains for which routers are being enrolled. + /// * `addresses` - An array of `u256` values representing the addresses of the routers to be enrolled. + /// + /// # Panics + /// + /// Panics if the lengths of `domains` and `addresses` do not match. fn enroll_remote_routers( ref self: ComponentState, domains: Array, addresses: Array ) { @@ -80,6 +111,14 @@ pub mod RouterComponent { } } + /// Unenrolls the router for the specified `domain`. + /// + /// This function requires ownership verification. Once verified, it calls the internal method + /// `_unenroll_remote_router` to complete the unenrollment. + /// + /// # Arguments + /// + /// * `domain` - A `u32` representing the domain for which the router is being unenrolled. fn unenroll_remote_router(ref self: ComponentState, domain: u32) { let mut ownable_comp = get_dep_component_mut!(ref self, Owner); ownable_comp.assert_only_owner(); @@ -87,6 +126,14 @@ pub mod RouterComponent { self._unenroll_remote_router(domain); } + /// Unenrolls the routers for multiple `domains`. + /// + /// This function removes the router for each domain in the `domains` array + /// using the `_unenroll_remote_router` method. + /// + /// # Arguments + /// + /// * `domains` - An array of `u32` values representing the domains for which routers are being unenrolled. fn unenroll_remote_routers(ref self: ComponentState, domains: Array,) { let domains_len = domains.len(); let mut i = 0; @@ -96,6 +143,21 @@ pub mod RouterComponent { } } + /// Handles an incoming message from a remote router. + /// + /// This function checks if a remote router is enrolled for the `origin` domain, verifies that the + /// `sender` matches the enrolled router, and calls the `_handle` method on the `Hook` to process + /// the message. + /// + /// # Arguments + /// + /// * `origin` - A `u32` representing the origin domain of the message. + /// * `sender` - A `u256` representing the address of the message sender. + /// * `message` - The message payload as a `Bytes` object. + /// + /// # Panics + /// + /// Panics if the sender does not match the enrolled router for the origin domain. fn handle( ref self: ComponentState, origin: u32, sender: u256, message: Bytes ) { @@ -105,10 +167,30 @@ pub mod RouterComponent { Hook::_handle(ref self, origin, sender, message); } + /// Returns an array of enrolled domains. + /// + /// This function reads the keys from the `routers` map, which represent the enrolled + /// domains. + /// + /// # Returns + /// + /// An array of `u32` values representing the enrolled domains. fn domains(self: @ComponentState) -> Array { self.routers.read().keys() } + /// Returns the router address for a given `domain`. + /// + /// This function retrieves the address of the enrolled router for the specified + /// `domain` from the `routers` map. + /// + /// # Arguments + /// + /// * `domain` - A `u32` representing the domain for which the router address is being queried. + /// + /// # Returns + /// + /// A `u256` value representing the router address for the specified domain. fn routers(self: @ComponentState, domain: u32) -> u256 { self.routers.read().get(domain) } diff --git a/cairo/src/contracts/libs/enumerable_map.cairo b/cairo/src/contracts/libs/enumerable_map.cairo index abd1d8e..c5e4c71 100644 --- a/cairo/src/contracts/libs/enumerable_map.cairo +++ b/cairo/src/contracts/libs/enumerable_map.cairo @@ -24,14 +24,45 @@ pub struct EnumerableMap { base: StorageBaseAddress } +/// A storage interface for an `EnumerableMap` that allows reading and writing +/// to a system store. This trait defines how to read, write, and handle storage +/// for `EnumerableMap` structures. It also provides methods to handle reading +/// and writing at specific offsets in storage. +/// +/// # Parameters: +/// - `K`: The key type. +/// - `V`: The value type. +/// +/// # Example: +/// ```rust +/// let map = EnumerableMapStore::::read(domain, address); +/// ``` pub impl EnumerableMapStore< K, V, +Store, +Drop, +Store, +Drop > of Store> { + /// Reads the `EnumerableMap` from storage at the given `base` address + /// within the specified `address_domain`. + /// + /// # Arguments: + /// - `address_domain`: The domain in which the map is stored. + /// - `base`: The base storage address for the map. + /// + /// # Returns: + /// - `SyscallResult>`: The map read from storage. #[inline(always)] fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { SyscallResult::Ok(EnumerableMap:: { address_domain, base }) } + /// Attempts to write the `EnumerableMap` to storage. Currently not implemented. + /// + /// # Arguments: + /// - `address_domain`: The domain in which to write the map. + /// - `base`: The base storage address for the map. + /// - `value`: The `EnumerableMap` to write. + /// + /// # Returns: + /// - `SyscallResult<()>`: Error indicating not implemented. #[inline(always)] fn write( address_domain: u32, base: StorageBaseAddress, value: EnumerableMap @@ -39,18 +70,45 @@ pub impl EnumerableMapStore< SyscallResult::Err(array![Err::NOT_IMPLEMENTED]) } + /// Attempts to read the `EnumerableMap` from storage at a specific offset. + /// + /// # Arguments: + /// - `address_domain`: The domain in which the map is stored. + /// - `base`: The base storage address for the map. + /// - `offset`: The offset in storage where the map is read from. + /// + /// # Returns: + /// - `SyscallResult>`: Error indicating not implemented. #[inline(always)] fn read_at_offset( address_domain: u32, base: StorageBaseAddress, offset: u8 ) -> SyscallResult> { SyscallResult::Err(array![Err::NOT_IMPLEMENTED]) } + + + /// Attempts to write the `EnumerableMap` to storage at a specific offset. + /// + /// # Arguments: + /// - `address_domain`: The domain in which to write the map. + /// - `base`: The base storage address for the map. + /// - `offset`: The offset in storage where the map is written to. + /// - `value`: The `EnumerableMap` to write. + /// + /// # Returns: + /// - `SyscallResult<()>`: Error indicating not implemented. #[inline(always)] fn write_at_offset( address_domain: u32, base: StorageBaseAddress, offset: u8, value: EnumerableMap ) -> SyscallResult<()> { SyscallResult::Err(array![Err::NOT_IMPLEMENTED]) } + + + /// Returns the size of the `EnumerableMap` in bytes. Currently set to `0`. + /// + /// # Returns: + /// - `u8`: The size of the map in bytes. #[inline(always)] fn size() -> u8 { // 0 was selected because the read method doesn't actually read from storage @@ -58,13 +116,74 @@ pub impl EnumerableMapStore< } } +/// Trait defining basic operations for a key-value map where the keys are stored +/// in an enumerable way. This provides functionality to get, set, check for keys, +/// and retrieve values. +/// +/// # Parameters: +/// - `K`: The key type. +/// - `V`: The value type. +/// +/// # Example: +/// ```rust +/// let value = map.get(key); +/// map.set(key, value); +/// ``` pub trait EnumerableMapTrait { + /// Retrieves the value associated with the specified `key`. + /// + /// # Arguments: + /// - `key`: The key for which to retrieve the value. + /// + /// # Returns: + /// - `V`: The value associated with the `key` fn get(self: @EnumerableMap, key: K) -> V; + + /// Associates the specified `key` with the provided `val` and adds it to + /// the map if it does not already exist. + /// + /// # Arguments: + /// - `key`: The key to associate with the value. + /// - `val`: The value to associate with the key. fn set(ref self: EnumerableMap, key: K, val: V) -> (); + + /// Returns the number of key-value pairs stored in the map. + /// + /// # Returns: + /// - `u32`: The number of elements in the map. fn len(self: @EnumerableMap) -> u32; + + /// Checks if the map contains the specified `key`. + /// + /// # Arguments: + /// - `key`: The key to check. + /// + /// # Returns: + /// - `bool`: `true` if the key exists, `false` otherwise. fn contains(self: @EnumerableMap, key: K) -> bool; + + /// Removes the key-value pair associated with the specified `key` from the map. + /// + /// # Arguments: + /// - `key`: The key to remove. + /// + /// # Returns: + /// - `bool`: `true` if the removal was successful, `false` if the key does not exist. fn remove(ref self: EnumerableMap, key: K) -> bool; + + /// Retrieves the key-value pair stored at the specified `index` in the map. + /// + /// # Arguments: + /// - `index`: The index at which to retrieve the key-value pair. + /// + /// # Returns: + /// - `(K, V)`: The key-value pair at the specified index. fn at(self: @EnumerableMap, index: u32) -> (K, V); + + /// Returns an array of all keys stored in the map. + /// + /// # Returns: + /// - `Array`: An array of all keys in the map. fn keys(self: @EnumerableMap) -> Array; } @@ -136,18 +255,90 @@ pub impl EnumerableMapImpl< } } +/// Internal trait for managing the internal structures of an `EnumerableMap`. +/// This trait handles reading and writing key-value pairs and their positions, +/// as well as managing the array of keys for enumeration. +/// +/// # Parameters: +/// - `K`: The key type. +/// - `V`: The value type. +/// +/// # Example: +/// ```rust +/// EnumerableMapInternalTrait::::values_mapping_write(map, key, value); +/// ``` trait EnumerableMapInternalTrait { + /// Writes the specified `val` associated with the `key` into the `values` mapping. + /// + /// # Arguments: + /// - `key`: The key to associate with the value. + /// - `val`: The value to store. fn values_mapping_write(ref self: EnumerableMap, key: K, val: V); + + /// Reads the value associated with the `key` from the `values` mapping. + /// + /// # Arguments: + /// - `key`: The key for which to read the value. + /// + /// # Returns: + /// - `V`: The value associated with the `key`. fn values_mapping_read(self: @EnumerableMap, key: K) -> V; + + /// Writes the position of the `key` in the `positions` mapping. + /// + /// # Arguments: + /// - `key`: The key for which to store the position. + /// - `val`: The position to store. fn positions_mapping_write(ref self: EnumerableMap, key: K, val: u32); + + /// Reads the position of the `key` from the `positions` mapping. + /// + /// # Arguments: + /// - `key`: The key for which to retrieve the position. + /// + /// # Returns: + /// - `u32`: The position associated with the `key`. fn positions_mapping_read(self: @EnumerableMap, key: K) -> u32; + + /// Updates the length of the key array in storage. + /// + /// # Arguments: + /// - `new_len`: The new length of the array. fn update_array_len(ref self: EnumerableMap, new_len: u32); + + /// Appends the `key` to the array of keys. + /// + /// # Arguments: + /// - `key`: The key to append to the array. fn array_append(ref self: EnumerableMap, key: K); + + /// Removes the key-value pair at the specified `index` from the array. + /// + /// # Arguments: + /// - `index`: The index of the key-value pair to remove. + /// + /// # Returns: + /// - `bool`: `true` if the removal was successful, `false` otherwise. fn array_remove(ref self: EnumerableMap, index: u32) -> bool; + + /// Reads the key at the specified `index` from the array of keys. + /// + /// # Arguments: + /// - `index`: The index at which to read the key. + /// + /// # Returns: + /// - `K`: The key at the specified index. fn array_read(self: @EnumerableMap, index: u32) -> K; + + /// Writes the specified `key` at the given `index` in the array of keys. + /// + /// # Arguments: + /// - `index`: The index at which to write the key. + /// - `val`: The key to write. fn array_write(ref self: EnumerableMap, index: u32, val: K); } + impl EnumerableMapInternalImpl< K, V, diff --git a/cairo/src/contracts/token/components/erc721_uri_storage.cairo b/cairo/src/contracts/token/components/erc721_uri_storage.cairo index 1cca252..0d9af81 100644 --- a/cairo/src/contracts/token/components/erc721_uri_storage.cairo +++ b/cairo/src/contracts/token/components/erc721_uri_storage.cairo @@ -36,6 +36,20 @@ pub mod ERC721URIStorageComponent { +ERC721HooksTrait, impl ERC721: ERC721Component::HasComponent, > of super::IERC721URIStorage> { + /// Returns the URI associated with a given `token_id`. + /// + /// This function retrieves the URI for an ERC721 token based on its `token_id`. + /// It first ensures that the token is owned by the caller, then checks the token-specific URI. + /// If the token has no specific URI, it appends the token's base URI if one exists. + /// + /// # Arguments + /// + /// * `token_id` - A `u256` representing the ID of the token whose URI is being queried. + /// + /// # Returns + /// + /// A `ByteArray` representing the URI associated with the token. If a specific URI is not found, + /// it may return the base URI or the token's metadata URI. fn token_uri(self: @ComponentState, token_id: u256) -> ByteArray { let erc721_component = get_dep_component!(self, ERC721); erc721_component._require_owned(token_id); @@ -65,6 +79,19 @@ pub mod ERC721URIStorageComponent { +ERC721HooksTrait, +ERC721Component::HasComponent, > of InternalTrait { + // Sets the URI for a specific `token_id`. + /// + /// This internal function allows setting a URI for an ERC721 token. After setting the URI, + /// it emits a `MetadataUpdate` event to indicate that the token's metadata has been updated. + /// + /// # Arguments + /// + /// * `token_id` - A `u256` representing the ID of the token whose URI is being set. + /// * `token_uri` - A `ByteArray` representing the new URI for the token. + /// + /// # Emits + /// + /// Emits a `MetadataUpdate` event once the token URI has been updated. fn _set_token_uri( ref self: ComponentState, token_id: u256, token_uri: ByteArray ) { diff --git a/cairo/src/contracts/token/components/fast_token_router.cairo b/cairo/src/contracts/token/components/fast_token_router.cairo index a4e9fc1..8c91a14 100644 --- a/cairo/src/contracts/token/components/fast_token_router.cairo +++ b/cairo/src/contracts/token/components/fast_token_router.cairo @@ -104,6 +104,23 @@ pub mod FastTokenRouterComponent { impl GasRouter: GasRouterComponent::HasComponent, impl TokenRouter: TokenRouterComponent::HasComponent, > of super::IFastTokenRouter> { + /// Fills a fast transfer request by transferring the specified amount minus the fast fee to the recipient. + /// + /// This function is used to process a fast transfer request, ensuring that the transfer has not already been filled. + /// It deducts the fast fee from the total amount and transfers the remaining amount to the recipient. The function also + /// records the sender's address in the filled fast transfer mapping. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient of the fast transfer. + /// * `amount` - A `u256` representing the total amount of the fast transfer. + /// * `fast_fee` - A `u256` representing the fee to be deducted from the transfer amount. + /// * `origin` - A `u32` representing the domain of origin for the transfer. + /// * `fast_transfer_id` - A `u256` representing the unique ID of the fast transfer request. + /// + /// # Panics + /// + /// Panics if the fast transfer has already been filled. fn fill_fast_transfer( ref self: ComponentState, recipient: u256, @@ -129,6 +146,23 @@ pub mod FastTokenRouterComponent { FTRHooks::fast_transfer_to_hook(ref self, recipient, amount - fast_fee); } + /// Initiates a fast transfer to a remote domain and returns the message ID for tracking. + /// + /// This function sends a fast transfer to a recipient in a specified remote domain. It deducts the fast fee + /// from the total amount and dispatches the transfer using the gas router and mailbox components. The function + /// emits an event for the sent transfer and returns the message ID for tracking the transfer. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount to transfer or the token ID. + /// * `fast_fee` - A `u256` representing the fast transfer fee. + /// * `value` - A `u256` representing the value being transferred with the message. + /// + /// # Returns + /// + /// A `u256` representing the message ID for the dispatched fast transfer. fn fast_transfer_remote( ref self: ComponentState, destination: u32, diff --git a/cairo/src/contracts/token/components/hyp_erc20_collateral_component.cairo b/cairo/src/contracts/token/components/hyp_erc20_collateral_component.cairo index e30affa..6a97e83 100644 --- a/cairo/src/contracts/token/components/hyp_erc20_collateral_component.cairo +++ b/cairo/src/contracts/token/components/hyp_erc20_collateral_component.cairo @@ -78,10 +78,29 @@ pub mod HypErc20CollateralComponent { +GasRouterComponent::HasComponent, +TokenRouterComponent::HasComponent, > of super::IHypErc20Collateral> { + /// Returns the balance of the given account for the wrapped ERC20 token. + /// + /// This function retrieves the balance of the wrapped ERC20 token for a specified account by calling the + /// `balance_of` function on the wrapped token dispatcher. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the balance of the specified account. fn balance_of(self: @ComponentState, account: ContractAddress) -> u256 { self.wrapped_token.read().balance_of(account) } + /// Returns the contract address of the wrapped ERC20 token. + /// + /// This function retrieves the contract address of the wrapped ERC20 token from the token dispatcher. + /// + /// # Returns + /// + /// A `ContractAddress` representing the address of the wrapped ERC20 token. fn get_wrapped_token(self: @ComponentState) -> ContractAddress { let wrapped_token: ERC20ABIDispatcher = self.wrapped_token.read(); wrapped_token.contract_address diff --git a/cairo/src/contracts/token/components/hyp_erc20_component.cairo b/cairo/src/contracts/token/components/hyp_erc20_component.cairo index a21f348..2a35b65 100644 --- a/cairo/src/contracts/token/components/hyp_erc20_component.cairo +++ b/cairo/src/contracts/token/components/hyp_erc20_component.cairo @@ -80,19 +80,41 @@ pub mod HypErc20Component { +ERC20HooksTrait, impl ERC20: ERC20Component::HasComponent > of IERC20Metadata> { - /// Returns the name of the token. + /// Returns the name of the ERC20 token. + /// + /// This function retrieves the name of the token by reading from the `ERC20_name` field + /// of the ERC20 component. + /// + /// # Returns + /// + /// A `ByteArray` representing the name of the token. fn name(self: @ComponentState) -> ByteArray { let erc20 = get_dep_component!(self, ERC20); erc20.ERC20_name.read() } - /// Returns the ticker symbol of the token, usually a shorter version of the name. + /// Returns the symbol of the ERC20 token. + /// + /// This function retrieves the symbol, or ticker, of the token by reading from the `ERC20_symbol` + /// field of the ERC20 component. + /// + /// # Returns + /// + /// A `ByteArray` representing the token's symbol. fn symbol(self: @ComponentState) -> ByteArray { let erc20 = get_dep_component!(self, ERC20); erc20.ERC20_symbol.read() } - /// Returns the number of decimals used to get its user representation. + /// Returns the number of decimals used to represent the token. + /// + /// This function returns the number of decimals defined for the token, which represents the + /// smallest unit of the token used in its user-facing operations. The value is read from the + /// `decimals` field of the component's storage. + /// + /// # Returns + /// + /// A `u8` representing the number of decimals used by the token. fn decimals(self: @ComponentState) -> u8 { self.decimals.read() } @@ -111,16 +133,45 @@ pub mod HypErc20Component { +ERC20HooksTrait, impl ERC20: ERC20Component::HasComponent > of InternalTrait { + /// Initializes the token with a specific number of decimals. + /// + /// This function sets the `decimals` value for the token during the initialization phase, defining + /// how many decimal places the token will support. + /// + /// # Arguments + /// + /// * `decimals` - A `u8` value representing the number of decimals for the token. fn initialize(ref self: ComponentState, decimals: u8) { self.decimals.write(decimals); } + /// Burns tokens from the sender's account. + /// + /// This function transfers the specified amount of tokens from the sender's account by + /// calling the `burn` function on the ERC20 component. + /// + /// # Arguments + /// + /// * `amount` - A `u256` value representing the amount of tokens to be burned. + /// + /// # Returns + /// + /// A `Bytes` object representing an empty payload. fn _transfer_from_sender(ref self: ComponentState, amount: u256) -> Bytes { let mut erc20 = get_dep_component_mut!(ref self, ERC20); erc20.burn(starknet::get_caller_address(), amount); BytesTrait::new_empty() } + /// Mints tokens to the specified recipient. + /// + /// This function mints new tokens and transfers them to the recipient's account by calling + /// the `mint` function on the ERC20 component. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` value representing the recipient's address. + /// * `amount` - A `u256` value representing the amount of tokens to mint. fn _transfer_to(ref self: ComponentState, recipient: u256, amount: u256) { let mut erc20 = get_dep_component_mut!(ref self, ERC20); erc20.mint(recipient.try_into().expect('u256 to ContractAddress failed'), amount); diff --git a/cairo/src/contracts/token/components/hyp_erc721_collateral_component.cairo b/cairo/src/contracts/token/components/hyp_erc721_collateral_component.cairo index 14384ef..1f60436 100644 --- a/cairo/src/contracts/token/components/hyp_erc721_collateral_component.cairo +++ b/cairo/src/contracts/token/components/hyp_erc721_collateral_component.cairo @@ -33,14 +33,46 @@ pub mod HypErc721CollateralComponent { +OwnableComponent::HasComponent, impl Mailboxclient: MailboxclientComponent::HasComponent, > of super::IHypErc721Collateral> { + /// Returns the owner of a given ERC721 token ID. + /// + /// This function queries the wrapped ERC721 token contract to retrieve the address of the owner + /// of the specified `token_id`. + /// + /// # Arguments + /// + /// * `token_id` - A `u256` representing the ID of the token whose owner is being queried. + /// + /// # Returns + /// + /// A `ContractAddress` representing the owner of the specified token. fn owner_of(self: @ComponentState, token_id: u256) -> ContractAddress { self.wrapped_token.read().owner_of(token_id) } + /// Returns the balance of ERC721 tokens held by a given account. + /// + /// This function retrieves the number of ERC721 tokens held by the specified account by querying + /// the wrapped ERC721 token contract. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the number of tokens held by the specified account. fn balance_of(self: @ComponentState, account: ContractAddress) -> u256 { self.wrapped_token.read().balance_of(account) } + /// Returns the contract address of the wrapped ERC721 token. + /// + /// This function retrieves the contract address of the wrapped ERC721 token from the component's + /// storage. + /// + /// # Returns + /// + /// A `ContractAddress` representing the address of the wrapped ERC721 token. fn get_wrapped_token(self: @ComponentState) -> ContractAddress { let wrapped_token: ERC721ABIDispatcher = self.wrapped_token.read(); wrapped_token.contract_address diff --git a/cairo/src/contracts/token/components/hyp_erc721_component.cairo b/cairo/src/contracts/token/components/hyp_erc721_component.cairo index 9111ab2..14416ab 100644 --- a/cairo/src/contracts/token/components/hyp_erc721_component.cairo +++ b/cairo/src/contracts/token/components/hyp_erc721_component.cairo @@ -39,6 +39,17 @@ pub mod HypErc721Component { impl Mailboxclient: MailboxclientComponent::HasComponent, impl ERC721: ERC721Component::HasComponent, > of super::IHypErc721> { + /// Initializes the ERC721 token contract with a specified mint amount, name, and symbol. + /// + /// This function sets the name and symbol for the ERC721 token contract and mints the specified number + /// of tokens to the caller's address. The initialization process ensures that the contract is set up + /// with the given name, symbol, and initial minting operation. + /// + /// # Arguments + /// + /// * `mint_amount` - A `u256` representing the number of tokens to mint initially. + /// * `name` - A `ByteArray` representing the name of the token. + /// * `symbol` - A `ByteArray` representing the symbol (ticker) of the token. fn initialize( ref self: ComponentState, mint_amount: u256, @@ -67,6 +78,18 @@ pub mod HypErc721Component { +ERC721Component::ERC721HooksTrait, impl ERC721: ERC721Component::HasComponent, > of InternalTrait { + /// Burns a token owned by the sender. + /// + /// This function ensures that the sender is the owner of the specified token before burning it. + /// The token is permanently removed from the sender's balance. + /// + /// # Arguments + /// + /// * `token_id` - A `u256` representing the ID of the token to be burned. + /// + /// # Panics + /// + /// Panics if the caller is not the owner of the token. fn transfer_from_sender(ref self: ComponentState, token_id: u256) { let erc721_comp_read = get_dep_component!(@self, ERC721); assert!( @@ -78,6 +101,15 @@ pub mod HypErc721Component { erc721_comp_write.burn(token_id); } + /// Mints a token to a specified recipient. + /// + /// This function mints the specified token to the given recipient's address. The newly minted token + /// will be transferred to the recipient. + /// + /// # Arguments + /// + /// * `recipient` - A `ContractAddress` representing the recipient's address. + /// * `token_id` - A `u256` representing the ID of the token to be minted. fn transfer_to( ref self: ComponentState, recipient: ContractAddress, token_id: u256 ) { diff --git a/cairo/src/contracts/token/components/token_message.cairo b/cairo/src/contracts/token/components/token_message.cairo index 99d3367..ed00cd8 100644 --- a/cairo/src/contracts/token/components/token_message.cairo +++ b/cairo/src/contracts/token/components/token_message.cairo @@ -3,6 +3,20 @@ use alexandria_bytes::{Bytes, BytesTrait}; #[generate_trait] pub impl TokenMessage of TokenMessageTrait { + /// Formats a token message with the recipient, amount, and metadata. + /// + /// This function creates a token message by combining the recipient address, the transfer amount, + /// and any additional metadata. The resulting message is returned as a `Bytes` object. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount` - A `u256` representing the amount of tokens to transfer. + /// * `metadata` - A `Bytes` object representing additional metadata for the transfer. + /// + /// # Returns + /// + /// A `Bytes` object containing the formatted token message. fn format(recipient: u256, amount: u256, metadata: Bytes) -> Bytes { let data: Array = array![recipient.low, recipient.high, amount.low, amount.high]; let mut bytes = BytesTrait::new(4, data); @@ -10,20 +24,52 @@ pub impl TokenMessage of TokenMessageTrait { bytes } + /// Extracts the recipient address from the token message. + /// + /// This function reads the recipient address from the token message, starting at the beginning of + /// the message data. The recipient is returned as a `u256`. + /// + /// # Returns + /// + /// A `u256` representing the recipient address. fn recipient(self: @Bytes) -> u256 { let (_, recipient) = self.read_u256(0); recipient } + /// Extracts the transfer amount from the token message. + /// + /// This function reads the amount of tokens to be transferred from the token message, starting at + /// byte offset 32. The amount is returned as a `u256`. + /// + /// # Returns + /// + /// A `u256` representing the amount of tokens to be transferred. fn amount(self: @Bytes) -> u256 { let (_, amount) = self.read_u256(32); amount } + /// Extracts the token ID from the token message. + /// + /// This function is equivalent to the `amount` function, as in certain token standards the token + /// ID is encoded in the same field as the transfer amount. + /// + /// # Returns + /// + /// A `u256` representing the token ID or transfer amount. fn token_id(self: @Bytes) -> u256 { self.amount() } + /// Extracts the metadata from the token message. + /// + /// This function reads and returns the metadata portion of the token message, starting at byte + /// offset 64 and extending to the end of the message. + /// + /// # Returns + /// + /// A `Bytes` object representing the metadata included in the token message. fn metadata(self: @Bytes) -> Bytes { let (_, bytes) = self.read_bytes(64, self.size() - 64); bytes diff --git a/cairo/src/contracts/token/components/token_router.cairo b/cairo/src/contracts/token/components/token_router.cairo index a247fe4..fca0a0c 100644 --- a/cairo/src/contracts/token/components/token_router.cairo +++ b/cairo/src/contracts/token/components/token_router.cairo @@ -104,6 +104,17 @@ pub mod TokenRouterComponent { impl Hooks: TokenRouterHooksTrait, +Drop, > of IMessageRecipientInternalHookTrait { + /// Handles the receipt of a message and processes a token transfer. + /// + /// This function is invoked when a message is received, processing the transfer of tokens to the recipient. + /// It retrieves the recipient, amount, and metadata from the message and triggers the appropriate hook to + /// handle the transfer. The function also emits a `ReceivedTransferRemote` event after processing the transfer. + /// + /// # Arguments + /// + /// * `origin` - A `u32` representing the origin domain. + /// * `sender` - A `u256` representing the sender's address. + /// * `message` - A `Bytes` object representing the incoming message. fn _handle( ref self: RouterComponent::ComponentState, origin: u32, @@ -132,6 +143,24 @@ pub mod TokenRouterComponent { +TokenRouterHooksTrait, impl TransferRemoteHook: TokenRouterTransferRemoteHookTrait > of super::ITokenRouter> { + /// Initiates a token transfer to a remote domain. + /// + /// This function dispatches a token transfer to the specified recipient on a remote domain, transferring + /// either an amount of tokens or a token ID. It supports optional hooks and metadata for additional + /// processing during the transfer. The function emits a `SentTransferRemote` event once the transfer is initiated. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `value` - A `u256` representing the value of the transfer. + /// * `hook_metadata` - An optional `Bytes` object representing metadata for the hook. + /// * `hook` - An optional `ContractAddress` representing the contract hook to invoke during the transfer. + /// + /// # Returns + /// + /// A `u256` representing the message ID of the dispatched transfer. fn transfer_remote( ref self: ComponentState, destination: u32, diff --git a/cairo/src/contracts/token/extensions/fast_hyp_erc20.cairo b/cairo/src/contracts/token/extensions/fast_hyp_erc20.cairo index 2ab7348..6203152 100644 --- a/cairo/src/contracts/token/extensions/fast_hyp_erc20.cairo +++ b/cairo/src/contracts/token/extensions/fast_hyp_erc20.cairo @@ -153,6 +153,15 @@ pub mod FastHypERC20 { } pub impl FastTokenRouterHooksImpl of FastTokenRouterHooksTrait { + /// Transfers tokens to the recipient as part of the fast token router process. + /// + /// This function mints tokens to the recipient as part of the fast token router process by calling + /// the `mint` function of the ERC20 component. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount` - A `u256` representing the amount of tokens to mint. fn fast_transfer_to_hook( ref self: FastTokenRouterComponent::ComponentState, recipient: u256, @@ -165,6 +174,16 @@ pub mod FastHypERC20 { .erc20 .mint(recipient.try_into().expect('u256 to ContractAddress failed'), amount); } + + /// Receives tokens from the sender as part of the fast token router process. + /// + /// This function burns tokens from the sender as part of the fast token router process by calling + /// the `burn` function of the ERC20 component. + /// + /// # Arguments + /// + /// * `sender` - A `ContractAddress` representing the sender's address. + /// * `amount` - A `u256` representing the amount of tokens to burn. fn fast_receive_from_hook( ref self: FastTokenRouterComponent::ComponentState, sender: ContractAddress, diff --git a/cairo/src/contracts/token/extensions/fast_hyp_erc20_collateral.cairo b/cairo/src/contracts/token/extensions/fast_hyp_erc20_collateral.cairo index 9fa25f1..319b462 100644 --- a/cairo/src/contracts/token/extensions/fast_hyp_erc20_collateral.cairo +++ b/cairo/src/contracts/token/extensions/fast_hyp_erc20_collateral.cairo @@ -132,6 +132,18 @@ pub mod FastHypERC20Collateral { } impl FastHypERC20Impl of super::IFastHypERC20 { + /// Returns the balance of the specified account for the wrapped ERC20 token. + /// + /// This function retrieves the balance of the wrapped ERC20 token for a given account by calling + /// the `balance_of` function on the `HypErc20CollateralComponent`. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose token balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the balance of the specified account. fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { self.collateral.balance_of(account) } @@ -151,6 +163,15 @@ pub mod FastHypERC20Collateral { } pub impl FastTokenRouterHooksImpl of FastTokenRouterHooksTrait { + /// Transfers tokens to the recipient as part of the fast token router process. + /// + /// This function handles the fast token transfer process by invoking the `transfer` method of the + /// wrapped token from the `HypErc20CollateralComponent`. The recipient receives the transferred amount. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount` - A `u256` representing the amount of tokens to transfer. fn fast_transfer_to_hook( ref self: FastTokenRouterComponent::ComponentState, recipient: u256, @@ -166,6 +187,15 @@ pub mod FastHypERC20Collateral { .transfer(recipient.try_into().expect('u256 to ContractAddress failed'), amount); } + /// Receives tokens from the sender as part of the fast token router process. + /// + /// This function handles the receipt of tokens from the sender by calling the `transfer_from` method + /// of the wrapped token within the `HypErc20CollateralComponent`. + /// + /// # Arguments + /// + /// * `sender` - A `ContractAddress` representing the sender's address. + /// * `amount` - A `u256` representing the amount of tokens to receive. fn fast_receive_from_hook( ref self: FastTokenRouterComponent::ComponentState, sender: ContractAddress, diff --git a/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo b/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo index e177632..4fc1af1 100644 --- a/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo +++ b/cairo/src/contracts/token/extensions/hyp_erc20_collateral_vault_deposit.cairo @@ -134,6 +134,11 @@ pub mod HypERC20CollateralVaultDeposit { impl HypERC20CollateralVaultDepositImpl of super::IHypERC20CollateralVaultDeposit< ContractState > { + /// Sweeps excess shares from the vault. + /// + /// This function checks for excess shares in the vault, which are shares that exceed the amount + /// that was initially deposited. It redeems these excess shares and transfers the redeemed assets + /// to the contract owner. The function emits an `ExcessSharesSwept` event after completing the sweep. fn sweep(ref self: ContractState) { self.ownable.assert_only_owner(); let this_address = starknet::get_contract_address(); @@ -148,10 +153,26 @@ pub mod HypERC20CollateralVaultDeposit { ); } + /// Returns the contract address of the vault. + /// + /// This function retrieves the contract address of the vault that is being used for collateral + /// deposits and withdrawals. + /// + /// # Returns + /// + /// A `ContractAddress` representing the vault's contract address. fn get_vault(self: @ContractState) -> ContractAddress { self.vault.read().contract_address } + /// Returns the total amount of assets deposited in the vault. + /// + /// This function returns the total amount of assets that have been deposited into the vault by + /// this contract. + /// + /// # Returns + /// + /// A `u256` representing the total assets deposited. fn get_asset_deposited(self: @ContractState) -> u256 { self.asset_deposited.read() } @@ -186,12 +207,28 @@ pub mod HypERC20CollateralVaultDeposit { #[generate_trait] impl InternalImpl of InternalTrait { + /// Deposits the specified amount into the vault. + /// + /// This internal function deposits the specified amount of assets into the vault and updates the + /// total amount of assets deposited by the contract. + /// + /// # Arguments + /// + /// * `amount` - A `u256` representing the amount of assets to deposit. fn _deposit_into_vault(ref self: ContractState, amount: u256) { let asset_deposited = self.asset_deposited.read(); self.asset_deposited.write(asset_deposited + amount); self.vault.read().deposit(amount, starknet::get_contract_address()); } + // Returns the total amount of assets deposited in the vault. + /// + /// This function returns the total amount of assets that have been deposited into the vault by + /// this contract. + /// + /// # Returns + /// + /// A `u256` representing the total assets deposited. fn _withdraw_from_vault(ref self: ContractState, amount: u256, recipient: ContractAddress) { let asset_deposited = self.asset_deposited.read(); self.asset_deposited.write(asset_deposited - amount); diff --git a/cairo/src/contracts/token/extensions/hyp_erc20_vault.cairo b/cairo/src/contracts/token/extensions/hyp_erc20_vault.cairo index 3124c63..22ed0a8 100644 --- a/cairo/src/contracts/token/extensions/hyp_erc20_vault.cairo +++ b/cairo/src/contracts/token/extensions/hyp_erc20_vault.cairo @@ -151,6 +151,24 @@ mod HypErc20Vault { } impl TokenRouterTransferRemoteHookImpl of TokenRouterTransferRemoteHookTrait { + /// Initiates a remote token transfer with optional hooks and metadata. + /// + /// This function handles the transfer of tokens to a recipient on a remote domain. It converts + /// the token amount to shares, generates the token message, and dispatches the message to the + /// specified destination. The transfer can optionally use a hook for additional processing. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `value` - A `u256` representing the value associated with the transfer. + /// * `hook_metadata` - An optional `Bytes` object containing metadata for the hook. + /// * `hook` - An optional `ContractAddress` representing the hook for additional processing. + /// + /// # Returns + /// + /// A `u256` representing the message ID of the dispatched transfer. fn _transfer_remote( ref self: TokenRouterComponent::ComponentState, destination: u32, @@ -218,32 +236,104 @@ mod HypErc20Vault { #[abi(embed_v0)] impl HypeErc20Vault of super::IHypErc20Vault { + // Converts a specified amount of assets to shares based on the current exchange rate. + /// + /// This function calculates the number of shares corresponding to the given amount of assets + /// by using the exchange rate stored in the contract. + /// + /// # Arguments + /// + /// * `amount` - A `u256` representing the amount of assets to convert. + /// + /// # Returns + /// + /// A `u256` representing the number of shares equivalent to the given amount of assets. fn assets_to_shares(self: @ContractState, amount: u256) -> u256 { math::mul_div(amount, PRECISION, self.exchange_rate.read()) } + /// Converts a specified number of shares to assets based on the current exchange rate. + /// + /// This function calculates the number of assets corresponding to the given number of shares + /// by using the exchange rate stored in the contract. + /// + /// # Arguments + /// + /// * `shares` - A `u256` representing the number of shares to convert. + /// + /// # Returns + /// + /// A `u256` representing the number of assets equivalent to the given number of shares. + /// fn shares_to_assets(self: @ContractState, shares: u256) -> u256 { math::mul_div(shares, self.exchange_rate.read(), PRECISION) } + /// Returns the balance of shares for the specified account. + /// + /// This function retrieves the number of shares owned by the given account. The shares are represented + /// by the balance in the ERC20 component. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose share balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the share balance of the specified account. fn share_balance_of(self: @ContractState, account: ContractAddress) -> u256 { self.erc20.balance_of(account) } + /// Returns the precision value used for calculations in the vault. + /// + /// This function returns the precision value applied to vault calculations, which is a constant + /// defined in the contract. + /// + /// # Returns + /// + /// A `u256` representing the precision value. fn get_precision(self: @ContractState) -> u256 { PRECISION } + /// Returns the collateral domain used by the vault. + /// + /// This function retrieves the collateral domain in which the vault operates, which is defined + /// at the time of contract deployment. + /// + /// # Returns + /// + /// A `u32` representing the collateral domain. fn get_collateral_domain(self: @ContractState) -> u32 { self.collateral_domain.read() } + /// Returns the current exchange rate between assets and shares. + /// + /// This function retrieves the current exchange rate used by the vault for converting assets + /// to shares and vice versa. + /// + /// # Returns + /// + /// A `u256` representing the exchange rate. fn get_exchange_rate(self: @ContractState) -> u256 { self.exchange_rate.read() } } impl MessageRecipientInternalHookImpl of IMessageRecipientInternalHookTrait { + /// Handles incoming messages and updates the exchange rate if necessary. + /// + /// This internal function processes messages received from remote domains. If the message + /// is from the collateral domain, it updates the vault's exchange rate based on the metadata + /// contained in the message. + /// + /// # Arguments + /// + /// * `origin` - A `u32` representing the origin domain of the message. + /// * `sender` - A `u256` representing the sender of the message. + /// * `message` - A `Bytes` object containing the message data. fn _handle( ref self: RouterComponent::ComponentState, origin: u32, diff --git a/cairo/src/contracts/token/extensions/hyp_erc20_vault_collateral.cairo b/cairo/src/contracts/token/extensions/hyp_erc20_vault_collateral.cairo index 4d71ae0..3f743b7 100644 --- a/cairo/src/contracts/token/extensions/hyp_erc20_vault_collateral.cairo +++ b/cairo/src/contracts/token/extensions/hyp_erc20_vault_collateral.cairo @@ -130,6 +130,24 @@ mod HypErc20VaultCollateral { } impl TokenRouterTransferRemoteHookImpl of TokenRouterTransferRemoteHookTrait { + /// Initiates a remote token transfer with optional hooks and metadata. + /// + /// This function handles the process of transferring tokens to a recipient on a remote domain. + /// It deposits the token amount into the vault, calculates the exchange rate, and appends it to the token metadata. + /// The transfer is then dispatched to the specified destination domain using the provided hook and metadata. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address on the remote domain. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `value` - A `u256` representing the value associated with the transfer. + /// * `hook_metadata` - An optional `Bytes` object containing metadata for the hook. + /// * `hook` - An optional `ContractAddress` representing the hook for additional processing. + /// + /// # Returns + /// + /// A `u256` representing the message ID of the dispatched transfer. fn _transfer_remote( ref self: TokenRouterComponent::ComponentState, destination: u32, @@ -165,6 +183,19 @@ mod HypErc20VaultCollateral { } impl TokenRouterHooksTraitImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender and generates metadata. + /// + /// This hook is invoked during the transfer of tokens from the sender as part of the token router process. + /// It generates metadata for the token transfer based on the amount or token ID provided and processes the + /// transfer by depositing the amount into the vault. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object representing the metadata associated with the token transfer. fn transfer_from_sender_hook( ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 ) -> Bytes { @@ -173,6 +204,17 @@ mod HypErc20VaultCollateral { ) } + /// Processes a token transfer to a recipient. + /// + /// This hook handles the transfer of tokens to the recipient as part of the token router process. It withdraws + /// the specified amount from the vault and transfers it to the recipient's address. The hook also processes any + /// associated metadata. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. fn transfer_to_hook( ref self: TokenRouterComponent::ComponentState, recipient: u256, @@ -194,6 +236,16 @@ mod HypErc20VaultCollateral { } impl HypeErc20VaultCollateral of super::IHypErc20VaultCollateral { + /// Rebases the vault collateral and sends a message to a remote domain. + /// + /// This function handles rebalancing the vault collateral by sending a rebase operation + /// to the specified remote domain. It sends a message indicating the amount of the rebase + /// without specifying a recipient (null recipient). + /// + /// # Arguments + /// + /// * `destination_domain` - A `u32` representing the destination domain to which the rebase message is sent. + /// * `value` - A `u256` representing the value to be used for the rebase operation. fn rebase(ref self: ContractState, destination_domain: u32, value: u256) { TokenRouterTransferRemoteHookImpl::_transfer_remote( ref self.token_router, @@ -206,14 +258,37 @@ mod HypErc20VaultCollateral { ); } + /// Returns the contract address of the vault. + /// + /// This function retrieves the vault's contract address where the ERC20 collateral is stored. + /// + /// # Returns + /// + /// A `ContractAddress` representing the vault's contract address. fn get_vault(self: @ContractState) -> ContractAddress { self.vault.read().contract_address } + // Returns the precision value used for calculations in the vault. + /// + /// This function returns the precision value that is applied to the vault's calculations, + /// which is a constant value. + /// + /// # Returns + /// + /// A `u256` representing the precision used in the vault. fn get_precision(self: @ContractState) -> u256 { PRECISION } + /// Returns the null recipient used in rebase operations. + /// + /// This function retrieves the null recipient, which is a constant used in certain vault operations, + /// particularly during rebase operations. + /// + /// # Returns + /// + /// A `u256` representing the null recipient. fn get_null_recipient(self: @ContractState) -> u256 { NULL_RECIPIENT } diff --git a/cairo/src/contracts/token/extensions/hyp_erc721_URI_collateral.cairo b/cairo/src/contracts/token/extensions/hyp_erc721_URI_collateral.cairo index 6472361..3633c6f 100644 --- a/cairo/src/contracts/token/extensions/hyp_erc721_URI_collateral.cairo +++ b/cairo/src/contracts/token/extensions/hyp_erc721_URI_collateral.cairo @@ -115,6 +115,19 @@ pub mod HypERC721URICollateral { } impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers the token from the sender and retrieves its metadata. + /// + /// This hook handles the transfer of a token from the sender and appends its URI to the metadata. + /// It retrieves the token URI from the ERC721 contract and appends it to the metadata for processing + /// as part of the transfer message. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the token ID being transferred. + /// + /// # Returns + /// + /// A `Bytes` object containing the token's URI as metadata. fn transfer_from_sender_hook( ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 ) -> Bytes { diff --git a/cairo/src/contracts/token/extensions/hyp_fiat_token.cairo b/cairo/src/contracts/token/extensions/hyp_fiat_token.cairo index 553028c..fc0d729 100644 --- a/cairo/src/contracts/token/extensions/hyp_fiat_token.cairo +++ b/cairo/src/contracts/token/extensions/hyp_fiat_token.cairo @@ -129,6 +129,18 @@ pub mod HypFiatToken { } impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender and burns them. + /// + /// This hook transfers tokens from the sender and then burns the corresponding amount of the fiat token. + /// It retrieves the token metadata and ensures that the correct amount is transferred from the sender. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object containing the metadata for the transfer. fn transfer_from_sender_hook( ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 ) -> Bytes { @@ -139,6 +151,16 @@ pub mod HypFiatToken { metadata } + /// Transfers tokens to the recipient and mints new fiat tokens. + /// + /// This hook transfers tokens to the recipient and mints the corresponding amount of fiat tokens + /// based on the provided amount or token ID. It ensures that the mint operation is successful. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. fn transfer_to_hook( ref self: TokenRouterComponent::ComponentState, recipient: u256, diff --git a/cairo/src/contracts/token/extensions/hyp_xerc20.cairo b/cairo/src/contracts/token/extensions/hyp_xerc20.cairo index 23f73ab..51dc8e0 100644 --- a/cairo/src/contracts/token/extensions/hyp_xerc20.cairo +++ b/cairo/src/contracts/token/extensions/hyp_xerc20.cairo @@ -127,6 +127,18 @@ pub mod HypXERC20 { } impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender, burns the xERC20 tokens, and returns metadata. + /// + /// This hook transfers tokens from the sender, burns the corresponding xERC20 tokens, and returns any metadata + /// associated with the transfer. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object representing the metadata associated with the transfer. fn transfer_from_sender_hook( ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 ) -> Bytes { @@ -140,6 +152,16 @@ pub mod HypXERC20 { BytesTrait::new_empty() } + /// Mints xERC20 tokens for the recipient and returns the transferred amount. + /// + /// This hook mints xERC20 tokens for the recipient based on the transferred amount of tokens and updates the + /// corresponding ERC20 balances. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. fn transfer_to_hook( ref self: TokenRouterComponent::ComponentState, recipient: u256, diff --git a/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo b/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo index 989dcfd..ac6acff 100644 --- a/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo +++ b/cairo/src/contracts/token/extensions/hyp_xerc20_lockbox.cairo @@ -136,6 +136,10 @@ pub mod HypXERC20Lockbox { #[abi(embed_v0)] impl HypXERC20LockboxImpl of super::IHypXERC20Lockbox { + /// Approves the lockbox for both the ERC20 and xERC20 tokens. + /// + /// This function approves the lockbox contract to handle the maximum allowed amount of both the ERC20 and xERC20 tokens. + /// It ensures that both the ERC20 and xERC20 tokens are authorized for transfer to the lockbox. fn approve_lockbox(ref self: ContractState) { let lockbox_address = self.lockbox.read().contract_address; assert!( @@ -148,9 +152,25 @@ pub mod HypXERC20Lockbox { "xerc20 lockbox approve failed" ); } + + /// Retrieves the contract address of the lockbox. + /// + /// This function returns the `ContractAddress` of the lockbox that has been approved for the ERC20 and xERC20 tokens. + /// + /// # Returns + /// + /// The `ContractAddress` of the lockbox. fn lockbox(ref self: ContractState) -> ContractAddress { self.lockbox.read().contract_address } + + /// Retrieves the contract address of the xERC20 token. + /// + /// This function returns the `ContractAddress` of the xERC20 token that is used in conjunction with the lockbox. + /// + /// # Returns + /// + /// The `ContractAddress` of the xERC20 token. fn xERC20(ref self: ContractState) -> ContractAddress { self.xerc20.read().contract_address } @@ -170,6 +190,18 @@ pub mod HypXERC20Lockbox { } impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender, deposits them into the lockbox, and burns the corresponding xERC20 tokens. + /// + /// This hook first transfers tokens from the sender, deposits them into the lockbox, and then burns the + /// corresponding xERC20 tokens associated with the transfer. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object representing the transfer metadata. fn transfer_from_sender_hook( ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 ) -> Bytes { @@ -182,6 +214,16 @@ pub mod HypXERC20Lockbox { BytesTrait::new_empty() } + /// Transfers tokens to the recipient, mints xERC20 tokens, and withdraws tokens from the lockbox. + /// + /// This hook first mints the corresponding xERC20 tokens and then withdraws the corresponding amount + /// of ERC20 tokens from the lockbox to the specified recipient. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. fn transfer_to_hook( ref self: TokenRouterComponent::ComponentState, recipient: u256, From 4a9f443b424cf906856f1d9ca37e4c3db2c2ea93 Mon Sep 17 00:00:00 2001 From: Sameer Kumar <34837873+cyberhawk12121@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:55:42 +0530 Subject: [PATCH 6/6] hyp_erc20_collateral_test (#111) * exposed token router impl * added function signature to support collateral token functions * collateral test * formatted --------- Co-authored-by: Sameer Kumar --- .../token/hyp_erc20_collateral.cairo | 6 +- cairo/src/tests/token/hyp_erc20/common.cairo | 6 + .../hyp_erc20/hyp_erc20_collateral_test.cairo | 155 ++++++++++++++++++ 3 files changed, 166 insertions(+), 1 deletion(-) diff --git a/cairo/src/contracts/token/hyp_erc20_collateral.cairo b/cairo/src/contracts/token/hyp_erc20_collateral.cairo index 29506c7..e2527b6 100644 --- a/cairo/src/contracts/token/hyp_erc20_collateral.cairo +++ b/cairo/src/contracts/token/hyp_erc20_collateral.cairo @@ -6,7 +6,8 @@ pub mod HypErc20Collateral { use hyperlane_starknet::contracts::client::router_component::RouterComponent; use hyperlane_starknet::contracts::token::components::{ token_router::{ - TokenRouterComponent, TokenRouterComponent::MessageRecipientInternalHookImpl + TokenRouterComponent, TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl }, hyp_erc20_collateral_component::{ HypErc20CollateralComponent, HypErc20CollateralComponent::TokenRouterHooksImpl @@ -51,6 +52,9 @@ pub mod HypErc20Collateral { HypErc20CollateralComponent::HypErc20CollateralInternalImpl; // Upgradeable impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; #[storage] struct Storage { diff --git a/cairo/src/tests/token/hyp_erc20/common.cairo b/cairo/src/tests/token/hyp_erc20/common.cairo index 9db0b1c..5502908 100644 --- a/cairo/src/tests/token/hyp_erc20/common.cairo +++ b/cairo/src/tests/token/hyp_erc20/common.cairo @@ -72,6 +72,12 @@ pub fn SYMBOL() -> ByteArray { #[starknet::interface] pub trait IHypERC20Test { + // Collateral + fn transfer_from_sender_hook(ref self: TContractState, amount_or_id: u256) -> Bytes; + fn transfer_to_hook( + ref self: TContractState, recipient: ContractAddress, amount: u256, metadata: Bytes + ) -> bool; + fn get_wrapped_token(self: @TContractState) -> ContractAddress; // MailboxClient fn set_hook(ref self: TContractState, _hook: ContractAddress); fn set_interchain_security_module(ref self: TContractState, _module: ContractAddress); diff --git a/cairo/src/tests/token/hyp_erc20/hyp_erc20_collateral_test.cairo b/cairo/src/tests/token/hyp_erc20/hyp_erc20_collateral_test.cairo index 8b13789..3cc020a 100644 --- a/cairo/src/tests/token/hyp_erc20/hyp_erc20_collateral_test.cairo +++ b/cairo/src/tests/token/hyp_erc20/hyp_erc20_collateral_test.cairo @@ -1 +1,156 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use hyperlane_starknet::contracts::client::gas_router_component::{ + GasRouterComponent::GasRouterConfig, IGasRouterDispatcher, IGasRouterDispatcherTrait +}; +use hyperlane_starknet::contracts::mocks::{ + test_post_dispatch_hook::{ + ITestPostDispatchHookDispatcher, ITestPostDispatchHookDispatcherTrait + }, + mock_mailbox::{IMockMailboxDispatcher, IMockMailboxDispatcherTrait}, + test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait}, + test_interchain_gas_payment::{ + ITestInterchainGasPaymentDispatcher, ITestInterchainGasPaymentDispatcherTrait + }, + mock_eth::{MockEthDispatcher, MockEthDispatcherTrait} +}; +use hyperlane_starknet::contracts::token::hyp_erc20_collateral::HypErc20Collateral; +use hyperlane_starknet::tests::setup::{ + OWNER, LOCAL_DOMAIN, DESTINATION_DOMAIN, RECIPIENT_ADDRESS, MAILBOX, DESTINATION_MAILBOX, + setup_protocol_fee, setup_mock_hook, PROTOCOL_FEE, INITIAL_SUPPLY, setup_mock_fee_hook, + setup_mock_ism, setup_mock_token +}; +use hyperlane_starknet::tests::token::hyp_erc20::common::{ + setup, Setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, DESTINATION, TRANSFER_AMT, ALICE, BOB, + perform_remote_transfer_with_emit, perform_remote_transfer_and_gas, E18, + IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, enroll_remote_router, + enroll_local_router, set_custom_gas_config, REQUIRED_VALUE, GAS_LIMIT +}; +use hyperlane_starknet::utils::utils::U256TryIntoContractAddress; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + start_prank, stop_prank, declare, ContractClassTrait, CheatTarget, spy_events, SpyOn +}; +use starknet::ContractAddress; +fn setup_hyp_erc20_collateral() -> (IHypERC20TestDispatcher, Setup) { + let setup = setup(); + let hyp_erc20_collateral_contract = declare("HypErc20Collateral").unwrap(); + let constructor_args: Array = array![ + setup.local_mailbox.contract_address.into(), + setup.primary_token.contract_address.into(), + ALICE().into(), + setup.noop_hook.contract_address.into(), + setup.primary_token.contract_address.into() // just a placeholder + ]; + + let (collateral_address, _) = hyp_erc20_collateral_contract.deploy(@constructor_args).unwrap(); + let collateral = IHypERC20TestDispatcher { contract_address: collateral_address }; + + // Enroll remote router + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + collateral.enroll_remote_router(DESTINATION, remote_token_address.into()); + stop_prank(CheatTarget::One(collateral.contract_address)); + + // Transfer tokens to collateral contract and ALICE + setup.primary_token.transfer(collateral.contract_address, 1000 * E18); + setup.primary_token.transfer(ALICE(), 1000 * E18); + let addr: felt252 = collateral.contract_address.into(); + // Enroll remote router for the remote token + setup.remote_token.enroll_remote_router(ORIGIN, addr.into()); + (collateral, setup) +} + +fn perform_remote_transfer_collateral( + setup: @Setup, + collateral: @IHypERC20TestDispatcher, + msg_value: u256, + extra_gas: u256, + amount: u256, + approve: bool +) { + // Approve + if approve { + start_prank(CheatTarget::One(*setup.primary_token.contract_address), ALICE()); + (*setup.primary_token).approve(*collateral.contract_address, amount); + stop_prank(CheatTarget::One(*setup.primary_token.contract_address)); + } + // Remote transfer + start_prank(CheatTarget::One(*collateral.contract_address), ALICE()); + let bob_felt: felt252 = BOB().into(); + let bob_address: u256 = bob_felt.into(); + (*collateral) + .transfer_remote(DESTINATION, bob_address, amount, msg_value, Option::None, Option::None); + + process_transfers_collateral(setup, collateral, BOB(), amount); + + let remote_token = IERC20Dispatcher { + contract_address: (*setup).remote_token.contract_address + }; + assert_eq!(remote_token.balance_of(BOB()), amount); + + stop_prank(CheatTarget::One(*collateral.contract_address)); +} + +fn process_transfers_collateral( + setup: @Setup, collateral: @IHypERC20TestDispatcher, recipient: ContractAddress, amount: u256 +) { + start_prank( + CheatTarget::One((*setup).remote_token.contract_address), + (*setup).remote_mailbox.contract_address + ); + let local_token_address: felt252 = (*collateral).contract_address.into(); + let mut message = BytesTrait::new_empty(); + message.append_address(recipient); + message.append_u256(amount); + (*setup).remote_token.handle(ORIGIN, local_token_address.into(), message); + stop_prank(CheatTarget::One((*setup).remote_token.contract_address)); +} + +#[test] +fn test_remote_transfer() { + let (collateral, setup) = setup_hyp_erc20_collateral(); + let balance_before = collateral.balance_of(ALICE()); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + perform_remote_transfer_collateral(@setup, @collateral, REQUIRED_VALUE, 0, TRANSFER_AMT, true); + stop_prank(CheatTarget::One(collateral.contract_address)); + // Check balance after transfer + assert_eq!( + collateral.balance_of(ALICE()), + balance_before - TRANSFER_AMT, + "Incorrect balance after transfer" + ); +} + +#[test] +#[should_panic] +fn test_remote_transfer_invalid_allowance() { + let (collateral, setup) = setup_hyp_erc20_collateral(); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + perform_remote_transfer_collateral(@setup, @collateral, REQUIRED_VALUE, 0, TRANSFER_AMT, false); + stop_prank(CheatTarget::One(collateral.contract_address)); +} + +#[test] +fn test_remote_transfer_with_custom_gas_config() { + let (collateral, setup) = setup_hyp_erc20_collateral(); + // Check balance before transfer + let balance_before = collateral.balance_of(ALICE()); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + // Set custom gas config + collateral.set_hook(setup.igp.contract_address); + let config = array![GasRouterConfig { domain: DESTINATION, gas: GAS_LIMIT }]; + collateral.set_destination_gas(Option::Some(config), Option::None, Option::None); + // Do a remote transfer + perform_remote_transfer_collateral( + @setup, @collateral, REQUIRED_VALUE, setup.igp.gas_price(), TRANSFER_AMT, true + ); + + stop_prank(CheatTarget::One(collateral.contract_address)); + // Check balance after transfer + assert_eq!( + collateral.balance_of(ALICE()), + balance_before - TRANSFER_AMT, + "Incorrect balance after transfer" + ); +}