Skip to main content

Cross Contract Calls

The cross contract call example demonstrates how to call a contract from another contract.

info

In this example there are two contracts that are compiled separately, deployed separately, and then tested together. There are a variety of ways to develop and test contracts with dependencies on other contracts, and the Soroban SDK and tooling is still building out the tools to support these workflows. Feedback appreciated here.

Run the Example

First go through the Setup process to get your development environment configured, then clone the v0.0.4 tag of soroban-examples repository:

git clone -b v0.0.4 https://github.com/stellar/soroban-examples

To run the tests for the example, navigate to the cross_contract/contract_b directory, and use cargo test.

cd cross_contract/contract_b
cargo test

You should see the output:

running 1 test
test test::test ... ok

Code

cross_contract/contract_a/src/lib.rs
pub struct ContractA;

#[contractimpl]
impl ContractA {
pub fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow")
}
}
cross_contract/contract_b/src/lib.rs
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)
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/cross_contract

How it Works

Cross contract calls are made by invoking another contract by its contract ID.

Contracts to invoke can be imported into your contract with the use of contractimport!(file = "..."). The import will code generate:

  • A ContractClient type that can be used to invoke functions on the contract.
  • Any types in the contract that were annotated with #[contracttype].
tip

The contractimport! macro will generate the types in the module it is used, so it's a good idea to use the macro inside a mod { ... } block, or inside its own file, so that the names of generated types don't collide with names of types in your own contract.

Open the files above to follow along.

Contract A: The Contract to be Called

The contract to be called is Contract A. It is a simple contract that accepts x and y parameters, adds them together and returns the result.

cross_contract/contract_a/src/lib.rs
pub struct ContractA;

#[contractimpl]
impl ContractA {
pub fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow")
}
}
tip

The contract uses the checked_add method to ensure that there is no overflow, and if there is overflow, panics rather than returning an overflowed value. Rust's primitive integer types all have checked operations available as functions with the prefix checked_.

Contract B: The Contract doing the Calling

The contract that does the calling is Contract B. It accepts a contract ID that it will call, as well as the same parameters to pass through. In many contracts the contract to call might have been stored as contract data and be retrieved, but in this simple example it is being passed in as a parameter each time.

The contract imports Contract A into the contract_a module.

The contract_a::ContractClient is constructed pointing at the contract ID passed in.

The client is used to execute the add function with the x and y parameters on Contract A.

cross_contract_calls/src/a.rs
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)
}
}

Tests

Open the cross_contract/contract_b/src/test.rs file to follow along.

cross_contract/contract_b/src/test.rs
#[test]
fn test() {
let env = Env::default();

// Define IDs for contract A and B.
let contract_a_id = BytesN::from_array(&env, &[0; 32]);
let contract_b_id = BytesN::from_array(&env, &[1; 32]);

// Register contract A using the imported WASM.
env.register_contract_wasm(&contract_a_id, contract_a::WASM);

// Register contract B defined in this crate.
env.register_contract(&contract_b_id, 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);
}

In any test the first thing that is always required is an Env, which is the Soroban environment that the contract will run in.

let env = Env::default();

Contracts must be registered with the environment with a contract ID, which is a 32-byte value. Both contracts a and b have IDs defined that are used in the rest of the test.

let contract_a_id = BytesN::from_array(&env, &[0; 32]);
let contract_b_id = BytesN::from_array(&env, &[1; 32]);

Contract A is registered with the environment using the imported WASM.

env.register_contract_wasm(&contract_a_id, contract_a::WASM);

Contract B is registered with the environment using the type that is in the crate.

env.register_contract(&contract_b_id, ContractB);

All public functions within an impl block that is annotated with the #[contractimpl] attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with Client appended. For example, in our contract the contract type is ContractB, and the client is named ContractBClient. The client can be constructed and used in the same way that client generated for Contract A can be.

let client = ContractBClient::new(&env, &contract_b_id);

The client is used to invoke the add_with function on Contract B. Contract B will invoke Contract A, and the result will be returned.

let sum = client.add_with(&contract_a_id, &5, &7);

The test asserts that the result that is returned is as we expect.

assert_eq!(sum, 12);

Build the Contracts

To build the contract into a .wasm file, use the cargo build command. Both contract_call/contract_a and contract_call/contract_b must be built.

cargo build --target wasm32-unknown-unknown --release

Both .wasm files should be found in the ../target directory after building both contracts:

target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm
target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm

Run the Contract

If you have [soroban-cli] installed, you can invoke contract functions. Both contracts must be deployed.

soroban-cli deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm \
--id a
soroban-cli deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm \
--id b

Invoke Contract B's add_with function, passing in values for x and y (e.g. as 5 and 7), and then pass in the contract ID of Contract A.

soroban-cli invoke \
--id b \
--fn add_with \
--arg a \
--arg 5 \
--arg 7

The following output should occur using the code above.

12

Contract B's add_with function invoked Contract A's add function to do the addition.