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 16-4 describes the library dispatcher and its associated IERC20DispatcherTrait
trait and impl using the same IERC20
example:
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 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
}
}
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 16-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 core::starknet::{ContractAddress};
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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 core::starknet::{ContractAddress, ClassHash};
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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()
}
}
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 16-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 core::starknet::{ClassHash, syscalls, SyscallResultTrait};
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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()
}
}
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.