From 600737a791e7c4838dc4277222c062657a72bc65 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 21 Nov 2023 09:55:36 -0500 Subject: [PATCH 01/20] add extensibility docs --- docs/modules/ROOT/nav.adoc | 2 +- docs/modules/ROOT/pages/extensibility.adoc | 166 ++------------------- 2 files changed, 13 insertions(+), 155 deletions(-) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index e1b2dc12a..067c9a2c9 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,6 +1,6 @@ * xref:index.adoc[Overview] //* xref:wizard.adoc[Wizard] -//* xref:extensibility.adoc[Extensibility] +* xref:extensibility.adoc[Extensibility] * xref:interfaces.adoc[Interfaces and Dispatchers] * xref:upgrades.adoc[Upgrades] ** xref:/api/upgrades.adoc[API Reference] diff --git a/docs/modules/ROOT/pages/extensibility.adoc b/docs/modules/ROOT/pages/extensibility.adoc index 7b119ff01..30f9c9ffe 100644 --- a/docs/modules/ROOT/pages/extensibility.adoc +++ b/docs/modules/ROOT/pages/extensibility.adoc @@ -1,166 +1,24 @@ = Extensibility -NOTE: Expect this pattern to evolve (as it has already done) or even disappear if https://community.starknet.io/t/contract-extensibility-pattern/210/11?u=martriay[proper extensibility features] are implemented into Cairo. - -== Table of Contents - -* <> -* <> - ** <> - ** <> -* <> -* <> -* <> - -== The extensibility problem - Smart contract development is a critical task. As with all software development, it is error prone; but unlike most scenarios, a bug can result in major losses for organizations as well as individuals. -Therefore writing complex smart contracts is a delicate task. - -One of the best approaches to minimize introducing bugs is to reuse existing, battle-tested code, a.k.a. -using libraries. -But code reutilization in StarkNet's smart contracts is not easy: - -* Cairo has no explicit smart contract extension mechanisms such as inheritance or composability. -* Using imports for modularity can result in clashes (more so given that arguments are not part of the selector), and lack of overrides or aliasing leaves no way to resolve them. -* Any `@external` function defined in an imported module will be automatically re-exposed by the importer (i.e. -the smart contract). - -To overcome these problems, this project builds on the following guidelines™. - -== The pattern - -The idea is to have two types of Cairo modules: libraries and contracts. -Libraries define reusable logic and storage variables which can then be extended and exposed by contracts. -Contracts can be deployed, libraries cannot. - -To minimize risk, boilerplate, and avoid function naming clashes, we follow these rules: - -=== Libraries - -Considering the following types of functions: - -* `private`: private to a library, not meant to be used outside the module or imported. -* `public`: part of the public API of a library. -* `internal`: subset of `public` that is either discouraged or potentially unsafe (e.g. -`_transfer` on ERC20). -* `external`: subset of `public` that is ready to be exported as-is by contracts (e.g. -`transfer` on ERC20). -* `storage`: storage variable functions. - -Then: - -* Must implement `public` and `external` functions under a namespace. -* Must implement `private` functions outside the namespace to avoid exposing them. -* Must prefix `internal` functions with an underscore (e.g. -`ERC20._mint`). -* Must not prefix `external` functions with an underscore (e.g. -`ERC20.transfer`). -* Must prefix `storage` functions with the name of the namespace to prevent clashing with other libraries (e.g. -`ERC20balances`). -* Must not implement any `@external`, `@view`, or `@constructor` functions. -* Can implement initializers (never as `@constructor` or `@external`). -* Must not call initializers on any function. - -=== Contracts - -* Can import from libraries. -* Should implement `@external` functions if needed. -* Should implement a `@constructor` function that calls initializers. -* Must not call initializers in any function beside the constructor. - -Note that since initializers will never be marked as `@external` and they won't be called from anywhere but the constructor, there's no risk of re-initialization after deployment. -It's up to the library developers not to make initializers interdependent to avoid weird dependency paths that may lead to double construction of libraries. - -== Presets - -Presets are pre-written contracts that extend from our library of contracts. -They can be deployed as-is or used as templates for customization. - -Some presets are: - -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/account/presets/Account.cairo[Account] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/tests/mocks/ERC165.cairo[ERC165] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/token/erc20/presets/ERC20Mintable.cairo[ERC20Mintable] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/token/erc20/presets/ERC20Pausable.cairo[ERC20Pausable] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/token/erc20/presets/ERC20Upgradeable.cairo[ERC20Upgradeable] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/token/erc20/presets/ERC20.cairo[ERC20] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/token/erc721/presets/ERC721MintableBurnable.cairo[ERC721MintableBurnable] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/token/erc721/presets/ERC721MintablePausable.cairo[ERC721MintablePausable] -* https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.6.1/src/openzeppelin/token/erc721/enumerable/presets/ERC721EnumerableMintableBurnable.cairo[ERC721EnumerableMintableBurnable] - -In previous versions of Cairo importing any function from a module would automatically import all `@external` functions. We used to leverage this behavior to just import the constructor of the preset contract to load it. -Since Cairo v0.10, however, contracts that utilize the preset contracts should import all of the exposed functions from it. For example: - -[,cairo] -``` -%lang starknet - -from openzeppelin.token.erc20.presets.ERC20 import constructor -``` - -should now look like: - -[,cairo] -``` -%lang starknet - -from openzeppelin.token.erc20.presets.ERC20 import ( - constructor, - name, - symbol, - totalSupply, - decimals, - balanceOf, - allowance, - transfer, - transferFrom, - approve, - increaseAllowance, - decreaseAllowance -) -``` - - -== Function names and coding style - -* Following Cairo's programming style, we use `snake_case` for library APIs (e.g. -`ERC20.transfer_from`, `ERC721.safe_transfer_from`). -* But for standard EVM ecosystem compatibility, we implement external functions in contracts using `camelCase` (e.g. -`transferFrom` in a ERC20 contract). -* Guard functions such as the so-called "only owner" are prefixed with `assert_` (e.g. -`Ownable.assert_only_owner`). +Therefore, writing complex smart contracts is a delicate task. -== Emulating hooks +One of the best approaches to minimize introducing bugs is to reuse existing, battle-tested code. +Starknet uses components to allow developers to easily integrate extensible code into their contracts. -Unlike the Solidity version of https://github.com/OpenZeppelin/openzeppelin-contracts[OpenZeppelin Contracts], this library does not implement https://docs.openzeppelin.com/contracts/4.x/extending-contracts#using-hooks[hooks]. -The main reason being that Cairo does not support overriding functions. +== Components -This is what a hook looks like in Solidity: +Starknet components are separate modules that contain storage, events, and external functions that can be integrated into a contract. +Components themselves cannot be declared or deployed. +Another way to think of components is that they are abstract modules that must be instantiated. -[,solidity] ----- -abstract contract ERC20Pausable is ERC20, Pausable { - function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { - super._beforeTokenTransfer(from, to, amount); +== Integrating components - require(!paused(), "ERC20Pausable: token transfer while paused"); - } -} ----- -Instead, the extensibility pattern allows us to simply extend the library implementation of a function (namely `transfer`) by adding lines before or after calling it. -This way, we can get away with: +Component integration requires the following steps: -[,cairo] ----- -@external -func transfer{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( - recipient: felt, amount: Uint256 -) -> (success: felt) { - Pausable.assert_not_paused(); - return ERC20.transfer(recipient, amount); -} ----- +1. Declare the component with `component!` +2. Add the component's storage and events to the contract's `Storage` and `Event` types. +3. Bring the requisite implementations into scope From 22019dce1184d6d92b9eb47281bf33a6a89cfd26 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 22 Nov 2023 09:55:24 -0500 Subject: [PATCH 02/20] start setup guide --- docs/modules/ROOT/pages/extensibility.adoc | 63 ++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/extensibility.adoc b/docs/modules/ROOT/pages/extensibility.adoc index 30f9c9ffe..8d70c559d 100644 --- a/docs/modules/ROOT/pages/extensibility.adoc +++ b/docs/modules/ROOT/pages/extensibility.adoc @@ -16,9 +16,64 @@ Another way to think of components is that they are abstract modules that must b == Integrating components - Component integration requires the following steps: -1. Declare the component with `component!` -2. Add the component's storage and events to the contract's `Storage` and `Event` types. -3. Bring the requisite implementations into scope +1. Declare the component with `component!` macro. +2. Add the requisite implementations and (if applicable) embed the external implementations. +3. Add the component's storage and events to the contract's `Storage` and `Event` types. + +In order to integrate a component into a contract, + +=== Setup + +Integrating components into a contract must include the following: + +1. Declare the component with the `component!` macro. +2. Add the requisite implementations and (if applicable) add the embed macro for external implementations. +3. Add the component's storage and events to the contract's `Storage` and `Event` types. + +Let's start with declaring the component with the `component!` macro: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + // Import the Contracts for Cairo component + use openzeppelin::access::ownable::OwnableComponent; + use starknet::ContractAddress; + + // Declare the component + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); +} +---- + +The `path` argument should be the imported component itself (in this case, `OwnableComponent`). +Notice that the `storage` and `event` arguments are actually defined within the macro. +In other words, the `ownable` and `OwnableEvent` names follow Contracts for Cairo convention, but they can be renamed. + +Next, we need to add the implementations from the component: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + + (...) + + // Methods within these `impl`s are exposed + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + #[abi(embed_v0)] + impl OwnableCamelOnlyImpl = + OwnableComponent::OwnableCamelOnlyImpl; + + // Methods within this `impl` are accessible only by the contract + impl InternalImpl = OwnableComponent::InternalImpl; +} +---- + +The local `impl`s of the component's `impl`s allow the contract to access the methods within that implementation. + + +=== Accessing component state + From 34ca611b9eb350dde1cb5da790797837b106b6f6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 24 Nov 2023 12:00:21 -0500 Subject: [PATCH 03/20] change file name --- docs/modules/ROOT/nav.adoc | 2 +- docs/modules/ROOT/pages/components.adoc | 186 +++++++++++++++++++++ docs/modules/ROOT/pages/extensibility.adoc | 79 --------- 3 files changed, 187 insertions(+), 80 deletions(-) create mode 100644 docs/modules/ROOT/pages/components.adoc delete mode 100644 docs/modules/ROOT/pages/extensibility.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 067c9a2c9..41ca9eee0 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,6 +1,6 @@ * xref:index.adoc[Overview] //* xref:wizard.adoc[Wizard] -* xref:extensibility.adoc[Extensibility] +* xref:components.adoc[Using Components] * xref:interfaces.adoc[Interfaces and Dispatchers] * xref:upgrades.adoc[Upgrades] ** xref:/api/upgrades.adoc[API Reference] diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc new file mode 100644 index 000000000..dcdb2caf6 --- /dev/null +++ b/docs/modules/ROOT/pages/components.adoc @@ -0,0 +1,186 @@ += Using Components + +The following documentation provides reasoning and examples on how to use Contracts for Cairo components. + +== Components + +:shamans-post: https://community.starknet.io/t/cairo-components/101136#components-1[Starknet Shamans post] + +Starknet components are separate modules that contain storage, events, and implementations that can be integrated into a contract. +Components themselves cannot be declared or deployed. +Another way to think of components is that they are abstract modules that must be instantiated. + +TIP: For more information on the construction and design of Starknet components, see the {shamans-post}. + +=== Setting up + +:initializable-component: xref:/security.adoc#initializable[InitializableComponent] + +Integrating a component first requires that the contract declares the component with the `component!` macro: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + // Import the Contracts for Cairo component + use openzeppelin::security::InitializableComponent; + + // Declare the component + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); +} +---- + +The `path` argument should be the imported component itself (in this case, {initializable-component}). +Notice that the `storage` and `event` arguments are representations set within the macro. +In other words, the `initializable` and `InitializableEvent` names follow the Contracts for Cairo convention, but they can be renamed. + +=== Accessing component state + +Before a contract can access a component's state, the contract must integrate the implementations that will be used. +For example: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + use openzeppelin::security::InitializableComponent; + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + + // Gives the contract access to the implementation methods + impl InitializableImpl = + InitializableComponent::InitializableImpl; + impl InternalImpl = InitializableComponent::InternalImpl; +} +---- + +Defining the ``impl``s give the contract access to the methods within those implementations from the component. +For example, `initialize` is defined in the `InternalImpl`. +It can, therefore, be called like this: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + (...) + + fn initialize_contract(ref self: ContractState) { + self.initializable.initialize(); + } +} +---- + +TIP: Defining the `InternalImpl` in a contract also gives the contract indirect access to the component's `Storage`. + +=== Embedding implementations + +A contract can embed implementations into the ABI which will expose the methods of the implementation. +To embed implementations, add the `#[abi(embed_v0)]` attribute above the `impl`: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + (...) + + // This attribute exposes the methods of the `impl` + #[abi(embed_v0)] + impl InitializableImpl = + InitializableComponent::InitializableImpl; +} +---- + +`InitializableImpl` defines the `is_initialized` method in the component. +By adding the embed attribute, `is_initialized` becomes a contract entrypoint for `MyContract`. + +=== Component storage and events + +The component's storage and events must be added to the contract's `Storage` trait and `Event` enum respectively. +If the component doesn't define any events, the compiler will still create an empty event enum inside the component module. + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + use openzeppelin::security::InitializableComponent; + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + + (...) + + #[storage] + struct Storage { + #[substorage(v0)] + initializable: InitializableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event + } +} +---- + +The `#[substorage(v0)]` attribute must be included for each component in the `Storage` trait. +This allows the contract to have indirect access to the component's storage. + +The `#[flat]` attribute for events in the `Event` enum, however, is not required. +Component events are not flattened in the component itself to offer greater flexibility regarding how events are handled. +Note that if contracts do not flatten component events, the first key in the event log will be the component ID. +By flattening the component event, the first key will be the event ID. + +=== Component dependencies + +:access-component: xref:/api/access.adoc#AccessControlComponent[AccessControlComponent] +:src5-component: xref:/api/introspection.adoc#SRC5Component[SRC5Component] + +Some components include dependencies of other components. +Contracts that integrate components with dependencies must also include the component dependency. +For instance, {access-component} depends on {src5-component}. +Creating a contract with `AccessControlComponent` should look like this: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + use openzeppelin::access::accesscontrol::AccessControlComponent; + use openzeppelin::introspection::src5::SRC5Component; + + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // AccessControl + #[abi(embed_v0)] + impl AccessControlImpl = + AccessControlComponent::AccessControlImpl; + #[abi(embed_v0)] + impl AccessControlCamelImpl = + AccessControlComponent::AccessControlCamelImpl; + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + struct Storage { + #[substorage(v0)] + accesscontrol: AccessControlComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccessControlEvent: AccessControlComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + (...) +} +---- diff --git a/docs/modules/ROOT/pages/extensibility.adoc b/docs/modules/ROOT/pages/extensibility.adoc deleted file mode 100644 index 8d70c559d..000000000 --- a/docs/modules/ROOT/pages/extensibility.adoc +++ /dev/null @@ -1,79 +0,0 @@ -= Extensibility - -Smart contract development is a critical task. -As with all software development, it is error prone; -but unlike most scenarios, a bug can result in major losses for organizations as well as individuals. -Therefore, writing complex smart contracts is a delicate task. - -One of the best approaches to minimize introducing bugs is to reuse existing, battle-tested code. -Starknet uses components to allow developers to easily integrate extensible code into their contracts. - -== Components - -Starknet components are separate modules that contain storage, events, and external functions that can be integrated into a contract. -Components themselves cannot be declared or deployed. -Another way to think of components is that they are abstract modules that must be instantiated. - -== Integrating components - -Component integration requires the following steps: - -1. Declare the component with `component!` macro. -2. Add the requisite implementations and (if applicable) embed the external implementations. -3. Add the component's storage and events to the contract's `Storage` and `Event` types. - -In order to integrate a component into a contract, - -=== Setup - -Integrating components into a contract must include the following: - -1. Declare the component with the `component!` macro. -2. Add the requisite implementations and (if applicable) add the embed macro for external implementations. -3. Add the component's storage and events to the contract's `Storage` and `Event` types. - -Let's start with declaring the component with the `component!` macro: - -[,javascript] ----- -#[starknet::contract] -mod MyContract { - // Import the Contracts for Cairo component - use openzeppelin::access::ownable::OwnableComponent; - use starknet::ContractAddress; - - // Declare the component - component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); -} ----- - -The `path` argument should be the imported component itself (in this case, `OwnableComponent`). -Notice that the `storage` and `event` arguments are actually defined within the macro. -In other words, the `ownable` and `OwnableEvent` names follow Contracts for Cairo convention, but they can be renamed. - -Next, we need to add the implementations from the component: - -[,javascript] ----- -#[starknet::contract] -mod MyContract { - - (...) - - // Methods within these `impl`s are exposed - #[abi(embed_v0)] - impl OwnableImpl = OwnableComponent::OwnableImpl; - #[abi(embed_v0)] - impl OwnableCamelOnlyImpl = - OwnableComponent::OwnableCamelOnlyImpl; - - // Methods within this `impl` are accessible only by the contract - impl InternalImpl = OwnableComponent::InternalImpl; -} ----- - -The local `impl`s of the component's `impl`s allow the contract to access the methods within that implementation. - - -=== Accessing component state - From f409e0e88af51c1a0229010f93fea2e3e6e91f28 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Sat, 25 Nov 2023 20:52:25 -0500 Subject: [PATCH 04/20] Apply suggestions from code review Co-authored-by: Eric Nordelo --- docs/modules/ROOT/pages/components.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index dcdb2caf6..a1ea7cb50 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -22,7 +22,7 @@ Integrating a component first requires that the contract declares the component ---- #[starknet::contract] mod MyContract { - // Import the Contracts for Cairo component + // Import the component use openzeppelin::security::InitializableComponent; // Declare the component @@ -95,7 +95,7 @@ By adding the embed attribute, `is_initialized` becomes a contract entrypoint fo === Component storage and events -The component's storage and events must be added to the contract's `Storage` trait and `Event` enum respectively. +The component's storage and events must be added to the contract's `Storage` struct and `Event` enum respectively. If the component doesn't define any events, the compiler will still create an empty event enum inside the component module. [,javascript] From 46191e252e12c150a8387968621218daaeed3e03 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 25 Nov 2023 21:00:13 -0500 Subject: [PATCH 05/20] fix name --- 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 a1ea7cb50..c899849e8 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -32,7 +32,7 @@ mod MyContract { The `path` argument should be the imported component itself (in this case, {initializable-component}). Notice that the `storage` and `event` arguments are representations set within the macro. -In other words, the `initializable` and `InitializableEvent` names follow the Contracts for Cairo convention, but they can be renamed. +In other words, the `initializable` and `InitializableEvent` names follow this library's convention, but they can be renamed. === Accessing component state From a9f307995a8171be40192fc80a73f180ad262722 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 29 Nov 2023 19:09:44 -0500 Subject: [PATCH 06/20] change title --- docs/modules/ROOT/nav.adoc | 2 +- docs/modules/ROOT/pages/{components.adoc => usage.adoc} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename docs/modules/ROOT/pages/{components.adoc => usage.adoc} (99%) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index e7e79bf4f..486865c6b 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,6 +1,6 @@ * xref:index.adoc[Overview] //* xref:wizard.adoc[Wizard] -* xref:components.adoc[Using Components] +* xref:usage.adoc[Usage] * xref:interfaces.adoc[Interfaces and Dispatchers] * xref:upgrades.adoc[Upgrades] ** xref:/api/upgrades.adoc[API Reference] diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/usage.adoc similarity index 99% rename from docs/modules/ROOT/pages/components.adoc rename to docs/modules/ROOT/pages/usage.adoc index c899849e8..f22d36b7f 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -1,4 +1,4 @@ -= Using Components += Usage The following documentation provides reasoning and examples on how to use Contracts for Cairo components. @@ -12,7 +12,7 @@ Another way to think of components is that they are abstract modules that must b TIP: For more information on the construction and design of Starknet components, see the {shamans-post}. -=== Setting up +== Setting up :initializable-component: xref:/security.adoc#initializable[InitializableComponent] From 007d63332524b9333a16bb2f57019cf388e1ad20 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 30 Nov 2023 02:21:37 -0500 Subject: [PATCH 07/20] change title to component impl --- docs/modules/ROOT/pages/usage.adoc | 83 +++++++++++++++--------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index f22d36b7f..f12000da5 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -34,9 +34,48 @@ The `path` argument should be the imported component itself (in this case, {init Notice that the `storage` and `event` arguments are representations set within the macro. In other words, the `initializable` and `InitializableEvent` names follow this library's convention, but they can be renamed. -=== Accessing component state +=== Component storage and events + +The component's storage and events must be added to the contract's `Storage` struct and `Event` enum respectively. +If the component doesn't define any events, the compiler will still create an empty event enum inside the component module. + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + use openzeppelin::security::InitializableComponent; + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + + #[storage] + struct Storage { + #[substorage(v0)] + initializable: InitializableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event + } + + (...) +} +---- + +The `#[substorage(v0)]` attribute must be included for each component in the `Storage` trait. +This allows the contract to have indirect access to the component's storage. + +The `#[flat]` attribute for events in the `Event` enum, however, is not required. +Component events are not flattened in the component itself to offer greater flexibility regarding how events are handled. +Note that if contracts do not flatten component events, the first key in the event log will be the component ID. +By flattening the component event, the first key will be the event ID. + +=== Component implementations -Before a contract can access a component's state, the contract must integrate the implementations that will be used. +Component implementations allow contracts to access the methods of the defined implementation. +A contract should instantiate the component implementations that it will use. For example: [,javascript] @@ -55,7 +94,7 @@ mod MyContract { ---- Defining the ``impl``s give the contract access to the methods within those implementations from the component. -For example, `initialize` is defined in the `InternalImpl`. +for example, `initialize` is defined in the `InternalImpl`. It can, therefore, be called like this: [,javascript] @@ -93,44 +132,6 @@ mod MyContract { `InitializableImpl` defines the `is_initialized` method in the component. By adding the embed attribute, `is_initialized` becomes a contract entrypoint for `MyContract`. -=== Component storage and events - -The component's storage and events must be added to the contract's `Storage` struct and `Event` enum respectively. -If the component doesn't define any events, the compiler will still create an empty event enum inside the component module. - -[,javascript] ----- -#[starknet::contract] -mod MyContract { - use openzeppelin::security::InitializableComponent; - - component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); - - (...) - - #[storage] - struct Storage { - #[substorage(v0)] - initializable: InitializableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - InitializableEvent: InitializableComponent::Event - } -} ----- - -The `#[substorage(v0)]` attribute must be included for each component in the `Storage` trait. -This allows the contract to have indirect access to the component's storage. - -The `#[flat]` attribute for events in the `Event` enum, however, is not required. -Component events are not flattened in the component itself to offer greater flexibility regarding how events are handled. -Note that if contracts do not flatten component events, the first key in the event log will be the component ID. -By flattening the component event, the first key will be the event ID. - === Component dependencies :access-component: xref:/api/access.adoc#AccessControlComponent[AccessControlComponent] From 8ddd61ddc56fddb58a8b3a04f4b3a44afe2a7033 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 30 Nov 2023 02:34:18 -0500 Subject: [PATCH 08/20] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a61afa6..f52d09b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added + +- Usage docs (#823) From 30669d299ae5122a07e047d78704681ee34cb838 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 30 Nov 2023 14:08:48 -0500 Subject: [PATCH 09/20] add comp storage section --- docs/modules/ROOT/pages/usage.adoc | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index f12000da5..c896945e2 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -72,7 +72,7 @@ Component events are not flattened in the component itself to offer greater flex Note that if contracts do not flatten component events, the first key in the event log will be the component ID. By flattening the component event, the first key will be the event ID. -=== Component implementations +== Component implementations Component implementations allow contracts to access the methods of the defined implementation. A contract should instantiate the component implementations that it will use. @@ -95,20 +95,34 @@ mod MyContract { Defining the ``impl``s give the contract access to the methods within those implementations from the component. for example, `initialize` is defined in the `InternalImpl`. -It can, therefore, be called like this: +It can, therefore, be exposed like this: [,javascript] ---- #[starknet::contract] mod MyContract { + use openzeppelin::security::InitializableComponent; + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + + #[storage] + struct Storage { + #[substorage(v0)] + initializable: InitializableComponent::Storage + } + (...) - fn initialize_contract(ref self: ContractState) { - self.initializable.initialize(); + #[external(v0)] + fn is_initialized(ref self: ContractState) -> bool { + self.initializable.is_initialized() } } ---- +While there's nothing wrong with manually exposing methods as above, this process can be tedious for implementations with many methods. +Fortunately, embedding implementations makes it easy to expose entire implementations. + TIP: Defining the `InternalImpl` in a contract also gives the contract indirect access to the component's `Storage`. === Embedding implementations @@ -132,7 +146,11 @@ mod MyContract { `InitializableImpl` defines the `is_initialized` method in the component. By adding the embed attribute, `is_initialized` becomes a contract entrypoint for `MyContract`. -=== Component dependencies +=== Accessing component storage + + + +== Component dependencies :access-component: xref:/api/access.adoc#AccessControlComponent[AccessControlComponent] :src5-component: xref:/api/introspection.adoc#SRC5Component[SRC5Component] From 97b2c7f4e38e0fad27c3fcde8fa939b92ebdeccf Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Dec 2023 16:38:32 -0500 Subject: [PATCH 10/20] finish new structure --- docs/modules/ROOT/pages/usage.adoc | 128 +++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 25 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index c896945e2..9e45e4dba 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -12,11 +12,11 @@ Another way to think of components is that they are abstract modules that must b TIP: For more information on the construction and design of Starknet components, see the {shamans-post}. -== Setting up +== Building a contract :initializable-component: xref:/security.adoc#initializable[InitializableComponent] -Integrating a component first requires that the contract declares the component with the `component!` macro: +The contract should first import the component and declare it with the `component!` macro: [,javascript] ---- @@ -68,34 +68,36 @@ The `#[substorage(v0)]` attribute must be included for each component in the `St This allows the contract to have indirect access to the component's storage. The `#[flat]` attribute for events in the `Event` enum, however, is not required. -Component events are not flattened in the component itself to offer greater flexibility regarding how events are handled. +Component events are not flattened in the component itself because it would remove the event ID from the event log. Note that if contracts do not flatten component events, the first key in the event log will be the component ID. -By flattening the component event, the first key will be the event ID. +By flattening the component event in the contract, the first key will be the event ID. -== Component implementations +=== Integrating implementations -Component implementations allow contracts to access the methods of the defined implementation. -A contract should instantiate the component implementations that it will use. -For example: +:accessing-storage: xref:/usage.adoc#accessing_component_storage[Accessing component storage] + +Components come with granular implementations of different interfaces. +This allows contracts to integrate only the implementations that they'll use and avoid unnecessary bloat. +Integrating an implementation looks like this: [,javascript] ---- -#[starknet::contract] mod MyContract { use openzeppelin::security::InitializableComponent; component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + (...) + // Gives the contract access to the implementation methods impl InitializableImpl = InitializableComponent::InitializableImpl; - impl InternalImpl = InitializableComponent::InternalImpl; } ---- -Defining the ``impl``s give the contract access to the methods within those implementations from the component. -for example, `initialize` is defined in the `InternalImpl`. -It can, therefore, be exposed like this: +Defining an `impl` gives the contract access to the methods within the implementation from the component. +For example, `is_initialized` is defined in the `InitializableImpl`. +A function on the contract level can expose it like this: [,javascript] ---- @@ -105,14 +107,11 @@ mod MyContract { component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); - #[storage] - struct Storage { - #[substorage(v0)] - initializable: InitializableComponent::Storage - } - (...) + impl InitializableImpl = + InitializableComponent::InitializableImpl; + #[external(v0)] fn is_initialized(ref self: ContractState) -> bool { self.initializable.is_initialized() @@ -120,15 +119,15 @@ mod MyContract { } ---- -While there's nothing wrong with manually exposing methods as above, this process can be tedious for implementations with many methods. -Fortunately, embedding implementations makes it easy to expose entire implementations. +Notice that the function must pass the state (`self`) and the component's storage (`initializable`) before finally accessing the implementation method. -TIP: Defining the `InternalImpl` in a contract also gives the contract indirect access to the component's `Storage`. +NOTE: Contracts can also (indirectly) access a component's storage. See {accessing-storage}. === Embedding implementations -A contract can embed implementations into the ABI which will expose the methods of the implementation. -To embed implementations, add the `#[abi(embed_v0)]` attribute above the `impl`: +While there's nothing wrong with manually exposing methods like in the previous example, this process can be tedious for implementations with many methods. +Fortunately, a contract can embed implementations into the ABI which will expose the methods of the implementation. +To embed an implementation, add the `#[abi(embed_v0)]` attribute above the `impl`: [,javascript] ---- @@ -146,9 +145,88 @@ mod MyContract { `InitializableImpl` defines the `is_initialized` method in the component. By adding the embed attribute, `is_initialized` becomes a contract entrypoint for `MyContract`. -=== Accessing component storage +=== Using component initializers + +WARNING: Failing to use a component's `initializer` can result in irreparable contract deployments. +Always read the API documentation for each integrated component. +Some components require some sort of setup upon construction. +Usually, this would be a job for a constructor; however, components themselves cannot provide constructors. +Components instead offer ``initializer``s within their `InterlImpl` which enables a contract to create a constructor and invoke the component's `initializer`. +Let's look at how a contract would integrate `OwnableComponent`: +[,javascript] +---- +#[starknet::contract] +mod MyContract { + use openzeppelin::access::ownable::OwnableComponent; + use starknet::ContractAddress; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + // Instantiate `InternalImpl` to give the contract access to the `initializer` + impl InternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + // Invoke ownable's `initializer` + self.ownable.initializer(owner); + } +} +---- + +== Custom implementations + +:erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] +:decimals: xref:/api/erc20.adoc#IERC20Metadata-decimals[decimals] +:ierc20metadata: xref:/api/erc20.adoc#IERC20Metadata[IERC20Metadata] +:custom-decimals: xref:/erc20.adoc#customizing_decimals[Customizing decimals] + +There may be instances where a contract requires different behavior in a component implementation. +In {erc20-component}, for example, the `ERC20MetadataImpl` provides a static {decimals} method which returns`18`. +If a contract wanted to make the `decimals` method dynamic, the `impl` can no longer be embedded. +Rather, the contract would have to create its own `impl` of the {ierc20metadata} interface and expose it in the contract. + +TIP: {custom-decimals} provides a guide for this exact scenario. + +== Accessing component storage + +WARNING: Writing to component storage can easily corrupt the state, bypass security checks, and undermine the component logic. +*Exercise extreme caution*. + +Just as contracts can access methods within a component implementation, contracts can also access component storage. +Storage members are accessible to the contract by instantiating the component's `InternalImpl` like this: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + (...) + + impl InternalImpl = InitializableComponent::InternalImpl; + + fn write_to_comp_storage(self: @ContractState) -> bool { + self.initializable.Initializable_initialized.write(true); + } + + fn read_from_comp_storage(self: @ContractState) -> bool { + self.initializable.Initializable_initialized.read() + } +} +---- == Component dependencies From 441697e6b740d64b295ef14e28cbdfe76b4db455 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 2 Dec 2023 13:21:11 -0500 Subject: [PATCH 11/20] finish edits, add customization section --- docs/modules/ROOT/pages/usage.adoc | 100 ++++++++++++++++------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 9e45e4dba..b13bcfb00 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -126,7 +126,7 @@ NOTE: Contracts can also (indirectly) access a component's storage. See {accessi === Embedding implementations While there's nothing wrong with manually exposing methods like in the previous example, this process can be tedious for implementations with many methods. -Fortunately, a contract can embed implementations into the ABI which will expose the methods of the implementation. +Fortunately, a contract can embed implementations into the ABI which will expose all of the methods of the implementation. To embed an implementation, add the `#[abi(embed_v0)]` attribute above the `impl`: [,javascript] @@ -152,7 +152,7 @@ Always read the API documentation for each integrated component. Some components require some sort of setup upon construction. Usually, this would be a job for a constructor; however, components themselves cannot provide constructors. -Components instead offer ``initializer``s within their `InterlImpl` which enables a contract to create a constructor and invoke the component's `initializer`. +Components instead offer ``initializer``s within their `InternalImpl` which enables a contract to create a constructor and invoke the component's `initializer`. Let's look at how a contract would integrate `OwnableComponent`: [,javascript] @@ -188,47 +188,7 @@ mod MyContract { } ---- -== Custom implementations - -:erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] -:decimals: xref:/api/erc20.adoc#IERC20Metadata-decimals[decimals] -:ierc20metadata: xref:/api/erc20.adoc#IERC20Metadata[IERC20Metadata] -:custom-decimals: xref:/erc20.adoc#customizing_decimals[Customizing decimals] - -There may be instances where a contract requires different behavior in a component implementation. -In {erc20-component}, for example, the `ERC20MetadataImpl` provides a static {decimals} method which returns`18`. -If a contract wanted to make the `decimals` method dynamic, the `impl` can no longer be embedded. -Rather, the contract would have to create its own `impl` of the {ierc20metadata} interface and expose it in the contract. - -TIP: {custom-decimals} provides a guide for this exact scenario. - -== Accessing component storage - -WARNING: Writing to component storage can easily corrupt the state, bypass security checks, and undermine the component logic. -*Exercise extreme caution*. - -Just as contracts can access methods within a component implementation, contracts can also access component storage. -Storage members are accessible to the contract by instantiating the component's `InternalImpl` like this: - -[,javascript] ----- -#[starknet::contract] -mod MyContract { - (...) - - impl InternalImpl = InitializableComponent::InternalImpl; - - fn write_to_comp_storage(self: @ContractState) -> bool { - self.initializable.Initializable_initialized.write(true); - } - - fn read_from_comp_storage(self: @ContractState) -> bool { - self.initializable.Initializable_initialized.read() - } -} ----- - -== Component dependencies +=== Handling component dependencies :access-component: xref:/api/access.adoc#AccessControlComponent[AccessControlComponent] :src5-component: xref:/api/introspection.adoc#SRC5Component[SRC5Component] @@ -281,3 +241,57 @@ mod MyContract { (...) } ---- + +== Customization + +:security: xref:/usage.adoc#security[Security] + +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}. + +=== Custom implementations + +:erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] +:decimals: xref:/api/erc20.adoc#IERC20Metadata-decimals[decimals] +:ierc20metadata: xref:/api/erc20.adoc#IERC20Metadata[IERC20Metadata] +:custom-decimals: xref:/erc20.adoc#customizing_decimals[Customizing decimals] + +There may be instances where a contract requires different behavior from a component implementation. +The contract can create a custom implementation by importing the appropriate interface and defining the new logic. + +In {erc20-component}, for example, the `ERC20MetadataImpl` provides a static {decimals} method which returns`18`. +If a contract wanted to change the return value of `decimals`, the `impl` can no longer be embedded. +Rather, the contract would have to create its own `impl` of the {ierc20metadata} interface and expose it in the contract. + +TIP: {custom-decimals} provides a guide for this exact scenario. + +=== Accessing component storage + +Just as contracts can access methods within a component implementation, contracts can also access component storage. +Storage members are accessible to the contract by instantiating the component's `InternalImpl` like this: + +[,javascript] +---- +#[starknet::contract] +mod MyContract { + (...) + + impl InternalImpl = InitializableComponent::InternalImpl; + + fn write_to_comp_storage(ref self: ContractState) -> bool { + self.initializable.Initializable_initialized.write(true); + } + + fn read_from_comp_storage(self: @ContractState) -> bool { + self.initializable.Initializable_initialized.read() + } +} +---- + +== Security + +The maintainers of OpenZeppelin Contracts for Cairo are mainly concerned with the correctness and security of the code as published in the library. + +Customizing implementations and manipulating the component state may break some important assumptions and introduce vulnerabilities in otherwise secure code. +While we try to ensure the components remain secure in the face of a wide range of potential customizations, this is done in a best-effort manner. +Any and all customizations to the component logic should be carefully reviewed and checked against the source code of the component they are customizing so as to fully understand their impact and guarantee their security. From cad8e8841f6c168d1c59042395e20006b3a15ccf Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 2 Dec 2023 16:13:02 -0500 Subject: [PATCH 12/20] simplify titles --- docs/modules/ROOT/pages/usage.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index b13bcfb00..2cb7df730 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -34,7 +34,7 @@ The `path` argument should be the imported component itself (in this case, {init Notice that the `storage` and `event` arguments are representations set within the macro. In other words, the `initializable` and `InitializableEvent` names follow this library's convention, but they can be renamed. -=== Component storage and events +=== Storage and events The component's storage and events must be added to the contract's `Storage` struct and `Event` enum respectively. If the component doesn't define any events, the compiler will still create an empty event enum inside the component module. @@ -72,7 +72,7 @@ Component events are not flattened in the component itself because it would remo Note that if contracts do not flatten component events, the first key in the event log will be the component ID. By flattening the component event in the contract, the first key will be the event ID. -=== Integrating implementations +=== Implementations :accessing-storage: xref:/usage.adoc#accessing_component_storage[Accessing component storage] @@ -123,8 +123,6 @@ Notice that the function must pass the state (`self`) and the component's storag NOTE: Contracts can also (indirectly) access a component's storage. See {accessing-storage}. -=== Embedding implementations - While there's nothing wrong with manually exposing methods like in the previous example, this process can be tedious for implementations with many methods. Fortunately, a contract can embed implementations into the ABI which will expose all of the methods of the implementation. To embed an implementation, add the `#[abi(embed_v0)]` attribute above the `impl`: @@ -145,7 +143,9 @@ mod MyContract { `InitializableImpl` defines the `is_initialized` method in the component. By adding the embed attribute, `is_initialized` becomes a contract entrypoint for `MyContract`. -=== Using component initializers +=== Initializers + +:ownable-component: xref:/api/access.adoc#OwnableComponent[OwnableComponent] WARNING: Failing to use a component's `initializer` can result in irreparable contract deployments. Always read the API documentation for each integrated component. @@ -153,7 +153,7 @@ Always read the API documentation for each integrated component. Some components require some sort of setup upon construction. Usually, this would be a job for a constructor; however, components themselves cannot provide constructors. Components instead offer ``initializer``s within their `InternalImpl` which enables a contract to create a constructor and invoke the component's `initializer`. -Let's look at how a contract would integrate `OwnableComponent`: +Let's look at how a contract would integrate {ownable-component}: [,javascript] ---- @@ -188,7 +188,7 @@ mod MyContract { } ---- -=== Handling component dependencies +=== Dependencies :access-component: xref:/api/access.adoc#AccessControlComponent[AccessControlComponent] :src5-component: xref:/api/introspection.adoc#SRC5Component[SRC5Component] From 15c29d933597ee4d972a84b1ac9cf110adbfa783 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 10 Dec 2023 21:57:53 -0500 Subject: [PATCH 13/20] finish custom impl section --- docs/modules/ROOT/pages/usage.adoc | 109 ++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 8 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 2cb7df730..87bbe3de0 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -252,18 +252,111 @@ WARNING: Customizing implementations and accessing component storage can potenti === Custom implementations :erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] -:decimals: xref:/api/erc20.adoc#IERC20Metadata-decimals[decimals] -:ierc20metadata: xref:/api/erc20.adoc#IERC20Metadata[IERC20Metadata] :custom-decimals: xref:/erc20.adoc#customizing_decimals[Customizing decimals] -There may be instances where a contract requires different behavior from a component implementation. -The contract can create a custom implementation by importing the appropriate interface and defining the new logic. +There are instances where a contract requires different or amended behaviors from a component implementation. +In these scenarios, a contract must create a custom implementation of the interface. +Let's break down a pausable ERC20 contract to see what that looks like. +Here's the setup: -In {erc20-component}, for example, the `ERC20MetadataImpl` provides a static {decimals} method which returns`18`. -If a contract wanted to change the return value of `decimals`, the `impl` can no longer be embedded. -Rather, the contract would have to create its own `impl` of the {ierc20metadata} interface and expose it in the contract. +[,javascript] +---- +#[starknet::contract] +mod ERC20Pausable { + use openzeppelin::security::pausable::PausableComponent; + use openzeppelin::token::erc20::ERC20Component; + // Import the ERC20 interfaces to create custom implementations + use openzeppelin::token::erc20::interface::{IERC20, IERC20CamelOnly}; + use starknet::ContractAddress; + + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + // `ERC20MetadataImpl` can keep the embed directive because the implementation + // will not change + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + // Do not add the embed directive to these implementations because + // these will be customized + impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + (...) +} +---- + +The first thing to notice is that the contract imports the interfaces of the implementations that will be customized. +These will be used in the next code example. + +Next, the contract includes the {erc20-component} implementations; however, `ERC20Impl` and `ERC20CamelOnlyImplt` are *not* embedded. +Creating a custom implementation of an interface means that the component implementation cannot be embedded. +The following example shows the pausable logic integrated into the ERC20 implementations: + +[,javascript] +---- +#[starknet::contract] +mod ERC20Pausable { + (...) + + // Custom ERC20 implementation + #[external(v0)] + impl CustomERC20Impl of IERC20 { + fn transfer( + ref self: ContractState, recipient: ContractAddress, amount: u256 + ) -> bool { + // Add the custom logic + self.pausable.assert_not_paused(); + // Add the original implementation method from `IERC20Impl` + self.erc20.transfer(recipient, amount) + } + + fn total_supply(self: @ContractState) -> u256 { + // This method's behavior does not change from the component + // implementation, but this method must still be defined. + // Simply add the original implementation method from `IERC20Impl` + self.erc20.total_supply() + } + + (...) + } + + // Custom ERC20CamelOnly implementation + #[external(v0)] + impl CustomERC20CamelOnlyImpl of IERC20CamelOnly { + fn totalSupply(self: @ContractState) -> u256 { + self.erc20.total_supply() + } + + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.pausable.assert_not_paused(); + self.erc20.transfer_from(sender, recipient, amount) + } + } +} +---- + +Notice that in the `CustomERC20Impl`, the `transfer` method integrates `pausable.assert_not_paused` as well as `erc20.transfer` from `PausableImpl` and `ERC20Impl` respectively. +This is why the contract defined the `ERC20Impl` from the component in the previous example. + +Creating a custom implementation of an interface must define *all* methods from that interface. +This is true even if the behavior of a method does not change from the component implementation (as `total_supply` exemplifies in this example). -TIP: {custom-decimals} provides a guide for this exact scenario. +TIP: The ERC20 documentation provides a more specific custom implementations guide with {custom-decimals}. === Accessing component storage From a56dc3f830f4b8336a2fdd5c38a5583769dece03 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Mon, 11 Dec 2023 13:09:40 -0500 Subject: [PATCH 14/20] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Martín Triay --- docs/modules/ROOT/pages/usage.adoc | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 87bbe3de0..482abbcb6 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -68,9 +68,8 @@ The `#[substorage(v0)]` attribute must be included for each component in the `St This allows the contract to have indirect access to the component's storage. The `#[flat]` attribute for events in the `Event` enum, however, is not required. -Component events are not flattened in the component itself because it would remove the event ID from the event log. -Note that if contracts do not flatten component events, the first key in the event log will be the component ID. -By flattening the component event in the contract, the first key will be the event ID. +For component events, the first key in the event log is the component ID. +Flattening the component event removes it, leaving the event ID as the first key. === Implementations @@ -152,7 +151,7 @@ Always read the API documentation for each integrated component. Some components require some sort of setup upon construction. Usually, this would be a job for a constructor; however, components themselves cannot provide constructors. -Components instead offer ``initializer``s within their `InternalImpl` which enables a contract to create a constructor and invoke the component's `initializer`. +Components instead offer ``initializer``s within their `InternalImpl` to be called by the contract's constructor. Let's look at how a contract would integrate {ownable-component}: [,javascript] @@ -295,7 +294,7 @@ The first thing to notice is that the contract imports the interfaces of the imp These will be used in the next code example. Next, the contract includes the {erc20-component} implementations; however, `ERC20Impl` and `ERC20CamelOnlyImplt` are *not* embedded. -Creating a custom implementation of an interface means that the component implementation cannot be embedded. +Instead, we want to embed our custom implementation of an interface. The following example shows the pausable logic integrated into the ERC20 implementations: [,javascript] @@ -356,7 +355,7 @@ This is why the contract defined the `ERC20Impl` from the component in the previ Creating a custom implementation of an interface must define *all* methods from that interface. This is true even if the behavior of a method does not change from the component implementation (as `total_supply` exemplifies in this example). -TIP: The ERC20 documentation provides a more specific custom implementations guide with {custom-decimals}. +TIP: The ERC20 documentation provides another custom implementation guide for {custom-decimals}. === Accessing component storage @@ -385,6 +384,6 @@ mod MyContract { The maintainers of OpenZeppelin Contracts for Cairo are mainly concerned with the correctness and security of the code as published in the library. -Customizing implementations and manipulating the component state may break some important assumptions and introduce vulnerabilities in otherwise secure code. +Customizing implementations and manipulating the component state may break some important assumptions and introduce vulnerabilities. While we try to ensure the components remain secure in the face of a wide range of potential customizations, this is done in a best-effort manner. Any and all customizations to the component logic should be carefully reviewed and checked against the source code of the component they are customizing so as to fully understand their impact and guarantee their security. From 8165f923b81ce02ca58ff5eafb60da382086593b Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 12 Dec 2023 04:51:05 -0500 Subject: [PATCH 15/20] fix comp storage section --- docs/modules/ROOT/pages/usage.adoc | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 482abbcb6..573760fb6 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -359,18 +359,26 @@ TIP: The ERC20 documentation provides another custom implementation guide for {c === Accessing component storage -Just as contracts can access methods within a component implementation, contracts can also access component storage. -Storage members are accessible to the contract by instantiating the component's `InternalImpl` like this: +There may be cases where the contract must read or write to an integrated component's storage. +To do so, use the same syntax as calling an implementation method except replace the name of the method with the storage variable like this: [,javascript] ---- #[starknet::contract] mod MyContract { - (...) + use openzeppelin::security::InitializableComponent; + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); - impl InternalImpl = InitializableComponent::InternalImpl; + #[storage] + struct Storage { + #[substorage(v0)] + initializable: InitializableComponent::Storage + } + + (...) - fn write_to_comp_storage(ref self: ContractState) -> bool { + fn write_to_comp_storage(ref self: ContractState) { self.initializable.Initializable_initialized.write(true); } From d714cafb7b0f18d05cfbc108a3061f37518af5b9 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 12 Dec 2023 04:53:23 -0500 Subject: [PATCH 16/20] change section title to setup, minor edits --- docs/modules/ROOT/pages/usage.adoc | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 573760fb6..ac9a07915 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -14,7 +14,10 @@ TIP: For more information on the construction and design of Starknet components, == Building a contract +=== Setup + :initializable-component: xref:/security.adoc#initializable[InitializableComponent] +:accessing-storage: xref:/usage.adoc#accessing_component_storage[Accessing component storage] The contract should first import the component and declare it with the `component!` macro: @@ -31,13 +34,8 @@ mod MyContract { ---- The `path` argument should be the imported component itself (in this case, {initializable-component}). -Notice that the `storage` and `event` arguments are representations set within the macro. -In other words, the `initializable` and `InitializableEvent` names follow this library's convention, but they can be renamed. - -=== Storage and events - -The component's storage and events must be added to the contract's `Storage` struct and `Event` enum respectively. -If the component doesn't define any events, the compiler will still create an empty event enum inside the component module. +The `storage` and `event` arguments are the variable names that will be set in the `Storage` struct and `Event` enum, respectively. +Note that even if the component doesn't define any events, the compiler will still create an empty event enum inside the component module. [,javascript] ---- @@ -59,8 +57,6 @@ mod MyContract { #[flat] InitializableEvent: InitializableComponent::Event } - - (...) } ---- From 4032962fe1260e568510947f81e9c2daa0a5ef82 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 12 Dec 2023 04:54:39 -0500 Subject: [PATCH 17/20] add cmp storage link, clean up impl section --- docs/modules/ROOT/pages/usage.adoc | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index ac9a07915..6f613d164 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -62,6 +62,7 @@ mod MyContract { The `#[substorage(v0)]` attribute must be included for each component in the `Storage` trait. This allows the contract to have indirect access to the component's storage. +See {accessing-storage} for more on this. The `#[flat]` attribute for events in the `Event` enum, however, is not required. For component events, the first key in the event log is the component ID. @@ -69,8 +70,6 @@ Flattening the component event removes it, leaving the event ID as the first key === Implementations -:accessing-storage: xref:/usage.adoc#accessing_component_storage[Accessing component storage] - Components come with granular implementations of different interfaces. This allows contracts to integrate only the implementations that they'll use and avoid unnecessary bloat. Integrating an implementation looks like this: @@ -114,12 +113,8 @@ mod MyContract { } ---- -Notice that the function must pass the state (`self`) and the component's storage (`initializable`) before finally accessing the implementation method. - -NOTE: Contracts can also (indirectly) access a component's storage. See {accessing-storage}. - While there's nothing wrong with manually exposing methods like in the previous example, this process can be tedious for implementations with many methods. -Fortunately, a contract can embed implementations into the ABI which will expose all of the methods of the implementation. +Fortunately, a contract can embed implementations which will expose all of the methods of the implementation. To embed an implementation, add the `#[abi(embed_v0)]` attribute above the `impl`: [,javascript] From 319b3b2ec2b3fe78f6f26a5b6e976f7b22cff454 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 12 Dec 2023 12:34:10 -0500 Subject: [PATCH 18/20] add cairo book link --- docs/modules/ROOT/pages/usage.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 6f613d164..e6992a904 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -5,12 +5,13 @@ The following documentation provides reasoning and examples on how to use Contra == Components :shamans-post: https://community.starknet.io/t/cairo-components/101136#components-1[Starknet Shamans post] +:cairo-book: https://book.cairo-lang.org/ch99-01-05-00-components.html[Cairo book] Starknet components are separate modules that contain storage, events, and implementations that can be integrated into a contract. Components themselves cannot be declared or deployed. Another way to think of components is that they are abstract modules that must be instantiated. -TIP: For more information on the construction and design of Starknet components, see the {shamans-post}. +TIP: For more information on the construction and design of Starknet components, see the {shamans-post} and the {cairo-book}. == Building a contract From a0b5eddb755c221a91cdeb4f436b67f4d12174cb Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 12 Dec 2023 16:51:26 -0500 Subject: [PATCH 19/20] add api design tip --- docs/modules/ROOT/pages/usage.adoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index e6992a904..98c878a04 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -71,6 +71,8 @@ Flattening the component event removes it, leaving the event ID as the first key === Implementations +:erc20-component: xref:/api/erc20.adoc#ERC20Component[ERC20Component] + Components come with granular implementations of different interfaces. This allows contracts to integrate only the implementations that they'll use and avoid unnecessary bloat. Integrating an implementation looks like this: @@ -134,6 +136,18 @@ mod MyContract { `InitializableImpl` defines the `is_initialized` method in the component. By adding the embed attribute, `is_initialized` becomes a contract entrypoint for `MyContract`. +[TIP] +==== +Embeddable implementations, when available in this library's components, are segregated from the internal component implementation which makes it easier to safely embed. +Components also separate standard implementations (`snake_case`) from `camelCase`. +This trichotomy structures the API documentation design. +See {erc20-component} as an example which includes: + +- *Embeddable implementations* +- *Embeddable implementations (camelCase)* +- *Internal implementations* +==== + === Initializers :ownable-component: xref:/api/access.adoc#OwnableComponent[OwnableComponent] From 6e0e45b09b7ddb70f4460fdd48cc02f78aa66e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Wed, 13 Dec 2023 01:22:34 -0300 Subject: [PATCH 20/20] Apply suggestions from code review --- docs/modules/ROOT/pages/usage.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 98c878a04..2f8c3dc72 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -138,7 +138,7 @@ By adding the embed attribute, `is_initialized` becomes a contract entrypoint fo [TIP] ==== -Embeddable implementations, when available in this library's components, are segregated from the internal component implementation which makes it easier to safely embed. +Embeddable implementations, when available in this library's components, are segregated from the internal component implementation which makes it easier to safely expose. Components also separate standard implementations (`snake_case`) from `camelCase`. This trichotomy structures the API documentation design. See {erc20-component} as an example which includes: @@ -156,8 +156,8 @@ WARNING: Failing to use a component's `initializer` can result in irreparable co Always read the API documentation for each integrated component. Some components require some sort of setup upon construction. -Usually, this would be a job for a constructor; however, components themselves cannot provide constructors. -Components instead offer ``initializer``s within their `InternalImpl` to be called by the contract's constructor. +Usually, this would be a job for a constructor; however, components themselves cannot implement constructors. +Components instead offer ``initializer``s within their `InternalImpl` to call from the contract's constructor. Let's look at how a contract would integrate {ownable-component}: [,javascript] @@ -300,7 +300,7 @@ The first thing to notice is that the contract imports the interfaces of the imp These will be used in the next code example. Next, the contract includes the {erc20-component} implementations; however, `ERC20Impl` and `ERC20CamelOnlyImplt` are *not* embedded. -Instead, we want to embed our custom implementation of an interface. +Instead, we want to expose our custom implementation of an interface. The following example shows the pausable logic integrated into the ERC20 implementations: [,javascript]