Skip to content

Commit 2f231fb

Browse files
authored
UniversalResolver w/CCIPBatcher (#421)
BET-318, BET-193
1 parent f697e79 commit 2f231fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3248
-2088
lines changed

.prettierrc.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
"tabWidth": 4,
1515
"useTabs": false,
1616
"singleQuote": false,
17-
"bracketSpacing": false,
18-
"explicitTypes": "always"
17+
"bracketSpacing": false
1918
}
2019
}
2120
]

contracts/ccipRead/CCIPBatcher.sol

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
import {IBatchGateway} from "./IBatchGateway.sol";
5+
import {CCIPReader, EIP3668, OffchainLookup} from "./CCIPReader.sol";
6+
7+
contract CCIPBatcher is CCIPReader {
8+
/// @dev The batch gateway supplied an incorrect number of responses.
9+
error InvalidBatchGatewayResponse();
10+
11+
uint256 constant FLAG_OFFCHAIN = 1 << 0; // the lookup reverted `OffchainLookup`
12+
uint256 constant FLAG_CALL_ERROR = 1 << 1; // the initial call or callback reverted
13+
uint256 constant FLAG_BATCH_ERROR = 1 << 2; // `OffchainLookup` failed on the batch gateway
14+
uint256 constant FLAG_EMPTY_RESPONSE = 1 << 3; // the initial call or callback returned `0x`
15+
uint256 constant FLAG_EIP140_BEFORE = 1 << 4; // does not have revert op code
16+
uint256 constant FLAG_EIP140_AFTER = 1 << 5; // has revert op code
17+
uint256 constant FLAG_DONE = 1 << 6; // the lookup has finished processing (private)
18+
19+
uint256 constant FLAGS_ANY_ERROR =
20+
FLAG_CALL_ERROR | FLAG_BATCH_ERROR | FLAG_EMPTY_RESPONSE;
21+
uint256 constant FLAGS_ANY_EIP140 = FLAG_EIP140_BEFORE | FLAG_EIP140_AFTER;
22+
23+
/// @dev An independent `OffchainLookup` session.
24+
struct Lookup {
25+
address target; // contract to call
26+
bytes call; // initial calldata
27+
bytes data; // response or error
28+
uint256 flags; // see: FLAG_*
29+
}
30+
31+
/// @dev A batch gateway session.
32+
struct Batch {
33+
Lookup[] lookups;
34+
string[] gateways;
35+
}
36+
37+
/// @dev Use `CCIPReader.ccipRead()` to call this function with a batch.
38+
/// The callback `response` will be `abi.encode(batch)`.
39+
function ccipBatch(
40+
Batch memory batch
41+
) external view returns (Batch memory) {
42+
for (uint256 i; i < batch.lookups.length; i++) {
43+
Lookup memory lu = batch.lookups[i];
44+
if ((lu.flags & FLAGS_ANY_EIP140) == 0) {
45+
uint256 flags = _detectEIP140(lu.target)
46+
? FLAG_EIP140_AFTER
47+
: FLAG_EIP140_BEFORE;
48+
for (uint256 j = i; j < batch.lookups.length; j++) {
49+
if (batch.lookups[j].target == lu.target) {
50+
batch.lookups[j].flags |= flags;
51+
}
52+
}
53+
}
54+
bool old = (lu.flags & FLAG_EIP140_AFTER) == 0;
55+
(bool ok, bytes memory v) = _safeCall(!old, lu.target, lu.call);
56+
if (ok || (old && v.length == 0)) {
57+
lu.flags |= FLAG_DONE;
58+
if (v.length == 0) {
59+
v = abi.encodePacked(bytes4(lu.call));
60+
lu.flags |= FLAG_EMPTY_RESPONSE;
61+
}
62+
} else if (bytes4(v) == OffchainLookup.selector) {
63+
lu.flags |= FLAG_OFFCHAIN;
64+
} else {
65+
lu.flags |= FLAG_DONE | FLAG_CALL_ERROR;
66+
}
67+
lu.data = v;
68+
}
69+
_revertBatchGateway(batch); // reverts if any offchain
70+
return batch;
71+
}
72+
73+
/// @dev Check if the batch is "done". If not, revert `OffchainLookup` for batch gateway.
74+
function _revertBatchGateway(Batch memory batch) internal view {
75+
IBatchGateway.Request[] memory requests = new IBatchGateway.Request[](
76+
batch.lookups.length
77+
);
78+
uint256 count;
79+
for (uint256 i; i < batch.lookups.length; i++) {
80+
Lookup memory lu = batch.lookups[i];
81+
if ((lu.flags & FLAG_DONE) == 0) {
82+
EIP3668.Params memory p = decodeOffchainLookup(lu.data);
83+
requests[count++] = IBatchGateway.Request(
84+
p.sender,
85+
p.urls,
86+
p.callData
87+
);
88+
}
89+
}
90+
if (count > 0) {
91+
assembly {
92+
mstore(requests, count) // truncate to number of offchain requests
93+
}
94+
revert OffchainLookup(
95+
address(this),
96+
batch.gateways,
97+
abi.encodeCall(IBatchGateway.query, (requests)),
98+
this.ccipBatchCallback.selector,
99+
abi.encode(batch)
100+
);
101+
}
102+
}
103+
104+
/// @dev CCIP-Read callback for `ccipBatch()`.
105+
/// Updates `batch` using the batch gateway response. Reverts again if not "done".
106+
/// @param response The response from the batch gateway.
107+
/// @param extraData The contextual data passed from `ccipBatch()`.
108+
/// @return batch The batch where every lookup is "done".
109+
function ccipBatchCallback(
110+
bytes calldata response,
111+
bytes calldata extraData
112+
) external view returns (Batch memory batch) {
113+
(bool[] memory failures, bytes[] memory responses) = abi.decode(
114+
response,
115+
(bool[], bytes[])
116+
);
117+
if (failures.length != responses.length) {
118+
revert InvalidBatchGatewayResponse();
119+
}
120+
batch = abi.decode(extraData, (Batch));
121+
uint256 expected;
122+
for (uint256 i; i < batch.lookups.length; i++) {
123+
Lookup memory lu = batch.lookups[i];
124+
if ((lu.flags & FLAG_DONE) == 0) {
125+
if (expected < responses.length) {
126+
bytes memory v = responses[expected];
127+
if (failures[expected]) {
128+
lu.flags |= FLAG_DONE | FLAG_BATCH_ERROR;
129+
} else {
130+
EIP3668.Params memory p = decodeOffchainLookup(lu.data);
131+
bool ok;
132+
(ok, v) = p.sender.staticcall(
133+
abi.encodeWithSelector(
134+
p.callbackFunction,
135+
v,
136+
p.extraData
137+
)
138+
);
139+
if (ok) {
140+
lu.flags |= FLAG_DONE;
141+
if (v.length == 0) {
142+
v = abi.encodePacked(p.callbackFunction);
143+
lu.flags |= FLAG_EMPTY_RESPONSE;
144+
}
145+
} else if (bytes4(v) != OffchainLookup.selector) {
146+
lu.flags |= FLAG_DONE | FLAG_CALL_ERROR;
147+
}
148+
}
149+
lu.data = v;
150+
}
151+
++expected;
152+
}
153+
}
154+
if (expected != responses.length) {
155+
revert InvalidBatchGatewayResponse();
156+
}
157+
_revertBatchGateway(batch);
158+
}
159+
}

