Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hooks section #1090

Merged
merged 12 commits into from
Aug 15, 2024
80 changes: 80 additions & 0 deletions docs/modules/ROOT/pages/components.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
immrsd marked this conversation as resolved.
Show resolved Hide resolved

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 <<custom_implementations, custom implementations>>.

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<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;

#[abi(embed_v0)]
impl PausableImpl = PausableComponent::PausableImpl<ContractState>;
impl PausableInternalImpl = PausableComponent::InternalImpl<ContractState>;

// Create the hooks implementation
impl ERC20HooksImpl of ERC20Component::ERC20HooksTrait<ContractState> {
// Occurs before token transfers
fn before_update(
ref self: ERC20Component::ComponentState<ContractState>,
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]
Expand Down
22 changes: 3 additions & 19 deletions packages/token/src/erc1155/erc1155.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,15 @@ pub mod ERC1155Component {
to: ContractAddress,
token_ids: Span<u256>,
values: Span<u256>
);
) {}

fn after_update(
ref self: ComponentState<TContractState>,
from: ContractAddress,
to: ContractAddress,
token_ids: Span<u256>,
values: Span<u256>
);
) {}
}

//
Expand Down Expand Up @@ -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<TContractState> {
fn before_update(
ref self: ERC1155Component::ComponentState<TContractState>,
from: ContractAddress,
to: ContractAddress,
token_ids: Span<u256>,
values: Span<u256>
) {}

fn after_update(
ref self: ERC1155Component::ComponentState<TContractState>,
from: ContractAddress,
to: ContractAddress,
token_ids: Span<u256>,
values: Span<u256>
) {}
}
> of ERC1155Component::ERC1155HooksTrait<TContractState> {}
20 changes: 3 additions & 17 deletions packages/token/src/erc20/erc20.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ pub mod ERC20Component {
from: ContractAddress,
recipient: ContractAddress,
amount: u256
);
) {}

fn after_update(
ref self: ComponentState<TContractState>,
from: ContractAddress,
recipient: ContractAddress,
amount: u256
);
) {}
}

//
Expand Down Expand Up @@ -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<TContractState> of ERC20Component::ERC20HooksTrait<TContractState> {
fn before_update(
ref self: ERC20Component::ComponentState<TContractState>,
from: ContractAddress,
recipient: ContractAddress,
amount: u256
) {}

fn after_update(
ref self: ERC20Component::ComponentState<TContractState>,
from: ContractAddress,
recipient: ContractAddress,
amount: u256
) {}
}
pub impl ERC20HooksEmptyImpl<TContractState> of ERC20Component::ERC20HooksTrait<TContractState> {}
22 changes: 5 additions & 17 deletions packages/token/src/erc721/erc721.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ pub mod ERC721Component {
to: ContractAddress,
token_id: u256,
auth: ContractAddress
);
) {}

fn after_update(
ref self: ComponentState<TContractState>,
to: ContractAddress,
token_id: u256,
auth: ContractAddress
);
) {}
}

//
Expand Down Expand Up @@ -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<TContractState> of ERC721Component::ERC721HooksTrait<TContractState> {
fn before_update(
ref self: ERC721Component::ComponentState<TContractState>,
to: ContractAddress,
token_id: u256,
auth: ContractAddress
) {}

fn after_update(
ref self: ERC721Component::ComponentState<TContractState>,
to: ContractAddress,
token_id: u256,
auth: ContractAddress
) {}
}
pub impl ERC721HooksEmptyImpl<
TContractState
> of ERC721Component::ERC721HooksTrait<TContractState> {}
7 changes: 0 additions & 7 deletions packages/token/src/tests/mocks/erc20_votes_mocks.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,6 @@ pub(crate) mod DualCaseERC20VotesMock {
+NoncesComponent::HasComponent<TContractState>,
+Drop<TContractState>
> of ERC20Component::ERC20HooksTrait<TContractState> {
fn before_update(
ref self: ERC20Component::ComponentState<TContractState>,
from: ContractAddress,
recipient: ContractAddress,
amount: u256
) {}

fn after_update(
ref self: ERC20Component::ComponentState<TContractState>,
from: ContractAddress,
Expand Down