diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index 443e2528..b9d81858 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -384795 \ No newline at end of file +311137 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index d54245e8..c3edfa69 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -179162 \ No newline at end of file +122946 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index 8f92ae5c..b9e04365 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -128156 \ No newline at end of file +80287 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 3098aea8..7a0170eb 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1017534 \ No newline at end of file +1015181 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 35b55d27..4444368b 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -169368 \ No newline at end of file +110544 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index da17b718..1bc2d893 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -345987 \ No newline at end of file +240022 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index 9295bd7a..c1cac22b 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -89085 \ No newline at end of file +45997 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index 111771b5..97d86500 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -126958 \ No newline at end of file +79418 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index d1007040..03924f26 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -122853 \ No newline at end of file +122359 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 73c96768..e34af74b 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -222794 \ No newline at end of file +114257 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index 4d9543e1..9bf14262 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -167494 \ No newline at end of file +112378 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index af1b03da..79a741b2 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -128154 \ No newline at end of file +74001 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 58654a31..c8a011cf 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -136428 \ No newline at end of file +77793 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index a9b719e8..5d250ba5 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -475877 \ No newline at end of file +422785 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index 7ca9159e..95aa41f9 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -478504 \ No newline at end of file +475768 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d2dc450b..8e108254 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index 72bff2c4..7a31a8d9 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -8,6 +8,7 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {SafeCallback} from "./base/SafeCallback.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; +import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); @@ -40,7 +41,7 @@ abstract contract BaseHook is IHooks, SafeCallback { Hooks.validateHookPermissions(_this, getHookPermissions()); } - function _lockAcquired(bytes calldata data) internal virtual override returns (bytes memory) { + function _unlockCallback(bytes calldata data) internal virtual override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); @@ -86,7 +87,7 @@ abstract contract BaseHook is IHooks, SafeCallback { IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external virtual returns (bytes4) { + ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } @@ -96,14 +97,14 @@ abstract contract BaseHook is IHooks, SafeCallback { IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external virtual returns (bytes4) { + ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { revert HookNotImplemented(); } @@ -111,7 +112,7 @@ abstract contract BaseHook is IHooks, SafeCallback { function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, int128) { revert HookNotImplemented(); } diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index 9ad8df13..500e95d8 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -10,48 +10,35 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; -import {LiquidityRange, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FeeMath} from "./libraries/FeeMath.sol"; -import {PoolStateLibrary} from "./libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; // TODO: remove import {console2} from "forge-std/console2.sol"; -contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePositionManager, ERC721 { +contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721 { using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; using LiquidityRangeIdLibrary for LiquidityRange; - using PoolStateLibrary for IPoolManager; - /// @dev The ID of the next token that will be minted. Skips 0 + using StateLibrary for IPoolManager; + /// @dev The ID of the next token that will be minted. Skips 0 uint256 private _nextId = 1; - mapping(uint256 tokenId => Position position) public positions; - constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} - - // --- View Functions --- // - function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { - Position memory position = positions[tokenId]; + struct TokenPosition { + address owner; + LiquidityRange range; + } - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( - position.range.key.toId(), position.range.tickLower, position.range.tickUpper - ); + mapping(uint256 tokenId => TokenPosition position) public tokenPositions; - (token0Owed, token1Owed) = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); - token0Owed += position.tokensOwed0; - token1Owed += position.tokensOwed1; - } + constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {} // NOTE: more gas efficient as LiquidityAmounts is used offchain // TODO: deadline check @@ -62,131 +49,47 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi address recipient, bytes calldata hookData ) public payable returns (uint256 tokenId, BalanceDelta delta) { - delta = BaseLiquidityManagement.modifyLiquidity( - range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: int256(liquidity) - }), - hookData, - recipient - ); + delta = _increaseLiquidity(range, liquidity, hookData, false, msg.sender); // mint receipt token - // GAS: uncheck this mf _mint(recipient, (tokenId = _nextId++)); - - positions[tokenId] = Position({ - nonce: 0, - operator: address(0), - range: range, - liquidity: uint128(liquidity), - feeGrowthInside0LastX128: 0, // TODO: - feeGrowthInside1LastX128: 0, // TODO: - tokensOwed0: 0, - tokensOwed1: 0 - }); - - // TODO: event + tokenPositions[tokenId] = TokenPosition({owner: msg.sender, range: range}); } // NOTE: more expensive since LiquidityAmounts is used onchain - function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, params.range.key.toId()); - (tokenId, delta) = mint( - params.range, - LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtRatioAtTick(params.range.tickLower), - TickMath.getSqrtRatioAtTick(params.range.tickUpper), - params.amount0Desired, - params.amount1Desired - ), - params.deadline, - params.recipient, - params.hookData - ); - require(params.amount0Min <= uint256(uint128(delta.amount0())), "INSUFFICIENT_AMOUNT0"); - require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); - } - - function increaseLiquidity(IncreaseLiquidityParams memory params, bytes calldata hookData, bool claims) - public - isAuthorizedForToken(params.tokenId) + // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) { + // (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(params.range.key.toId()); + // (tokenId, delta) = mint( + // params.range, + // LiquidityAmounts.getLiquidityForAmounts( + // sqrtPriceX96, + // TickMath.getSqrtPriceAtTick(params.range.tickLower), + // TickMath.getSqrtPriceAtTick(params.range.tickUpper), + // params.amount0Desired, + // params.amount1Desired + // ), + // params.deadline, + // params.recipient, + // params.hookData + // ); + // require(params.amount0Min <= uint256(uint128(delta.amount0())), "INSUFFICIENT_AMOUNT0"); + // require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1"); + // } + + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) + external + isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - require(params.liquidityDelta != 0, "Must increase liquidity"); - Position storage position = positions[params.tokenId]; - - (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(position); - - delta = BaseLiquidityManagement.increaseLiquidity( - position.range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: position.range.tickLower, - tickUpper: position.range.tickUpper, - liquidityDelta: int256(uint256(params.liquidityDelta)) - }), - hookData, - claims, - ownerOf(params.tokenId), - token0Owed, - token1Owed - ); - // TODO: slippage checks & test - - delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; - delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; - position.liquidity += params.liquidityDelta; + delta = _increaseLiquidity(tokenPositions[tokenId].range, liquidity, hookData, claims, msg.sender); } - function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData, bool claims) + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) public - isAuthorizedForToken(params.tokenId) + isAuthorizedForToken(tokenId) returns (BalanceDelta delta) { - require(params.liquidityDelta != 0, "Must decrease liquidity"); - Position storage position = positions[params.tokenId]; - - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(poolManager, position.range.key.toId()); - (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( - sqrtPriceX96, - TickMath.getSqrtRatioAtTick(position.range.tickLower), - TickMath.getSqrtRatioAtTick(position.range.tickUpper), - params.liquidityDelta - ); - BaseLiquidityManagement.modifyLiquidity( - position.range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: position.range.tickLower, - tickUpper: position.range.tickUpper, - liquidityDelta: -int256(uint256(params.liquidityDelta)) - }), - hookData, - ownerOf(params.tokenId) - ); - require(params.amount0Min <= uint256(uint128(-delta.amount0())), "INSUFFICIENT_AMOUNT0"); - require(params.amount1Min <= uint256(uint128(-delta.amount1())), "INSUFFICIENT_AMOUNT1"); - - (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(position); - // TODO: for now we'll assume user always collects the totality of their fees - token0Owed += (position.tokensOwed0 + uint128(amount0)); - token1Owed += (position.tokensOwed1 + uint128(amount1)); - - // TODO: does this account for 0 token transfers - if (claims) { - poolManager.transfer(params.recipient, position.range.key.currency0.toId(), token0Owed); - poolManager.transfer(params.recipient, position.range.key.currency1.toId(), token1Owed); - } else { - sendToken(params.recipient, position.range.key.currency0, token0Owed); - sendToken(params.recipient, position.range.key.currency1, token1Owed); - } - - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - position.liquidity -= params.liquidityDelta; - delta = toBalanceDelta(-int128(token0Owed), -int128(token1Owed)); + delta = _decreaseLiquidity(tokenPositions[tokenId].range, liquidity, hookData, claims, msg.sender); } function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims) @@ -195,24 +98,15 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi returns (BalanceDelta delta) { // remove liquidity - Position storage position = positions[tokenId]; + TokenPosition storage tokenPosition = tokenPositions[tokenId]; + LiquidityRangeId rangeId = tokenPosition.range.toId(); + Position storage position = positions[msg.sender][rangeId]; if (0 < position.liquidity) { - decreaseLiquidity( - DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: position.liquidity, - amount0Min: 0, - amount1Min: 0, - recipient: recipient, - deadline: block.timestamp - }), - hookData, - claims - ); + decreaseLiquidity(tokenId, position.liquidity, hookData, claims); } - require(position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "NOT_EMPTY"); - delete positions[tokenId]; + delete positions[msg.sender][rangeId]; + delete tokenPositions[tokenId]; // burn the token _burn(tokenId); @@ -223,49 +117,26 @@ contract NonfungiblePositionManager is BaseLiquidityManagement, INonfungiblePosi external returns (BalanceDelta delta) { - Position storage position = positions[tokenId]; - BaseLiquidityManagement.collect(position.range, hookData); - - (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(position); - delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); - - // TODO: for now we'll assume user always collects the totality of their fees - if (claims) { - poolManager.transfer(recipient, position.range.key.currency0.toId(), token0Owed + position.tokensOwed0); - poolManager.transfer(recipient, position.range.key.currency1.toId(), token1Owed + position.tokensOwed1); - } else { - sendToken(recipient, position.range.key.currency0, token0Owed + position.tokensOwed0); - sendToken(recipient, position.range.key.currency1, token1Owed + position.tokensOwed1); - } - - position.tokensOwed0 = 0; - position.tokensOwed1 = 0; - - // TODO: event + delta = _collect(tokenPositions[tokenId].range, hookData, claims, msg.sender); } - function _updateFeeGrowth(Position storage position) internal returns (uint128 token0Owed, uint128 token1Owed) { - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = poolManager.getFeeGrowthInside( - position.range.key.toId(), position.range.tickLower, position.range.tickUpper - ); - - (token0Owed, token1Owed) = FeeMath.getFeesOwed( - feeGrowthInside0X128, - feeGrowthInside1X128, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128, - position.liquidity - ); - - position.feeGrowthInside0LastX128 = feeGrowthInside0X128; - position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) { + TokenPosition memory tokenPosition = tokenPositions[tokenId]; + return feesOwed(tokenPosition.owner, tokenPosition.range); } function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { - Position storage position = positions[firstTokenId]; + TokenPosition storage tokenPosition = tokenPositions[firstTokenId]; + LiquidityRangeId rangeId = tokenPosition.range.toId(); + Position storage position = positions[from][rangeId]; position.operator = address(0x0); - liquidityOf[from][position.range.toId()] -= position.liquidity; - liquidityOf[to][position.range.toId()] += position.liquidity; + + // transfer position data to destination + positions[to][rangeId] = position; + delete positions[from][rangeId]; + + // update token position + tokenPositions[firstTokenId] = TokenPosition({owner: to, range: tokenPosition.range}); } modifier isAuthorizedForToken(uint256 tokenId) { diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol index 8657478b..e203becc 100644 --- a/contracts/SimpleBatchCall.sol +++ b/contracts/SimpleBatchCall.sol @@ -6,17 +6,21 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./base/ImmutableState.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; /// @title SimpleBatchCall /// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. contract SimpleBatchCall is LockAndBatchCall { using CurrencyLibrary for Currency; + using TransientStateLibrary for IPoolManager; + using CurrencySettler for Currency; constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} struct SettleConfig { - bool withdrawTokens; // If true, takes the underlying ERC20s. - bool settleUsingTransfer; // If true, sends the underlying ERC20s. + bool takeClaims; + bool settleUsingBurn; // If true, sends the underlying ERC20s. } /// @notice We naively settle all currencies that are touched by the batch call. This data is passed in intially to `execute`. @@ -30,19 +34,10 @@ contract SimpleBatchCall is LockAndBatchCall { int256 delta = poolManager.currencyDelta(address(this), currenciesTouched[i]); if (delta < 0) { - if (config.settleUsingTransfer) { - ERC20(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), uint256(-delta)); - poolManager.settle(currency); - } else { - poolManager.transferFrom(address(poolManager), address(this), currency.toId(), uint256(-delta)); - } + currency.settle(poolManager, sender, uint256(-delta), config.settleUsingBurn); } if (delta > 0) { - if (config.withdrawTokens) { - poolManager.mint(address(this), currency.toId(), uint256(delta)); - } else { - poolManager.take(currency, address(this), uint256(delta)); - } + currency.take(poolManager, address(this), uint256(delta), config.takeClaims); } } } diff --git a/contracts/base/BaseLiquidityHandler.sol b/contracts/base/BaseLiquidityHandler.sol new file mode 100644 index 00000000..0b66c450 --- /dev/null +++ b/contracts/base/BaseLiquidityHandler.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {SafeCallback} from "./SafeCallback.sol"; +import {ImmutableState} from "./ImmutableState.sol"; +import {FeeMath} from "../libraries/FeeMath.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {CurrencySenderLibrary} from "../libraries/CurrencySenderLibrary.sol"; +import {CurrencyDeltas} from "../libraries/CurrencyDeltas.sol"; +import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol"; + +import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; + +// TODO: remove +import {console2} from "forge-std/console2.sol"; + +abstract contract BaseLiquidityHandler is SafeCallback { + using LiquidityRangeIdLibrary for LiquidityRange; + using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; + using CurrencySenderLibrary for Currency; + using CurrencyDeltas for IPoolManager; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; + using LiquiditySaltLibrary for IHooks; + using PoolIdLibrary for PoolKey; + using SafeCast for uint256; + + // details about the liquidity position + struct Position { + // the nonce for permits + uint96 nonce; + // the address that is approved for spending this token + address operator; + uint256 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } + + mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions; + + error LockFailure(); + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + (bool success, bytes memory returnData) = address(this).call(data); + if (success) return returnData; + if (returnData.length == 0) revert LockFailure(); + // if the call failed, bubble up the reason + /// @solidity memory-safe-assembly + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + + // TODO: selfOnly modifier + function handleIncreaseLiquidity( + address sender, + LiquidityRange calldata range, + uint256 liquidityToAdd, + bytes calldata hookData, + bool claims + ) external returns (BalanceDelta delta) { + Position storage position = positions[sender][range.toId()]; + + { + BalanceDelta feeDelta; + (delta, feeDelta) = poolManager.modifyLiquidity( + range.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: int256(liquidityToAdd), + salt: range.key.hooks.getLiquiditySalt(sender) + }), + hookData + ); + // take fees not accrued by user's position + (uint256 token0Owed, uint256 token1Owed) = _updateFeeGrowth(range, position); + BalanceDelta excessFees = feeDelta - toBalanceDelta(token0Owed.toInt128(), token1Owed.toInt128()); + range.key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); + range.key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); + } + + { + // get remaining deltas: the user pays additional to increase liquidity OR the user collects fees + delta = poolManager.currencyDeltas(address(this), range.key.currency0, range.key.currency1); + if (delta.amount0() < 0) { + range.key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), claims); + } + if (delta.amount1() < 0) { + range.key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), claims); + } + if (delta.amount0() > 0) { + range.key.currency0.take(poolManager, address(this), uint256(int256(delta.amount0())), true); + } + if (delta.amount1() > 0) { + range.key.currency1.take(poolManager, address(this), uint256(int256(delta.amount1())), true); + } + } + + { + positions[sender][range.toId()].liquidity += liquidityToAdd; + + // collected fees are credited to the position OR zero'd out + delta.amount0() > 0 ? position.tokensOwed0 += uint128(delta.amount0()) : position.tokensOwed0 = 0; + delta.amount1() > 0 ? position.tokensOwed1 += uint128(delta.amount1()) : position.tokensOwed1 = 0; + } + return delta; + } + + function handleDecreaseLiquidity( + address owner, + LiquidityRange calldata range, + uint256 liquidityToRemove, + bytes calldata hookData, + bool useClaims + ) external returns (BalanceDelta) { + (BalanceDelta delta, BalanceDelta feesAccrued) = poolManager.modifyLiquidity( + range.key, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: -int256(liquidityToRemove), + salt: range.key.hooks.getLiquiditySalt(owner) + }), + hookData + ); + + // take all tokens first + // do NOT take tokens directly to the owner because this contract might be holding fees + // that need to be paid out (position.tokensOwed) + if (delta.amount0() > 0) { + range.key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); + } + if (delta.amount1() > 0) { + range.key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); + } + + uint128 token0Owed; + uint128 token1Owed; + { + Position storage position = positions[owner][range.toId()]; + (token0Owed, token1Owed) = _updateFeeGrowth(range, position); + + BalanceDelta principalDelta = delta - feesAccrued; + token0Owed += position.tokensOwed0 + uint128(principalDelta.amount0()); + token1Owed += position.tokensOwed1 + uint128(principalDelta.amount1()); + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + position.liquidity -= liquidityToRemove; + } + { + delta = toBalanceDelta(int128(token0Owed), int128(token1Owed)); + + // sending tokens to the owner + if (token0Owed > 0) range.key.currency0.send(poolManager, owner, token0Owed, useClaims); + if (token1Owed > 0) range.key.currency1.send(poolManager, owner, token1Owed, useClaims); + } + + return delta; + } + + function handleCollect(address owner, LiquidityRange calldata range, bytes calldata hookData, bool takeClaims) + external + returns (BalanceDelta) + { + PoolKey memory key = range.key; + Position storage position = positions[owner][range.toId()]; + + (, BalanceDelta feesAccrued) = poolManager.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams({ + tickLower: range.tickLower, + tickUpper: range.tickUpper, + liquidityDelta: 0, + salt: key.hooks.getLiquiditySalt(owner) + }), + hookData + ); + + // take all fees first then distribute + if (feesAccrued.amount0() > 0) { + key.currency0.take(poolManager, address(this), uint128(feesAccrued.amount0()), true); + } + if (feesAccrued.amount1() > 0) { + key.currency1.take(poolManager, address(this), uint128(feesAccrued.amount1()), true); + } + + (uint128 token0Owed, uint128 token1Owed) = _updateFeeGrowth(range, position); + token0Owed += position.tokensOwed0; + token1Owed += position.tokensOwed1; + + if (token0Owed > 0) key.currency0.send(poolManager, owner, token0Owed, takeClaims); + if (token1Owed > 0) key.currency1.send(poolManager, owner, token1Owed, takeClaims); + + position.tokensOwed0 = 0; + position.tokensOwed1 = 0; + + return toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); + } + + function _updateFeeGrowth(LiquidityRange memory range, Position storage position) + internal + returns (uint128 token0Owed, uint128 token1Owed) + { + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + poolManager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); + + (token0Owed, token1Owed) = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity + ); + + position.feeGrowthInside0LastX128 = feeGrowthInside0X128; + position.feeGrowthInside1LastX128 = feeGrowthInside1X128; + } +} diff --git a/contracts/base/BaseLiquidityManagement.sol b/contracts/base/BaseLiquidityManagement.sol index 6b243e20..13269f69 100644 --- a/contracts/base/BaseLiquidityManagement.sol +++ b/contracts/base/BaseLiquidityManagement.sol @@ -6,196 +6,88 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../types/LiquidityRange.sol"; -import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol"; import {SafeCallback} from "./SafeCallback.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; -import {PoolStateLibrary} from "../libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; import {FeeMath} from "../libraries/FeeMath.sol"; +import {BaseLiquidityHandler} from "./BaseLiquidityHandler.sol"; // TODO: remove import {console2} from "forge-std/console2.sol"; -abstract contract BaseLiquidityManagement is SafeCallback, IBaseLiquidityManagement { +abstract contract BaseLiquidityManagement is BaseLiquidityHandler { using LiquidityRangeIdLibrary for LiquidityRange; using CurrencyLibrary for Currency; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; - using PoolStateLibrary for IPoolManager; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; - error LockFailure(); + constructor(IPoolManager _poolManager) BaseLiquidityHandler(_poolManager) {} - struct CallbackData { - address sender; - PoolKey key; - IPoolManager.ModifyLiquidityParams params; - bool claims; - bytes hookData; - } - - mapping(address owner => mapping(LiquidityRangeId positionId => uint256 liquidity)) public liquidityOf; - - constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} - - // NOTE: handles mint/remove/collect - function modifyLiquidity( - PoolKey memory key, - IPoolManager.ModifyLiquidityParams memory params, + function _increaseLiquidity( + LiquidityRange memory range, + uint256 liquidityToAdd, bytes calldata hookData, + bool claims, address owner - ) public payable override returns (BalanceDelta delta) { - // if removing liquidity, check that the owner is the sender? - if (params.liquidityDelta < 0) require(msg.sender == owner, "Cannot redeem position"); - + ) internal returns (BalanceDelta delta) { delta = abi.decode( - poolManager.lock(abi.encodeCall(this.handleModifyPosition, (msg.sender, key, params, hookData, false))), + poolManager.unlock( + abi.encodeCall(this.handleIncreaseLiquidity, (msg.sender, range, liquidityToAdd, hookData, claims)) + ), (BalanceDelta) ); - - params.liquidityDelta < 0 - ? liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] -= - uint256(-params.liquidityDelta) - : liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] += - uint256(params.liquidityDelta); - - // TODO: handle & test - // uint256 ethBalance = address(this).balance; - // if (ethBalance > 0) { - // CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); - // } } - function increaseLiquidity( - PoolKey memory key, - IPoolManager.ModifyLiquidityParams memory params, + function _decreaseLiquidity( + LiquidityRange memory range, + uint256 liquidityToRemove, bytes calldata hookData, bool claims, - address owner, - uint256 token0Owed, - uint256 token1Owed + address owner ) internal returns (BalanceDelta delta) { delta = abi.decode( - poolManager.lock( - abi.encodeCall( - this.handleIncreaseLiquidity, - ( - msg.sender, - key, - params, - hookData, - claims, - toBalanceDelta(int128(int256(token0Owed)), int128(int256(token1Owed))) - ) - ) + poolManager.unlock( + abi.encodeCall(this.handleDecreaseLiquidity, (owner, range, liquidityToRemove, hookData, claims)) ), (BalanceDelta) ); - - liquidityOf[owner][LiquidityRange(key, params.tickLower, params.tickUpper).toId()] += - uint256(params.liquidityDelta); } - function collect(LiquidityRange memory range, bytes calldata hookData) internal returns (BalanceDelta delta) { + function _collect(LiquidityRange memory range, bytes calldata hookData, bool claims, address owner) + internal + returns (BalanceDelta delta) + { delta = abi.decode( - poolManager.lock( - abi.encodeCall( - this.handleModifyPosition, - ( - address(this), - range.key, - IPoolManager.ModifyLiquidityParams({ - tickLower: range.tickLower, - tickUpper: range.tickUpper, - liquidityDelta: 0 - }), - hookData, - true - ) - ) - ), - (BalanceDelta) + poolManager.unlock(abi.encodeCall(this.handleCollect, (owner, range, hookData, claims))), (BalanceDelta) ); } - function sendToken(address recipient, Currency currency, uint256 amount) internal { - poolManager.lock(abi.encodeCall(this.handleRedeemClaim, (recipient, currency, amount))); - } - - function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { - (bool success, bytes memory returnData) = address(this).call(data); - if (success) return returnData; - if (returnData.length == 0) revert LockFailure(); - // if the call failed, bubble up the reason - /// @solidity memory-safe-assembly - assembly { - revert(add(returnData, 32), mload(returnData)) - } - } - - // TODO: selfOnly modifier - function handleModifyPosition( - address sender, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata hookData, - bool claims - ) external returns (BalanceDelta delta) { - delta = poolManager.modifyLiquidity(key, params, hookData); - - if (params.liquidityDelta <= 0) { - // removing liquidity/fees so mint tokens to the router - // the router will be responsible for sending the tokens to the desired recipient - key.currency0.take(poolManager, address(this), uint128(delta.amount0()), true); - key.currency1.take(poolManager, address(this), uint128(delta.amount1()), true); - } else { - // adding liquidity so pay tokens - key.currency0.settle(poolManager, sender, uint128(-delta.amount0()), claims); - key.currency1.settle(poolManager, sender, uint128(-delta.amount1()), claims); - } - } - - // TODO: selfOnly modifier - function handleIncreaseLiquidity( - address sender, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata hookData, - bool claims, - BalanceDelta tokensOwed - ) external returns (BalanceDelta delta) { - BalanceDelta feeDelta = poolManager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: params.tickLower, - tickUpper: params.tickUpper, - liquidityDelta: 0 - }), - hookData + // --- View Functions --- // + function feesOwed(address owner, LiquidityRange memory range) + public + view + returns (uint256 token0Owed, uint256 token1Owed) + { + Position memory position = positions[owner][range.toId()]; + + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + poolManager.getFeeGrowthInside(range.key.toId(), range.tickLower, range.tickUpper); + + (token0Owed, token1Owed) = FeeMath.getFeesOwed( + feeGrowthInside0X128, + feeGrowthInside1X128, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.liquidity ); - - poolManager.modifyLiquidity(key, params, hookData); - - { - BalanceDelta excessFees = feeDelta - tokensOwed; - key.currency0.take(poolManager, address(this), uint128(excessFees.amount0()), true); - key.currency1.take(poolManager, address(this), uint128(excessFees.amount1()), true); - - int256 amount0Delta = poolManager.currencyDelta(address(this), key.currency0); - int256 amount1Delta = poolManager.currencyDelta(address(this), key.currency1); - if (amount0Delta < 0) key.currency0.settle(poolManager, sender, uint256(-amount0Delta), claims); - if (amount1Delta < 0) key.currency1.settle(poolManager, sender, uint256(-amount1Delta), claims); - if (amount0Delta > 0) key.currency0.take(poolManager, address(this), uint256(amount0Delta), true); - if (amount1Delta > 0) key.currency1.take(poolManager, address(this), uint256(amount1Delta), true); - delta = toBalanceDelta(int128(amount0Delta), int128(amount1Delta)); - } - } - - // TODO: selfOnly modifier - function handleRedeemClaim(address recipient, Currency currency, uint256 amount) external { - poolManager.burn(address(this), currency.toId(), amount); - poolManager.take(currency, recipient, amount); + token0Owed += position.tokensOwed0; + token1Owed += position.tokensOwed1; } } diff --git a/contracts/base/CallsWithLock.sol b/contracts/base/CallsWithLock.sol index c871c797..113d1ebd 100644 --- a/contracts/base/CallsWithLock.sol +++ b/contracts/base/CallsWithLock.sol @@ -5,6 +5,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ImmutableState} from "./ImmutableState.sol"; import {ICallsWithLock} from "../interfaces/ICallsWithLock.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; /// @title CallsWithLock /// @notice Handles all the calls to the pool manager contract. Assumes the integrating contract has already acquired a lock. @@ -29,7 +30,8 @@ abstract contract CallsWithLock is ICallsWithLock, ImmutableState { IPoolManager.ModifyLiquidityParams calldata params, bytes calldata hookData ) external onlyBySelf returns (bytes memory) { - return abi.encode(poolManager.modifyLiquidity(key, params, hookData)); + (BalanceDelta delta, BalanceDelta feeDelta) = poolManager.modifyLiquidity(key, params, hookData); + return abi.encode(delta, feeDelta); } function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol index 76deb511..fe450730 100644 --- a/contracts/base/LockAndBatchCall.sol +++ b/contracts/base/LockAndBatchCall.sol @@ -14,14 +14,14 @@ abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { /// @param executeData The function selectors and calldata for any of the function selectors in ICallsWithLock encoded as an array of bytes. function execute(bytes memory executeData, bytes memory settleData) external { - (bytes memory lockReturnData) = poolManager.lock(abi.encode(executeData, abi.encode(msg.sender, settleData))); + (bytes memory lockReturnData) = poolManager.unlock(abi.encode(executeData, abi.encode(msg.sender, settleData))); (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); _handleAfterExecute(executeReturnData, settleReturnData); } /// @param data This data is passed from the top-level execute function to the internal _executeWithLockCalls and _settle function. It is decoded as two separate dynamic bytes parameters. - /// @dev _lockAcquired is responsible for executing the internal calls under the lock and settling open deltas left on the pool - function _lockAcquired(bytes calldata data) internal override returns (bytes memory) { + /// @dev _unlockCallback is responsible for executing the internal calls under the lock and settling open deltas left on the pool + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { (bytes memory executeData, bytes memory settleDataWithSender) = abi.decode(data, (bytes, bytes)); (address sender, bytes memory settleData) = abi.decode(settleDataWithSender, (address, bytes)); return abi.encode(_executeWithLockCalls(executeData), _settle(sender, settleData)); diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol index a2656287..3eb693dd 100644 --- a/contracts/base/SafeCallback.sol +++ b/contracts/base/SafeCallback.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./ImmutableState.sol"; -abstract contract SafeCallback is ImmutableState, ILockCallback { +abstract contract SafeCallback is ImmutableState, IUnlockCallback { error NotManager(); modifier onlyByManager() { @@ -14,9 +14,9 @@ abstract contract SafeCallback is ImmutableState, ILockCallback { } /// @dev There is no way to force the onlyByManager modifier but for this callback to be safe, it MUST check that the msg.sender is the pool manager. - function lockAcquired(bytes calldata data) external onlyByManager returns (bytes memory) { - return _lockAcquired(data); + function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { + return _unlockCallback(data); } - function _lockAcquired(bytes calldata data) internal virtual returns (bytes memory); + function _unlockCallback(bytes calldata data) internal virtual returns (bytes memory); } diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol index 6f7b1178..8d750a76 100644 --- a/contracts/hooks/examples/FullRange.sol +++ b/contracts/hooks/examples/FullRange.sol @@ -8,10 +8,11 @@ import {BaseHook} from "../../BaseHook.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; @@ -20,14 +21,18 @@ import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import "../../libraries/LiquidityAmounts.sol"; contract FullRange is BaseHook { using CurrencyLibrary for Currency; + using CurrencySettler for Currency; using PoolIdLibrary for PoolKey; using SafeCast for uint256; using SafeCast for uint128; + using StateLibrary for IPoolManager; /// @notice Thrown when trying to interact with a non-initialized pool error PoolNotInitialized(); @@ -98,7 +103,11 @@ contract FullRange is BaseHook { beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -127,8 +136,8 @@ contract FullRange is BaseHook { liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(MIN_TICK), - TickMath.getSqrtRatioAtTick(MAX_TICK), + TickMath.getSqrtPriceAtTick(MIN_TICK), + TickMath.getSqrtPriceAtTick(MAX_TICK), params.amount0Desired, params.amount1Desired ); @@ -141,7 +150,8 @@ contract FullRange is BaseHook { IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256() + liquidityDelta: liquidity.toInt256(), + salt: 0 }) ); @@ -185,7 +195,8 @@ contract FullRange is BaseHook { IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: -(params.liquidity.toInt256()) + liquidityDelta: -(params.liquidity.toInt256()), + salt: 0 }) ); @@ -233,7 +244,7 @@ contract FullRange is BaseHook { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { PoolId poolId = key.toId(); @@ -242,32 +253,19 @@ contract FullRange is BaseHook { pool.hasAccruedFees = true; } - return IHooks.beforeSwap.selector; + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function modifyLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) internal returns (BalanceDelta delta) { - delta = abi.decode(poolManager.lock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); + delta = abi.decode(poolManager.unlock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); } function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - _settleDelta(sender, key.currency0, uint128(-delta.amount0())); - _settleDelta(sender, key.currency1, uint128(-delta.amount1())); - } - - function _settleDelta(address sender, Currency currency, uint128 amount) internal { - if (currency.isNative()) { - poolManager.settle{value: amount}(currency); - } else { - if (sender == address(this)) { - currency.transfer(address(poolManager), amount); - } else { - IERC20Minimal(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), amount); - } - poolManager.settle(currency); - } + key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), false); + key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), false); } function _takeDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { @@ -293,11 +291,11 @@ contract FullRange is BaseHook { ); params.liquidityDelta = -(liquidityToRemove.toInt256()); - delta = poolManager.modifyLiquidity(key, params, ZERO_BYTES); + (delta,) = poolManager.modifyLiquidity(key, params, ZERO_BYTES); pool.hasAccruedFees = false; } - function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory) { + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { CallbackData memory data = abi.decode(rawData, (CallbackData)); BalanceDelta delta; @@ -305,7 +303,7 @@ contract FullRange is BaseHook { delta = _removeLiquidity(data.key, data.params); _takeDeltas(data.sender, data.key, delta); } else { - delta = poolManager.modifyLiquidity(data.key, data.params, ZERO_BYTES); + (delta,) = poolManager.modifyLiquidity(data.key, data.params, ZERO_BYTES); _settleDeltas(data.sender, data.key, delta); } return abi.encode(delta); @@ -313,12 +311,13 @@ contract FullRange is BaseHook { function _rebalance(PoolKey memory key) public { PoolId poolId = key.toId(); - BalanceDelta balanceDelta = poolManager.modifyLiquidity( + (BalanceDelta balanceDelta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()) + liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()), + salt: 0 }), ZERO_BYTES ); @@ -343,18 +342,19 @@ contract FullRange is BaseHook { uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( newSqrtPriceX96, - TickMath.getSqrtRatioAtTick(MIN_TICK), - TickMath.getSqrtRatioAtTick(MAX_TICK), + TickMath.getSqrtPriceAtTick(MIN_TICK), + TickMath.getSqrtPriceAtTick(MAX_TICK), uint256(uint128(balanceDelta.amount0())), uint256(uint128(balanceDelta.amount1())) ); - BalanceDelta balanceDeltaAfter = poolManager.modifyLiquidity( + (BalanceDelta balanceDeltaAfter,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256() + liquidityDelta: liquidity.toInt256(), + salt: 0 }), ZERO_BYTES ); diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol index 9dfb2210..137d4207 100644 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ b/contracts/hooks/examples/GeomeanOracle.sol @@ -8,6 +8,8 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Oracle} from "../../libraries/Oracle.sol"; import {BaseHook} from "../../BaseHook.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; /// @notice A hook for a pool that allows a Uniswap pool to act as an oracle. Pools that use this hook must have full range /// tick spacing and liquidity is always permanently locked in these pools. This is the suggested configuration @@ -15,6 +17,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; contract GeomeanOracle is BaseHook { using Oracle for Oracle.Observation[65535]; using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; /// @notice Oracle pools do not have fees because they exist to serve as an oracle for a pair of tokens error OnlyOneOraclePoolAllowed(); @@ -71,7 +74,11 @@ contract GeomeanOracle is BaseHook { beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -141,10 +148,10 @@ contract GeomeanOracle is BaseHook { external override onlyByManager - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { _updatePool(key); - return GeomeanOracle.beforeSwap.selector; + return (GeomeanOracle.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } /// @notice Observe the given pool for the timestamps diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol index 530922a6..3d26f740 100644 --- a/contracts/hooks/examples/LimitOrder.sol +++ b/contracts/hooks/examples/LimitOrder.sol @@ -10,8 +10,10 @@ import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Mini import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {BaseHook} from "../../BaseHook.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; type Epoch is uint232; @@ -31,6 +33,8 @@ contract LimitOrder is BaseHook { using EpochLibrary for Epoch; using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; + using CurrencySettler for Currency; + using StateLibrary for IPoolManager; error ZeroLiquidity(); error InRange(); @@ -84,7 +88,11 @@ contract LimitOrder is BaseHook { beforeSwap: false, afterSwap: true, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -134,9 +142,9 @@ contract LimitOrder is BaseHook { IPoolManager.SwapParams calldata params, BalanceDelta, bytes calldata - ) external override onlyByManager returns (bytes4) { + ) external override onlyByManager returns (bytes4, int128) { (int24 tickLower, int24 lower, int24 upper) = _getCrossedTicks(key.toId(), key.tickSpacing); - if (lower > upper) return LimitOrder.afterSwap.selector; + if (lower > upper) return (LimitOrder.afterSwap.selector, 0); // note that a zeroForOne swap means that the pool is actually gaining token0, so limit // order fills are the opposite of swap fills, hence the inversion below @@ -146,7 +154,7 @@ contract LimitOrder is BaseHook { } setTickLowerLast(key.toId(), tickLower); - return LimitOrder.afterSwap.selector; + return (LimitOrder.afterSwap.selector, 0); } function _fillEpoch(PoolKey calldata key, int24 lower, bool zeroForOne) internal { @@ -157,7 +165,7 @@ contract LimitOrder is BaseHook { epochInfo.filled = true; (uint256 amount0, uint256 amount1) = - _lockAcquiredFill(key, lower, -int256(uint256(epochInfo.liquidityTotal))); + _unlockCallbackFill(key, lower, -int256(uint256(epochInfo.liquidityTotal))); unchecked { epochInfo.token0Total += amount0; @@ -187,17 +195,18 @@ contract LimitOrder is BaseHook { } } - function _lockAcquiredFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) + function _unlockCallbackFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) private onlyByManager returns (uint128 amount0, uint128 amount1) { - BalanceDelta delta = poolManager.modifyLiquidity( + (BalanceDelta delta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta + liquidityDelta: liquidityDelta, + salt: 0 }), ZERO_BYTES ); @@ -216,8 +225,10 @@ contract LimitOrder is BaseHook { { if (liquidity == 0) revert ZeroLiquidity(); - poolManager.lock( - abi.encodeCall(this.lockAcquiredPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender)) + poolManager.unlock( + abi.encodeCall( + this.unlockCallbackPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender) + ) ); EpochInfo storage epochInfo; @@ -245,19 +256,20 @@ contract LimitOrder is BaseHook { emit Place(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); } - function lockAcquiredPlace( + function unlockCallbackPlace( PoolKey calldata key, int24 tickLower, bool zeroForOne, int256 liquidityDelta, address owner ) external selfOnly { - BalanceDelta delta = poolManager.modifyLiquidity( + (BalanceDelta delta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta + liquidityDelta: liquidityDelta, + salt: 0 }), ZERO_BYTES ); @@ -265,26 +277,15 @@ contract LimitOrder is BaseHook { if (delta.amount0() < 0) { if (delta.amount1() != 0) revert InRange(); if (!zeroForOne) revert CrossedRange(); - // TODO use safeTransferFrom - IERC20Minimal(Currency.unwrap(key.currency0)).transferFrom( - owner, address(poolManager), uint256(uint128(-delta.amount0())) - ); - poolManager.settle(key.currency0); + key.currency0.settle(poolManager, owner, uint256(uint128(-delta.amount0())), false); } else { if (delta.amount0() != 0) revert InRange(); if (zeroForOne) revert CrossedRange(); - // TODO use safeTransferFrom - IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom( - owner, address(poolManager), uint256(uint128(-delta.amount1())) - ); - poolManager.settle(key.currency1); + key.currency1.settle(poolManager, owner, uint256(uint128(-delta.amount1())), false); } } - function kill(PoolKey calldata key, int24 tickLower, bool zeroForOne, address to) - external - returns (uint256 amount0, uint256 amount1) - { + function kill(PoolKey calldata key, int24 tickLower, bool zeroForOne, address to) external { Epoch epoch = getEpoch(key, tickLower, zeroForOne); EpochInfo storage epochInfo = epochInfos[epoch]; @@ -296,14 +297,14 @@ contract LimitOrder is BaseHook { uint256 amount0Fee; uint256 amount1Fee; - (amount0, amount1, amount0Fee, amount1Fee) = abi.decode( - poolManager.lock( + (amount0Fee, amount1Fee) = abi.decode( + poolManager.unlock( abi.encodeCall( - this.lockAcquiredKill, + this.unlockCallbackKill, (key, tickLower, -int256(uint256(liquidity)), to, liquidity == epochInfo.liquidityTotal) ) ), - (uint256, uint256, uint256, uint256) + (uint256, uint256) ); epochInfo.liquidityTotal -= liquidity; unchecked { @@ -314,13 +315,13 @@ contract LimitOrder is BaseHook { emit Kill(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); } - function lockAcquiredKill( + function unlockCallbackKill( PoolKey calldata key, int24 tickLower, int256 liquidityDelta, address to, bool removingAllLiquidity - ) external selfOnly returns (uint256 amount0, uint256 amount1, uint128 amount0Fee, uint128 amount1Fee) { + ) external selfOnly returns (uint128 amount0Fee, uint128 amount1Fee) { int24 tickUpper = tickLower + key.tickSpacing; // because `modifyPosition` includes not just principal value but also fees, we cannot allocate @@ -328,9 +329,14 @@ contract LimitOrder is BaseHook { // could be unfairly diluted by a user sychronously placing then killing a limit order to skim off fees. // to prevent this, we allocate all fee revenue to remaining limit order placers, unless this is the last order. if (!removingAllLiquidity) { - BalanceDelta deltaFee = poolManager.modifyLiquidity( + (, BalanceDelta deltaFee) = poolManager.modifyLiquidity( key, - IPoolManager.ModifyLiquidityParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: 0}), + IPoolManager.ModifyLiquidityParams({ + tickLower: tickLower, + tickUpper: tickUpper, + liquidityDelta: 0, + salt: 0 + }), ZERO_BYTES ); @@ -342,21 +348,22 @@ contract LimitOrder is BaseHook { } } - BalanceDelta delta = poolManager.modifyLiquidity( + (BalanceDelta delta,) = poolManager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickUpper, - liquidityDelta: liquidityDelta + liquidityDelta: liquidityDelta, + salt: 0 }), ZERO_BYTES ); if (delta.amount0() > 0) { - poolManager.take(key.currency0, to, amount0 = uint128(delta.amount0())); + key.currency0.take(poolManager, to, uint256(uint128(delta.amount0())), false); } if (delta.amount1() > 0) { - poolManager.take(key.currency1, to, amount1 = uint128(delta.amount1())); + key.currency1.take(poolManager, to, uint256(uint128(delta.amount1())), false); } } @@ -378,14 +385,16 @@ contract LimitOrder is BaseHook { epochInfo.token1Total -= amount1; epochInfo.liquidityTotal = liquidityTotal - liquidity; - poolManager.lock( - abi.encodeCall(this.lockAcquiredWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to)) + poolManager.unlock( + abi.encodeCall( + this.unlockCallbackWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to) + ) ); emit Withdraw(msg.sender, epoch, liquidity); } - function lockAcquiredWithdraw( + function unlockCallbackWithdraw( Currency currency0, Currency currency1, uint256 token0Amount, diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index 8bc3aadb..c619e900 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -19,10 +19,14 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolGetters} from "../../libraries/PoolGetters.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; contract TWAMM is BaseHook, ITWAMM { using TransferHelper for IERC20Minimal; using CurrencyLibrary for Currency; + using CurrencySettler for Currency; using OrderPool for OrderPool.State; using PoolIdLibrary for PoolKey; using TickMath for int24; @@ -30,6 +34,7 @@ contract TWAMM is BaseHook, ITWAMM { using SafeCast for uint256; using PoolGetters for IPoolManager; using TickBitmap for mapping(int16 => uint256); + using StateLibrary for IPoolManager; bytes internal constant ZERO_BYTES = bytes(""); @@ -71,7 +76,11 @@ contract TWAMM is BaseHook, ITWAMM { beforeSwap: true, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -101,10 +110,10 @@ contract TWAMM is BaseHook, ITWAMM { external override onlyByManager - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { executeTWAMMOrders(key); - return BaseHook.beforeSwap.selector; + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function lastVirtualOrderTimestamp(PoolId key) external view returns (uint256) { @@ -142,7 +151,9 @@ contract TWAMM is BaseHook, ITWAMM { ); if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - poolManager.lock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96))); + poolManager.unlock( + abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96)) + ); } } @@ -298,7 +309,7 @@ contract TWAMM is BaseHook, ITWAMM { IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); } - function _lockAcquired(bytes calldata rawData) internal override returns (bytes memory) { + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); @@ -306,19 +317,17 @@ contract TWAMM is BaseHook, ITWAMM { if (swapParams.zeroForOne) { if (delta.amount0() < 0) { - key.currency0.transfer(address(poolManager), uint256(uint128(-delta.amount0()))); - poolManager.settle(key.currency0); + key.currency0.settle(poolManager, address(this), uint256(uint128(-delta.amount0())), false); } if (delta.amount1() > 0) { - poolManager.take(key.currency1, address(this), uint256(uint128(delta.amount1()))); + key.currency1.take(poolManager, address(this), uint256(uint128(delta.amount1())), false); } } else { if (delta.amount1() < 0) { - key.currency1.transfer(address(poolManager), uint256(uint128(-delta.amount1()))); - poolManager.settle(key.currency1); + key.currency1.settle(poolManager, address(this), uint256(uint128(-delta.amount1())), false); } if (delta.amount0() > 0) { - poolManager.take(key.currency0, address(this), uint256(uint128(delta.amount0()))); + key.currency0.take(poolManager, address(this), uint256(uint128(delta.amount0())), false); } } return bytes(""); @@ -512,8 +521,8 @@ contract TWAMM is BaseHook, ITWAMM { _isCrossingInitializedTick(params.pool, poolManager, poolKey, finalSqrtPriceX96); if (crossingInitializedTick) { - int128 liquidityNetAtTick = poolManager.getPoolTickInfo(poolKey.toId(), tick).liquidityNet; - uint160 initializedSqrtPrice = TickMath.getSqrtRatioAtTick(tick); + (, int128 liquidityNetAtTick) = poolManager.getTickLiquidity(poolKey.toId(), tick); + uint160 initializedSqrtPrice = TickMath.getSqrtPriceAtTick(tick); uint256 swapDelta0 = SqrtPriceMath.getAmount0Delta( params.pool.sqrtPriceX96, initializedSqrtPrice, params.pool.liquidity, true @@ -570,7 +579,7 @@ contract TWAMM is BaseHook, ITWAMM { PoolKey memory poolKey, TickCrossingParams memory params ) private returns (PoolParamsOnExecute memory, uint256) { - uint160 initializedSqrtPrice = params.initializedTick.getSqrtRatioAtTick(); + uint160 initializedSqrtPrice = params.initializedTick.getSqrtPriceAtTick(); uint256 secondsUntilCrossingX96 = TwammMath.calculateTimeBetweenTicks( params.pool.liquidity, @@ -596,7 +605,7 @@ contract TWAMM is BaseHook, ITWAMM { unchecked { // update pool - int128 liquidityNet = poolManager.getPoolTickInfo(poolKey.toId(), params.initializedTick).liquidityNet; + (, int128 liquidityNet) = poolManager.getTickLiquidity(poolKey.toId(), params.initializedTick); if (initializedSqrtPrice < params.pool.sqrtPriceX96) liquidityNet = -liquidityNet; params.pool.liquidity = liquidityNet < 0 ? params.pool.liquidity - uint128(-liquidityNet) @@ -614,8 +623,8 @@ contract TWAMM is BaseHook, ITWAMM { uint160 nextSqrtPriceX96 ) internal view returns (bool crossingInitializedTick, int24 nextTickInit) { // use current price as a starting point for nextTickInit - nextTickInit = pool.sqrtPriceX96.getTickAtSqrtRatio(); - int24 targetTick = nextSqrtPriceX96.getTickAtSqrtRatio(); + nextTickInit = pool.sqrtPriceX96.getTickAtSqrtPrice(); + int24 targetTick = nextSqrtPriceX96.getTickAtSqrtPrice(); bool searchingLeft = nextSqrtPriceX96 < pool.sqrtPriceX96; bool nextTickInitFurtherThanTarget = false; // initialize as false diff --git a/contracts/hooks/examples/VolatilityOracle.sol b/contracts/hooks/examples/VolatilityOracle.sol index 76a3e8ce..ede61bf5 100644 --- a/contracts/hooks/examples/VolatilityOracle.sol +++ b/contracts/hooks/examples/VolatilityOracle.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.19; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {SwapFeeLibrary} from "@uniswap/v4-core/src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; import {BaseHook} from "../../BaseHook.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; contract VolatilityOracle is BaseHook { - using SwapFeeLibrary for uint24; + using LPFeeLibrary for uint24; error MustUseDynamicFee(); @@ -34,7 +34,11 @@ contract VolatilityOracle is BaseHook { beforeSwap: false, afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -52,7 +56,7 @@ contract VolatilityOracle is BaseHook { uint24 startingFee = 3000; uint32 lapsed = _blockTimestamp() - deployTimestamp; uint24 fee = startingFee + (uint24(lapsed) * 100) / 60; // 100 bps a minute - poolManager.updateDynamicSwapFee(key, fee); // initial fee 0.30% + poolManager.updateDynamicLPFee(key, fee); // initial fee 0.30% } function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) diff --git a/contracts/interfaces/IAdvancedLiquidityManagement.sol b/contracts/interfaces/IAdvancedLiquidityManagement.sol deleted file mode 100644 index 5f5f9f8f..00000000 --- a/contracts/interfaces/IAdvancedLiquidityManagement.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; -import {LiquidityRange} from "../types/LiquidityRange.sol"; - -interface IAdvancedLiquidityManagement is IBaseLiquidityManagement { - /// @notice Move an existing liquidity position into a new range - function rebalanceLiquidity( - LiquidityRange memory position, - int24 tickLowerNew, - int24 tickUpperNew, - int256 liquidityDelta - ) external; - - /// @notice Move an existing liquidity position into a new pool, keeping the same range - function migrateLiquidity(LiquidityRange memory position, PoolKey memory newKey) external; -} diff --git a/contracts/interfaces/IBaseLiquidityManagement.sol b/contracts/interfaces/IBaseLiquidityManagement.sol deleted file mode 100644 index fe289195..00000000 --- a/contracts/interfaces/IBaseLiquidityManagement.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; -import {LiquidityRange, LiquidityRangeId} from "../types/LiquidityRange.sol"; - -interface IBaseLiquidityManagement is ILockCallback { - function liquidityOf(address owner, LiquidityRangeId positionId) external view returns (uint256 liquidity); - - // NOTE: handles add/remove/collect - function modifyLiquidity( - PoolKey memory key, - IPoolManager.ModifyLiquidityParams memory params, - bytes calldata hookData, - address owner - ) external payable returns (BalanceDelta delta); -} diff --git a/contracts/interfaces/INonfungiblePositionManager.sol b/contracts/interfaces/INonfungiblePositionManager.sol index cde005e9..be182907 100644 --- a/contracts/interfaces/INonfungiblePositionManager.sol +++ b/contracts/interfaces/INonfungiblePositionManager.sol @@ -4,39 +4,8 @@ pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {LiquidityRange} from "../types/LiquidityRange.sol"; -import {IBaseLiquidityManagement} from "./IBaseLiquidityManagement.sol"; - -interface INonfungiblePositionManager is IBaseLiquidityManagement { - // details about the uniswap position - struct Position { - // the nonce for permits - uint96 nonce; - // the address that is approved for spending this token - address operator; - LiquidityRange range; - // the liquidity of the position - // NOTE: this value will be less than BaseLiquidityManagement.liquidityOf, if the user - // owns multiple positions with the same range - uint128 liquidity; - // the fee growth of the aggregate position as of the last action on the individual position - uint256 feeGrowthInside0LastX128; - uint256 feeGrowthInside1LastX128; - // how many uncollected tokens are owed to the position, as of the last computation - uint128 tokensOwed0; - uint128 tokensOwed1; - } - - struct MintParams { - LiquidityRange range; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - address recipient; - bytes hookData; - } +interface INonfungiblePositionManager { // NOTE: more gas efficient as LiquidityAmounts is used offchain function mint( LiquidityRange calldata position, @@ -47,26 +16,12 @@ interface INonfungiblePositionManager is IBaseLiquidityManagement { ) external payable returns (uint256 tokenId, BalanceDelta delta); // NOTE: more expensive since LiquidityAmounts is used onchain - function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); - - struct IncreaseLiquidityParams { - uint256 tokenId; - uint128 liquidityDelta; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - struct DecreaseLiquidityParams { - uint256 tokenId; - uint128 liquidityDelta; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - address recipient; - } + // function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta); + function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) + external + returns (BalanceDelta delta); - function decreaseLiquidity(DecreaseLiquidityParams memory params, bytes calldata hookData, bool claims) + function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims) external returns (BalanceDelta delta); diff --git a/contracts/interfaces/IQuoter.sol b/contracts/interfaces/IQuoter.sol index 90a390fc..8774e548 100644 --- a/contracts/interfaces/IQuoter.sol +++ b/contracts/interfaces/IQuoter.sol @@ -11,7 +11,7 @@ import {PathKey} from "../libraries/PathKey.sol"; /// @dev These functions are not marked view because they rely on calling non-view functions and reverting /// to compute the result. They are also not gas efficient and should not be called on-chain. interface IQuoter { - error InvalidLockAcquiredSender(); + error InvalidUnlockCallbackSender(); error InvalidLockCaller(); error InvalidQuoteBatchParams(); error InsufficientAmountOut(); diff --git a/contracts/lens/Quoter.sol b/contracts/lens/Quoter.sol index c039a7b7..9e9bfda2 100644 --- a/contracts/lens/Quoter.sol +++ b/contracts/lens/Quoter.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; @@ -13,11 +13,13 @@ import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {IQuoter} from "../interfaces/IQuoter.sol"; import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -contract Quoter is IQuoter, ILockCallback { +contract Quoter is IQuoter, IUnlockCallback { using Hooks for IHooks; using PoolIdLibrary for PoolKey; using PathKeyLib for PathKey; + using StateLibrary for IPoolManager; /// @dev cache used to check a safety condition in exact output swaps. uint128 private amountOutCached; @@ -62,7 +64,7 @@ contract Quoter is IQuoter, ILockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.lock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} catch (bytes memory reason) { return _handleRevertSingle(reason); } @@ -77,7 +79,7 @@ contract Quoter is IQuoter, ILockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.lock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } @@ -89,7 +91,7 @@ contract Quoter is IQuoter, ILockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.lock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} catch (bytes memory reason) { if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; return _handleRevertSingle(reason); @@ -106,16 +108,16 @@ contract Quoter is IQuoter, ILockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.lock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} + try manager.unlock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } } - /// @inheritdoc ILockCallback - function lockAcquired(bytes calldata data) external returns (bytes memory) { + /// @inheritdoc IUnlockCallback + function unlockCallback(bytes calldata data) external returns (bytes memory) { if (msg.sender != address(manager)) { - revert InvalidLockAcquiredSender(); + revert InvalidUnlockCallbackSender(); } (bool success, bytes memory returnData) = address(this).call(data); @@ -331,7 +333,7 @@ contract Quoter is IQuoter, ILockCallback { /// @dev return either the sqrtPriceLimit from user input, or the max/min value possible depending on trade direction function _sqrtPriceLimitOrDefault(uint160 sqrtPriceLimitX96, bool zeroForOne) private pure returns (uint160) { return sqrtPriceLimitX96 == 0 - ? zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 + ? zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 : sqrtPriceLimitX96; } } diff --git a/contracts/libraries/CurrencyDeltas.sol b/contracts/libraries/CurrencyDeltas.sol new file mode 100644 index 00000000..339e71f6 --- /dev/null +++ b/contracts/libraries/CurrencyDeltas.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {console2} from "forge-std/console2.sol"; + +library CurrencyDeltas { + using SafeCast for uint256; + + /// @notice Get the current delta for a caller in the two given currencies + /// @param caller_ The address of the caller + /// @param currency0 The currency for which to lookup the delta + /// @param currency1 The other currency for which to lookup the delta + function currencyDeltas(IPoolManager manager, address caller_, Currency currency0, Currency currency1) + internal + view + returns (BalanceDelta) + { + bytes32 key0; + bytes32 key1; + assembly { + mstore(0, caller_) + mstore(32, currency0) + key0 := keccak256(0, 64) + + mstore(0, caller_) + mstore(32, currency1) + key1 := keccak256(0, 64) + } + bytes32[] memory slots = new bytes32[](2); + slots[0] = key0; + slots[1] = key1; + bytes32[] memory result = manager.exttload(slots); + return toBalanceDelta(int128(int256(uint256(result[0]))), int128(int256(uint256(result[1])))); + } +} diff --git a/contracts/libraries/CurrencySenderLibrary.sol b/contracts/libraries/CurrencySenderLibrary.sol new file mode 100644 index 00000000..65a44e07 --- /dev/null +++ b/contracts/libraries/CurrencySenderLibrary.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; +import {CurrencySettleTake} from "./CurrencySettleTake.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; + +/// @notice Library used to send Currencies from address to address +library CurrencySenderLibrary { + using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; + + /// @notice Send a custodied Currency to a recipient + /// @dev If sending ERC20 or native, the PoolManager must be unlocked + /// @param currency The Currency to send + /// @param manager The PoolManager + /// @param recipient The recipient address + /// @param amount The amount to send + /// @param useClaims If true, transfer ERC-6909 tokens + function send(Currency currency, IPoolManager manager, address recipient, uint256 amount, bool useClaims) + internal + { + if (useClaims) { + manager.transfer(recipient, currency.toId(), amount); + } else { + manager.burn(address(this), currency.toId(), amount); + currency.take(manager, recipient, amount, false); + } + } +} diff --git a/contracts/libraries/CurrencySettleTake.sol b/contracts/libraries/CurrencySettleTake.sol index 858963bf..9ea8f1c2 100644 --- a/contracts/libraries/CurrencySettleTake.sol +++ b/contracts/libraries/CurrencySettleTake.sol @@ -5,20 +5,41 @@ import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; +/// @notice Library used to interact with PoolManager.sol to settle any open deltas. +/// To settle a positive delta (a credit to the user), a user may take or mint. +/// To settle a negative delta (a debt on the user), a user make transfer or burn to pay off a debt. +/// @dev Note that sync() is called before any erc-20 transfer in `settle`. library CurrencySettleTake { - using CurrencyLibrary for Currency; - + /// @notice Settle (pay) a currency to the PoolManager + /// @param currency Currency to settle + /// @param manager IPoolManager to settle to + /// @param payer Address of the payer, the token sender + /// @param amount Amount to send + /// @param burn If true, burn the ERC-6909 token, otherwise ERC20-transfer to the PoolManager function settle(Currency currency, IPoolManager manager, address payer, uint256 amount, bool burn) internal { - if (currency.isNative()) { - manager.settle{value: uint128(amount)}(currency); - } else if (burn) { + // for native currencies or burns, calling sync is not required + // short circuit for ERC-6909 burns to support ERC-6909-wrapped native tokens + if (burn) { manager.burn(payer, currency.toId(), amount); + } else if (currency.isNative()) { + manager.settle{value: amount}(currency); } else { - IERC20Minimal(Currency.unwrap(currency)).transferFrom(payer, address(manager), uint128(amount)); + manager.sync(currency); + if (payer != address(this)) { + IERC20Minimal(Currency.unwrap(currency)).transferFrom(payer, address(manager), amount); + } else { + IERC20Minimal(Currency.unwrap(currency)).transfer(address(manager), amount); + } manager.settle(currency); } } + /// @notice Take (receive) a currency from the PoolManager + /// @param currency Currency to take + /// @param manager IPoolManager to take from + /// @param recipient Address of the recipient, the token receiver + /// @param amount Amount to receive + /// @param claims If true, mint the ERC-6909 token, otherwise ERC20-transfer from the PoolManager to recipient function take(Currency currency, IPoolManager manager, address recipient, uint256 amount, bool claims) internal { claims ? manager.mint(recipient, currency.toId(), amount) : manager.take(currency, recipient, amount); } diff --git a/contracts/libraries/FeeMath.sol b/contracts/libraries/FeeMath.sol index 30e97d6c..cf202dc2 100644 --- a/contracts/libraries/FeeMath.sol +++ b/contracts/libraries/FeeMath.sol @@ -3,25 +3,28 @@ pragma solidity ^0.8.24; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; library FeeMath { + using SafeCast for uint256; + function getFeesOwed( uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, - uint128 liquidity + uint256 liquidity ) internal pure returns (uint128 token0Owed, uint128 token1Owed) { token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); } - function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint128 liquidity) + function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint256 liquidity) internal pure returns (uint128 tokenOwed) { tokenOwed = - uint128(FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)); + (FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)).toUint128(); } } diff --git a/contracts/libraries/LiquiditySaltLibrary.sol b/contracts/libraries/LiquiditySaltLibrary.sol new file mode 100644 index 00000000..c0a4fda8 --- /dev/null +++ b/contracts/libraries/LiquiditySaltLibrary.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +/// @notice Library used to interact with PoolManager.sol to settle any open deltas. +/// To settle a positive delta (a credit to the user), a user may take or mint. +/// To settle a negative delta (a debt on the user), a user make transfer or burn to pay off a debt. +/// @dev Note that sync() is called before any erc-20 transfer in `settle`. +library LiquiditySaltLibrary { + /// @notice Calculates the salt parameters for IPoolManager.ModifyLiquidityParams + /// If the hook uses after*LiquidityReturnDelta, the salt is the address of the sender + /// otherwise, use 0 for warm-storage gas savings + function getLiquiditySalt(IHooks hooks, address sender) internal pure returns (bytes32 salt) { + salt = Hooks.hasPermission(hooks, Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG) + || Hooks.hasPermission(hooks, Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG) + ? bytes32(uint256(uint160(sender))) + : bytes32(0); + } +} diff --git a/contracts/libraries/PoolGetters.sol b/contracts/libraries/PoolGetters.sol index e3cb318b..df31f3c1 100644 --- a/contracts/libraries/PoolGetters.sol +++ b/contracts/libraries/PoolGetters.sol @@ -5,6 +5,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; /// @title Helper functions to access pool information /// TODO: Expose other getters on core with extsload. Only use when extsload is available and storage layout is frozen. @@ -13,6 +14,8 @@ library PoolGetters { uint256 constant TICKS_OFFSET = 4; uint256 constant TICK_BITMAP_OFFSET = 5; + using StateLibrary for IPoolManager; + function getNetLiquidityAtTick(IPoolManager poolManager, PoolId poolId, int24 tick) internal view @@ -63,7 +66,8 @@ library PoolGetters { // all the 1s at or to the right of the current bitPos uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); // uint256 masked = self[wordPos] & mask; - uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; + uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); + uint256 masked = tickBitmap & mask; // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word initialized = masked != 0; @@ -76,7 +80,8 @@ library PoolGetters { (int16 wordPos, uint8 bitPos) = position(compressed + 1); // all the 1s at or to the left of the bitPos uint256 mask = ~((1 << bitPos) - 1); - uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; + uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); + uint256 masked = tickBitmap & mask; // if there are no initialized ticks to the left of the current tick, return leftmost in the word initialized = masked != 0; diff --git a/contracts/libraries/PoolStateLibrary.sol b/contracts/libraries/PoolStateLibrary.sol deleted file mode 100644 index 487c5530..00000000 --- a/contracts/libraries/PoolStateLibrary.sol +++ /dev/null @@ -1,336 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; - -import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; - -library PoolStateLibrary { - // forge inspect lib/v4-core/src/PoolManager.sol:PoolManager storage --pretty - // | Name | Type | Slot | Offset | Bytes | Contract | - // |-----------------------|---------------------------------------------------------------------|------|--------|-------|---------------------------------------------| - // | pools | mapping(PoolId => struct Pool.State) | 8 | 0 | 32 | lib/v4-core/src/PoolManager.sol:PoolManager | - uint256 public constant POOLS_SLOT = 8; - - // index of feeGrowthGlobal0X128 in Pool.State - uint256 public constant FEE_GROWTH_GLOBAL0_OFFSET = 1; - // index of feeGrowthGlobal1X128 in Pool.State - uint256 public constant FEE_GROWTH_GLOBAL1_OFFSET = 2; - - // index of liquidity in Pool.State - uint256 public constant LIQUIDITY_OFFSET = 3; - - // index of TicksInfo mapping in Pool.State - uint256 public constant TICK_INFO_OFFSET = 4; - - // index of tickBitmap mapping in Pool.State - uint256 public constant TICK_BITMAP_OFFSET = 5; - - // index of Position.Info mapping in Pool.State - uint256 public constant POSITION_INFO_OFFSET = 6; - - /** - * @notice Get Slot0 of the pool: sqrtPriceX96, tick, protocolFee, swapFee - * @dev Corresponds to pools[poolId].slot0 - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @return sqrtPriceX96 The square root of the price of the pool, in Q96 precision. - * @return tick The current tick of the pool. - * @return protocolFee The protocol fee of the pool. - * @return swapFee The swap fee of the pool. - */ - function getSlot0(IPoolManager manager, PoolId poolId) - internal - view - returns (uint160 sqrtPriceX96, int24 tick, uint16 protocolFee, uint24 swapFee) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - bytes32 data = manager.extsload(stateSlot); - - // 32 bits |24bits|16bits |24 bits|160 bits - // 0x00000000 000bb8 0000 ffff75 0000000000000000fe3aa841ba359daa0ea9eff7 - // ---------- | fee |protocolfee | tick | sqrtPriceX96 - assembly { - // bottom 160 bits of data - sqrtPriceX96 := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - // next 24 bits of data - tick := and(shr(160, data), 0xFFFFFF) - // next 16 bits of data - protocolFee := and(shr(184, data), 0xFFFF) - // last 24 bits of data - swapFee := and(shr(200, data), 0xFFFFFF) - } - } - - /** - * @notice Retrieves the tick information of a pool at a specific tick. - * @dev Corresponds to pools[poolId].ticks[tick] - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve information for. - * @return liquidityGross The total position liquidity that references this tick - * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) - * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - */ - function getTickInfo(IPoolManager manager, PoolId poolId, int24 tick) - internal - view - returns ( - uint128 liquidityGross, - int128 liquidityNet, - uint256 feeGrowthOutside0X128, - uint256 feeGrowthOutside1X128 - ) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int24 => TickInfo) ticks` - bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); - - // slot key of the tick key: `pools[poolId].ticks[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); - - // read all 3 words of the TickInfo struct - bytes memory data = manager.extsload(slot, 3); - assembly { - liquidityGross := shr(128, mload(add(data, 32))) - liquidityNet := and(mload(add(data, 32)), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - feeGrowthOutside0X128 := mload(add(data, 64)) - feeGrowthOutside1X128 := mload(add(data, 96)) - } - } - - /** - * @notice Retrieves the liquidity information of a pool at a specific tick. - * @dev Corresponds to pools[poolId].ticks[tick].liquidityGross and pools[poolId].ticks[tick].liquidityNet. A more gas efficient version of getTickInfo - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve liquidity for. - * @return liquidityGross The total position liquidity that references this tick - * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) - */ - function getTickLiquidity(IPoolManager manager, PoolId poolId, int24 tick) - internal - view - returns (uint128 liquidityGross, int128 liquidityNet) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int24 => TickInfo) ticks` - bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); - - // slot key of the tick key: `pools[poolId].ticks[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); - - bytes32 value = manager.extsload(slot); - assembly { - liquidityNet := shr(128, value) - liquidityGross := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - } - } - - /** - * @notice Retrieves the fee growth outside a tick range of a pool - * @dev Corresponds to pools[poolId].ticks[tick].feeGrowthOutside0X128 and pools[poolId].ticks[tick].feeGrowthOutside1X128. A more gas efficient version of getTickInfo - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve fee growth for. - * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - */ - function getTickFeeGrowthOutside(IPoolManager manager, PoolId poolId, int24 tick) - internal - view - returns (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int24 => TickInfo) ticks` - bytes32 ticksMapping = bytes32(uint256(stateSlot) + TICK_INFO_OFFSET); - - // slot key of the tick key: `pools[poolId].ticks[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), ticksMapping)); - - // TODO: offset to feeGrowth, to avoid 3-word read - bytes memory data = manager.extsload(slot, 3); - assembly { - feeGrowthOutside0X128 := mload(add(data, 64)) - feeGrowthOutside1X128 := mload(add(data, 96)) - } - } - - /** - * @notice Retrieves the global fee growth of a pool. - * @dev Corresponds to pools[poolId].feeGrowthGlobal0X128 and pools[poolId].feeGrowthGlobal1X128 - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @return feeGrowthGlobal0 The global fee growth for token0. - * @return feeGrowthGlobal1 The global fee growth for token1. - */ - function getFeeGrowthGlobal(IPoolManager manager, PoolId poolId) - internal - view - returns (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State, `uint256 feeGrowthGlobal0X128` - bytes32 slot_feeGrowthGlobal0X128 = bytes32(uint256(stateSlot) + FEE_GROWTH_GLOBAL0_OFFSET); - - // reads 3rd word of Pool.State, `uint256 feeGrowthGlobal1X128` - // bytes32 slot_feeGrowthGlobal1X128 = bytes32(uint256(stateSlot) + uint256(FEE_GROWTH_GLOBAL1_OFFSET)); - - // feeGrowthGlobal0 = uint256(manager.extsload(slot_feeGrowthGlobal0X128)); - // feeGrowthGlobal1 = uint256(manager.extsload(slot_feeGrowthGlobal1X128)); - - // read the 2 words of feeGrowthGlobal - bytes memory data = manager.extsload(slot_feeGrowthGlobal0X128, 2); - assembly { - feeGrowthGlobal0 := mload(add(data, 32)) - feeGrowthGlobal1 := mload(add(data, 64)) - } - } - - /** - * @notice Retrieves total the liquidity of a pool. - * @dev Corresponds to pools[poolId].liquidity - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @return liquidity The liquidity of the pool. - */ - function getLiquidity(IPoolManager manager, PoolId poolId) internal view returns (uint128 liquidity) { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `uint128 liquidity` - bytes32 slot = bytes32(uint256(stateSlot) + LIQUIDITY_OFFSET); - - liquidity = uint128(uint256(manager.extsload(slot))); - } - - /** - * @notice Retrieves the tick bitmap of a pool at a specific tick. - * @dev Corresponds to pools[poolId].tickBitmap[tick] - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tick The tick to retrieve the bitmap for. - * @return tickBitmap The bitmap of the tick. - */ - function getTickBitmap(IPoolManager manager, PoolId poolId, int16 tick) - internal - view - returns (uint256 tickBitmap) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(int16 => uint256) tickBitmap;` - bytes32 tickBitmapMapping = bytes32(uint256(stateSlot) + TICK_BITMAP_OFFSET); - - // slot id of the mapping key: `pools[poolId].tickBitmap[tick] - bytes32 slot = keccak256(abi.encodePacked(int256(tick), tickBitmapMapping)); - - tickBitmap = uint256(manager.extsload(slot)); - } - - /** - * @notice Retrieves the position information of a pool at a specific position ID. - * @dev Corresponds to pools[poolId].positions[positionId] - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param positionId The ID of the position. - * @return liquidity The liquidity of the position. - * @return feeGrowthInside0LastX128 The fee growth inside the position for token0. - * @return feeGrowthInside1LastX128 The fee growth inside the position for token1. - */ - function getPositionInfo(IPoolManager manager, PoolId poolId, bytes32 positionId) - internal - view - returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(bytes32 => Position.Info) positions;` - bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITION_INFO_OFFSET); - - // first value slot of the mapping key: `pools[poolId].positions[positionId] (liquidity) - bytes32 slot = keccak256(abi.encodePacked(positionId, positionMapping)); - - // read all 3 words of the Position.Info struct - bytes memory data = manager.extsload(slot, 3); - - assembly { - liquidity := mload(add(data, 32)) - feeGrowthInside0LastX128 := mload(add(data, 64)) - feeGrowthInside1LastX128 := mload(add(data, 96)) - } - } - - /** - * @notice Retrieves the liquidity of a position. - * @dev Corresponds to pools[poolId].positions[positionId].liquidity. A more gas efficient version of getPositionInfo - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param positionId The ID of the position. - * @return liquidity The liquidity of the position. - */ - function getPositionLiquidity(IPoolManager manager, PoolId poolId, bytes32 positionId) - internal - view - returns (uint128 liquidity) - { - // slot key of Pool.State value: `pools[poolId]` - bytes32 stateSlot = keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); - - // Pool.State: `mapping(bytes32 => Position.Info) positions;` - bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITION_INFO_OFFSET); - - // first value slot of the mapping key: `pools[poolId].positions[positionId] (liquidity) - bytes32 slot = keccak256(abi.encodePacked(positionId, positionMapping)); - - liquidity = uint128(uint256(manager.extsload(slot))); - } - - /** - * @notice Live calculate the fee growth inside a tick range of a pool - * @dev pools[poolId].feeGrowthInside0LastX128 in Position.Info is cached and can become stale. This function will live calculate the feeGrowthInside - * @param manager The pool manager contract. - * @param poolId The ID of the pool. - * @param tickLower The lower tick of the range. - * @param tickUpper The upper tick of the range. - * @return feeGrowthInside0X128 The fee growth inside the tick range for token0. - * @return feeGrowthInside1X128 The fee growth inside the tick range for token1. - */ - function getFeeGrowthInside(IPoolManager manager, PoolId poolId, int24 tickLower, int24 tickUpper) - internal - view - returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) - { - (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = getFeeGrowthGlobal(manager, poolId); - - (uint256 lowerFeeGrowthOutside0X128, uint256 lowerFeeGrowthOutside1X128) = - getTickFeeGrowthOutside(manager, poolId, tickLower); - (uint256 upperFeeGrowthOutside0X128, uint256 upperFeeGrowthOutside1X128) = - getTickFeeGrowthOutside(manager, poolId, tickUpper); - (, int24 tickCurrent,,) = getSlot0(manager, poolId); - unchecked { - if (tickCurrent < tickLower) { - feeGrowthInside0X128 = lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; - feeGrowthInside1X128 = lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; - } else if (tickCurrent >= tickUpper) { - feeGrowthInside0X128 = upperFeeGrowthOutside0X128 - lowerFeeGrowthOutside0X128; - feeGrowthInside1X128 = upperFeeGrowthOutside1X128 - lowerFeeGrowthOutside1X128; - } else { - feeGrowthInside0X128 = feeGrowthGlobal0X128 - lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; - feeGrowthInside1X128 = feeGrowthGlobal1X128 - lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; - } - } - } -} diff --git a/contracts/libraries/PoolTicksCounter.sol b/contracts/libraries/PoolTicksCounter.sol index 077ef4a6..60fdbbe5 100644 --- a/contracts/libraries/PoolTicksCounter.sol +++ b/contracts/libraries/PoolTicksCounter.sol @@ -5,9 +5,11 @@ import {PoolGetters} from "./PoolGetters.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; library PoolTicksCounter { using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; struct TickCache { int16 wordPosLower; @@ -41,15 +43,13 @@ library PoolTicksCounter { // If the initializable tick after the swap is initialized, our original tickAfter is a // multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized // and we shouldn't count it. - uint256 bmAfter = self.getPoolBitmapInfo(key.toId(), wordPosAfter); - //uint256 bmAfter = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPosAfter); + uint256 bmAfter = self.getTickBitmap(key.toId(), wordPosAfter); cache.tickAfterInitialized = ((bmAfter & (1 << bitPosAfter)) > 0) && ((tickAfter % key.tickSpacing) == 0) && (tickBefore > tickAfter); // In the case where tickBefore is initialized, we only want to count it if we are swapping upwards. // Use the same logic as above to decide whether we should count tickBefore or not. - uint256 bmBefore = self.getPoolBitmapInfo(key.toId(), wordPos); - //uint256 bmBefore = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPos); + uint256 bmBefore = self.getTickBitmap(key.toId(), wordPos); cache.tickBeforeInitialized = ((bmBefore & (1 << bitPos)) > 0) && ((tickBefore % key.tickSpacing) == 0) && (tickBefore < tickAfter); @@ -76,8 +76,7 @@ library PoolTicksCounter { mask = mask & (type(uint256).max >> (255 - cache.bitPosHigher)); } - //uint256 bmLower = PoolGetters.getTickBitmapAtWord(self, key.toId(), cache.wordPosLower); - uint256 bmLower = self.getPoolBitmapInfo(key.toId(), cache.wordPosLower); + uint256 bmLower = self.getTickBitmap(key.toId(), cache.wordPosLower); uint256 masked = bmLower & mask; initializedTicksLoaded += countOneBits(masked); cache.wordPosLower++; diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index 2b58ecbc..00000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 diff --git a/lib/v4-core b/lib/v4-core index f5674e46..6e6ce35b 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit f5674e46720c0fc4606b287cccc583d56245e724 +Subproject commit 6e6ce35b69b15cb61bd8cb8488c7d064fab52886 diff --git a/remappings.txt b/remappings.txt index e05c5bd6..94b76d6a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,4 @@ @uniswap/v4-core/=lib/v4-core/ solmate/=lib/solmate/src/ -forge-std/=lib/forge-std/src/ @openzeppelin/=lib/openzeppelin-contracts/ +forge-std/=lib/v4-core/lib/forge-std/src/ \ No newline at end of file diff --git a/test/FullRange.t.sol b/test/FullRange.t.sol index f0867ba4..5edec106 100644 --- a/test/FullRange.t.sol +++ b/test/FullRange.t.sol @@ -20,14 +20,16 @@ import {UniswapV4ERC20} from "../contracts/libraries/UniswapV4ERC20.sol"; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; contract TestFullRange is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; using SafeCast for uint256; using CurrencyLibrary for Currency; + using StateLibrary for IPoolManager; event Initialize( - PoolId indexed poolId, + PoolId poolId, Currency indexed currency0, Currency indexed currency1, uint24 fee, @@ -39,7 +41,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); event Swap( PoolId indexed id, - address indexed sender, + address sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, @@ -104,7 +106,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { token1.approve(address(router), type(uint256).max); token2.approve(address(router), type(uint256).max); - initPool(keyWithLiq.currency0, keyWithLiq.currency1, fullRange, 3000, SQRT_RATIO_1_1, ZERO_BYTES); + initPool(keyWithLiq.currency0, keyWithLiq.currency1, fullRange, 3000, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( keyWithLiq.currency0, @@ -127,7 +129,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { emit Initialize(id, testKey.currency0, testKey.currency1, testKey.fee, testKey.tickSpacing, testKey.hooks); snapStart("FullRangeInitialize"); - manager.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); snapEnd(); (, address liquidityToken) = fullRange.poolInfo(id); @@ -139,11 +141,11 @@ contract TestFullRange is Test, Deployers, GasSnapshot { PoolKey memory wrongKey = PoolKey(key.currency0, key.currency1, 0, TICK_SPACING + 1, fullRange); vm.expectRevert(FullRange.TickSpacingNotDefault.selector); - manager.initialize(wrongKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(wrongKey, SQRT_PRICE_1_1, ZERO_BYTES); } function testFullRange_addLiquidity_InitialAddSucceeds() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -169,7 +171,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_InitialAddFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); if (amount <= LOCKED_LIQUIDITY) { vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); fullRange.addLiquidity( @@ -244,7 +246,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_SwapThenAddSucceeds() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -269,9 +271,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("FullRangeSwap"); router.swap(key, params, settings, ZERO_BYTES); @@ -298,7 +300,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_addLiquidity_FailsIfTooMuchSlippage() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -307,9 +309,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1000 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1000 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, settings, ZERO_BYTES); @@ -323,7 +325,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { function testFullRange_swap_TwoSwaps() public { PoolKey memory testKey = key; - manager.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -332,9 +334,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("FullRangeFirstSwap"); router.swap(testKey, params, settings, ZERO_BYTES); @@ -352,8 +354,8 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_swap_TwoPools() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - manager.initialize(key2, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(key2, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -367,10 +369,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10000000, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10000000, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, testSettings, ZERO_BYTES); router.swap(key2, params, testSettings, ZERO_BYTES); @@ -408,7 +410,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_InitialRemoveFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); fullRange.addLiquidity( FullRange.AddLiquidityParams( @@ -456,7 +458,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_FailsIfNoLiquidity() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); @@ -468,7 +470,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_SucceedsWithPartial() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOfSelf(); uint256 prevBalance1 = key.currency1.balanceOfSelf(); @@ -503,7 +505,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_DiffRatios() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); uint256 prevBalance0 = key.currency0.balanceOf(address(this)); uint256 prevBalance1 = key.currency1.balanceOf(address(this)); @@ -550,10 +552,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { (, address liquidityToken) = fullRange.poolInfo(idWithLiq); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_RATIO_1_2}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(keyWithLiq, params, testSettings, ZERO_BYTES); @@ -571,7 +573,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_RemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); if (amount <= LOCKED_LIQUIDITY) { @@ -626,7 +628,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { vm.prank(address(2)); token1.approve(address(fullRange), type(uint256).max); - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); // Test contract adds liquidity @@ -677,10 +679,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot { ); IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: SQRT_RATIO_1_4}); + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: SQRT_PRICE_1_4}); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, testSettings, ZERO_BYTES); @@ -704,7 +706,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_removeLiquidity_SwapRemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); (, address liquidityToken) = fullRange.poolInfo(id); if (amount <= LOCKED_LIQUIDITY) { @@ -731,11 +733,11 @@ contract TestFullRange is Test, Deployers, GasSnapshot { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: (FullMath.mulDiv(amount, 1, 4)).toInt256(), - sqrtPriceLimitX96: SQRT_RATIO_1_4 + sqrtPriceLimitX96: SQRT_PRICE_1_4 }); HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); router.swap(key, params, testSettings, ZERO_BYTES); @@ -753,12 +755,12 @@ contract TestFullRange is Test, Deployers, GasSnapshot { } function testFullRange_BeforeModifyPositionFailsWithWrongMsgSender() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); vm.expectRevert(FullRange.SenderMustBeHook.selector); modifyLiquidityRouter.modifyLiquidity( key, - IPoolManager.ModifyLiquidityParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100}), + IPoolManager.ModifyLiquidityParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100, salt: 0}), ZERO_BYTES ); } diff --git a/test/GeomeanOracle.t.sol b/test/GeomeanOracle.t.sol index 05255e93..e6ff1695 100644 --- a/test/GeomeanOracle.t.sol +++ b/test/GeomeanOracle.t.sol @@ -65,14 +65,14 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeInitializeAllowsPoolCreation() public { - manager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); } function testBeforeInitializeRevertsIfFee() public { vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); manager.initialize( PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 1, MAX_TICK_SPACING, geomeanOracle), - SQRT_RATIO_1_1, + SQRT_PRICE_1_1, ZERO_BYTES ); } @@ -81,13 +81,13 @@ contract TestGeomeanOracle is Test, Deployers { vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); manager.initialize( PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 0, 60, geomeanOracle), - SQRT_RATIO_1_1, + SQRT_PRICE_1_1, ZERO_BYTES ); } function testAfterInitializeState() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); assertEq(observationState.index, 0); assertEq(observationState.cardinality, 1); @@ -95,7 +95,7 @@ contract TestGeomeanOracle is Test, Deployers { } function testAfterInitializeObservation() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); assertTrue(observation.initialized); assertEq(observation.blockTimestamp, 1); @@ -104,7 +104,7 @@ contract TestGeomeanOracle is Test, Deployers { } function testAfterInitializeObserve0() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); uint32[] memory secondsAgo = new uint32[](1); secondsAgo[0] = 0; (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = @@ -116,11 +116,11 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeModifyPositionNoObservations() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -138,12 +138,12 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeModifyPositionObservation() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -161,7 +161,7 @@ contract TestGeomeanOracle is Test, Deployers { } function testBeforeModifyPositionObservationAndCardinality() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds geomeanOracle.increaseCardinalityNext(key, 2); GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); @@ -172,7 +172,7 @@ contract TestGeomeanOracle is Test, Deployers { modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -199,12 +199,12 @@ contract TestGeomeanOracle is Test, Deployers { } function testPermanentLiquidity() public { - manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); geomeanOracle.setTime(3); // advance 2 seconds modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 ), ZERO_BYTES ); @@ -213,7 +213,7 @@ contract TestGeomeanOracle is Test, Deployers { modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), -1000 + TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), -1000, 0 ), ZERO_BYTES ); diff --git a/test/LimitOrder.t.sol b/test/LimitOrder.t.sol index 9b9e3116..29b1093f 100644 --- a/test/LimitOrder.t.sol +++ b/test/LimitOrder.t.sol @@ -15,11 +15,13 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; contract TestLimitOrder is Test, Deployers { using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; - uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + uint160 constant SQRT_PRICE_10_1 = 250541448375047931186413801569; HookEnabledSwapRouter router; TestERC20 token0; @@ -48,7 +50,7 @@ contract TestLimitOrder is Test, Deployers { } // key = PoolKey(currency0, currency1, 3000, 60, limitOrder); - (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_PRICE_1_1, ZERO_BYTES); token0.approve(address(limitOrder), type(uint256).max); token1.approve(address(limitOrder), type(uint256).max); @@ -63,7 +65,7 @@ contract TestLimitOrder is Test, Deployers { function testGetTickLowerLastWithDifferentPrice() public { PoolKey memory differentKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 61, limitOrder); - manager.initialize(differentKey, SQRT_RATIO_10_1, ZERO_BYTES); + manager.initialize(differentKey, SQRT_PRICE_10_1, ZERO_BYTES); assertEq(limitOrder.getTickLowerLast(differentKey.toId()), 22997); } @@ -82,7 +84,8 @@ contract TestLimitOrder is Test, Deployers { uint128 liquidity = 1000000; limitOrder.place(key, tickLower, zeroForOne, liquidity); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity); + + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); } function testZeroForOneLeftBoundaryOfCurrentRange() public { @@ -91,7 +94,7 @@ contract TestLimitOrder is Test, Deployers { uint128 liquidity = 1000000; limitOrder.place(key, tickLower, zeroForOne, liquidity); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); } function testZeroForOneCrossedRangeRevert() public { @@ -103,8 +106,8 @@ contract TestLimitOrder is Test, Deployers { // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei router.swap( key, - IPoolManager.SwapParams(false, -1 ether, SQRT_RATIO_1_1 + 1), - HookEnabledSwapRouter.TestSettings(true, true), + IPoolManager.SwapParams(false, -1 ether, SQRT_PRICE_1_1 + 1), + HookEnabledSwapRouter.TestSettings(false, false), ZERO_BYTES ); vm.expectRevert(LimitOrder.InRange.selector); @@ -117,7 +120,7 @@ contract TestLimitOrder is Test, Deployers { uint128 liquidity = 1000000; limitOrder.place(key, tickLower, zeroForOne, liquidity); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); } function testNotZeroForOneCrossedRangeRevert() public { @@ -129,8 +132,8 @@ contract TestLimitOrder is Test, Deployers { // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei router.swap( key, - IPoolManager.SwapParams(true, -1 ether, SQRT_RATIO_1_1 - 1), - HookEnabledSwapRouter.TestSettings(true, true), + IPoolManager.SwapParams(true, -1 ether, SQRT_PRICE_1_1 - 1), + HookEnabledSwapRouter.TestSettings(false, false), ZERO_BYTES ); vm.expectRevert(LimitOrder.InRange.selector); @@ -151,7 +154,7 @@ contract TestLimitOrder is Test, Deployers { limitOrder.place(key, tickLower, zeroForOne, liquidity); vm.stopPrank(); assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), liquidity * 2); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity * 2); ( bool filled, @@ -191,8 +194,8 @@ contract TestLimitOrder is Test, Deployers { router.swap( key, - IPoolManager.SwapParams(false, -1e18, TickMath.getSqrtRatioAtTick(60)), - HookEnabledSwapRouter.TestSettings(true, true), + IPoolManager.SwapParams(false, -1e18, TickMath.getSqrtPriceAtTick(60)), + HookEnabledSwapRouter.TestSettings(false, false), ZERO_BYTES ); @@ -205,7 +208,7 @@ contract TestLimitOrder is Test, Deployers { assertTrue(filled); assertEq(token0Total, 0); assertEq(token1Total, 2996 + 17); // 3013, 2 wei of dust - assertEq(manager.getLiquidity(id, address(limitOrder), tickLower, tickLower + 60), 0); + assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, 0); vm.expectEmit(true, true, true, true, address(token1)); emit Transfer(address(manager), new GetSender().sender(), 2996 + 17); diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index f3d2ceb1..f434fd19 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -19,18 +19,20 @@ import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; contract QuoterTest is Test, Deployers { using SafeCast for *; using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; // Min tick for full range with tick spacing of 60 int24 internal constant MIN_TICK = -887220; // Max tick for full range with tick spacing of 60 int24 internal constant MAX_TICK = -MIN_TICK; - uint160 internal constant SQRT_RATIO_100_102 = 78447570448055484695608110440; - uint160 internal constant SQRT_RATIO_102_100 = 80016521857016594389520272648; + uint160 internal constant SQRT_PRICE_100_102 = 78447570448055484695608110440; + uint160 internal constant SQRT_PRICE_102_100 = 80016521857016594389520272648; uint256 internal constant CONTROLLER_GAS_LIMIT = 500000; @@ -119,11 +121,11 @@ contract QuoterTest is Test, Deployers { assertEq(initializedTicksLoaded, 2); } - // nested self-call into lockAcquired reverts - function testQuoter_callLockAcquired_reverts() public { + // nested self-call into unlockCallback reverts + function testQuoter_callUnlockCallback_reverts() public { vm.expectRevert(IQuoter.LockFailure.selector); vm.prank(address(manager)); - quoter.lockAcquired(abi.encodeWithSelector(quoter.lockAcquired.selector, address(this), "0x")); + quoter.unlockCallback(abi.encodeWithSelector(quoter.unlockCallback.selector, address(this), "0x")); } function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { @@ -325,13 +327,13 @@ contract QuoterTest is Test, Deployers { zeroForOne: true, recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_100_102, + sqrtPriceLimitX96: SQRT_PRICE_100_102, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[0], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_100_102); + assertEq(sqrtPriceX96After, SQRT_PRICE_100_102); assertEq(initializedTicksLoaded, 0); } @@ -343,13 +345,13 @@ contract QuoterTest is Test, Deployers { zeroForOne: false, recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_102_100, + sqrtPriceLimitX96: SQRT_PRICE_102_100, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[1], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_102_100); + assertEq(sqrtPriceX96After, SQRT_PRICE_102_100); assertEq(initializedTicksLoaded, 0); } @@ -542,7 +544,7 @@ contract QuoterTest is Test, Deployers { } function setupPool(PoolKey memory poolKey) internal { - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity( @@ -550,14 +552,15 @@ contract QuoterTest is Test, Deployers { IPoolManager.ModifyLiquidityParams( MIN_TICK, MAX_TICK, - calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + 0 ), ZERO_BYTES ); } function setupPoolMultiplePositions(PoolKey memory poolKey) internal { - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity( @@ -565,21 +568,22 @@ contract QuoterTest is Test, Deployers { IPoolManager.ModifyLiquidityParams( MIN_TICK, MAX_TICK, - calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - -60, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -60, 60, 100, 100).toInt256() + -60, 60, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -60, 60, 100, 100).toInt256(), 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - -120, 120, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 120, 100, 100).toInt256() + -120, 120, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -120, 120, 100, 100).toInt256(), 0 ), ZERO_BYTES ); @@ -589,7 +593,7 @@ contract QuoterTest is Test, Deployers { PoolId poolId = poolKey.toId(); (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); if (sqrtPriceX96 == 0) { - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); } MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); @@ -599,21 +603,22 @@ contract QuoterTest is Test, Deployers { IPoolManager.ModifyLiquidityParams( MIN_TICK, MAX_TICK, - calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + calculateLiquidityFromAmounts(SQRT_PRICE_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - 0, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, 0, 60, 100, 100).toInt256() + 0, 60, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, 0, 60, 100, 100).toInt256(), 0 ), ZERO_BYTES ); positionManager.modifyLiquidity( poolKey, IPoolManager.ModifyLiquidityParams( - -120, 0, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 0, 100, 100).toInt256() + -120, 0, calculateLiquidityFromAmounts(SQRT_PRICE_1_1, -120, 0, 100, 100).toInt256(), 0 ), ZERO_BYTES ); @@ -626,8 +631,8 @@ contract QuoterTest is Test, Deployers { uint256 amount0, uint256 amount1 ) internal pure returns (uint128 liquidity) { - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); + uint160 sqrtRatioAX96 = TickMath.getSqrtPriceAtTick(tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtPriceAtTick(tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1); } diff --git a/test/SimpleBatchCallTest.t.sol b/test/SimpleBatchCallTest.t.sol index 367dcb1a..04a0e922 100644 --- a/test/SimpleBatchCallTest.t.sol +++ b/test/SimpleBatchCallTest.t.sol @@ -14,11 +14,13 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Test} from "forge-std/Test.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; /// @title SimpleBatchCall /// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. contract SimpleBatchCallTest is Test, Deployers { using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; SimpleBatchCall batchCall; @@ -35,30 +37,28 @@ contract SimpleBatchCallTest is Test, Deployers { function test_initialize() public { bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_RATIO_1_1, ZERO_BYTES); - bytes memory settleData = - abi.encode(SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true})); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + bytes memory settleData = abi.encode(SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false})); batchCall.execute(abi.encode(calls), ZERO_BYTES); (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId()); - assertEq(sqrtPriceX96, SQRT_RATIO_1_1); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); } function test_initialize_modifyPosition() public { bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_RATIO_1_1, ZERO_BYTES); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); calls[1] = abi.encodeWithSelector( ICallsWithLock.modifyPositionWithLock.selector, key, - IPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18}), + IPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18, salt: 0}), ZERO_BYTES ); Currency[] memory currenciesTouched = new Currency[](2); currenciesTouched[0] = currency0; currenciesTouched[1] = currency1; - bytes memory settleData = abi.encode( - currenciesTouched, SimpleBatchCall.SettleConfig({withdrawTokens: true, settleUsingTransfer: true}) - ); + bytes memory settleData = + abi.encode(currenciesTouched, SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false})); uint256 balance0 = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); uint256 balance1 = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); batchCall.execute(abi.encode(calls), settleData); @@ -69,6 +69,6 @@ contract SimpleBatchCallTest is Test, Deployers { assertGt(balance0After, balance0); assertGt(balance1After, balance1); - assertEq(sqrtPriceX96, SQRT_RATIO_1_1); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); } } diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol index 96941963..0f2f82e0 100644 --- a/test/TWAMM.t.sol +++ b/test/TWAMM.t.sol @@ -69,21 +69,21 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { } } - (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_PRICE_1_1, ZERO_BYTES); token0.approve(address(modifyLiquidityRouter), 100 ether); token1.approve(address(modifyLiquidityRouter), 100 ether); token0.mint(address(this), 100 ether); token1.mint(address(this), 100 ether); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether, 0), ZERO_BYTES ); } @@ -93,7 +93,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(twamm.lastVirtualOrderTimestamp(initId), 0); vm.warp(10000); - manager.initialize(initKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(initKey, SQRT_PRICE_1_1, ZERO_BYTES); assertEq(twamm.lastVirtualOrderTimestamp(initId), 10000); } @@ -363,7 +363,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { token0.approve(address(twamm), 100e18); token1.approve(address(twamm), 100e18); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES ); vm.warp(10000); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 0f6afbc7..a0b78ac0 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -19,7 +19,6 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; @@ -48,7 +47,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); lpm = new NonfungiblePositionManager(manager); @@ -70,20 +69,17 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { vm.stopPrank(); } - function test_collect_6909(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + function test_collect_6909(IPoolManager.ModifyLiquidityParams memory params) public { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees uint256 swapAmount = 0.01e18; swap(key, false, -int256(swapAmount), ZERO_BYTES); // collect fees - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); BalanceDelta delta = lpm.collect(tokenId, address(this), ZERO_BYTES, true); assertEq(delta.amount0(), 0); @@ -93,12 +89,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertEq(uint256(int256(delta.amount1())), manager.balanceOf(address(this), currency1.toId())); } - function test_collect_erc20(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + function test_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); uint256 tokenId; - liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity // swap to create fees uint256 swapAmount = 0.01e18; @@ -118,37 +113,24 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { } // two users with the same range; one user cannot collect the other's fees - function test_collect_sameRange_6909( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDeltaAlice, - uint128 liquidityDeltaBob - ) public { + function test_collect_sameRange_6909(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) + public + { uint256 tokenIdAlice; uint256 tokenIdBob; - liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity - liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDeltaAlice); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); vm.prank(alice); - (tokenIdAlice,) = lpm.mint( - LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), - liquidityDeltaAlice, - block.timestamp + 1, - alice, - ZERO_BYTES - ); + (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); vm.prank(bob); - (tokenIdBob,) = lpm.mint( - LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}), - liquidityDeltaBob, - block.timestamp + 1, - alice, - ZERO_BYTES - ); + (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); // swap to create fees uint256 swapAmount = 0.01e18; @@ -173,31 +155,28 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { assertApproxEqAbs(manager.balanceOf(address(lpm), currency1.toId()), 0, 1 wei); } - function test_collect_sameRange_erc20( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDeltaAlice, - uint128 liquidityDeltaBob - ) public { - liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity - liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); - + function test_collect_sameRange_erc20(IPoolManager.ModifyLiquidityParams memory params, uint256 liquidityDeltaBob) + public + { uint256 tokenIdAlice; - vm.startPrank(alice); - (tokenIdAlice, tickLower, tickUpper, liquidityDeltaAlice,) = - createFuzzyLiquidity(lpm, alice, key, tickLower, tickUpper, liquidityDeltaAlice, ZERO_BYTES); - vm.stopPrank(); - uint256 tokenIdBob; - vm.startPrank(bob); - (tokenIdBob,,,,) = createFuzzyLiquidity(lpm, bob, key, tickLower, tickUpper, liquidityDeltaBob, ZERO_BYTES); - vm.stopPrank(); + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + vm.prank(alice); + (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); - vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity + vm.prank(bob); + (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); // confirm the positions are same range - (,, LiquidityRange memory rangeAlice,,,,,) = lpm.positions(tokenIdAlice); - (,, LiquidityRange memory rangeBob,,,,,) = lpm.positions(tokenIdBob); + (, LiquidityRange memory rangeAlice) = lpm.tokenPositions(tokenIdAlice); + (, LiquidityRange memory rangeBob) = lpm.tokenPositions(tokenIdBob); assertEq(rangeAlice.tickLower, rangeBob.tickLower); assertEq(rangeAlice.tickUpper, rangeBob.tickUpper); @@ -238,69 +217,40 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { function test_collect_donate_sameRange() public {} function test_decreaseLiquidity_sameRange( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDeltaAlice, - uint128 liquidityDeltaBob + IPoolManager.ModifyLiquidityParams memory params, + uint256 liquidityDeltaBob ) public { - liquidityDeltaAlice = uint128(bound(liquidityDeltaAlice, 100e18, 100_000e18)); // require nontrivial amount of liquidity - liquidityDeltaBob = uint128(bound(liquidityDeltaBob, 100e18, 100_000e18)); - uint256 tokenIdAlice; - BalanceDelta lpDeltaAlice; - vm.startPrank(alice); - (tokenIdAlice, tickLower, tickUpper, liquidityDeltaAlice, lpDeltaAlice) = - createFuzzyLiquidity(lpm, alice, key, tickLower, tickUpper, liquidityDeltaAlice, ZERO_BYTES); - vm.stopPrank(); - uint256 tokenIdBob; - BalanceDelta lpDeltaBob; - vm.startPrank(bob); - (tokenIdBob,,,, lpDeltaBob) = - createFuzzyLiquidity(lpm, bob, key, tickLower, tickUpper, liquidityDeltaBob, ZERO_BYTES); - vm.stopPrank(); + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity - vm.assume(tickLower < -key.tickSpacing && key.tickSpacing < tickUpper); // require two-sided liquidity + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + vm.prank(alice); + (tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES); + + vm.prank(bob); + (tokenIdBob,) = lpm.mint(range, liquidityDeltaBob, block.timestamp + 1, bob, ZERO_BYTES); // swap to create fees uint256 swapAmount = 0.001e18; swap(key, true, -int256(swapAmount), ZERO_BYTES); // alice removes all of her liquidity - // uint256 balance0AliceBefore = manager.balanceOf(alice, currency0.toId()); - // uint256 balance1AliceBefore = manager.balanceOf(alice, currency1.toId()); vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: liquidityDeltaAlice, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: alice - }), - ZERO_BYTES, - true - ); - assertEq(uint256(uint128(-aliceDelta.amount0())), manager.balanceOf(alice, currency0.toId())); - assertEq(uint256(uint128(-aliceDelta.amount1())), manager.balanceOf(alice, currency1.toId())); + BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, uint256(params.liquidityDelta), ZERO_BYTES, true); + assertEq(uint256(uint128(aliceDelta.amount0())), manager.balanceOf(alice, currency0.toId())); + assertEq(uint256(uint128(aliceDelta.amount1())), manager.balanceOf(alice, currency1.toId())); // bob removes half of his liquidity vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdBob, - liquidityDelta: liquidityDeltaBob / 2, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: bob - }), - ZERO_BYTES, - true - ); - assertEq(uint256(uint128(-bobDelta.amount0())), manager.balanceOf(bob, currency0.toId())); - assertEq(uint256(uint128(-bobDelta.amount1())), manager.balanceOf(bob, currency1.toId())); + BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityDeltaBob / 2, ZERO_BYTES, true); + assertEq(uint256(uint128(bobDelta.amount0())), manager.balanceOf(bob, currency0.toId())); + assertEq(uint256(uint128(bobDelta.amount1())), manager.balanceOf(bob, currency1.toId())); // position manager holds no fees now assertApproxEqAbs(manager.balanceOf(address(lpm), currency0.toId()), 0, 1 wei); @@ -331,18 +281,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice decreases liquidity vm.prank(alice); - BalanceDelta aliceDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityAlice), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: alice - }), - ZERO_BYTES, - true - ); + BalanceDelta aliceDelta = lpm.decreaseLiquidity(tokenIdAlice, liquidityAlice, ZERO_BYTES, true); uint256 tolerance = 0.000000001 ether; @@ -362,18 +301,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // bob decreases half of his liquidity vm.prank(bob); - BalanceDelta bobDelta = lpm.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: tokenIdBob, - liquidityDelta: uint128(liquidityBob / 2), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: bob - }), - ZERO_BYTES, - true - ); + BalanceDelta bobDelta = lpm.decreaseLiquidity(tokenIdBob, liquidityBob / 2, ZERO_BYTES, true); // bob claims half of the original principal + his fees assertApproxEqAbs( diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/Gas.t.sol index 5b98ac97..939d88be 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/Gas.t.sol @@ -15,16 +15,14 @@ import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {PoolStateLibrary} from "../../contracts/libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; -import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; contract GasTest is Test, Deployers, GasSnapshot { using FixedPointMathLib for uint256; @@ -52,7 +50,7 @@ contract GasTest is Test, Deployers, GasSnapshot { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); lpm = new NonfungiblePositionManager(manager); @@ -68,23 +66,23 @@ contract GasTest is Test, Deployers, GasSnapshot { range = LiquidityRange({key: key, tickLower: -300, tickUpper: 300}); } - function test_gas_mint() public { - uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - snapStart("mint"); - lpm.mint(params); - snapEnd(); - } + // function test_gas_mint() public { + // uint256 amount0Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity + // uint256 amount1Desired = 148873216119575134691; // 148 ether tokens, 10_000 liquidity + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: 0, + // amount1Min: 0, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // snapStart("mint"); + // lpm.mint(params); + // snapEnd(); + // } function test_gas_mintWithLiquidity() public { snapStart("mintWithLiquidity"); @@ -95,66 +93,32 @@ contract GasTest is Test, Deployers, GasSnapshot { function test_gas_increaseLiquidity_erc20() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager - .IncreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 1000 ether, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }); snapStart("increaseLiquidity_erc20"); - lpm.increaseLiquidity(params, ZERO_BYTES, false); + lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, false); snapEnd(); } function test_gas_increaseLiquidity_erc6909() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager - .IncreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 1000 ether, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }); snapStart("increaseLiquidity_erc6909"); - lpm.increaseLiquidity(params, ZERO_BYTES, true); + lpm.increaseLiquidity(tokenId, 1000 ether, ZERO_BYTES, true); snapEnd(); } function test_gas_decreaseLiquidity_erc20() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 10_000 ether, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); snapStart("decreaseLiquidity_erc20"); - lpm.decreaseLiquidity(params, ZERO_BYTES, false); + lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, false); snapEnd(); } function test_gas_decreaseLiquidity_erc6909() public { (uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: 10_000 ether, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); snapStart("decreaseLiquidity_erc6909"); - lpm.decreaseLiquidity(params, ZERO_BYTES, true); + lpm.decreaseLiquidity(tokenId, 10_000 ether, ZERO_BYTES, true); snapEnd(); } diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index 666619db..c3863b9f 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -15,18 +15,17 @@ import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {LiquidityAmounts} from "../../contracts/libraries/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {PoolStateLibrary} from "../../contracts/libraries/PoolStateLibrary.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; -import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; -contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { +contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using LiquidityRangeIdLibrary for LiquidityRange; @@ -52,7 +51,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); lpm = new NonfungiblePositionManager(manager); @@ -99,30 +98,18 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice uses her exact fees to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); - console2.log("token0Owed", token0Owed); - console2.log("token1Owed", token1Owed); - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(range.tickLower), - TickMath.getSqrtRatioAtTick(range.tickUpper), + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), token0Owed, token1Owed ); vm.prank(alice); - lpm.increaseLiquidity( - INonfungiblePositionManager.IncreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityDelta), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }), - ZERO_BYTES, - false - ); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); // TODO: assertions, currently increasing liquidity does not perfectly use the fees } @@ -147,30 +134,20 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { swap(key, true, -int256(swapAmount), ZERO_BYTES); swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back - // alice will half of her fees to increase liquidity + // alice will use half of her fees to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); { - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(range.tickLower), - TickMath.getSqrtRatioAtTick(range.tickUpper), + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), token0Owed / 2, token1Owed / 2 ); vm.prank(alice); - lpm.increaseLiquidity( - INonfungiblePositionManager.IncreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityDelta), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }), - ZERO_BYTES, - false - ); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); } { @@ -237,11 +214,11 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { // alice will use all of her fees + additional capital to increase liquidity (uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice); { - (uint160 sqrtPriceX96,,,) = PoolStateLibrary.getSlot0(manager, range.key.toId()); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.key.toId()); uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, - TickMath.getSqrtRatioAtTick(range.tickLower), - TickMath.getSqrtRatioAtTick(range.tickUpper), + TickMath.getSqrtPriceAtTick(range.tickLower), + TickMath.getSqrtPriceAtTick(range.tickUpper), token0Owed * 2, token1Owed * 2 ); @@ -249,17 +226,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers { uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); vm.prank(alice); - lpm.increaseLiquidity( - INonfungiblePositionManager.IncreaseLiquidityParams({ - tokenId: tokenIdAlice, - liquidityDelta: uint128(liquidityDelta), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 - }), - ZERO_BYTES, - false - ); + lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false); uint256 balance0AfterAlice = currency0.balanceOf(alice); uint256 balance1AfterAlice = currency1.balanceOf(alice); diff --git a/test/position-managers/NonfungiblePositionManager.t.sol b/test/position-managers/NonfungiblePositionManager.t.sol index d4d0ee6c..47d537d4 100644 --- a/test/position-managers/NonfungiblePositionManager.t.sol +++ b/test/position-managers/NonfungiblePositionManager.t.sol @@ -19,7 +19,6 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {INonfungiblePositionManager} from "../../contracts/interfaces/INonfungiblePositionManager.sol"; import {NonfungiblePositionManager} from "../../contracts/NonfungiblePositionManager.sol"; import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "../../contracts/types/LiquidityRange.sol"; @@ -42,7 +41,7 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_RATIO_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); lpm = new NonfungiblePositionManager(manager); @@ -50,171 +49,176 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max); } - function test_mint_withLiquidityDelta(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + function test_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params) public { + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); (uint256 tokenId, BalanceDelta delta) = - lpm.mint(position, liquidityDelta, block.timestamp + 1, address(this), ZERO_BYTES); + lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, address(this), ZERO_BYTES); uint256 balance0After = currency0.balanceOfSelf(); uint256 balance1After = currency1.balanceOfSelf(); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta)); assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0())), "incorrect amount0"); assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1())), "incorrect amount1"); } - function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency1.balanceOfSelf(); - - assertEq(tokenId, 1); - assertEq(lpm.ownerOf(1), address(this)); - assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - } - - // minting with perfect token ratios will use all of the tokens - function test_mint_perfect() public { - int24 tickLower = -int24(key.tickSpacing); - int24 tickUpper = int24(key.tickSpacing); - uint256 amount0Desired = 100e18; - uint256 amount1Desired = 100e18; - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: amount0Desired, - amount1Min: amount1Desired, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - uint256 balance0After = currency0.balanceOfSelf(); - uint256 balance1After = currency1.balanceOfSelf(); - - assertEq(tokenId, 1); - assertEq(lpm.ownerOf(1), address(this)); - assertEq(uint256(int256(-delta.amount0())), amount0Desired); - assertEq(uint256(int256(-delta.amount1())), amount1Desired); - assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); - assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); - } - - function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - public - { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: alice, - hookData: ZERO_BYTES - }); - (uint256 tokenId,) = lpm.mint(params); - assertEq(tokenId, 1); - assertEq(lpm.ownerOf(tokenId), alice); - } - - function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - public - { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - vm.assume(tickLower < 0 && 0 < tickUpper); - - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); - vm.assume(0.00001e18 < amount0Desired); - vm.assume(0.00001e18 < amount1Desired); - - uint256 amount0Min = amount0Desired - 1; - uint256 amount1Min = amount1Desired - 1; - - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: amount0Min, - amount1Min: amount1Min, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - - // seed some liquidity so we can move the price - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: TickMath.minUsableTick(key.tickSpacing), - tickUpper: TickMath.maxUsableTick(key.tickSpacing), - liquidityDelta: 100_000e18 - }), - ZERO_BYTES - ); - - // swap to move the price - swap(key, true, -1000e18, ZERO_BYTES); - - // will revert because amount0Min and amount1Min are very strict - vm.expectRevert(); - lpm.mint(params); - } - - function test_burn(int24 tickLower, int24 tickUpper, uint128 liquidityDelta) public { + // function test_mint(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) public { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: 0, + // amount1Min: 0, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // uint256 balance0After = currency0.balanceOfSelf(); + // uint256 balance1After = currency1.balanceOfSelf(); + + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(1), address(this)); + // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // } + + // // minting with perfect token ratios will use all of the tokens + // function test_mint_perfect() public { + // int24 tickLower = -int24(key.tickSpacing); + // int24 tickUpper = int24(key.tickSpacing); + // uint256 amount0Desired = 100e18; + // uint256 amount1Desired = 100e18; + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: amount0Desired, + // amount1Min: amount1Desired, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); + // uint256 balance0After = currency0.balanceOfSelf(); + // uint256 balance1After = currency1.balanceOfSelf(); + + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(1), address(this)); + // assertEq(uint256(int256(-delta.amount0())), amount0Desired); + // assertEq(uint256(int256(-delta.amount1())), amount1Desired); + // assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + // assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + // } + + // function test_mint_recipient(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // public + // { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: 0, + // amount1Min: 0, + // deadline: block.timestamp + 1, + // recipient: alice, + // hookData: ZERO_BYTES + // }); + // (uint256 tokenId,) = lpm.mint(params); + // assertEq(tokenId, 1); + // assertEq(lpm.ownerOf(tokenId), alice); + // } + + // function test_mint_slippageRevert(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) + // public + // { + // (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); + // vm.assume(tickLower < 0 && 0 < tickUpper); + + // (amount0Desired, amount1Desired) = + // createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + // vm.assume(0.00001e18 < amount0Desired); + // vm.assume(0.00001e18 < amount1Desired); + + // uint256 amount0Min = amount0Desired - 1; + // uint256 amount1Min = amount1Desired - 1; + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + // INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ + // range: range, + // amount0Desired: amount0Desired, + // amount1Desired: amount1Desired, + // amount0Min: amount0Min, + // amount1Min: amount1Min, + // deadline: block.timestamp + 1, + // recipient: address(this), + // hookData: ZERO_BYTES + // }); + + // // seed some liquidity so we can move the price + // modifyLiquidityRouter.modifyLiquidity( + // key, + // IPoolManager.ModifyLiquidityParams({ + // tickLower: TickMath.minUsableTick(key.tickSpacing), + // tickUpper: TickMath.maxUsableTick(key.tickSpacing), + // liquidityDelta: 100_000e18, + // salt: 0 + // }), + // ZERO_BYTES + // ); + + // // swap to move the price + // swap(key, true, -1000e18, ZERO_BYTES); + + // // will revert because amount0Min and amount1Min are very strict + // vm.expectRevert(); + // lpm.mint(params); + // } + + function test_burn(IPoolManager.ModifyLiquidityParams memory params) public { uint256 balance0Start = currency0.balanceOfSelf(); uint256 balance1Start = currency1.balanceOfSelf(); // create liquidity we can burn uint256 tokenId; - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); assertEq(tokenId, 1); assertEq(lpm.ownerOf(1), address(this)); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta)); // burn liquidity uint256 balance0BeforeBurn = currency0.balanceOfSelf(); uint256 balance1BeforeBurn = currency1.balanceOfSelf(); BalanceDelta delta = lpm.burn(tokenId, address(this), ZERO_BYTES, false); - assertEq(lpm.liquidityOf(address(this), position.toId()), 0); + (,, liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, 0); // TODO: slightly off by 1 bip (0.0001%) assertApproxEqRel(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(-delta.amount0())), 0.0001e18); @@ -229,119 +233,60 @@ contract NonfungiblePositionManagerTest is Test, Deployers, GasSnapshot, Liquidi assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } - function test_increaseLiquidity() public {} - - function test_decreaseLiquidity( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDelta, - uint128 decreaseLiquidityDelta - ) public { - uint256 tokenId; - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(0 < decreaseLiquidityDelta); - vm.assume(decreaseLiquidityDelta <= liquidityDelta); - - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: decreaseLiquidityDelta, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); - BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES, false); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta - decreaseLiquidityDelta); - - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0()))); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1()))); - } - - function test_decreaseLiquidity_collectFees( - int24 tickLower, - int24 tickUpper, - uint128 liquidityDelta, - uint128 decreaseLiquidityDelta - ) public { + function test_decreaseLiquidity(IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta) + public + { uint256 tokenId; - liquidityDelta = uint128(bound(liquidityDelta, 100e18, 100_000e18)); // require nontrivial amount of liquidity - (tokenId, tickLower, tickUpper, liquidityDelta,) = - createFuzzyLiquidity(lpm, address(this), key, tickLower, tickUpper, liquidityDelta, ZERO_BYTES); - vm.assume(tickLower < -60 && 60 < tickUpper); // require two-sided liquidity + (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); vm.assume(0 < decreaseLiquidityDelta); - vm.assume(decreaseLiquidityDelta <= liquidityDelta); - - // swap to create fees - uint256 swapAmount = 0.01e18; - swap(key, false, int256(swapAmount), ZERO_BYTES); + vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); - LiquidityRange memory position = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + LiquidityRange memory range = + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); uint256 balance0Before = currency0.balanceOfSelf(); uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidityDelta: decreaseLiquidityDelta, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp + 1 - }); - BalanceDelta delta = lpm.decreaseLiquidity(params, ZERO_BYTES, false); - assertEq(lpm.liquidityOf(address(this), position.toId()), liquidityDelta - decreaseLiquidityDelta, "GRR"); - - // express key.fee as wad (i.e. 3000 = 0.003e18) - uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); - - assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); - assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); - } - - function test_mintTransferBurn(int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired) - public - { - (tickLower, tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, DEAD_VALUE); - (amount0Desired, amount1Desired) = - createFuzzyAmountDesired(key, tickLower, tickUpper, amount0Desired, amount1Desired); + BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); - LiquidityRange memory range = LiquidityRange({key: key, tickLower: tickLower, tickUpper: tickUpper}); + (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); - uint256 balance0Before = currency0.balanceOfSelf(); - uint256 balance1Before = currency1.balanceOfSelf(); - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - range: range, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1, - recipient: address(this), - hookData: ZERO_BYTES - }); - (uint256 tokenId, BalanceDelta delta) = lpm.mint(params); - uint256 liquidity = lpm.liquidityOf(address(this), range.toId()); - - // transfer to Alice - lpm.transferFrom(address(this), alice, tokenId); - - assertEq(lpm.liquidityOf(address(this), range.toId()), 0); - assertEq(lpm.ownerOf(tokenId), alice); - assertEq(lpm.liquidityOf(alice, range.toId()), liquidity); - - // Alice can burn the token - vm.prank(alice); - lpm.burn(tokenId, address(this), ZERO_BYTES, false); - - // TODO: assert balances + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); + assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); } + // function test_decreaseLiquidity_collectFees( + // IPoolManager.ModifyLiquidityParams memory params, + // uint256 decreaseLiquidityDelta + // ) public { + // uint256 tokenId; + // (tokenId, params,) = createFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + // vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + // vm.assume(0 < decreaseLiquidityDelta); + // vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + // vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + + // LiquidityRange memory range = LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // // swap to create fees + // uint256 swapAmount = 0.01e18; + // swap(key, false, int256(swapAmount), ZERO_BYTES); + + // uint256 balance0Before = currency0.balanceOfSelf(); + // uint256 balance1Before = currency1.balanceOfSelf(); + // BalanceDelta delta = lpm.decreaseLiquidity(tokenId, decreaseLiquidityDelta, ZERO_BYTES, false); + // (,, uint256 liquidity,,,,) = lpm.positions(address(this), range.toId()); + // assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // // express key.fee as wad (i.e. 3000 = 0.003e18) + // uint256 feeWad = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + // assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(-delta.amount0())), "boo"); + // assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(-delta.amount1())), "guh"); + // } + + function test_mintTransferBurn() public {} function test_mintTransferCollect() public {} function test_mintTransferIncrease() public {} function test_mintTransferDecrease() public {} diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 1facdf59..6f1e7f0a 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -2,118 +2,35 @@ pragma solidity ^0.8.24; import {Vm} from "forge-std/Vm.sol"; -import {StdUtils} from "forge-std/StdUtils.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; + import {INonfungiblePositionManager} from "../../../contracts/interfaces/INonfungiblePositionManager.sol"; import {LiquidityRange} from "../../../contracts/types/LiquidityRange.sol"; -contract LiquidityFuzzers is StdUtils { - Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - - function assumeLiquidityDelta(PoolKey memory key, uint128 liquidityDelta) internal pure { - _vm.assume(0.0000001e18 < liquidityDelta); - _vm.assume(liquidityDelta < Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)); - } - - function boundTicks(PoolKey memory key, int24 tickLower, int24 tickUpper) internal view returns (int24, int24) { - tickLower = int24( - bound( - int256(tickLower), - int256(TickMath.minUsableTick(key.tickSpacing)), - int256(TickMath.maxUsableTick(key.tickSpacing)) - ) - ); - tickUpper = int24( - bound( - int256(tickUpper), - int256(TickMath.minUsableTick(key.tickSpacing)), - int256(TickMath.maxUsableTick(key.tickSpacing)) - ) - ); - - // round down ticks - tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; - tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; - _vm.assume(tickLower < tickUpper); - return (tickLower, tickUpper); - } - - /// @dev Obtain fuzzed parameters for creating liquidity - /// @param key The pool key - /// @param tickLower The lower tick - /// @param tickUpper The upper tick - /// @param liquidityDelta The liquidity delta - function createFuzzyLiquidityParams(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidityDelta) - internal - view - returns (int24 _tickLower, int24 _tickUpper) - { - assumeLiquidityDelta(key, liquidityDelta); - (_tickLower, _tickUpper) = boundTicks(key, tickLower, tickUpper); - } - +contract LiquidityFuzzers is Fuzzers { function createFuzzyLiquidity( INonfungiblePositionManager lpm, address recipient, PoolKey memory key, - int24 tickLower, - int24 tickUpper, - uint128 liquidityDelta, + IPoolManager.ModifyLiquidityParams memory params, + uint160 sqrtPriceX96, bytes memory hookData - ) - internal - returns (uint256 _tokenId, int24 _tickLower, int24 _tickUpper, uint128 _liquidityDelta, BalanceDelta _delta) - { - (_tickLower, _tickUpper) = createFuzzyLiquidityParams(key, tickLower, tickUpper, liquidityDelta); - _liquidityDelta = liquidityDelta; - (_tokenId, _delta) = lpm.mint( - LiquidityRange({key: key, tickLower: _tickLower, tickUpper: _tickUpper}), - _liquidityDelta, + ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory, BalanceDelta) { + params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); + + (uint256 tokenId, BalanceDelta delta) = lpm.mint( + LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper}), + uint256(params.liquidityDelta), block.timestamp, recipient, hookData ); - } - - function createFuzzyAmountDesired( - PoolKey memory key, - int24 tickLower, - int24 tickUpper, - uint256 amount0, - uint256 amount1 - ) internal view returns (uint256 _amount0, uint256 _amount1) { - // fuzzing amount desired is a nice to have instead of using liquidityDelta, however we often violate TickOverflow - // (too many tokens in a tight range) -- need to figure out how to bound it better - bool tight = (tickUpper - tickLower) < 300 * key.tickSpacing; - uint256 maxAmount0 = tight ? 100e18 : 1_000e18; - uint256 maxAmount1 = tight ? 100e18 : 1_000e18; - _amount0 = bound(amount0, 0, maxAmount0); - _amount1 = bound(amount1, 0, maxAmount1); - _vm.assume(_amount0 != 0 && _amount1 != 0); - } - - function createFuzzySameRange( - INonfungiblePositionManager lpm, - address alice, - address bob, - LiquidityRange memory range, - uint128 liquidityA, - uint128 liquidityB, - bytes memory hookData - ) internal returns (uint256, uint256, int24, int24, uint128, uint128) { - assumeLiquidityDelta(range.key, liquidityA); - assumeLiquidityDelta(range.key, liquidityB); - - (range.tickLower, range.tickUpper) = boundTicks(range.key, range.tickLower, range.tickUpper); - - (uint256 tokenIdA,) = lpm.mint(range, liquidityA, block.timestamp + 1, alice, hookData); - - (uint256 tokenIdB,) = lpm.mint(range, liquidityB, block.timestamp + 1, bob, hookData); - return (tokenIdA, tokenIdB, range.tickLower, range.tickUpper, liquidityA, liquidityB); + return (tokenId, params, delta); } } diff --git a/test/utils/HookEnabledSwapRouter.sol b/test/utils/HookEnabledSwapRouter.sol index 4311439c..4021f453 100644 --- a/test/utils/HookEnabledSwapRouter.sol +++ b/test/utils/HookEnabledSwapRouter.sol @@ -8,9 +8,11 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolTestBase} from "@uniswap/v4-core/src/test/PoolTestBase.sol"; import {Test} from "forge-std/Test.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; contract HookEnabledSwapRouter is PoolTestBase { using CurrencyLibrary for Currency; + using CurrencySettler for Currency; error NoSwapOccurred(); @@ -25,8 +27,8 @@ contract HookEnabledSwapRouter is PoolTestBase { } struct TestSettings { - bool withdrawTokens; - bool settleUsingTransfer; + bool takeClaims; + bool settleUsingBurn; } function swap( @@ -36,14 +38,14 @@ contract HookEnabledSwapRouter is PoolTestBase { bytes memory hookData ) external payable returns (BalanceDelta delta) { delta = abi.decode( - manager.lock(abi.encode(CallbackData(msg.sender, testSettings, key, params, hookData))), (BalanceDelta) + manager.unlock(abi.encode(CallbackData(msg.sender, testSettings, key, params, hookData))), (BalanceDelta) ); uint256 ethBalance = address(this).balance; if (ethBalance > 0) CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); } - function lockAcquired(bytes calldata rawData) external returns (bytes memory) { + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { require(msg.sender == address(manager)); CallbackData memory data = abi.decode(rawData, (CallbackData)); @@ -54,14 +56,22 @@ contract HookEnabledSwapRouter is PoolTestBase { if (BalanceDelta.unwrap(delta) == 0) revert NoSwapOccurred(); if (data.params.zeroForOne) { - _settle(data.key.currency0, data.sender, delta.amount0(), data.testSettings.settleUsingTransfer); + data.key.currency0.settle( + manager, data.sender, uint256(int256(-delta.amount0())), data.testSettings.settleUsingBurn + ); if (delta.amount1() > 0) { - _take(data.key.currency1, data.sender, delta.amount1(), data.testSettings.withdrawTokens); + data.key.currency1.take( + manager, data.sender, uint256(int256(delta.amount1())), data.testSettings.takeClaims + ); } } else { - _settle(data.key.currency1, data.sender, delta.amount1(), data.testSettings.settleUsingTransfer); + data.key.currency1.settle( + manager, data.sender, uint256(int256(-delta.amount1())), data.testSettings.settleUsingBurn + ); if (delta.amount0() > 0) { - _take(data.key.currency0, data.sender, delta.amount0(), data.testSettings.withdrawTokens); + data.key.currency0.take( + manager, data.sender, uint256(int256(delta.amount0())), data.testSettings.takeClaims + ); } }