Home Calling contracts using dispatch
Post
Cancel

Calling contracts using dispatch

A defining feature of programmable blockchains is the ability to call other contracts. Let’s explore how to do this in Starknet using Cairo.

We start by declaring an interface of the called contract by using the #[abi] attribute:

1
2
3
4
#[abi]
trait IMintable {
    fn mint_to(receiver: ContractAddress, amount: u128);
}

When compiled, the #[abi] block is expanded into code that looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
trait IMintableDispatcherTrait<T> {
    fn mint_to(self: T, receiver: ContractAddress, amount: u128);
}

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

impl IMintableDispatcherImpl of IMintableDispatcherTrait::<IMintableDispatcher> {
    fn mint_to(self: IMintableDispatcher, receiver: ContractAddress, amount: u128) {
        // starknet::call_contract_syscall is called
        // in the body of the function
    }
}

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

impl IMintableLibraryDispatcherImpl of IMintableDispatcherTrait::<IMintableLibraryDispatcher> {
    fn mint_to(self: IMintableLibraryDispatcher, receiver: ContractAddress, amount: u128) {
        // starknet::syscalls::library_call_syscall is called
        // in the body of the function
    }
}

Behold the power of Cairo plugins

When the compiler encounters the #[abi] attribute, it executes the dispatcher plugin that generates all the boilerplate code for us. The generated code contains a new trait, two new structs and their implementation of this trait. We didn’t have to write anything else besides the initial interface declaration.

To do a regular contract call, we only need to instantiate the IMintableDispatcher struct. It takes a contract_address value, the address of an external contract which we want to interact with. We can now call any of the interface functions on this struct. Note if we declared IMintable in a different scope, we’ll need to import it via use.

Here’s a full example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[abi]
trait IMintable {
    fn mint_to(receiver: starknet::ContractAddress, amount: u128);
}   

#[contract]
mod Altruist {
    // importing IMintableDispacher and the trait into the module scope
    use super::IMintableDispatcher;
    use super::IMintableDispatcherTrait;
    use starknet::contract_address_const;
    use starknet::ContractAddress;

    #[external]
    fn mint_some(receiver: ContractAddress, amount: u128) {
        // address of the contract we want to call
        let token_addr: ContractAddress = contract_address_const::<0xc0ffee>();

        // create a dispatcher using the token address
        let token = IMintableDispatcher { contract_address: token_addr };

        // call a function from the IMintable interface
        token.mint_to(receiver, amount);
    }
}

If we need to do a library call, we can do it in a similar manner using IMintableLibraryDispatcher struct.

1
2
let token = IMintableLibraryDispatcher { class_hash: token_class_hash };
token.mint_to(...);
This post is licensed under CC BY 4.0 by the author.