contracts/ccipRead/CCIPReader.sol

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
/// @author Modified from https://github.com/unruggable-labs/CCIPReader.sol/blob/341576fe7ff2b6e0c93fc08f37740cf6439f5873/contracts/CCIPReader.sol
5+
6+
/// MIT License
7+
/// Portions Copyright (c) 2025 Unruggable
8+
/// Portions Copyright (c) 2025 ENS Labs Ltd
9+
10+
/// @dev Instructions:
11+
/// 1. inherit this contract
12+
/// 2. call `ccipRead()` similar to `staticcall()`
13+
/// 3. do not put logic after this invocation
14+
/// 4. implement all response logic in callback
15+
/// 5. ensure that return type of calling function == callback function
16+
17+
import {EIP3668, OffchainLookup} from "./EIP3668.sol";
18+
import {BytesUtils} from "../utils/BytesUtils.sol";
19+
20+
contract CCIPReader {
21+
/// @dev A recursive CCIP-Read session.
22+
struct Context {
23+
address target;
24+
bytes4 callbackFunction;
25+
bytes extraData;
26+
bytes4 myCallbackFunction;
27+
bytes myExtraData;
28+
}
29+
30+
/// @dev Special-purpose value for identity callback: `f(x) = x`.
31+
bytes4 constant IDENTITY_FUNCTION = bytes4(0);
32+
33+
/// @dev Same as `ccipRead()` but the callback function is the identity.
34+
function ccipRead(address target, bytes memory call) internal view {
35+
ccipRead(target, call, IDENTITY_FUNCTION, "");
36+
}
37+
38+
/// @dev Performs a CCIP-Read and handles internal recursion.
39+
/// Reverts `OffchainLookup` if necessary.
40+
/// @param target The contract address.
41+
/// @param call The calldata to `staticcall()` on `target`.
42+
/// @param callbackFunction The function selector of callback.
43+
/// @param extraData The contextual data relayed to `callbackFunction`.
44+
function ccipRead(
45+
address target,
46+
bytes memory call,
47+
bytes4 callbackFunction,
48+
bytes memory extraData
49+
) internal view {
50+
// We call the intended function that **could** revert with an `OffchainLookup`
51+
// We destructure the response into an execution status bool and our return bytes
52+
(bool ok, bytes memory v) = _safeCall(
53+
_detectEIP140(target),
54+
target,
55+
call
56+
);
57+
// IF the function reverted with an `OffchainLookup`
58+
if (!ok && bytes4(v) == OffchainLookup.selector) {
59+
// We decode the response error into a tuple
60+
// tuples allow flexibility noting stack too deep constraints
61+
EIP3668.Params memory p = decodeOffchainLookup(v);
62+
if (p.sender == target) {
63+
// We then wrap the error data in an `OffchainLookup` sent/'owned' by this contract
64+
revert OffchainLookup(
65+
address(this),
66+
p.urls,
67+
p.callData,
68+
this.ccipReadCallback.selector,
69+
abi.encode(
70+
Context(
71+
target,
72+
p.callbackFunction,
73+
p.extraData,
74+
callbackFunction,
75+
extraData
76+
)
77+
)
78+
);
79+
}
80+
}
81+
// IF we have gotten here, the 'real' target does not revert with an `OffchainLookup` error
82+
if (ok && callbackFunction != IDENTITY_FUNCTION) {
83+
// The exit point of this architecture is OUR callback in the 'real'
84+
// We pass through the response to that callback
85+
(ok, v) = address(this).staticcall(
86+
abi.encodeWithSelector(callbackFunction, v, extraData)
87+
);
88+
}
89+
// OR the call to the 'real' target reverts with a different error selector
90+
// OR the call to OUR callback reverts with ANY error selector
91+
if (ok) {
92+
assembly {
93+
return(add(v, 32), mload(v))
94+
}
95+
} else {
96+
assembly {
97+
revert(add(v, 32), mload(v))
98+
}
99+
}
100+
}
101+
102+
/// @dev CCIP-Read callback for `ccipRead()`.
103+
/// @param response The response from offchain.
104+
/// @param extraData The contextual data passed from `ccipRead()`.
105+
/// @dev The return type of this function is polymorphic depending on the caller.
106+
function ccipReadCallback(
107+
bytes memory response,
108+
bytes memory extraData
109+
) external view {
110+
Context memory ctx = abi.decode(extraData, (Context));
111+
// Since the callback can revert too (but has the same return structure)
112+
// We can reuse the calling infrastructure to call the callback
113+
ccipRead(
114+
ctx.target,
115+
abi.encodeWithSelector(
116+
ctx.callbackFunction,
117+
response,
118+
ctx.extraData
119+
),
120+
ctx.myCallbackFunction,
121+
ctx.myExtraData
122+
);
123+
}
124+
125+
/// @dev Decode `OffchainLookup` error data into a struct.
126+
/// @param v The error data of the revert.
127+
/// @return p The decoded `OffchainLookup` params.
128+
function decodeOffchainLookup(
129+
bytes memory v
130+
) internal pure returns (EIP3668.Params memory p) {
131+
p = EIP3668.decode(BytesUtils.substring(v, 4, v.length - 4));
132+
}
133+
134+
/// @dev Determine if `target` uses `revert()` instead of `invalid()`.
135+
// Assumption: only newer contracts revert `OffchainLookup`.
136+
/// @param target The contract to test.
137+
/// @return safe True if safe to call.
138+
function _detectEIP140(address target) internal view returns (bool safe) {
139+
if (target == address(this)) return true;
140+
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-140.md
141+
assembly {
142+
let G := 5000
143+
let g := gas()
144+
pop(staticcall(G, target, 0, 0, 0, 0))
145+
safe := lt(sub(g, gas()), G)
146+
}
147+
}
148+
149+
/// @dev Same as `staticcall()` but prevents OOG when not `safe`.
150+
function _safeCall(
151+
bool safe,
152+
address target,
153+
bytes memory call
154+
) internal view returns (bool ok, bytes memory v) {
155+
(ok, v) = target.staticcall{gas: safe ? gasleft() : 50000}(call);
156+
}
157+
}

