From c0ef417c72a474e593b4a323a3fe659ea3964036 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 2 Aug 2024 10:40:03 -0500 Subject: [PATCH 01/11] start hooks section --- docs/modules/ROOT/pages/components.adoc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 33dbbe539..c6ffa0375 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -344,6 +344,19 @@ 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 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 <>. + +Here's an example of how an ERC20 contract can leverage hooks and ensure tokens cannot move when the contract is paused. + +[,cairo] +---- + +---- + === Custom implementations :erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] From 820ef78b6005b29d99511088dcc7b21722cadec4 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 7 Aug 2024 16:50:15 -0500 Subject: [PATCH 02/11] finish hooks section draft --- docs/modules/ROOT/pages/components.adoc | 73 ++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index c6ffa0375..09dcc69f6 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -346,17 +346,88 @@ WARNING: Customizing implementations and accessing component storage can potenti === 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 <>. -Here's an example of how an ERC20 contract can leverage hooks and ensure tokens cannot move when the contract is paused. +All token components include the generic HooksTrait. +When creating a token contract, the using contract must create an implementation of the HooksTrait. +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 an hooks implementation + impl ERC20HooksImpl of ERC20Component::ERC20HooksTrait { + // Occurs before any token transfer + 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); + // Function from integrated component + contract_state.pausable.assert_not_paused(); + } + // Occurs after any token transfer + fn after_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) {} + } + + (...) +} ---- +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 (through ERC20Component in this example). +From here, the hook can access the using contract's integrated components, storage, and implementations. + +Even if a contract does not want to use hooks, the `HooksTrait` must still be implemented. +The Contracts for Cairo library offers default empty hook implementations which simplifies this process. +Rather than defining the hooks implementation, the using contract just needs to bring the empty implementation into scope like this: + +[,cairo] +---- +#[starknet::contract] +mod MyToken { + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::ERC20HooksEmptyImpl; + + (...) +} +---- + +See {hooks-post} for a more in-depth guide. + === Custom implementations :erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] From 331ea600005c26383af14056796b996af3da2565 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 7 Aug 2024 22:35:35 -0500 Subject: [PATCH 03/11] improve instructions for default impls --- docs/modules/ROOT/pages/components.adoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 09dcc69f6..4b62316e5 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -411,9 +411,10 @@ Instead of passing the component state, the using contract's state can be passed The hook then moves the scope up with the Cairo-generated `get_contract` through the `HasComponent` trait (through ERC20Component in this example). From here, the hook can access the using contract's integrated components, storage, and implementations. -Even if a contract does not want to use hooks, the `HooksTrait` must still be implemented. -The Contracts for Cairo library offers default empty hook implementations which simplifies this process. -Rather than defining the hooks implementation, the using contract just needs to bring the empty implementation into scope like this: +Be advised that even if a token contract does not require hooks, the `HooksTrait` must still be implemented. +Contracts may implement the trait with empty functions. +The Contracts for Cairo library, however, offers default empty hook implementations to abstract this away from contracts. +Rather than defining the hooks implementation, the using contract just needs to bring the default implementation into scope like this: [,cairo] ---- From d7badf3996627390bfbf1f9d825f4128ebe82a65 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 8 Aug 2024 23:27:15 -0500 Subject: [PATCH 04/11] clean up snippet comments --- docs/modules/ROOT/pages/components.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 4b62316e5..134f35234 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -378,9 +378,9 @@ mod MyToken { impl PausableImpl = PausableComponent::PausableImpl; impl PausableInternalImpl = PausableComponent::InternalImpl; - // Create an hooks implementation + // Create the hooks implementation impl ERC20HooksImpl of ERC20Component::ERC20HooksTrait { - // Occurs before any token transfer + // Occurs before token transfers fn before_update( ref self: ERC20Component::ComponentState, from: ContractAddress, @@ -389,11 +389,11 @@ mod MyToken { ) { // Access local state from component state let contract_state = ERC20Component::HasComponent::get_contract(@self); - // Function from integrated component + // Call function from integrated component contract_state.pausable.assert_not_paused(); } - // Occurs after any token transfer + // Occurs after token transfers fn after_update( ref self: ERC20Component::ComponentState, from: ContractAddress, From eef700bf9618ab40a78887fdd69e9d0632c77bf2 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 8 Aug 2024 23:31:59 -0500 Subject: [PATCH 05/11] tidy up doc --- docs/modules/ROOT/pages/components.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 134f35234..7405926f3 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -352,8 +352,8 @@ Hooks are entrypoints to the business logic of a token component that are access 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 the generic HooksTrait. -When creating a token contract, the using contract must create an implementation of the HooksTrait. +All token components include a generic hooks trait. +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. @@ -411,7 +411,7 @@ Instead of passing the component state, the using contract's state can be passed The hook then moves the scope up with the Cairo-generated `get_contract` through the `HasComponent` trait (through 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 `HooksTrait` must still be implemented. +Be advised that even if a token contract does not require hooks, the hooks trait must still be implemented. Contracts may implement the trait with empty functions. The Contracts for Cairo library, however, offers default empty hook implementations to abstract this away from contracts. Rather than defining the hooks implementation, the using contract just needs to bring the default implementation into scope like this: From 06691090fc8a2d3ef6764ac2ec16ab9ababda293 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Mon, 12 Aug 2024 01:15:11 -0500 Subject: [PATCH 06/11] Apply suggestions from code review Co-authored-by: immrsd <103599616+immrsd@users.noreply.github.com> --- docs/modules/ROOT/pages/components.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 7405926f3..66940b3e6 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -408,7 +408,7 @@ mod MyToken { 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 (through ERC20Component in this example). +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. From 98045d29cedce29b7967e2429fce577ee5e65983 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 Aug 2024 01:17:57 -0500 Subject: [PATCH 07/11] apply suggestion to sentence --- docs/modules/ROOT/pages/components.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 66940b3e6..d12a701d1 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -413,7 +413,7 @@ From here, the hook can access the using contract's integrated components, stora Be advised that even if a token contract does not require hooks, the hooks trait must still be implemented. Contracts may implement the trait with empty functions. -The Contracts for Cairo library, however, offers default empty hook implementations to abstract this away from contracts. +However, the Contracts for Cairo library offers default empty hook implementations to abstract this away from contracts. Rather than defining the hooks implementation, the using contract just needs to bring the default implementation into scope like this: [,cairo] From 23cafea188aaebb2c8953ad39a4593fc0d06b2da Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 Aug 2024 11:14:53 -0500 Subject: [PATCH 08/11] use default fns in hook traits --- packages/token/src/erc1155/erc1155.cairo | 22 +++------------------- packages/token/src/erc20/erc20.cairo | 20 +++----------------- packages/token/src/erc721/erc721.cairo | 22 +++++----------------- 3 files changed, 11 insertions(+), 53 deletions(-) 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 {} From beb90e666f492097451f7d47d8d8407c3aaac7d9 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 Aug 2024 12:06:09 -0500 Subject: [PATCH 09/11] update hooks section mentioning default fns --- docs/modules/ROOT/pages/components.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index d12a701d1..0f11cce50 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -352,7 +352,7 @@ Hooks are entrypoints to the business logic of a token component that are access 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. +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. @@ -412,9 +412,9 @@ The hook then moves the scope up with the Cairo-generated `get_contract` through 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. -Contracts may implement the trait with empty functions. -However, the Contracts for Cairo library offers default empty hook implementations to abstract this away from contracts. -Rather than defining the hooks implementation, the using contract just needs to bring the default implementation into scope like this: +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] ---- From 6b525f0c8dd86f32d97f3fb1754201212a5d689e Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 Aug 2024 12:26:58 -0500 Subject: [PATCH 10/11] add TIP block --- docs/modules/ROOT/pages/components.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 0f11cce50..449fdf697 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -427,7 +427,7 @@ mod MyToken { } ---- -See {hooks-post} for a more in-depth guide. +TIP: For a more in-depth guide on hooks, see {hooks-post}. === Custom implementations From 4104333884961cec7a06d3a40d8feffdf53eb86d Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 12 Aug 2024 14:31:32 -0500 Subject: [PATCH 11/11] remove unnecessary hooks --- docs/modules/ROOT/pages/components.adoc | 9 ++------- packages/token/src/tests/mocks/erc20_votes_mocks.cairo | 7 ------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 449fdf697..75c53e7a0 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -393,13 +393,8 @@ mod MyToken { contract_state.pausable.assert_not_paused(); } - // Occurs after token transfers - fn after_update( - ref self: ERC20Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) {} + // Omitting the `after_update` hook because the default behavior + // is already implemented in the trait } (...) 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,