使用调度程序和系统调用与其他合约和类交互

每次定义合约接口时,编译器都会自动创建并导出两个调度程序。我们将一个接口命名为 IERC20,它们是:

  1. 合约调度器 IERC20Dispatcher
  2. 库调度器 IERC20LibraryDispatcher

编译器还会生成一个名为 "IERC20DispatcherTrait"的trait,这使得我们可以在调度器结构上调用接口中定义的函数。

在本章中,我们将讨论它们是什么、如何工作以及如何使用。

为了有效地拆解本章的概念,我们将使用前一章的IERC20接口(参考示例99-4):

合约调度器

如前所述,使用 #[starknet::interface] 属性注释的trait会在编译时自动生成一个调度器和一个trait。 我们的 IERC20 接口扩展至如下:

注: IERC20 界面的扩展代码较长,但为了使本章简明扼要,我们将重点放在一个视图函数 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
    }
}

示例99-5:IERC20 trait的扩展

如你所见,"典型"的调度器只是一个结构体,它封装了一个合约地址,并实现了编译器生成的 DispatcherTrait,允许我们调用另一个合约的函数。这意味着我们可以用要调用的合约地址实例化一个结构体,然后简单地调用调度器结构体上的接口定义的函数,就像调用该类型的方法一样。

而且值得注意的是,所有这些都被Cairo插件在幕后抽象化了。

使用合约调度器调用合约

这是一个名为 TokenWrapper 的合约使用调度器调用定义在 ERC-20 令牌上的函数的示例。调用 transfer_token 将修改部署在 contract_address 的合约状态。

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

#[starknet::interface]
trait ITokenWrapper<TContractState> {
    fn token_name(self: @TContractState, contract_address: ContractAddress) -> felt252;

    fn transfer_token(
        ref self: TContractState,
        contract_address: ContractAddress,
        recipient: ContractAddress,
        amount: u256
    ) -> bool;
}


//**** 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)
        }
    }
}

示例99-6:一个使用合约调度器的样本合约

正如您所看到的,我们必须首先导入编译器生成的 IERC20DispatcherTraitIERC20Dispatcher,这样我们就可以调用为 IERC20Dispatcher 结构实现的方法(nametransfer 等),并在 IERC20Dispatcher 结构中传入我们要调用的合约的 contract_address

库调度器

合约调度器和库调度器的主要区别在于类中定义的逻辑的执行上下文。普通调度程序用于调用来自 合约(有相关状态)的函数,而库调度程序则用于调用 (无状态)。

让我们设想两个合约 A 和 B。

当A使用 IBDispatcher 来调用 合约 B中的函数时,定义在B中的逻辑的执行上下文是B的。这意味着在B中由get_caller_address()返回的值将返回A的地址,并且在B中更新存储变量将更新B的存储。

当A使用 IBLibraryDispatcher 来调用B的 中的函数时,定义在B类中的逻辑的执行上下文是A的。这意味着在B中由 get_caller_address() 变量返回的值将返回A的调用者的地址,并且在B的类中更新存储变量将更新A的存储(请记住,B的类是无状态的;没有可以更新的状态!)

编译器生成的 struct 和 trait 的扩展形式如下:

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 生成的,而后者则使用了 library_call_syscall

示例99-7:IERC20 trait的扩展

使用库调度器调用合约

下面是一个关于使用库调度器调用合约的示例代码。

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

示例99-8:一个使用库调度器的样本合约

正如你所看到的,我们必须首先在我们的合约中导入IContractBDispatcherTraitIContractBLibraryDispatcher,它们是由编译器从我们的接口中生成的。然后,我们可以创建一个 IContractBLibraryDispatcher 实例,并将我们要调用库的类的 class_hash 传递进去。在这里,我们可以调用该类中定义的函数,在我们的合约上下文中执行其逻辑。当我们在合约 A 上调用 set_value 时,它将对合约 B 中的 set_value 函数进行库调用,更新合约 A 中存储变量 value 的值。

使用底层系统调用来

调用其他合约和类的另一种方法是使用 starknet::call_contract_syscallstarknet::library_call_syscall 系统调用。我们在前几节中描述的调度器就是这些低级系统调用的高级语法。

使用这些系统调用可以方便地进行自定义错误处理,或对调用数据和返回数据的序列化/反序列化进行更多控制。下面的示例演示了如何使用 call_contract_sycall调用 ERC20 合约的 transfer函数:

use starknet::ContractAddress;
#[starknet::interface]
trait ITokenWrapper<TContractState> {
    fn transfer_token(
        ref self: TContractState,
        address: ContractAddress,
        sender: ContractAddress,
        recipient: ContractAddress,
        amount: u256
    ) -> bool;
}

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

    #[storage]
    struct Storage {}

    impl TokenWrapper of ITokenWrapper<ContractState> {
        fn transfer_token(
            ref self: ContractState,
            address: ContractAddress,
            sender: ContractAddress,
            recipient: ContractAddress,
            amount: u256
        ) -> bool {
            let mut call_data: Array<felt252> = ArrayTrait::new();
            Serde::serialize(@sender, ref call_data);
            Serde::serialize(@recipient, ref call_data);
            Serde::serialize(@amount, ref call_data);
            let mut res = starknet::call_contract_syscall(
                address, selector!("transferFrom"), call_data.span()
            )
                .unwrap_syscall();
            Serde::<bool>::deserialize(ref res).unwrap()
        }
    }
}

示例 99-9:使用系统调用的合约示例

为了使用这个系统调用,我们传入了合约地址、我们想要调用的函数的选择器以及调用参数。

调用参数必须以felt252 数组的形式提供。为了构建这个数组,我们使用 'Serde' trait 将预期的函数参数序列化为一个 Array<felt252>,然后将这个数组作为 calldata 传递。最后,我们返回一个序列化值,我们需要自己反序列化该值!

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