Soroban Contracts 101 : Cross Contract Call
yuzurush
Posted on March 18, 2023
Hi there! Welcome to my eigth post of my series called "Soroban Contracts 101", where I'll be explaining the basics of Soroban contracts, such as data storage, authentication, custom types, and more. All the code that we're gonna explain throughout this series will mostly come from soroban-contracts-101 github repository.
In this eigth post of the series, I'll be covering soroban cross contract call. Cross-contract calls (where one contract calls methods on another contract) are useful for a few reasons in blockchain smart contracts:
Modularity - you can break your system into separate contracts/modules that call on each other, keeping components focused and reusable
Abstraction - contracts can expose a high-level interface that hides internal complexity
Sharing code/logic - common logic/data storage could be in a shared contract
Interoperability - contracts could call into standard contracts to access common blockchain functionality
We're gonna using 2 contract, Contract A act as the "called contract", and Contract B act as the "calling contract". Contract B gonna calls function on Contract A and handles the return values/errors.
The Contracts Code
Contract A Code
pub struct ContractA;
#[contractimpl]
impl ContractA {
pub fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow")
}
}
Contract A is very simple contract with one function:
- It defines a
ContractA
struct - It implements a
contractimpl
on ContractA - The
add
function:
- Takes two u32 parameters (
x
andy
)- Uses
checked_add
to add them, which returns anOption<u32>
(None if there is an overflow)- Uses
expect
to panic if there is an overflow (with a message), Otherwise returns the sum
Contract B Code
mod contract_a {
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm"
);
}
pub struct ContractB;
#[contractimpl]
impl ContractB {
pub fn add_with(env: Env, contract_id: BytesN<32>, x: u32, y: u32) -> u32 {
let client = contract_a::ContractClient::new(&env, contract_id);
client.add(&x, &y)
}
}
Our ContractB calls into the first contract (ContractA). It:
- Imports the
ContractA
contract as a module (contract_a
) using acontractimport
- Defines a
ContractB
struct - Implements a
contractimpl
onContractB
- Defines an
add_with
function that:
- Takes the
Env
,ContractA
contract ID, and two u32 values (x
andy
)- Creates a client for
ContractA
using the passed in ID- Calls
add
on theContractA
client, passing inx
andy
- Returns the result from the
ContractA
call
The Test Code
#[test]
fn test() {
let env = Env::default();
// Register contract A using the imported WASM.
let contract_a_id = env.register_contract_wasm(None, contract_a::WASM);
// Register contract B defined in this crate.
let contract_b_id = env.register_contract(None, ContractB);
// Create a client for calling contract B.
let client = ContractBClient::new(&env, &contract_b_id);
// Invoke contract B via its client. Contract B will invoke contract A.
let sum = client.add_with(&contract_a_id, &5, &7);
assert_eq!(sum, 12);
}
Our test code used to test ContractB
contract. It:
- Creates an
Env
- Registers
ContractA
from its WASM file - Registers
ContractB
- Creates a
ContractB
client - Calls
add_with
on theContractB
client, passing in theContractA
ID,x
andy
values toadd
- Asserts that the correct sum is returned
So this test exercises the full cross-contract call flow, and verifies that the correct result is returned.
Running Contract Tests
Test conducted in contract_b directory
cd cross_contract/contract_b
cargo test
If the tests are successful, you should see an output similar to:
running 1 test
test test::test ... ok
Building The Contract
We need to build both Contract A and Contract B, to build Contract A use the following command:
cd .../cross_contract/contract_a
cargo build --target wasm32-unknown-unknown --release
To build Contract B use the following command:
cd .../cross_contract/contract_b
cargo build --target wasm32-unknown-unknown --release
Both .wasm files should be found in the ../target directory:
target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm
target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm
Deploying The Contract
Unlike previous post, we need to deploy the contract first, instead of invoking the contract WASM files directly.To deploy the contracts use this command :
Deploy ContractA
soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm \
--id a
Deploy ContractB
soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm \
--id b
Invoking The Contract
To do cross contract call, we only need to invoke ContractB
contract, use the following command with Soroban-CLI :
soroban contract invoke \
--id b \
-- \
add_with \
--contract_id a \
--x 10 \
--y 5
You should see the following output:
15
Conclusion
We explored how to implement and use cross-contract calls in Soroban. Cross-contract calls allow contracts to execute methods on other contracts. Overall, cross-contract calls are a useful pattern for contract modularity and abstraction, if designed and tested carefully to avoid risks. Stay tuned for more post in this "Soroban Contracts 101" Series where we will dive deeper into Soroban Contracts and their functionalities.
Posted on March 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
March 20, 2023