Skip to main content
Version: 4.x

Cross-Contract Calls

In ink! contracts it is possible to call messages and constructors of other on-chain contracts.

There are a few approaches to performing these cross-contract calls in ink!:

  1. Contract references (i.e ContractRef)
  2. Builders (i.e CreateBuilder and CallBuilder)

Contract references can only be used for cross-contract calls to other ink! contracts. Builders can be used to issue cross-contract calls to any Wasm contract, such as those written in ink!, Solang, or ask!.

Contract References

Contract references refer to structs that are generated by the ink! code generation for the purposes of cross-contract calls.

They give developers a type-safe way of interacting with a contract.

A downside to using them is that you need to import the contract you want to call as a dependency of your own contract.

If you want to interact with a contract that is already on-chain you will need to use the Builders approach instead.

BasicContractRef walkthrough

We will walk through the cross-contract-calls example in order to demonstrate how cross-contract calls using contract references work.

The general workflow will be:

  1. Prepare OtherContract to be imported to other contracts
  2. Import OtherContract into BasicContractRef
  3. Upload OtherContract on-chain
  4. Instantiate OtherContract using BasicContractRef
  5. Call OtherContract using BasicContractRef

Prepping OtherContract

We need to make sure that the ink! generated contract ref for OtherContract is available to other pieces of code.

We do this by re-exporting the contract reference as follows:

pub use self::other_contract::OtherContractRef;

Importing OtherContract

Next, we need to import OtherContract to our BasicContractRef contract.

First, we add the following lines to our Cargo.toml file:

# In `basic_contract_ref/Cargo.toml`

other_contract = { path = "other_contract", default-features = false, features = ["ink-as-dependency"] }

# -- snip --

[features]
default = ["std"]
std = [
"ink/std",
# -- snip --
"other_contract/std",
]

Two things to note here:

  1. If we don't specify the ink-as-dependency feature we will end up with linking errors.
  2. If we don't enable the std feature for std builds we will not be able to generate our contract's metadata.

Wiring BasicContractRef

First, we will import the contract reference of OtherContract, and declare the reference to be part of our storage struct.

// In `basic_contract_ref/lib.rs`

use other_contract::OtherContractRef;

#[ink(storage)]
pub struct BasicContractRef {
other_contract: OtherContractRef,
}

Next, we to add a way to instantiate OtherContract. We do this from the constructor of our of contract.

// In `basic_contract_ref/lib.rs`

#[ink(constructor)]
pub fn new(other_contract_code_hash: Hash) -> Self {
let other_contract = OtherContractRef::new(true)
.code_hash(other_contract_code_hash)
.endowment(0)
.salt_bytes([0xDE, 0xAD, 0xBE, 0xEF])
.instantiate();

Self { other_contract }
}

Note that for instantiating a contract we need access to the uploaded on-chain code_hash. We will get back to this later.

Once we have an instantiated reference to OtherContract we can call its messages just like normal Rust methods!

// In `basic_contract_ref/lib.rs`

#[ink(message)]
pub fn flip_and_get(&mut self) -> bool {
self.other_contract.flip();
self.other_contract.get()
}

Uploading OtherContract

You will need the substrate-contracts-node running in the background for the next steps.

We can upload OtherContract using cargo-contract as follows:

# In the `basic_contract_ref` directory
cargo contract build --manifest-path other_contract/Cargo.toml
cargo contract upload --manifest-path other_contract/Cargo.toml --suri //Alice

If successful, this will output in a code_hash similar to:

Code hash "0x74a610235df4ff0161f0247e4c9d73934b70c1520d24ef843f9df9fcc3e63caa"

We can then use this code_hash to instantiate our BasicContractRef contract.

Instantiating OtherContract through BasicContractRef

We will first need to instantiate BasicContractRef.

# In the `basic_contract_ref` directory
cargo contract build
cargo contract instantiate \
--constructor new \
--args 0x74a610235df4ff0161f0247e4c9d73934b70c1520d24ef843f9df9fcc3e63caa \
--suri //Alice --salt $(date +%s)

If successful, this will output in a contract address for BasicContractRef similar to:

Contract 5CWz6Xnivp9PSoZq6qPRP8xVAShZgtNVGTCLCsq3qzqPV7Rq

Calling with OtherContract through BasicContractRef

Finally, we can call the OtherContract methods through BasicContractRef as follows:

cargo contract call --contract 5CWz6Xnivp9PSoZq6qPRP8xVAShZgtNVGTCLCsq3qzqPV7Rq \
--message flip_and_get --suri //Alice --dry-run

