-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathSwapper.sol
558 lines (513 loc) · 18.7 KB
/
Swapper.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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "./libs/BitMask.sol";
/** _ _ _
* __ _ ___| |_ _ __ ___ | | __ _| |__
* / ` / __| _| '__/ \| |/ ` | ' \
* | O \__ \ |_| | | O | | O | O |
* \__,_|___/.__|_| \___/|_|\__,_|_.__/ ©️ 2023
*
* @title Swapper - On-chain swap calldata executor
* @author Astrolab DAO
* @notice This contract gatekeeps the execution of foreign swap calldata
* @dev The swap calldata can be generated using the swapper SDK https://github.com/AstrolabDAO/swapper
*/
contract Swapper is Ownable2Step {
using SafeERC20 for IERC20;
using BitMask for uint256;
uint256 public constant ownershipCooldown = 3 days; // cooldown period for ownership transfer
uint256 public constant ownershipAcceptanceWindow = 4 days; // acceptance window for ownership transfer
uint256 public pendingOwnershipTimestamp;
/**
* @notice Restrictions bitmask, positions:
* 0 = restrictCaller
* 1 = restrictRouter
* 2 = restrictInput
* 3 = restrictOutput
* 4 = approveMax
* 5 = autoRevoke
*/
uint256 private restrictions = 0;
mapping(address => bool) private whitelist; // whitelisted addresses (for tokens and callers)
// Events
event Swapped(
address indexed user,
address indexed assetIn,
address indexed assetOut,
uint256 amountIn,
uint256 amountOut
);
event Whitelisted(address indexed account); // caller/asset/router whitelist
event RemovedFromWhitelist(address indexed account);
event OwnershipTransferred(
address indexed prevOwner,
address indexed newOwner,
uint256 timestamp
);
event OwnershipTransferInitiated(
address indexed prevOwner,
address indexed newOwner,
uint256 timestamp
);
event Rescued(address indexed token, uint256 amount);
error CallRestricted();
error RouterError();
error UnexpectedOutput();
error SlippageTooHigh();
constructor(
address _owner,
bool _restrictCaller,
bool _restictRouter,
bool _approveMax
) Ownable(_owner) {
if (_restrictCaller) restrictions = restrictions.setBit(0);
if (_restictRouter) restrictions = restrictions.setBit(1);
if (_approveMax) restrictions = restrictions.setBit(4);
}
/**
* @notice Toggles caller restriction (only whitelisted can swap)
* @param _restrictCaller Boolean value to set restrictCaller
*/
function setCallerRestriction(bool _restrictCaller) external onlyOwner {
restrictions = _restrictCaller
? restrictions.setBit(0)
: restrictions.resetBit(0);
}
/**
* @notice Toggles router restriction (only whitelisted can be used as router)
* @param _restrictRouter Boolean value to set restrictRouter
*/
function setRouterRestriction(bool _restrictRouter) external onlyOwner {
restrictions = _restrictRouter
? restrictions.setBit(1)
: restrictions.resetBit(1);
}
/**
* @notice Toggles input token restriction
* @param _inputRestiction Boolean value to set inputRestiction
*/
function setInputRestriction(bool _inputRestiction) external onlyOwner {
restrictions = _inputRestiction
? restrictions.setBit(2)
: restrictions.resetBit(2);
}
/**
* @notice Toggles output token restriction
* @param _outputRestiction Boolean value to set outputRestiction
*/
function setOutputRestriction(bool _outputRestiction) external onlyOwner {
restrictions = _outputRestiction
? restrictions.setBit(3)
: restrictions.resetBit(3);
}
/**
* @notice Toggles default max approval (approve max amount to router on first use)
* @param _approveMax Boolean value to set approveMax
*/
function setApproveMax(bool _approveMax) external onlyOwner {
restrictions = _approveMax
? restrictions.setBit(4)
: restrictions.resetBit(4);
}
/**
* @notice Toggles auto revoke approval (revoke router approval after every swap)
* @param _autoRevoke Boolean value to set autoRevoke
*/
function setAutoRevoke(bool _autoRevoke) external onlyOwner {
restrictions = _autoRevoke
? restrictions.setBit(5)
: restrictions.resetBit(5);
}
/**
* @notice Adds an address to the whitelist (caller/asset/router)
* @param _address Address to be added to the whitelist
*/
function addToWhitelist(address _address) external onlyOwner {
whitelist[_address] = true;
emit Whitelisted(_address);
}
/**
* @notice Removes an address from the whitelist (caller/asset/router)
* @param _address Address to be removed from the whitelist
*/
function removeFromWhitelist(address _address) external onlyOwner {
whitelist[_address] = false;
emit RemovedFromWhitelist(_address);
}
/**
* @notice Checks if an address is whitelisted (caller/asset/router)
* @param _address Address to check
* @return Boolean indicating whether the whitelisting is whitelisted
*/
function isWhitelisted(address _address) public view returns (bool) {
return whitelist[_address];
}
/**
* @notice Returns true if the caller is restricted for the given address
* @param _caller Address to check for caller restriction
* @return Boolean indicating whether the caller is restricted
*/
function isCallerRestricted(address _caller) public view returns (bool) {
return restrictions.getBit(0) && !isWhitelisted(_caller);
}
/**
* @notice Returns true if the router is restricted for the given address
* @param _router Address to check for router restriction
* @return Boolean indicating whether the router is restricted
*/
function isRouterRestricted(address _router) public view returns (bool) {
return restrictions.getBit(1) && !isWhitelisted(_router);
}
/**
* @notice Returns true if the input token is restricted for the given address
* @param _input Address to check for input token restriction
* @return Boolean indicating whether the input token is restricted
*/
function isInputRestricted(address _input) public view returns (bool) {
return restrictions.getBit(2) && !isWhitelisted(_input);
}
/**
* @notice Returns true if the output token is restricted for the given address
* @param _output Address to check for output token restriction
* @return Boolean indicating whether the output token is restricted
*/
function isOutputRestricted(address _output) public view returns (bool) {
return restrictions.getBit(3) && !isWhitelisted(_output);
}
/**
* @notice Returns true if the contract is set to approve the maximum amount to the router on the first use
* @return Boolean indicating whether the contract is set to approve the maximum amount
*/
function isApproveMax() public view returns (bool) {
return restrictions.getBit(4);
}
/**
* @notice Returns true if the contract is set to automatically revoke router approval after every swap
* @return Boolean indicating whether the contract is set to automatically revoke approval
*/
function isAutoRevoke() public view returns (bool) {
return restrictions.getBit(5);
}
/**
* @notice Executes a single swap
* @param _input Address of the input token
* @param _output Address of the output token
* @param _amountIn Amount of input tokens to swap
* @param _minAmountOut Minimum amount of output tokens to receive from the swap
* @param _targetRouter Address of the router to be used for the swap
* @param _callData Encoded routing data (built off-chain) to be passed to the router
* NOTE: Receiver should be msg.sender in calldata
* @return received Amount of output tokens received
* @return spent Amount of input tokens spent
*/
function swap(
address _input,
address _output,
uint256 _amountIn,
uint256 _minAmountOut,
address _targetRouter,
bytes memory _callData
) public payable returns (uint256 received, uint256 spent) {
if (
isInputRestricted(_input) ||
isOutputRestricted(_output) ||
isRouterRestricted(_targetRouter) ||
isCallerRestricted(msg.sender)
) revert CallRestricted();
(IERC20 input, IERC20 output) = (IERC20(_input), IERC20(_output));
input.safeTransferFrom(msg.sender, address(this), _amountIn);
(uint256 inputBefore, uint256 outputBefore) = (
input.balanceOf(address(this)),
output.balanceOf(msg.sender)
);
if (input.allowance(address(this), _targetRouter) < _amountIn)
input.forceApprove(
_targetRouter,
isApproveMax() ? type(uint256).max : _amountIn
);
(bool ok, ) = address(_targetRouter).call(_callData);
if (!ok) revert RouterError();
received = output.balanceOf(msg.sender) - outputBefore;
spent = inputBefore - input.balanceOf(address(this));
if (spent < 1 || received < 1) revert UnexpectedOutput();
if (received < _minAmountOut) revert SlippageTooHigh();
// return leftover input tokens to msg.sender
if (spent < _amountIn) input.safeTransfer(msg.sender, _amountIn - spent);
if (isAutoRevoke()) input.forceApprove(_targetRouter, 0);
emit Swapped(msg.sender, _input, _output, _amountIn, received);
}
/**
* @notice Executes a single swap using the entire balance of the input token
* @param _input Address of the input token
* @param _output Address of the output token
* @param _minAmountOut Minimum amount of output tokens to receive from the swap
* @param _targetRouter Address of the router to be used for the swap
* @param _callData Encoded routing data (built off-chain) to be passed to the router
* @return received Amount of output tokens received
* @return spent Amount of input tokens spent
*/
function swapBalance(
address _input,
address _output,
uint256 _minAmountOut,
address _targetRouter,
bytes memory _callData
) public payable returns (uint256 received, uint256 spent) {
return
swap(
_input,
_output,
IERC20(_input).balanceOf(msg.sender),
_minAmountOut,
_targetRouter,
_callData
);
}
/**
* @notice Helper function to decode swap parameters (router+minAmountOut+callData)
* @param _params Encoded swap parameters
* @return target Router address
* @return minAmount Minimum output amount
* @return callData Encoded routing data (built off-chain) to be passed to the router
*/
function decodeSwapperParams(
bytes memory _params
)
internal
pure
returns (address target, uint256 minAmount, bytes memory callData)
{
return abi.decode(_params, (address, uint256, bytes));
}
/**
* @notice Returns the swap output amount
* @dev We consider this (strat) to be the sole receiver of all swaps
* @param _input Asset to be swapped into strategy input
* @param _output Asset to invest
* @param _amount Amount of _input to be swapped
* @param _params Encoded swap parameters
* @return received Amount of output tokens received
* @return spent Amount of input tokens spent
*/
function decodeAndSwap(
address _input,
address _output,
uint256 _amount,
bytes memory _params
) public returns (uint256 received, uint256 spent) {
(
address targetRouter,
uint256 minAmountReceived,
bytes memory swapData
) = decodeSwapperParams(_params);
return
swap({
_input: _input,
_output: _output,
_amountIn: _amount,
_minAmountOut: minAmountReceived,
_targetRouter: targetRouter,
_callData: swapData
});
}
/**
* @notice Executes a single swap using the entire balance of the input token
* @param _input Address of the input token
* @param _output Address of the output token
* @param _params Encoded swap parameters
* @return received Amount of output tokens received
* @return spent Amount of input tokens spent
*/
function decodeAndSwapBalance(
address _input,
address _output,
bytes memory _params
) public returns (uint256 received, uint256 spent) {
return
decodeAndSwap(
_input,
_output,
IERC20(_input).balanceOf(msg.sender),
_params
);
}
/**
* @notice Helper function to execute multiple swaps
* @param _inputs Array of input token addresses
* @param _outputs Array of output token addresses
* @param _amountsIn Array of input token amounts for each swap
* @param _minAmountsOut Array of minimum output token amounts for each swap
* @param _targetRouters Array of router addresses to be used for each swap
* @param _params Array of encoded routing data (built off-chain) to be passed to the router
* @return received Array of output token amounts received for each swap
* @return spent Array of input token amounts spent for each swap
*/
function multiSwap(
address[] memory _inputs,
address[] memory _outputs,
uint256[] memory _amountsIn,
uint256[] memory _minAmountsOut,
address[] memory _targetRouters,
bytes[] memory _params
) public returns (uint256[] memory received, uint256[] memory spent) {
require(
_inputs.length == _outputs.length &&
_inputs.length == _amountsIn.length &&
_inputs.length == _minAmountsOut.length &&
_inputs.length == _params.length,
"invalid input"
);
received = new uint256[](_inputs.length);
spent = new uint256[](_inputs.length);
for (uint256 i = 0; i < _inputs.length; i++)
(received[i], spent[i]) = swap(
_inputs[i],
_outputs[i],
_amountsIn[i],
_minAmountsOut[i],
_targetRouters[i],
_params[i]
);
}
/**
* @notice Executes multiple swaps using the entire balance of the input token for each swap
* @param _inputs Array of input token addresses
* @param _outputs Array of output token addresses
* @param _minAmountsOut Array of minimum output token amounts for each swap
* @param _targetRouters Array of router addresses to be used for each swap
* @param _params Array of encoded routing data (built off-chain) to be passed to the router
* @return received Array of output token amounts received for each swap
* @return spent Array of input token amounts spent for each swap
*/
function multiSwapBalances(
address[] memory _inputs,
address[] memory _outputs,
uint256[] memory _minAmountsOut,
address[] memory _targetRouters,
bytes[] memory _params
) public returns (uint256[] memory received, uint256[] memory spent) {
require(
_inputs.length == _outputs.length &&
_inputs.length == _minAmountsOut.length &&
_inputs.length == _targetRouters.length &&
_inputs.length == _params.length,
"invalid input"
);
received = new uint256[](_inputs.length);
spent = new uint256[](_inputs.length);
for (uint256 i = 0; i < _inputs.length; i++)
(received[i], spent[i]) = swapBalance(
_inputs[i],
_outputs[i],
_minAmountsOut[i],
_targetRouters[i],
_params[i]
);
}
/**
* @notice Executes multiple swaps using the entire balance of the input token for each swap
* @param _inputs Array of input token addresses
* @param _outputs Array of output token addresses
* @param _amountsIn Array of input token amounts for each swap
* @param _params Array of encoded routing data (built off-chain) to be passed to the router
* @return received Array of output token amounts received for each swap
* @return spent Array of input token amounts spent for each swap
*/
function decodeAndMultiSwap(
address[] memory _inputs,
address[] memory _outputs,
uint256[] memory _amountsIn,
bytes[] memory _params
) public returns (uint256[] memory received, uint256[] memory spent) {
require(
_inputs.length == _outputs.length &&
_inputs.length == _amountsIn.length &&
_inputs.length == _params.length,
"invalid input"
);
received = new uint256[](_inputs.length);
spent = new uint256[](_inputs.length);
for (uint256 i = 0; i < _inputs.length; i++)
(received[i], spent[i]) = decodeAndSwap(
_inputs[i],
_outputs[i],
_amountsIn[i],
_params[i]
);
}
/**
* @notice Executes multiple swaps using the entire balance of the input token for each swap
* @param _inputs Array of input token addresses
* @param _outputs Array of output token addresses
* @param _params Array of encoded routing data (built off-chain) to be passed to the router
* @return received Array of output token amounts received for each swap
* @return spent Array of input token amounts spent for each swap
*/
function decodeAndMultiSwapBalances(
address[] memory _inputs,
address[] memory _outputs,
bytes[] memory _params
) public returns (uint256[] memory received, uint256[] memory spent) {
require(
_inputs.length == _outputs.length && _inputs.length == _params.length,
"invalid input"
);
received = new uint256[](_inputs.length);
spent = new uint256[](_inputs.length);
for (uint256 i = 0; i < _inputs.length; i++)
(received[i], spent[i]) = decodeAndSwapBalance(
_inputs[i],
_outputs[i],
_params[i]
);
}
/**
* @notice Transfers ownership of the contract to a new address
* @param newOwner The address to transfer ownership to
*/
function transferOwnership(address newOwner) public override onlyOwner {
emit OwnershipTransferInitiated(
owner(),
newOwner,
pendingOwnershipTimestamp
);
super.transferOwnership(newOwner);
pendingOwnershipTimestamp = block.timestamp + ownershipCooldown;
}
/**
* @notice Accepts the pending ownership transfer
*/
function acceptOwnership() public override {
emit OwnershipTransferred(owner(), msg.sender, pendingOwnershipTimestamp);
require(
block.timestamp >= pendingOwnershipTimestamp,
"Ownership transfer cooldown not met"
);
require(
block.timestamp <= pendingOwnershipTimestamp + ownershipAcceptanceWindow,
"Ownership acceptance window expired"
);
super.acceptOwnership();
}
/**
* @notice Rescues the contract's `_token` (ERC20 or native) full balance by sending it to `msg.sender` if owner
* @param _token Token to be rescued - Use address(1) for native/gas tokens (ETH)
* @dev Simplified, cooldown-less AsRescuable (cf. https://github.com/AstrolabDAO/strats/blob/main/src/abstract/AsRescuable.sol)
*/
function rescue(address _token) external onlyOwner {
uint256 rescued;
// send to admin multisig
if (_token == address(1)) {
rescued = address(this).balance;
(bool ok, ) = payable(msg.sender).call{value: rescued}("");
require(ok);
} else {
rescued = IERC20(_token).balanceOf(address(this));
IERC20(_token).safeTransfer(msg.sender, rescued);
}
emit Rescued(_token, rescued);
}
}