Full Guide to Improvements in Starknet Syntax

Original Article: Improved Starknet Syntax

Translated and proofread by: “Starknet Chinese Community”

Summary

The second version of the Cairo compiler has made changes to the Starknet syntax to make the code more explicit and secure. The smart contract public interface is defined using feature definitions, and access to storage is done through the ContractState feature. Private methods must be defined using a different implementation than the public interface. Events are now defined as enumerations, with each variant being a struct with the same name.

Disclaimer: The terms used here refer to different versions of the Cairo compiler, and the syntax is temporary as the Starknet community is still discussing which terms are best. Once determined, this article will be updated accordingly.

The Compiler v2

Just last week, the new major version 2.0.0-rc0 of the Cairo compiler was released on Github. The new compiler has made significant improvements to the Starknet plugin, making our code more secure, explicit, and reusable. Please note that the Starknet testnet or mainnet does not yet support this new version of the compiler as it is still in an integration environment.

The goal of this article is to show you how to rewrite a Starknet smart contract created for Cairo compiler version 1.x to be compatible with the 2.x compiler version. Our starting point is the Ownable smart contract created in the previous article, which is compatible with the Cario compiler version 1.x.

#[contract]mod Ownable {use starknet::ContractAddress;use starknet::get_caller_address;

#[event]fn OwnershipTransferred(previous_owner: ContractAddress, new_owner: ContractAddress) {}

struct Storage {owner: ContractAddress,}

#[constructor]fn constructor() {let deployer = get_caller_address();owner::write(deployer);}

#[view]fn get_owner() -> ContractAddress {owner::read()}

#[external]fn transfer_ownership(new_owner: ContractAddress) {only_owner();let previous_owner = owner::read();owner::write(new_owner);OwnershipTransferred(previous_owner, new_owner);}

fn only_owner() {let caller = get_caller_address();assert(caller == owner::read(), ‘Caller is not the owner’);}}

Project Setup

Since Protostar does not yet support compiler v2, this article will rely on the Scarb pre-release version (version 0.5.0-alpha.1) that supports it. To install Scarb for this specific version, you can use the following command.

以下のコマンドを使用して、Scarb をインストールします。

$ curl –proto ‘=https’ –tlsv1.2 -sSf | bash -s — -v 0.5.0-alpha.1

インストールが完了したら、正しいバージョンが取得できたかどうかを確認してください。

$ scarb –version>>>scarb 0.5.0-alpha.1 (546dad33d 2023-06-19)cairo:2.0.0-rc3()

これで、Scarb プロジェクトを作成できます。

$ scarb new cairo1_v2$cdcairo1_v2

以下のフォルダ構造が表示されます。

$ tree .>>>.├── Scarb.toml└── src└──lib.cairo

Scarb が Starknet スマートコントラクトをコンパイルできるようにするには、依存関係として Starknet プラグインを有効にする必要があります。

// Scarb.toml…[dependencies]starknet=”2.0.0-rc3″

設定が完了したら、src/lib.cairo に移動してスマートコントラクトの作成を開始できます。

ストレージとコンストラクタ

Cairo コンパイラのバージョン 2 では、スマートコントラクトは、プラグインの名前である contract 属性注釈を持つモジュール定義によって定義されます。この例では、starknet です。

#[starknet::contract]mod Ownable {}

内部ストレージは、それを注釈化するためにストレージプロパティを使用する必要がある Storage という名前で引き続き定義されます。

#[starknet::contract]mod Ownable {use super::ContractAddress; #[storage]struct Storage {owner: ContractAddress,}}

コンストラクタを定義するには、コンストラクタ属性を使用して関数に注釈を付けます。これは v1 で行ったのと同じですが、関数には今では「コンストラクタ」という名称を付ける必要はなく、任意の名前を付けることができます。これは必須ではありませんが、私は依然として「コンストラクタ」と呼ぶ傾向がありますが、呼び出し方には異なる方法があります。

もう1つの重要な変更は、この時点でコンストラクタが自動的に ContractState への参照を渡すことで、ストレージ変数の中介として機能することです。この例では、「オーナー」です。

#[starknet::contract]mod Ownable {use super::ContractAddress; #[storage]struct Storage {owner: ContractAddress,} #[constructor]fn constructor(ref self: ContractState) {let deployer = get_caller_address();self.owner.write(deployer);}}

以前の owner::write() から、self.owner.write() に書き換える必要があることに注意してください。ストレージから読み取る場合も同様です。

ちなみに、ContractState 型は手動でスコープに入れる必要はありません。それは前提条件に含まれています。

Public Methods

One important difference with the Cairo compiler version 1 is that now we need to use features with the starknet::interface attribute annotation to explicitly define a smart contract’s public interface.

use starknet::ContractAddress;

#[starknet::interface]trait OwnableTrait { fn transfer_ownership(ref self: T, new_owner: ContractAddress); fn get_owner(self: @T) -> ContractAddress; }

#[starknet::contract]mod Ownable { … }

If you remember the original code from v1, our smart contract had two “public” methods (get_owner and transfer_ownership) and one “private” method (only_owner). This feature only deals with public methods, without relying on “external” or “view” attributes to indicate which method can modify the contract’s state and which method is not allowed to. Instead, this is now made explicit through the type of parameter self.

If a method requires a reference to ContractStorage (which will be once implemented, generic T is like this), then that method can modify the smart contract’s internal state. This is what we used to call an “external” method. On the other hand, if a method requires a snapshot of the ContractStorage, then it can only read from it, not modify it. This is what we used to call a “view” method.

Now, we can create an implementation for the just defined feature using the keyword impl. Remember, the difference between Cairo and Rust is that the implementation has a name.

use starknet::ContractAddress;

