mirror of
https://github.com/lux-amm/v4-core.git
synced 2026-10-30 20:54:53 +00:00
383 lines
19 KiB
Solidity
383 lines
19 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {Deployers} from "./utils/Deployers.sol";
|
|
import {FeeTakingHook} from "../src/test/FeeTakingHook.sol";
|
|
import {LPFeeTakingHook} from "../src/test/LPFeeTakingHook.sol";
|
|
import {CustomCurveHook} from "../src/test/CustomCurveHook.sol";
|
|
import {DeltaReturningHook} from "../src/test/DeltaReturningHook.sol";
|
|
import {IHooks} from "../src/interfaces/IHooks.sol";
|
|
import {Hooks} from "../src/libraries/Hooks.sol";
|
|
import {PoolSwapTest} from "../src/test/PoolSwapTest.sol";
|
|
import {PoolId} from "../src/types/PoolId.sol";
|
|
import {SwapParams} from "../src/types/PoolOperation.sol";
|
|
import {IPoolManager} from "../src/interfaces/IPoolManager.sol";
|
|
import {Currency} from "../src/types/Currency.sol";
|
|
import {BalanceDelta} from "../src/types/BalanceDelta.sol";
|
|
import {SafeCast} from "../src/libraries/SafeCast.sol";
|
|
|
|
contract CustomAccountingTest is Test, Deployers {
|
|
using SafeCast for *;
|
|
|
|
address hook;
|
|
|
|
function setUp() public {
|
|
initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0)));
|
|
}
|
|
|
|
function _setUpDeltaReturnFuzzPool() internal {
|
|
address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG));
|
|
address impl = address(new DeltaReturningHook(manager));
|
|
_etchHookAndInitPool(hookAddr, impl);
|
|
}
|
|
|
|
function _setUpCustomCurvePool() internal {
|
|
address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG));
|
|
address impl = address(new CustomCurveHook(manager));
|
|
_etchHookAndInitPool(hookAddr, impl);
|
|
}
|
|
|
|
function _setUpFeeTakingPool() internal {
|
|
address hookAddr = address(
|
|
uint160(
|
|
Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG
|
|
| Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG
|
|
| Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG
|
|
)
|
|
);
|
|
address impl = address(new FeeTakingHook(manager));
|
|
_etchHookAndInitPool(hookAddr, impl);
|
|
}
|
|
|
|
function _setUpLPFeeTakingPool() internal {
|
|
address hookAddr = address(
|
|
uint160(
|
|
Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG
|
|
| Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG
|
|
)
|
|
);
|
|
address impl = address(new LPFeeTakingHook(manager));
|
|
_etchHookAndInitPool(hookAddr, impl);
|
|
}
|
|
|
|
function _etchHookAndInitPool(address hookAddr, address implAddr) internal {
|
|
vm.etch(hookAddr, implAddr.code);
|
|
hook = hookAddr;
|
|
|
|
(key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hookAddr), 100, SQRT_PRICE_1_1);
|
|
}
|
|
|
|
// ------------------------ SWAP ------------------------
|
|
|
|
function test_swap_afterSwapFeeOnUnspecified_exactInput() public {
|
|
_setUpFeeTakingPool();
|
|
uint256 balanceBefore0 = currency0.balanceOf(address(this));
|
|
uint256 balanceBefore1 = currency1.balanceOf(address(this));
|
|
|
|
uint256 amountToSwap = 1000;
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: -int256(amountToSwap), sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("swap CA fee on unspecified");
|
|
|
|
// input is 1000 for output of 998 with this much liquidity available
|
|
// plus a fee of 1.23% on unspecified (output) => (998*123)/10000 = 12
|
|
assertEq(currency0.balanceOf(address(this)), balanceBefore0 - amountToSwap, "amount 0");
|
|
assertEq(currency1.balanceOf(address(this)), balanceBefore1 + (998 - 12), "amount 1");
|
|
}
|
|
|
|
function test_swap_afterSwapFeeOnUnspecified_exactOutput() public {
|
|
_setUpFeeTakingPool();
|
|
|
|
uint256 balanceBefore0 = currency0.balanceOf(address(this));
|
|
uint256 balanceBefore1 = currency1.balanceOf(address(this));
|
|
|
|
uint256 amountToSwap = 1000;
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: int256(amountToSwap), sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
// input is 1002 for output of 1000 with this much liquidity available
|
|
// plus a fee of 1.23% on unspecified (input) => (1002*123)/10000 = 12
|
|
assertEq(currency0.balanceOf(address(this)), balanceBefore0 - 1002 - 12, "amount 0");
|
|
assertEq(currency1.balanceOf(address(this)), balanceBefore1 + amountToSwap, "amount 1");
|
|
}
|
|
|
|
function test_swap_beforeSwapNoOpsSwap_exactInput() public {
|
|
_setUpCustomCurvePool();
|
|
|
|
// add liquidity by sending tokens straight into the contract
|
|
key.currency0.transfer(hook, 10e18);
|
|
key.currency1.transfer(hook, 10e18);
|
|
|
|
uint256 balanceBefore0 = currency0.balanceOf(address(this));
|
|
uint256 balanceBefore1 = currency1.balanceOf(address(this));
|
|
|
|
uint256 amountToSwap = 123456;
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: -int256(amountToSwap), sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("swap CA custom curve + swap noop");
|
|
|
|
// the custom curve hook is 1-1 linear
|
|
assertEq(currency0.balanceOf(address(this)), balanceBefore0 - amountToSwap, "amount 0");
|
|
assertEq(currency1.balanceOf(address(this)), balanceBefore1 + amountToSwap, "amount 1");
|
|
}
|
|
|
|
function test_swap_beforeSwapNoOpsSwap_exactOutput() public {
|
|
_setUpCustomCurvePool();
|
|
|
|
// add liquidity by sending tokens straight into the contract
|
|
key.currency0.transfer(hook, 10e18);
|
|
key.currency1.transfer(hook, 10e18);
|
|
|
|
uint256 balanceBefore0 = currency0.balanceOf(address(this));
|
|
uint256 balanceBefore1 = currency1.balanceOf(address(this));
|
|
|
|
uint256 amountToSwap = 123456;
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
SwapParams memory params =
|
|
SwapParams({zeroForOne: true, amountSpecified: int256(amountToSwap), sqrtPriceLimitX96: SQRT_PRICE_1_2});
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
// the custom curve hook is 1-1 linear
|
|
assertEq(currency0.balanceOf(address(this)), balanceBefore0 - amountToSwap, "amount 0");
|
|
assertEq(currency1.balanceOf(address(this)), balanceBefore1 + amountToSwap, "amount 1");
|
|
}
|
|
|
|
// maximum available liquidity in each direction for the pool in test_fuzz_swap_beforeSwap_returnsDeltaSpecified
|
|
int128 maxPossibleIn_fuzz_test = -6018336102428409;
|
|
int128 maxPossibleOut_fuzz_test = 5981737760509662;
|
|
|
|
function test_fuzz_swap_beforeSwap_returnsDeltaSpecified(
|
|
int128 hookDeltaSpecified,
|
|
int256 amountSpecified,
|
|
bool zeroForOne
|
|
) public {
|
|
// ------------------------ SETUP ------------------------
|
|
Currency specifiedCurrency;
|
|
bool isExactIn;
|
|
_setUpDeltaReturnFuzzPool();
|
|
|
|
// initialize the pool and give the hook tokens to pay into swaps
|
|
key.currency0.transfer(hook, type(uint128).max);
|
|
key.currency1.transfer(hook, type(uint128).max);
|
|
|
|
// bound amount specified to be a fair amount less than the amount of liquidity we have
|
|
amountSpecified = int128(bound(amountSpecified, -3e11, 3e11));
|
|
isExactIn = amountSpecified < 0;
|
|
specifiedCurrency = (isExactIn == zeroForOne) ? key.currency0 : key.currency1;
|
|
|
|
// bound delta in specified to not take more than the reserves available, nor be the minimum int to
|
|
// stop the hook reverting on take/settle
|
|
uint128 reservesOfSpecified = uint128(specifiedCurrency.balanceOf(address(manager)));
|
|
hookDeltaSpecified = int128(bound(hookDeltaSpecified, type(int128).min + 1, int128(reservesOfSpecified)));
|
|
DeltaReturningHook(hook).setDeltaSpecified(hookDeltaSpecified);
|
|
|
|
// setup swap variables
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
SwapParams memory params = SwapParams({
|
|
zeroForOne: zeroForOne,
|
|
amountSpecified: amountSpecified,
|
|
sqrtPriceLimitX96: (zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT)
|
|
});
|
|
|
|
// ------------------------ FUZZING CASES ------------------------
|
|
// with an amount specified of 0: the trade reverts
|
|
if (amountSpecified == 0) {
|
|
vm.expectRevert(IPoolManager.SwapAmountCannotBeZero.selector);
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
// trade is exact input of n:, the hook cannot TAKE (+ve hookDeltaSpecified) more than n in input
|
|
// otherwise the user would have to send more than n in input
|
|
} else if (isExactIn && (hookDeltaSpecified > -amountSpecified)) {
|
|
vm.expectRevert(Hooks.HookDeltaExceedsSwapAmount.selector);
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
// exact output of n: the hook cannot GIVE (-ve hookDeltaSpecified) more than n in output
|
|
// otherwise the user would receive more than n in output
|
|
} else if (!isExactIn && (amountSpecified < -hookDeltaSpecified)) {
|
|
vm.expectRevert(Hooks.HookDeltaExceedsSwapAmount.selector);
|
|
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
|
|
// successful swaps !
|
|
} else {
|
|
uint256 balanceThisBefore = specifiedCurrency.balanceOf(address(this));
|
|
uint256 balanceHookBefore = specifiedCurrency.balanceOf(hook);
|
|
uint256 balanceManagerBefore = specifiedCurrency.balanceOf(address(manager));
|
|
|
|
BalanceDelta delta = swapRouter.swap(key, params, testSettings, ZERO_BYTES);
|
|
int128 deltaSpecified = (zeroForOne == isExactIn) ? delta.amount0() : delta.amount1();
|
|
|
|
// in all cases the hook gets what they took, and the user gets the swap's output delta (checked more below)
|
|
assertEq(
|
|
balanceHookBefore.toInt256() + hookDeltaSpecified,
|
|
specifiedCurrency.balanceOf(hook).toInt256(),
|
|
"hook balance change incorrect"
|
|
);
|
|
assertEq(
|
|
balanceThisBefore.toInt256() + deltaSpecified,
|
|
specifiedCurrency.balanceOf(address(this)).toInt256(),
|
|
"swapper balance change incorrect"
|
|
);
|
|
|
|
// exact input, where there arent enough input reserves available to pay swap and hook
|
|
// note: all 3 values are negative, so we use <
|
|
if (isExactIn && (hookDeltaSpecified + amountSpecified < maxPossibleIn_fuzz_test)) {
|
|
// the hook will have taken hookDeltaSpecified of the maxPossibleIn
|
|
assertEq(deltaSpecified, maxPossibleIn_fuzz_test - hookDeltaSpecified, "deltaSpecified exact input");
|
|
// the manager received all possible input tokens
|
|
assertEq(
|
|
balanceManagerBefore.toInt256() - maxPossibleIn_fuzz_test,
|
|
specifiedCurrency.balanceOf(address(manager)).toInt256(),
|
|
"manager balance change exact input"
|
|
);
|
|
|
|
// exact output, where there isn't enough output reserves available to pay swap and hook
|
|
} else if (!isExactIn && (hookDeltaSpecified + amountSpecified > maxPossibleOut_fuzz_test)) {
|
|
// the hook will have taken hookDeltaSpecified of the maxPossibleOut
|
|
assertEq(deltaSpecified, maxPossibleOut_fuzz_test - hookDeltaSpecified, "deltaSpecified exact output");
|
|
// the manager sent out all possible output tokens
|
|
assertEq(
|
|
balanceManagerBefore.toInt256() - maxPossibleOut_fuzz_test,
|
|
specifiedCurrency.balanceOf(address(manager)).toInt256(),
|
|
"manager balance change exact output"
|
|
);
|
|
|
|
// enough reserves were available, so the user got what they desired
|
|
} else {
|
|
assertEq(deltaSpecified, amountSpecified, "deltaSpecified not amountSpecified");
|
|
assertEq(
|
|
balanceManagerBefore.toInt256() - amountSpecified - hookDeltaSpecified,
|
|
specifiedCurrency.balanceOf(address(manager)).toInt256(),
|
|
"manager balance change not"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------ MODIFY LIQUIDITY ------------------------
|
|
|
|
function test_addLiquidity_withFeeTakingHook() public {
|
|
_setUpFeeTakingPool();
|
|
|
|
uint256 balanceBefore0 = currency0.balanceOf(address(this));
|
|
uint256 balanceBefore1 = currency1.balanceOf(address(this));
|
|
uint256 hookBalanceBefore0 = currency0.balanceOf(hook);
|
|
uint256 hookBalanceBefore1 = currency1.balanceOf(hook);
|
|
uint256 managerBalanceBefore0 = currency0.balanceOf(address(manager));
|
|
uint256 managerBalanceBefore1 = currency1.balanceOf(address(manager));
|
|
// console2.log(address(key.hooks));
|
|
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("addLiquidity CA fee");
|
|
|
|
uint256 hookGain0 = currency0.balanceOf(hook) - hookBalanceBefore0;
|
|
uint256 hookGain1 = currency1.balanceOf(hook) - hookBalanceBefore1;
|
|
uint256 thisLoss0 = balanceBefore0 - currency0.balanceOf(address(this));
|
|
uint256 thisLoss1 = balanceBefore1 - currency1.balanceOf(address(this));
|
|
uint256 managerGain0 = currency0.balanceOf(address(manager)) - managerBalanceBefore0;
|
|
uint256 managerGain1 = currency1.balanceOf(address(manager)) - managerBalanceBefore1;
|
|
|
|
// Assert that the hook got 5.43% of the added liquidity
|
|
assertEq(hookGain0, managerGain0 * 543 / 10000, "hook amount 0");
|
|
assertEq(hookGain1, managerGain1 * 543 / 10000, "hook amount 1");
|
|
assertEq(thisLoss0 - hookGain0, managerGain0, "manager amount 0");
|
|
assertEq(thisLoss1 - hookGain1, managerGain1, "manager amount 1");
|
|
}
|
|
|
|
function test_removeLiquidity_withFeeTakingHook() public {
|
|
_setUpFeeTakingPool();
|
|
|
|
uint256 balanceBefore0 = currency0.balanceOf(address(this));
|
|
uint256 balanceBefore1 = currency1.balanceOf(address(this));
|
|
uint256 hookBalanceBefore0 = currency0.balanceOf(hook);
|
|
uint256 hookBalanceBefore1 = currency1.balanceOf(hook);
|
|
uint256 managerBalanceBefore0 = currency0.balanceOf(address(manager));
|
|
uint256 managerBalanceBefore1 = currency1.balanceOf(address(manager));
|
|
|
|
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
|
|
vm.snapshotGasLastCall("removeLiquidity CA fee");
|
|
|
|
uint256 hookGain0 = currency0.balanceOf(hook) - hookBalanceBefore0;
|
|
uint256 hookGain1 = currency1.balanceOf(hook) - hookBalanceBefore1;
|
|
uint256 thisGain0 = currency0.balanceOf(address(this)) - balanceBefore0;
|
|
uint256 thisGain1 = currency1.balanceOf(address(this)) - balanceBefore1;
|
|
uint256 managerLoss0 = managerBalanceBefore0 - currency0.balanceOf(address(manager));
|
|
uint256 managerLoss1 = managerBalanceBefore1 - currency1.balanceOf(address(manager));
|
|
|
|
// Assert that the hook got 5.43% of the withdrawn liquidity
|
|
assertEq(hookGain0, managerLoss0 * 543 / 10000, "hook amount 0");
|
|
assertEq(hookGain1, managerLoss1 * 543 / 10000, "hook amount 1");
|
|
assertEq(thisGain0 + hookGain0, managerLoss0, "manager amount 0");
|
|
assertEq(thisGain1 + hookGain1, managerLoss1, "manager amount 1");
|
|
}
|
|
|
|
function test_fuzz_addLiquidity_withLPFeeTakingHook(uint128 feeRevenue0, uint128 feeRevenue1) public {
|
|
feeRevenue0 = uint128(bound(feeRevenue0, 0, type(uint128).max / 2));
|
|
feeRevenue1 = uint128(bound(feeRevenue1, 0, type(uint128).max / 2));
|
|
_setUpLPFeeTakingPool(); // creates liquidity as part of setup
|
|
|
|
// donate to generate fee revenue
|
|
donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES);
|
|
|
|
uint256 hookBalanceBefore0 = currency0.balanceOf(hook);
|
|
uint256 hookBalanceBefore1 = currency1.balanceOf(hook);
|
|
|
|
// add liquidity again to trigger the hook, which should take the fee revenue
|
|
modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES);
|
|
|
|
uint256 hookGain0 = currency0.balanceOf(hook) - hookBalanceBefore0;
|
|
uint256 hookGain1 = currency1.balanceOf(hook) - hookBalanceBefore1;
|
|
|
|
// Assert that the hook took ALL of the fee revenue, minus 1 wei of imprecision
|
|
assertApproxEqAbs(hookGain0, feeRevenue0, 1 wei);
|
|
assertApproxEqAbs(hookGain1, feeRevenue1, 1 wei);
|
|
assertTrue(hookGain0 <= feeRevenue0);
|
|
assertTrue(hookGain1 <= feeRevenue1);
|
|
}
|
|
|
|
function test_fuzz_removeLiquidity_withLPFeeTakingHook(uint128 feeRevenue0, uint128 feeRevenue1) public {
|
|
// test fails when fee revenue approaches int128.max because PoolManager is limited by (principal + fees)
|
|
feeRevenue0 = uint128(bound(feeRevenue0, 0, type(uint128).max / 3));
|
|
feeRevenue1 = uint128(bound(feeRevenue1, 0, type(uint128).max / 3));
|
|
_setUpLPFeeTakingPool(); // creates liquidity as part of setup
|
|
|
|
// donate to generate fee revenue
|
|
donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES);
|
|
|
|
uint256 balanceBefore0 = currency0.balanceOf(address(this));
|
|
uint256 balanceBefore1 = currency1.balanceOf(address(this));
|
|
uint256 hookBalanceBefore0 = currency0.balanceOf(hook);
|
|
uint256 hookBalanceBefore1 = currency1.balanceOf(hook);
|
|
uint256 managerBalanceBefore0 = currency0.balanceOf(address(manager));
|
|
uint256 managerBalanceBefore1 = currency1.balanceOf(address(manager));
|
|
|
|
// remove liquidity to trigger the hook, which should take the fee revenue
|
|
modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES);
|
|
|
|
uint256 hookGain0 = currency0.balanceOf(hook) - hookBalanceBefore0;
|
|
uint256 hookGain1 = currency1.balanceOf(hook) - hookBalanceBefore1;
|
|
uint256 thisGain0 = currency0.balanceOf(address(this)) - balanceBefore0;
|
|
uint256 thisGain1 = currency1.balanceOf(address(this)) - balanceBefore1;
|
|
uint256 managerLoss0 = managerBalanceBefore0 - currency0.balanceOf(address(manager));
|
|
uint256 managerLoss1 = managerBalanceBefore1 - currency1.balanceOf(address(manager));
|
|
|
|
// Assert that the hook took ALL of the fee revenue, minus 1 wei of imprecision
|
|
assertApproxEqAbs(hookGain0, feeRevenue0, 1 wei);
|
|
assertApproxEqAbs(hookGain1, feeRevenue1, 1 wei);
|
|
assertTrue(hookGain0 <= feeRevenue0);
|
|
assertTrue(hookGain1 <= feeRevenue1);
|
|
assertEq(thisGain0 + hookGain0, managerLoss0, "manager amount 0");
|
|
assertEq(thisGain1 + hookGain1, managerLoss1, "manager amount 1");
|
|
}
|
|
}
|