Skip to main content

If you already read the previous tutorials you already know how to use zEVM. A very common use case on zEVM is a smart contract with a single input from one chain, do some magic, and then execute the output to another or multiple chains.

In this tutorial we will show you how to do that, leaving an empty place to write your own code.

ZEVMSwapApp.sol
interface ZetaMultiOutputErrors {
error NoAvailableTransfers();
}
contract ZetaMultiOutput is zContract, Ownable, ZetaMultiOutputErrors {
SystemContract public immutable systemContract;
address[] public destinationTokens;
//...
event DestinationRegistered(address);
event Withdrawal(address, uint256, address);
//...
constructor(address systemContractAddress) {
systemContract = SystemContract(systemContractAddress);
}
//...
function registerDestinationToken(address destinationToken) external onlyOwner {
destinationTokens.push(destinationToken);
emit DestinationRegistered(destinationToken);
}
//...
function _getTotalTransfers(address zrc20) internal view returns (uint256) {
uint256 total = 0;
for (uint256 i; i < destinationTokens.length; i++) {
if (destinationTokens[i] == zrc20) continue;
total++;
}
return total;
}
//...
function onCrossChainCall(address zrc20, uint256 amount, bytes calldata message) external virtual override {
if (_getTotalTransfers(zrc20) == 0) revert NoAvailableTransfers();
address receipient = BytesHelperLib.bytesToAddress(message, 0);
// todo: add your own code here decoding message with the format you decide
uint256 amountToTransfer = amount / _getTotalTransfers(zrc20);
uint256 leftOver = amount - amountToTransfer * _getTotalTransfers(zrc20);
uint256 lastTransferIndex = destinationTokens[destinationTokens.length - 1] == zrc20
? destinationTokens.length - 2
: destinationTokens.length - 1;
for (uint256 i; i < destinationTokens.length; i++) {
address targetZRC20 = destinationTokens[i];
if (targetZRC20 == zrc20) continue;
if (lastTransferIndex == i) {
amountToTransfer += leftOver;
}
uint256 outputAmount = SwapHelperLib._doSwap(
systemContract.wZetaContractAddress(),
systemContract.uniswapv2FactoryAddress(),
systemContract.uniswapv2Router02Address(),
zrc20,
amountToTransfer,
targetZRC20,
0
);
SwapHelperLib._doWithdrawal(targetZRC20, outputAmount, BytesHelperLib.addressToBytes(receipient));
emit Withdrawal(targetZRC20, outputAmount, receipient);
}
}
}

If you need the helpers we use in this example you can find it in the Helpers Section.

What does this contract do? At the beginning reads an address from message, and then send some tokens to that address in several chains.

This contract shows a fundamental building block for your smart contract. You can complete the contract in the todo section and change the final transfer to your own needs.