#[starknet::interface]trait OwnableTrait { fn transfer_ownership(ref self: T, new_owner: ContractAddress); fn get_owner(self: @T) -> ContractAddress; }

#[starknet::contract]mod Ownable { … #[external(v0)] impl OwnableImpl of super::OwnableTrait { fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { let prev_owner = self.owner.read(); self.owner.write(new_owner); }

fn get_owner(self: @ContractState) -> ContractAddress { self.owner.read() } }}

We create an implementation for our feature within the module that defines the smart contract, passing the type ContractState as the generic type T, which allows us to access storage like in the constructor.

Our implementation is annotated with the attribute external(v0). The version 0 in the attribute means that the selector is derived only from the method name, just like in the past. The downside is that if you define an implementation for another different feature for your smart contract and both features happen to use the same name for one of their methods, the compiler will throw an error due to selector collision.

A future version of this attribute may add a new way to calculate the selector to avoid conflicts, but it is not yet available. For now, we can only use version 0 of the external attribute.

Private Methods

We also need to define another method for the smart contract, only_owner. This method checks whether the person calling it should be the owner of the smart contract. Because this is a private method that is not allowed to be called from outside, it cannot be defined as part of the OwnableTrait (the public interface of the smart contract). Instead, we will use the generate_trait attribute to create a new implementation that automatically generates traits.

…#[starknet::contract]mod Ownable { … #[generate_trait] impl PrivateMethods of PrivateMethodsTrait { fn only_owner(self: @ContractState) { let caller = get_caller_address(); assert(caller == self.owner.read(), ‘Caller is not the owner’); } }}

Now you can use the only_owner method by calling self.only_owner() where necessary.

#[starknet::contract]mod Ownable { … #[external(v0)] impl OwnableImpl of super::OwnableTrait { fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { self.only_owner(); … } … }

#[generate_trait] impl PrivateMethods of PrivateMethodsTrait { fn only_owner(self: @ContractState) { … } }}

Events

In Cairo v1, events were simply functions without a body, annotated with the event attribute. In v2, events are an enum annotated with the same attribute, but now some additional features are implemented using derive.

…#[starknet::contract]mod Ownable { … #[event] #[derive(Drop, starknet::Event)] enum Event { OwnershipTransferred: OwnershipTransferred, }

#[derive(Drop, starknet::Event)] struct OwnershipTransferred { #[key] prev_owner: ContractAddress, #[key] new_owner: ContractAddress, }

Each variant of the event enum must be a struct with the same name. In that struct, use the optional key attribute to define all the values we want to emit to notify the system which values we want Starknet to index, so that the indexer can search and retrieve more quickly. In this case, we want to index two values (prev_owner and new_owner).

The ContractState trait defines an emit method that can be used to emit events.

…#[starknet::contract]mod Ownable { … #[external(v0)] impl OwnableImpl of super::OwnableTrait { fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { … self.emit(Event::OwnershipTransferred(OwnershipTransferred { prev_owner: prev_owner, new_owner: new_owner, })); } … } …}

With this final feature, we have completed the migration of the Ownable smart contract from v1 to v2. The complete code is shown below.

use starknet::ContractAddress;

#[starknet::interface]trait OwnableTrait { fn transfer_ownership(ref self: T, new_owner: ContractAddress); fn get_owner(self: @T) -> ContractAddress;}

#[starknet::contract]mod Ownable { use super::ContractAddress; use starknet::get_caller_address;

#[event] #[derive(Drop, starknet::Event)] enum Event { OwnershipTransferred: OwnershipTransferred, }

#[derive(Drop, starknet::Event)] struct OwnershipTransferred { #[key] prev_owner: ContractAddress, #[key] new_owner: ContractAddress, }

#[storage] struct Storage { owner: ContractAddress, }

#[constructor] fn constructor(ref self: ContractState) { let deployer = get_caller_address(); self.owner.write(deployer); }

#[external(v0)] impl OwnableImpl of super::OwnableTrait { fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { self.only_owner(); let prev_owner = self.owner.read(); self.owner.write(new_owner); self.emit(Event::OwnershipTransferred(OwnershipTransferred { prev_owner: prev_owner, new_owner: new_owner, })); }

fn get_owner(self: @ContractState) -> ContractAddress { self.owner.read() } }

#[generate_trait] impl PrivateMethods of PrivateMethodsTrait { fn only_owner(self: @ContractState) { let caller = get_caller_address(); assert(caller == self.owner.read(), ‘Caller is not the owner’); } }}

You can find this code on Github as well.

Conclusion

The Cairo compiler version 2 brings new syntax to Starknet that makes smart contract code look more consistent with Cairo itself and more similar to Rust in its extensions. Even if it sacrifices more verbose code, the security benefits are worth considering.

In this article, we have not touched on all the content about the new syntax, especially on how to interact with other smart contracts, but you can read the changelog of the compiler, read this article on the forum, or watch the video on StarkWare’s YouTube channel to learn more.

This new version of the compiler will be available on the Starknet testnet in a few weeks and on the mainnet a few weeks later, so do not try to deploy this code yet, as it cannot run.

Cairo is getting better and better.

Resources

  • Contract Syntax – Migration Guide
  • Cairo 1: Contract Syntax Evolving

Like what you're reading? Subscribe to our top stories.

We will continue to update Gambling Chain; if you have any questions or suggestions, please contact us!

Follow us on Twitter, Facebook, YouTube, and TikTok.

Share:

Was this article helpful?

93 out of 132 found this helpful

Gambling Chain Logo
Industry
Digital Asset Investment
Location
Real world, Metaverse and Network.
Goals
Build Daos that bring Decentralized finance to more and more persons Who love Web3.
Type
Website and other Media Daos

Products used

GC Wallet

Send targeted currencies to the right people at the right time.