starknet合约学习(内容来自对cairo book的学习,不太全面也不是最新的)

#[starknet::interface]
trait IHelloStarknet<TContractState> {
    fn increase_balance(ref self: TContractState, amount: felt252);
    fn get_balance(self: @TContractState) -> felt252;
}

interface

self是为了read和write

public function包含external和view function,其中view function可以被call,但是不能修改合约状态

deeper dive into

Storage Variables

#[storage]
    struct Storage {
        id: u8,
        names: LegacyMap::<ContractAddress, felt252>,
    }

Storage variables in Starknet contracts are stored in a special struct called Storage

和别的struct一样,除了标记 #[storage]才能使用LegacyMap类型

Storing data types

StorageBaseAddress 的值是sn_keccak(variable_name)计算出,variable_name 是变量名字的ASCII编码。keccak256 哈希输出256bits,比felt252多4bits,所以sn_keccak只取前250bits

Storing structs

core library中的类型都是实现了Store这个trait,所以无需手动定义存储,但是如果自定义struct,就需要自己定义存储(明确告诉编译器如何存储这个struct)。

#[derive(Copy, Drop, Serde, starknet::Store)]
    struct Person {
        name: felt252,
        address: ContractAddress
    }

#[derive(starknet::Store)] 加这个属性

storing mapping

#[storage]
    struct Storage {
        allowances: LegacyMap::<(ContractAddress, ContractAddress), u256>
    }

map中,key地址的值是

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果key是struct,那么struct需要实现Hash trait,需要标记#[derive(Hash)] 属性

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

reading from Storage

let name = self.names.read(address);

Writing to Storage

self.names.write(user, name);

合约函数

Constructors

用于初始化合约

  1. 每个合约仅有一个
  2. 名字就叫constructor
  3. 必须标记#[constructor] 属性
#[constructor]
    fn constructor(ref self: ContractState, owner: Person) {
        self.names.write(owner.address, owner.name);
        self.total_names.write(1);
        self.owner.write(owner);
    }

public functons

定义在implementation中,标记#[external(v0)] 属性

External functions

fn store_name(ref self: ContractState, name: felt252) {
            let caller = get_caller_address();
            self._store_name(caller, name);
        }

ContractState 通过ref传递,用于修改合约状态

View functions

fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
            let name = self.names.read(address);
            name
        }

只读函数,ContractState是通过快照传递,阻止修改合约状态

Private functions

没有添加#[external(v0)] 属性的就是私有的,块内块外一样,其中块内代码函数就是为了操作合约属性

Event

用于与外部世界交流,它们通过记录合约中特定事件的信息,为智能合约提供了一种与外部世界通信的方式

Defining events

实现starknet::Event trait 这个trait来自core library

trait Event<T> {
    fn append_keys_and_data(self: T, ref keys: Array<felt252>, ref data: Array<felt252>);
    fn deserialize(ref keys: Span<felt252>, ref data: Span<felt252>) -> Option<T>;
}

#[derive(starknet::Event)] 这个属性可以生成上面trait的实现

生成的implementation如下所示:

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

    #[derive(Drop, starknet::Event)]
    struct StoredName {
        #[key]
        user: ContractAddress,
        name: felt252
    }

Emitting events

using self.emit

self.emit(StoredName {user:user,name:name});

Reducing boilerplate

#[generate_trait]
    impl InternalFunctions of InternalFunctionsTrait {
        fn _store_name(ref self: ContractState, user: ContractAddress, name: felt252) {
            let mut total_names = self.total_names.read();
            self.names.write(user, name);
            self.total_names.write(total_names + 1);
            self.emit(StoredName { user: user, name: name });

        }
    }

这个就是为了使用通用参数ContractState, 通常我们都是先定义一个trait,包含一个ContractState,然后实现这个trait。通过使用#[generate_trait] 这个过程就能被跳过。如上所示。手动则如下所示:

trait InternalFunctionsTrait<TContractState> {
        fn _store_name(ref self: TContractState, user: ContractAddress, name: felt252);
    }
    impl InternalFunctions of InternalFunctionsTrait<ContractState> {
        fn _store_name(ref self: ContractState, user: ContractAddress, name: felt252) {
            let mut total_names = self.total_names.read();
            self.names.write(user, name);
            self.total_names.write(total_names + 1);
            self.emit(Event::StoredName(StoredName { user: user, name: name }));

        }
    }
}

