DEV Community


Posted on • Updated on

Soroban Contracts 101 : Cross Contract Call

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;

impl ContractA {
    pub fn add(x: u32, y: u32) -> u32 {
        x.checked_add(y).expect("no overflow")
Enter fullscreen mode Exit fullscreen mode

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 and y)
  • Uses checked_add to add them, which returns an Option<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 {
        file = "../../target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm"

pub struct ContractB;

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)
Enter fullscreen mode Exit fullscreen mode

Our ContractB calls into the first contract (ContractA). It:

  • Imports the ContractA contract as a module (contract_a) using a contractimport
  • Defines a ContractB struct
  • Implements a contractimpl on ContractB
  • Defines an add_with function that:
  • Takes the Env, ContractA contract ID, and two u32 values (x and y)
  • Creates a client for ContractA using the passed in ID
  • Calls add on the ContractA client, passing in x and y
  • Returns the result from the ContractA call

The Test Code

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);
Enter fullscreen mode Exit fullscreen mode

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 the ContractB client, passing in the ContractA ID, x and y values to add
  • 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
Enter fullscreen mode Exit fullscreen mode

If the tests are successful, you should see an output similar to:

running 1 test
test test::test ... ok
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

To build Contract B use the following command:

cd .../cross_contract/contract_b
cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode

Both .wasm files should be found in the ../target directory:

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Deploy ContractB

soroban contract deploy \
    --wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm \
    --id b
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You should see the following output:

Enter fullscreen mode Exit fullscreen mode


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.

Top comments (0)