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!:
- Contract references (i.e
ContractRef
) - Builders (i.e
CreateBuilder
andCallBuilder
)
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:
- Prepare
OtherContract
to be imported to other contracts - Import
OtherContract
intoBasicContractRef
- Upload
OtherContract
on-chain - Instantiate
OtherContract
usingBasicContractRef
- Call
OtherContract
usingBasicContractRef
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:
- If we don't specify the
ink-as-dependency
feature we will end up with linking errors. - If we don't enable the
std
feature forstd
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.
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
of0x4242...
- 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 value42
- a
bool
with valuetrue
- an array of 32
u8
with value0x10
- a
- generate the address (
AccountId
) using the specifiedsalt_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: Call
s and DelegateCall
s. We will briefly cover both
here.
CallBuilder: Call
When using Call
s 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 value42
- a
bool
with valuetrue
- an array of 32
u8
with value0x10
- a
- 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 DelegateCall
s, 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!) of0x4242...
- calling the
flip
message - with the following arguments
- a
u8
with value42
- a
bool
with valuetrue
- an array of 32
u8
with value0x10
- a
- 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:
- Errors from the underlying execution environment (e.g the Contracts pallet)
- Error from the programming language (e.g
LangError
s)
See the documentation for
try_instantiate
,
try_invoke
,
ink::env::Error
and
ink::LangError
for more details on proper error handling.
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.