#[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
用于初始化合约
- 每个合约仅有一个
- 名字就叫constructor
- 必须标记
#[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)]
属性
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
ContractState
通过ref传递,用于修改合约状态
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
-
声明
component!()
- 路径path::to::component
- 合约存储指向组件存储的名字,,上文的
Ownable
- 合约event变量名指向组件event名,例如
OwnableEvent
-
添加组件storage和event路径到合约,必须匹配步骤1中的名字
(e.g. ownable: ownable_component::Storage and OwnableEvent: ownable_component::Event)
-
嵌入具体逻辑,通过具体的
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定义了桥接TContractState
和ComponentState<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, IERC20Dispatcher
和 IERC20LibraryDispatcher
同时编译器产生一个trait IERC20DispatcherTrait
,用于调用dispatcher struct的接口函数。
Contract Dispatcher
生成的dispatcher如下(很长,下面是重点 name
和 transfer
)
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);
}
防重入攻击
- Checks: Validate all conditions and inputs before performing any state changes.
- Effects: Perform all state changes.
- 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可以认识新的朋友: