-
Notifications
You must be signed in to change notification settings - Fork 12k
/
Copy pathERC2771Forwarder.t.sol
279 lines (232 loc) · 10.6 KB
/
ERC2771Forwarder.t.sol
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {ERC2771Forwarder} from "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";
import {CallReceiverMockTrustingForwarder, CallReceiverMock} from "@openzeppelin/contracts/mocks/CallReceiverMock.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
enum TamperType {
FROM,
TO,
VALUE,
DATA,
SIGNATURE
}
contract ERC2771ForwarderMock is ERC2771Forwarder {
constructor(string memory name) ERC2771Forwarder(name) {}
function forwardRequestStructHash(
ERC2771Forwarder.ForwardRequestData calldata request,
uint256 nonce
) external view returns (bytes32) {
return
_hashTypedDataV4(
keccak256(
abi.encode(
_FORWARD_REQUEST_TYPEHASH,
request.from,
request.to,
request.value,
request.gas,
nonce,
request.deadline,
keccak256(request.data)
)
)
);
}
}
contract ERC2771ForwarderTest is Test {
using ECDSA for bytes32;
ERC2771ForwarderMock internal _erc2771Forwarder;
CallReceiverMockTrustingForwarder internal _receiver;
uint256 internal _signerPrivateKey = 0xA11CE;
address internal _signer = vm.addr(_signerPrivateKey);
uint256 internal constant _MAX_ETHER = 10_000_000; // To avoid overflow
function setUp() public {
_erc2771Forwarder = new ERC2771ForwarderMock("ERC2771Forwarder");
_receiver = new CallReceiverMockTrustingForwarder(address(_erc2771Forwarder));
}
// Forge a new ForwardRequestData
function _forgeRequestData() private view returns (ERC2771Forwarder.ForwardRequestData memory) {
return
_forgeRequestData({
value: 0,
deadline: uint48(block.timestamp + 1),
data: abi.encodeCall(CallReceiverMock.mockFunction, ())
});
}
function _forgeRequestData(
uint256 value,
uint48 deadline,
bytes memory data
) private view returns (ERC2771Forwarder.ForwardRequestData memory) {
return
ERC2771Forwarder.ForwardRequestData({
from: _signer,
to: address(_receiver),
value: value,
gas: 30000,
deadline: deadline,
data: data,
signature: ""
});
}
// Sign a ForwardRequestData (in place) for a given nonce. Also returns it for convenience.
function _signRequestData(
ERC2771Forwarder.ForwardRequestData memory request,
uint256 nonce
) private view returns (ERC2771Forwarder.ForwardRequestData memory) {
bytes32 digest = _erc2771Forwarder.forwardRequestStructHash(request, nonce);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPrivateKey, digest);
request.signature = abi.encodePacked(r, s, v);
return request;
}
// Tamper a ForwardRequestData (in place). Also returns it for convenience.
function _tamperRequestData(
ERC2771Forwarder.ForwardRequestData memory request,
TamperType tamper
) private returns (ERC2771Forwarder.ForwardRequestData memory) {
if (tamper == TamperType.FROM) request.from = vm.randomAddress();
else if (tamper == TamperType.TO) request.to = vm.randomAddress();
else if (tamper == TamperType.VALUE) request.value = vm.randomUint();
else if (tamper == TamperType.DATA) request.data = vm.randomBytes(4);
else if (tamper == TamperType.SIGNATURE) request.signature = vm.randomBytes(65);
return request;
}
// Predict the revert error for a tampered request, and expect it is emitted.
function _tamperedExpectRevert(
ERC2771Forwarder.ForwardRequestData memory request,
TamperType tamper,
uint256 nonce
) private returns (ERC2771Forwarder.ForwardRequestData memory) {
if (tamper == TamperType.FROM) nonce = _erc2771Forwarder.nonces(request.from);
// predict revert
if (tamper == TamperType.TO) {
vm.expectRevert(
abi.encodeWithSelector(
ERC2771Forwarder.ERC2771UntrustfulTarget.selector,
request.to,
address(_erc2771Forwarder)
)
);
} else {
(address recovered, , ) = _erc2771Forwarder.forwardRequestStructHash(request, nonce).tryRecover(
request.signature
);
vm.expectRevert(
abi.encodeWithSelector(ERC2771Forwarder.ERC2771ForwarderInvalidSigner.selector, recovered, request.from)
);
}
return request;
}
function testExecuteAvoidsETHStuck(uint256 initialBalance, uint256 value, bool targetReverts) public {
initialBalance = bound(initialBalance, 0, _MAX_ETHER);
value = bound(value, 0, _MAX_ETHER);
// create and sign request
ERC2771Forwarder.ForwardRequestData memory request = _forgeRequestData({
value: value,
deadline: uint48(block.timestamp + 1),
data: targetReverts
? abi.encodeCall(CallReceiverMock.mockFunctionRevertsNoReason, ())
: abi.encodeCall(CallReceiverMock.mockFunction, ())
});
_signRequestData(request, _erc2771Forwarder.nonces(_signer));
vm.deal(address(_erc2771Forwarder), initialBalance);
vm.deal(address(this), request.value);
if (targetReverts) vm.expectRevert();
_erc2771Forwarder.execute{value: value}(request);
assertEq(address(_erc2771Forwarder).balance, initialBalance);
}
function testExecuteBatchAvoidsETHStuck(uint256 initialBalance, uint256 batchSize, uint256 value) public {
uint256 seed = uint256(keccak256(abi.encodePacked(initialBalance, batchSize, value)));
batchSize = bound(batchSize, 1, 10);
initialBalance = bound(initialBalance, 0, _MAX_ETHER);
value = bound(value, 0, _MAX_ETHER);
address refundReceiver = address(0xebe);
uint256 refundExpected = 0;
uint256 nonce = _erc2771Forwarder.nonces(_signer);
// create an array of signed requests (that may fail)
ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](batchSize);
for (uint256 i = 0; i < batchSize; ++i) {
bool failure = (seed >> i) & 0x1 == 0x1;
requests[i] = _forgeRequestData({
value: value,
deadline: uint48(block.timestamp + 1),
data: failure
? abi.encodeCall(CallReceiverMock.mockFunctionRevertsNoReason, ())
: abi.encodeCall(CallReceiverMock.mockFunction, ())
});
_signRequestData(requests[i], nonce + i);
refundExpected += SafeCast.toUint(failure) * value;
}
// distribute ether
vm.deal(address(_erc2771Forwarder), initialBalance);
vm.deal(address(this), value * batchSize);
// execute batch
_erc2771Forwarder.executeBatch{value: value * batchSize}(requests, payable(refundReceiver));
// check balances
assertEq(address(_erc2771Forwarder).balance, initialBalance);
assertEq(refundReceiver.balance, refundExpected);
}
function testVerifyTamperedValues(uint8 _tamper) public {
TamperType tamper = _asTamper(_tamper);
// create request, sign, tamper
ERC2771Forwarder.ForwardRequestData memory request = _forgeRequestData();
_signRequestData(request, 0);
_tamperRequestData(request, tamper);
// should not pass verification
assertFalse(_erc2771Forwarder.verify(request));
}
function testExecuteTamperedValues(uint8 _tamper) public {
TamperType tamper = _asTamper(_tamper);
// create request, sign, tamper, expect execution revert
ERC2771Forwarder.ForwardRequestData memory request = _forgeRequestData();
_signRequestData(request, 0);
_tamperRequestData(request, tamper);
_tamperedExpectRevert(request, tamper, 0);
vm.deal(address(this), request.value);
_erc2771Forwarder.execute{value: request.value}(request);
}
function testExecuteBatchTamperedValuesZeroReceiver(uint8 _tamper) public {
TamperType tamper = _asTamper(_tamper);
uint256 nonce = _erc2771Forwarder.nonces(_signer);
// create an array of signed requests
ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](3);
for (uint256 i = 0; i < requests.length; ++i) {
requests[i] = _forgeRequestData({
value: 0,
deadline: uint48(block.timestamp + 1),
data: abi.encodeCall(CallReceiverMock.mockFunction, ())
});
_signRequestData(requests[i], nonce + i);
}
// tamper with request[1] and expect execution revert
_tamperRequestData(requests[1], tamper);
_tamperedExpectRevert(requests[1], tamper, nonce + 1);
vm.deal(address(this), requests[1].value);
_erc2771Forwarder.executeBatch{value: requests[1].value}(requests, payable(address(0)));
}
function testExecuteBatchTamperedValues(uint8 _tamper) public {
TamperType tamper = _asTamper(_tamper);
uint256 nonce = _erc2771Forwarder.nonces(_signer);
// create an array of signed requests
ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](3);
for (uint256 i = 0; i < requests.length; ++i) {
requests[i] = _forgeRequestData({
value: 0,
deadline: uint48(block.timestamp + 1),
data: abi.encodeCall(CallReceiverMock.mockFunction, ())
});
_signRequestData(requests[i], nonce + i);
}
// tamper with request[1]
_tamperRequestData(requests[1], tamper);
// should not revert
vm.expectCall(address(_receiver), abi.encodeCall(CallReceiverMock.mockFunction, ()), 1);
vm.deal(address(this), requests[1].value);
_erc2771Forwarder.executeBatch{value: requests[1].value}(requests, payable(address(0xebe)));
}
function _asTamper(uint8 _tamper) private pure returns (TamperType) {
return TamperType(bound(_tamper, uint8(TamperType.FROM), uint8(TamperType.SIGNATURE)));
}
}