// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {IHooks} from "../src/interfaces/IHooks.sol";
import {Hooks} from "../src/libraries/Hooks.sol";
import {IPoolManager} from "../src/interfaces/IPoolManager.sol";
import {IProtocolFees} from "../src/interfaces/IProtocolFees.sol";
import {PoolManager} from "../src/PoolManager.sol";
import {TickMath} from "../src/libraries/TickMath.sol";
import {Pool} from "../src/libraries/Pool.sol";
import {Deployers} from "./utils/Deployers.sol";
import {Currency, CurrencyLibrary} from "../src/types/Currency.sol";
import {MockHooks} from "../src/test/MockHooks.sol";
import {MockContract} from "../src/test/MockContract.sol";
import {EmptyTestHooks} from "../src/test/EmptyTestHooks.sol";
import {PoolKey} from "../src/types/PoolKey.sol";
import {ModifyLiquidityParams, SwapParams} from "../src/types/PoolOperation.sol";
import {PoolModifyLiquidityTest} from "../src/test/PoolModifyLiquidityTest.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "../src/types/BalanceDelta.sol";
import {PoolSwapTest} from "../src/test/PoolSwapTest.sol";
import {TestInvalidERC20} from "../src/test/TestInvalidERC20.sol";
import {PoolEmptyUnlockTest} from "../src/test/PoolEmptyUnlockTest.sol";
import {Action} from "../src/test/PoolNestedActionsTest.sol";
import {PoolId} from "../src/types/PoolId.sol";
import {LPFeeLibrary} from "../src/libraries/LPFeeLibrary.sol";
import {Position} from "../src/libraries/Position.sol";
import {Constants} from "./utils/Constants.sol";
import {SafeCast} from "../src/libraries/SafeCast.sol";
import {AmountHelpers} from "./utils/AmountHelpers.sol";
import {ProtocolFeeLibrary} from "../src/libraries/ProtocolFeeLibrary.sol";
import {IProtocolFees} from "../src/interfaces/IProtocolFees.sol";
import {StateLibrary} from "../src/libraries/StateLibrary.sol";
import {TransientStateLibrary} from "../src/libraries/TransientStateLibrary.sol";
import {Actions} from "../src/test/ActionsRouter.sol";
import {CustomRevert} from "../src/libraries/CustomRevert.sol";
contract PoolManagerTest is Test, Deployers {
using Hooks for IHooks;
using LPFeeLibrary for uint24;
using SafeCast for *;
using ProtocolFeeLibrary for uint24;
using StateLibrary for IPoolManager;
using TransientStateLibrary for IPoolManager;
event UnlockCallback();
event ProtocolFeeControllerUpdated(address feeController);
event ModifyLiquidity(
PoolId indexed poolId,
address indexed sender,
int24 tickLower,
int24 tickUpper,
int256 liquidityDelta,
bytes32 salt
);
event Swap(
PoolId indexed poolId,
address indexed sender,
int128 amount0,
int128 amount1,
uint160 sqrtPriceX96,
uint128 liquidity,
int24 tick,
uint24 fee
);
event Donate(PoolId indexed id, address indexed sender, uint256 amount0, uint256 amount1);
event Transfer(
address caller, address indexed sender, address indexed receiver, uint256 indexed id, uint256 amount
);
PoolEmptyUnlockTest emptyUnlockRouter;
uint24 constant MAX_PROTOCOL_FEE_BOTH_TOKENS = (1000 << 12) | 1000; // 1000 1000
address recipientAddress = makeAddr("recipientAddress");
function setUp() public {
initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0)));
emptyUnlockRouter = new PoolEmptyUnlockTest(manager);
}
function test_bytecodeSize() public {
vm.snapshotValue("poolManager bytecode size", address(manager).code.length);
}
function test_initcodeHash() public {
vm.snapshotValue(
"poolManager initcode hash (without constructor params, as uint256)",
uint256(keccak256(type(PoolManager).creationCode))
);
}
function test_addLiquidity_failsIfNotInitialized() public {
vm.expectRevert(Pool.PoolNotInitialized.selector);
modifyLiquidityRouter.modifyLiquidity(uninitializedKey, LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_addLiquidity_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.modifyLiquidity(uninitializedKey, LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_removeLiquidity_failsIfNotInitialized() public {
vm.expectRevert(Pool.PoolNotInitialized.selector);
modifyLiquidityRouter.modifyLiquidity(uninitializedKey, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_removeLiquidity_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_addLiquidity_succeedsIfInitialized(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
vm.expectEmit(true, true, false, true, address(manager));
emit ModifyLiquidity(
key.toId(),
address(modifyLiquidityRouter),
LIQUIDITY_PARAMS.tickLower,
LIQUIDITY_PARAMS.tickUpper,
LIQUIDITY_PARAMS.liquidityDelta,
LIQUIDITY_PARAMS.salt
);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_removeLiquidity_succeedsIfInitialized(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
vm.expectEmit(true, true, false, true, address(manager));
emit ModifyLiquidity(
key.toId(),
address(modifyLiquidityRouter),
REMOVE_LIQUIDITY_PARAMS.tickLower,
REMOVE_LIQUIDITY_PARAMS.tickUpper,
REMOVE_LIQUIDITY_PARAMS.liquidityDelta,
LIQUIDITY_PARAMS.salt
);
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_addLiquidity_succeedsForNativeTokensIfInitialized(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
vm.expectEmit(true, true, false, true, address(manager));
emit ModifyLiquidity(
nativeKey.toId(),
address(modifyLiquidityRouter),
LIQUIDITY_PARAMS.tickLower,
LIQUIDITY_PARAMS.tickUpper,
LIQUIDITY_PARAMS.liquidityDelta,
LIQUIDITY_PARAMS.salt
);
modifyLiquidityRouter.modifyLiquidity{value: 1 ether}(nativeKey, LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_removeLiquidity_succeedsForNativeTokensIfInitialized(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
vm.expectEmit(true, true, false, true, address(manager));
emit ModifyLiquidity(
nativeKey.toId(),
address(modifyLiquidityRouter),
REMOVE_LIQUIDITY_PARAMS.tickLower,
REMOVE_LIQUIDITY_PARAMS.tickUpper,
REMOVE_LIQUIDITY_PARAMS.liquidityDelta,
LIQUIDITY_PARAMS.salt
);
modifyLiquidityRouter.modifyLiquidity{value: 1 ether}(nativeKey, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_addLiquidity_succeedsWithHooksIfInitialized(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
address payable mockAddr =
payable(address(uint160(Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG)));
address payable hookAddr = payable(Constants.ALL_HOOKS);
vm.etch(hookAddr, vm.getDeployedCode("EmptyTestHooks.sol:EmptyTestHooks"));
MockContract mockContract = new MockContract();
vm.etch(mockAddr, address(mockContract).code);
MockContract(mockAddr).setImplementation(hookAddr);
(key,) = initPool(currency0, currency1, IHooks(mockAddr), 3000, sqrtPriceX96);
BalanceDelta balanceDelta = modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
bytes32 beforeSelector = MockHooks.beforeAddLiquidity.selector;
bytes memory beforeParams = abi.encode(address(modifyLiquidityRouter), key, LIQUIDITY_PARAMS, ZERO_BYTES);
bytes32 afterSelector = MockHooks.afterAddLiquidity.selector;
bytes memory afterParams = abi.encode(
address(modifyLiquidityRouter),
key,
LIQUIDITY_PARAMS,
balanceDelta,
BalanceDeltaLibrary.ZERO_DELTA,
ZERO_BYTES
);
assertEq(MockContract(mockAddr).timesCalledSelector(beforeSelector), 1);
assertTrue(MockContract(mockAddr).calledWithSelector(beforeSelector, beforeParams));
assertEq(MockContract(mockAddr).timesCalledSelector(afterSelector), 1);
assertTrue(MockContract(mockAddr).calledWithSelector(afterSelector, afterParams));
}
function test_removeLiquidity_succeedsWithHooksIfInitialized(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
address payable mockAddr =
payable(address(uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)));
address payable hookAddr = payable(Constants.ALL_HOOKS);
vm.etch(hookAddr, vm.getDeployedCode("EmptyTestHooks.sol:EmptyTestHooks"));
MockContract mockContract = new MockContract();
vm.etch(mockAddr, address(mockContract).code);
MockContract(mockAddr).setImplementation(hookAddr);
(key,) = initPool(currency0, currency1, IHooks(mockAddr), 3000, sqrtPriceX96);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
BalanceDelta balanceDelta = modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
bytes32 beforeSelector = MockHooks.beforeRemoveLiquidity.selector;
bytes memory beforeParams = abi.encode(address(modifyLiquidityRouter), key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
bytes32 afterSelector = MockHooks.afterRemoveLiquidity.selector;
bytes memory afterParams = abi.encode(
address(modifyLiquidityRouter),
key,
REMOVE_LIQUIDITY_PARAMS,
balanceDelta,
BalanceDeltaLibrary.ZERO_DELTA,
ZERO_BYTES
);
assertEq(MockContract(mockAddr).timesCalledSelector(beforeSelector), 1);
assertTrue(MockContract(mockAddr).calledWithSelector(beforeSelector, beforeParams));
assertEq(MockContract(mockAddr).timesCalledSelector(afterSelector), 1);
assertTrue(MockContract(mockAddr).calledWithSelector(afterSelector, afterParams));
}
function test_addLiquidity_failsWithIncorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPool(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
mockHooks.setReturnValue(mockHooks.beforeAddLiquidity.selector, bytes4(0xdeadbeef));
mockHooks.setReturnValue(mockHooks.afterAddLiquidity.selector, bytes4(0xdeadbeef));
// Fails at beforeAddLiquidity hook.
vm.expectRevert(Hooks.InvalidHookResponse.selector);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
// Fail at afterAddLiquidity hook.
mockHooks.setReturnValue(mockHooks.beforeAddLiquidity.selector, mockHooks.beforeAddLiquidity.selector);
vm.expectRevert(Hooks.InvalidHookResponse.selector);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_removeLiquidity_failsWithIncorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPool(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
mockHooks.setReturnValue(mockHooks.beforeRemoveLiquidity.selector, bytes4(0xdeadbeef));
mockHooks.setReturnValue(mockHooks.afterRemoveLiquidity.selector, bytes4(0xdeadbeef));
// Fails at beforeRemoveLiquidity hook.
vm.expectRevert(Hooks.InvalidHookResponse.selector);
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
// Fail at afterRemoveLiquidity hook.
mockHooks.setReturnValue(mockHooks.beforeRemoveLiquidity.selector, mockHooks.beforeRemoveLiquidity.selector);
vm.expectRevert(Hooks.InvalidHookResponse.selector);
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_addLiquidity_succeedsWithCorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPool(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
mockHooks.setReturnValue(mockHooks.beforeAddLiquidity.selector, mockHooks.beforeAddLiquidity.selector);
mockHooks.setReturnValue(mockHooks.afterAddLiquidity.selector, mockHooks.afterAddLiquidity.selector);
vm.expectEmit(true, true, false, true, address(manager));
emit ModifyLiquidity(
key.toId(),
address(modifyLiquidityRouter),
LIQUIDITY_PARAMS.tickLower,
LIQUIDITY_PARAMS.tickUpper,
LIQUIDITY_PARAMS.liquidityDelta,
LIQUIDITY_PARAMS.salt
);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_removeLiquidity_succeedsWithCorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPool(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
mockHooks.setReturnValue(mockHooks.beforeRemoveLiquidity.selector, mockHooks.beforeRemoveLiquidity.selector);
mockHooks.setReturnValue(mockHooks.afterRemoveLiquidity.selector, mockHooks.afterRemoveLiquidity.selector);
vm.expectEmit(true, true, false, true, address(manager));
emit ModifyLiquidity(
key.toId(),
address(modifyLiquidityRouter),
REMOVE_LIQUIDITY_PARAMS.tickLower,
REMOVE_LIQUIDITY_PARAMS.tickUpper,
REMOVE_LIQUIDITY_PARAMS.liquidityDelta,
REMOVE_LIQUIDITY_PARAMS.salt
);
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_addLiquidity_6909() public {
// convert test tokens into ERC6909 claims
claimsRouter.deposit(currency0, address(this), 10_000e18);
claimsRouter.deposit(currency1, address(this), 10_000e18);
assertEq(manager.balanceOf(address(this), currency0.toId()), 10_000e18);
assertEq(manager.balanceOf(address(this), currency1.toId()), 10_000e18);
uint256 currency0BalanceBefore = currency0.balanceOfSelf();
uint256 currency1BalanceBefore = currency1.balanceOfSelf();
uint256 currency0PMBalanceBefore = currency0.balanceOf(address(manager));
uint256 currency1PMBalanceBefore = currency1.balanceOf(address(manager));
// allow liquidity router to burn our 6909 tokens
manager.setOperator(address(modifyLiquidityRouter), true);
// add liquidity with 6909: settleUsingBurn=true, takeClaims=true (unused)
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES, true, true);
assertLt(manager.balanceOf(address(this), currency0.toId()), 10_000e18);
assertLt(manager.balanceOf(address(this), currency1.toId()), 10_000e18);
// ERC20s are unspent
assertEq(currency0.balanceOfSelf(), currency0BalanceBefore);
assertEq(currency1.balanceOfSelf(), currency1BalanceBefore);
// PoolManager did not receive net-new ERC20s
assertEq(currency0.balanceOf(address(manager)), currency0PMBalanceBefore);
assertEq(currency1.balanceOf(address(manager)), currency1PMBalanceBefore);
}
function test_removeLiquidity_6909() public {
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
assertEq(manager.balanceOf(address(this), currency0.toId()), 0);
assertEq(manager.balanceOf(address(this), currency1.toId()), 0);
uint256 currency0BalanceBefore = currency0.balanceOfSelf();
uint256 currency1BalanceBefore = currency1.balanceOfSelf();
uint256 currency0PMBalanceBefore = currency0.balanceOf(address(manager));
uint256 currency1PMBalanceBefore = currency1.balanceOf(address(manager));
// remove liquidity as 6909: settleUsingBurn=true (unused), takeClaims=true
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES, true, true);
assertTrue(manager.balanceOf(address(this), currency0.toId()) > 0);
assertTrue(manager.balanceOf(address(this), currency1.toId()) > 0);
// ERC20s are unspent
assertEq(currency0.balanceOfSelf(), currency0BalanceBefore);
assertEq(currency1.balanceOfSelf(), currency1BalanceBefore);
// PoolManager did lose ERC-20s
assertEq(currency0.balanceOf(address(manager)), currency0PMBalanceBefore);
assertEq(currency1.balanceOf(address(manager)), currency1PMBalanceBefore);
}
function test_addLiquidity_gas() public {
ModifyLiquidityParams memory uniqueParams =
ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0});
modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES);
vm.snapshotGasLastCall("simple addLiquidity");
}
function test_addLiquidity_secondAdditionSameRange_gas() public {
ModifyLiquidityParams memory uniqueParams =
ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0});
modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES);
modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES);
vm.snapshotGasLastCall("simple addLiquidity second addition same range");
}
function test_removeLiquidity_gas() public {
ModifyLiquidityParams memory uniqueParams =
ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0});
// add some liquidity to remove
modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES);
uniqueParams.liquidityDelta *= -1;
modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES);
vm.snapshotGasLastCall("simple removeLiquidity");
}
function test_removeLiquidity_someLiquidityRemains_gas() public {
// add double the liquidity to remove
ModifyLiquidityParams memory uniqueParams =
ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0});
modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES);
uniqueParams.liquidityDelta /= -2;
modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES);
vm.snapshotGasLastCall("simple removeLiquidity some liquidity remains");
}
function test_addLiquidity_succeeds() public {
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_removeLiquidity_succeeds() public {
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
}
function test_addLiquidity_withNative_gas() public {
modifyLiquidityRouter.modifyLiquidity{value: 1 ether}(nativeKey, LIQUIDITY_PARAMS, ZERO_BYTES);
vm.snapshotGasLastCall("addLiquidity with native token");
}
function test_removeLiquidity_withNative_gas() public {
modifyLiquidityRouter.modifyLiquidity{value: 1 ether}(nativeKey, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
vm.snapshotGasLastCall("removeLiquidity with native token");
}
function test_addLiquidity_withHooks_gas() public {
address allHooksAddr = Constants.ALL_HOOKS;
MockHooks impl = new MockHooks();
vm.etch(allHooksAddr, address(impl).code);
MockHooks mockHooks = MockHooks(allHooksAddr);
(key,) = initPool(currency0, currency1, mockHooks, 3000, SQRT_PRICE_1_1);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
vm.snapshotGasLastCall("addLiquidity with empty hook");
}
function test_removeLiquidity_withHooks_gas() public {
address allHooksAddr = Constants.ALL_HOOKS;
MockHooks impl = new MockHooks();
vm.etch(allHooksAddr, address(impl).code);
MockHooks mockHooks = MockHooks(allHooksAddr);
(key,) = initPool(currency0, currency1, mockHooks, 3000, SQRT_PRICE_1_1);
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
vm.snapshotGasLastCall("removeLiquidity with empty hook");
}
function test_swap_failsIfNotInitialized(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
key.fee = 100;
SwapParams memory params =
SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: sqrtPriceX96});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
vm.expectRevert(Pool.PoolNotInitialized.selector);
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
}
function test_swap_succeedsIfInitialized() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
vm.expectEmit(true, true, true, true);
emit Swap(
key.toId(), address(swapRouter), int128(-100), int128(98), 79228162514264329749955861424, 1e18, -1, 3000
);
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
}
function test_swap_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.swap(key, SWAP_PARAMS, ZERO_BYTES);
}
function test_swap_succeedsWithNativeTokensIfInitialized() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
vm.expectEmit(true, true, true, true);
emit Swap(
nativeKey.toId(),
address(swapRouter),
int128(-100),
int128(98),
79228162514264329749955861424,
1e18,
-1,
3000
);
swapRouter.swap{value: 100}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
}
function test_swap_succeedsWithHooksIfInitialized() public {
address payable mockAddr = payable(address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG)));
address payable hookAddr = payable(Constants.ALL_HOOKS);
vm.etch(hookAddr, vm.getDeployedCode("EmptyTestHooks.sol:EmptyTestHooks"));
MockContract mockContract = new MockContract();
vm.etch(mockAddr, address(mockContract).code);
MockContract(mockAddr).setImplementation(hookAddr);
(key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(mockAddr), 3000, SQRT_PRICE_1_1);
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
BalanceDelta balanceDelta = swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
bytes32 beforeSelector = MockHooks.beforeSwap.selector;
bytes memory beforeParams = abi.encode(address(swapRouter), key, SWAP_PARAMS, ZERO_BYTES);
bytes32 afterSelector = MockHooks.afterSwap.selector;
bytes memory afterParams = abi.encode(address(swapRouter), key, SWAP_PARAMS, balanceDelta, ZERO_BYTES);
assertEq(MockContract(mockAddr).timesCalledSelector(beforeSelector), 1);
assertTrue(MockContract(mockAddr).calledWithSelector(beforeSelector, beforeParams));
assertEq(MockContract(mockAddr).timesCalledSelector(afterSelector), 1);
assertTrue(MockContract(mockAddr).calledWithSelector(afterSelector, afterParams));
}
function test_swap_failsWithIncorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPoolAndAddLiquidity(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
SwapParams memory swapParams =
SwapParams({zeroForOne: true, amountSpecified: 10, sqrtPriceLimitX96: SQRT_PRICE_1_2});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
mockHooks.setReturnValue(mockHooks.beforeSwap.selector, bytes4(0xdeadbeef));
mockHooks.setReturnValue(mockHooks.afterSwap.selector, bytes4(0xdeadbeef));
// Fails at beforeSwap hook.
vm.expectRevert(Hooks.InvalidHookResponse.selector);
swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES);
// Fail at afterSwap hook.
mockHooks.setReturnValue(mockHooks.beforeSwap.selector, mockHooks.beforeSwap.selector);
vm.expectRevert(Hooks.InvalidHookResponse.selector);
swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES);
}
function test_swap_succeedsWithCorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPoolAndAddLiquidity(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
SwapParams memory swapParams =
SwapParams({zeroForOne: true, amountSpecified: -10, sqrtPriceLimitX96: SQRT_PRICE_1_2});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
mockHooks.setReturnValue(mockHooks.beforeSwap.selector, mockHooks.beforeSwap.selector);
mockHooks.setReturnValue(mockHooks.afterSwap.selector, mockHooks.afterSwap.selector);
vm.expectEmit(true, true, true, true);
emit Swap(key.toId(), address(swapRouter), -10, 8, 79228162514264336880490487708, 1e18, -1, 100);
swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES);
}
function test_swap_succeeds() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
}
function test_swap_gas() public {
swapRouterNoChecks.swap(key, SWAP_PARAMS);
vm.snapshotGasLastCall("simple swap");
}
function test_swap_withNative_succeeds() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: 100}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
}
function test_swap_withNative_gas() public {
swapRouterNoChecks.swap{value: 100}(nativeKey, SWAP_PARAMS);
vm.snapshotGasLastCall("simple swap with native");
}
function test_swap_withHooks_gas() public {
address allHooksAddr = Constants.ALL_HOOKS;
MockHooks impl = new MockHooks();
vm.etch(allHooksAddr, address(impl).code);
MockHooks mockHooks = MockHooks(allHooksAddr);
(key,) = initPoolAndAddLiquidity(currency0, currency1, mockHooks, 3000, SQRT_PRICE_1_1);
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
SwapParams memory swapParams =
SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_4});
testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES);
vm.snapshotGasLastCall("swap with hooks");
}
function test_swap_mint6909IfOutputNotTaken_gas() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
vm.expectEmit();
emit Transfer(address(swapRouter), address(0), address(this), CurrencyLibrary.toId(currency1), 98);
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
vm.snapshotGasLastCall("swap mint output as 6909");
uint256 erc6909Balance = manager.balanceOf(address(this), CurrencyLibrary.toId(currency1));
assertEq(erc6909Balance, 98);
}
function test_swap_mint6909IfNativeOutputNotTaken_gas() public {
SwapParams memory params =
SwapParams({zeroForOne: false, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_2_1});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
vm.expectEmit();
emit Transfer(
address(swapRouter), address(0), address(this), CurrencyLibrary.toId(CurrencyLibrary.ADDRESS_ZERO), 98
);
swapRouter.swap(nativeKey, params, testSettings, ZERO_BYTES);
vm.snapshotGasLastCall("swap mint native output as 6909");
uint256 erc6909Balance = manager.balanceOf(address(this), CurrencyLibrary.toId(CurrencyLibrary.ADDRESS_ZERO));
assertEq(erc6909Balance, 98);
}
function test_swap_burn6909AsInput_gas() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
vm.expectEmit();
emit Transfer(address(swapRouter), address(0), address(this), CurrencyLibrary.toId(currency1), 98);
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
uint256 erc6909Balance = manager.balanceOf(address(this), uint256(uint160(Currency.unwrap(currency1))));
assertEq(erc6909Balance, 98);
// give permission for swapRouter to burn the 6909s
manager.setOperator(address(swapRouter), true);
// swap from currency1 to currency0 again, using 6909s as input tokens
SwapParams memory params =
SwapParams({zeroForOne: false, amountSpecified: 25, sqrtPriceLimitX96: SQRT_PRICE_4_1});
testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: true});
vm.expectEmit();
emit Transfer(address(swapRouter), address(this), address(0), CurrencyLibrary.toId(currency1), 27);
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
vm.snapshotGasLastCall("swap burn 6909 for input");
erc6909Balance = manager.balanceOf(address(this), CurrencyLibrary.toId(currency1));
assertEq(erc6909Balance, 71);
}
function test_swap_burnNative6909AsInput_gas() public {
SwapParams memory params =
SwapParams({zeroForOne: false, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_2_1});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});
vm.expectEmit();
emit Transfer(
address(swapRouter), address(0), address(this), CurrencyLibrary.toId(CurrencyLibrary.ADDRESS_ZERO), 98
);
swapRouter.swap(nativeKey, params, testSettings, ZERO_BYTES);
uint256 erc6909Balance = manager.balanceOf(address(this), CurrencyLibrary.toId(CurrencyLibrary.ADDRESS_ZERO));
assertEq(erc6909Balance, 98);
// give permission for swapRouter to burn the 6909s
manager.setOperator(address(swapRouter), true);
// swap from currency0 to currency1, using 6909s as input tokens
params = SwapParams({zeroForOne: true, amountSpecified: 25, sqrtPriceLimitX96: SQRT_PRICE_1_4});
testSettings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: true});
vm.expectEmit();
emit Transfer(
address(swapRouter), address(this), address(0), CurrencyLibrary.toId(CurrencyLibrary.ADDRESS_ZERO), 27
);
// don't have to send in native currency since burning 6909 for input
swapRouter.swap(nativeKey, params, testSettings, ZERO_BYTES);
vm.snapshotGasLastCall("swap burn native 6909 for input");
erc6909Balance = manager.balanceOf(address(this), CurrencyLibrary.toId(CurrencyLibrary.ADDRESS_ZERO));
assertEq(erc6909Balance, 71);
}
function test_swap_againstLiquidity_gas() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
SwapParams memory params =
SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_4});
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
vm.snapshotGasLastCall("swap against liquidity");
}
function test_swap_againstLiqWithNative_gas() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: 1 ether}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
SwapParams memory params =
SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_4});
swapRouter.swap{value: 1 ether}(nativeKey, params, testSettings, ZERO_BYTES);
vm.snapshotGasLastCall("swap against liquidity with native token");
}
function test_swap_accruesProtocolFees(uint16 protocolFee0, uint16 protocolFee1, int256 amountSpecified) public {
protocolFee0 = uint16(bound(protocolFee0, 0, 1000));
protocolFee1 = uint16(bound(protocolFee1, 0, 1000));
vm.assume(amountSpecified != 0);
uint24 protocolFee = (uint24(protocolFee1) << 12) | uint24(protocolFee0);
(,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, 0);
vm.prank(feeController);
manager.setProtocolFee(key, protocolFee);
(,, slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, protocolFee);
// Add liquidity - Fees dont accrue for positive liquidity delta.
ModifyLiquidityParams memory params = LIQUIDITY_PARAMS;
modifyLiquidityRouter.modifyLiquidity(key, params, ZERO_BYTES);
assertEq(manager.protocolFeesAccrued(currency0), 0);
assertEq(manager.protocolFeesAccrued(currency1), 0);
// Remove liquidity - Fees dont accrue for negative liquidity delta.
params.liquidityDelta = -LIQUIDITY_PARAMS.liquidityDelta;
modifyLiquidityRouter.modifyLiquidity(key, params, ZERO_BYTES);
assertEq(manager.protocolFeesAccrued(currency0), 0);
assertEq(manager.protocolFeesAccrued(currency1), 0);
// Now re-add the liquidity to test swap
params.liquidityDelta = LIQUIDITY_PARAMS.liquidityDelta;
modifyLiquidityRouter.modifyLiquidity(key, params, ZERO_BYTES);
SwapParams memory swapParams = SwapParams(false, amountSpecified, TickMath.MAX_SQRT_PRICE - 1);
BalanceDelta delta = swapRouter.swap(key, swapParams, PoolSwapTest.TestSettings(false, false), ZERO_BYTES);
uint256 expectedProtocolFee =
uint256(uint128(-delta.amount1())) * protocolFee1 / ProtocolFeeLibrary.PIPS_DENOMINATOR;
assertEq(manager.protocolFeesAccrued(currency0), 0);
assertEq(manager.protocolFeesAccrued(currency1), expectedProtocolFee);
}
function test_donate_failsIfNotInitialized() public {
vm.expectRevert(Pool.PoolNotInitialized.selector);
donateRouter.donate(uninitializedKey, 100, 100, ZERO_BYTES);
}
function test_donate_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.donate(key, 100, 100, ZERO_BYTES);
}
function test_donate_failsIfNoLiquidity(uint160 sqrtPriceX96) public {
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE - 1));
(key,) = initPool(currency0, currency1, IHooks(address(0)), 100, sqrtPriceX96);
vm.expectRevert(Pool.NoLiquidityToReceiveFees.selector);
donateRouter.donate(key, 100, 100, ZERO_BYTES);
}
// test successful donation if pool has liquidity
function test_donate_succeedsWhenPoolHasLiquidity() public {
(uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = manager.getFeeGrowthGlobals(key.toId());
assertEq(feeGrowthGlobal0X128, 0);
assertEq(feeGrowthGlobal1X128, 0);
donateRouter.donate(key, 100, 200, ZERO_BYTES);
vm.snapshotGasLastCall("donate gas with 2 tokens");
(feeGrowthGlobal0X128, feeGrowthGlobal1X128) = manager.getFeeGrowthGlobals(key.toId());
assertEq(feeGrowthGlobal0X128, 34028236692093846346337);
assertEq(feeGrowthGlobal1X128, 68056473384187692692674);
}
function test_donate_succeedsForNativeTokensWhenPoolHasLiquidity() public {
(uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = manager.getFeeGrowthGlobals(nativeKey.toId());
assertEq(feeGrowthGlobal0X128, 0);
assertEq(feeGrowthGlobal1X128, 0);
donateRouter.donate{value: 100}(nativeKey, 100, 200, ZERO_BYTES);
(feeGrowthGlobal0X128, feeGrowthGlobal1X128) = manager.getFeeGrowthGlobals(nativeKey.toId());
assertEq(feeGrowthGlobal0X128, 34028236692093846346337);
assertEq(feeGrowthGlobal1X128, 68056473384187692692674);
}
function test_donate_failsWithIncorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPoolAndAddLiquidity(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
mockHooks.setReturnValue(mockHooks.beforeDonate.selector, bytes4(0xdeadbeef));
mockHooks.setReturnValue(mockHooks.afterDonate.selector, bytes4(0xdeadbeef));
// Fails at beforeDonate hook.
vm.expectRevert(Hooks.InvalidHookResponse.selector);
donateRouter.donate(key, 100, 200, ZERO_BYTES);
// Fail at afterDonate hook.
mockHooks.setReturnValue(mockHooks.beforeDonate.selector, mockHooks.beforeDonate.selector);
vm.expectRevert(Hooks.InvalidHookResponse.selector);
donateRouter.donate(key, 100, 200, ZERO_BYTES);
}
function test_donate_succeedsWithCorrectSelectors() public {
address hookAddr = address(uint160(Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG));
MockHooks impl = new MockHooks();
vm.etch(hookAddr, address(impl).code);
MockHooks mockHooks = MockHooks(hookAddr);
(key,) = initPoolAndAddLiquidity(currency0, currency1, mockHooks, 100, SQRT_PRICE_1_1);
mockHooks.setReturnValue(mockHooks.beforeDonate.selector, mockHooks.beforeDonate.selector);
mockHooks.setReturnValue(mockHooks.afterDonate.selector, mockHooks.afterDonate.selector);
donateRouter.donate(key, 100, 200, ZERO_BYTES);
}
function test_donate_OneToken_gas() public {
donateRouter.donate(key, 100, 0, ZERO_BYTES);
vm.snapshotGasLastCall("donate gas with 1 token");
}
function test_fuzz_donate_emits_event(uint256 amount0, uint256 amount1) public {
amount0 = bound(amount0, 0, uint256(int256(type(int128).max)));
amount1 = bound(amount1, 0, uint256(int256(type(int128).max)));
vm.expectEmit(true, true, false, true, address(manager));
emit Donate(key.toId(), address(donateRouter), uint256(amount0), uint256(amount1));
donateRouter.donate(key, amount0, amount1, ZERO_BYTES);
}
function test_take_failsWithNoLiquidity() public {
deployFreshManagerAndRouters();
vm.expectRevert();
takeRouter.take(key, 100, 0);
}
function test_take_failsWithInvalidTokensThatDoNotReturnTrueOnTransfer() public {
TestInvalidERC20 invalidToken = new TestInvalidERC20(2 ** 255);
Currency invalidCurrency = Currency.wrap(address(invalidToken));
invalidToken.approve(address(modifyLiquidityRouter), type(uint256).max);
invalidToken.approve(address(takeRouter), type(uint256).max);
bool currency0Invalid = invalidCurrency < currency0;
(key,) = initPoolAndAddLiquidity(
(currency0Invalid ? invalidCurrency : currency0),
(currency0Invalid ? currency0 : invalidCurrency),
IHooks(address(0)),
3000,
SQRT_PRICE_1_1
);
(uint256 amount0, uint256 amount1) = currency0Invalid ? (1, 0) : (0, 1);
vm.expectRevert(
abi.encodeWithSelector(
CustomRevert.WrappedError.selector,
address(invalidToken),
TestInvalidERC20.transfer.selector,
abi.encode(bytes32(0)),
abi.encodeWithSelector(CurrencyLibrary.ERC20TransferFailed.selector)
)
);
takeRouter.take(key, amount0, amount1);
// should not revert when non zero amount passed in for valid currency
// assertions inside takeRouter because it takes then settles
(amount0, amount1) = currency0Invalid ? (0, 1) : (1, 0);
takeRouter.take(key, amount0, amount1);
}
function test_take_succeedsWithPoolWithLiquidity() public {
takeRouter.take(key, 1, 1); // assertions inside takeRouter because it takes then settles
}
function test_take_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.take(key.currency0, address(this), 1);
}
function test_take_succeedsWithPoolWithLiquidityWithNativeToken() public {
takeRouter.take{value: 1}(nativeKey, 1, 1); // assertions inside takeRouter because it takes then settles
}
function test_settle_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.settle();
}
function test_settle_revertsSendingNative_withTokenSynced() public {
Actions[] memory actions = new Actions[](2);
bytes[] memory params = new bytes[](2);
actions[0] = Actions.SYNC;
params[0] = abi.encode(key.currency0);
// Revert with NonzeroNativeValue
actions[1] = Actions.SETTLE_NATIVE;
params[1] = abi.encode(1);
vm.expectRevert(IPoolManager.NonzeroNativeValue.selector);
actionsRouter.executeActions{value: 1}(actions, params);
}
function test_mint_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.mint(address(this), key.currency0.toId(), 1);
}
function test_burn_failsIfLocked() public {
vm.expectRevert(IPoolManager.ManagerLocked.selector);
manager.burn(address(this), key.currency0.toId(), 1);
}
function test_collectProtocolFees_locked_revertsWithProtocolFeeCurrencySynced() public noIsolate {
manager.setProtocolFeeController(address(this));
// currency1 is never native
manager.sync(key.currency1);
assertEq(Currency.unwrap(key.currency1), Currency.unwrap(manager.getSyncedCurrency()));
vm.expectRevert(IProtocolFees.ProtocolFeeCurrencySynced.selector);
manager.collectProtocolFees(address(this), key.currency1, 1);
}
function test_sync_locked_collectProtocolFees_unlocked_revertsWithProtocolFeeCurrencySynced() public noIsolate {
manager.setProtocolFeeController(address(actionsRouter));
manager.sync(key.currency1);
assertEq(Currency.unwrap(key.currency1), Currency.unwrap(manager.getSyncedCurrency()));
Actions[] memory actions = new Actions[](1);
bytes[] memory params = new bytes[](1);
actions[0] = Actions.COLLECT_PROTOCOL_FEES;
params[0] = abi.encode(address(this), key.currency1, 1);
vm.expectRevert(IProtocolFees.ProtocolFeeCurrencySynced.selector);
actionsRouter.executeActions(actions, params);
}
function test_collectProtocolFees_unlocked_revertsWithProtocolFeeCurrencySynced() public {
manager.setProtocolFeeController(address(actionsRouter));
Actions[] memory actions = new Actions[](2);
bytes[] memory params = new bytes[](2);
actions[0] = Actions.SYNC;
params[0] = abi.encode(key.currency1);
actions[1] = Actions.COLLECT_PROTOCOL_FEES;
params[1] = abi.encode(address(this), key.currency1, 1);
vm.expectRevert(IProtocolFees.ProtocolFeeCurrencySynced.selector);
actionsRouter.executeActions(actions, params);
}
function test_collectProtocolFees_ERC20_accumulateFees_gas() public {
uint256 expectedFees = 10;
(,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, 0);
vm.prank(feeController);
manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS);
(,, slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS);
swapRouter.swap(
key, SwapParams(true, -10000, SQRT_PRICE_1_2), PoolSwapTest.TestSettings(false, false), ZERO_BYTES
);
assertEq(manager.protocolFeesAccrued(currency0), expectedFees);
assertEq(manager.protocolFeesAccrued(currency1), 0);
assertEq(currency0.balanceOf(recipientAddress), 0);
vm.prank(feeController);
manager.collectProtocolFees(recipientAddress, currency0, expectedFees);
vm.snapshotGasLastCall("erc20 collect protocol fees");
assertEq(currency0.balanceOf(recipientAddress), expectedFees);
assertEq(manager.protocolFeesAccrued(currency0), 0);
}
function test_collectProtocolFees_ERC20_accumulateFees_exactOutput() public {
uint256 expectedFees = 10;
(,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, 0);
vm.prank(feeController);
manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS);
(,, slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS);
swapRouter.swap(
key, SwapParams(true, 10000, SQRT_PRICE_1_2), PoolSwapTest.TestSettings(false, false), ZERO_BYTES
);
assertEq(manager.protocolFeesAccrued(currency0), expectedFees);
assertEq(manager.protocolFeesAccrued(currency1), 0);
assertEq(currency0.balanceOf(recipientAddress), 0);
vm.prank(feeController);
manager.collectProtocolFees(recipientAddress, currency0, expectedFees);
assertEq(currency0.balanceOf(recipientAddress), expectedFees);
assertEq(manager.protocolFeesAccrued(currency0), 0);
}
function test_collectProtocolFees_ERC20_returnsAllFeesIf0IsProvidedAsParameter() public {
uint256 expectedFees = 10;
(,, uint24 slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, 0);
vm.prank(feeController);
manager.setProtocolFee(key, MAX_PROTOCOL_FEE_BOTH_TOKENS);
(,, slot0ProtocolFee,) = manager.getSlot0(key.toId());
assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS);
swapRouter.swap(
key,
SwapParams(false, -10000, TickMath.MAX_SQRT_PRICE - 1),
PoolSwapTest.TestSettings(false, false),
ZERO_BYTES
);
assertEq(manager.protocolFeesAccrued(currency0), 0);
assertEq(manager.protocolFeesAccrued(currency1), expectedFees);
assertEq(currency1.balanceOf(recipientAddress), 0);
vm.prank(feeController);
manager.collectProtocolFees(recipientAddress, currency1, 0);
assertEq(currency1.balanceOf(recipientAddress), expectedFees);
assertEq(manager.protocolFeesAccrued(currency1), 0);
}
function test_collectProtocolFees_nativeToken_accumulateFees_gas() public {
uint256 expectedFees = 10;
Currency nativeCurrency = CurrencyLibrary.ADDRESS_ZERO;
(,, uint24 slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId());
assertEq(slot0ProtocolFee, 0);
vm.prank(feeController);
manager.setProtocolFee(nativeKey, MAX_PROTOCOL_FEE_BOTH_TOKENS);
(,, slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId());
assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS);
swapRouter.swap{value: 10000}(
nativeKey, SwapParams(true, -10000, SQRT_PRICE_1_2), PoolSwapTest.TestSettings(false, false), ZERO_BYTES
);
assertEq(manager.protocolFeesAccrued(nativeCurrency), expectedFees);
assertEq(manager.protocolFeesAccrued(currency1), 0);
assertEq(nativeCurrency.balanceOf(recipientAddress), 0);
vm.prank(feeController);
manager.collectProtocolFees(recipientAddress, nativeCurrency, expectedFees);
vm.snapshotGasLastCall("native collect protocol fees");
assertEq(nativeCurrency.balanceOf(recipientAddress), expectedFees);
assertEq(manager.protocolFeesAccrued(nativeCurrency), 0);
}
function test_collectProtocolFees_nativeToken_returnsAllFeesIf0IsProvidedAsParameter() public {
uint256 expectedFees = 10;
Currency nativeCurrency = CurrencyLibrary.ADDRESS_ZERO;
(,, uint24 slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId());
assertEq(slot0ProtocolFee, 0);
vm.prank(feeController);
manager.setProtocolFee(nativeKey, MAX_PROTOCOL_FEE_BOTH_TOKENS);
(,, slot0ProtocolFee,) = manager.getSlot0(nativeKey.toId());
assertEq(slot0ProtocolFee, MAX_PROTOCOL_FEE_BOTH_TOKENS);
swapRouter.swap{value: 10000}(
nativeKey, SwapParams(true, -10000, SQRT_PRICE_1_2), PoolSwapTest.TestSettings(false, false), ZERO_BYTES
);
assertEq(manager.protocolFeesAccrued(nativeCurrency), expectedFees);
assertEq(manager.protocolFeesAccrued(currency1), 0);
assertEq(nativeCurrency.balanceOf(recipientAddress), 0);
vm.prank(feeController);
manager.collectProtocolFees(recipientAddress, nativeCurrency, 0);
assertEq(nativeCurrency.balanceOf(recipientAddress), expectedFees);
assertEq(manager.protocolFeesAccrued(nativeCurrency), 0);
}
function test_unlock_EmitsCorrectId() public {
vm.expectEmit(false, false, false, true);
emit UnlockCallback();
emptyUnlockRouter.unlock();
}
Action[] _actions;
function test_unlock_cannotBeCalledTwiceByCaller() public {
_actions = [Action.NESTED_SELF_UNLOCK];
nestedActionRouter.unlock(abi.encode(_actions));
}
function test_unlock_cannotBeCalledTwiceByDifferentCallers() public {
_actions = [Action.NESTED_EXECUTOR_UNLOCK];
nestedActionRouter.unlock(abi.encode(_actions));
}
// function testExtsloadForPoolPrice() public {
// IPoolManager.key = IPoolManager.PoolKey({
// currency0: currency0,
// currency1: currency1,
// fee: 100,
// hooks: IHooks(address(0)),
// tickSpacing: 10
// });
// manager.initialize(key, SQRT_PRICE_1_1);
// PoolId poolId = key.toId();
// bytes32 slot0Bytes = manager.extsload(keccak256(abi.encode(poolId, POOL_SLOT)));
// vm.snapshotGasLastCall("poolExtsloadSlot0");
// uint160 sqrtPriceX96Extsload;
// assembly {
// sqrtPriceX96Extsload := and(slot0Bytes, sub(shl(160, 1), 1))
// }
// (uint160 sqrtPriceX96Slot0,,,,,) = manager.getSlot0(poolId);
// // assert that extsload loads the correct storage slot which matches the true slot0
// assertEq(sqrtPriceX96Extsload, sqrtPriceX96Slot0);
// }
// function testExtsloadMultipleSlots() public {
// IPoolManager.key = IPoolManager.PoolKey({
// currency0: currency0,
// currency1: currency1,
// fee: 100,
// hooks: IHooks(address(0)),
// tickSpacing: 10
// });
// manager.initialize(key, SQRT_PRICE_1_1);
// // populate feeGrowthGlobalX128 struct w/ modify + swap
// modifyLiquidityRouter.modifyLiquidity(key, ModifyLiquidityParams(-120, 120, 5 ether, 0));
// swapRouter.swap(
// key,
// SwapParams(false, 1 ether, TickMath.MAX_SQRT_PRICE - 1),
// PoolSwapTest.TestSettings(true, true)
// );
// swapRouter.swap(
// key,
// SwapParams(true, 5 ether, TickMath.MIN_SQRT_PRICE + 1),
// PoolSwapTest.TestSettings(true, true)
// );
// PoolId poolId = key.toId();
// bytes memory value = manager.extsload(bytes32(uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + 1), 2);
// vm.snapshotGasLastCall("poolExtsloadTickInfoStruct");
// uint256 feeGrowthGlobal0X128Extsload;
// uint256 feeGrowthGlobal1X128Extsload;
// assembly {
// feeGrowthGlobal0X128Extsload := and(mload(add(value, 0x20)), sub(shl(256, 1), 1))
// feeGrowthGlobal1X128Extsload := and(mload(add(value, 0x40)), sub(shl(256, 1), 1))
// }
// assertEq(feeGrowthGlobal0X128Extsload, 408361710565269213475534193967158);
// assertEq(feeGrowthGlobal1X128Extsload, 204793365386061595215803889394593);
// }
function test_getPosition() public view {
(uint128 liquidity,,) = manager.getPositionInfo(key.toId(), address(modifyLiquidityRouter), -120, 120, 0);
assert(LIQUIDITY_PARAMS.liquidityDelta > 0);
assertEq(liquidity, uint128(uint256(LIQUIDITY_PARAMS.liquidityDelta)));
}
function supportsInterface(bytes4) external pure returns (bool) {
return true;
}
}