Batch Transfer
Here is an example app that sends tokens from one sender at the source chain to multiple receivers at the destination chain through a single cross-chain token transfer.
Source code at GitHub. The high-level workflow consists of three steps:
The sender calls
batchTransfer
at the source chain, which internally calls app framework'ssendMessageWithTransfer
to send tokens and a message specifying a list of <receivers, amounts> to the app contract at the destination chain.The receiver side implements the
executeMessageWithTransfer
interface to handle the batch transfer message, and distribute received tokens according to the message content. It also internally callssendMessage
to send a receipt back to the source chain app contract.The sender side implements the
executeMessage
interface to handle the receipt message.
contract BatchTransfer is MessageApp {
using SafeERC20 for IERC20;
struct TransferRequest {
uint64 nonce;
address[] accounts;
uint256[] amounts;
address sender;
}
enum TransferStatus {
Null,
Success,
Fail
}
struct TransferReceipt {
uint64 nonce;
TransferStatus status;
}
constructor(address _messageBus) MessageApp(_messageBus) {}
// ============== functions and states on source chain ==============
uint64 nonce;
struct BatchTransferStatus {
bytes32 h; // hash(receiver, dstChainId)
TransferStatus status;
}
// nonce -> BatchTransferStatus
mapping(uint64 => BatchTransferStatus) public status;
modifier onlyEOA() {
require(msg.sender == tx.origin, "Not EOA");
_;
}
// called by sender on source chain to send tokens to a list of
// <_accounts, _amounts> on the destination chain
function batchTransfer(
address _dstContract, // BatchTransfer contract address at the dst chain
address _token,
uint256 _amount,
uint64 _dstChainId,
uint32 _maxSlippage,
MsgDataTypes.BridgeSendType _bridgeSendType,
address[] calldata _accounts,
uint256[] calldata _amounts
) external payable onlyEOA {
uint256 totalAmt;
for (uint256 i = 0; i < _amounts.length; i++) {
totalAmt += _amounts[i];
}
uint256 minRecv = _amount - (_amount * _maxSlippage) / 1e6;
require(minRecv > totalAmt, "invalid maxSlippage");
nonce += 1;
status[nonce] = BatchTransferStatus({
h: keccak256(abi.encodePacked(_dstContract, _dstChainId)),
status: TransferStatus.Null
});
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
bytes memory message = abi.encode(
TransferRequest({
nonce: nonce,
accounts: _accounts,
amounts: _amounts,
sender: msg.sender
})
);
// send token and message to the destination chain
sendMessageWithTransfer(
_dstContract,
_token,
_amount,
_dstChainId,
nonce,
_maxSlippage,
message,
_bridgeSendType,
msg.value
);
}
// called by MessageBus on the source chain to handle token transfer failures
// (e.g., due to bad slippage).
// the associated token transfer is guaranteed to have already been refunded
function executeMessageWithTransferRefund(
address _token,
uint256 _amount,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferRequest memory transfer = abi.decode(
(_message),
(TransferRequest)
);
IERC20(_token).safeTransfer(transfer.sender, _amount);
return ExecutionStatus.Success;
}
// called by MessageBus on the source chain to receive receipts
function executeMessage(
address _sender,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferReceipt memory receipt = abi.decode(
(_message),
(TransferReceipt)
);
require(
status[receipt.nonce].h ==
keccak256(abi.encodePacked(_sender, _srcChainId)),
"invalid message"
);
status[receipt.nonce].status = receipt.status;
return ExecutionStatus.Success;
}
// ============== functions on destination chain ==============
// called by MessageBus on destination chain to handle batchTransfer message by
// distributing tokens to receivers and sending receipt.
// the lump sum token transfer associated with the message is guaranteed to have
// already been received.
function executeMessageWithTransfer(
address _srcContract,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferRequest memory transfer = abi.decode(
(_message),
(TransferRequest)
);
uint256 totalAmt;
for (uint256 i = 0; i < transfer.accounts.length; i++) {
IERC20(_token).safeTransfer(
transfer.accounts[i],
transfer.amounts[i]
);
totalAmt += transfer.amounts[i];
}
uint256 remainder = _amount - totalAmt;
if (_amount > totalAmt) {
// transfer the remainder of the money to the sender as a fee for
// executing this transfer
IERC20(_token).safeTransfer(transfer.sender, remainder);
}
bytes memory message = abi.encode(
TransferReceipt({
nonce: transfer.nonce,
status: TransferStatus.Success
})
);
// send receipt back to the source chain contract
sendMessage(_srcContract, _srcChainId, message, msg.value);
return ExecutionStatus.Success;
}
// called by MessageBus if handleMessageWithTransfer above got reverted
function executeMessageWithTransferFallback(
address _srcContract,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferRequest memory transfer = abi.decode(
(_message),
(TransferRequest)
);
IERC20(_token).safeTransfer(transfer.sender, _amount);
bytes memory message = abi.encode(
TransferReceipt({
nonce: transfer.nonce,
status: TransferStatus.Fail
})
);
// send receipt back to the source chain contract
sendMessage(_srcContract, _srcChainId, message, msg.value);
return ExecutionStatus.Success;
}
}
Last updated