-
Notifications
You must be signed in to change notification settings - Fork 12
/
UniswapV4.sol
921 lines (818 loc) · 39.8 KB
/
UniswapV4.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
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {IERC20} from "@forge-std/interfaces/IERC20.sol";
import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol";
import {SafeTransferLib} from "../vendor/SafeTransferLib.sol";
import {SettlerAbstract} from "../SettlerAbstract.sol";
import {Panic} from "../utils/Panic.sol";
import {UnsafeMath} from "../utils/UnsafeMath.sol";
import {
TooMuchSlippage,
DeltaNotPositive,
DeltaNotNegative,
ZeroSellAmount,
ZeroBuyAmount,
BoughtSellToken,
TokenHashCollision,
ZeroToken
} from "./SettlerErrors.sol";
import {
BalanceDelta, IHooks, IPoolManager, UnsafePoolManager, POOL_MANAGER, IUnlockCallback
} from "./UniswapV4Types.sol";
library CreditDebt {
using UnsafeMath for int256;
function asCredit(int256 delta, IERC20 token) internal pure returns (uint256) {
if (delta < 0) {
revert DeltaNotPositive(token);
}
return uint256(delta);
}
function asDebt(int256 delta, IERC20 token) internal pure returns (uint256) {
if (delta > 0) {
revert DeltaNotNegative(token);
}
return uint256(delta.unsafeNeg());
}
}
/// This library is a highly-optimized, in-memory, enumerable mapping from tokens to amounts. It
/// consists of 2 components that must be kept synchronized. There is a `memory` array of `Note`
/// (aka `Note[] memory`) that has up to `_MAX_TOKENS` pre-allocated. And there is an implicit heap
/// packed at the end of the array that stores the `Note`s. Each `Note` has a backpointer that knows
/// its location in the `Notes[] memory`. While the length of the `Notes[]` array grows and shrinks
/// as tokens are added and retired, heap objects are only cleared/deallocated when the context of
/// `unlockCallback` returns. Looking up the `Note` object corresponding to a token uses the perfect
/// hash formed by `hashMul` and `hashMod`. Pay special attention to these parameters. See further
/// below in `contract UniswapV4` for recommendations on how to select values for them. A hash
/// collision will result in a revert with signature `TokenHashCollision(address,address)`.
library NotesLib {
uint256 private constant _ADDRESS_MASK = 0x00ffffffffffffffffffffffffffffffffffffffff;
/// This is the maximum number of tokens that may be involved in a UniV4 action. Increasing or
/// decreasing this value requires no other changes elsewhere in this file.
uint256 private constant _MAX_TOKENS = 8;
type NotePtr is uint256;
type NotePtrPtr is uint256;
struct Note {
uint256 amount;
IERC20 token;
NotePtrPtr backPtr;
}
function construct() internal pure returns (Note[] memory r) {
assembly ("memory-safe") {
r := mload(0x40)
// set the length of `r` to zero
mstore(r, 0x00)
// zeroize the heap
codecopy(add(add(0x20, shl(0x05, _MAX_TOKENS)), r), codesize(), mul(0x60, _MAX_TOKENS))
// allocate memory
mstore(0x40, add(add(0x20, shl(0x07, _MAX_TOKENS)), r))
}
}
function eq(Note memory x, Note memory y) internal pure returns (bool r) {
assembly ("memory-safe") {
r := eq(x, y)
}
}
function unsafeGet(Note[] memory a, uint256 i) internal pure returns (IERC20 retToken, uint256 retAmount) {
assembly ("memory-safe") {
let x := mload(add(add(0x20, shl(0x05, i)), a))
retToken := mload(add(0x20, x))
retAmount := mload(x)
}
}
function get(Note[] memory a, IERC20 newToken, uint256 hashMul, uint256 hashMod)
internal
pure
returns (NotePtr x)
{
assembly ("memory-safe") {
newToken := and(_ADDRESS_MASK, newToken)
x := add(add(0x20, shl(0x05, _MAX_TOKENS)), a) // `x` now points at the first `Note` on the heap
x := add(mod(mulmod(newToken, hashMul, hashMod), mul(0x60, _MAX_TOKENS)), x) // combine with token hash
// `x` now points at the exact `Note` object we want; let's check it to be sure, though
let x_token_ptr := add(0x20, x)
// check that we haven't encountered a hash collision. checking for a hash collision is
// equivalent to checking for array out-of-bounds or overflow.
{
let old_token := mload(x_token_ptr)
if mul(or(mload(add(0x40, x)), old_token), xor(old_token, newToken)) {
mstore(0x00, 0x9a62e8b4) // selector for `TokenHashCollision(address,address)`
mstore(0x20, old_token)
mstore(0x40, newToken)
revert(0x1c, 0x44)
}
}
// zero `newToken` is a footgun; check for it
if iszero(newToken) {
mstore(0x00, 0xad1991f5) // selector for `ZeroToken()`
revert(0x1c, 0x04)
}
// initialize the token (possibly redundant)
mstore(x_token_ptr, newToken)
}
}
function add(Note[] memory a, Note memory x) internal pure {
assembly ("memory-safe") {
let backptr_ptr := add(0x40, x)
let backptr := mload(backptr_ptr)
if iszero(backptr) {
let len := add(0x01, mload(a))
// We don't need to check for overflow or out-of-bounds access here; the checks in
// `get` above for token collision handle that for us. It's not possible to `get`
// more than `_MAX_TOKENS` tokens
mstore(a, len)
backptr := add(shl(0x05, len), a)
mstore(backptr, x)
mstore(backptr_ptr, backptr)
}
}
}
function del(Note[] memory a, Note memory x) internal pure {
assembly ("memory-safe") {
let x_backptr_ptr := add(0x40, x)
let x_backptr := mload(x_backptr_ptr)
if x_backptr {
// Clear the backpointer in the referred-to `Note`
mstore(x_backptr_ptr, 0x00)
// We do not deallocate `x`
// Decrement the length of `a`
let len := mload(a)
mstore(a, sub(len, 0x01))
// Check if this is a "swap and pop" or just a "pop"
let end_ptr := add(shl(0x05, len), a)
if iszero(eq(end_ptr, x_backptr)) {
// Overwrite the vacated indirection pointer `x_backptr` with the value at the end.
let end := mload(end_ptr)
mstore(x_backptr, end)
// Fix up the backpointer in `end` to point to the new location of the indirection
// pointer.
let end_backptr_ptr := add(0x40, end)
mstore(end_backptr_ptr, x_backptr)
}
}
}
}
}
library StateLib {
using NotesLib for NotesLib.Note;
using NotesLib for NotesLib.Note[];
struct State {
NotesLib.Note buy;
NotesLib.Note sell;
NotesLib.Note globalSell;
uint256 globalSellAmount;
uint256 _hashMul;
uint256 _hashMod;
}
function construct(State memory state, IERC20 token, uint256 hashMul, uint256 hashMod)
internal
pure
returns (NotesLib.Note[] memory notes)
{
assembly ("memory-safe") {
// Solc is real dumb and has allocated a bunch of extra memory for us. Thanks solc.
mstore(0x40, add(0xc0, state))
}
// All the pointers in `state` are now pointing into unallocated memory
notes = NotesLib.construct();
// The pointers in `state` are now illegally aliasing elements in `notes`
NotesLib.NotePtr notePtr = notes.get(token, hashMul, hashMod);
// Here we actually set the pointers into a legal area of memory
setBuy(state, notePtr);
setSell(state, notePtr);
assembly ("memory-safe") {
// Set `state.globalSell`
mstore(add(0x40, state), notePtr)
}
state._hashMul = hashMul;
state._hashMod = hashMod;
}
function setSell(State memory state, NotesLib.NotePtr notePtr) private pure {
assembly ("memory-safe") {
mstore(add(0x20, state), notePtr)
}
}
function setSell(State memory state, NotesLib.Note[] memory notes, IERC20 token) internal pure {
setSell(state, notes.get(token, state._hashMul, state._hashMod));
}
function setBuy(State memory state, NotesLib.NotePtr notePtr) private pure {
assembly ("memory-safe") {
mstore(state, notePtr)
}
}
function setBuy(State memory state, NotesLib.Note[] memory notes, IERC20 token) internal pure {
setBuy(state, notes.get(token, state._hashMul, state._hashMod));
}
}
abstract contract UniswapV4 is SettlerAbstract {
using SafeTransferLib for IERC20;
using UnsafeMath for uint256;
using UnsafeMath for int256;
using CreditDebt for int256;
using UnsafePoolManager for IPoolManager;
using NotesLib for NotesLib.Note;
using NotesLib for NotesLib.Note[];
using StateLib for StateLib.State;
//// These two functions are the entrypoints to this set of actions. Because UniV4 has a
//// mandatory callback, and the vast majority of the business logic has to be executed inside
//// the callback, they're pretty minimal. Both end up inside the last function in this file
//// `unlockCallback`, which is where most of the business logic lives. Primarily, these
//// functions are concerned with correctly encoding the argument to
//// `POOL_MANAGER.unlock(...)`. Pay special attention to the `payer` field, which is what
//// signals to the callback whether we should be spending a coupon.
//// How to generate `fills` for UniV4:
////
//// Linearize your DAG of fills by doing a topological sort on the tokens involved. In the
//// topological sort of tokens, when there is a choice of the next token, break ties by
//// preferring a token if it is the lexicographically largest token that is bought among fills
//// with sell token equal to the previous token in the topological sort. Then sort the fills
//// belonging to each sell token by their buy token. This technique isn't *quite* optimal, but
//// it's pretty close. The buy token of the final fill is special-cased. It is the token that
//// will be transferred to `recipient` and have its slippage checked against `amountOutMin`. In
//// the event that you are encoding a series of fills with more than one output token, ensure
//// that at least one of the global buy token's fills is positioned appropriately.
////
//// Now that you have a list of fills, encode each fill as follows.
//// First encode the `bps` for the fill as 2 bytes. Remember that this `bps` is relative to the
//// running balance at the moment that the fill is settled.
//// Second, encode the packing key for that fill as 1 byte. The packing key byte depends on the
//// tokens involved in the previous fill. The packing key for the first fill must be 1;
//// i.e. encode only the buy token for the first fill.
//// 0 -> sell and buy tokens remain unchanged from the previous fill (pure multiplex)
//// 1 -> sell token remains unchanged from the previous fill, buy token is encoded (diamond multiplex)
//// 2 -> sell token becomes the buy token from the previous fill, new buy token is encoded (multihop)
//// 3 -> both sell and buy token are encoded
//// Obviously, after encoding the packing key, you encode 0, 1, or 2 tokens (each as 20 bytes),
//// as appropriate.
//// The remaining fields of the fill are mandatory.
//// Third, encode the pool fee as 3 bytes, and the pool tick spacing as 3 bytes.
//// Fourth, encode the hook address as 20 bytes.
//// Fifth, encode the hook data for the fill. Encode the length of the hook data as 3 bytes,
//// then append the hook data itself.
////
//// Repeat the process for each fill and concatenate the results without padding.
//// How to generate a perfect hash for UniV4:
////
//// The arguments `hashMul` and `hashMod` are required to form a perfect hash for a table with
//// size `_MAX_TOKENS` when applied to all the tokens involved in fills. The hash function is
//// constructed as `uint256 hash = mulmod(uint256(uint160(address(token))), hashMul, hashMod) %
//// _MAX_TOKENS`.
////
//// The "simple" or "obvious" way to do this is to simply try random 128-bit numbers for both
//// `hashMul` and `hashMod` until you obtain a function that has no collisions when applied to
//// the tokens involved in fills. A substantially more optimized algorithm can be obtained by
//// selecting several (at least 10) prime values for `hashMod`, precomputing the limb moduluses
//// for each value, and then selecting randomly from among them. The author recommends using
//// the 10 largest 64-bit prime numbers: 2^64 - {59, 83, 95, 179, 189, 257, 279, 323, 353,
//// 363}. `hashMul` can then be selected randomly or via some other optimized method.
////
//// Note that in spite of the fact that the pool manager represents Ether (or the native asset
//// of the chain) as `address(0)`, we represent Ether as `SettlerAbstract.ETH_ADDRESS` (the
//// address of all `e`s) for homogeneity with other parts of the codebase, and because the
//// decision to represent Ether as `address(0)` was stupid in the first place. `address(0)`
//// represents the absence of a thing, not a special case of the thing. It creates confusion
//// with uninitialized memory, storage, and variables.
function sellToUniswapV4(
address recipient,
IERC20 sellToken,
uint256 bps,
bool feeOnTransfer,
uint256 hashMul,
uint256 hashMod,
bytes memory fills,
uint256 amountOutMin
) internal returns (uint256 buyAmount) {
if (amountOutMin > uint128(type(int128).max)) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}
if (bps > BASIS) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}
hashMul *= 96;
hashMod *= 96;
if (hashMul > type(uint128).max) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}
if (hashMod > type(uint128).max) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}
bytes memory data;
assembly ("memory-safe") {
data := mload(0x40)
let pathLen := mload(fills)
mcopy(add(0xd3, data), add(0x20, fills), pathLen)
mstore(add(0xb3, data), bps)
mstore(add(0xb1, data), sellToken)
mstore(add(0x9d, data), address()) // payer
// feeOnTransfer (1 byte)
mstore(add(0x88, data), hashMod)
mstore(add(0x78, data), hashMul)
mstore(add(0x68, data), amountOutMin)
mstore(add(0x58, data), recipient)
mstore(add(0x44, data), add(0x6f, pathLen))
mstore(add(0x24, data), 0x20)
mstore(add(0x04, data), 0x48c89491) // selector for `unlock(bytes)`
mstore(data, add(0xb3, pathLen))
mstore8(add(0xa8, data), feeOnTransfer)
mstore(0x40, add(data, add(0xd3, pathLen)))
}
bytes memory encodedBuyAmount = _setOperatorAndCall(
address(POOL_MANAGER), data, uint32(IUnlockCallback.unlockCallback.selector), _uniV4Callback
);
// buyAmount = abi.decode(abi.decode(encodedBuyAmount, (bytes)), (uint256));
assembly ("memory-safe") {
// We can skip all the checks performed by `abi.decode` because we know that this is the
// verbatim result from `unlockCallback` and that `unlockCallback` encoded the buy
// amount correctly.
buyAmount := mload(add(0x60, encodedBuyAmount))
}
}
function sellToUniswapV4VIP(
address recipient,
bool feeOnTransfer,
uint256 hashMul,
uint256 hashMod,
bytes memory fills,
ISignatureTransfer.PermitTransferFrom memory permit,
bytes memory sig,
uint256 amountOutMin
) internal returns (uint256 buyAmount) {
if (amountOutMin > uint128(type(int128).max)) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}
hashMul *= 96;
hashMod *= 96;
if (hashMul > type(uint128).max) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}
if (hashMod > type(uint128).max) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}
bool isForwarded = _isForwarded();
bytes memory data;
assembly ("memory-safe") {
data := mload(0x40)
let pathLen := mload(fills)
let sigLen := mload(sig)
{
let ptr := add(0x132, data)
// sig length as 3 bytes goes at the end of the callback
mstore(sub(add(sigLen, add(pathLen, ptr)), 0x1d), sigLen)
// fills go at the end of the header
mcopy(ptr, add(0x20, fills), pathLen)
ptr := add(pathLen, ptr)
// signature comes after the fills
mcopy(ptr, add(0x20, sig), sigLen)
ptr := add(sigLen, ptr)
mstore(0x40, add(0x03, ptr))
}
mstore8(add(0x131, data), isForwarded)
mcopy(add(0xf1, data), add(0x20, permit), 0x40)
mcopy(add(0xb1, data), mload(permit), 0x40) // aliases `payer` on purpose
mstore(add(0x9d, data), 0x00) // payer
// feeOnTransfer (1 byte)
mstore(add(0x88, data), hashMod)
mstore(add(0x78, data), hashMul)
mstore(add(0x68, data), amountOutMin)
mstore(add(0x58, data), recipient)
mstore(add(0x44, data), add(0xd1, add(pathLen, sigLen)))
mstore(add(0x24, data), 0x20)
mstore(add(0x04, data), 0x48c89491) // selector for `unlock(bytes)`
mstore(data, add(0x115, add(pathLen, sigLen)))
mstore8(add(0xa8, data), feeOnTransfer)
}
bytes memory encodedBuyAmount = _setOperatorAndCall(
address(POOL_MANAGER), data, uint32(IUnlockCallback.unlockCallback.selector), _uniV4Callback
);
// buyAmount = abi.decode(abi.decode(encodedBuyAmount, (bytes)), (uint256));
assembly ("memory-safe") {
// We can skip all the checks performed by `abi.decode` because we know that this is the
// verbatim result from `unlockCallback` and that `unlockCallback` encoded the buy
// amount correctly.
buyAmount := mload(add(0x60, encodedBuyAmount))
}
}
function _uniV4Callback(bytes calldata data) private returns (bytes memory) {
// We know that our calldata is well-formed. Therefore, the first slot is 0x20 and the
// second slot is the length of the strict ABIEncoded payload
assembly ("memory-safe") {
data.length := calldataload(add(0x20, data.offset))
data.offset := add(0x40, data.offset)
}
return unlockCallback(data);
}
//// The following functions are the helper functions for `unlockCallback`. They abstract much
//// of the complexity of tracking which tokens need to be zeroed out at the end of the
//// callback.
////
//// The two major pieces of state that are maintained through the callback are `Note[] memory
//// notes` and `State memory state`
////
//// `notes` keeps track of the list of the tokens that have been touched throughout the
//// callback that have nonzero credit. At the end of the fills, all tokens with credit will be
//// swept back to Settler. These are the global buy token (against which slippage is checked)
//// and any other multiplex-out tokens. Only the global sell token is allowed to have debt, but
//// it is accounted slightly differently from the other tokens. The function `_take` is
//// responsible for iterating over the list of tokens and withdrawing any credit to the
//// appropriate recipient.
////
//// `state` exists to reduce stack pressure and to simplify/gas-optimize the process of
//// swapping. By keeping track of the sell and buy token on each hop, we're able to compress
//// the representation of the fills required to satisfy the swap. Most often in a swap, the
//// tokens in adjacent fills are somewhat in common. By caching, we avoid having them appear
//// multiple times in the calldata.
// the mandatory fields are
// 2 - sell bps
// 1 - pool key tokens case
// 3 - pool fee
// 3 - pool tick spacing
// 20 - pool hooks
// 3 - hook data length
uint256 private constant _HOP_DATA_LENGTH = 32;
/// Update `state` for the next fill packed in `data`. This also may allocate/append `Note`s
/// into `notes`. Returns the suffix of the bytes that are not consumed in the decoding
/// process. The first byte of `data` describes which of the compact representations for the hop
/// is used.
///
/// 0 -> sell and buy tokens remain unchanged from the previous fill (pure multiplex)
/// 1 -> sell token remains unchanged from the previous fill, buy token is read from `data` (diamond multiplex)
/// 2 -> sell token becomes the buy token from the previous fill, new buy token is read from `data` (multihop)
/// 3 -> both sell and buy token are read from `data`
///
/// This function is responsible for calling `NotesLib.get(Note[] memory, IERC20, uint256,
/// uint256)` (via `StateLib.setSell` and `StateLib.setBuy`), which maintains the `notes` array
/// and heap.
function _updateState(StateLib.State memory state, NotesLib.Note[] memory notes, bytes calldata data)
private
pure
returns (bytes calldata)
{
bytes32 dataWord;
assembly ("memory-safe") {
dataWord := calldataload(data.offset)
}
uint256 dataConsumed = 1;
uint256 caseKey = uint256(dataWord) >> 248;
if (caseKey != 0) {
notes.add(state.buy);
if (caseKey > 1) {
if (state.sell.amount == 0) {
notes.del(state.sell);
}
if (caseKey == 2) {
state.sell = state.buy;
} else {
assert(caseKey == 3);
IERC20 sellToken = IERC20(address(uint160(uint256(dataWord) >> 88)));
assembly ("memory-safe") {
dataWord := calldataload(add(0x14, data.offset))
}
unchecked {
dataConsumed += 20;
}
state.setSell(notes, sellToken);
}
}
IERC20 buyToken = IERC20(address(uint160(uint256(dataWord) >> 88)));
unchecked {
dataConsumed += 20;
}
state.setBuy(notes, buyToken);
if (state.buy.eq(state.globalSell)) {
revert BoughtSellToken(state.globalSell.token);
}
}
assembly ("memory-safe") {
data.offset := add(dataConsumed, data.offset)
data.length := sub(data.length, dataConsumed)
// we don't check for array out-of-bounds here; we will check it later in `_getHookData`
}
return data;
}
uint256 private constant _ADDRESS_MASK = 0x00ffffffffffffffffffffffffffffffffffffffff;
/// Decode a `PoolKey` from its packed representation in `bytes` and the token information in
/// `state`. Returns the `zeroForOne` flag and the suffix of the bytes that are not consumed in
/// the decoding process.
function _setPoolKey(IPoolManager.PoolKey memory key, StateLib.State memory state, bytes calldata data)
private
pure
returns (bool, bytes calldata)
{
(IERC20 sellToken, IERC20 buyToken) = (state.sell.token, state.buy.token);
bool zeroForOne;
assembly ("memory-safe") {
sellToken := and(_ADDRESS_MASK, sellToken)
buyToken := and(_ADDRESS_MASK, buyToken)
zeroForOne :=
or(
eq(sellToken, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee),
and(iszero(eq(buyToken, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee)), lt(sellToken, buyToken))
)
}
(key.token0, key.token1) = zeroForOne ? (sellToken, buyToken) : (buyToken, sellToken);
uint256 packed;
assembly ("memory-safe") {
packed := shr(0x30, calldataload(data.offset))
data.offset := add(0x1a, data.offset)
data.length := sub(data.length, 0x1a)
// we don't check for array out-of-bounds here; we will check it later in `_getHookData`
}
key.fee = uint24(packed >> 184);
key.tickSpacing = int24(uint24(packed >> 160));
key.hooks = IHooks.wrap(address(uint160(packed)));
return (zeroForOne, data);
}
/// Decode an ABI-ish encoded `bytes` from `data`. It is "-ish" in the sense that the encoding
/// of the length doesn't take up an entire word. The length is encoded as only 3 bytes (2^24
/// bytes of calldata consumes ~67M gas, much more than the block limit). The payload is also
/// unpadded. The next fill's `bps` is encoded immediately after the `hookData` payload.
function _getHookData(bytes calldata data) private pure returns (bytes calldata hookData, bytes calldata retData) {
assembly ("memory-safe") {
hookData.length := shr(0xe8, calldataload(data.offset))
hookData.offset := add(0x03, data.offset)
let hop := add(0x03, hookData.length)
retData.offset := add(data.offset, hop)
retData.length := sub(data.length, hop)
if gt(retData.length, 0xffffff) { // length underflow
mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)`
mstore(0x20, 0x32) // array out-of-bounds
revert(0x1c, 0x24)
}
}
}
/// `_take` is responsible for removing the accumulated credit in each token from the pool
/// manager. The current `state.buy` is the global buy token. We return the settled amount of
/// that token (`buyAmount`), after checking it against the slippage limit
/// (`minBuyAmount`). Each token with credit causes a corresponding call to `POOL_MANAGER.take`.
function _take(StateLib.State memory state, NotesLib.Note[] memory notes, address recipient, uint256 minBuyAmount)
private
returns (uint256 buyAmount)
{
notes.del(state.buy);
if (state.sell.amount == 0) {
notes.del(state.sell);
}
uint256 length = notes.length;
// `length` of zero implies that we fully liquidated the global sell token (there is no
// `amount` remaining) and that the only token in which we have credit is the global buy
// token. We're about to `take` that token below.
if (length != 0) {
{
NotesLib.Note memory firstNote = notes[0]; // out-of-bounds is impossible
if (!firstNote.eq(state.globalSell)) {
// The global sell token being in a position other than the 1st would imply that
// at some point we _bought_ that token. This is illegal and results in a revert
// with reason `BoughtSellToken(address)`.
IPoolManager(msg.sender).unsafeTake(firstNote.token, address(this), firstNote.amount);
}
}
for (uint256 i = 1; i < length; i = i.unsafeInc()) {
(IERC20 token, uint256 amount) = notes.unsafeGet(i);
IPoolManager(msg.sender).unsafeTake(token, address(this), amount);
}
}
// The final token to be bought is considered the global buy token. We bypass `notes` and
// read it directly from `state`. Check the slippage limit. Transfer to the recipient.
{
IERC20 buyToken = state.buy.token;
buyAmount = state.buy.amount;
if (buyAmount < minBuyAmount) {
revert TooMuchSlippage(buyToken, minBuyAmount, buyAmount);
}
IPoolManager(msg.sender).unsafeTake(buyToken, recipient, buyAmount);
}
}
function _pay(
IERC20 sellToken,
address payer,
uint256 sellAmount,
ISignatureTransfer.PermitTransferFrom calldata permit,
bool isForwarded,
bytes calldata sig
) private returns (uint256) {
IPoolManager(msg.sender).unsafeSync(sellToken);
if (payer == address(this)) {
sellToken.safeTransfer(msg.sender, sellAmount);
} else {
// assert(payer == address(0));
ISignatureTransfer.SignatureTransferDetails memory transferDetails =
ISignatureTransfer.SignatureTransferDetails({to: msg.sender, requestedAmount: sellAmount});
_transferFrom(permit, transferDetails, sig, isForwarded);
}
return IPoolManager(msg.sender).unsafeSettle();
}
function _initialize(bytes calldata data, bool feeOnTransfer, uint256 hashMul, uint256 hashMod, address payer)
private
returns (
bytes calldata newData,
StateLib.State memory state,
NotesLib.Note[] memory notes,
ISignatureTransfer.PermitTransferFrom calldata permit,
bool isForwarded,
bytes calldata sig
)
{
{
IERC20 sellToken;
assembly ("memory-safe") {
sellToken := shr(0x60, calldataload(data.offset))
}
// We don't advance `data` here because there's a special interaction between `payer`
// (which is the 20 bytes in calldata immediately before `data`), `sellToken`, and
// `permit` that's handled below.
notes = state.construct(sellToken, hashMul, hashMod);
}
// This assembly block is just here to appease the compiler. We only use `permit` and `sig`
// in the codepaths where they are set away from the values initialized here.
assembly ("memory-safe") {
permit := calldatasize()
sig.offset := calldatasize()
sig.length := 0x00
}
if (state.globalSell.token == ETH_ADDRESS) {
assert(payer == address(this));
uint16 bps;
assembly ("memory-safe") {
// `data` hasn't been advanced from decoding `sellToken` above. so we have to
// implicitly advance it by 20 bytes to decode `bps` then advance by 22 bytes
bps := shr(0x50, calldataload(data.offset))
data.offset := add(0x16, data.offset)
data.length := sub(data.length, 0x16)
// We check for array out-of-bounds below
}
unchecked {
state.globalSell.amount = (address(this).balance * bps).unsafeDiv(BASIS);
}
} else {
if (payer == address(this)) {
uint16 bps;
assembly ("memory-safe") {
// `data` hasn't been advanced from decoding `sellToken` above. so we have to
// implicitly advance it by 20 bytes to decode `bps` then advance by 22 bytes
bps := shr(0x50, calldataload(data.offset))
data.offset := add(0x16, data.offset)
data.length := sub(data.length, 0x16)
// We check for array out-of-bounds below
}
unchecked {
state.globalSell.amount =
(state.globalSell.token.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS);
}
} else {
assert(payer == address(0));
assembly ("memory-safe") {
// this is super dirty, but it works because although `permit` is aliasing in
// the middle of `payer`, because `payer` is all zeroes, it's treated as padding
// for the first word of `permit`, which is the sell token
permit := sub(data.offset, 0x0c)
isForwarded := and(0x01, calldataload(add(0x55, data.offset)))
// `sig` is packed at the end of `data`, in "reverse ABI-ish encoded" fashion
sig.offset := sub(add(data.offset, data.length), 0x03)
sig.length := shr(0xe8, calldataload(sig.offset))
sig.offset := sub(sig.offset, sig.length)
// Remove `permit` and `isForwarded` from the front of `data`
data.offset := add(0x75, data.offset)
if gt(data.offset, sig.offset) { revert(0x00, 0x00) }
// Remove `sig` from the back of `data`
data.length := sub(sub(data.length, 0x78), sig.length)
// We check for array out-of-bounds below
}
state.globalSell.amount = _permitToSellAmountCalldata(permit);
}
if (feeOnTransfer) {
state.globalSell.amount =
_pay(state.globalSell.token, payer, state.globalSell.amount, permit, isForwarded, sig);
}
}
if (data.length > 16777215) {
Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
}
if (state.globalSell.amount == 0) {
revert ZeroSellAmount(state.globalSell.token);
}
state.globalSellAmount = state.globalSell.amount;
newData = data;
}
function unlockCallback(bytes calldata data) private returns (bytes memory) {
// These values are user-supplied
address recipient;
uint256 minBuyAmount;
uint256 hashMul;
uint256 hashMod;
bool feeOnTransfer;
assembly ("memory-safe") {
recipient := shr(0x60, calldataload(data.offset))
let packed := calldataload(add(0x14, data.offset))
minBuyAmount := shr(0x80, packed)
hashMul := and(0xffffffffffffffffffffffffffffffff, packed)
packed := calldataload(add(0x34, data.offset))
hashMod := shr(0x80, packed)
feeOnTransfer := iszero(iszero(and(0x1000000000000000000000000000000, packed)))
data.offset := add(0x45, data.offset)
data.length := sub(data.length, 0x45)
// we don't check for array out-of-bounds here; we will check it later in `_initialize`
}
// `payer` is special and is authenticated
address payer;
assembly ("memory-safe") {
payer := shr(0x60, calldataload(data.offset))
data.offset := add(0x14, data.offset)
data.length := sub(data.length, 0x14)
// we don't check for array out-of-bounds here; we will check it later in `_initialize`
}
// Set up `state` and `notes`. The other values are ancillary and might be used when we need
// to settle global sell token debt at the end of swapping.
(
bytes calldata newData,
StateLib.State memory state,
NotesLib.Note[] memory notes,
ISignatureTransfer.PermitTransferFrom calldata permit,
bool isForwarded,
bytes calldata sig
) = _initialize(data, feeOnTransfer, hashMul, hashMod, payer);
data = newData;
// Now that we've unpacked and decoded the header, we can begin decoding the array of swaps
// and executing them.
IPoolManager.PoolKey memory key;
IPoolManager.SwapParams memory params;
while (data.length >= _HOP_DATA_LENGTH) {
uint16 bps;
assembly ("memory-safe") {
bps := shr(0xf0, calldataload(data.offset))
data.offset := add(0x02, data.offset)
data.length := sub(data.length, 0x02)
// we don't check for array out-of-bounds here; we will check it later in `_getHookData`
}
data = _updateState(state, notes, data);
bool zeroForOne;
(zeroForOne, data) = _setPoolKey(key, state, data);
bytes calldata hookData;
(hookData, data) = _getHookData(data);
params.zeroForOne = zeroForOne;
unchecked {
params.amountSpecified = int256((state.sell.amount * bps).unsafeDiv(BASIS)).unsafeNeg();
}
// TODO: price limits
params.sqrtPriceLimitX96 = zeroForOne ? 4295128740 : 1461446703485210103287273052203988822378723970341;
BalanceDelta delta = IPoolManager(msg.sender).unsafeSwap(key, params, hookData);
{
(int256 settledSellAmount, int256 settledBuyAmount) =
zeroForOne ? (delta.amount0(), delta.amount1()) : (delta.amount1(), delta.amount0());
// Some insane hooks may increase the sell amount; obviously this may result in
// unavoidable reverts in some cases. But we still need to make sure that we don't
// underflow to avoid wildly unexpected behavior. The pool manager enforces that the
// settled sell amount cannot be positive
state.sell.amount -= uint256(settledSellAmount.unsafeNeg());
// If `state.buy.amount()` overflows an `int128`, we'll get a revert inside the pool
// manager later. We cannot overflow a `uint256`.
unchecked {
state.buy.amount += settledBuyAmount.asCredit(state.buy.token);
}
}
}
// `data` has been consumed. All that remains is to settle out the net result of all the
// swaps. Any credit in any token other than `state.buy.token` will be swept to
// Settler. `state.buy.token` will be sent to `recipient`.
{
(IERC20 globalSellToken, uint256 globalSellAmount) = (state.globalSell.token, state.globalSell.amount);
uint256 globalBuyAmount = _take(state, notes, recipient, minBuyAmount);
if (feeOnTransfer) {
// We've already transferred the sell token to the pool manager and
// `settle`'d. `globalSellAmount` is the verbatim credit in that token stored by the
// pool manager. We only need to handle the case of incomplete filling.
if (globalSellAmount != 0) {
IPoolManager(msg.sender).unsafeTake(
globalSellToken, payer == address(this) ? address(this) : _msgSender(), globalSellAmount
);
}
} else {
// While `notes` records a credit value, the pool manager actually records a debt
// for the global sell token. We recover the exact amount of that debt and then pay
// it.
// `globalSellAmount` is _usually_ zero, but if it isn't it represents a partial
// fill. This subtraction recovers the actual debt recorded in the pool manager.
uint256 debt;
unchecked {
debt = state.globalSellAmount - globalSellAmount;
}
if (debt == 0) {
revert ZeroSellAmount(globalSellToken);
}
if (globalSellToken == ETH_ADDRESS) {
IPoolManager(msg.sender).unsafeSettle(debt);
} else {
_pay(globalSellToken, payer, debt, permit, isForwarded, sig);
}
}
bytes memory returndata;
assembly ("memory-safe") {
returndata := mload(0x40)
mstore(returndata, 0x60)
mstore(add(0x20, returndata), 0x20)
mstore(add(0x40, returndata), 0x20)
mstore(add(0x60, returndata), globalBuyAmount)
mstore(0x40, add(0x80, returndata))
}
return returndata;
}
}
}