Starknet by Example
Starknet By Example是如何使用Cairo编程语言在Starknet上创建智能合约的范例集。 中文版由 StarknetAstro 社区翻译。
Starknet是一种支持通用计算的无权限Validity-Rollup。它目前被用作以太坊的第二层。Starknet 使用 STARK 加密证明系统来确保高安全性和可扩展性。
Starknet智能合约是用Cairo语言编写的。Cairo语言是一种图灵完备的编程语言,旨在编写可证明的程序,将 zk-STARK 证明系统从程序员手中抽象出来。
The current version of this book use:
scarb 2.3.1
谁该读这本书?
Starknet By Example适合想要快速学习如何使用 Cairo 在 Starknet 上编写智能合约,并具有一定编程和区块链技术背景的人。
前几章将让你基本了解Cairo编程语言,以及如何在Starknet编写、部署和使用智能合约。 后面的章节将涉及更高级的主题,并向你展示如何编写更复杂的智能合约。
进一步阅读
如果你想进一步了解 Cairo 编程语言,可以阅读Cairo Book。 如果你想进一步了解星网,可以阅读Starknet documentation 和Starknet Book。
以下是您可能会用到的其他资源清单:
- Starklings:让您使用 Cairo v1 和Starknet互动的教程
- Cairopractice:关于Cairo和Starknet的一系列文章的博客
- Cairo by example:Cairo 简介,附带简单示例
Cairo的智能合约基础知识
以下章节将向你介绍Starknet智能合约以及如何用Cairo编写这些合约。
存储
这是您用Cairo能写的最简短的合约:
#[starknet::contract]
mod Contract {
#[storage]
struct Storage {}
}
存储是一个结构体,用 #[storage]
标注。每个合约必须有且仅有一个存储空间。
它是一个键值存储空间,其中每个键都将映射到合约存储空间的存储地址。
您可以在合约中定义 [存储变量](./variables.md#storage-variables),然后使用它们来存储和检索数据。
#[starknet::contract]
mod Contract {
#[storage]
struct Storage {
a: u128,
b: u8,
c: u256
}
}
实际上,这两个合约的底层 sierra 程序是一样的。 从编译器的角度来看,存储变量在使用之前是不存在的。
您还可以阅读有关 存储自定义类型 的内容。
构造函数
构造函数是一种特殊类型的函数,只在部署合约时运行一次,可用于初始化合约的状态。你的合约不能有一个以上的构造函数,而且构造函数必须使用 #[constructor]
属性注释。此外,一个好的做法是将该函数命名为 constructor
。
下面是一个简单的示例,演示如何通过在构造函数中定义逻辑,在部署时初始化合约的状态。
#[starknet::contract]
mod ExampleConstructor {
use starknet::ContractAddress;
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
}
// The constructor is decorated with a `#[constructor]` attribute.
// It is not inside an `impl` block.
#[constructor]
fn constructor(ref self: ContractState, name: felt252, address: ContractAddress) {
self.names.write(address, name);
}
}
访问 Voyager 上的合约,或在 Remix 中尝试它。
变量
Cairo合约中有 3 种变量:
- 局部
- 在函数中声明
- 不存储在区块链中
- 存储
- 在合约的 Storage 中声明
- 可从一个执行过程访问到另一个执行过程
- 全局
- 提供有关区块链的信息
- 可在任何地方访问,甚至在库函数中
局部变量
局部变量在特定函数或代码块的范围内使用和访问。它们是临时的,只在特定函数或代码块执行期间存在。
局部变量存储在内存中,不会存储在区块链上。这就意味着在执行过程中无法访问它们。局部变量可用于存储仅在特定上下文中相关的临时数据。通过为中间值命名,它们还能使代码更具可读性。
下面是一个只有局部变量的简单合约示例:
#[starknet::interface]
trait ILocalVariablesExample<TContractState> {
fn do_something(self: @TContractState, value: u32) -> u32;
}
#[starknet::contract]
mod LocalVariablesExample {
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl LocalVariablesExample of super::ILocalVariablesExample<ContractState> {
fn do_something(self: @ContractState, value: u32) -> u32 {
// This variable is local to the current block. It can't be accessed once it goes out of scope.
let increment = 10;
{
// The scope of a code block allows for local variable declaration
// We can access variables defined in higher scopes.
let sum = value + increment;
sum
}
}
}
}
访问 Voyager 上的合约,或在 Remix 中尝试它。
存储用变量
存储变量是存储在区块链上的持久数据。它们可以在不同的执行过程中被访问,从而使合约能够保存和更新信息。
要写入或更新存储变量,需要通过外部入口点发送交易与合约交互。
另一方面,只需与节点交互,就可以免费读取状态变量,无需发出任何交易。
下面是一个带有一个存储变量的简单合约示例:
#[starknet::interface]
trait IStorageVariableExample<TContractState> {
fn set(ref self: TContractState, value: u32);
fn get(self: @TContractState) -> u32;
}
#[starknet::contract]
mod StorageVariablesExample {
// All storage variables are contained in a struct called Storage
// annotated with the `#[storage]` attribute
#[storage]
struct Storage {
// Storage variable holding a number
value: u32
}
#[abi(embed_v0)]
impl StorageVariablesExample of super::IStorageVariableExample<ContractState> {
// Write to storage variables by sending a transaction that calls an external function
fn set(ref self: ContractState, value: u32) {
self.value.write(value);
}
// Read from storage variables without sending transactions
fn get(self: @ContractState) -> u32 {
self.value.read()
}
}
}
访问 Voyager 上的合约,或在 Remix 中尝试它。
全局变量
全局变量是预定义变量,可提供有关区块链和当前执行环境的信息。可以随时随地访问它们!
在 Starknet 中,您可以通过使用 starknet 核心库中的特定函数来访问全局变量。
例如,get_caller_address
函数返回当前事务的调用者地址,get_contract_address
函数返回当前合同的地址。
#[starknet::interface]
trait IGlobalExample<TContractState> {
fn foo(ref self: TContractState);
}
#[starknet::contract]
mod GlobalExample {
// import the required functions from the starknet core library
use starknet::get_caller_address;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl GlobalExampleImpl of super::IGlobalExample<ContractState> {
fn foo(ref self: ContractState) {
// Call the get_caller_address function to get the sender address
let caller = get_caller_address();
// ...
}
}
}
访问 Voyager 上的合约,或在 Remix 中尝试它。
可见性和可变性
可见性
Starknet合约有两种功能:
- 外部可访问、任何人都可调用的函数。
- 只能在内部访问的函数,只能被合约中的其他函数调用。
这些函数通常也分为两个不同的实现块。第一个impl
块用于外部访问的函数,明确标注了 #[abi(embed_v0)]
属性。这表明该代码块中的所有函数都可以作为交易或视图函数调用。第二个用于内部可访问函数的 impl
块没有注释任何属性,这意味着该块中的所有函数默认都是私有的。
状态可变性
无论函数是内部函数还是外部函数,它都可以修改或不修改合约的状态。当我们在智能合约中声明与存储变量交互的函数时,
我们需要将 ContractState
添加为函数的第一个参数,明确说明我们正在访问 合约的状态。这有两种不同的方法:
- 如果我们希望我们的函数能够更改合约的状态,我们可以像这样通过引用来传递它:ref self:ContractState`。
- 如果我们希望我们的函数是只读的,并且不更改合约的状态,我们可以通过快照传递它,如下所示:
self:@ContractState
.
只读函数(也称为视图函数)可以直接调用,无需进行事务处理。你可以直接通过 RPC 节点与它们交互,读取合约的状态,而且可以自由调用! 而修改合约状态的外部函数则只能通过交易来调用。
内部函数不能被外部调用,同样的原则也适用于状态可变性。
让我们通过一个简单的合约示例来了解这些功能:
#[starknet::interface]
trait IExampleContract<TContractState> {
fn set(ref self: TContractState, value: u32);
fn get(self: @TContractState) -> u32;
}
#[starknet::contract]
mod ExampleContract {
#[storage]
struct Storage {
value: u32
}
// The `abi(embed_v0)` attribute indicates that all the functions in this implementation can be called externally.
// Omitting this attribute would make all the functions in this implementation internal.
#[abi(embed_v0)]
impl ExampleContract of super::IExampleContract<ContractState> {
// The `set` function can be called externally because it is written inside an implementation marked as `#[external]`.
// It can modify the contract's state as it is passed as a reference.
fn set(ref self: ContractState, value: u32) {
self.value.write(value);
}
// The `get` function can be called externally because it is written inside an implementation marked as `#[external]`.
// However, it can't modify the contract's state is passed as a snapshot: it is only a "view" function.
fn get(self: @ContractState) -> u32 {
// We can call an internal function from any functions within the contract
PrivateFunctionsTrait::_read_value(self)
}
}
// The lack of the `external` attribute indicates that all the functions in this implementation can only be called internally.
// We name the trait `PrivateFunctionsTrait` to indicate that it is an internal trait allowing us to call internal functions.
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
// The `_read_value` function is outside the implementation that is marked as `#[abi(embed_v0)]`, so it's an _internal_ function
// and can only be called from within the contract.
// However, it can't modify the contract's state is passed as a snapshot: it is only a "view" function.
fn _read_value(self: @ContractState) -> u32 {
self.value.read()
}
}
}
访问 Voyager 上的合约,或在 Remix 中尝试它。
简单计数器
这是一个简单的计数合约。
这个合约是这样工作的:
-
合约有一个名为 'counter'的状态变量,初始化为 0。
-
当用户调用 'increment'时,合约会将计数器递增 1。
-
当用户调用 'decrement'时,合约会将计数器递减 1。
#[starknet::interface]
trait ISimpleCounter<TContractState> {
fn get_current_count(self: @TContractState) -> u128;
fn increment(ref self: TContractState);
fn decrement(ref self: TContractState);
}
#[starknet::contract]
mod SimpleCounter {
#[storage]
struct Storage {
// Counter variable
counter: u128,
}
#[constructor]
fn constructor(ref self: ContractState, init_value: u128) {
// Store initial value
self.counter.write(init_value);
}
#[abi(embed_v0)]
impl SimpleCounter of super::ISimpleCounter<ContractState> {
fn get_current_count(self: @ContractState) -> u128 {
return self.counter.read();
}
fn increment(ref self: ContractState) {
// Store counter value + 1
let counter = self.counter.read() + 1;
self.counter.write(counter);
}
fn decrement(ref self: ContractState) {
// Store counter value - 1
let counter = self.counter.read() - 1;
self.counter.write(counter);
}
}
}
访问 Voyager 上的合约,或在 Remix 中尝试它。
映射
映射是一种键值数据结构,用于在智能合约中存储数据。在开罗,它们使用 LegacyMap
类型实现。值得注意的是,LegacyMap
类型只能在合约的 Storage
结构中使用,不能用在其他地方。
在此,我们演示如何在Cairo合约中使用 LegacyMap
类型,在 ContractAddress
类型的键和 felt252
类型的值之间进行映射。键值类型在角括号 <> 中指定。我们通过调用 write()
方法,传入键和值,写入映射。同样,我们可以通过调用 read()
方法并输入相关键值来读取与给定键值相关的值。
一些补充说明:
-
也有更复杂的键值对映射,例如,我们可以使用
LegacyMap::<(ContractAddress, ContractAddress), felt252>
在 ERC20 代币合约上创建一个代币授权许可。 -
在映射中,键
k_1,...,k_n
处的值的地址是h(...h(h(sn_keccak(variable_name),k_1),k_2),...,k_n)
,其中ℎ
是 Pedersen 哈希值,最终值取mod2251-256
。有关合约存储布局的更多信息,请参阅 Starknet Documentation。
use starknet::ContractAddress;
#[starknet::interface]
trait IMapContract<TContractState> {
fn set(ref self: TContractState, key: ContractAddress, value: felt252);
fn get(self: @TContractState, key: ContractAddress) -> felt252;
}
#[starknet::contract]
mod MapContract {
use starknet::ContractAddress;
#[storage]
struct Storage {
// The `LegacyMap` type is only available inside the `Storage` struct.
map: LegacyMap::<ContractAddress, felt252>,
}
#[abi(embed_v0)]
impl MapContractImpl of super::IMapContract<ContractState> {
fn set(ref self: ContractState, key: ContractAddress, value: felt252) {
self.map.write(key, value);
}
fn get(self: @ContractState, key: ContractAddress) -> felt252 {
self.map.read(key)
}
}
}
错误
错误可用于处理智能合约执行过程中可能发生的验证和其他条件。 如果在执行智能合约调用期间抛出错误,则将停止执行,并恢复在交易期间所做的任何更改。
要抛出错误,请使用 assert
或 panic
函数:
-
'assert' 用于验证条件。 如果检查失败,则会引发错误以及指定的值,通常是一条消息。 它类似于 Solidity 中的
require
语句。 -
'panic' 立即停止执行,并给出错误值。 当要检查的条件复杂且存在内部错误时,应使用它。它类似于 Solidity 中的
revert
语句。 (使用panic_with_felt252
可以直接传递一个felt252作为错误值)
下面是一个简单的示例,演示了这些函数的用法:
#[starknet::interface]
trait IErrorsExample<TContractState> {
fn test_assert(self: @TContractState, i: u256);
fn test_panic(self: @TContractState, i: u256);
}
#[starknet::contract]
mod ErrorsExample {
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl ErrorsExample of super::IErrorsExample<ContractState> {
fn test_assert(self: @ContractState, i: u256) {
// Assert used to validate a condition
// and abort execution if the condition is not met
assert(i > 0, 'i must be greater than 0');
}
fn test_panic(self: @ContractState, i: u256) {
if (i == 0) {
// Panic used to abort execution directly
panic_with_felt252('i must not be 0');
}
}
}
}
自定义错误
您可以通过在特定模块中定义错误代码来简化错误处理。
mod Errors {
const NOT_POSITIVE: felt252 = 'must be greater than 0';
const NOT_NULL: felt252 = 'must not be null';
}
#[starknet::interface]
trait ICustomErrorsExample<TContractState> {
fn test_assert(self: @TContractState, i: u256);
fn test_panic(self: @TContractState, i: u256);
}
#[starknet::contract]
mod CustomErrorsExample {
use super::Errors;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl CustomErrorsExample of super::ICustomErrorsExample<ContractState> {
fn test_assert(self: @ContractState, i: u256) {
assert(i > 0, Errors::NOT_POSITIVE);
}
fn test_panic(self: @ContractState, i: u256) {
if (i == 0) {
panic_with_felt252(Errors::NOT_NULL);
}
}
}
}
在 Voyager 上访问 contract 或在 Remix 中尝试它。
Vault 示例
下面是另一个示例,演示了在更复杂的合约中使用错误:
mod VaultErrors {
const INSUFFICIENT_BALANCE: felt252 = 'insufficient_balance';
// you can define more errors here
}
#[starknet::interface]
trait IVaultErrorsExample<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, amount: u256);
}
#[starknet::contract]
mod VaultErrorsExample {
use super::VaultErrors;
#[storage]
struct Storage {
balance: u256,
}
#[abi(embed_v0)]
impl VaultErrorsExample of super::IVaultErrorsExample<ContractState> {
fn deposit(ref self: ContractState, amount: u256) {
let mut balance = self.balance.read();
balance = balance + amount;
self.balance.write(balance);
}
fn withdraw(ref self: ContractState, amount: u256) {
let mut balance = self.balance.read();
assert(balance >= amount, VaultErrors::INSUFFICIENT_BALANCE);
// Or using panic:
if (balance >= amount) {
panic_with_felt252(VaultErrors::INSUFFICIENT_BALANCE);
}
let balance = balance - amount;
self.balance.write(balance);
}
}
}
在 Voyager 上访问 contract 或在 Remix 中尝试它。
事件
事件是从合约发出数据的一种方式。所有事件都必须在Event
枚举中定义,该枚举必须使用#[event]
属性进行注释。
事件被定义为派生#[starknet::Event]
特征的结构。该结构的字段对应于将要发出的数据。可以对事件编制索引,以便在以后查询数据时轻松快速地访问。可以通过向字段成员添加#[key]
属性来索引事件数据。
下面是合约使用事件的简单示例,这些事件在每次计数器通过“increment”函数递增时发出一个事件:
#[starknet::interface]
trait IEventCounter<TContractState> {
fn increment(ref self: TContractState);
}
#[starknet::contract]
mod EventCounter {
use starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
// Counter value
counter: u128,
}
#[event]
#[derive(Drop, starknet::Event)]
// The event enum must be annotated with the `#[event]` attribute.
// It must also derive the `Drop` and `starknet::Event` traits.
enum Event {
CounterIncreased: CounterIncreased,
UserIncreaseCounter: UserIncreaseCounter
}
// By deriving the `starknet::Event` trait, we indicate to the compiler that
// this struct will be used when emitting events.
#[derive(Drop, starknet::Event)]
struct CounterIncreased {
amount: u128
}
#[derive(Drop, starknet::Event)]
struct UserIncreaseCounter {
// The `#[key]` attribute indicates that this event will be indexed.
#[key]
user: ContractAddress,
new_value: u128,
}
#[abi(embed_v0)]
impl EventCounter of super::IEventCounter<ContractState> {
fn increment(ref self: ContractState) {
let mut counter = self.counter.read();
counter += 1;
self.counter.write(counter);
// Emit event
self.emit(Event::CounterIncreased(CounterIncreased { amount: 1 }));
self
.emit(
Event::UserIncreaseCounter(
UserIncreaseCounter {
user: get_caller_address(), new_value: self.counter.read()
}
)
);
}
}
}
在 Voyager 上访问 合约 或在 Remix 中尝试它。
存储自定义类型
虽然本机类型可以存储在合约的存储中,而无需任何额外的工作,但自定义类型需要更多的工作。这是因为在编译时,编译器不知道如何在存储中存储自定义类型。为了解决这个问题,我们需要为我们的自定义类型实现 Store
特征。希望我们可以为我们的自定义类型派生这个特征 - 除非它包含数组或字典。
#[starknet::interface]
trait IStoringCustomType<TContractState> {
fn set_person(ref self: TContractState, person: Person);
}
// Deriving the starknet::Store trait
// allows us to store the `Person` struct in the contract's storage.
#[derive(Drop, Serde, Copy, starknet::Store)]
struct Person {
age: u8,
name: felt252
}
#[starknet::contract]
mod StoringCustomType {
use super::Person;
#[storage]
struct Storage {
person: Person
}
#[abi(embed_v0)]
impl StoringCustomType of super::IStoringCustomType<ContractState> {
fn set_person(ref self: ContractState, person: Person) {
self.person.write(person);
}
}
}
在 Remix 中尝试这个合约。
入口点中的自定义类型
在入口点中使用自定义类型需要我们的类型来实现Serde
trait。这是因为在调用入口点时,输入以felt252
数组的形式发送到入口点,我们需要能够将其反序列化为我们的自定义类型。同样,当从入口点返回自定义类型时,我们需要能够将其序列化为felt252
数组。
值得庆幸的是,我们可以为我们的自定义类型派生Serde
特征。
#[starknet::interface]
trait ISerdeCustomType<TContractState> {
fn person_input(ref self: TContractState, person: SerdeCustomType::Person);
fn person_output(self: @TContractState) -> SerdeCustomType::Person;
}
#[starknet::contract]
mod SerdeCustomType {
#[storage]
struct Storage {}
// Deriving the `Serde` trait allows us to use
// the Person type as an entrypoint parameter and return value
#[derive(Drop, Serde)]
struct Person {
age: u8,
name: felt252
}
#[abi(embed_v0)]
impl SerdeCustomType of super::ISerdeCustomType<ContractState> {
fn person_input(ref self: ContractState, person: Person) {}
fn person_output(self: @ContractState) -> Person {
Person { age: 10, name: 'Joe' }
}
}
}
在 Remix 中尝试这个合约。
文档
花时间为你的代码写文档非常重要。它将帮助开发人员和用户了解合约及其功能。
在Cairo,您可以使用“//”添加注释。
最佳实践:
自 Cairo 1 以来,社区采用了 类似 Rust 的文档风格。
合约接口:
在智能合约中,你通常会有一个定义合约接口的trait(带有'#[starknet::interface]')。 这是包含详细文档的理想场所,这些文档解释了合约入口点的用途和功能。您可以遵循以下模板:
#[starknet::interface]
trait IContract<TContractState> {
/// High-level description of the function
///
/// # Arguments
///
/// * `arg_1` - Description of the argument
/// * `arg_n` - ...
///
/// # Returns
///
/// High-level description of the return value
fn do_something(ref self: TContractState, arg_1: T_arg_1) -> T_return;
}
请记住,这不应该描述函数的实现细节,而应该从用户的角度描述合约的高级目的和功能。
实装细节:
在编写合约逻辑时,可以添加注释来描述函数的技术实现细节。
避免过度注释:注释应提供额外的价值和清晰度。
部署合约并与合约交互
在本章中,我们将了解如何部署合约并与之交互。
合约接口和Trait生成
合约接口定义合约的结构和行为,充当合约的公共 ABI。它们列出了合约公开的所有函数签名。接口的详细说明可以参考 [Cairo之书](https://book.cairo-lang.org/ch99-01-02-a-simple-contract.html)。
在cairo中,要指定接口,您需要定义一个带有#[starknet::interface]
注释的特征,然后在合约中实现该特征。
当函数需要访问协定状态时,它必须具有类型为ContractState
的self
参数。这意味着接口特征中的相应函数签名也必须采用TContractState
类型作为参数。需要注意的是,合约接口中的每个函数都必须具有此类型为TContractState
的self
参数。
您可以使用#[generate_trait]
属性隐式生成特定实现块的特征。此属性会自动生成一个特征,其功能与已实现块中的函数相同,将self
参数替换为通用的TContractState
参数。但是,您需要使用#[abi(per_item)]
属性注释块,并且每个函数都具有适当的属性,具体取决于它是外部函数、构造函数还是 l1 处理程序。
总之,有两种方法可以处理接口:
- 显示地,通过定义一个用
#[starknet::interface]
标记的特征 - 隐式地,通过将
#[generate_trait]
与#[abi(per_item)]
属性结合使用,并使用适当的属性注释实现块中的每个函数。
显式接口
#[starknet::interface]
trait IExplicitInterfaceContract<TContractState> {
fn get_value(self: @TContractState) -> u32;
fn set_value(ref self: TContractState, value: u32);
}
#[starknet::contract]
mod ExplicitInterfaceContract {
#[storage]
struct Storage {
value: u32
}
#[abi(embed_v0)]
impl ExplicitInterfaceContract of super::IExplicitInterfaceContract<ContractState> {
fn get_value(self: @ContractState) -> u32 {
self.value.read()
}
fn set_value(ref self: ContractState, value: u32) {
self.value.write(value);
}
}
}
在 Remix 中尝试这个合约。
隐式接口
#[starknet::contract]
mod ImplicitInterfaceContract {
#[storage]
struct Storage {
value: u32
}
#[abi(per_item)]
#[generate_trait]
impl ImplicitInterfaceContract of IImplicitInterfaceContract {
#[external(v0)]
fn get_value(self: @ContractState) -> u32 {
self.value.read()
}
#[external(v0)]
fn set_value(ref self: ContractState, value: u32) {
self.value.write(value);
}
}
}
在 Remix 中尝试这个合约。
注意:您可以使用
use contract::{GeneratedContractInterface}
导入隐式生成的合约接口。但是,Dispatcher
不会自动生成。
内部函数
您还可以将#[generate_trait]
用于内部函数。
由于此特征是在合约的上下文中生成的,因此您也可以定义纯函数(没有“self”参数的函数)。
#[starknet::interface]
trait IImplicitInternalContract<TContractState> {
fn add(ref self: TContractState, nb: u32);
fn get_value(self: @TContractState) -> u32;
fn get_const(self: @TContractState) -> u32;
}
#[starknet::contract]
mod ImplicitInternalContract {
#[storage]
struct Storage {
value: u32
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn set_value(ref self: ContractState, value: u32) {
self.value.write(value);
}
fn get_const() -> u32 {
42
}
}
#[constructor]
fn constructor(ref self: ContractState) {
self.set_value(0);
}
#[abi(embed_v0)]
impl ImplicitInternalContract of super::IImplicitInternalContract<ContractState> {
fn add(ref self: ContractState, nb: u32) {
self.set_value(self.value.read() + nb);
}
fn get_value(self: @ContractState) -> u32 {
self.value.read()
}
fn get_const(self: @ContractState) -> u32 {
self.get_const()
}
}
}
在 Remix 中尝试这个合约。
调用其他合约
在Cairo,有两种不同的方式可以调用其他合约。
调用其他合约的最简单方法是使用要调用的合约的调度程序。 您可以在 Cairo Book 中阅读有关 Dispatchers 的更多信息
另一种方法是自己使用starknet::call_contract_syscall
系统调用。但是,不建议使用此方法。
为了使用调度程序调用其他合约,您需要将被调用合约的接口定义为使用 #[starknet::interface]
属性注释的trait,然后将 IContractDispatcher
和 IContractDispatcherTrait
项导入到合约中。
#[starknet::interface]
trait ICallee<TContractState> {
fn set_value(ref self: TContractState, value: u128) -> u128;
}
#[starknet::contract]
mod Callee {
#[storage]
struct Storage {
value: u128,
}
#[abi(embed_v0)]
impl ICalleeImpl of super::ICallee<ContractState> {
fn set_value(ref self: ContractState, value: u128) -> u128 {
self.value.write(value);
value
}
}
}
在 Voyager 上访问 合约 或在 Remix中尝试它。
use starknet::ContractAddress;
// We need to have the interface of the callee contract defined
// so that we can import the Dispatcher.
#[starknet::interface]
trait ICallee<TContractState> {
fn set_value(ref self: TContractState, value: u128) -> u128;
}
#[starknet::interface]
trait ICaller<TContractState> {
fn set_value_from_address(ref self: TContractState, addr: ContractAddress, value: u128);
}
#[starknet::contract]
mod Caller {
// We import the Dispatcher of the called contract
use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
use starknet::ContractAddress;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl ICallerImpl of super::ICaller<ContractState> {
fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
ICalleeDispatcher { contract_address: addr }.set_value(value);
}
}
}
工厂模式
工厂模式是面向对象编程中众所周知的模式。它提供了有关如何实例化类的抽象。
在智能合约里,我们可以通过定义一个工厂合约来使用这种模式,该合约全权负责创建和管理其他合约。
类哈希(Class hash)和合约实例
在Starknet中,合约的类和实例是分开的。合约类充当蓝图,由底层 Cairo 字节码、合约的入口点、ABI 和 Sierra 程序哈希定义。合约类由类哈希标识。当您想向网络添加一个新类时,首先需要声明它。
部署合约时,需要指定要部署的合约的类哈希值。合约的每个实例都有自己的存储,这与类哈希无关。
使用工厂模式,我们可以部署同一合约类的多个实例,并轻松处理升级。
最小范例
下面是部署SimpleCounter
合约的工厂合约的最小范例:
use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
trait ICounterFactory<TContractState> {
/// Create a new counter contract from stored arguments
fn create_counter(ref self: TContractState) -> ContractAddress;
/// Create a new counter contract from the given arguments
fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;
/// Update the argument
fn update_init_value(ref self: TContractState, init_value: u128);
/// Update the class hash of the Counter contract to deploy when creating a new counter
fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}
#[starknet::contract]
mod CounterFactory {
use starknet::{ContractAddress, ClassHash};
use starknet::syscalls::deploy_syscall;
#[storage]
struct Storage {
/// Store the constructor arguments of the contract to deploy
init_value: u128,
/// Store the class hash of the contract to deploy
counter_class_hash: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) {
self.init_value.write(init_value);
self.counter_class_hash.write(class_hash);
}
#[abi(embed_v0)]
impl Factory of super::ICounterFactory<ContractState> {
fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress {
// Contructor arguments
let mut constructor_calldata: Array::<felt252> = array![init_value.into()];
// Contract deployment
let (deployed_address, _) = deploy_syscall(
self.counter_class_hash.read(), 0, constructor_calldata.span(), false
)
.expect('failed to deploy counter');
deployed_address
}
fn create_counter(ref self: ContractState) -> ContractAddress {
self.create_counter_at(self.init_value.read())
}
fn update_init_value(ref self: ContractState, init_value: u128) {
self.init_value.write(init_value);
}
fn update_counter_class_hash(ref self: ContractState, counter_class_hash: ClassHash) {
self.counter_class_hash.write(counter_class_hash);
}
}
}
此工厂可用于通过调用SimpleCounter
和create_counter
函数来部署create_counter_at
合约的多个实例。
SimpleCounter
类哈希存储在工厂内部,可以使用update_counter_class_hash
函数进行升级,该函数允许在升级SimpleCounter
合约时重用相同的工厂合约。
这个最小的范例缺少几个有用的功能,例如访问控制、跟踪已部署的合约、事件......
合约测试
测试在软件开发中起着至关重要的作用,尤其是对于智能合约而言。在本节中,我们将通过Starknet上的scarb
,引导你了解智能合约测试的基础知识。
让我们以一个简单的智能合约作为例子开始:
use starknet::ContractAddress;
#[starknet::interface]
trait ISimpleContract<TContractState> {
fn get_value(self: @TContractState) -> u32;
fn get_owner(self: @TContractState) -> ContractAddress;
fn set_value(ref self: TContractState, value: u32);
}
#[starknet::contract]
mod SimpleContract {
use starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
value: u32,
owner: ContractAddress
}
#[constructor]
fn constructor(ref self: ContractState, initial_value: u32) {
self.value.write(initial_value);
self.owner.write(get_caller_address());
}
#[abi(embed_v0)]
impl SimpleContract of super::ISimpleContract<ContractState> {
fn get_value(self: @ContractState) -> u32 {
self.value.read()
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
fn set_value(ref self: ContractState, value: u32) {
assert(self.owner.read() == get_caller_address(), ‘Not owner’);
self.value.write(value);
}
}
}
现在,让我们看一下这个合约的测试:
#[cfg(test)]
mod tests {
// Import the interface and dispatcher to be able to interact with the contract.
use testing_how_to::contract::{
ISimpleContract, SimpleContract, ISimpleContractDispatcher, ISimpleContractDispatcherTrait
};
// Import the deploy syscall to be able to deploy the contract.
use starknet::class_hash::Felt252TryIntoClassHash;
use starknet::{
deploy_syscall, ContractAddress, get_caller_address, get_contract_address,
contract_address_const
};
// Use starknet test utils to fake the transaction context.
use starknet::testing::{set_caller_address, set_contract_address};
// Deploy the contract and return its dispatcher.
fn deploy(initial_value: u32) -> ISimpleContractDispatcher {
// Set up constructor arguments.
let mut calldata = ArrayTrait::new();
initial_value.serialize(ref calldata);
// Declare and deploy
let (contract_address, _) = deploy_syscall(
SimpleContract::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
)
.unwrap();
// Return the dispatcher.
// The dispatcher allows to interact with the contract based on its interface.
ISimpleContractDispatcher { contract_address }
}
#[test]
#[available_gas(2000000000)]
fn test_deploy() {
let initial_value: u32 = 10;
let contract = deploy(initial_value);
assert(contract.get_value() == initial_value, 'wrong initial value');
assert(contract.get_owner() == get_contract_address(), 'wrong owner');
}
#[test]
#[available_gas(2000000000)]
fn test_set_as_owner() {
// Fake the caller address to address 1
let owner = contract_address_const::<1>();
set_contract_address(owner);
let contract = deploy(10);
assert(contract.get_owner() == owner, 'wrong owner');
// Fake the contract address to address 1
set_contract_address(owner);
let new_value: u32 = 20;
contract.set_value(new_value);
assert(contract.get_value() == new_value, 'wrong value');
}
#[test]
#[should_panic]
#[available_gas(2000000000)]
fn test_set_not_owner() {
let owner = contract_address_const::<1>();
set_contract_address(owner);
let contract = deploy(10);
let not_owner = contract_address_const::<2>();
set_contract_address(not_owner);
let new_value: u32 = 20;
contract.set_value(new_value);
}
}
Play with this contract in Remix.
为了定义我们的测试,我们使用 scarb,它允许我们创建一个被 #[cfg(test)]
保护的独立模块。这样可以确保测试模块只在使用 scarb test
运行测试时被编译。
每个测试都被定义为带有 #[test]
属性的函数。您还可以使用 #[should_panic]
属性检查测试是否会引发 panic。
由于我们处于智能合约的上下文中,设置 gas 限制非常重要。你可以通过使用 #[available_gas(X)]
属性来指定测试的 gas 限制。这也是确保合约功能保持在某个特定 gas 限制下的好方法!
注意:这里的 “gas” 一词指的是 Sierra gas,而不是 L1 的 gas
现在,让我们进入测试过程:
- 使用
deploy
函数的逻辑来声明和部署您的合约。 - 使用
assert
来验证合约在给定的上下文中的行为是否符合预期。
为了使测试更加方便,corelib 的 testing
模块提供了一些有用的函数:
set_caller_address(address: ContractAddress)
set_contract_address(address: ContractAddress)
set_block_number(block_number: u64)
set_block_timestamp(block_timestamp: u64)
set_account_contract_address(address: ContractAddress)
set_max_fee(fee: u128)
你可能还需要 corelib 中的 info
模块,它允许你访问有关当前交易上下文的信息:
get_caller_address() -> ContractAddress
get_contract_address() -> ContractAddress
get_block_info() -> Box<BlockInfo>
get_tx_info() -> Box<TxInfo>
get_block_timestamp() -> u64
get_block_number() -> u64
你可以在 Starknet Corelib 仓库 中找到完整的函数列表。 你还可以在 Cairo book 第8章 中找到有关在 cairo 中进行测试的详细说明。
Starknet Foundry
<!— TODO update this when Starknet Foundry is more mature. —>
Starknet Foundry是在Starknet上开发智能合约的强大工具包。它提供了对使用Forge
工具在 scarb
上测试Starknet智能合约的支持。
使用 snforge
进行测试与我们刚刚描述的过程类似,但更简化。此外,还有其他功能正在开发中,包括作弊码或并行测试执行。我们强烈推荐探索Starknet Foundry并将其纳入您你的项目中。
有关使用Starknet Foundry测试合约的更详细信息,请参阅Starknet Foundry Book - 合约测试。
Cairo 备忘单
本章旨在为最常见的Cairo结构提供快速参考。
Felt252
Felt252是Cairo中的基本数据类型,所有其他数据类型都派生自它。 Felt252也可以用于存储最多31个字符长度的短字符串表示。
例如:
let felt: felt252 = 100;
let felt_as_str = ‘Hello Starknet!’;
let felt = felt + felt_as_str;
Mapping
LegacyMap
类型可以用于表示键值对的集合。
use starknet::ContractAddress;
#[starknet::interface]
trait IMappingExample<TContractState> {
fn register_user(ref self: TContractState, student_add: ContractAddress, studentName: felt252);
fn record_student_score(
ref self: TContractState, student_add: ContractAddress, subject: felt252, score: u16
);
fn view_student_name(self: @TContractState, student_add: ContractAddress) -> felt252;
fn view_student_score(
self: @TContractState, student_add: ContractAddress, subject: felt252
) -> u16;
}
#[starknet::contract]
mod MappingContract {
use starknet::ContractAddress;
#[storage]
struct Storage {
students_name: LegacyMap::<ContractAddress, felt252>,
students_result_record: LegacyMap::<(ContractAddress, felt252), u16>,
}
#[abi(embed_v0)]
impl External of super::IMappingExample<ContractState> {
fn register_user(
ref self: ContractState, student_add: ContractAddress, studentName: felt252
) {
self.students_name.write(student_add, studentName);
}
fn record_student_score(
ref self: ContractState, student_add: ContractAddress, subject: felt252, score: u16
) {
self.students_result_record.write((student_add, subject), score);
}
fn view_student_name(self: @ContractState, student_add: ContractAddress) -> felt252 {
self.students_name.read(student_add)
}
fn view_student_score(
self: @ContractState, student_add: ContractAddress, subject: felt252
) -> u16 {
// 对于二维映射,重要的是注意使用的括号数量。
self.students_result_record.read((student_add, subject))
}
}
}
数组
数组是相同类型元素的集合。
可以使用 corelib 的 array::ArrayTrait
来定义可能的数组操作:
trait ArrayTrait<T> {
fn new() -> Array<T>;
fn append(ref self: Array<T>, value: T);
fn pop_front(ref self: Array<T>) -> Option<T> nopanic;
fn pop_front_consume(self: Array<T>) -> Option<(Array<T>, T)> nopanic;
fn get(self: @Array<T>, index: usize) -> Option<Box<@T>>;
fn at(self: @Array<T>, index: usize) -> @T;
fn len(self: @Array<T>) -> usize;
fn is_empty(self: @Array<T>) -> bool;
fn span(self: @Array<T>) -> Span<T>;
}
例如:
fn array() -> bool {
let mut arr = ArrayTrait::<u32>::new();
arr.append(10);
arr.append(20);
arr.append(30);
assert(arr.len() == 3, ‘array length should be 3’);
let first_value = arr.pop_front().unwrap();
assert(first_value == 10, ‘first value should match’);
let second_value = *arr.at(0);
assert(second_value == 20, ‘second value should match’);
// 如果数组为空,返回 true;如果不为空,返回 false。
arr.is_empty()
}
循环
循环指定一个代码块,该代码块将重复运行,直到遇到停止条件。 例如:
let mut arr = ArrayTrait::new();
// 与 ~ while (i < 10) arr.append(i++); 相同
let mut i: u32 = 0;
let limit = 10;
loop {
if i == limit {
break;
};
arr.append(i);
i += 1;
};
分支
在 Cairo 中,”match” 表达式允许我们通过将 felt 数据类型或枚举与各种模式进行比较,然后根据匹配的模式运行特定的代码来控制代码的流程。例如:
#[derive(Drop, Serde)]
enum Colour {
Red,
Blue,
Green,
Orange,
Black
}
#[derive(Drop, Serde)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> felt252 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn specified_colour(colour: Colour) -> felt252 {
let mut response: felt252 = ‘’;
match colour {
Colour::Red => { response = ‘You passed in Red’; },
Colour::Blue => { response = ‘You passed in Blue’; },
Colour::Green => { response = ‘You passed in Green’; },
Colour::Orange => { response = ‘You passed in Orange’; },
Colour::Black => { response = ‘You passed in Black’; },
};
response
}
fn quiz(num: felt252) -> felt252 {
let mut response: felt252 = ‘’;
match num {
0 => { response = ‘You failed’ },
_ => { response = ‘You Passed’ },
};
response
}
元组
元组是一种数据类型,用于将固定数量的不同类型的项组合成一个单一的复合结构。与数组不同,元组具有固定的长度,并且可以包含不同类型的元素。一旦创建了元组,其大小就无法更改。 例如:
let address = “0x000”;
let age = 20;
let active = true;
// Create tuple
let user_tuple = (address, age, active);
// Access tuple
let (address, age, active) = stored_tuple;
结构体
结构体是一种类似于元组的数据类型。与元组类似,它们可以用于保存不同类型的数据。 例如:
// 使用 Store,您可以将 Data 结构体存储在合约的存储部分。
#[derive(Drop, starknet::Store)]
struct Data {
address: starknet::ContractAddress,
age: u8
}
类型转换
Cairo支持使用into和try_into方法将一个标量类型转换为另一个类型。
traits::Into
用于从较小的数据类型转换为较大的数据类型,而 traits::TryInto
用于从较大的数据类型转换为较小的数据类型,可能会发生溢出的情况。
例如:
let a_number: u32 = 15;
let my_felt252 = 15;
// 由于 u32 可能不匹配 u8 和 u16,我们需要使用 try_into
// 然后解包返回的 Option<T> 类型。
let new_u8: u8 = a_number.try_into().unwrap();
let new_u16: u16 = a_number.try_into().unwrap();
// 由于 new_u32 的类型(u32)与 a_number 相同,我们可以直接赋值
// 或使用 .into() 方法
let new_u32: u32 = a_number;
// 当从较小的大小类型强制转换为相等或较大的大小类型时,我们使用 .into() 方法
// 注意:u64 和 u128 大于 u32,所以 u32 类型将始终适合其中
let new_u64: u64 = a_number.into();
let new_u128: u128 = a_number.into();
// 由于 felt252 比 u256 小,我们可以使用 into() 方法
let new_u256: u256 = my_felt252.into();
let new_felt252: felt252 = new_u16.into();
//注意,usize 比 felt252 小,因此我们使用 try_into
let new_usize: usize = my_felt252.try_into().unwrap();
可升级合约
在Starknet中,合约分为两个部分:合约类和合约实例。 这种划分遵循了面向对象编程语言中的类和实例的概念。 这样,我们区分了对象的定义和实现。
合约类是合约的定义:它指定了合约的行为方式。 合约类包含了关键信息,如Cairo字节码、提示信息、入口点名称等, 以及一切明确定义合约类语义的内容。
为了识别不同的合约类,Starknet为每个类分配一个唯一的标识符:类哈希。 合约实例是对应于特定合约类的已部署合约。 可以将其视为在诸如Java等语言中对象的一个实例。
每个类由其类哈希值标识,类似于面向对象编程语言中的类名。合约实例是对应于某个类的已部署合约。
当调用replace_class_syscall
函数,你可以将已部署的合约升级到更新的版本。通过使用这个函数,你可以更新与已部署合约相关联的类哈希,从而有效地升级合约的实现。然而,这不会修改合约中的存储,因此合约中存储的所有数据将保持不变。
为了说明这个概念,让我们以两个合约为例:UpgradeableContract_V0
和UpgradeableContract_V1
。
首先,部署UpgradeableContract_V0
作为初始版本。接下来,发送一个调用upgrade
函数的交易,将部署合约的类哈希升级为UpgradeableContract_V1
的类哈希。然后,调用合约上的version
方法,查看合约是否已升级到V1版本。
use starknet::class_hash::ClassHash;
#[starknet::interface]
trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
mod UpgradeableContract_V0 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Upgraded: Upgraded
}
#[derive(Drop, starknet::Event)]
struct Upgraded {
implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
starknet::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
0
}
}
}
use starknet::class_hash::ClassHash;
#[starknet::interface]
trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
mod UpgradeableContract_V1 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Upgraded: Upgraded
}
#[derive(Drop, starknet::Event)]
struct Upgraded {
implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
starknet::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
1
}
}
}
简单的去中心化金融保险库
这是 Solidity by example Vault 的Cairo版本 以下是它的工作原理:
-
当用户存入代笔时,合约会计算要铸造的份额数量。
-
当用户取款时,合约会销毁他们的份额,计算收益,并提取存款的收益和初始代币金额。
use starknet::{ContractAddress};
// In order to make contract calls within our Vault,
// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher.
#[starknet::interface]
trait IERC20<TContractState> {
fn name(self: @TContractState) -> felt252;
fn symbol(self: @TContractState) -> felt252;
fn decimals(self: @TContractState) -> u8;
fn total_supply(self: @TContractState) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transfer_from(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
}
#[starknet::interface]
trait ISimpleVault<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, shares: u256);
}
#[starknet::contract]
mod SimpleVault {
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_contract_address};
#[storage]
struct Storage {
token: IERC20Dispatcher,
total_supply: u256,
balance_of: LegacyMap<ContractAddress, u256>
}
#[constructor]
fn constructor(ref self: ContractState, token: ContractAddress) {
self.token.write(IERC20Dispatcher { contract_address: token });
}
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
fn _mint(ref self: ContractState, to: ContractAddress, shares: u256) {
self.total_supply.write(self.total_supply.read() + shares);
self.balance_of.write(to, self.balance_of.read(to) + shares);
}
fn _burn(ref self: ContractState, from: ContractAddress, shares: u256) {
self.total_supply.write(self.total_supply.read() - shares);
self.balance_of.write(from, self.balance_of.read(from) - shares);
}
}
#[abi(embed_v0)]
impl SimpleVault of super::ISimpleVault<ContractState> {
fn deposit(ref self: ContractState, amount: u256) {
// a = amount
// B = balance of token before deposit
// T = total supply
// s = shares to mint
//
// (T + s) / T = (a + B) / B
//
// s = aT / B
let caller = get_caller_address();
let this = get_contract_address();
let mut shares = 0;
if self.total_supply.read() == 0 {
shares = amount;
} else {
let balance = self.token.read().balance_of(this);
shares = (amount * self.total_supply.read()) / balance;
}
PrivateFunctions::_mint(ref self, caller, shares);
self.token.read().transfer_from(caller, this, amount);
}
fn withdraw(ref self: ContractState, shares: u256) {
// a = amount
// B = balance of token before withdraw
// T = total supply
// s = shares to burn
//
// (T - s) / T = (B - a) / B
//
// a = sB / T
let caller = get_caller_address();
let this = get_contract_address();
let balance = self.token.read().balance_of(this);
let amount = (shares * balance) / self.total_supply.read();
PrivateFunctions::_burn(ref self, caller, shares);
self.token.read().transfer(caller, amount);
}
}
}
在 Remix 中尝试这个合约。
ERC20 代币
遵循 ERC20 Standard 的合约被称为 ERC20 代币。它们用于代表可互换的资产。
要创建 ERC20 合约,必须实现以下接口:
#[starknet::interface]
trait IERC20<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_decimals(self: @TContractState) -> u8;
fn get_total_supply(self: @TContractState) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
fn allowance(
self: @TContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
);
fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
fn decrease_allowance(
ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
);
}
在Starknet中,函数名应该使用snake_case(蛇形命名法)。而在Solidity中,函数名使用camelCase(驼峰命名法)。因此,Starknet的ERC20接口与Solidity的ERC20接口略有不同。
以下是一个在Cairo中实现的ERC20接口的示例:
#[starknet::contract]
mod erc20 {
use zeroable::Zeroable;
use starknet::get_caller_address;
use starknet::contract_address_const;
use starknet::ContractAddress;
#[storage]
struct Storage {
name: felt252,
symbol: felt252,
decimals: u8,
total_supply: felt252,
balances: LegacyMap::<ContractAddress, felt252>,
allowances: LegacyMap::<(ContractAddress, ContractAddress), felt252>,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Transfer: Transfer,
Approval: Approval,
}
#[derive(Drop, starknet::Event)]
struct Transfer {
from: ContractAddress,
to: ContractAddress,
value: felt252,
}
#[derive(Drop, starknet::Event)]
struct Approval {
owner: ContractAddress,
spender: ContractAddress,
value: felt252,
}
mod Errors {
const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0';
const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0';
const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0';
const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0';
const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0';
const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0';
}
#[constructor]
fn constructor(
ref self: ContractState,
recipient: ContractAddress,
name: felt252,
decimals: u8,
initial_supply: felt252,
symbol: felt252
) {
self.name.write(name);
self.symbol.write(symbol);
self.decimals.write(decimals);
self.mint(recipient, initial_supply);
}
#[abi(embed_v0)]
impl IERC20Impl of super::IERC20<ContractState> {
fn get_name(self: @ContractState) -> felt252 {
self.name.read()
}
fn get_symbol(self: @ContractState) -> felt252 {
self.symbol.read()
}
fn get_decimals(self: @ContractState) -> u8 {
self.decimals.read()
}
fn get_total_supply(self: @ContractState) -> felt252 {
self.total_supply.read()
}
fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 {
self.balances.read(account)
}
fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252 {
self.allowances.read((owner, spender))
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
let sender = get_caller_address();
self._transfer(sender, recipient, amount);
}
fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
) {
let caller = get_caller_address();
self.spend_allowance(sender, caller, amount);
self._transfer(sender, recipient, amount);
}
fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) {
let caller = get_caller_address();
self.approve_helper(caller, spender, amount);
}
fn increase_allowance(
ref self: ContractState, spender: ContractAddress, added_value: felt252
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) + added_value
);
}
fn decrease_allowance(
ref self: ContractState, spender: ContractAddress, subtracted_value: felt252
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
);
}
}
#[generate_trait]
impl InternalImpl of InternalTrait {
fn _transfer(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
) {
assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO);
assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO);
self.balances.write(sender, self.balances.read(sender) - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
self.emit(Transfer { from: sender, to: recipient, value: amount });
}
fn spend_allowance(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
let allowance = self.allowances.read((owner, spender));
self.allowances.write((owner, spender), allowance - amount);
}
fn approve_helper(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO);
self.allowances.write((owner, spender), amount);
self.emit(Approval { owner, spender, value: amount });
}
fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
assert(!recipient.is_zero(), Errors::MINT_TO_ZERO);
let supply = self.total_supply.read() + amount; // What can go wrong here?
self.total_supply.write(supply);
let balance = self.balances.read(recipient) + amount;
self.balances.write(recipient, amount);
self
.emit(
Event::Transfer(
Transfer {
from: contract_address_const::<0>(), to: recipient, value: amount
}
)
);
}
}
}
在 Remix 中尝试这个合约。
还有一些其他的实现,比如 Open Zeppelin 或者 Cairo By Example 中的实现。
恒定乘积自动做市商
这个是 用Cairo 改编的 Solidity by example Constant Product AMM.
use starknet::ContractAddress;
#[starknet::interface]
trait IConstantProductAmm<TContractState> {
fn swap(ref self: TContractState, token_in: ContractAddress, amount_in: u256) -> u256;
fn add_liquidity(ref self: TContractState, amount0: u256, amount1: u256) -> u256;
fn remove_liquidity(ref self: TContractState, shares: u256) -> (u256, u256);
}
#[starknet::contract]
mod ConstantProductAmm {
use core::traits::Into;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{
ContractAddress, get_caller_address, get_contract_address, contract_address_const
};
use integer::u256_sqrt;
#[storage]
struct Storage {
token0: IERC20Dispatcher,
token1: IERC20Dispatcher,
reserve0: u256,
reserve1: u256,
total_supply: u256,
balance_of: LegacyMap::<ContractAddress, u256>,
// Fee 0 - 1000 (0% - 100%, 1 decimal places)
// E.g. 3 = 0.3%
fee: u16,
}
#[constructor]
fn constructor(
ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u16
) {
// assert(fee <= 1000, 'fee > 1000');
self.token0.write(IERC20Dispatcher { contract_address: token0 });
self.token1.write(IERC20Dispatcher { contract_address: token1 });
self.fee.write(fee);
}
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
fn _mint(ref self: ContractState, to: ContractAddress, amount: u256) {
self.balance_of.write(to, self.balance_of.read(to) + amount);
self.total_supply.write(self.total_supply.read() + amount);
}
fn _burn(ref self: ContractState, from: ContractAddress, amount: u256) {
self.balance_of.write(from, self.balance_of.read(from) - amount);
self.total_supply.write(self.total_supply.read() - amount);
}
fn _update(ref self: ContractState, reserve0: u256, reserve1: u256) {
self.reserve0.write(reserve0);
self.reserve1.write(reserve1);
}
#[inline(always)]
fn select_token(self: @ContractState, token: ContractAddress) -> bool {
assert(
token == self.token0.read().contract_address
|| token == self.token1.read().contract_address,
'invalid token'
);
token == self.token0.read().contract_address
}
#[inline(always)]
fn min(x: u256, y: u256) -> u256 {
if (x <= y) {
x
} else {
y
}
}
}
#[abi(embed_v0)]
impl ConstantProductAmm of super::IConstantProductAmm<ContractState> {
fn swap(ref self: ContractState, token_in: ContractAddress, amount_in: u256) -> u256 {
assert(amount_in > 0, 'amount in = 0');
let is_token0: bool = self.select_token(token_in);
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
let (
token_in, token_out, reserve_in, reserve_out
): (IERC20Dispatcher, IERC20Dispatcher, u256, u256) =
if (is_token0) {
(token0, token1, reserve0, reserve1)
} else {
(token1, token0, reserve1, reserve0)
};
let caller = get_caller_address();
let this = get_contract_address();
token_in.transfer_from(caller, this, amount_in);
// How much dy for dx?
// xy = k
// (x + dx)(y - dy) = k
// y - dy = k / (x + dx)
// y - k / (x + dx) = dy
// y - xy / (x + dx) = dy
// (yx + ydx - xy) / (x + dx) = dy
// ydx / (x + dx) = dy
let amount_in_with_fee = (amount_in * (1000 - self.fee.read().into()) / 1000);
let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee);
token_out.transfer(caller, amount_out);
self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
amount_out
}
fn add_liquidity(ref self: ContractState, amount0: u256, amount1: u256) -> u256 {
let caller = get_caller_address();
let this = get_contract_address();
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
token0.transfer_from(caller, this, amount0);
token1.transfer_from(caller, this, amount1);
// How much dx, dy to add?
//
// xy = k
// (x + dx)(y + dy) = k'
//
// No price change, before and after adding liquidity
// x / y = (x + dx) / (y + dy)
//
// x(y + dy) = y(x + dx)
// x * dy = y * dx
//
// x / y = dx / dy
// dy = y / x * dx
let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
if (reserve0 > 0 || reserve1 > 0) {
assert(reserve0 * amount1 == reserve1 * amount0, 'x / y != dx / dy');
}
// How much shares to mint?
//
// f(x, y) = value of liquidity
// We will define f(x, y) = sqrt(xy)
//
// L0 = f(x, y)
// L1 = f(x + dx, y + dy)
// T = total shares
// s = shares to mint
//
// Total shares should increase proportional to increase in liquidity
// L1 / L0 = (T + s) / T
//
// L1 * T = L0 * (T + s)
//
// (L1 - L0) * T / L0 = s
// Claim
// (L1 - L0) / L0 = dx / x = dy / y
//
// Proof
// --- Equation 1 ---
// (L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy)
//
// dx / dy = x / y so replace dy = dx * y / x
//
// --- Equation 2 ---
// Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy)
//
// Multiply by sqrt(x) / sqrt(x)
// Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y)
// = (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(y)sqrt(x^2))
// sqrt(y) on top and bottom cancels out
//
// --- Equation 3 ---
// Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(x^2)
// = (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2)
// = ((x + dx) - x) / x
// = dx / x
// Since dx / dy = x / y,
// dx / x = dy / y
//
// Finally
// (L1 - L0) / L0 = dx / x = dy / y
let total_supply = self.total_supply.read();
let shares = if (total_supply == 0) {
u256_sqrt(amount0 * amount1).into()
} else {
PrivateFunctions::min(
amount0 * total_supply / reserve0, amount1 * total_supply / reserve1
)
};
assert(shares > 0, 'shares = 0');
self._mint(caller, shares);
self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
shares
}
fn remove_liquidity(ref self: ContractState, shares: u256) -> (u256, u256) {
let caller = get_caller_address();
let this = get_contract_address();
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
// Claim
// dx, dy = amount of liquidity to remove
// dx = s / T * x
// dy = s / T * y
//
// Proof
// Let's find dx, dy such that
// v / L = s / T
//
// where
// v = f(dx, dy) = sqrt(dxdy)
// L = total liquidity = sqrt(xy)
// s = shares
// T = total supply
//
// --- Equation 1 ---
// v = s / T * L
// sqrt(dxdy) = s / T * sqrt(xy)
//
// Amount of liquidity to remove must not change price so
// dx / dy = x / y
//
// replace dy = dx * y / x
// sqrt(dxdy) = sqrt(dx * dx * y / x) = dx * sqrt(y / x)
//
// Divide both sides of Equation 1 with sqrt(y / x)
// dx = s / T * sqrt(xy) / sqrt(y / x)
// = s / T * sqrt(x^2) = s / T * x
//
// Likewise
// dy = s / T * y
// bal0 >= reserve0
// bal1 >= reserve1
let (bal0, bal1): (u256, u256) = (token0.balance_of(this), token1.balance_of(this));
let total_supply = self.total_supply.read();
let (amount0, amount1): (u256, u256) = (
(shares * bal0) / total_supply, (shares * bal1) / total_supply
);
assert(amount0 > 0 && amount1 > 0, 'amount0 or amount1 = 0');
self._burn(caller, shares);
self._update(bal0 - amount0, bal1 - amount1);
token0.transfer(caller, amount0);
token1.transfer(caller, amount1);
(amount0, amount1)
}
}
}
在 Remix 中尝试这个合约。
写入任何存储槽
在Starknet上,一个合约的存储是一个拥有 2^251 个槽的map,每个槽是一个初始化为 0 的 felt。存储变量的地址在编译时通过公式计算得出:存储变量地址 := pedersen(keccak(变量名), keys)
。与存储变量的交互通常使用 self.var.read()
和 self.var.write()
。
然而,我们可以使用 storage_write_syscall
和 storage_read_syscall
系统调用,来对任何存储槽进行写入和读取。
这在写入那些在编译时还未确定的存储变量时非常有用,这也可以确保即使合约升级且存储变量地址的计算方法改变,这些变量仍然可访问。
在以下示例中,我们使用 Poseidon 哈希函数来计算存储变量的地址。Poseidon 是一个 ZK 友好的哈希函数,比 Pedersen 更便宜、更快,是链上计算的绝佳选择。一旦地址被计算出来,我们就使用存储的系统调用与之交互。
#[starknet::interface]
trait IWriteToAnySlots<TContractState> {
fn write_slot(ref self: TContractState, value: u32);
fn read_slot(self: @TContractState) -> u32;
}
#[starknet::contract]
mod WriteToAnySlot {
use starknet::syscalls::{storage_read_syscall, storage_write_syscall};
use starknet::SyscallResultTrait;
use poseidon::poseidon_hash_span;
use starknet::storage_access::Felt252TryIntoStorageAddress;
use starknet::StorageAddress;
#[storage]
struct Storage {}
const SLOT_NAME: felt252 = 'test_slot';
#[abi(embed_v0)]
impl WriteToAnySlot of super::IWriteToAnySlots<ContractState> {
fn write_slot(ref self: ContractState, value: u32) {
storage_write_syscall(0, get_address_from_name(SLOT_NAME), value.into());
}
fn read_slot(self: @ContractState) -> u32 {
storage_read_syscall(0, get_address_from_name(SLOT_NAME))
.unwrap_syscall()
.try_into()
.unwrap()
}
}
fn get_address_from_name(variable_name: felt252) -> StorageAddress {
let mut data: Array<felt252> = ArrayTrait::new();
data.append(variable_name);
let hashed_name: felt252 = poseidon_hash_span(data.span());
let MASK_250: u256 = 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
// By taking the 250 least significant bits of the hash output, we get a valid 250bits storage address.
let result: felt252 = (hashed_name.into() & MASK_250).try_into().unwrap();
let result: StorageAddress = result.try_into().unwrap();
result
}
}
访问 Voyager 上的合约,或者在 Remix 中测试它.
存储数组
在Starknet上,复杂值(例如元组或结构体)存储在以该存储变量地址开头的连续段中。复杂存储值的大小有 256 个元素的限制,这意味着要在存储中存储超过 255 个元素的数组,我们需要将其分割为大小 n <= 255
的段,并将这些段存储在多个存储地址中。目前 Cairo 没有原生支持存储数组,所以你需要为你希望存储的数组类型实现自己的 Store
特性。
注:虽然在存储中保存数组是可行的,但并不总是推荐这么做,因为读写操作的成本可能非常高。例如,读取一个大小为 n 的数组需要进行
n
次存储读取,而向一个大小为n
的数组写入需要进行n
次存储写入。如果你只需要一次访问数组中的一个元素,建议使用LegacyMap
并在另一个变量中存储数组长度。
以下示例展示了如何为 Array<felt252>
类型实现一个简单的 StorageAccess
特性,使我们能够存储多达 255 个 felt252
元素的数组。
impl StoreFelt252Array of Store<Array<felt252>> {
fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<Array<felt252>> {
StoreFelt252Array::read_at_offset(address_domain, base, 0)
}
fn write(
address_domain: u32, base: StorageBaseAddress, value: Array<felt252>
) -> SyscallResult<()> {
StoreFelt252Array::write_at_offset(address_domain, base, 0, value)
}
fn read_at_offset(
address_domain: u32, base: StorageBaseAddress, mut offset: u8
) -> SyscallResult<Array<felt252>> {
let mut arr: Array<felt252> = ArrayTrait::new();
// Read the stored array's length. If the length is superior to 255, the read will fail.
let len: u8 = Store::<u8>::read_at_offset(address_domain, base, offset)
.expect('Storage Span too large');
offset += 1;
// Sequentially read all stored elements and append them to the array.
let exit = len + offset;
loop {
if offset >= exit {
break;
}
let value = Store::<felt252>::read_at_offset(address_domain, base, offset).unwrap();
arr.append(value);
offset += Store::<felt252>::size();
};
// Return the array.
Result::Ok(arr)
}
fn write_at_offset(
address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array<felt252>
) -> SyscallResult<()> {
// // Store the length of the array in the first storage slot.
let len: u8 = value.len().try_into().expect('Storage - Span too large');
Store::<u8>::write_at_offset(address_domain, base, offset, len);
offset += 1;
// Store the array elements sequentially
loop {
match value.pop_front() {
Option::Some(element) => {
Store::<felt252>::write_at_offset(address_domain, base, offset, element);
offset += Store::<felt252>::size();
},
Option::None(_) => { break Result::Ok(()); }
};
}
}
fn size() -> u8 {
255 * Store::<felt252>::size()
}
}
您可以在合约中导入上面的实现方式,并使用它来在存储中存储数组:
#[starknet::interface]
trait IStoreArrayContract<TContractState> {
fn store_array(ref self: TContractState, arr: Array<felt252>);
fn read_array(self: @TContractState) -> Array<felt252>;
}
#[starknet::contract]
mod StoreArrayContract {
use super::StoreFelt252Array;
#[storage]
struct Storage {
arr: Array<felt252>
}
#[abi(embed_v0)]
impl StoreArrayImpl of super::IStoreArrayContract<ContractState> {
fn store_array(ref self: ContractState, arr: Array<felt252>) {
self.arr.write(arr);
}
fn read_array(self: @ContractState) -> Array<felt252> {
self.arr.read()
}
}
}
结构体作为映射键
为了使用结构体作为映射键,您可以在结构体定义上使用 #[derive(Hash)]
。这将为结构体自动生成一个哈希函数,可以在 LegacyMap
中将该结构体作为键来使用。
考虑以下示例,我们希望使用类型为 Pet
的对象作为 LegacyMap
中的键。Pet
结构体有三个字段:name
、age
和 owner
。假设这三个字段的组合能唯一地标识一只宠物。
#[derive(Copy, Drop, Serde, Hash)]
struct Pet {
name: felt252,
age: u8,
owner: felt252,
}
#[starknet::interface]
trait IPetRegistry<TContractState> {
fn register_pet(ref self: TContractState, key: Pet, timestamp: u64);
fn get_registration_date(self: @TContractState, key: Pet) -> u64;
}
#[starknet::contract]
mod PetRegistry {
use hash::{HashStateTrait, Hash};
use super::Pet;
#[storage]
struct Storage {
registration_time: LegacyMap::<Pet, u64>,
}
#[abi(embed_v0)]
impl PetRegistry of super::IPetRegistry<ContractState> {
fn register_pet(ref self: ContractState, key: Pet, timestamp: u64) {
self.registration_time.write(key, timestamp);
}
fn get_registration_date(self: @ContractState, key: Pet) -> u64 {
self.registration_time.read(key)
}
}
}
在 Remix 上测试这个合约.
兼容Hash Solidity
这个合约展示了在 Cairo 中进行 Keccak 哈希处理以匹配 Solidity 的 keccak256。尽管两者都使用 Keccak,但它们的字节序不同:Cairo 是小端序,Solidity 是大端序。该合约通过使用 keccak_u256s_be_inputs
以大端序进行哈希处理,并使用 u128_byte_reverse
反转结果的字节来实现兼容。
例如:
#[starknet::interface]
trait ISolidityHashExample<TContractState> {
fn hash_data(ref self: TContractState, input_data: Span<u256>) -> u256;
}
#[starknet::contract]
mod SolidityHashExample {
use keccak::{keccak_u256s_be_inputs};
use array::Span;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl SolidityHashExample of super::ISolidityHashExample<ContractState> {
fn hash_data(ref self: ContractState, input_data: Span<u256>) -> u256 {
let hashed = keccak_u256s_be_inputs(input_data);
// Split the hashed value into two 128-bit segments
let low: u128 = hashed.low;
let high: u128 = hashed.high;
// Reverse each 128-bit segment
let reversed_low = integer::u128_byte_reverse(low);
let reversed_high = integer::u128_byte_reverse(high);
// Reverse merge the reversed segments back into a u256 value
let compatible_hash = u256 { low: reversed_high, high: reversed_low };
compatible_hash
}
}
}
在 Remix 上测试这个合约.
优化
这里列出了一系列优化模式,用以节省Gas和计算步骤。
存储优化
智能合约只有有限的存储槽位。每个槽位可以存储一个 felt252
值。
写入一个存储槽位会产生成本,因此我们希望尽可能少地使用存储槽位。
在 Cairo 中,每种类型都源自 felt252
类型,它使用 252 位来存储一个值。
这种设计相当简单,但它有一个缺点:它在存储效率方面并不高。例如,如果要存储一个 u8
值,我们需要使用整个槽位,尽管我们只需要 8 位。
打包
当存储多个值时,我们可以使用一种称为**打包(packing)**的技术。打包是一种允许我们在单个 felt 值中存储多个值的技术。这是通过使用 felt 值的位来存储多个值来实现的。
例如,如果我们想存储两个 u8
值,我们可以使用 felt 值的前 8 位来存储第一个 u8
值,而使用后 8 位来存储第二个 u8
值。这样,我们就可以在单个 felt 值中存储两个 u8
值。
Cairo 提供了一个内置的打包存储功能,您可以通过 StorePacking
特性来使用它。
trait StorePacking<T, PackedT> {
fn pack(value: T) -> PackedT;
fn unpack(value: PackedT) -> T;
}
这允许通过首先使用 pack
函数将类型 T
打包成 PackedT
类型,然后使用其 Store
实现来存储 PackedT
值。在读取值时,我们首先获取 PackedT
值,然后使用 unpack
函数将其解包为类型 T
。
以下是一个使用 StorePacking
特性存储包含两个 u8
值的 Time
结构体的示例:
#[starknet::interface]
trait ITime<TContractState> {
fn set(ref self: TContractState, value: TimeContract::Time);
fn get(self: @TContractState) -> TimeContract::Time;
}
#[starknet::contract]
mod TimeContract {
use starknet::storage_access::StorePacking;
use integer::{
U8IntoFelt252, Felt252TryIntoU16, U16DivRem, u16_as_non_zero, U16IntoFelt252,
Felt252TryIntoU8
};
use traits::{Into, TryInto, DivRem};
use option::OptionTrait;
use serde::Serde;
#[storage]
struct Storage {
time: Time
}
#[derive(Copy, Serde, Drop)]
struct Time {
hour: u8,
minute: u8
}
impl TimePackable of StorePacking<Time, felt252> {
fn pack(value: Time) -> felt252 {
let msb: felt252 = 256 * value.hour.into();
let lsb: felt252 = value.minute.into();
return msb + lsb;
}
fn unpack(value: felt252) -> Time {
let value: u16 = value.try_into().unwrap();
let (q, r) = U16DivRem::div_rem(value, u16_as_non_zero(256));
let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap();
let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap();
return Time { hour, minute };
}
}
#[abi(embed_v0)]
impl TimeContract of super::ITime<ContractState> {
fn set(ref self: ContractState, value: Time) {
// This will call the pack method of the TimePackable trait
// and store the resulting felt252
self.time.write(value);
}
fn get(self: @ContractState) -> Time {
// This will read the felt252 value from storage
// and return the result of the unpack method of the TimePackable trait
return self.time.read();
}
}
}
在 Remix 上测试这个合约.
列表
默认情况下,Cairo 不支持列表类型,但您可以使用 Alexandria。您可以参考 Alexandria 文档 获取更多详细信息。
List
是什么?
可以在 Starknet 存储中使用的有序值序列:
#[storage]
stuct Storage {
amounts: List<u128>
}
接口
trait ListTrait<T> {
fn len(self: @List<T>) -> u32;
fn is_empty(self: @List<T>) -> bool;
fn append(ref self: List<T>, value: T) -> u32;
fn get(self: @List<T>, index: u32) -> Option<T>;
fn set(ref self: List<T>, index: u32, value: T);
fn pop_front(ref self: List<T>) -> Option<T>;
fn array(self: @List<T>) -> Array<T>;
}
List
还实现了 IndexView
,因此您可以使用熟悉的方括号表示法来访问其成员:
let second = self.amounts.read()[1];
请注意,与 get
不同的是,使用这种方括号表示法在访问越界索引时会引发 panic(崩溃)。
支持自定义类型
List
默认支持大多数 corelib 类型。如果您想在 List
中存储自己的自定义类型,该类型必须实现 Store
特性。您可以使用 #[derive(starknet::Store)]
属性让编译器自动生成。
注意事项
在使用 List
时,有两个特点应该注意:
append
操作消耗 2 次存储写入操作 - 一次是为了值本身,另一次是为了更新列表的长度。- 由于编译器的限制,不能使用单个内联语句进行变更操作。例如,
self.amounts.read().append(42);
是不行的。你必须分两步进行:
let mut amounts = self.amounts.read();
amounts.append(42);
依赖关系
在 Scarb.toml
里更新您项目的依赖:
[dependencies]
(...)
alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" }
例如,我们用 List
来创建一个跟踪amount
和tasks
的合约:
#[starknet::interface]
trait IListExample<TContractState> {
fn add_in_amount(ref self: TContractState, number: u128);
fn add_in_task(ref self: TContractState, description: felt252, status: felt252);
fn is_empty_list(self: @TContractState) -> bool;
fn list_length(self: @TContractState) -> u32;
fn get_from_index(self: @TContractState, index: u32) -> u128;
fn set_from_index(ref self: TContractState, index: u32, number: u128);
fn pop_front_list(ref self: TContractState);
fn array_conversion(self: @TContractState) -> Array<u128>;
}
#[starknet::contract]
mod ListExample {
use alexandria_storage::list::{List, ListTrait};
#[storage]
struct Storage {
amount: List<u128>,
tasks: List<Task>
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Task {
description: felt252,
status: felt252
}
#[abi(embed_v0)]
impl ListExample of super::IListExample<ContractState> {
fn add_in_amount(ref self: ContractState, number: u128) {
let mut current_amount_list = self.amount.read();
current_amount_list.append(number);
}
fn add_in_task(ref self: ContractState, description: felt252, status: felt252) {
let new_task = Task { description: description, status: status };
let mut current_tasks_list = self.tasks.read();
current_tasks_list.append(new_task);
}
fn is_empty_list(self: @ContractState) -> bool {
let mut current_amount_list = self.amount.read();
current_amount_list.is_empty()
}
fn list_length(self: @ContractState) -> u32 {
let mut current_amount_list = self.amount.read();
current_amount_list.len()
}
fn get_from_index(self: @ContractState, index: u32) -> u128 {
self.amount.read()[index]
}
fn set_from_index(ref self: ContractState, index: u32, number: u128) {
let mut current_amount_list = self.amount.read();
current_amount_list.set(index, number);
}
fn pop_front_list(ref self: ContractState) {
let mut current_amount_list = self.amount.read();
current_amount_list.pop_front();
}
fn array_conversion(self: @ContractState) -> Array<u128> {
let mut current_amount_list = self.amount.read();
current_amount_list.array()
}
}
}