It's pretty straightforward to call a contract from a pallet. I'm going to assume you're calling an ink! contract with the following message for the purposes of this answer.
#[ink(message)]
pub fn flip(&mut self, foo: u32) {
todo!()
}
I'm also gonna assume this contract has already been deployed on-chain.
You'll need a couple of things:
- Some sort of coupling to
pallet-contracts
- Knowledge about the contract's message selector and arguments
With that you'll able to use pallet_contracts::bare_call
to call into our contract.
Coupling
Your custom pallet needs to have access to the interface from the Contracts pallet. It
can do this in one of two ways:
I'm not gonna get into the details here, but just know that you have to use one of
them. We'll use tight coupling for our example.
Selector and Arguments
The message selector is the identifier used to pick out which of the many functions in
your smart contract should be called. Assuming you have an ink! contract, you can find
this info in the metadata generated by cargo-contract
.
When you do cargo +nightly contract build
you'll get something like:
Your contract artifacts are ready. You can find them in:
/private/tmp/flipper/target/ink
- flipper.contract (code + metadata)
- flipper.wasm (the contract's code)
- metadata.json (the contract's metadata)
In the metadata.json
file you'll find a section like this:
"messages": [
{
"args": [
{
"label": "foo",
"type": {
"displayName": [
"u32"
],
"type": 4
}
}
],
"docs": [],
"label": "flip",
"mutates": true,
"payable": false,
"returnType": null,
"selector": "0x633aa551"
}
]
Find the message you want to call and take a note of the selector bytes. If your function
has any arguments you'll also need to take a note of what those are.
Crafting a bare_call
The bare_call
function takes an input_data
buffer. This buffer needs to be of the
form:
[ MESSAGE_SELECTOR | SCALE_ENCODED_ARG_1 | ... | SCALE_ENCODED_ARG_N ]
We can craft this as follows:
let mut selector: Vec<u8> = [0x63, 0x3A, 0xA5, 0x51].into();
let mut message_arg = 15663040u32.encode();
let mut data = Vec::new();
data.append(&mut selector);
data.append(&mut message_arg);
Now, to put it call together. In our pallet we'll need a call
(so a function within the
#[pallet::call]
macro) which will itself call bare_call
:
pub fn call_contract(
origin: OriginFor<T>,
dest: T::AccountId, // <- This is the address of the deployed contract we're calling
) -> DispatchResult {
let who = ensure_signed(origin)?;
// Amount to transfer to the message. Not gonna transfer anything here, so we'll
// leave this as `0`.
let value: BalanceOf<T> = Default::default();
// You'll have to play around with this depending on your contract. I don't recommend
// hardcoding it but for demo purposes this'll do the trick
let gas_limit = 10_000_000_000;
let debug = false;
// Remember, we pulled this out from the `metadata.json` file.
//
// Again, probably shouldn't be hardcoded but :shrug:
let mut selector: Vec<u8> = [0x63, 0x3A, 0xA5, 0x51].into();
let mut message_arg = 15663040u32.encode();
let mut data = Vec::new();
data.append(&mut selector);
data.append(&mut message_arg);
// This ends up calling our contract!
pallet_contracts::Pallet::<T>::bare_call(
who,
dest.clone(),
value,
gas_limit,
data,
debug,
)
.result?;
Ok(())
}
Further Reading