-
Notifications
You must be signed in to change notification settings - Fork 578
Expand file tree
/
Copy pathCCIPBatcher.sol
More file actions
executable file
·246 lines (233 loc) · 9.95 KB
/
Copy pathCCIPBatcher.sol
File metadata and controls
executable file
·246 lines (233 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {IBatchGateway} from "./IBatchGateway.sol";
import {CCIPReader, EIP3668, OffchainLookup} from "./CCIPReader.sol";
/// @dev CCIP-Read batch gateway client implementation.
///
/// Since requests are read-only, empty responses are considered an error.
///
/// Usage: `ccipRead(address(this), abi.encodeCall(this.ccipBatch, (createBatch(...))), ...)`
///
abstract contract CCIPBatcher is CCIPReader {
/// @notice The batch gateway supplied an incorrect number of responses.
/// @dev Error selector: `0x4a5c31ea`
error InvalidBatchGatewayResponse();
/// @notice A batch gateway lookup failed with an unexpected selector.
/// @dev Error selector: `0x2fa87250`
error UnsafeBatchGatewayResponse(bytes);
uint256 constant FLAG_OFFCHAIN = 1 << 0; // the lookup reverted `OffchainLookup`
uint256 constant FLAG_CALL_ERROR = 1 << 1; // the initial call or callback reverted
uint256 constant FLAG_BATCH_ERROR = 1 << 2; // `OffchainLookup` failed on the batch gateway
uint256 constant FLAG_EMPTY_RESPONSE = 1 << 3; // the initial call or callback returned `0x`
uint256 constant FLAG_EIP140_BEFORE = 1 << 4; // does not have revert op code
uint256 constant FLAG_EIP140_AFTER = 1 << 5; // has revert op code
uint256 constant FLAG_DONE = 1 << 6; // the lookup has finished processing (private)
uint256 constant FLAGS_ANY_ERROR =
FLAG_CALL_ERROR | FLAG_BATCH_ERROR | FLAG_EMPTY_RESPONSE;
uint256 constant FLAGS_ANY_EIP140 = FLAG_EIP140_BEFORE | FLAG_EIP140_AFTER;
/// @dev An independent `OffchainLookup` session.
struct Lookup {
address target; // contract to call
bytes call; // initial calldata
bytes data; // response or error
uint256 flags; // see: FLAG_*
}
/// @dev A batch gateway session.
struct Batch {
Lookup[] lookups;
string[] gateways;
}
/// @dev Create a batch for a single target with multiple calls.
/// @param target The target contract.
/// @param calls The list of calldata.
/// @param gateways The batch gateway URLs.
function createBatch(
address target,
bytes[] memory calls,
string[] memory gateways
) internal pure returns (Batch memory) {
Lookup[] memory lookups = new Lookup[](calls.length);
for (uint256 i; i < calls.length; ++i) {
Lookup memory lu = lookups[i];
lu.target = target;
lu.call = calls[i];
}
return Batch(lookups, gateways);
}
/// @dev Use `ccipRead()` to call this function with a batch.
/// The callback response will be `abi.encode(batch)`.
function ccipBatch(
Batch memory batch
) external view returns (Batch memory) {
for (uint256 i; i < batch.lookups.length; ++i) {
Lookup memory lu = batch.lookups[i];
if ((lu.flags & FLAG_DONE) != 0) {
continue; // don't call a lookup that's already done
}
if ((lu.flags & FLAGS_ANY_EIP140) == 0) {
uint256 flags = detectEIP140(lu.target)
? FLAG_EIP140_AFTER
: FLAG_EIP140_BEFORE;
for (uint256 j = i; j < batch.lookups.length; ++j) {
if (batch.lookups[j].target == lu.target) {
batch.lookups[j].flags |= flags;
}
}
}
bool unsafe = (lu.flags & FLAG_EIP140_AFTER) == 0;
(bool ok, bytes memory v) = safeCall(!unsafe, lu.target, lu.call);
if (!ok && bytes4(v) == OffchainLookup.selector) {
lu.flags |= FLAG_OFFCHAIN;
} else {
lu.flags |= FLAG_DONE;
if (unsafe && v.length == 0) {
// unsafe contracts appear the same for throw and unimplemented fallback
// decision: interpret like an unimplemented function selector response
} else if (!ok) {
lu.flags |= FLAG_CALL_ERROR;
}
if (v.length == 0) {
lu.flags |= FLAG_EMPTY_RESPONSE;
}
}
lu.data = v;
}
_revertBatchGateway(batch); // reverts if any offchain
return batch;
}
/// @dev Check if the batch is "done". If not, revert `OffchainLookup` for batch gateway.
function _revertBatchGateway(Batch memory batch) internal view {
IBatchGateway.Request[] memory requests = new IBatchGateway.Request[](
batch.lookups.length
);
uint256 count;
for (uint256 i; i < batch.lookups.length; ++i) {
Lookup memory lu = batch.lookups[i];
if ((lu.flags & FLAG_DONE) == 0) {
EIP3668.Params memory p = decodeOffchainLookup(lu.data);
requests[count++] = IBatchGateway.Request(
p.sender,
p.urls,
p.callData
);
}
}
if (count > 0) {
assembly {
mstore(requests, count) // truncate to number of offchain requests
}
revert OffchainLookup(
address(this),
batch.gateways,
abi.encodeCall(IBatchGateway.query, (requests)),
this.ccipBatchCallback.selector,
abi.encode(batch)
);
}
}
/// @dev CCIP-Read callback for `ccipBatch()`.
/// Updates `batch` using the batch gateway response. Reverts again if not "done".
/// @param response The response from the batch gateway.
/// @param extraData The contextual data passed from `ccipBatch()`.
/// @return batch The batch where every lookup is "done".
function ccipBatchCallback(
bytes calldata response,
bytes calldata extraData
) external view returns (Batch memory batch) {
(bool[] memory failures, bytes[] memory responses) = abi.decode(
response,
(bool[], bytes[])
);
if (failures.length != responses.length) {
revert InvalidBatchGatewayResponse();
}
batch = abi.decode(extraData, (Batch));
uint256 expected;
for (uint256 i; i < batch.lookups.length; ++i) {
Lookup memory lu = batch.lookups[i];
if ((lu.flags & FLAG_DONE) == 0) {
if (expected < responses.length) {
bytes memory v = responses[expected];
if (failures[expected]) {
lu.flags |= FLAG_DONE | FLAG_BATCH_ERROR;
if (!_isSafeBatchGatewayError(bytes4(v))) {
v = abi.encodeWithSelector(
UnsafeBatchGatewayResponse.selector,
v
); // wrap unless safe
}
} else {
EIP3668.Params memory p = decodeOffchainLookup(lu.data);
bool ok;
// assumption: unsafe contracts don't revert OffchainLookup()
(ok, v) = p.sender.staticcall(
abi.encodeWithSelector(
p.callbackFunction,
v,
p.extraData
)
);
if (ok || bytes4(v) != OffchainLookup.selector) {
lu.flags |= FLAG_DONE;
// decision: promote empty response from the callback => call error
// ie. the initial function was implemented but the callback was not
// this can be detected via FLAG_OFFCHAIN
if (!ok || v.length == 0) {
lu.flags |= FLAG_CALL_ERROR;
}
if (v.length == 0) {
lu.flags |= FLAG_EMPTY_RESPONSE;
}
}
}
lu.data = v;
}
++expected;
}
}
if (expected != responses.length) {
revert InvalidBatchGatewayResponse();
}
_revertBatchGateway(batch);
}
/// @dev Determine if the batch gateway error is safe to propagate.
/// Note: `error OffchainLookup` should *NEVER* be considered safe.
function _isSafeBatchGatewayError(
bytes4 selector
) internal view virtual returns (bool) {
return
selector == IBatchGateway.HttpError.selector ||
selector == 0x08c379a0; // Error(string)
}
/// @dev Safely collapse `Lookup[]` into `bytes[]`.
/// If `FLAGS_ANY_ERROR` and response is non-empty, the response is zero-padded so that `length % 32 == 4`.
/// @param lookups Array of completed lookups.
/// @param wrapped If `true`, successful responses are unwrapped as `bytes`.
/// @return arr Array of call responses.
function _toResponseArray(
Lookup[] memory lookups,
bool wrapped
) internal pure returns (bytes[] memory arr) {
arr = new bytes[](lookups.length);
for (uint256 i; i < lookups.length; ++i) {
Lookup memory lu = lookups[i];
bytes memory v = lu.data;
if ((lu.flags & FLAGS_ANY_ERROR) == 0) {
if (wrapped) {
v = abi.decode(v, (bytes));
}
} else if (v.length != 0) {
// force pad error response to length mod 32 == 4
// prevents unverified data from passing as valid response
unchecked {
uint256 pad = (4 - v.length) & 31;
if (pad > 0) {
v = abi.encodePacked(v, new bytes(pad));
}
}
}
arr[i] = v;
}
return arr;
}
}