Executing Code from Another Class

In previous chapters, we explored how to call external contracts to execute their logic and update their state. But what if we want to execute code from another class without updating the state of another contract? Starknet makes this possible with library calls, which allow a contract to execute the logic of another class in its own context, updating its own state.

Library calls

The key differences between contract calls and library calls lie in the execution context of the logic defined in the class. While contract calls are used to call functions from deployed contracts, library calls are used to call stateless classes in the context of the caller.

To illustrate this, let's consider two contracts A and B.

When A performs a contract call to the contract B, the execution context of the logic defined in B is that of B. As such, the value returned by get_caller_address() in B will return the address of A, get_contract_address() in B will return the address of B, and any storage updates in B will update the storage of B.

However, when A uses a library call to call the class of B, the execution context of the logic defined in B is that of A. This means that the value returned by get_caller_address() in B will be the address of the caller of A, get_contract_address() in B's class will return the address of A, and updating a storage variable in B's class will update the storage of A.

Library calls can be performed using the dispatcher pattern presented in the previous chapter, only with a class hash instead of a contract address.

Listing 15-4 describes the library dispatcher and its associated IERC20DispatcherTrait trait and impl using the same IERC20 example:

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

Listing 15-4: A simplified example of the IERC20DLibraryDispatcher and its associated trait and impl

One notable difference with the contract dispatcher is that the library dispatcher uses library_call_syscall instead of call_contract_syscall. Otherwise, the process is similar.

Let's see how to use library calls to execute the logic of another class in the context of the current contract.

Using the Library Dispatcher

Listing 15-5 defines two contracts: ValueStoreLogic, which defines the logic of our example, and ValueStoreExecutor, which simply executes the logic of ValueStoreLogic's class.

We first need to import the IValueStoreDispatcherTrait and IValueStoreLibraryDispatcher which were generated from our interface by the compiler. Then, we can create an instance of IValueStoreLibraryDispatcher, passing in the class_hash of the class we want to make library calls to. From there, we can call the functions defined in that class, executing its logic in the context of our contract.

#[starknet::interface]
trait IValueStore<TContractState> {
    fn set_value(ref self: TContractState, value: u128);
    fn get_value(self: @TContractState) -> u128;
}

#[starknet::contract]
mod ValueStoreLogic {
    use starknet::{ContractAddress};

    #[storage]
    struct Storage {
        value: u128
    }

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

        fn get_value(self: @ContractState) -> u128 {
            self.value.read()
        }
    }
}

#[starknet::contract]
mod ValueStoreExecutor {
    use super::{IValueStoreDispatcherTrait, IValueStoreLibraryDispatcher};
    use starknet::{ContractAddress, ClassHash};

    #[storage]
    struct Storage {
        logic_library: ClassHash,
        value: u128
    }

    #[constructor]
    fn constructor(ref self: ContractState, logic_library: ClassHash) {
        self.logic_library.write(logic_library);
    }

    #[abi(embed_v0)]
    impl ValueStoreExecutor of super::IValueStore<ContractState> {
        fn set_value(ref self: ContractState, value: u128) {
            IValueStoreLibraryDispatcher { class_hash: self.logic_library.read() }
                .set_value((value));
        }

        fn get_value(self: @ContractState) -> u128 {
            IValueStoreLibraryDispatcher { class_hash: self.logic_library.read() }.get_value()
        }
    }

    #[external(v0)]
    fn get_value_local(self: @ContractState) -> u128 {
        self.value.read()
    }
}

Listing 15-5: An example contract using a Library Dispatcher

When we call the set_value function on ValueStoreExecutor, it will make a library call to the set_value function defined in ValueStoreLogic. Because we are using a library call, ValueStoreExecutor's storage variable value will be updated. Similarly, when we call the get_value function, it will make a library call to the get_value function defined in ValueStoreLogic, returning the value of the storage variable value - still in the context of ValueStoreExecutor.

As such, both get_value and get_value_local return the same value, as they are reading the same storage slot.

Calling Classes using Low-Level Calls

Another way to call classes is to directly use library_call_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-6 shows an example demonstrating how to use a library_call_syscall to call the set_value function of ValueStore contract:

#[starknet::contract]
mod ValueStore {
    use starknet::{ClassHash, syscalls, SyscallResultTrait};

    #[storage]
    struct Storage {
        logic_library: ClassHash,
        value: u128,
    }

    #[constructor]
    fn constructor(ref self: ContractState, logic_library: ClassHash) {
        self.logic_library.write(logic_library);
    }

    #[external(v0)]
    fn set_value(ref self: ContractState, value: u128) -> bool {
        let mut call_data: Array<felt252> = array![];
        Serde::serialize(@value, ref call_data);

        let mut res = syscalls::library_call_syscall(
            self.logic_library.read(), selector!("set_value"), call_data.span()
        )
            .unwrap_syscall();

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

    #[external(v0)]
    fn get_value(self: @ContractState) -> u128 {
        self.value.read()
    }
}

Listing 15-6: A sample contract using library_call_syscall system call

To use this syscall, we passed in the class hash, 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!

Summary

Congratulations for finishing this chapter! You have learned a lot of new concepts:

  • How Contracts differ from Classes and how the ABI describes them for external sources
  • How to call functions from other contracts and classes using the Dispatcher pattern
  • How to use Library calls to execute the logic of another class in the context of the caller
  • The two syscalls that Starknet provides to interact with contracts and classes

You now have all the required tools to develop complex applications with logic spread across multiple contracts and classes. In the next chapter, we will explore more advanced topics that will help you unleash the full potential of Starknet.