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
andIERC20SafeDispatcher
- Library Dispatchers:
IERC20LibraryDispatcher
andIERC20SafeLibraryDispatcher
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__,
);
()
}
}
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 core::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 core::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)
}
}
}
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 core::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 core::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()
}
}
}
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!