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.cairoinside thesrcfolder - Copy the final code from the Counter Workshop into the
counter.cairofile
Note: You'll be working on the
counter.cairoandownable.cairofiles to complete the requirements of each step. The folderprev_solutionwill 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 testNote: 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
ownerasContractAddresstype in theStoragestruct - modify the constructor function so that it accepts a new input variable named
initial_ownerand then updates theownervariable from theStoragewith this value - implement a public function named
owner()which returns the value of theownervariable
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
implwhich implements theIOwnableinterface 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_ownerwhich checks: - protect the
increase_counterfunction with theassert_only_ownerfunction
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
calleris not the zero address, otherwise, it will panic with the following message'Caller is the zero address - check that the
calleris the same as theownerwhich 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_addressavailable in thestarknetmodule - 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
IOwnableinterface - check that the
new_ownervariable 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
ownervariable with thenew_owner
Note: You can implement a private function which only updates the
ownervariable with thenew_ownervariable. 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
OwnershipTransferredwhich emits theprevious_ownerand thenew_ownervariables - 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 testEvents 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
IOwnableinterface - 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 thestarknetmodule - You can use the private function
_transfer_ownershipthat you created inStep 3to 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_ownershipthat you created in theStep 3to 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.cairocontract 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
ownervariable - the private functions
When completed, execute the test suite to verify you've met all the requirements for this section.
$ snforge testIn 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.cairoto i - in your
counter.cairostore the component's storage path asownableinside theStorage - in your
counter.cairostore 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
TContractStateand+HasComponent<TContractState> -
change the
selfargument toComponentState<TContractState>Note: Read more on Chapter 12.4: Migrating a Contract to a Component
-
-
to use the component inside the
counter.cairoyou will need to-
declare the component with the
component!()macro` -
add the path to the component's storage and events to your contract's
StorageandEvents -
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.tomlconfiguration - 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 testIf 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.