Calling other contracts

There are two different ways to call other contracts in Cairo.

The easiest way to call other contracts is by using the dispatcher of the contract you want to call. You can read more about Dispatchers in the Cairo Book.

The other way is to use the starknet::call_contract_syscall syscall yourself. However, this method is not recommended and will not be covered in this chapter.

In order to call other contracts using dispatchers, you will need to define the called contract's interface as a trait annotated with the #[starknet::interface] attribute, and then import the IContractDispatcher and IContractDispatcherTrait items in your contract.

Here's the Callee contract interface and implementation:

// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
    fn set_value(ref self: TContractState, value: u128) -> u128;
}

#[starknet::contract]
pub mod Callee {
    #[storage]
    struct Storage {
        value: u128,
    }

    #[abi(embed_v0)]
    impl ICalleeImpl of super::ICallee<ContractState> {
        fn set_value(ref self: ContractState, value: u128) -> u128 {
            self.value.write(value);
            value
        }
    }
}

#[starknet::interface]
pub trait ICaller<TContractState> {
    fn set_value_from_address(
        ref self: TContractState, addr: starknet::ContractAddress, value: u128
    );
}

#[starknet::contract]
pub mod Caller {
    // We need to import the dispatcher of the callee contract
    // If you don't have a proper import, you can redefine the interface by yourself
    use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
    use starknet::ContractAddress;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ICallerImpl of super::ICaller<ContractState> {
        fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
            ICalleeDispatcher { contract_address: addr }.set_value(value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Callee, ICalleeDispatcher, ICalleeDispatcherTrait, Callee::valueContractMemberStateTrait,
        Caller, ICallerDispatcher, ICallerDispatcherTrait
    };
    use starknet::{
        ContractAddress, contract_address_const, testing::set_contract_address,
        syscalls::deploy_syscall, SyscallResultTrait
    };

    fn deploy() -> (ICalleeDispatcher, ICallerDispatcher) {
        let (address_callee, _) = deploy_syscall(
            Callee::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let (address_caller, _) = deploy_syscall(
            Caller::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        (
            ICalleeDispatcher { contract_address: address_callee },
            ICallerDispatcher { contract_address: address_caller }
        )
    }

    #[test]
    fn test_caller() {
        let init_value: u128 = 42;

        let (callee, caller) = deploy();
        caller.set_value_from_address(callee.contract_address, init_value);

        let state = Callee::contract_state_for_testing();
        set_contract_address(callee.contract_address);

        let value_read: u128 = state.value.read();

        assert_eq!(value_read, init_value);
    }
}

The following Caller contract uses the Callee dispatcher to call the Callee contract:

// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
    fn set_value(ref self: TContractState, value: u128) -> u128;
}

#[starknet::contract]
pub mod Callee {
    #[storage]
    struct Storage {
        value: u128,
    }

    #[abi(embed_v0)]
    impl ICalleeImpl of super::ICallee<ContractState> {
        fn set_value(ref self: ContractState, value: u128) -> u128 {
            self.value.write(value);
            value
        }
    }
}

#[starknet::interface]
pub trait ICaller<TContractState> {
    fn set_value_from_address(
        ref self: TContractState, addr: starknet::ContractAddress, value: u128
    );
}

#[starknet::contract]
pub mod Caller {
    // We need to import the dispatcher of the callee contract
    // If you don't have a proper import, you can redefine the interface by yourself
    use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
    use starknet::ContractAddress;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ICallerImpl of super::ICaller<ContractState> {
        fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
            ICalleeDispatcher { contract_address: addr }.set_value(value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Callee, ICalleeDispatcher, ICalleeDispatcherTrait, Callee::valueContractMemberStateTrait,
        Caller, ICallerDispatcher, ICallerDispatcherTrait
    };
    use starknet::{
        ContractAddress, contract_address_const, testing::set_contract_address,
        syscalls::deploy_syscall, SyscallResultTrait
    };

    fn deploy() -> (ICalleeDispatcher, ICallerDispatcher) {
        let (address_callee, _) = deploy_syscall(
            Callee::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let (address_caller, _) = deploy_syscall(
            Caller::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        (
            ICalleeDispatcher { contract_address: address_callee },
            ICallerDispatcher { contract_address: address_caller }
        )
    }

    #[test]
    fn test_caller() {
        let init_value: u128 = 42;

        let (callee, caller) = deploy();
        caller.set_value_from_address(callee.contract_address, init_value);

        let state = Callee::contract_state_for_testing();
        set_contract_address(callee.contract_address);

        let value_read: u128 = state.value.read();

        assert_eq!(value_read, init_value);
    }
}
Last change: 2024-06-09, commit: 3fbfb60