This repository has been archived by the owner on Jan 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
LimitOrderRegistry.sol
1525 lines (1365 loc) · 64.4 KB
/
LimitOrderRegistry.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
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
// Used to interact with ERC20 tokens
import { ERC20 } from "@solmate/tokens/ERC20.sol";
// Used to perform the safeTransfer
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";
// Used for chainlink automation
import { AutomationCompatibleInterface } from "@chainlink/contracts/src/v0.8/interfaces/AutomationCompatibleInterface.sol";
// used to have an owner for our contract
import { Owned } from "@solmate/auth/Owned.sol";
// for interacting with uniswap pools,
import { UniswapV3Pool } from "src/interfaces/uniswapV3/UniswapV3Pool.sol";
// for creating uniswap NFT positions
import { NonFungiblePositionManager } from "src/interfaces/uniswapV3/NonFungiblePositionManager.sol";
// dealing with NFTs
import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
// used to maintain upkeep
import { LinkTokenInterface } from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
// keeper for chainlink automation
import { IKeeperRegistrar, RegistrationParams } from "src/interfaces/chainlink/IKeeperRegistrar.sol";
// used to define _msgSender() for future metatransaction support
import { Context } from "@openzeppelin/contracts/utils/Context.sol";
// used to get the gas price from the chainlink gas oracle
import { IChainlinkAggregator } from "src/interfaces/chainlink/IChainlinkAggregator.sol";
/**
* @title Limit Order Registry
* @notice Allows users to create decentralized limit orders.
* @dev DO NOT PLACE LIMIT ORDERS FOR STRONGLY CORRELATED ASSETS.
* - If a stable coin pair were to temporarily depeg, and a user places a limit order
* whose tick range encompasses the normal trading tick, there is NO way to cancel the order
* because the order is mixed. The user would have to wait for another depeg event to happen
* so that the order can be fulfilled, or the order can be cancelled.
* @author crispymangoes
*/
contract LimitOrderRegistry is Owned, AutomationCompatibleInterface, ERC721Holder, Context {
using SafeTransferLib for ERC20;
using SafeTransferLib for address;
/*//////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/
/**
* @notice Stores linked list center values, and frequently used pool values.
* @param centerHead Linked list center value closer to head of the list
* @param centerTail Linked list center value closer to tail of the list
* @param token0 ERC20 token0 of the pool
* @param token1 ERC20 token1 of the pool
* @param fee Uniswap V3 pool fee
*/
struct PoolData {
uint256 centerHead;
uint256 centerTail;
ERC20 token0;
ERC20 token1;
uint24 fee;
}
/**
* @notice Stores information about batches of orders.
* @dev User orders can be batched together if they share the same target price.
* @param direction Determines what direction the tick must move in order for the order to be filled
* - true, pool tick must INCREASE to fill this order
* - false, pool tick must DECREASE to fill this order
* @param tickUpper The upper tick of the underlying LP position
* @param tickLower The lower tick of the underlying LP position
* @param userCount The number of users in this batch order
* @param batchId Unique id used to distinguish this batch order from another batch order in the past that used the same LP position
* @param token0Amount The amount of token0 in this order
* @param token1Amount The amount of token1 in this order
* @param head The next node in the linked list when moving toward the head
* @param tail The next node in the linked list when moving toward the tail
*/
struct BatchOrder {
bool direction;
int24 tickUpper;
int24 tickLower;
uint64 userCount;
uint128 batchId;
uint128 token0Amount;
uint128 token1Amount;
uint256 head;
uint256 tail;
}
/**
* @notice Stores information needed for users to make claims.
* @param pool The Uniswap V3 pool the batch order was in
* @param token0Amount The amount of token0 in the order
* @param token1Amount The amount of token1 in the order
* @param feePerUser The native token fee that must be paid on order claiming
* @param direction The underlying order direction, used to determine input/output token of the order
* @param isReadyForClaim Explicit bool indicating whether or not this order is ready to be claimed
*/
struct Claim {
UniswapV3Pool pool;
uint128 token0Amount; //Can either be the deposit amount or the amount got out of liquidity changing to the other token
uint128 token1Amount;
uint128 feePerUser; // Fee in terms of network native asset.
bool direction; //Determines the token out
bool isReadyForClaim;
}
/**
* @notice Struct used to store variables needed during order creation.
* @param tick The target tick of the order
* @param upper The upper tick of the underlying LP position
* @param lower The lower tick of the underlying LP position
* @param userTotal The total amount of assets the user has in the order
* @param positionId The underling LP position token id this order is adding liquidity to
* @param amount0 Can be the amount of assets user added to the order, based off orders direction
* @param amount1 Can be the amount of assets user added to the order, based off orders direction
*/
struct OrderDetails {
int24 tick;
int24 upper;
int24 lower;
uint128 userTotal;
uint256 positionId;
uint128 amount0;
uint128 amount1;
}
/*//////////////////////////////////////////////////////////////
GLOBAL STATE
//////////////////////////////////////////////////////////////*/
/**
* @notice Stores swap fees earned from limit order where the input token earns swap fees.
*/
mapping(address => uint256) public tokenToSwapFees;
/**
* @notice Used to store claim information needed when users are claiming their orders.
*/
mapping(uint128 => Claim) public claim;
/**
* @notice Stores the pools center head/tail, as well as frequently read values.
*/
mapping(UniswapV3Pool => PoolData) public poolToData;
/**
* @notice Maps tick ranges to LP positions owned by this contract.
* @dev maps pool -> direction -> lower -> upper -> positionId
*/
mapping(UniswapV3Pool => mapping(bool => mapping(int24 => mapping(int24 => uint256)))) public getPositionFromTicks;
/**
* @notice The minimum amount of assets required to create a `newOrder`.
* @dev Changeable by owner.
*/
mapping(ERC20 => uint256) public minimumAssets;
/**
* @notice Approximated amount of gas needed to fulfill 1 BatchOrder.
* @dev Changeable by owner.
*/
uint32 public upkeepGasLimit = 300_000;
/**
* @notice Approximated gas price used to fulfill orders.
* @dev Changeable by owner.
*/
uint32 public upkeepGasPrice = 30;
/**
* @notice Max number of orders that can be filled in 1 upkeep call.
* @dev Changeable by owner.
*/
uint16 public maxFillsPerUpkeep = 10;
/**
* @notice Value is incremented whenever a new BatchOrder is added to the `orderBook`.
* @dev Zero is reserved.
*/
uint128 public batchCount = 1;
/**
* @notice Mapping is used to store user deposit amounts in each BatchOrder.
*/
mapping(uint128 => mapping(address => uint128)) public batchIdToUserDepositAmount;
/**
* @notice The `orderBook` maps Uniswap V3 token ids to BatchOrder information.
* @dev Each BatchOrder contains a head and tail value which effectively,
* which means BatchOrders are connected using a doubly linked list.
*/
mapping(uint256 => BatchOrder) public orderBook;
/**
* @notice Chainlink Automation Registrar contract.
*/
IKeeperRegistrar public registrar;
/**
* @notice Whether or not the contract is shutdown in case of an emergency.
*/
bool public isShutdown;
/**
* @notice Chainlink Fast Gas Feed.
* @dev Feed for ETH Mainnet 0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C.
*/
address public fastGasFeed;
/**
* @notice The max possible gas the owner can set for the gas limit.
*/
uint32 public constant MAX_GAS_LIMIT = 750_000;
/**
* @notice The max possible gas price the owner can set for the gas price.
* @dev In units of gwei.
*/
uint32 public constant MAX_GAS_PRICE = 1_000;
/**
* @notice The max number of orders that can be fulfilled in a single upkeep TX.
*/
uint16 public constant MAX_FILLS_PER_UPKEEP = 20;
/**
* @notice The ETH Fast Gas Feed heartbeat.
* @dev If answer is stale, owner set gas price is used.
*/
uint256 public constant FAST_GAS_HEARTBEAT = 7200;
/**
* @notice Function signature used to create V1 Upkeep versions.
*/
string private constant FUNC_SIGNATURE = "register(string,bytes,address,uint32,address,bytes,uint96,uint8,address)";
/**
* @notice Function selector used to create V1 Upkeep versions.
*/
bytes4 private constant FUNC_SELECTOR = bytes4(keccak256(bytes(FUNC_SIGNATURE)));
/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/
/**
* @notice Prevent a function from being called during a shutdown.
*/
modifier whenNotShutdown() {
if (isShutdown) revert LimitOrderRegistry__ContractShutdown();
_;
}
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
// On new limit order created
event NewOrder(address user, address pool, uint128 amount, uint128 userTotal, BatchOrder affectedOrder);
// On limit order claimed
event ClaimOrder(address user, uint128 batchId, uint256 amount);
// On limit order cancelled
event CancelOrder(address user, uint128 amount0, uint128 amount1, BatchOrder affectedOrder);
// On limit order filled
event OrderFilled(uint256 batchId, address pool);
// On change of shutdown condition
event ShutdownChanged(bool isShutdown);
// On setup of limit order for the pool
event LimitOrderSetup(address pool);
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
// @notice When attempting to submit an order that is ITM
error LimitOrderRegistry__OrderITM(int24 currentTick, int24 targetTick, bool direction);
// @notice When attempting to setup a pool that is already setup
error LimitOrderRegistry__PoolAlreadySetup(address pool);
// @notice When pool is not setup and one tries to make an order
error LimitOrderRegistry__PoolNotSetup(address pool);
// @notice When an invalid target tick is provided for creating an order
error LimitOrderRegistry__InvalidTargetTick(int24 targetTick, int24 tickSpacing);
// @notice When the user for the batchid is not found
error LimitOrderRegistry__UserNotFound(address user, uint256 batchId);
// @notice When the position id is invalid
error LimitOrderRegistry__InvalidPositionId();
// @notice When there is no liquidity in the order
error LimitOrderRegistry__NoLiquidityInOrder();
// @notice When there are no orders to fulfill
error LimitOrderRegistry__NoOrdersToFulfill();
// @notice When the center is ITM
error LimitOrderRegistry__CenterITM();
// @notice When the order does not appear in the linked list
error LimitOrderRegistry__OrderNotInList(uint256 tokenId);
// @notice When the minimum for the asset is not set
error LimitOrderRegistry__MinimumNotSet(address asset);
// @notice When the minimum for the asset is not met
error LimitOrderRegistry__MinimumNotMet(address asset, uint256 minimum, uint256 amount);
// @notice When the tick range specified is incorrect
error LimitOrderRegistry__InvalidTickRange(int24 upper, int24 lower);
// @notice When there are no fees to withdraw
error LimitOrderRegistry__ZeroFeesToWithdraw(address token);
// @notice When there is no native balance to withdraw
error LimitOrderRegistry__ZeroNativeBalance();
// @notice When an invalid batch id is provided
error LimitOrderRegistry__InvalidBatchId();
// @notice When the order is not yet ready to claim
error LimitOrderRegistry__OrderNotReadyToClaim(uint128 batchId);
// @notice When the contract is shutdown and actions are performed
error LimitOrderRegistry__ContractShutdown();
// @notice When the contract is not shutdown and shutdown is attempted to be ended
error LimitOrderRegistry__ContractNotShutdown();
// @notice When an invalid gas limit, above the max, is provided
error LimitOrderRegistry__InvalidGasLimit();
// @notice When an invalid gas price, above the max, is provided
error LimitOrderRegistry__InvalidGasPrice();
// @notice When the fills for the upkeep are invalid
error LimitOrderRegistry__InvalidFillsPerUpkeep();
// @notice When amounts should be 0 but is not
error LimitOrderRegistry__AmountShouldBeZero();
// @notice When direction for the order doesn't match what state has.
error LimitOrderRegistry__DirectionMisMatch();
/*//////////////////////////////////////////////////////////////
ENUMS
//////////////////////////////////////////////////////////////*/
// @notice OrderStatus is used to show the status of an order
enum OrderStatus {
// When the order is through the book, it is considered ITM. this means that either the order was filled, or it should not exist.
ITM,
// When an order is not yet filled, it is considered OTM.
OTM,
// When the current tick is within the ticks of the limit order, it is considered MIXED.
MIXED
}
/*//////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
// @notice the token addres of the wrapped native token
ERC20 public immutable WRAPPED_NATIVE; // Mainnet 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
// @notice the address of the position manager
NonFungiblePositionManager public immutable POSITION_MANAGER; // Mainnet 0xC36442b4a4522E871399CD717aBDD847Ab11FE88
// @notice the address of the link token
LinkTokenInterface public immutable LINK; // Mainnet 0x514910771AF9Ca656af840dff83E8264EcF986CA
constructor(
address _owner,
NonFungiblePositionManager _positionManager,
ERC20 wrappedNative,
LinkTokenInterface link,
IKeeperRegistrar _registrar,
address _fastGasFeed
) Owned(_owner) {
POSITION_MANAGER = _positionManager;
WRAPPED_NATIVE = wrappedNative;
LINK = link;
registrar = _registrar;
fastGasFeed = _fastGasFeed;
}
/*//////////////////////////////////////////////////////////////
OWNER LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice No input validation is done because it is in the owners best interest to choose a valid registrar.
*/
function setRegistrar(IKeeperRegistrar _registrar) external onlyOwner {
registrar = _registrar;
}
/**
* @notice Allows owner to set the fills per upkeep.
*/
function setMaxFillsPerUpkeep(uint16 newVal) external onlyOwner {
if (newVal == 0 || newVal > MAX_FILLS_PER_UPKEEP) revert LimitOrderRegistry__InvalidFillsPerUpkeep();
maxFillsPerUpkeep = newVal;
}
/**
* @notice Allows owner to setup a new limit order for a new pool.
* @param pool The uniswap v3 pool to setup limit orders for
* @param initialUpkeepFunds the amount of initial upkeep funds to provide for the pool
* @dev New Limit orders, should have a keeper to fulfill orders.
* @dev If `initialUpkeepFunds` is zero, upkeep creation is skipped.
*/
function setupLimitOrder(UniswapV3Pool pool, uint256 initialUpkeepFunds) external onlyOwner {
// Check if Limit Order is already setup for `pool`.
if (address(poolToData[pool].token0) != address(0)) revert LimitOrderRegistry__PoolAlreadySetup(address(pool));
// Create Upkeep, transfering funds only if initialUpkeepFunds is above 0.
if (initialUpkeepFunds > 0) {
// Owner wants to automatically create an upkeep for new pool.
ERC20(address(LINK)).safeTransferFrom(owner, address(this), initialUpkeepFunds);
// check the upkeep registration version
if (bytes(registrar.typeAndVersion())[16] == bytes("1")[0]) {
// Use V1 Upkeep Registration.
bytes memory data = abi.encodeWithSelector(
FUNC_SELECTOR,
"Limit Order Registry",
abi.encode(0),
address(this),
uint32(maxFillsPerUpkeep * upkeepGasLimit),
owner,
abi.encode(pool),
uint96(initialUpkeepFunds),
77,
address(this)
);
LINK.transferAndCall(address(registrar), initialUpkeepFunds, data);
} else {
// Use V2 Upkeep Registration.
ERC20(address(LINK)).safeApprove(address(registrar), initialUpkeepFunds);
RegistrationParams memory params = RegistrationParams({
name: "Limit Order Registry",
encryptedEmail: abi.encode(0),
upkeepContract: address(this),
gasLimit: uint32(maxFillsPerUpkeep * upkeepGasLimit),
adminAddress: owner,
checkData: abi.encode(pool),
offchainConfig: abi.encode(0),
amount: uint96(initialUpkeepFunds)
});
registrar.registerUpkeep(params);
}
}
// initialize the data for the pool. the center and tail of the linked list are 0, while the tokens and fees are set to the correct
// ones for the pool
poolToData[pool] = PoolData({
centerHead: 0,
centerTail: 0,
token0: ERC20(pool.token0()),
token1: ERC20(pool.token1()),
fee: pool.fee()
});
emit LimitOrderSetup(address(pool));
}
/**
* @notice Allows owner to set the minimum assets used to create `newOrder`s.
* @param amount the amount to set minimum assets for the token to
* @param asset the erc20 token address to set minimum assets for
* @dev This value can be zero, but then this contract can be griefed by an attacker spamming low liquidity orders.
*/
function setMinimumAssets(uint256 amount, ERC20 asset) external onlyOwner {
minimumAssets[asset] = amount;
}
/**
* @notice Allows owner to change the gas limit value used to determine the Native asset fee needed to claim orders.
* @param gasLimit the gas limit that the upkeepGasLimit will be set to
* @dev premium should be factored into this value.
*/
function setUpkeepGasLimit(uint32 gasLimit) external onlyOwner {
// should revert if the gas limit provided is greater than the provided max gas limit.
if (gasLimit > MAX_GAS_LIMIT) revert LimitOrderRegistry__InvalidGasLimit();
upkeepGasLimit = gasLimit;
}
/**
* @notice Allows owner to change the gas price used to determine the Native asset fee needed to claim orders.
* @param gasPrice the gas limit that the upkeepGasPrice will be set to
* @dev `gasPrice` uses units of gwei.
*/
function setUpkeepGasPrice(uint32 gasPrice) external onlyOwner {
// should revert if the gas price provided is greater than the provided max gas price.
if (gasPrice > MAX_GAS_PRICE) revert LimitOrderRegistry__InvalidGasPrice();
upkeepGasPrice = gasPrice;
}
/**
* @notice Allows owner to set the fast gas feed.
* @param feed the address of the chainlink-compatible gas oracle
* @dev The feed should be a chainlink-compatible gas oracle
*/
function setFastGasFeed(address feed) external onlyOwner {
fastGasFeed = feed;
}
/**
* @notice Allows owner to withdraw swap fees earned from the input token of orders.
* @param tokenFeeIsIn the address of the token fee.
*/
function withdrawSwapFees(address tokenFeeIsIn) external onlyOwner {
uint256 fee = tokenToSwapFees[tokenFeeIsIn];
// Make sure there are actually fees to withdraw;
if (fee == 0) revert LimitOrderRegistry__ZeroFeesToWithdraw(tokenFeeIsIn);
// set fees to 0
tokenToSwapFees[tokenFeeIsIn] = 0;
// transfer fees to the user
ERC20(tokenFeeIsIn).safeTransfer(owner, fee);
}
/**
* @notice Allows owner to withdraw wrapped native and native assets from this contract.
*/
function withdrawNative() external onlyOwner {
uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
uint256 nativeBalance = address(this).balance;
// Make sure there is something to withdraw.
if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();
// transfer wrappedNativeBalance if it exists
if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
// transfer nativeBalance if it exists
if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
}
/**
* @notice Shutdown the registry. Used in an emergency or if the registry has been deprecated.
*/
function initiateShutdown() external whenNotShutdown onlyOwner {
isShutdown = true;
emit ShutdownChanged(true);
}
/**
* @notice Restart the registry.
*/
function liftShutdown() external onlyOwner {
if (!isShutdown) revert LimitOrderRegistry__ContractNotShutdown();
isShutdown = false;
emit ShutdownChanged(false);
}
/*//////////////////////////////////////////////////////////////
USER ORDER MANAGEMENT LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Creates a new limit order for a specific pool.
* @dev Limit orders can be created to buy either token0, or token1 of the pool.
* @param pool the Uniswap V3 pool to create a limit order on.
* @param targetTick the tick, that when `pool`'s tick passes, the order will be completely fulfilled
* @param amount the amount of the input token to sell for the desired token out
* @param direction bool indicating what the desired token out is
* - true token in = token0 ; token out = token1
* - false token in = token1 ; token out = token0
* @param startingNode an NFT position id indicating where this contract should start searching for a spot in the list
* - can be zero which defaults to starting the search at center of list
* @dev reverts if
* - pool is not setup
* - targetTick is not divisible by the pools tick spacing
* - the new order would be ITM, or in a MIXED state
* - the new order does not meet minimum liquidity requirements
* - transferFrom fails
* @dev Emits a `NewOrder` event which contains meta data about the order including the orders `batchId`(which is used for claiming/cancelling).
*/
function newOrder(
UniswapV3Pool pool,
int24 targetTick,
uint128 amount,
bool direction,
uint256 startingNode,
uint256 deadline
) external whenNotShutdown returns (uint128) {
if (address(poolToData[pool].token0) == address(0)) revert LimitOrderRegistry__PoolNotSetup(address(pool));
address sender = _msgSender();
// Transfer assets into contract before setting/checking any state.
{
// if direction is true, it means that assetIn is token0
ERC20 assetIn = direction ? poolToData[pool].token0 : poolToData[pool].token1;
// we make sure that there is enough amount of the assetIn
_enforceMinimumLiquidity(amount, assetIn);
// and transfer
assetIn.safeTransferFrom(sender, address(this), amount);
}
OrderDetails memory details;
// initialize the order =details tick with the pool slot0 value
(, details.tick, , , , , ) = pool.slot0();
// Determine upper and lower ticks.
{
// we need to grab the tickspace from the pool because we must send our order so that it aligns to ticks.
int24 tickSpacing = pool.tickSpacing();
// Make sure targetTick is divisible by spacing.
if (targetTick % tickSpacing != 0) revert LimitOrderRegistry__InvalidTargetTick(targetTick, tickSpacing);
if (direction) {
// if assetIn is token0, then the limit goes from targetTick to targetTick-tickSpacing
details.upper = targetTick;
details.lower = targetTick - tickSpacing;
} else {
// if assetIn is token1, then the limit goes from targetTick to targetTick+tickSpacing
details.upper = targetTick + tickSpacing;
details.lower = targetTick;
}
}
// Validate lower, upper,and direction.
{
OrderStatus status = _getOrderStatus(details.tick, details.lower, details.upper, direction);
// forbid orders that are "ITM". basically, if your order is through the market, then instead of limit order, you should swap
if (status != OrderStatus.OTM) revert LimitOrderRegistry__OrderITM(details.tick, targetTick, direction);
}
// Get the position id. this is the underlying position that we are managing
details.positionId = getPositionFromTicks[pool][direction][details.lower][details.upper];
// the amount should match with the tokenIn, so if tokenIn is token0, then set amount0, else set amount1
if (direction) details.amount0 = amount;
else details.amount1 = amount;
// check if the position exists in the nft contract
if (details.positionId == 0) {
// Create new LP position(which adds liquidity)
PoolData memory data = poolToData[pool];
details.positionId = _mintPosition(
data,
details.upper,
details.lower,
details.amount0,
details.amount1,
direction,
deadline
);
// Add it to the list.
_addPositionToList(data, startingNode, targetTick, details.positionId, direction);
// Set new orders upper and lower tick.
orderBook[details.positionId].tickLower = details.lower;
orderBook[details.positionId].tickUpper = details.upper;
// Setup BatchOrder, setting batchId, direction.
_setupOrder(direction, details.positionId);
// Update token0Amount, token1Amount, batchIdToUserDepositAmount mapping.
details.userTotal = _updateOrder(details.positionId, sender, amount);
// Update the center values if need be.
_updateCenter(pool, details.positionId, details.tick, details.upper, details.lower);
// Update getPositionFromTicks since we have a new LP position.
getPositionFromTicks[pool][direction][details.lower][details.upper] = details.positionId;
} else {
// Check if the position id is already being used in List.
BatchOrder memory order = orderBook[details.positionId];
if (order.token0Amount > 0 || order.token1Amount > 0) {
// Check that supplied direction and order.direction are the same.
if (direction != order.direction) revert LimitOrderRegistry__DirectionMisMatch();
// Need to add liquidity.
PoolData memory data = poolToData[pool];
_addToPosition(data, details.positionId, details.amount0, details.amount1, direction, deadline);
// Update token0Amount, token1Amount, batchIdToUserDepositAmount mapping.
details.userTotal = _updateOrder(details.positionId, sender, amount);
} else {
// We already have an LP position with given tick ranges, but it is not in linked list.
PoolData memory data = poolToData[pool];
// Add it to the list.
_addPositionToList(data, startingNode, targetTick, details.positionId, direction);
// Setup BatchOrder, setting batchId, direction.
_setupOrder(direction, details.positionId);
// Need to add liquidity.
_addToPosition(data, details.positionId, details.amount0, details.amount1, direction, deadline);
// Update token0Amount, token1Amount, batchIdToUserDepositAmount mapping.
details.userTotal = _updateOrder(details.positionId, sender, amount);
// Update the center values if need be.
_updateCenter(pool, details.positionId, details.tick, details.upper, details.lower);
}
}
// emit the event
emit NewOrder(sender, address(pool), amount, details.userTotal, orderBook[details.positionId]);
// return the batch id of the position
return orderBook[details.positionId].batchId;
}
/**
* @notice Users can claim fulfilled orders by passing in the `batchId` corresponding to the order they want to claim.
* @param batchId the batchId corresponding to a fulfilled order to claim
* @param user the address of the user in the order to claim for
* @return address erc20 address
* @return uint256 amount claimed
* @dev Caller must either approve this contract to spend their Wrapped Native token, and have at least `getFeePerUser` tokens in their wallet.
* Or caller must send `getFeePerUser` value with this call.
*/
function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
Claim storage userClaim = claim[batchId];
if (!userClaim.isReadyForClaim) revert LimitOrderRegistry__OrderNotReadyToClaim(batchId);
uint256 depositAmount = batchIdToUserDepositAmount[batchId][user];
if (depositAmount == 0) revert LimitOrderRegistry__UserNotFound(user, batchId);
// Zero out user balance.
delete batchIdToUserDepositAmount[batchId][user];
// Calculate owed amount.
uint256 totalTokenDeposited;
uint256 totalTokenOut;
ERC20 tokenOut;
// again, remembering that direction == true means that the input token is token0.
if (userClaim.direction) {
totalTokenDeposited = userClaim.token0Amount;
totalTokenOut = userClaim.token1Amount;
tokenOut = poolToData[userClaim.pool].token1;
} else {
totalTokenDeposited = userClaim.token1Amount;
totalTokenOut = userClaim.token0Amount;
tokenOut = poolToData[userClaim.pool].token0;
}
uint256 owed = (totalTokenOut * depositAmount) / totalTokenDeposited;
// Transfer tokens owed to user.
tokenOut.safeTransfer(user, owed);
// Transfer fee in.
address sender = _msgSender();
if (msg.value >= userClaim.feePerUser) {
// refund if necessary.
uint256 refund = msg.value - userClaim.feePerUser;
if (refund > 0) sender.safeTransferETH(refund);
} else {
WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
// If value is non zero send it back to caller.
if (msg.value > 0) sender.safeTransferETH(msg.value);
}
emit ClaimOrder(user, batchId, owed);
return (tokenOut, owed);
}
/**
* @notice Allows users to cancel orders as long as they are completely OTM.
* @param pool the Uniswap V3 pool that contains the limit order to cancel
* @param targetTick the targetTick of the order you want to cancel
* @param direction bool indication the direction of the order
* @return amount0 amount0 withdrawn
* @return amount1 amount1 withdrawn
* @return batchId batch id withdrawn
*/
function cancelOrder(
UniswapV3Pool pool,
int24 targetTick,
bool direction,
uint256 deadline
) external returns (uint128 amount0, uint128 amount1, uint128 batchId) {
// defined here since we want to grab it out of the closure
uint256 positionId;
// this is in a closure to avoid namespace pollusion
{
// Make sure order is OTM.
(, int24 tick, , , , , ) = pool.slot0();
// Determine upper and lower ticks.
int24 upper;
int24 lower;
{
int24 tickSpacing = pool.tickSpacing();
// Make sure targetTick is divisible by spacing.
if (targetTick % tickSpacing != 0) revert LimitOrderRegistry__InvalidTargetTick(targetTick, tickSpacing);
if (direction) {
upper = targetTick;
lower = targetTick - tickSpacing;
} else {
upper = targetTick + tickSpacing;
lower = targetTick;
}
}
// Validate lower, upper,and direction. Make sure order is not ITM or MIXED
{
OrderStatus status = _getOrderStatus(tick, lower, upper, direction);
if (status != OrderStatus.OTM) revert LimitOrderRegistry__OrderITM(tick, targetTick, direction);
}
// Get the position id.
positionId = getPositionFromTicks[pool][direction][lower][upper];
if (positionId == 0) revert LimitOrderRegistry__InvalidPositionId();
}
// Get the users deposit amount in the order.
BatchOrder storage order = orderBook[positionId];
// the batch id can't be zero
if (order.batchId == 0) revert LimitOrderRegistry__InvalidBatchId();
// this variable is set conditionally, so we declare it here first
uint256 liquidityPercentToTake;
// grab sender for shorthand.
address sender = _msgSender();
{
// set the batchId return variable
batchId = order.batchId;
// grab the deposit amount from the mapping
uint128 depositAmount = batchIdToUserDepositAmount[batchId][sender];
// if there is no deposit, then there is nothing to do for this batch
if (depositAmount == 0) revert LimitOrderRegistry__UserNotFound(sender, batchId);
// Remove one from the userCount.
order.userCount--;
// Zero out user balance.
delete batchIdToUserDepositAmount[batchId][sender];
uint128 orderAmount;
// remember, direction=true, tokenIn = token0
if (order.direction) {
orderAmount = order.token0Amount;
if (orderAmount == depositAmount) {
liquidityPercentToTake = 1e18;
// Update order tokenAmount.
order.token0Amount = 0;
} else {
liquidityPercentToTake = (1e18 * uint256(depositAmount)) / orderAmount;
// Update order tokenAmount.
order.token0Amount = orderAmount - depositAmount;
}
} else {
orderAmount = order.token1Amount;
if (orderAmount == depositAmount) {
liquidityPercentToTake = 1e18;
// Update order tokenAmount.
order.token1Amount = 0;
} else {
liquidityPercentToTake = (1e18 * uint256(depositAmount)) / orderAmount;
// Update order tokenAmount.
order.token1Amount = orderAmount - depositAmount;
}
}
// actual movement of money is here
(amount0, amount1) = _takeFromPosition(positionId, pool, liquidityPercentToTake, deadline);
// emit event
emit CancelOrder(sender, amount0, amount1, order);
// special case for percent is 100%
if (liquidityPercentToTake == 1e18) {
_removeOrderFromList(positionId, pool, order);
// Zero out balances for cancelled order.
order.token0Amount = 0;
order.token1Amount = 0;
order.batchId = 0;
}
}
if (order.direction) {
if (amount0 > 0) poolToData[pool].token0.safeTransfer(sender, amount0);
else revert LimitOrderRegistry__NoLiquidityInOrder();
if (amount1 > 0) revert LimitOrderRegistry__AmountShouldBeZero();
} else {
if (amount1 > 0) poolToData[pool].token1.safeTransfer(sender, amount1);
else revert LimitOrderRegistry__NoLiquidityInOrder();
if (amount0 > 0) revert LimitOrderRegistry__AmountShouldBeZero();
}
}
/*//////////////////////////////////////////////////////////////
CHAINLINK AUTOMATION LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Returned `performData` simply contains a bool indicating which direction in the `orderBook` has orders that need to be fulfilled.
* @param checkData the data to decode when calling this from automation
*/
function checkUpkeep(bytes calldata checkData) external view returns (bool upkeepNeeded, bytes memory performData) {
UniswapV3Pool pool = abi.decode(checkData, (UniswapV3Pool));
(, int24 currentTick, , , , , ) = pool.slot0();
PoolData memory data = poolToData[pool];
BatchOrder memory order;
OrderStatus status;
bool walkDirection;
uint256 deadline = block.timestamp + 900;
if (data.centerHead != 0) {
// centerHead is set, check if it is ITM.
order = orderBook[data.centerHead];
status = _getOrderStatus(currentTick, order.tickLower, order.tickUpper, order.direction);
if (status == OrderStatus.ITM) {
walkDirection = true; // Walk towards head of list.
upkeepNeeded = true;
performData = abi.encode(pool, walkDirection, deadline);
return (upkeepNeeded, performData);
}
}
if (data.centerTail != 0) {
// If walk direction has not been set, then we know, no head orders are ITM.
// So check tail orders.
order = orderBook[data.centerTail];
status = _getOrderStatus(currentTick, order.tickLower, order.tickUpper, order.direction);
if (status == OrderStatus.ITM) {
walkDirection = false; // Walk towards tail of list.
upkeepNeeded = true;
performData = abi.encode(pool, walkDirection, deadline);
return (upkeepNeeded, performData);
}
}
return (false, abi.encode(0));
}
/**
* @notice Callable by anyone, as long as there are orders ITM, that need to be fulfilled.
* @param performData checkData the data to decode when calling this from automation
* @dev Does not use _removeOrderFromList, so that the center head/tail
* value is not updated every single time and order is fulfilled, instead we just update it once at the end.
*/
function performUpkeep(bytes calldata performData) external {
(UniswapV3Pool pool, bool walkDirection, uint256 deadline) = abi.decode(
performData,
(UniswapV3Pool, bool, uint256)
);
if (address(poolToData[pool].token0) == address(0)) revert LimitOrderRegistry__PoolNotSetup(address(pool));
PoolData storage data = poolToData[pool];
// Estimate gas cost.
uint256 estimatedFee = uint256(upkeepGasLimit * getGasPrice());
(, int24 currentTick, , , , , ) = pool.slot0();
bool orderFilled;
// Fulfill orders.
uint256 target = walkDirection ? data.centerHead : data.centerTail;
for (uint256 i; i < maxFillsPerUpkeep; ++i) {
if (target == 0) break;
BatchOrder storage order = orderBook[target];
OrderStatus status = _getOrderStatus(currentTick, order.tickLower, order.tickUpper, order.direction);
if (status == OrderStatus.ITM) {
_fulfillOrder(target, pool, order, estimatedFee, deadline);
target = walkDirection ? order.head : order.tail;
// Reconnect List and Zero out orders head and tail values removing order from the list.
orderBook[order.tail].head = order.head;
orderBook[order.head].tail = order.tail;
order.head = 0;
order.tail = 0;
// Update bool to indicate batch order is ready to handle claims.
claim[order.batchId].isReadyForClaim = true;
// Zero out orders batch id.
order.batchId = 0;
// Reset user count.
order.userCount = 0;
orderFilled = true;
} else break;
}
if (!orderFilled) revert LimitOrderRegistry__NoOrdersToFulfill();
// Update appropriate center value.
if (walkDirection) {
data.centerHead = target;
} else {
data.centerTail = target;
}
}
/*//////////////////////////////////////////////////////////////
INTERNAL ORDER LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Check if a given Uniswap V3 position is already in the `orderBook`.
* @dev Looks at Nodes head and tail, and checks for edge case of node being the only node in the `orderBook`
*/
function _checkThatNodeIsInList(uint256 node, BatchOrder memory order, PoolData memory data) internal pure {
if (order.head == 0 && order.tail == 0) {
// Possible but the order may be centerTail or centerHead.
if (data.centerHead != node && data.centerTail != node) revert LimitOrderRegistry__OrderNotInList(node);
}
}
/**
* @notice Finds appropriate spot in `orderBook` for an order.
* @param data the linked list in memory
* @param startingNode the index to start searching from
* @param targetTick the tick to stop searching at
* @param direction the direction to traverse the list
* @dev implemented as a linked list
*/
function _findSpot(
PoolData memory data,
uint256 startingNode,
int24 targetTick,
bool direction
) internal view returns (uint256 proposedHead, uint256 proposedTail) {
BatchOrder memory node;
if (startingNode == 0) {
if (direction && data.centerHead != 0) {
startingNode = data.centerHead;
node = orderBook[startingNode];
} else if (!direction && data.centerTail != 0) {
startingNode = data.centerTail;
node = orderBook[startingNode];