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-5 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
}
}
16-5: 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 16-6 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;
use 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 starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::{ClassHash, ContractAddress};
use super::{IValueStoreDispatcherTrait, IValueStoreLibraryDispatcher};
#[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()
}
}
16-6: 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 16-7 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::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::{ClassHash, SyscallResultTrait, syscalls};
#[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()
}
}
16-7: 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.