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.
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.
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.
BasicContractRef walkthrough
This walkthrough uses the cross-contract-calls example to illustrate how contract references enable cross-contract calls.
The general workflow will be:
- Prepare
OtherContractto be imported to other contracts - Import
OtherContractintoBasicContractRef - Upload
OtherContracton-chain - Instantiate
OtherContractusingBasicContractRef - Call
OtherContractusingBasicContractRef
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;
We intend to automatically generate this re-export in future releases of ink! v6.
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-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 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 ink-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 -x
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.
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.