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
CreateBuilderandCallBuilder)
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.
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:
- Import
OtherContractintoCrossContractCalls - Call
OtherContractusingCrossContractCalls
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",
]
There are two important things to emphasize here:
- If we don't specify the
ink-as-dependencyfeature we will end up with linking errors. - If we don't enable the
stdfeature forstdbuilds 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).
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.
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.
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.
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_hashof0x4242... - with no gas limit specified (
0means unlimited) - sending
10units of transferred value to the contract instance - instantiating with the
newconstructor - with the following arguments
- a
u8with value42 - a
boolwith valuetrue - an array of 32
u8with 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]))
.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.
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 (
0means unlimited) - sending
10units of transferred value to the contract instance - calling the
flipmessage - with the following arguments
- a
u8with value42 - a
boolwith valuetrue - an array of 32
u8with value0x10
- a
- 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();
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.
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!) of0x4242... - calling the
flipmessage - with the following arguments
- a
u8with value42 - a
boolwith valuetrue - an array of 32
u8with value0x10
- a
- 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:
- Errors from the underlying execution environment (e.g the Contracts pallet)
- 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.