mirror of
https://github.com/lux-amm/v4-core.git
synced 2026-10-30 20:54:53 +00:00
344 lines
13 KiB
Solidity
344 lines
13 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {Vm} from "forge-std/Vm.sol";
|
|
import {PoolId} from "../src/types/PoolId.sol";
|
|
import {Hooks} from "../src/libraries/Hooks.sol";
|
|
import {LPFeeLibrary} from "../src/libraries/LPFeeLibrary.sol";
|
|
import {IPoolManager} from "../src/interfaces/IPoolManager.sol";
|
|
import {IProtocolFees} from "../src/interfaces/IProtocolFees.sol";
|
|
import {IHooks} from "../src/interfaces/IHooks.sol";
|
|
import {PoolKey} from "../src/types/PoolKey.sol";
|
|
import {SwapParams} from "../src/types/PoolOperation.sol";
|
|
import {PoolManager} from "../src/PoolManager.sol";
|
|
import {PoolSwapTest} from "../src/test/PoolSwapTest.sol";
|
|
import {Deployers} from "./utils/Deployers.sol";
|
|
import {DynamicFeesTestHook} from "../src/test/DynamicFeesTestHook.sol";
|
|
import {Currency} from "../src/types/Currency.sol";
|
|
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
|
|
import {Pool} from "../src/libraries/Pool.sol";
|
|
import {BalanceDelta, BalanceDeltaLibrary} from "../src/types/BalanceDelta.sol";
|
|
import {StateLibrary} from "../src/libraries/StateLibrary.sol";
|
|
import {CustomRevert} from "../src/libraries/CustomRevert.sol";
|
|
import {ProtocolFeeLibrary} from "../src/libraries/ProtocolFeeLibrary.sol";
|
|
|
|
contract TestDynamicFees is Test, Deployers {
|
|
using StateLibrary for IPoolManager;
|
|
using ProtocolFeeLibrary for uint16;
|
|
|
|
DynamicFeesTestHook dynamicFeesHooks = DynamicFeesTestHook(
|
|
address(
|
|
uint160(
|
|
uint256(type(uint160).max) & clearAllHookPermissionsMask | Hooks.BEFORE_SWAP_FLAG
|
|
| Hooks.AFTER_INITIALIZE_FLAG
|
|
)
|
|
)
|
|
);
|
|
|
|
DynamicFeesTestHook dynamicFeesNoHooks =
|
|
DynamicFeesTestHook(address(uint160(uint256(type(uint160).max) & clearAllHookPermissionsMask)));
|
|
|
|
event Swap(
|
|
PoolId indexed poolId,
|
|
address indexed sender,
|
|
int128 amount0,
|
|
int128 amount1,
|
|
uint160 sqrtPriceX96,
|
|
uint128 liquidity,
|
|
int24 tick,
|
|
uint24 fee
|
|
);
|
|
|
|
function setUp() public {
|
|
DynamicFeesTestHook impl = new DynamicFeesTestHook();
|
|
vm.etch(address(dynamicFeesHooks), address(impl).code);
|
|
vm.etch(address(dynamicFeesNoHooks), address(impl).code);
|
|
|
|
deployFreshManagerAndRouters();
|
|
dynamicFeesHooks.setManager(IPoolManager(manager));
|
|
dynamicFeesNoHooks.setManager(IPoolManager(manager));
|
|
|
|
deployMintAndApprove2Currencies();
|
|
(key,) = initPoolAndAddLiquidity(
|
|
currency0, currency1, IHooks(address(dynamicFeesHooks)), LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1
|
|
);
|
|
}
|
|
|
|
function test_updateDynamicLPFee_afterInitialize_failsWithTooLargeFee() public {
|
|
key.tickSpacing = 30;
|
|
uint24 fee = 1000001;
|
|
dynamicFeesHooks.setFee(fee);
|
|
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
CustomRevert.WrappedError.selector,
|
|
address(dynamicFeesHooks),
|
|
IHooks.afterInitialize.selector,
|
|
abi.encodeWithSelector(LPFeeLibrary.LPFeeTooLarge.selector, fee),
|
|
abi.encodeWithSelector(Hooks.HookCallFailed.selector)
|
|
)
|
|
);
|
|
manager.initialize(key, SQRT_PRICE_1_1);
|
|
}
|
|
|
|
function test_initialize_initializesFeeTo0() public {
|
|
key.hooks = dynamicFeesNoHooks;
|
|
|
|
// this fee is not fetched as theres no afterInitialize hook
|
|
dynamicFeesNoHooks.setFee(1000000);
|
|
|
|
manager.initialize(key, SQRT_PRICE_1_1);
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
}
|
|
|
|
function test_updateDynamicLPFee_afterInitialize_initializesFee() public {
|
|
key.tickSpacing = 30;
|
|
dynamicFeesHooks.setFee(123);
|
|
|
|
manager.initialize(key, SQRT_PRICE_1_1);
|
|
assertEq(_fetchPoolLPFee(key), 123);
|
|
}
|
|
|
|
function test_updateDynamicLPFee_revertsIfCallerIsntHook() public {
|
|
vm.expectRevert(IPoolManager.UnauthorizedDynamicLPFeeUpdate.selector);
|
|
manager.updateDynamicLPFee(key, 123);
|
|
}
|
|
|
|
function test_updateDynamicLPFee_revertsIfPoolHasStaticFee() public {
|
|
key.fee = 3000; // static fee
|
|
dynamicFeesHooks.setFee(123);
|
|
|
|
// afterInitialize will try to update the fee, and fail
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
CustomRevert.WrappedError.selector,
|
|
address(dynamicFeesHooks),
|
|
IHooks.afterInitialize.selector,
|
|
abi.encodeWithSelector(IPoolManager.UnauthorizedDynamicLPFeeUpdate.selector),
|
|
abi.encodeWithSelector(Hooks.HookCallFailed.selector)
|
|
)
|
|
);
|
|
manager.initialize(key, SQRT_PRICE_1_1);
|
|
}
|
|
|
|
function test_updateDynamicLPFee_beforeSwap_failsWithTooLargeFee() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
uint24 fee = 1000001;
|
|
dynamicFeesHooks.setFee(1000001);
|
|
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
CustomRevert.WrappedError.selector,
|
|
address(dynamicFeesHooks),
|
|
IHooks.beforeSwap.selector,
|
|
abi.encodeWithSelector(LPFeeLibrary.LPFeeTooLarge.selector, fee),
|
|
abi.encodeWithSelector(Hooks.HookCallFailed.selector)
|
|
)
|
|
);
|
|
|
|
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
|
|
}
|
|
|
|
function test_updateDynamicLPFee_beforeSwap_succeeds_gas() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(123);
|
|
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectEmit(true, true, true, true, address(manager));
|
|
emit Swap(key.toId(), address(swapRouter), -100, 98, 79228162514264329749955861424, 1e18, -1, 123);
|
|
|
|
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("update dynamic fee in before swap");
|
|
|
|
assertEq(_fetchPoolLPFee(key), 123);
|
|
}
|
|
|
|
function test_swap_100PercentLPFee_AmountIn_NoProtocol() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(1000000);
|
|
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectEmit(true, true, true, true, address(manager));
|
|
emit Swap(key.toId(), address(swapRouter), -100, 0, SQRT_PRICE_1_1, 1e18, -1, 1000000);
|
|
|
|
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
|
|
|
|
assertEq(_fetchPoolLPFee(key), 1000000);
|
|
}
|
|
|
|
function test_swap_50PercentLPFee_AmountIn_NoProtocol() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(500000);
|
|
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectEmit(true, true, true, true, address(manager));
|
|
emit Swap(key.toId(), address(swapRouter), -100, 49, 79228162514264333632135824623, 1e18, -1, 500000);
|
|
|
|
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
|
|
|
|
assertEq(_fetchPoolLPFee(key), 500000);
|
|
}
|
|
|
|
function test_swap_50PercentLPFee_AmountOut_NoProtocol() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(500000);
|
|
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectEmit(true, true, true, true, address(manager));
|
|
emit Swap(key.toId(), address(swapRouter), -202, 100, 79228162514264329670727698909, 1e18, -1, 500000);
|
|
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
assertEq(_fetchPoolLPFee(key), 500000);
|
|
}
|
|
|
|
function test_swap_revertsWith_InvalidFeeForExactOut_whenFeeIsMax() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(1000000);
|
|
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectRevert(Pool.InvalidFeeForExactOut.selector);
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
}
|
|
|
|
function test_swap_99PercentFee_AmountOut_WithProtocol() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(999999);
|
|
|
|
vm.prank(feeController);
|
|
manager.setProtocolFee(key, 1000);
|
|
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectRevert(Pool.InvalidFeeForExactOut.selector);
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
}
|
|
|
|
function test_swap_100PercentFee_AmountIn_WithProtocol() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(1000000);
|
|
|
|
vm.prank(feeController);
|
|
manager.setProtocolFee(key, 1000);
|
|
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: -1000, sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectEmit(true, true, true, true, address(manager));
|
|
emit Swap(key.toId(), address(swapRouter), -1000, 0, SQRT_PRICE_1_1, 1e18, -1, 1000000);
|
|
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
uint256 expectedProtocolFee = uint256(-params.amountSpecified) * 1000 / 1e6;
|
|
assertEq(manager.protocolFeesAccrued(currency0), expectedProtocolFee);
|
|
}
|
|
|
|
function test_emitsSwapFee() public {
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
dynamicFeesHooks.setFee(123);
|
|
|
|
vm.prank(feeController);
|
|
manager.setProtocolFee(key, 1000);
|
|
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectEmit(true, true, true, true, address(manager));
|
|
emit Swap(key.toId(), address(swapRouter), -100, 98, 79228162514264329749955861424, 1e18, -1, 1123);
|
|
|
|
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
|
|
|
|
assertEq(_fetchPoolLPFee(key), 123);
|
|
}
|
|
|
|
function test_fuzz_ProtocolAndLPFee(uint24 lpFee, uint16 protocolFee0, uint16 protocolFee1, int256 amountSpecified)
|
|
public
|
|
{
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
|
|
lpFee = uint16(bound(lpFee, 0, 1000000));
|
|
protocolFee0 = uint16(bound(protocolFee0, 0, 1000));
|
|
protocolFee1 = uint16(bound(protocolFee1, 0, 1000));
|
|
vm.assume(amountSpecified != 0);
|
|
|
|
uint24 protocolFee = (uint24(protocolFee1) << 12) | uint24(protocolFee0);
|
|
dynamicFeesHooks.setFee(lpFee);
|
|
|
|
vm.prank(feeController);
|
|
manager.setProtocolFee(key, protocolFee);
|
|
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: amountSpecified, sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
BalanceDelta delta = swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
uint24 swapFee = uint16(protocolFee).calculateSwapFee(lpFee);
|
|
|
|
uint256 expectedProtocolFee = uint256(uint128(-delta.amount0())) * protocolFee0 / 1e6;
|
|
if (lpFee == 0) {
|
|
assertEq(protocolFee0, swapFee);
|
|
if (((uint256(uint128(-delta.amount0())) * protocolFee0) % 1e6) != 0) expectedProtocolFee++;
|
|
}
|
|
|
|
assertEq(manager.protocolFeesAccrued(currency0), expectedProtocolFee);
|
|
}
|
|
|
|
function test_swap_withDynamicFee_gas() public {
|
|
(key,) = initPoolAndAddLiquidity(
|
|
currency0, currency1, dynamicFeesNoHooks, LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1
|
|
);
|
|
|
|
assertEq(_fetchPoolLPFee(key), 0);
|
|
dynamicFeesNoHooks.forcePoolFeeUpdate(key, 123);
|
|
assertEq(_fetchPoolLPFee(key), 123);
|
|
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
vm.expectEmit(true, true, true, true, address(manager));
|
|
emit Swap(key.toId(), address(swapRouter), -100, 98, 79228162514264329749955861424, 1e18, -1, 123);
|
|
|
|
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("swap with dynamic fee");
|
|
}
|
|
|
|
function _fetchPoolLPFee(PoolKey memory _key) internal view returns (uint256 lpFee) {
|
|
PoolId id = _key.toId();
|
|
(,,, lpFee) = manager.getSlot0(id);
|
|
}
|
|
}
|