In this workshop, you will learn how to create an Ownable contract, which assigns a ContractAddress
to be the owner of the contract. In addition, you will learn about how components can be implemented and finally use OpenZeppelin's Ownable component.
This workshop is a continuation of the Counter Workshop. If you haven't completed it, please do so.
After completing each step, run the associated script to verify it has been implemented correctly.
Use the Cairo book and the Starknet docs as a reference.
- Clone this repository
- Create a new file called
counter.cairo
inside thesrc
folder - Copy the final code from the Counter Workshop into the
counter.cairo
file
Note: You'll be working on the
counter.cairo
andownable.cairo
files to complete the requirements of each step. The folderprev_solution
will show up in future steps as a way to catch up with the workshop if you fall behind. Don't modify that file.
The next setup steps will depend on wether you prefer using Docker to manage global dependencies or not.
- Install Scarb 2.4.4 (instructions)
- Install Starknet Foundry 0.14.0 (instructions)
- Install the Cairo 1.0 extension for VSCode (marketplace)
- Run the tests to verify the project is setup correctly
$ scarb test
- Make sure Docker is installed and running
- Install the Dev Containers extension for VSCode (marketplace)
- Launch an instance of VSCode inside of the container by going to View -> Command Palette -> Dev Containers: Rebuild and Reopen in Container
- Open VSCode's integrated terminal and run the tests to verify the project is setup correctly
$ scarb test
Note: All the commands shown from this point on will assume that you are using the integrated terminal of a VSCode instance running inside the container. If you want to run the tests on a different terminal you'll need to use the command
docker compose run test
.
In this step, you will need to do the following:
- store a new variable named
owner
asContractAddress
type in theStorage
struct - modify the constructor function so that it accepts a new input variable named
initial_owner
and then updates theowner
variable from theStorage
with this value - implement a public function named
owner()
which returns the value of theowner
variable
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
- create a new public interface called
IOwnable<T>{}
and add the functionowner()
- create a new
impl
which implements theIOwnable
interface and add the#[abi(embed_v0)]
to expose theimpl
In this step, you will need to do the following:
- create a private function named
assert_only_owner
which checks: - protect the
increase_counter
function with theassert_only_owner
function
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
The assert_only_owner
function should:
- check that the
caller
is not the zero address, otherwise, it will panic with the following message'Caller is the zero address
- check that the
caller
is the same as theowner
which is stored in theStorage
, otherwise, it will panic with the following message'Caller is not the owner'
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
- You can read who the caller is by using the syscall
get_caller_address
available in thestarknet
module - You can check for the zero address with the
.is_zero()
function on the variable itself - To read more about Private Functions, check Chapter 12.3.2 Private functions
In this step, you will need to do create a public function named transfer_ownership()
which receives as input variable new_owner
as ContractAddress
type and updates owner
from the Storage
with the new value.
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
The transfer_ownership()
function should:
- be in the
IOwnable
interface - check that the
new_owner
variable is not the zero address, otherwise, it will panic with the following message'New owner is the zero address'
- check that only the owner can access the function
- update the
owner
variable with thenew_owner
Note: You can implement a private function which only updates the
owner
variable with thenew_owner
variable. We will call this private function several times and it makes the code modular. You can name this private function as_transfer_ownership()
.
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
- Make sure that the panic messages are the same as stated in the Requirements sections, otherwise, some test will fail.
In this step, you will need to do the following:
- implement an event named
OwnershipTransferred
which emits theprevious_owner
and thenew_owner
variables - emit this event when you successfully transfer the ownership of the contract
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
Events are custom data structures that are emitted by a contract. More information about Events can be found in Chapter 12.3.3 - Contract Events.
In this step, you will need to do create a public function named renounce_ownership()
which removes the current owner
and change it to the zero address.
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
The renounce_ownership()
function should:
- be in the
IOwnable
interface - check that only the owner can call this function
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
- You can use
Zeroable::zero()
for the zero address which is available in thestarknet
module - You can use the private function
_transfer_ownership
that you created inStep 3
to avoid code duplication
In this step, you will need to do create a private function called initializer()
, which initializes the owner variable. Modify the constructor function so that you call the private function initializer()
to initialize the owner.
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
- You can use the private function
_transfer_ownership
that you created in theStep 3
to avoid code duplication
In this step, you will need to create a new file named ownable.cairo
and move all the related code to the Ownable
exercise. The ownable.cairo
should be created as a normal starknet contract.
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
- name the
ownable.cairo
contract as the following:
#[starknet::contract]
mod OwnableComponent {
}
- add all the relevant code to this file, this includes:
- the interface
- the implementation of the interface
- storage to store the
owner
variable - the private functions
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
In this step, you will change the ownable.cairo
from a starknet contract to a starknet component. Then, you will import the component and use it within your counter.cairo
.
Before working on this step, make sure to read Chapter 12.4: Components and see how Components work.
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
- the name of your component impl block should be
OwnableImpl
- use the
component!()
in thecounter.cairo
to i - in your
counter.cairo
store the component's storage path asownable
inside theStorage
- in your
counter.cairo
store the component's events path asOwnableEvent
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
-
to migrate a contract to a component you will need to:
-
use the
#[starknet::component]
instead of the#[starknet::contract]
-
change the
#[abi(embed_v0)]
to#[embeddable_as(name)]
for the impl block -
add generic parameters for the impl block such as
TContractState
and+HasComponent<TContractState>
-
change the
self
argument toComponentState<TContractState>
Note: Read more on Chapter 12.4: Migrating a Contract to a Component
-
-
to use the component inside the
counter.cairo
you will need to-
declare the component with the
component!()
macro` -
add the path to the component's storage and events to your contract's
Storage
andEvents
-
instantiate the component's implementation
Note: Read more on Chapter 12.4: Using Components inside a contract
-
In this step, you will use OpenZeppelin's Ownable implementation in your contract. You will only need to change the import path of the OwnableComponent
to match OpenZeppelin's.
Note: If you fell behind, the folder prev_solution contains the solution to the previous step.
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge test
- OpenZepplin library has already been added to your
Scarb.toml
configuration - you only need modify your ownable import to match the OwnableComponent
Check that you have correctly created an account contract for Starknet by running the full test suite:
$ snforge test
If the test suite passes, congratulations, you have created your first custom Starknet Counter Contract which implement the component feature and succesfully uses the OpenZeppelin library.