-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFixedFeeSwapMarket.sol
More file actions
1245 lines (1080 loc) · 54.4 KB
/
Copy pathFixedFeeSwapMarket.sol
File metadata and controls
1245 lines (1080 loc) · 54.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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.26;
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {LiquidityMath} from "v4-core/libraries/LiquidityMath.sol";
import {SafeCast} from "v4-core/libraries/SafeCast.sol";
import {TickBitmap} from "v4-core/libraries/TickBitmap.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {BeforeSwapDelta} from "v4-core/types/BeforeSwapDelta.sol";
import {ModifyLiquidityParams, SwapParams} from "v4-core/types/PoolOperation.sol";
import {toBeforeSwapDelta} from "v4-core/types/BeforeSwapDelta.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
struct TickInfo {
// the total position liquidity that references this tick
uint128 liquidityGross;
// amount of net liquidity added (subtracted) when tick is crossed from left to right (right to
// left),
int128 liquidityNet;
// fee growth per unit of liquidity on the _other_ side of this tick (relative to the current
// tick)
// only has relative meaning, not absolute — the value depends on when the tick is initialized
uint256 feeGrowthOutside0X128;
uint256 feeGrowthOutside1X128;
}
struct TokenDelta {
int128 amount0;
int128 amount1;
}
struct PositionInfo {
address owner;
int24 tickLower;
int24 tickUpper;
int128 liquidity;
// Fee tracking
uint256 feeGrowthInside0LastX128;
uint256 feeGrowthInside1LastX128;
uint128 tokensOwed0;
uint128 tokensOwed1;
}
/// @dev Minimal struct for swap step context to avoid stack-too-deep
struct SwapContext {
bool principalForCash;
bool tickMovingLeft;
int24 tickLimit;
}
contract FixedFeeSwapMarket is BaseHook {
using SafeCast for uint256;
using SafeCast for int256;
using FixedPointMathLib for int256;
using FixedPointMathLib for int128;
using SafeCast for int128;
using SafeERC20 for IERC20;
using TickBitmap for mapping(int16 => uint256);
// ============ Events ============
/// @notice Emitted when fees are collected from a position
event FeesCollected(
bytes32 indexed positionId, address indexed recipient, uint128 amount0, uint128 amount1
);
/// @notice Emitted when a swap is executed
event Swap(address indexed sender, bool principalForCash, uint256 amountIn, uint256 amountOut);
/// @notice Emitted when liquidity is modified for a position
event LiquidityModified(
bytes32 indexed positionId,
address indexed owner,
int24 tickLower,
int24 tickUpper,
int128 liquidityDelta,
int128 amount0,
int128 amount1
);
// ============ Errors ============
/// @notice Thrown when the fee rate is 100% or higher
error FixedFeeSwapMarket_FeeRateTooHigh();
/// @notice Thrown when yield spacing is zero or negative
error FixedFeeSwapMarket_InvalidYieldSpacing();
/// @notice Thrown when trying to remove more liquidity than the position holds
error FixedFeeSwapMarket_InsufficientPositionLiquidity();
/// @notice Thrown when swap hook parameters are malformed or have incorrect length
error FixedFeeSwapMarket_InvalidHookParams();
/// @notice Thrown when liquidity amount is zero or exceeds slippage bounds
error FixedFeeSwapMarket_InvalidLiquidityAmount();
/// @notice Thrown when price moves in an unexpected direction during swap calculation
error FixedFeeSwapMarket_InvalidPriceDirection();
/// @notice Thrown when the lower tick is greater than or equal to the upper tick
error FixedFeeSwapMarket_InvalidTickRange();
/// @notice Thrown when tick traversal produces an invalid delta or zero boundary amount
error FixedFeeSwapMarket_InvalidTickTraversal();
/// @notice Thrown when decreaseLiquidity is called with a non-negative liquidity delta
error FixedFeeSwapMarket_LiquidityDeltaMustBeNegative();
/// @notice Thrown when attempting to swap with no active liquidity in range
error FixedFeeSwapMarket_NoActiveLiquidity();
/// @notice Thrown when the caller is not the owner of the position
error FixedFeeSwapMarket_NotPositionOwner();
/// @notice Thrown when the specified position does not exist
error FixedFeeSwapMarket_PositionNotFound();
/// @notice Thrown when the swap amount is zero or negative
error FixedFeeSwapMarket_SwapAmountCannotBeZero();
/// @notice Thrown when a swap or time-dependent calculation is attempted at or after maturity
error FixedFeeSwapMarket_MarketExpired();
/// @notice Thrown when a tick's gross liquidity would underflow
error FixedFeeSwapMarket_TickLiquidityGrossUnderflow();
/// @notice Thrown when liquidity is added or removed via PoolManager instead of this hook
error FixedFeeSwapMarket_LiquidityOnlyViaHook();
// ============ Constants ============
int24 constant NO_TICK_LIMIT = type(int24).max;
uint256 constant WAD = 1e18;
uint256 constant BIPS = 10_000;
uint256 constant SWAP_HOOK_PARAMS_LENGTH = 96;
/// @notice Denominator for fee calculations, set to 1e6 to support hundredths of a bip
uint256 constant FEE_DENOMINATOR = 1_000_000;
/// @notice Q128 fixed-point multiplier for fee growth calculations
uint256 constant Q128 = 1 << 128;
// ============ Immutables ============
uint48 immutable MATURITY;
uint48 immutable START_TIME;
IERC20 immutable TOKEN0;
IERC20 immutable TOKEN1;
/// @notice Fee rate in hundredths of a bip, where a value of 1 equals 0.0001%
/// @dev 3000 = 0.30% fee, similar to Uniswap v3 medium tier
uint24 public immutable FEE_RATE;
/// @notice Yield movement (in bps) represented by one tick step
int24 public immutable YIELD_PER_TICK;
// ============ State Variables ============
int24 currentTick;
int256 currentPrice;
uint128 activeLiquidity;
/// @notice Global fee growth per unit of liquidity for TOKEN0 (principal)
uint256 public feeGrowthGlobal0X128;
/// @notice Global fee growth per unit of liquidity for TOKEN1 (cash)
uint256 public feeGrowthGlobal1X128;
// ============ Mappings ============
mapping(int24 tick => TickInfo) ticks;
mapping(int16 wordPos => uint256) tickBitmap;
mapping(bytes32 positionId => PositionInfo) positions;
// ============ Constructor ============
constructor(
uint48 _lengthOfMarket,
address _token0,
address _token1,
uint24 _feeRate,
int24 _yieldPerTick,
IPoolManager _manager,
int24 _initialTick
) BaseHook(_manager) {
if (_feeRate >= FEE_DENOMINATOR) revert FixedFeeSwapMarket_FeeRateTooHigh();
if (_yieldPerTick <= 0) revert FixedFeeSwapMarket_InvalidYieldSpacing();
START_TIME = uint48(block.timestamp);
MATURITY = uint48(block.timestamp) + _lengthOfMarket;
TOKEN0 = IERC20(_token0);
TOKEN1 = IERC20(_token1);
FEE_RATE = _feeRate;
YIELD_PER_TICK = _yieldPerTick;
currentTick = _initialTick;
}
// ============ Public Functions ============
/// @notice Returns the hook permissions indicating which callbacks this hook implements
/// @return The permissions struct with beforeSwap enabled
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: true,
afterAddLiquidity: false,
beforeRemoveLiquidity: true,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
/// @notice Converts a yield in basis points to the corresponding tick
/// @param _yieldBps The yield rate in basis points
/// @return The tick value representing this yield
function yieldToTick(int256 _yieldBps) public view returns (int24) {
return int24(_yieldBps / int256(YIELD_PER_TICK));
}
/// @notice Converts a tick to its corresponding discount factor price
/// @dev Price is calculated as exp(-yield * scaledTimeRemaining) where yield = tick *
/// YIELD_PER_TICK. At START_TIME, remaining = duration so prices are fully discounted.
/// At MATURITY, all prices converge to 1.0 (PT redeemable 1:1).
/// @param _tick The tick value to convert
/// @return The price as a WAD-scaled discount factor, where 1e18 represents a price of 1.0
function tickToPrice(int256 _tick) public view returns (int256) {
if (block.timestamp >= MATURITY) return int256(WAD);
uint256 _remaining = MATURITY - block.timestamp;
uint256 _duration = MATURITY - START_TIME;
uint256 _scaledTime = (_remaining * WAD) / _duration;
return _priceFromYieldWad(_tickToYieldWad(_tick), _scaledTime);
}
// ============ External Functions ============
/// @notice Creates a new liquidity position or adds to an existing one at the same tick range
/// @dev Position ID is derived from tickUpper, tickLower, and msg.sender
/// @param _tickLower The lower tick boundary of the position, representing the lower yield
/// @param _tickUpper The upper tick boundary of the position, representing the higher yield
/// @param _liquidity The amount of liquidity to mint
/// @param _amount0Max Maximum TOKEN0 principal tokens willing to deposit
/// @param _amount1Max Maximum TOKEN1 cash tokens willing to deposit
function mintLiquidity(
int24 _tickLower,
int24 _tickUpper,
uint256 _liquidity,
uint256 _amount0Max,
uint256 _amount1Max
) external virtual {
if (_tickLower < 0) revert FixedFeeSwapMarket_InvalidTickRange();
bytes32 _positionId = _getPositionId(msg.sender, _tickUpper, _tickLower);
PositionInfo storage position = positions[_positionId];
bool _isNew = position.owner == address(0);
int128 _liquidityBefore = position.liquidity;
if (_isNew) {
position.tickUpper = _tickUpper;
position.tickLower = _tickLower;
position.owner = msg.sender;
}
// Call modifyLiquidity first so tick init sets feeGrowthOutside correctly
TokenDelta memory _tokenDelta =
_modifyLiquidity(msg.sender, _tickUpper, _tickLower, _liquidity.toInt128());
uint128 _amount0In = _tokenDelta.amount0.toUint128();
uint128 _amount1In = _tokenDelta.amount1.toUint128();
if (_amount0In > _amount0Max || _amount1In > _amount1Max) {
revert FixedFeeSwapMarket_InvalidLiquidityAmount();
}
// Update liquidity without overwriting fee fields
position.liquidity = position.liquidity + _liquidity.toInt128();
// Initialize snapshots when liquidity transitions from 0 -> positive
// (isNew implies liquidityBefore == 0, so we only need to check liquidityBefore)
if (_liquidityBefore == 0) _refreshFeeSnapshots(position);
if (_amount0In != 0) TOKEN0.safeTransferFrom(msg.sender, address(this), _amount0In);
if (_amount1In != 0) TOKEN1.safeTransferFrom(msg.sender, address(this), _amount1In);
}
/// @notice Increases liquidity for an existing position
/// @dev Caller must be the position owner. Accrues pending fees before adding liquidity.
/// @param _positionId The unique identifier of the position
/// @param _liquidity The amount of liquidity to add
/// @param _amount0Max Maximum TOKEN0 principal tokens willing to deposit
/// @param _amount1Max Maximum TOKEN1 cash tokens willing to deposit
function increaseLiquidity(
bytes32 _positionId,
uint256 _liquidity,
uint256 _amount0Max,
uint256 _amount1Max
) external virtual {
PositionInfo storage position = _requirePositionOwner(_positionId);
int128 _liquidityBefore = position.liquidity;
// modifyLiquidity accrues fees internally and refreshes snapshots
TokenDelta memory _tokenDelta =
_modifyLiquidity(msg.sender, position.tickUpper, position.tickLower, _liquidity.toInt128());
uint128 _amount0In = _tokenDelta.amount0.toUint128();
uint128 _amount1In = _tokenDelta.amount1.toUint128();
if (_amount0In > _amount0Max || _amount1In > _amount1Max) {
revert FixedFeeSwapMarket_InvalidLiquidityAmount();
}
// Update position liquidity without overwriting fee fields
position.liquidity = position.liquidity + _liquidity.toInt128();
// If the position had zero liquidity, refresh snapshots to prevent earning past fees
if (_liquidityBefore == 0) _refreshFeeSnapshots(position);
if (_amount0In != 0) TOKEN0.safeTransferFrom(msg.sender, address(this), _amount0In);
if (_amount1In != 0) TOKEN1.safeTransferFrom(msg.sender, address(this), _amount1In);
}
/// @notice Decreases liquidity from an existing position and withdraws tokens
/// @dev Caller must be the position owner. Accrues pending fees before removing liquidity.
/// @param _positionId The unique identifier of the position
/// @param _liquidity The amount of liquidity to remove, must be negative
/// @param _amount0Min Minimum TOKEN0 principal tokens to receive
/// @param _amount1Min Minimum TOKEN1 cash tokens to receive
function decreaseLiquidity(
bytes32 _positionId,
int128 _liquidity,
uint256 _amount0Min,
uint256 _amount1Min
) external virtual {
PositionInfo storage position = _requirePositionOwner(_positionId);
if (_liquidity >= 0) revert FixedFeeSwapMarket_LiquidityDeltaMustBeNegative();
if (uint128(position.liquidity) < uint128(_liquidity.abs())) {
revert FixedFeeSwapMarket_InsufficientPositionLiquidity();
}
// modifyLiquidity accrues fees owed before burning liquidity
TokenDelta memory _tokenDelta =
_modifyLiquidity(msg.sender, position.tickUpper, position.tickLower, _liquidity);
uint128 _amount0Out = uint128(_tokenDelta.amount0.abs());
uint128 _amount1Out = uint128(_tokenDelta.amount1.abs());
if (_amount0Out < _amount0Min || _amount1Out < _amount1Min) {
revert FixedFeeSwapMarket_InvalidLiquidityAmount();
}
// Update position liquidity without overwriting fee fields
position.liquidity = position.liquidity + _liquidity;
if (_tokenDelta.amount0 < 0) TOKEN0.safeTransfer(msg.sender, _amount0Out);
if (_tokenDelta.amount1 < 0) TOKEN1.safeTransfer(msg.sender, _amount1Out);
}
/// @notice Collects accumulated fees for a position
/// @param _positionId The position identifier
/// @param _recipient Address to receive the fees
/// @param _amount0Requested Maximum TOKEN0 fees to collect, or 0 to collect all available
/// @param _amount1Requested Maximum TOKEN1 fees to collect, or 0 to collect all available
/// @return _amount0 Actual TOKEN0 fees collected
/// @return _amount1 Actual TOKEN1 fees collected
function collectFees(
bytes32 _positionId,
address _recipient,
uint128 _amount0Requested,
uint128 _amount1Requested
) external returns (uint128 _amount0, uint128 _amount1) {
PositionInfo storage position = _requirePositionOwner(_positionId);
// Accrue pending fees before collection
_accruePositionFees(position);
// Calculate amounts to collect (0 means collect all, otherwise min of requested and owed)
_amount0 = (_amount0Requested == 0 || _amount0Requested > position.tokensOwed0)
? position.tokensOwed0
: _amount0Requested;
_amount1 = (_amount1Requested == 0 || _amount1Requested > position.tokensOwed1)
? position.tokensOwed1
: _amount1Requested;
// Update owed amounts (CEI: update state before transfers)
position.tokensOwed0 -= _amount0;
position.tokensOwed1 -= _amount1;
// Transfer fees
if (_amount0 > 0) TOKEN0.safeTransfer(_recipient, _amount0);
if (_amount1 > 0) TOKEN1.safeTransfer(_recipient, _amount1);
emit FeesCollected(_positionId, _recipient, _amount0, _amount1);
}
/// @notice Returns the current market price, computing from tick if not yet initialized
/// @return The current price in WAD precision
function getCurrentPrice() external view returns (int256) {
return currentPrice != 0 ? currentPrice : tickToPrice(currentTick);
}
/// @notice Swaps tokens directly without going through PoolManager
/// @param _amountIn The amount of input tokens
/// @param _principalForCash True to swap PT→cash, false to swap cash→PT
/// @param _tickLimit Tick limit (0 = no limit)
/// @return amountInUsed The amount of input tokens consumed
/// @return amountOut The amount of output tokens received
function swap(uint256 _amountIn, bool _principalForCash, int24 _tickLimit)
external
returns (uint256 amountInUsed, uint256 amountOut)
{
if (_amountIn == 0) revert FixedFeeSwapMarket_SwapAmountCannotBeZero();
if (block.timestamp >= MATURITY) revert FixedFeeSwapMarket_MarketExpired();
if (activeLiquidity == 0) revert FixedFeeSwapMarket_NoActiveLiquidity();
currentPrice = tickToPrice(currentTick);
int24 _effectiveTickLimit = _tickLimit == 0 ? NO_TICK_LIMIT : _tickLimit;
(amountInUsed, amountOut) = _executeSwapLoop(_amountIn, _principalForCash, _effectiveTickLimit);
// Execute token transfers
if (_principalForCash) {
// User sends principal (TOKEN0), receives cash (TOKEN1)
if (amountInUsed != 0) TOKEN0.safeTransferFrom(msg.sender, address(this), amountInUsed);
if (amountOut != 0) TOKEN1.safeTransfer(msg.sender, amountOut);
} else {
// User sends cash (TOKEN1), receives principal (TOKEN0)
if (amountInUsed != 0) TOKEN1.safeTransferFrom(msg.sender, address(this), amountInUsed);
if (amountOut != 0) TOKEN0.safeTransfer(msg.sender, amountOut);
}
emit Swap(msg.sender, _principalForCash, amountInUsed, amountOut);
}
// ============ Internal Functions - Hook Overrides ============
/// @notice Hook called before liquidity is added via PoolManager
/// @dev Always reverts because liquidity must be managed through this contract's mint/increase
/// functions
function _beforeAddLiquidity(
address,
PoolKey calldata,
ModifyLiquidityParams calldata,
bytes calldata
) internal virtual override returns (bytes4) {
revert FixedFeeSwapMarket_LiquidityOnlyViaHook();
}
/// @notice Hook called before liquidity is removed via PoolManager
/// @dev Always reverts because liquidity must be managed through this contract's decrease
/// function
function _beforeRemoveLiquidity(
address,
PoolKey calldata,
ModifyLiquidityParams calldata,
bytes calldata
) internal virtual override returns (bytes4) {
revert FixedFeeSwapMarket_LiquidityOnlyViaHook();
}
/// @notice Hook called before a swap is executed via PoolManager
/// @dev Hook data schema: `abi.encode(int256 amountIn, bool principalForCash, int24
/// tickLimit)`. If `principalForCash` is true, TOKEN0 is input and TOKEN1 is output, meaning
/// the tick increases and price decreases.
/// If `tickLimit` is `NO_TICK_LIMIT`, the swap only stops once `amountIn` is fully consumed.
/// amountIn is the gross amount before fees that the trader provides.
/// @param _sender The address initiating the swap
/// @param _hookParams Encoded swap parameters containing amountIn, principalForCash, and
/// tickLimit @return The hook selector, a zero delta because the swap is handled internally,
/// and zero fee override
function _beforeSwap(
address _sender,
PoolKey calldata,
SwapParams calldata,
bytes calldata _hookParams
) internal virtual override returns (bytes4, BeforeSwapDelta, uint24) {
if (_hookParams.length == 0) {
return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(0, 0), 0);
}
if (_hookParams.length != SWAP_HOOK_PARAMS_LENGTH) {
revert FixedFeeSwapMarket_InvalidHookParams();
}
(int256 _amountIn, bool _principalForCash, int24 _tickLimit) =
abi.decode(_hookParams, (int256, bool, int24));
if (_amountIn <= 0) revert FixedFeeSwapMarket_SwapAmountCannotBeZero();
if (block.timestamp >= MATURITY) revert FixedFeeSwapMarket_MarketExpired();
if (activeLiquidity == 0) revert FixedFeeSwapMarket_NoActiveLiquidity();
currentPrice = tickToPrice(currentTick);
(uint256 _amountInUsed, uint256 _amountOutCalculated) =
_executeSwapLoop(uint256(_amountIn), _principalForCash, _tickLimit);
// Execute token transfers
// TOKEN0 = principal token, TOKEN1 = cash token
if (_principalForCash) {
// User sends principal (TOKEN0), receives cash (TOKEN1)
if (_amountInUsed != 0) TOKEN0.safeTransferFrom(_sender, address(this), _amountInUsed);
if (_amountOutCalculated != 0) TOKEN1.safeTransfer(_sender, _amountOutCalculated);
} else {
// User sends cash (TOKEN1), receives principal (TOKEN0)
if (_amountInUsed != 0) TOKEN1.safeTransferFrom(_sender, address(this), _amountInUsed);
if (_amountOutCalculated != 0) TOKEN0.safeTransfer(_sender, _amountOutCalculated);
}
emit Swap(_sender, _principalForCash, _amountInUsed, _amountOutCalculated);
return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(0, 0), 0);
}
// ============ Internal Functions - Swap Execution ============
/// @notice Executes the main swap loop, iterating through ticks until input is consumed or tick
/// limit is hit
/// @param _amountInGross The gross input amount (before fees)
/// @param _principalForCash True for PT→cash swap, false for cash→PT swap
/// @param _tickLimit Tick limit; NO_TICK_LIMIT means no limit
/// @return _amountInUsed Total gross input consumed
/// @return _amountOutCalculated Total output tokens to send to trader
function _executeSwapLoop(uint256 _amountInGross, bool _principalForCash, int24 _tickLimit)
internal
returns (uint256 _amountInUsed, uint256 _amountOutCalculated)
{
uint256 _amountInNet = _netOfFee(_amountInGross);
uint256 _amountInRemainingNet = _amountInNet;
SwapContext memory _ctx = SwapContext({
principalForCash: _principalForCash, tickMovingLeft: !_principalForCash, tickLimit: _tickLimit
});
while (_amountInRemainingNet != 0) {
// Slippage protection: stop the swap if the tick has moved past the caller's limit.
// For principal→cash swaps, tick increases so we stop when tick reaches the limit.
// For cash→principal swaps, tick decreases so we stop when tick reaches the limit.
if (_ctx.tickLimit != NO_TICK_LIMIT) {
if (_ctx.principalForCash ? currentTick >= _ctx.tickLimit : currentTick <= _ctx.tickLimit) {
break;
}
}
// Find next initialized tick
int24 _nextTick;
bool _initialized;
{
int24 _searchTick = currentTick;
if (_ctx.tickMovingLeft) {
int256 _candidate = int256(currentTick) - int256(YIELD_PER_TICK);
_searchTick = _candidate < type(int24).min ? type(int24).min : int24(_candidate);
}
(_nextTick, _initialized) = tickBitmap.nextInitializedTickWithinOneWord(
_searchTick, YIELD_PER_TICK, _ctx.tickMovingLeft
);
}
// Compute swap step and update state
(uint256 _stepNet, uint256 _stepOut, int24 _newTick) =
_executeSwapStep(_amountInRemainingNet, _nextTick, _initialized, _ctx);
// If the step couldn't produce output (swap too small to move a full tick), stop.
if (_stepNet == 0) break;
_amountInRemainingNet -= _stepNet;
_amountOutCalculated += _stepOut;
currentTick = _newTick;
}
uint256 _totalNetUsed = _amountInNet - _amountInRemainingNet;
_amountInUsed =
FixedPointMathLib.mulDivUp(_totalNetUsed, FEE_DENOMINATOR, FEE_DENOMINATOR - FEE_RATE);
if (_amountInUsed > _amountInGross) _amountInUsed = _amountInGross;
}
/// @notice Executes a single swap step: calculates amounts, updates fees, crosses tick if needed
/// @param _amountInNet Net input amount (after fees) available for this step
/// @param _nextTick The next initialized tick boundary
/// @param _initialized Whether _nextTick is an initialized tick with liquidity changes
/// @param _ctx Swap context containing direction flags
function _executeSwapStep(
uint256 _amountInNet,
int24 _nextTick,
bool _initialized,
SwapContext memory _ctx
) internal returns (uint256, uint256, int24) {
(uint256 _netConsumed, uint256 _amountOut, int256 _stepPrice, int24 _stepTick) = _calculateSwap(
_amountInNet.toInt256(), currentPrice, _nextTick, _ctx.principalForCash, _ctx.tickLimit
);
// If the swap step couldn't move a full tick (sub-tick snap to zero), stop gracefully.
if (_netConsumed == 0 || _amountOut == 0) return (0, 0, currentTick);
// Compute fee for this step directly from net consumed and attribute to LPs.
// Per-step tracking is needed because activeLiquidity changes at tick crossings.
uint256 _stepFee = FixedPointMathLib.mulDiv(_netConsumed, FEE_RATE, FEE_DENOMINATOR - FEE_RATE);
if (_stepFee > 0 && activeLiquidity > 0) {
if (_ctx.principalForCash) {
feeGrowthGlobal0X128 += FixedPointMathLib.mulDiv(_stepFee, Q128, activeLiquidity);
} else {
feeGrowthGlobal1X128 += FixedPointMathLib.mulDiv(_stepFee, Q128, activeLiquidity);
}
}
// Cross tick if needed
bool _hitBoundary = _stepTick == _nextTick;
if (_hitBoundary && _initialized) {
_crossTick(_nextTick);
int128 _liquidityNet = ticks[_nextTick].liquidityNet;
if (_ctx.tickMovingLeft) _liquidityNet = -_liquidityNet;
activeLiquidity = LiquidityMath.addDelta(activeLiquidity, _liquidityNet);
}
// Update price and calculate new tick.
// The search offset (currentTick - YIELD_PER_TICK) is already applied in _executeSwapLoop,
// so we do NOT subtract here — that would double-adjust, pushing currentTick below the
// actually-traded price and inflating LP cash claims on withdrawal.
currentPrice = _stepPrice;
return (_netConsumed, _amountOut, _stepTick);
}
/// @notice Routes swap calculation to the appropriate direction-specific function
/// @param _amountIn The input amount to swap
/// @param _currentPrice The current price at the start of this step
/// @param _nextTick The next initialized tick boundary
/// @param _principalToCash True for PT→cash, false for cash→PT
/// @param _tickLimit Tick limit; NO_TICK_LIMIT means no limit
/// @return amountInConsumed Amount of input consumed
/// @return amountOut Amount of output produced
/// @return newPrice Price after the swap step
/// @return newTick Tick after the swap step
function _calculateSwap(
int256 _amountIn,
int256 _currentPrice,
int24 _nextTick,
bool _principalToCash,
int24 _tickLimit
) internal view returns (uint256, uint256, int256, int24) {
if (_principalToCash) {
return _getCashSwapAmount(_amountIn, _currentPrice, _nextTick, _tickLimit);
}
return _getPrincipalSwapAmount(_amountIn, _currentPrice, _nextTick, _tickLimit);
}
// ============ Internal Functions - PT→Cash Swap Math ============
//
// Worked example — PT→cash swap crossing an initialized tick boundary
//
// This function computes a single segment; `_executeSwapLoop` orchestrates multiple segments
// and handles tick crossing.
//
// Scenario: trader sells 9,000 PT for cash. Active segment liquidity L0 = 3,000,000.
// Next initialized tick is 25 bips away (650 − 625 = 25 bips).
//
// PT required to traverse exactly to the next boundary:
// PT_in_seg = L0 * (r0 − rNext)
// where r0 is the start yield, rNext is the boundary yield, L0 is the segment liquidity.
// Here 7,500 PT reaches the boundary; 1,500 PT remains for the next segment.
// (25 bips = 0.0025, so L0 = 7,500 / 0.0025 = 3,000,000 = 300 PT per bip.)
//
// Cash out for any segment is the integral of the discount-factor price over the yield move:
// CashOut = (Lseg / t) * (exp(-r0 * t) - exp(-r1 * t))
// where t is time-to-maturity, r0/r1 are the start/end yields for the segment.
//
// After crossing, the remaining PT moves yield within the next segment (L1 = 1,000,000):
// Δr = X / L1
// (1,500 / 1,000,000 = 0.0015 = 15 bips; L1 = 100 PT per bip.)
//
// Within-tick completion delegates to `_getCashSwapAmountWithinTick`.
// Total cash out = sum of per-segment cash outs.
/// @notice Calculate cash out for PT in, up to the next tick boundary
/// @param _amountIn The amount of PT available to trade for cash
/// @param _currentPrice The price calculated with the current yield tick
/// @param _nextTick The next initialized tick boundary
/// @param _tickLimit Tick limit; NO_TICK_LIMIT means no limit
/// @return _amountInConsumed PT consumed in this step
/// @return _cashOut Cash tokens to send to the trader
/// @return _newPrice Price after this step
/// @return _newTick Tick after this step; if equal to `_nextTick`, caller should cross the tick
function _getCashSwapAmount(
int256 _amountIn,
int256 _currentPrice,
int24 _nextTick,
int24 _tickLimit
)
internal
view
returns (uint256 _amountInConsumed, uint256 _cashOut, int256 _newPrice, int24 _newTick)
{
if (_amountIn <= 0) revert FixedFeeSwapMarket_SwapAmountCannotBeZero();
if (_currentPrice <= 0) revert FixedFeeSwapMarket_InvalidPriceDirection();
uint256 _scaledTime = _scaledTimeRemainingWad();
// Derive start yield directly from the integer tick to avoid exp/ln round-trip error.
// After integer-tick snapping, currentPrice == tickToPrice(currentTick), so the yield
// is exactly currentTick * WAD. Using _yieldWadFromPrice would introduce ~2000 wei of
// PT error per tick crossing, accumulating to insolvency over many crossings.
int256 _startYieldWad = _tickToYieldWad(currentTick);
// For PT→cash, tick increases. Use the tick limit as target if it's lower (closer) than
// the next tick boundary.
int256 _targetYieldWad = _tickToYieldWad(_nextTick);
int256 _targetPrice = tickToPrice(_nextTick);
if (_targetPrice > _currentPrice) revert FixedFeeSwapMarket_InvalidPriceDirection();
if (_targetYieldWad <= _startYieldWad) revert FixedFeeSwapMarket_InvalidTickTraversal();
if (_tickLimit != NO_TICK_LIMIT && _tickLimit < _nextTick) {
_nextTick = _tickLimit;
_targetYieldWad = _tickToYieldWad(_tickLimit);
_targetPrice = _priceFromYieldWad(_targetYieldWad, _scaledTime);
}
if (_targetPrice > _currentPrice) revert FixedFeeSwapMarket_InvalidPriceDirection();
if (_targetYieldWad <= _startYieldWad) revert FixedFeeSwapMarket_InvalidTickTraversal();
uint256 _principalToTarget = FixedPointMathLib.mulDivUp(
uint256(_targetYieldWad - _startYieldWad), uint256(activeLiquidity), BIPS * WAD
);
if (_principalToTarget == 0) revert FixedFeeSwapMarket_InvalidTickTraversal();
if (uint256(_amountIn) >= _principalToTarget) {
_amountInConsumed = _principalToTarget;
_newTick = _nextTick;
_newPrice = _targetPrice;
_cashOut = FixedPointMathLib.mulDiv(
uint256(activeLiquidity), uint256(_currentPrice - _newPrice), _scaledTime
);
return (_amountInConsumed, _cashOut, _newPrice, _newTick);
}
return _getCashSwapAmountWithinTick(uint256(_amountIn), _currentPrice, _startYieldWad);
}
/// @notice Computes within-tick swap for PT→cash using exact exponential math
/// @dev Uses expWad to handle fractional tick movements: endPrice = exp(-endYield / BIPS *
/// scaledTime) @param _amountIn The amount of principal tokens to swap
/// @param _startPrice The price at the start of this swap step
/// @param _startYieldWad The WAD-scaled yield at the start of this swap step
/// @return _amountInConsumed Amount of input consumed
/// @return _cashOut Amount of cash tokens output
/// @return _newPrice Price after the swap
/// @return _newTick Tick after the swap
function _getCashSwapAmountWithinTick(
uint256 _amountIn,
int256 _startPrice,
int256 _startYieldWad
)
internal
view
returns (uint256 _amountInConsumed, uint256 _cashOut, int256 _newPrice, int24 _newTick)
{
_amountInConsumed = _amountIn;
uint256 _scaledTime = _scaledTimeRemainingWad();
// Yield movement is linear in PT: Δyield = PT_in * BIPS / L
int256 _endYieldWad = _startYieldWad
+ int256(FixedPointMathLib.mulDiv(_amountIn, BIPS * WAD, uint256(activeLiquidity)));
// Snap to floored integer tick so swap output matches LP composition accounting.
// Returns zero consumed/output if the tick didn't move (swap too small).
_newTick = _wadToTick(_endYieldWad);
_newPrice = _priceFromYieldWad(_tickToYieldWad(_newTick), _scaledTime);
if (_newPrice >= _startPrice) return (0, 0, _startPrice, _wadToTick(_startYieldWad));
// Cash_out = (L / scaledTime) * (startPrice - newPrice)
_cashOut = FixedPointMathLib.mulDiv(
uint256(activeLiquidity), uint256(_startPrice - _newPrice), _scaledTime
);
}
// ============ Internal Functions - Cash→PT Swap Math ============
//
// Worked example — cash→PT swap crossing an initialized tick boundary
//
// This function computes a single segment; `_executeSwapLoop` orchestrates multiple segments
// and handles tick crossing.
//
// Scenario: t = 0.5, initial yield r0 = 665, next initialized tick rNext = 650.
// Segment 1 liquidity L1 = 3,500,000; after crossing, L2 = 1,000,000. Trader sells 6,000 cash.
//
// Cash required to reach the next tick boundary:
// Cash_in_seg = (L / t) * (exp(-rNext * t) - exp(-r * t))
//
// If cash budget >= Cash_in_seg, the swap reaches the boundary. The caller crosses the tick
// (updating feeGrowthOutside and activeLiquidity) then re-invokes with remaining cash.
//
// If cash budget < Cash_in_seg, the swap stays within the segment. Solving for end yield:
// r1 = -(1 / t) * ln( exp(-r0 * t) + (t / L) * C )
//
// Within-tick completion delegates to `_getPrincipalSwapAmountWithinTick`.
// Total PT out = sum of per-segment PT outs.
/// @notice Calculate PT out for cash in, up to the next tick boundary
/// @param _amountIn The amount of cash available to trade for PT
/// @param _currentPrice The price calculated with the current yield tick
/// @param _nextTick The next initialized tick boundary
/// @param _tickLimit Tick limit; NO_TICK_LIMIT means no limit
/// @return _amountInConsumed Cash consumed in this step
/// @return _principalOut PT tokens to send to the trader
/// @return _newPrice Price after this step
/// @return _newTick Tick after this step; if equal to `_nextTick`, caller should cross the tick
function _getPrincipalSwapAmount(
int256 _amountIn,
int256 _currentPrice,
int24 _nextTick,
int24 _tickLimit
)
internal
view
returns (uint256 _amountInConsumed, uint256 _principalOut, int256 _newPrice, int24 _newTick)
{
if (_amountIn <= 0) revert FixedFeeSwapMarket_SwapAmountCannotBeZero();
if (_currentPrice <= 0) revert FixedFeeSwapMarket_InvalidPriceDirection();
uint256 _scaledTime = _scaledTimeRemainingWad();
// Derive start yield directly from the integer tick (same rationale as _getCashSwapAmount).
int256 _startYieldWad = _tickToYieldWad(currentTick);
// For cash→PT, tick decreases. Use the tick limit as target if it's higher (closer) than
// the next tick boundary.
int256 _targetYieldWad = _tickToYieldWad(_nextTick);
int256 _targetPrice = tickToPrice(_nextTick);
if (_targetPrice < _currentPrice) revert FixedFeeSwapMarket_InvalidPriceDirection();
if (_targetYieldWad >= _startYieldWad) revert FixedFeeSwapMarket_InvalidTickTraversal();
if (_tickLimit != NO_TICK_LIMIT && _tickLimit > _nextTick) {
_nextTick = _tickLimit;
_targetYieldWad = _tickToYieldWad(_tickLimit);
_targetPrice = _priceFromYieldWad(_targetYieldWad, _scaledTime);
}
if (_targetPrice < _currentPrice) revert FixedFeeSwapMarket_InvalidPriceDirection();
if (_targetYieldWad >= _startYieldWad) revert FixedFeeSwapMarket_InvalidTickTraversal();
uint256 _priceDiff = uint256(_targetPrice - _currentPrice);
uint256 _cashInToTarget =
FixedPointMathLib.mulDivUp(_priceDiff, uint256(activeLiquidity), _scaledTime);
if (_cashInToTarget == 0) revert FixedFeeSwapMarket_InvalidTickTraversal();
if (uint256(_amountIn) >= _cashInToTarget) {
_amountInConsumed = _cashInToTarget;
_newTick = _nextTick;
_newPrice = _targetPrice;
uint256 _yieldDeltaWad = uint256(_startYieldWad - _targetYieldWad);
_principalOut = FixedPointMathLib.mulDiv(uint256(activeLiquidity), _yieldDeltaWad, BIPS * WAD);
return (_amountInConsumed, _principalOut, _newPrice, _newTick);
}
return _getPrincipalSwapAmountWithinTick(uint256(_amountIn), _currentPrice, _startYieldWad);
}
/// @notice Computes within-tick swap for cash→PT using exact logarithm math
/// @dev Uses lnWad to compute: endYield = -ln(newPrice) / scaledTime * BIPS, then PT_out = L *
/// (startYield - endYield) / BIPS @param _amountIn The amount of cash tokens to swap
/// @param _startPrice The price at the start of this swap step
/// @param _startYieldWad The WAD-scaled yield at the start of this swap step
/// @return _amountInConsumed Amount of input consumed
/// @return _principalOut Amount of principal tokens output
/// @return _newPrice Price after the swap
/// @return _newTick Tick after the swap
function _getPrincipalSwapAmountWithinTick(
uint256 _amountIn,
int256 _startPrice,
int256 _startYieldWad
)
internal
view
returns (uint256 _amountInConsumed, uint256 _principalOut, int256 _newPrice, int24 _newTick)
{
_amountInConsumed = _amountIn;
uint256 _scaledTime = _scaledTimeRemainingWad();
// newPrice = startPrice + (scaledTime / L) * cashIn
int256 _exactNewPrice = _startPrice
+ int256(FixedPointMathLib.mulDiv(_scaledTime, _amountIn, uint256(activeLiquidity)));
if (_exactNewPrice < _startPrice) revert FixedFeeSwapMarket_InvalidPriceDirection();
// endYieldWad = -ln(newPrice) / scaledTime * BIPS * WAD
int256 _endYieldWad = _yieldWadFromPrice(_exactNewPrice, _scaledTime);
// Snap to ceiled integer tick so swap output matches LP composition accounting.
// Cash→PT decreases yield, so ceiling rounds in favor of the pool (less PT output, higher
// price). Returns zero consumed/output if the tick didn't move (swap too small).
_newTick = _wadToTickCeil(_endYieldWad);
int256 _snappedYieldWad = _tickToYieldWad(_newTick);
_newPrice = _priceFromYieldWad(_snappedYieldWad, _scaledTime);
// PT_out = L * (startYield - snappedYield) / BIPS using WAD precision
int256 _yieldMovedWad = _startYieldWad - _snappedYieldWad;
if (_yieldMovedWad <= 0) return (0, 0, _startPrice, _wadToTickCeil(_startYieldWad));
_principalOut =
FixedPointMathLib.mulDiv(uint256(activeLiquidity), uint256(_yieldMovedWad), BIPS * WAD);
}
// ============ Internal Functions - Liquidity Management ============
/// @notice Internal function to modify liquidity for a position
/// @dev Handles tick updates, bitmap flips, active liquidity changes, and fee accrual
/// @param _owner The position owner address
/// @param _tickUpper The upper tick boundary of the position
/// @param _tickLower The lower tick boundary of the position
/// @param _liquidity The liquidity delta (positive to add, negative to remove)
/// @return The token amounts required to deposit (positive) or to withdraw (negative)
function _modifyLiquidity(address _owner, int24 _tickUpper, int24 _tickLower, int128 _liquidity)
internal
returns (TokenDelta memory)
{
if (_tickLower >= _tickUpper) revert FixedFeeSwapMarket_InvalidTickRange();
if (_liquidity == 0) return TokenDelta({amount0: 0, amount1: 0});
bytes32 _positionId = _getPositionId(_owner, _tickUpper, _tickLower);
PositionInfo storage position = positions[_positionId];
// Accrue fees before mutating ticks (must happen before tick data is cleared)
if (position.owner != address(0)) _accruePositionFees(position);
// Mutate ticks / bitmap / activeLiquidity
(bool _flippedLower,) = _updateTickLiquidity(_tickLower, _liquidity, false);
(bool _flippedUpper,) = _updateTickLiquidity(_tickUpper, _liquidity, true);
if (_flippedLower) {
tickBitmap.flipTick(_tickLower, YIELD_PER_TICK);
if (_liquidity < 0) delete ticks[_tickLower];
}
if (_flippedUpper) {
tickBitmap.flipTick(_tickUpper, YIELD_PER_TICK);
if (_liquidity < 0) delete ticks[_tickUpper];
}
// Get token amounts owed for the liquidity
// TOKEN0 = PT, TOKEN1 = Cash
// Below current tick (unvisited yields) → LP holds cash (TOKEN1)
// Above current tick (visited yields) → LP holds principal (TOKEN0)
TokenDelta memory _delta;
if (currentTick < _tickLower) {
// All liquidity at higher yields (unvisited) — LP holds only cash
_delta = TokenDelta({
amount0: 0, amount1: _getAmountCashDelta(_tickLower, _tickUpper, _liquidity).toInt128()
});
} else if (currentTick < _tickUpper) {
activeLiquidity = LiquidityMath.addDelta(activeLiquidity, _liquidity);
// Split at current position:
// [tickLower, currentTick] visited → principal (TOKEN0)
// [currentTick, tickUpper] unvisited → cash (TOKEN1)
_delta = TokenDelta({
amount0: _getAmountPrincipalDelta(_tickLower, currentTick, _liquidity).toInt128(),
amount1: _getAmountCashDelta(currentTick, _tickUpper, _liquidity).toInt128()
});
} else {
// All liquidity at lower yields (visited) — LP holds only principal
_delta = TokenDelta({
amount0: _getAmountPrincipalDelta(_tickLower, _tickUpper, _liquidity).toInt128(), amount1: 0
});
}
emit LiquidityModified(
_positionId, _owner, _tickLower, _tickUpper, _liquidity, _delta.amount0, _delta.amount1
);
return _delta;
}
/// @notice Updates the liquidity at a tick and initializes fee growth if tick is newly
/// initialized @param _tick The tick that will be updated
/// @param _liquidityDelta Amount of liquidity to be added when tick is crossed from left to
/// right, or subtracted when crossed from right to left
/// @param _upper true for updating a position's upper tick, or
/// false for updating a position's lower tick
/// @return flipped Whether the tick was flipped from initialized to uninitialized or vice versa
/// @return liquidityGrossAfter The gross liquidity at the tick after the update
function _updateTickLiquidity(int24 _tick, int128 _liquidityDelta, bool _upper)
internal
returns (bool, uint128)
{
TickInfo storage info = ticks[_tick];
uint128 _liquidityGrossBefore = info.liquidityGross;
int128 _liquidityNetBefore = info.liquidityNet;
uint128 _liquidityGrossAfter;
if (_liquidityDelta > 0) {
_liquidityGrossAfter = _liquidityGrossBefore + _liquidityDelta.toUint128();
} else {
if (_liquidityGrossBefore < _liquidityDelta.abs()) {
revert FixedFeeSwapMarket_TickLiquidityGrossUnderflow();
}
_liquidityGrossAfter = _liquidityGrossBefore - uint128(_liquidityDelta.abs());
}
bool _flipped = (_liquidityGrossAfter == 0) != (_liquidityGrossBefore == 0);
// Initialize fee growth outside when tick becomes initialized
// By convention, assume all prior fee growth occurred below this tick
if (_liquidityGrossBefore == 0 && _liquidityGrossAfter > 0) {
if (_tick <= currentTick) {
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
}
// If tick > currentTick, feeGrowthOutside stays 0 (all growth is below)
}
// when the lower (upper) tick is crossed left to right, liquidity must be added (removed)
// when the lower (upper) tick is crossed right to left, liquidity must be removed (added)
int128 _liquidityNet =