Storage Optimization with StorePacking 【很关键】

教你优化存储,少花gas的

一个storage slot 251bits,加入一个struct有三个字段,8+32+64=104bit,那么就可以放到一个u128类型变量中,只需要一个slot,而不是用三个。用越少的slot就花越少的gas

use starknet::{StorePacking};
use integer::{u128_safe_divmod, u128_as_non_zero};

#[derive(Drop, Serde)]
struct Sizes {
    tiny: u8,
    small: u32,
    medium: u64,
}

const TWO_POW_8: u128 = 0x100;
const TWO_POW_40: u128 = 0x10000000000;

const MASK_8: u128 = 0xff;
const MASK_32: u128 = 0xffffffff;

impl SizesStorePacking of StorePacking<Sizes, u128> {
##pack是打包,into()直接转化为u128,低8位是tiny,small是8-39位
    fn pack(value: Sizes) -> u128 {
        value.tiny.into() + (value.small.into() * TWO_POW_8) + (value.medium.into() * TWO_POW_40)
    }

    fn unpack(value: u128) -> Sizes {
        let tiny = value & MASK_8;
        let small = (value / TWO_POW_8) & MASK_32;
        let medium = (value / TWO_POW_40);

        Sizes {
            tiny: tiny.try_into().unwrap(),
            small: small.try_into().unwrap(),
            medium: medium.try_into().unwrap(),
        }
    }
}

#[starknet::contract]
mod SizeFactory {
    use super::Sizes;
    use super::SizesStorePacking; //don't forget to import it!

    #[storage]
    struct Storage {
        remaining_sizes: Sizes
    }

    #[external(v0)]
    fn update_sizes(ref self: ContractState, sizes: Sizes) {
        // This will automatically pack the
        // struct into a single u128
        self.remaining_sizes.write(sizes);
    }

    #[external(v0)]
    fn get_sizes(ref self: ContractState) -> Sizes {
        // this will automatically unpack the
        // packed-representation into the Sizes struct
        self.remaining_sizes.read()
    }
} 

Components

#[starknet::component] 装饰

#[embeddable_as(name)] internal functions

这个module中可以声明Storage和Event

几乎和合约没啥区别,重点关注 修饰属性

#[starknet::interface]
trait IOwnable<TContractState> {
    fn owner(self: @TContractState) -> ContractAddress;
    fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress);
    fn renounce_ownership(ref self: TContractState);
}
#[starknet::component]
mod ownable_component {
    use starknet::ContractAddress;
    use starknet::get_caller_address;
    use super::Errors;

    #[storage]
    struct Storage {
        owner: ContractAddress
    }

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

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

    #[embeddable_as(Ownable)]
    impl OwnableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::IOwnable<ComponentState<TContractState>> {
        fn owner(self: @ComponentState<TContractState>) -> ContractAddress {
            self.owner.read()
        }

        fn transfer_ownership(
            ref self: ComponentState<TContractState>, new_owner: ContractAddress
        ) {
            assert(!new_owner.is_zero(), Errors::ZERO_ADDRESS_OWNER);
            self.assert_only_owner();
            self._transfer_ownership(new_owner);
        }

        fn renounce_ownership(ref self: ComponentState<TContractState>) {
            self.assert_only_owner();
            self._transfer_ownership(Zeroable::zero());
        }
    }

    #[generate_trait]
    impl InternalImpl<
        TContractState, +HasComponent<TContractState>
    > of InternalTrait<TContractState> {
        fn initializer(ref self: ComponentState<TContractState>, owner: ContractAddress) {
            self._transfer_ownership(owner);
        }

        fn assert_only_owner(self: @ComponentState<TContractState>) {
            let owner: ContractAddress = self.owner.read();
            let caller: ContractAddress = get_caller_address();
            assert(!caller.is_zero(), Errors::ZERO_ADDRESS_CALLER);
            assert(caller == owner, Errors::NOT_OWNER);
        }

        fn _transfer_ownership(
            ref self: ComponentState<TContractState>, new_owner: ContractAddress
        ) {
            let previous_owner: ContractAddress = self.owner.read();
            self.owner.write(new_owner);
            self
                .emit(
                    OwnershipTransferred { previous_owner: previous_owner, new_owner: new_owner }
                );
        }
    }
}

