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 documentationStarknet Book

以下是您可能会用到的其他资源清单:

Last change: 2023-12-06, commit: 1af1816

Cairo的智能合约基础知识

以下章节将向你介绍Starknet智能合约以及如何用Cairo编写这些合约。

Last change: 2023-10-12, commit: 90aa7c0

存储

这是您用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 程序是一样的。 从编译器的角度来看,存储变量在使用之前是不存在的。

您还可以阅读有关 存储自定义类型 的内容。

Last change: 2023-11-20, commit: 3890c7b

构造函数

构造函数是一种特殊类型的函数,只在部署合约时运行一次,可用于初始化合约的状态。你的合约不能有一个以上的构造函数,而且构造函数必须使用 #[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 中尝试它。

Last change: 2023-10-12, commit: 90aa7c0

变量

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 中尝试它。

Last change: 2023-11-20, commit: 3890c7b

可见性和可变性

可见性

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 中尝试它。

Last change: 2023-10-19, commit: 3e4c697

简单计数器

这是一个简单的计数合约。

这个合约是这样工作的:

  • 合约有一个名为 '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 中尝试它。

Last change: 2023-11-04, commit: d1889be

映射

映射是一种键值数据结构,用于在智能合约中存储数据。在开罗,它们使用 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)
        }
    }
}

访问 Voyager 上的合约或在 Remix 中尝试它。

Last change: 2023-10-12, commit: 90aa7c0

错误

错误可用于处理智能合约执行过程中可能发生的验证和其他条件。 如果在执行智能合约调用期间抛出错误,则将停止执行,并恢复在交易期间所做的任何更改。

要抛出错误,请使用 assertpanic函数:

  • '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');
            }
        }
    }
}

Voyager 上访问合约或在 Remix 中尝试它。

自定义错误

您可以通过在特定模块中定义错误代码来简化错误处理。

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 中尝试它。

Last change: 2023-10-12, commit: 90aa7c0

事件

事件是从合约发出数据的一种方式。所有事件都必须在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 中尝试它。

Last change: 2023-10-12, commit: 90aa7c0

存储自定义类型

虽然本机类型可以存储在合约的存储中,而无需任何额外的工作,但自定义类型需要更多的工作。这是因为在编译时,编译器不知道如何在存储中存储自定义类型。为了解决这个问题,我们需要为我们的自定义类型实现 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 中尝试这个合约。

Last change: 2023-10-12, commit: 90aa7c0

入口点中的自定义类型

在入口点中使用自定义类型需要我们的类型来实现Serdetrait。这是因为在调用入口点时,输入以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 中尝试这个合约。

Last change: 2023-10-12, commit: 90aa7c0

文档

花时间为你的代码写文档非常重要。它将帮助开发人员和用户了解合约及其功能。

在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;
}

请记住,这不应该描述函数的实现细节,而应该从用户的角度描述合约的高级目的和功能。

实装细节:

在编写合约逻辑时,可以添加注释来描述函数的技术实现细节。

避免过度注释:注释应提供额外的价值和清晰度。

Last change: 2023-12-05, commit: d8bdbed

部署合约并与合约交互

在本章中,我们将了解如何部署合约并与之交互。

Last change: 2023-10-19, commit: dcadbd1

合约接口和Trait生成

合约接口定义合约的结构和行为,充当合约的公共 ABI。它们列出了合约公开的所有函数签名。接口的详细说明可以参考 [Cairo之书](https://book.cairo-lang.org/ch99-01-02-a-simple-contract.html)。

在cairo中,要指定接口,您需要定义一个带有#[starknet::interface]注释的特征,然后在合约中实现该特征。

当函数需要访问协定状态时,它必须具有类型为ContractStateself参数。这意味着接口特征中的相应函数签名也必须采用TContractState类型作为参数。需要注意的是,合约接口中的每个函数都必须具有此类型为TContractStateself参数。

您可以使用#[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 中尝试这个合约。

Last change: 2023-11-26, commit: 35ec815

调用其他合约

在Cairo,有两种不同的方式可以调用其他合约。

调用其他合约的最简单方法是使用要调用的合约的调度程序。 您可以在 Cairo Book 中阅读有关 Dispatchers 的更多信息

另一种方法是自己使用starknet::call_contract_syscall系统调用。但是,不建议使用此方法。

为了使用调度程序调用其他合约,您需要将被调用合约的接口定义为使用 #[starknet::interface] 属性注释的trait,然后将 IContractDispatcherIContractDispatcherTrait 项导入到合约中。

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

Voyager 上访问合约或在 Remix 中尝试它。

Last change: 2023-11-30, commit: fec6527

工厂模式

工厂模式是面向对象编程中众所周知的模式。它提供了有关如何实例化类的抽象。

在智能合约里,我们可以通过定义一个工厂合约来使用这种模式,该合约全权负责创建和管理其他合约。

类哈希(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);
        }
    }
}

此工厂可用于通过调用SimpleCountercreate_counter函数来部署create_counter_at合约的多个实例。

SimpleCounter类哈希存储在工厂内部,可以使用update_counter_class_hash 函数进行升级,该函数允许在升级SimpleCounter 合约时重用相同的工厂合约。

这个最小的范例缺少几个有用的功能,例如访问控制、跟踪已部署的合约、事件......

Last change: 2023-10-19, commit: dcadbd1

