Skip to main content
Version: v6

Cross Contract Title Picture

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)
note

In general, contract references should be preferred over builders because they provide higher-level type-safe interfaces. Only use builders if you need to manipulate low-level call parameters.

Contract References

Contract references are wrapper types that can be used for interacting with an on-chain/"callee" contract using a high-level type-safe interface.

They are either statically generated by the ink! code generation (for contract dependencies), or they can be manually defined as dynamic interfaces using the #[ink::contract_ref] attribute.

Statically generated contract references

To use statically generated contract references, you need to import the contract you want to call as a dependency of your own contract.

note

This means that this approach cannot be used if you want to interact with a contract that is either built in another language (e.g. Solidity), or has no publicly available package/crate.

For calling Solidity Contracts you will need to use either manually defined contract references using the #[ink::contract_ref] attribute (recommended), or the Builders approach instead.

CrossContractCalls walkthrough

This walkthrough uses the cross-contract-calls example to illustrate how contract references enable cross-contract calls.

The general workflow will be:

  1. Import OtherContract into CrossContractCalls
  2. Call OtherContract using CrossContractCalls

Importing OtherContract

We need to import OtherContract to our CrossContractCalls contract.

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

# In `cross-contract-calls/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",
]
note

There are two important things to emphasize 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 CrossContractCalls

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

// In `cross-contract-calls/lib.rs`

use other_contract::OtherContractRef;

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

Next, we will store the address of an instance of OtherContract. We do this from the constructor of our contract.

// In `cross-contract-calls/lib.rs`

#[ink(constructor)]
pub fn new(other_contract_address: ink::Address) -> Self {
let other_contract = ink::env::call::FromAddr::from_addr(other_contract_address);
Self { other_contract }
}

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

// In `cross-contract-calls/lib.rs`

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

Instantiating CrossContractCalls with an address for OtherContract

We will first need to instantiate CrossContractCalls.

We will need an address of an instance of OtherContract that is already on-chain (i.e. a 20 bytes pallet-revive address like 0xd051d56ffc5077e006d1fdb14a2311276873aa86).

note

For the next steps, you will either need the ink-node running in the background, or you'll need to provide the url of your target node.

For the latter, see the instructions for deploying to Passet Hub Testnet as an example.

# In the `cross-contract-calls` directory
cargo contract build --release
cargo contract instantiate \
--constructor new \
--args 0xd051d56ffc5077e006d1fdb14a2311276873aa86 \
--suri //Alice --salt $(date +%s) \
-x

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

Contract 0x427b4c31ce5cdc19ec19bc9d2fb0e22ba69c84c3

Calling OtherContract through CrossContractCalls

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

cargo contract call --contract 0x427b4c31ce5cdc19ec19bc9d2fb0e22ba69c84c3 \
--message flip_and_get --suri //Alice --dry-run

Which will result in something like:

Result Ok(true)
Reverted false

Manually defined contract references

See our section on using the #[ink::contract_ref] attribute for a detailed description and examples of how to manually define the dynamic interface for an on-chain/"callee" contract, and use the generated contract reference for calling the on-chain/"callee" contract in a type-safe manner.

caution

A downside to manually defined contract references is that mistakes in the interface definition are not caught at compile-time.

It's therefore important to make sure such interfaces are properly tested using end-to-end testing before contracts are deployed on-chain.

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.

caution

A downside to low-level CreateBuilders and CallBuilders is that mistakes in the generated calls (e.g. wrong selectors, wrong order and/or types of arguments e.t.c) are not caught at compile-time.

It's therefore important to make sure such calls are properly tested using end-to-end testing before contracts are deployed on-chain.

CreateBuilder

The CreateBuilder offers 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]))
.ref_time_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.

note

To instantiate a Solidity ABI contract, see Calling Solidity Contracts.

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(H160::from([0x42; 20]))
.ref_time_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();
caution

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 get any feedback about this at compile-time, so it's important to make sure such calls are properly tested with end-to-end testing before contracts are deployed on-chain.

note

To call Solidity ABI-encoded contracts, see Calling Solidity Contracts.

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(H160::from([0x42; 20]))
.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.