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

Update extensibility docs #823

Merged
merged 25 commits into from
Dec 13, 2023
Merged
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
600737a
add extensibility docs
andrew-fleming Nov 21, 2023
54cfe2a
Merge branch 'main' into extensibility-docs
andrew-fleming Nov 21, 2023
22019dc
start setup guide
andrew-fleming Nov 22, 2023
34ca611
change file name
andrew-fleming Nov 24, 2023
f409e0e
Apply suggestions from code review
andrew-fleming Nov 26, 2023
46191e2
fix name
andrew-fleming Nov 26, 2023
4f7035b
Merge branch 'main' into extensibility-docs
andrew-fleming Nov 29, 2023
a9f3079
change title
andrew-fleming Nov 30, 2023
007d633
change title to component impl
andrew-fleming Nov 30, 2023
8ddd61d
update changelog
andrew-fleming Nov 30, 2023
30669d2
add comp storage section
andrew-fleming Nov 30, 2023
97b2c7f
finish new structure
andrew-fleming Dec 1, 2023
441697e
finish edits, add customization section
andrew-fleming Dec 2, 2023
cad8e88
simplify titles
andrew-fleming Dec 2, 2023
269ff1e
fix conflicts
andrew-fleming Dec 6, 2023
a0a8833
fix conflicts
andrew-fleming Dec 7, 2023
15c29d9
finish custom impl section
andrew-fleming Dec 11, 2023
ed2b11c
fix conflicts
andrew-fleming Dec 11, 2023
a56dc3f
Apply suggestions from code review
andrew-fleming Dec 11, 2023
8165f92
fix comp storage section
andrew-fleming Dec 12, 2023
d714caf
change section title to setup, minor edits
andrew-fleming Dec 12, 2023
4032962
add cmp storage link, clean up impl section
andrew-fleming Dec 12, 2023
319b3b2
add cairo book link
andrew-fleming Dec 12, 2023
a0b5edd
add api design tip
andrew-fleming Dec 12, 2023
6e0e45b
Apply suggestions from code review
martriay Dec 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 41 additions & 28 deletions docs/modules/ROOT/pages/usage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ 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

=== 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:

Expand All @@ -31,13 +35,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]
----
Expand All @@ -59,22 +58,20 @@ mod MyContract {
#[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.
See {accessing-storage} for more on this.

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

:accessing-storage: xref:/usage.adoc#accessing_component_storage[Accessing component storage]
: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.
Expand Down Expand Up @@ -119,12 +116,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]
Expand All @@ -143,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.
martriay marked this conversation as resolved.
Show resolved Hide resolved
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]
Expand All @@ -152,7 +157,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.
martriay marked this conversation as resolved.
Show resolved Hide resolved
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.
martriay marked this conversation as resolved.
Show resolved Hide resolved
Let's look at how a contract would integrate {ownable-component}:

[,javascript]
Expand Down Expand Up @@ -295,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.
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.
martriay marked this conversation as resolved.
Show resolved Hide resolved
The following example shows the pausable logic integrated into the ERC20 implementations:

[,javascript]
Expand Down Expand Up @@ -356,22 +361,30 @@ 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

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<ContractState>;
#[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);
}

Expand All @@ -385,6 +398,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.