有两个impl块,一个实现了trait,是给外部用的,还有一个是给内部用的

#[embeddable_as] 这个属性后面跟的名字,是给合约里面用这个组件时候用的,上面组件嵌入到合约中是用 Ownable

一个主要不同就是storage和event的访问时使用ComponentState<TContractState> 类型,而不是ContractState

合约转化为Component

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

合约中使用Component

  1. 声明 component!()

    • 路径path::to::component
    • 合约存储指向组件存储的名字,,上文的 Ownable
    • 合约event变量名指向组件event名,例如 OwnableEvent
  2. 添加组件storage和event路径到合约,必须匹配步骤1中的名字 (e.g. ownable: ownable_component::Storage and OwnableEvent: ownable_component::Event)

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 嵌入具体逻辑,通过具体的 ContractState 别名实例化组件impl,别名要添加#[abi(embed_v0)] 属性以暴露外部函数。内部impl则不需要添加这个属性。

example:

#[starknet::contract]
mod OwnableCounter {
    use listing_01_ownable::component::ownable_component;

    component!(path: ownable_component, storage: ownable, event: OwnableEvent);

    #[abi(embed_v0)]
    impl OwnableImpl = ownable_component::Ownable<ContractState>;

    impl OwnableInternalImpl = ownable_component::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        counter: u128,
        #[substorage(v0)]
        ownable: ownable_component::Storage
    }

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

    #[external(v0)]
    fn foo(ref self: ContractState) {
        self.ownable.assert_only_owner();
        self.counter.write(self.counter.read() + 1);
    }
}

通过 IOwnableDispatcher 交互组件的尾部函数

疑难解答

  • Trait not found. Not a Trait

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Plugin diagnostic: name is not a substorage member in the contract’s Storage. Consider adding to Storage: (…)

    Make sure to add the path to the component’s storage annotated with the #[substorage(v0)] attribute to your contract’s storage.

  • Plugin diagnostic: name is not a nested event in the contract’s Event enum. Consider adding to the Event enum:

    Make sure to add the path to the component’s events to your contract’s events.

  • Components functions are not accessible externally

    This can happen if you forgot to annotate the component’s impl block with #[abi(embed_v0)]. Make sure to add this annotation when embedding the component’s impl in your contract.

Component可组合性机制

通用实现

从上文的这个组件实现讲:

#[embeddable_as(Ownable)]
    impl OwnableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::IOwnable<ComponentState<TContractState>> {

关键点

  • OwnableImpl 需要基础合约HasComponent<TContractState> trait的实现,通过component!() 这个宏自动实现。

    编译器会自动生成函数,参数中使用self: TContractState 代替 self: ComponentState<TContractState>HasComponent<TContractState> 这个trait中有一个get_component函数用来访问 component的state

    对于每个component,编译器会生成HasComponent trait。这个trait定义了桥接TContractStateComponentState<TContractState> 的接口。

    // generated per component
    trait HasComponent<TContractState> {
        fn get_component(self: @TContractState) -> @ComponentState<TContractState>;
        fn get_component_mut(ref self: TContractState) -> ComponentState<TContractState>;
        fn get_contract(self: @ComponentState<TContractState>) -> @TContractState;
        fn get_contract_mut(ref self: ComponentState<TContractState>) -> TContractState;
        fn emit<S, impl IntoImp: traits::Into<S, Event>>(ref self: ComponentState<TContractState>, event: S);
    }
    
  • Ownable 被标记embeddable_as(<name>) 属性,这个属性仅仅适用于starknet::interface 标记trait的实现。通过这个属性才能将impl嵌入到合约。也就是说嵌入 ownableImpl 到合约中,就必须有接口trait的实现

    ComponentState<TContractState>ContractState 转化过程:

    #[starknet::embeddable]
    impl Ownable<
              TContractState, +HasComponent<TContractState>
    , impl TContractStateDrop: Drop<TContractState>
    > of super::IOwnable<TContractState> {
    
      fn owner(self: @TContractState) -> ContractAddress {
          let component = HasComponent::get_component(self);
          OwnableImpl::owner(component, )
      }
    
      fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress
    ) {
          let mut component = HasComponent::get_component_mut(ref self);
          OwnableImpl::transfer_ownership(ref component, new_owner, )
      }
    
      fn renounce_ownership(ref self: TContractState) {
          let mut component = HasComponent::get_component_mut(ref self);
          OwnableImpl::renounce_ownership(ref component, )
      }
    } 
    

    编译器通过HasComponent<TContractState>的实现,包装函数的一个新的impl,不需要知道ComponentState 类型。

    合约整合

    合约使用 impl alias 来实例化组件的通用实现,with ContractState