Which will result in something like:

Result Success!
Reverted false
Data Ok(true)

Builders

The CreateBuilder and CallBuilder offer low-level, flexible interfaces for performing cross-contract calls. The CreateBuilder allows you to instantiate already uploaded contracts, and the CallBuilder allows you to call messages on instantiated contracts.

CreateBuilder

The CreateBuilder offers an an easy way for you to instantiate a contract. Note that you'll still need this contract to have been previously uploaded.

note

For a refresher on the difference between upload and instantiate see here.

In order to instantiate a contract you need a reference to your contract, just like in the previous section.

Below is an example of how to instantiate a contract using the CreateBuilder. We will:

  • instantiate the uploaded contract with a code_hash of 0x4242...
  • with no gas limit specified (0 means unlimited)
  • sending 10 units of transferred value to the contract instance
  • instantiating with the new constructor
  • with the following arguments
    • a u8 with value 42
    • a bool with value true
    • an array of 32 u8 with value 0x10
  • generate the address (AccountId) using the specified salt_bytes
  • and we expect it to return a value of type MyContractRef
use contract::MyContractRef;
let my_contract: MyContractRef = build_create::<MyContractRef>()
.code_hash(Hash::from([0x42; 32]))
.gas_limit(0)
.endowment(10)
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!("new")))
.push_arg(42)
.push_arg(true)
.push_arg(&[0x10u8; 32])
)
.salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF])
.returns::<MyContractRef>()
.instantiate();

Since CreateBuilder::instantiate() returns a contract reference, we can use this contract reference to call messages just like in the previous section.

CallBuilder

The CallBuilder gives you a couple of ways to call messages from other contracts. There are two main approaches to this: Calls and DelegateCalls. We will briefly cover both here.

CallBuilder: Call

When using Calls the CallBuilder requires an already instantiated contract.

We saw an example of how to use the CreateBuilder to instantiate contracts in the previous section.

Below is an example of how to call a contract using the CallBuilder. We will:

  • make a regular Call
  • to a contract at the address 0x4242...
  • with no gas limit specified (0 means unlimited)
  • sending 10 units of transferred value to the contract instance
  • calling the flip message
  • with the following arguments
    • a u8 with value 42
    • a bool with value true
    • an array of 32 u8 with value 0x10
  • and we expect it to return a value of type bool
let my_return_value = build_call::<DefaultEnvironment>()
.call(AccountId::from([0x42; 32]))
.gas_limit(0)
.transferred_value(10)
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!("flip")))
.push_arg(42u8)
.push_arg(true)
.push_arg(&[0x10u8; 32])
)
.returns::<bool>()
.invoke();

Note:

Message arguments will be encoded in the order in which they are provided to the CallBuilder. This means that they should match the order (and type) they appear in the function signature.

You will not be able to get any feedback about this at compile time. You will only find out your call failed at runtime!

CallBuilder: Delegate Call

You can also use the CallBuilder to craft calls using DelegateCall mechanics. If you need a refresher on what delegate calls are, see this article.

In the case of DelegateCalls, we don't require an already instantiated contract. We only need the code_hash of an uploaded contract.

Below is an example of how to delegate call a contract using the CallBuilder. We will:

  • make a DelegateCall
  • to a contract with a code_hash (not contract address!) of 0x4242...
  • calling the flip message
  • with the following arguments
    • a u8 with value 42
    • a bool with value true
    • an array of 32 u8 with value 0x10
  • and we expect it to return an i32
let my_return_value = build_call::<DefaultEnvironment>()
.delegate(ink::primitives::Hash::from([0x42; 32]))
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!("flip")))
.push_arg(42u8)
.push_arg(true)
.push_arg(&[0x10u8; 32])
)
.returns::<i32>()
.invoke();

Builder Error Handling

The CreateBuilder and the CallBuilder both offer error handling with the try_instantiate() and try_invoke() methods respectively.

These allow contract developers to handle two types of errors:

  1. Errors from the underlying execution environment (e.g the Contracts pallet)
  2. Error from the programming language (e.g LangErrors)

See the documentation for try_instantiate, try_invoke, ink::env::Error and ink::LangError for more details on proper error handling.

tip

Because the CallBuilder requires only a contract's AccountId and message selector, we can call Solidity contracts compiled using the Solang compiler and deployed to a chain that supports the pallet-contracts. See here for an example of how to do that.

The reverse, calls from Solidity to ink!, are not supported by Solang, but there are plans to implement this in the future.