diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 33dbbe539..75c53e7a0 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -344,6 +344,86 @@ mod MyContract { WARNING: Customizing implementations and accessing component storage can potentially corrupt the state, bypass security checks, and undermine the component logic. *Exercise extreme caution*. See {security}. +=== Hooks + +:hooks-post: https://fleming-andrew.medium.com/extending-cairo-contracts-with-hooks-c3ca21d1d6b8[Extending Cairo Contracts with Hooks] + +Hooks are entrypoints to the business logic of a token component that are accessible at the contract level. +This allows contracts to insert additional behaviors before and/or after token transfers (including mints and burns). +Prior to hooks, extending functionality required contracts to create <>. + +All token components include a generic hooks trait that include empty default functions. +When creating a token contract, the using contract must create an implementation of the hooks trait. +Suppose an ERC20 contract wanted to include Pausable functionality on token transfers. +The following snippet leverages the `before_update` hook to include this behavior. + +[,cairo] +---- +#[starknet::contract] +mod MyToken { + use openzeppelin::security::pausable::PausableComponent::InternalTrait; + use openzeppelin::security::pausable::PausableComponent; + use openzeppelin::token::erc20::ERC20Component; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + + // ERC20 Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + // Create the hooks implementation + impl ERC20HooksImpl of ERC20Component::ERC20HooksTrait { + // Occurs before token transfers + fn before_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + // Access local state from component state + let contract_state = ERC20Component::HasComponent::get_contract(@self); + // Call function from integrated component + contract_state.pausable.assert_not_paused(); + } + + // Omitting the `after_update` hook because the default behavior + // is already implemented in the trait + } + + (...) +} +---- + +Notice that the `self` parameter expects a component state type. +Instead of passing the component state, the using contract's state can be passed which simplifies the syntax. +The hook then moves the scope up with the Cairo-generated `get_contract` through the `HasComponent` trait (as illustrated with ERC20Component in this example). +From here, the hook can access the using contract's integrated components, storage, and implementations. + +Be advised that even if a token contract does not require hooks, the hooks trait must still be implemented. +The using contract may instantiate an empty impl of the trait; +however, the Contracts for Cairo library already provides the instantiated impl to abstract this away from contracts. +The using contract just needs to bring the implementation into scope like this: + +[,cairo] +---- +#[starknet::contract] +mod MyToken { + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::ERC20HooksEmptyImpl; + + (...) +} +---- + +TIP: For a more in-depth guide on hooks, see {hooks-post}. + === Custom implementations :erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] diff --git a/packages/token/src/erc1155/erc1155.cairo b/packages/token/src/erc1155/erc1155.cairo index 49fef7e5d..9bcc5991f 100644 --- a/packages/token/src/erc1155/erc1155.cairo +++ b/packages/token/src/erc1155/erc1155.cairo @@ -110,7 +110,7 @@ pub mod ERC1155Component { to: ContractAddress, token_ids: Span, values: Span - ); + ) {} fn after_update( ref self: ComponentState, @@ -118,7 +118,7 @@ pub mod ERC1155Component { to: ContractAddress, token_ids: Span, values: Span - ); + ) {} } // @@ -719,20 +719,4 @@ pub mod ERC1155Component { /// An empty implementation of the ERC1155 hooks to be used in basic ERC1155 preset contracts. pub impl ERC1155HooksEmptyImpl< TContractState -> of ERC1155Component::ERC1155HooksTrait { - fn before_update( - ref self: ERC1155Component::ComponentState, - from: ContractAddress, - to: ContractAddress, - token_ids: Span, - values: Span - ) {} - - fn after_update( - ref self: ERC1155Component::ComponentState, - from: ContractAddress, - to: ContractAddress, - token_ids: Span, - values: Span - ) {} -} +> of ERC1155Component::ERC1155HooksTrait {} diff --git a/packages/token/src/erc20/erc20.cairo b/packages/token/src/erc20/erc20.cairo index cc65e158b..528c4f6e6 100644 --- a/packages/token/src/erc20/erc20.cairo +++ b/packages/token/src/erc20/erc20.cairo @@ -79,14 +79,14 @@ pub mod ERC20Component { from: ContractAddress, recipient: ContractAddress, amount: u256 - ); + ) {} fn after_update( ref self: ComponentState, from: ContractAddress, recipient: ContractAddress, amount: u256 - ); + ) {} } // @@ -436,18 +436,4 @@ pub mod ERC20Component { } /// An empty implementation of the ERC20 hooks to be used in basic ERC20 preset contracts. -pub impl ERC20HooksEmptyImpl of ERC20Component::ERC20HooksTrait { - fn before_update( - ref self: ERC20Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) {} - - fn after_update( - ref self: ERC20Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) {} -} +pub impl ERC20HooksEmptyImpl of ERC20Component::ERC20HooksTrait {} diff --git a/packages/token/src/erc721/erc721.cairo b/packages/token/src/erc721/erc721.cairo index 534404386..cd637d9d8 100644 --- a/packages/token/src/erc721/erc721.cairo +++ b/packages/token/src/erc721/erc721.cairo @@ -96,14 +96,14 @@ pub mod ERC721Component { to: ContractAddress, token_id: u256, auth: ContractAddress - ); + ) {} fn after_update( ref self: ComponentState, to: ContractAddress, token_id: u256, auth: ContractAddress - ); + ) {} } // @@ -825,18 +825,6 @@ pub mod ERC721Component { } /// An empty implementation of the ERC721 hooks to be used in basic ERC721 preset contracts. -pub impl ERC721HooksEmptyImpl of ERC721Component::ERC721HooksTrait { - fn before_update( - ref self: ERC721Component::ComponentState, - to: ContractAddress, - token_id: u256, - auth: ContractAddress - ) {} - - fn after_update( - ref self: ERC721Component::ComponentState, - to: ContractAddress, - token_id: u256, - auth: ContractAddress - ) {} -} +pub impl ERC721HooksEmptyImpl< + TContractState +> of ERC721Component::ERC721HooksTrait {} diff --git a/packages/token/src/tests/mocks/erc20_votes_mocks.cairo b/packages/token/src/tests/mocks/erc20_votes_mocks.cairo index a2a79ca18..f657b545a 100644 --- a/packages/token/src/tests/mocks/erc20_votes_mocks.cairo +++ b/packages/token/src/tests/mocks/erc20_votes_mocks.cairo @@ -67,13 +67,6 @@ pub(crate) mod DualCaseERC20VotesMock { +NoncesComponent::HasComponent, +Drop > of ERC20Component::ERC20HooksTrait { - fn before_update( - ref self: ERC20Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) {} - fn after_update( ref self: ERC20Component::ComponentState, from: ContractAddress,