-
Notifications
You must be signed in to change notification settings - Fork 578
Expand file tree
/
Copy pathCCIPReader.sol
More file actions
executable file
·179 lines (164 loc) · 6.82 KB
/
Copy pathCCIPReader.sol
File metadata and controls
executable file
·179 lines (164 loc) · 6.82 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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
/// @author Modified from https://github.com/unruggable-labs/CCIPReader.sol/blob/341576fe7ff2b6e0c93fc08f37740cf6439f5873/contracts/CCIPReader.sol
/// MIT License
/// Portions Copyright (c) 2025 Unruggable
/// Portions Copyright (c) 2025 ENS Labs Ltd
/// @dev Instructions:
/// 1. inherit this contract
/// 2. call `ccipRead()` similar to `staticcall()`
/// 3. do not put logic after this invocation
/// 4. implement all response logic in callback
/// 5. ensure that return type of calling function == callback function
import {EIP3668, OffchainLookup} from "./EIP3668.sol";
import {BytesUtils} from "../utils/BytesUtils.sol";
contract CCIPReader {
/// @dev Default unsafe call gas (sufficient for legacy ENS resolver profiles).
uint256 constant DEFAULT_UNSAFE_CALL_GAS = 50000;
/// @dev Special-purpose value for identity callback: `f(x) = x`.
bytes4 constant IDENTITY_FUNCTION = bytes4(0);
/// @dev The gas limit for calling functions on unsafe contracts.
uint256 immutable unsafeCallGas;
constructor(uint256 _unsafeCallGas) {
unsafeCallGas = _unsafeCallGas;
}
/// @dev A recursive CCIP-Read session.
struct Context {
address target;
bytes4 callbackFunction;
bytes extraData;
bytes4 successCallbackFunction;
bytes4 failureCallbackFunction;
bytes myExtraData;
}
/// @dev Same as `ccipRead()` but the callback function is the identity.
function ccipRead(address target, bytes memory call) internal view {
ccipRead(target, call, IDENTITY_FUNCTION, IDENTITY_FUNCTION, "");
}
/// @dev Performs a CCIP-Read and handles internal recursion.
/// Reverts `OffchainLookup` if necessary.
/// Use `IDENTITY_FUNCTION` as the callback function selector for return/revert behavior.
/// @param target The contract address.
/// @param call The calldata to `staticcall()` on `target`.
/// @param successCallbackFunction The function selector of callback on success.
/// @param failureCallbackFunction The function selector of callback on failure.
/// @param extraData The contextual data relayed to callback function.
function ccipRead(
address target,
bytes memory call,
bytes4 successCallbackFunction,
bytes4 failureCallbackFunction,
bytes memory extraData
) internal view {
// We call the intended function that **could** revert with an `OffchainLookup`
// We destructure the response into an execution status bool and our return bytes
(bool ok, bytes memory v) = safeCall(
detectEIP140(target),
target,
call
);
// IF the function reverted with an `OffchainLookup`
if (!ok && bytes4(v) == OffchainLookup.selector) {
// We decode the response error into a tuple
// tuples allow flexibility noting stack too deep constraints
EIP3668.Params memory p = decodeOffchainLookup(v);
if (p.sender == target) {
// We then wrap the error data in an `OffchainLookup` sent/'owned' by this contract
revert OffchainLookup(
address(this),
p.urls,
p.callData,
this.ccipReadCallback.selector,
abi.encode(
Context(
target,
p.callbackFunction,
p.extraData,
successCallbackFunction,
failureCallbackFunction,
extraData
)
)
);
}
}
// IF we have gotten here, the 'real' target does not revert with an `OffchainLookup` error
// figure out what callback to call
bytes4 callbackFunction = ok
? successCallbackFunction
: failureCallbackFunction;
if (callbackFunction != IDENTITY_FUNCTION) {
// The exit point of this architecture is OUR callback in the 'real'
// We pass through the response to that callback
(ok, v) = address(this).staticcall(
abi.encodeWithSelector(callbackFunction, v, extraData)
);
}
// OR the call to the 'real' target reverts with a different error selector
// OR the call to OUR callback reverts with ANY error selector
if (ok) {
assembly {
return(add(v, 32), mload(v))
}
} else {
assembly {
revert(add(v, 32), mload(v))
}
}
}
/// @dev CCIP-Read callback for `ccipRead()`.
/// @param response The response from offchain.
/// @param extraData The contextual data passed from `ccipRead()`.
/// @dev The return type of this function is polymorphic depending on the caller.
function ccipReadCallback(
bytes memory response,
bytes memory extraData
) external view {
Context memory ctx = abi.decode(extraData, (Context));
// Since the callback can revert too (but has the same return structure)
// We can reuse the calling infrastructure to call the callback
ccipRead(
ctx.target,
abi.encodeWithSelector(
ctx.callbackFunction,
response,
ctx.extraData
),
ctx.successCallbackFunction,
ctx.failureCallbackFunction,
ctx.myExtraData
);
}
/// @dev Decode `OffchainLookup` error data into a struct.
/// @param v The error data of the revert.
/// @return p The decoded `OffchainLookup` params.
function decodeOffchainLookup(
bytes memory v
) internal pure returns (EIP3668.Params memory p) {
p = EIP3668.decode(BytesUtils.substring(v, 4, v.length - 4));
}
/// @dev Determine if `target` uses `revert()` instead of `invalid()`.
// Assumption: only newer contracts revert `OffchainLookup`.
/// @param target The contract to test.
/// @return safe True if safe to call.
function detectEIP140(address target) internal view returns (bool safe) {
if (target == address(this)) return true;
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-140.md
assembly {
let G := 5000
let g := gas()
pop(staticcall(G, target, 0, 0, 0, 0))
safe := lt(sub(g, gas()), G)
}
}
/// @dev Same as `staticcall()` but prevents OOG when not `safe`.
function safeCall(
bool safe,
address target,
bytes memory call
) internal view returns (bool ok, bytes memory v) {
(ok, v) = target.staticcall{gas: safe ? gasleft() : unsafeCallGas}(
call
);
}
}