πŸ’‘
Integration Guide: Advanced
If you haven't checked out the basic integration guide yet, please do so and make sure you understand the role of each component in the system before proceeding in this guide.

Introduction

In the basic guide, we walked through a minimal implementation of a cross-chain app, but it's hardly production ready. In this guide, we are going to discuss the following topics. The goal is to get you onboard with the patterns involved in deploying a robust service on top of the Celer IM infrastructure.
  • Enhance security through executor's "sender groups" config
  • Dealing with bridge failures (refunds)
  • Dealing with failed executions on destination chain
  • Chaining messages

Not So Simple BatchTransfer

Let's look at a more sophisticated version of BatchTransfer.sol and talk about the above topics.

Enhance security through executor's "sender groups" config

Under the current IM architecture, any contracts can send messages to any other contracts. This means that a malicious party can forge messages that conform to your contracts' message data type, send it to your contract, and exhaust your executor's gas fund. Thus, in production, it is important that executor checks where a message is originated from. Sender groups are designed just for that.
If you have experience with cloud services such as AWS, your might recognize that a sender group is pretty much a "security group".
An example sender group looks like this
1
[[service.contract_sender_groups]]
2
# the name/ID of the group. service.contracts refer to a sender group in allow_sender_groups
3
name = "app-contract-address"
4
allow = [
5
# allow and execute messages originated from <app-contract-address> on chain 1
6
{ chain_id = 5, address = "<app-contract-address>" },
7
# allow and execute messages originated from <app-contract-address> on chain 56
8
{ chain_id = 97, address = "<app-contract-address>" },
9
]
Copied!
After defining the security groups, we need to mount it to individual contract configs
1
[[service]]
2
[[service.contracts]]
3
chain_id = 5
4
address = "<app-contract-address>"
5
[[service.contracts]]
6
chain_id = 97
7
address = "<app-contract-address>"
Copied!

Dealing with Bridge Failures

Contract Changes

It is possible that bridging would fail when the user calls batchTransfer due to high slippage, not enough liquidity, etc. In These cases, executor automatically prepares a refund and executes it on the source chain. In order for this to work, the BatchTransfer contract on the source chain needs to implement executeMessageWithTransferRefund, so let's add it:
1
function executeMessageWithTransferRefund(
2
address _token,
3
uint256 _amount,
4
bytes calldata _message
5
) external payable override onlyMessageBus returns (ExecutionStatus) {
6
TransferRequest memory transfer = abi.decode((_message), (TransferRequest));
7
IERC20(_token).safeTransfer(transfer.sender, _amount);
8
return ExecutionStatus.Success;
9
}
Copied!
Note that this function is called with the original _message we encoded and sent out. And the funds are guaranteed to arrive before it is called.

Executor Changes

Executor has an option that you need to explicitly turn on to enable auto refund for bridge failures.
1
# executor.toml
2
[service]
3
enable_auto_refund = true
Copied!

Dealing with Failed Executions on the Destination Chain

Another point of failure is in our BatchTransfer contract on the destination chain. For whatever reason if the transaction reverts inside the app contract, we may want to decide what to do with the received funds in a fallback function.
1
function executeMessageWithTransferFallback(
2
address _sender,
3
address _token,
4
uint256 _amount,
5
uint64 _srcChainId,
6
bytes memory _message
7
) external payable override onlyMessageBus returns (ExecutionStatus) {
8
TransferRequest memory transfer = abi.decode((_message), (TransferRequest));
9
IERC20(_token).safeTransfer(transfer.sender, _amount);
10
bytes memory message = abi.encode(TransferReceipt({nonce: transfer.nonce, status: TransferStatus.Fail}));
11
sendMessage(_sender, _srcChainId, message, msg.value);
12
return ExecutionStatus.Success;
13
}
Copied!
Note the params of this function is exactly the same as the ones of executeMessageWithTransfer. If the execution of this function also fails, the message execution then enters a "failed" status, and the funds will be locked in the BatchTransfer contract.

Chaining Messages

You may want to "chain" or "nest" a message in executeMessageWithTransfer on the destination chain. Since the executeMessageWithTransfer interface is payable, this usage is supported.
In the BatchTransfer contract, a "receipt" message is chained inside executeMessageWithTransfer.
1
bytes memory message = abi.encode(TransferReceipt({nonce: transfer.nonce, status: TransferStatus.Success}));
2
sendMessage(_sender, _srcChainId, message, msg.value);
Copied!
Now we need to configure the executor to add a payable value when calling executeMessageWithTransfer to cover the fee introduced by chaining the additional message.
1
[service]
2
[[service.contracts]]
3
chain_id = 5 # Goerli
4
address = "0x09E4534B11D400BFcd2026b69E399763CeAfB42D"
5
add_payable_value_for_execution = 20000000000 # <-- add this line, amount in wei
6
[[service.contracts]]
7
chain_id = 97 # Bsc testnet
8
address = "0x570F9c2f224b002d75F287f5430Bc9598E850E13"
9
add_payable_value_for_execution = 20000000000 # <-- add this line, amount in wei
Copied!
But how do we know how much fee is needed? This is the tricky part since we can only estimate the amount of fee our sendMessage()call is incurring. Let's take a look at how the message fee is calculated in the MessageBus contract:
1
uint256 public feeBase;
2
uint256 public feePerByte;
3
​
4
function calcFee(bytes calldata _message) public view returns (uint256) {
5
return feeBase + _message.length * feePerByte;
6
}
Copied!
You can query the MessageBus contract on a chain for these parameters. If you use abi.encode to encode your message, the message length is likely fixed. If you happen to have variable length fields in your message, you should add a safe margin to the add_payable_value_for_execution to reduce the chance of having message execution reverted.