Interacting with Another Contract

In the previous section, we introduced the dispatcher pattern for contract interactions. This chapter will explore this pattern in depth and demonstrate how to use it.

The dispatcher pattern allows us to call functions on another contract by using a struct that wraps the contract address and implements the dispatcher trait generated by the compiler from the contract class ABI. This leverages Cairo's trait system to provide a clean and type-safe way to interact with other contracts.

When a contract interface is defined, the compiler automatically generates and exports multiple dispatchers. For instance, for an IERC20 interface, the compiler will generate the following dispatchers:

  • Contract Dispatchers: IERC20Dispatcher and IERC20SafeDispatcher
  • Library Dispatchers: IERC20LibraryDispatcher and IERC20SafeLibraryDispatcher

These dispatchers serve different purposes:

  • Contract dispatchers wrap a contract address and are used to call functions on other contracts.
  • Library dispatchers wrap a class hash and are used to call functions on classes. Library dispatchers will be discussed in the next chapter, "Executing code from another class".
  • 'Safe' dispatchers allow the caller to handle potential errors during the execution of the call.

Note: As of Starknet 0.13.2, error handling in contract calls is not yet available. This means that if a contract call fails, the entire transaction will fail. This will change in the future, allowing safe dispatchers to be used on Starknet.

Under the hood, these dispatchers use the low-level contract_call_syscall, which allows us to call functions on other contracts by passing the contract address, the function selector, and the function arguments. The dispatcher abstracts away the complexity of this syscall, providing a clean and type-safe way to interact with other contracts.

To effectively break down the concepts involved, we will use the ERC20 interface as an illustration.

The Dispatcher Pattern

We mentioned that the compiler would automatically generate the dispatcher struct and the dispatcher trait for a given interface. Listing 15-1 shows an example of the generated items for an IERC20 interface that exposes a name view function and a transfer external function:

use core::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 {
    pub contract_address: starknet::ContractAddress,
}

impl IERC20DispatcherImpl of IERC20DispatcherTrait<IERC20Dispatcher> {
    fn name(self: IERC20Dispatcher) -> felt252 {
        let mut __calldata__ = core::traits::Default::default();

        let mut __dispatcher_return_data__ = starknet::syscalls::call_contract_syscall(
            self.contract_address, selector!("name"), core::array::ArrayTrait::span(@__calldata__),
        );
        let mut __dispatcher_return_data__ = starknet::SyscallResultTrait::unwrap_syscall(
            __dispatcher_return_data__
        );
        core::option::OptionTrait::expect(
            core::serde::Serde::<felt252>::deserialize(ref __dispatcher_return_data__),
            'Returned data too short',
        )
    }
    fn transfer(self: IERC20Dispatcher, recipient: ContractAddress, amount: u256) {
        let mut __calldata__ = core::traits::Default::default();
        core::serde::Serde::<ContractAddress>::serialize(@recipient, ref __calldata__);
        core::serde::Serde::<u256>::serialize(@amount, ref __calldata__);

        let mut __dispatcher_return_data__ = starknet::syscalls::call_contract_syscall(
            self.contract_address,
            selector!("transfer"),
            core::array::ArrayTrait::span(@__calldata__),
        );
        let mut __dispatcher_return_data__ = starknet::SyscallResultTrait::unwrap_syscall(
            __dispatcher_return_data__
        );
        ()
    }
}

Listing 15-1: A simplified example of the IERC20Dispatcher and its associated trait and impl

As you can see, the contract dispatcher is a simple struct that wraps a contract address and implements the IERC20DispatcherTrait generated by the compiler. For each function, the implementation of the trait will contain the following elements:

  • A serialization of the function arguments into a felt252 array, __calldata__.
  • A low-level contract call using contract_call_syscall with the contract address, the function selector, and the __calldata__ array.
  • A deserialization of the returned value into the expected return type.

Calling Contracts Using the Contract Dispatcher

To illustrate the use of the contract dispatcher, let's create a simple contract that interacts with an ERC20 contract. This wrapper contract will allow us to call the name and transfer_from functions on the ERC20 contract, as shown in Listing 15-2:

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, address: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
}

//**** Specify interface here ****//
#[starknet::contract]
mod TokenWrapper {
    use super::{IERC20Dispatcher, IERC20DispatcherTrait};
    use super::ITokenWrapper;
    use starknet::{get_caller_address, 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,
            address: ContractAddress,
            recipient: ContractAddress,
            amount: u256
        ) -> bool {
            let erc20_dispatcher = IERC20Dispatcher { contract_address: address };
            erc20_dispatcher.transfer_from(get_caller_address(), recipient, amount)
        }
    }
}

Listing 15-2: A sample contract which uses the dispatcher pattern to call another contract

In this contract, we import the IERC20Dispatcher struct and the IERC20DispatcherTrait trait. We then wrap the address of the ERC20 contract in an instance of the IERC20Dispatcher struct. This allows us to call the name and transfer functions on the ERC20 contract.

Calling transfer_token external function will modify the state of the contract deployed at contract_address.

Calling Contracts using Low-Level Calls

Another way to call other contracts is to directly use the call_contract_syscall. While less convenient than using the dispatcher pattern, this syscall provides more control over the serialization and deserialization process and allows for more customized error handling.

Listing 15-3 shows an example demonstrating how to call the transfer_from function of an ERC20 contract with a low-level call_contract_sycall syscall:

use starknet::ContractAddress;

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

#[starknet::contract]
mod TokenWrapper {
    use super::ITokenWrapper;
    use starknet::{ContractAddress, syscalls, SyscallResultTrait, get_caller_address};

    #[storage]
    struct Storage {}

    impl TokenWrapper of ITokenWrapper<ContractState> {
        fn transfer_token(
            ref self: ContractState,
            address: ContractAddress,
            recipient: ContractAddress,
            amount: u256
        ) -> bool {
            let mut call_data: Array<felt252> = array![];
            Serde::serialize(@get_caller_address(), ref call_data);
            Serde::serialize(@recipient, ref call_data);
            Serde::serialize(@amount, ref call_data);

            let mut res = syscalls::call_contract_syscall(
                address, selector!("transfer_from"), call_data.span()
            )
                .unwrap_syscall();

            Serde::<bool>::deserialize(ref res).unwrap()
        }
    }
}

Listing 15-3: A sample contract using call_contract_sycall syscall

To use this syscall, we passed in the contract address, the selector of the function we want to call and the call arguments. The call arguments must be provided as an array of arguments, serialized to a Span<felt252>. To serialize the arguments, we can simply use the Serde trait, provided that the types being serialized implement this trait. The call returns an array of serialized values, which we'll need to deserialize ourselves!