contracts/ccipRead/EIP3668.sol

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
/// @dev https://eips.ethereum.org/EIPS/eip-3668
5+
/// Error selector: `0x556f1830`
6+
error OffchainLookup(
7+
address sender,
8+
string[] urls,
9+
bytes callData,
10+
bytes4 callbackFunction,
11+
bytes extraData
12+
);
13+
14+
/// @dev Simple library for decoding `OffchainLookup` error data.
15+
/// Avoids "stack too deep" issues as the natural decoding consumes 5 variables.
16+
library EIP3668 {
17+
/// @dev Struct with members matching `OffchainLookup`.
18+
struct Params {
19+
address sender;
20+
string[] urls;
21+
bytes callData;
22+
bytes4 callbackFunction;
23+
bytes extraData;
24+
}
25+
26+
/// @dev Decode an `OffchainLookup` into a struct from the data after the error selector.
27+
function decode(bytes memory v) internal pure returns (Params memory p) {
28+
(p.sender, p.urls, p.callData, p.callbackFunction, p.extraData) = abi
29+
.decode(v, (address, string[], bytes, bytes4, bytes));
30+
}
31+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
interface IBatchGateway {
5+
struct Request {
6+
address sender;
7+
string[] urls;
8+
bytes data;
9+
}
10+
11+
function query(
12+
Request[] memory
13+
) external view returns (bool[] memory failures, bytes[] memory responses);
14+
}

contracts/reverseRegistrar/ReverseRegistrar.sol

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,7 @@ contract ReverseRegistrar is Ownable, Controllable, IReverseRegistrar {
149149
assembly {
150150
for {
151151
let i := 40
152-
} gt(i, 0) {
153-
154-
} {
152+
} gt(i, 0) {} {
155153
i := sub(i, 1)
156154
mstore8(i, byte(and(addr, 0xf), lookup))
157155
addr := div(addr, 0x10)

contracts/test/TestBytesUtils.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ contract TestBytesUtils {
135135
"Compare long char with difference at start"
136136
);
137137
require(
138-
abi.encodePacked(type(int256).min).compare(abi.encodePacked(type(int256).max)) > 0,
138+
abi.encodePacked(type(int256).min).compare(
139+
abi.encodePacked(type(int256).max)
140+
) > 0,
139141
"Compare maximum difference"
140142
);
141143
}

0 commit comments

Comments
 (0)