#[abi(embed_v0)]
    impl OwnableImpl = ownable_component::Ownable<ContractState>;

    impl OwnableInternalImpl = ownable_component::InternalImpl<ContractState>;

重点

  • 可嵌入的impls允许注入组件逻辑到合约:添加入口点、修改abi
  • 编译器自动生成 HasComponent ,创建合约和组件state的桥梁

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ABI和跨合约交互

ABI-Application Binary Interface

starknet的ABI是用Json表示合约的function和structures,提供给任何人编码调用。

函数,函数参数等,就是一个调用形式。format the data

interface

公共函数list。指定函数签名signature(名字、参数、可见性、返回值)

ERC token例子

 use starknet::ContractAddress;

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

通过dispatchers和syscalls和其它合约交互

定义一个接口,编译器会自动生成两个dispatcher, IERC20DispatcherIERC20LibraryDispatcher

同时编译器产生一个trait IERC20DispatcherTrait ,用于调用dispatcher struct的接口函数。

Contract Dispatcher

生成的dispatcher如下(很长,下面是重点 nametransfer

use starknet::{ContractAddress};

trait IERC20DispatcherTrait<T> {
    fn name(self: T) -> felt252;
    fn transfer(self: T, recipient: ContractAddress, amount: u256);
}

#[derive(Copy, Drop, starknet::Store, Serde)]
struct IERC20Dispatcher {
    contract_address: ContractAddress,
}

impl IERC20DispatcherImpl of IERC20DispatcherTrait<IERC20Dispatcher> {
    fn name(
        self: IERC20Dispatcher
    ) -> felt252 { // starknet::call_contract_syscall is called in here
    }
    fn transfer(
        self: IERC20Dispatcher, recipient: ContractAddress, amount: u256
    ) { // starknet::call_contract_syscall is called in here
    }
}

Calling Contracts using the Contract Dispatcher

合约 TokenWrapper 使用dispatcher调用 ERC-20 token的函数通过被部署的 contract_address

//**** Specify interface here ****//
#[starknet::contract]
mod TokenWrapper {
    use super::IERC20DispatcherTrait;
    use super::IERC20Dispatcher;
    use super::ITokenWrapper;
    use starknet::ContractAddress;

    #[storage]
    struct Storage {}

    impl TokenWrapper of ITokenWrapper<ContractState> {
        fn token_name(self: @ContractState, contract_address: ContractAddress) -> felt252 {
            IERC20Dispatcher { contract_address }.name()
        }

        fn transfer_token(
            ref self: ContractState,
            contract_address: ContractAddress,
            recipient: ContractAddress,
            amount: u256
        ) -> bool {
            IERC20Dispatcher { contract_address }.transfer(recipient, amount)
        }
    }
}

Library Dispatcher

不同:通常dispatcher是被用来call合约中function,但是library dispatcher用来call clas

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

use starknet::ContractAddress;

trait IERC20DispatcherTrait<T> {
    fn name(self: T) -> felt252;
    fn transfer(self: T, recipient: ContractAddress, amount: u256);
}

#[derive(Copy, Drop, starknet::Store, Serde)]
struct IERC20LibraryDispatcher {
    class_hash: starknet::ClassHash,
}

impl IERC20LibraryDispatcherImpl of IERC20DispatcherTrait<IERC20LibraryDispatcher> {
    fn name(
        self: IERC20LibraryDispatcher
    ) -> felt252 { // starknet::syscalls::library_call_syscall  is called in here
    }
    fn transfer(
        self: IERC20LibraryDispatcher, recipient: ContractAddress, amount: u256
    ) { // starknet::syscalls::library_call_syscall  is called in here
    }
}

不同:call_contract_syscall vs library_call_syscall.

Calling Contracts using the Library Dispatcher

use starknet::ContractAddress;
#[starknet::interface]
trait IContractB<TContractState> {
    fn set_value(ref self: TContractState, value: u128);

    fn get_value(self: @TContractState) -> u128;
}

#[starknet::contract]
mod ContractA {
    use super::{IContractBDispatcherTrait, IContractBLibraryDispatcher};
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        value: u128
    }

    #[generate_trait]
    #[external(v0)]
    impl ContractA of IContractA {
        fn set_value(ref self: ContractState, value: u128) {
            IContractBLibraryDispatcher { class_hash: starknet::class_hash_const::<0x1234>() }
                .set_value(value)
        }

        fn get_value(self: @ContractState) -> u128 {
            self.value.read()
        }
    }
}

