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:
  1. 1.
    The sender calls batchTransfer at the source chain, which internally calls app framework's sendMessageWithTransfer to send tokens and a message specifying a list of <receivers, amounts> to the app contract at the destination chain.
  2. 2.
    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 calls sendMessage to send a receipt back to the source chain app contract.
  3. 3.
    The sender side implements the executeMessage interface to handle the receipt message.
1
contract BatchTransfer is MessageApp {
2
using SafeERC20 for IERC20;
3
4
struct TransferRequest {
5
uint64 nonce;
6
address[] accounts;
7
uint256[] amounts;
8
address sender;
9
}
10
11
enum TransferStatus {
12
Null,
13
Success,
14
Fail
15
}
16
17
struct TransferReceipt {
18
uint64 nonce;
19
TransferStatus status;
20
}
21
22
constructor(address _messageBus) MessageApp(_messageBus) {}
23
24
// ============== functions and states on source chain ==============
25
26
uint64 nonce;
27
28
struct BatchTransferStatus {
29
bytes32 h; // hash(receiver, dstChainId)
30
TransferStatus status;
31
}
32
// nonce -> BatchTransferStatus
33
mapping(uint64 => BatchTransferStatus) public status;
34
35
modifier onlyEOA() {
36
require(msg.sender == tx.origin, "Not EOA");
37
_;
38
}
39
40
// called by sender on source chain to send tokens to a list of
41
// <_accounts, _amounts> on the destination chain
42
function batchTransfer(
43
address _dstContract, // BatchTransfer contract address at the dst chain
44
address _token,
45
uint256 _amount,
46
uint64 _dstChainId,
47
uint32 _maxSlippage,
48
MsgDataTypes.BridgeSendType _bridgeSendType,
49
address[] calldata _accounts,
50
uint256[] calldata _amounts
51
) external payable onlyEOA {
52
uint256 totalAmt;
53
for (uint256 i = 0; i < _amounts.length; i++) {
54
totalAmt += _amounts[i];
55
}
56
uint256 minRecv = _amount - (_amount * _maxSlippage) / 1e6;
57
require(minRecv > totalAmt, "invalid maxSlippage");
58
nonce += 1;
59
status[nonce] = BatchTransferStatus({
60
h: keccak256(abi.encodePacked(_dstContract, _dstChainId)),
61
status: TransferStatus.Null
62
});
63
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
64
bytes memory message = abi.encode(
65
TransferRequest({
66
nonce: nonce,
67
accounts: _accounts,
68
amounts: _amounts,
69
sender: msg.sender
70
})
71
);
72
// send token and message to the destination chain
73
sendMessageWithTransfer(
74
_dstContract,
75
_token,
76
_amount,
77
_dstChainId,
78
nonce,
79
_maxSlippage,
80
message,
81
_bridgeSendType,
82
msg.value
83
);
84
}
85
86
// called by MessageBus on the source chain to handle token transfer failures
87
// (e.g., due to bad slippage).
88
// the associated token transfer is guaranteed to have already been refunded
89
function executeMessageWithTransferRefund(
90
address _token,
91
uint256 _amount,
92
bytes calldata _message,
93
address // executor
94
) external payable override onlyMessageBus returns (ExecutionStatus) {
95
TransferRequest memory transfer = abi.decode(
96
(_message),
97
(TransferRequest)
98
);
99
IERC20(_token).safeTransfer(transfer.sender, _amount);
100
return ExecutionStatus.Success;
101
}
102
103
// called by MessageBus on the source chain to receive receipts
104
function executeMessage(
105
address _sender,
106
uint64 _srcChainId,
107
bytes memory _message,
108
address // executor
109
) external payable override onlyMessageBus returns (ExecutionStatus) {
110
TransferReceipt memory receipt = abi.decode(
111
(_message),
112
(TransferReceipt)
113
);
114
require(
115
status[receipt.nonce].h ==
116
keccak256(abi.encodePacked(_sender, _srcChainId)),
117
"invalid message"
118
);
119
status[receipt.nonce].status = receipt.status;
120
return ExecutionStatus.Success;
121
}
122
123
// ============== functions on destination chain ==============
124
125
// called by MessageBus on destination chain to handle batchTransfer message by
126
// distributing tokens to receivers and sending receipt.
127
// the lump sum token transfer associated with the message is guaranteed to have
128
// already been received.
129
function executeMessageWithTransfer(
130
address _srcContract,
131
address _token,
132
uint256 _amount,
133
uint64 _srcChainId,
134
bytes memory _message,
135
address // executor
136
) external payable override onlyMessageBus returns (ExecutionStatus) {
137
TransferRequest memory transfer = abi.decode(
138
(_message),
139
(TransferRequest)
140
);
141
uint256 totalAmt;
142
for (uint256 i = 0; i < transfer.accounts.length; i++) {
143
IERC20(_token).safeTransfer(
144
transfer.accounts[i],
145
transfer.amounts[i]
146
);
147
totalAmt += transfer.amounts[i];
148
}
149
uint256 remainder = _amount - totalAmt;
150
if (_amount > totalAmt) {
151
// transfer the remainder of the money to the sender as a fee for
152
// executing this transfer
153
IERC20(_token).safeTransfer(transfer.sender, remainder);
154
}
155
bytes memory message = abi.encode(
156
TransferReceipt({
157
nonce: transfer.nonce,
158
status: TransferStatus.Success
159
})
160
);
161
// send receipt back to the source chain contract
162
sendMessage(_srcContract, _srcChainId, message, msg.value);
163
return ExecutionStatus.Success;
164
}
165
166
// called by MessageBus if handleMessageWithTransfer above got reverted
167
function executeMessageWithTransferFallback(
168
address _srcContract,
169
address _token,
170
uint256 _amount,
171
uint64 _srcChainId,
172
bytes memory _message,
173
address // executor
174
) external payable override onlyMessageBus returns (ExecutionStatus) {
175
TransferRequest memory transfer = abi.decode(
176
(_message),
177
(TransferRequest)
178
);
179
IERC20(_token).safeTransfer(transfer.sender, _amount);
180
bytes memory message = abi.encode(
181
TransferReceipt({
182
nonce: transfer.nonce,
183
status: TransferStatus.Fail
184
})
185
);
186
// send receipt back to the source chain contract
187
sendMessage(_srcContract, _srcChainId, message, msg.value);
188
return ExecutionStatus.Success;
189
}
190
}