合约测试

测试在软件开发中起着至关重要的作用,尤其是对于智能合约而言。在本节中,我们将通过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 - 合约测试

Last change: 2023-12-06, commit: 1af1816

Cairo 备忘单

本章旨在为最常见的Cairo结构提供快速参考。

Last change: 2023-10-31, commit: d530b71

Felt252

Felt252是Cairo中的基本数据类型,所有其他数据类型都派生自它。 Felt252也可以用于存储最多31个字符长度的短字符串表示。

例如:

    let felt: felt252 = 100;
    let felt_as_str = ‘Hello Starknet!’;

    let felt = felt + felt_as_str;
Last change: 2023-11-30, commit: 932307f

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))
        }
    }
}
Last change: 2023-10-31, commit: 0d79abb

数组

数组是相同类型元素的集合。 可以使用 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()
}
Last change: 2023-10-31, commit: 0d79abb

循环

循环指定一个代码块,该代码块将重复运行,直到遇到停止条件。 例如:

    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;
    };
Last change: 2023-11-30, commit: 932307f

分支

在 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
}
Last change: 2023-10-31, commit: 0d79abb

元组

元组是一种数据类型,用于将固定数量的不同类型的项组合成一个单一的复合结构。与数组不同,元组具有固定的长度,并且可以包含不同类型的元素。一旦创建了元组,其大小就无法更改。 例如:

    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;
Last change: 2023-11-30, commit: 932307f

结构体

结构体是一种类似于元组的数据类型。与元组类似,它们可以用于保存不同类型的数据。 例如:

// 使用 Store,您可以将 Data 结构体存储在合约的存储部分。
#[derive(Drop, starknet::Store)]
struct Data {
    address: starknet::ContractAddress,
    age: u8
}
Last change: 2023-10-31, commit: 0d79abb

类型转换

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();
Last change: 2023-11-30, commit: 932307f

可升级合约

在Starknet中,合约分为两个部分:合约类和合约实例。 这种划分遵循了面向对象编程语言中的类和实例的概念。 这样,我们区分了对象的定义和实现。

合约类是合约的定义:它指定了合约的行为方式。 合约类包含了关键信息,如Cairo字节码、提示信息、入口点名称等, 以及一切明确定义合约类语义的内容。

为了识别不同的合约类,Starknet为每个类分配一个唯一的标识符:类哈希。 合约实例是对应于特定合约类的已部署合约。 可以将其视为在诸如Java等语言中对象的一个实例。

每个类由其类哈希值标识,类似于面向对象编程语言中的类名。合约实例是对应于某个类的已部署合约。

当调用replace_class_syscall函数,你可以将已部署的合约升级到更新的版本。通过使用这个函数,你可以更新与已部署合约相关联的类哈希,从而有效地升级合约的实现。然而,这不会修改合约中的存储,因此合约中存储的所有数据将保持不变。

为了说明这个概念,让我们以两个合约为例:UpgradeableContract_V0UpgradeableContract_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
        }
    }
}

Voyager 上访问合约或在 Remix中尝试它 。

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
        }
    }
}

Voyager 上访问合约或在 Remix 中尝试它。

Last change: 2023-10-12, commit: 90aa7c0

简单的去中心化金融保险库

这是 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 中尝试这个合约。

Last change: 2023-10-12, commit: 90aa7c0

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 中的实现。

Last change: 2023-10-24, commit: c372633

恒定乘积自动做市商

这个是 用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 中尝试这个合约。

Last change: 2023-11-20, commit: 3890c7b

写入任何存储槽

在Starknet上,一个合约的存储是一个拥有 2^251 个槽的map,每个槽是一个初始化为 0 的 felt。存储变量的地址在编译时通过公式计算得出:存储变量地址 := pedersen(keccak(变量名), keys)。与存储变量的交互通常使用 self.var.read()self.var.write()

然而,我们可以使用 storage_write_syscallstorage_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 中测试它.

Last change: 2023-10-12, commit: 90aa7c0

存储数组

在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()
        }
    }
}

访问 Voyager 上的合约,或者在 Remix上测试.

Last change: 2023-10-12, commit: 90aa7c0

结构体作为映射键

为了使用结构体作为映射键,您可以在结构体定义上使用 #[derive(Hash)]。这将为结构体自动生成一个哈希函数,可以在 LegacyMap 中将该结构体作为键来使用。

考虑以下示例,我们希望使用类型为 Pet 的对象作为 LegacyMap 中的键。Pet 结构体有三个字段:nameageowner。假设这三个字段的组合能唯一地标识一只宠物。

#[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 上测试这个合约.

Last change: 2023-10-12, commit: 90aa7c0

兼容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 上测试这个合约.

Last change: 2023-11-21, commit: 58cde22

优化

这里列出了一系列优化模式,用以节省Gas和计算步骤。

Last change: 2023-10-12, commit: 90aa7c0

存储优化

智能合约只有有限的存储槽位。每个槽位可以存储一个 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 上测试这个合约.

Last change: 2023-10-12, commit: 90aa7c0

列表

默认情况下,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 时,有两个特点应该注意:

  1. append 操作消耗 2 次存储写入操作 - 一次是为了值本身,另一次是为了更新列表的长度。
  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 来创建一个跟踪amounttasks的合约:

#[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()
        }
    }
}
Last change: 2023-11-27, commit: e112ed3