更底层的syscalls

上面的dispatcher是顶层调用

下面使用call_contract_syscall来call transfer

#[starknet::interface]
trait ITokenWrapper<TContractState> {
    fn transfer_token(
        ref self: TContractState,
        address: starknet::ContractAddress,
        selector: felt252,
        calldata: Array<felt252>
    ) -> bool;
}

#[starknet::contract]
mod TokenWrapper {
    use super::ITokenWrapper;
    use starknet::SyscallResultTrait;

    #[storage]
    struct Storage {}

    impl TokenWrapper of ITokenWrapper<ContractState> {
        fn transfer_token(
            ref self: ContractState,
            address: starknet::ContractAddress,
            selector: felt252,
            calldata: Array<felt252>
        ) -> bool {
            let mut res = starknet::call_contract_syscall(address, selector, calldata.span())
                .unwrap_syscall();
            Serde::<bool>::deserialize(ref res).unwrap()
        }
    }
}

selector是function name的starknet_keccak hash。return我们会获得一个序列化的值自己反序列化。

合约安全

assert

impl Contract of IContract<ContractState> {
        fn withdraw(ref self: ContractState, amount: u256) {
            let current_balance = self.balance.read();

            assert(self.balance.read() >= amount, 'Insufficient funds');

            self.balance.write(current_balance - amount);
        }

防重入攻击

  1. Checks: Validate all conditions and inputs before performing any state changes.
  2. Effects: Perform all state changes.
  3. Interactions: All external calls to other contracts should be made at the end of the function.

权限控制

必须严格控制user或者roles,例子

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

    trait IContract<TContractState> {
        fn is_owner(self: @TContractState) -> bool;
        fn is_role_a(self: @TContractState) -> bool;
        fn only_owner(self: @TContractState);
        fn only_role_a(self: @TContractState);
        fn only_allowed(self: @TContractState);
        fn set_role_a(ref self: TContractState, _target: ContractAddress, _active: bool);
        fn role_a_action(ref self: ContractState);
        fn allowed_action(ref self: ContractState);
    }

    #[storage]
    struct Storage {
        // Role 'owner': only one address
        owner: ContractAddress,
        // Role 'role_a': a set of addresses
        role_a: LegacyMap::<ContractAddress, bool>
    }

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

    // Guard functions to check roles

    impl Contract of IContract<ContractState> {
        #[inline(always)]
        fn is_owner(self: @ContractState) -> bool {
            self.owner.read() == get_caller_address()
        }

        #[inline(always)]
        fn is_role_a(self: @ContractState) -> bool {
            self.role_a.read(get_caller_address())
        }

        #[inline(always)]
        fn only_owner(self: @ContractState) {
            assert(Contract::is_owner(self), 'Not owner');
        }

        #[inline(always)]
        fn only_role_a(self: @ContractState) {
            assert(Contract::is_role_a(self), 'Not role A');
        }

        // You can easily combine guards to perfom complex checks
        fn only_allowed(self: @ContractState) {
            assert(Contract::is_owner(self) || Contract::is_role_a(self), 'Not allowed');
        }

        // Functions to manage roles

        fn set_role_a(ref self: ContractState, _target: ContractAddress, _active: bool) {
            Contract::only_owner(@self);
            self.role_a.write(_target, _active);
        }

        // You can now focus on the business logic of your contract
        // and reduce the complexity of your code by using guard functions

        fn role_a_action(ref self: ContractState) {
            Contract::only_role_a(@self);
        // ...
        }

        fn allowed_action(ref self: ContractState) {
            Contract::only_allowed(@self);
        // ...
        }
    }
}

HELP

另外我自己在完成一个项目,有想做项目的朋友可以加入我一起开发,一个人能力太有限了,欢迎大家加我微信,当然即使不是为了一起做项目,只是一起交流也可以的,我也很高兴在web3可以认识新的朋友:

在这里插入图片描述

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值