// SPDX-License-Identifier: MIT pragma solidity 0.8.36; import {Test} from "forge-std/Test.sol"; import {FluxClFactory} from "../src/FluxClFactory.sol"; import {FluxClPool} from "../src/FluxClPool.sol"; import {TestErc20} from "../src/TestErc20.sol"; import {IErc20} from "../src/interfaces/IErc20.sol"; import {IFluxMintCallback} from "../src/interfaces/IFluxMintCallback.sol"; import {IFluxSwapCallback} from "../src/interfaces/IFluxSwapCallback.sol"; import {TickMath} from "../src/libraries/TickMath.sol"; contract PoolUser is IFluxMintCallback, IFluxSwapCallback { function fluxMintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external { (address token0, address token1) = abi.decode(data, (address, address)); if (amount0Owed > 0) IErc20(token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IErc20(token1).transfer(msg.sender, amount1Owed); } function fluxSwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { (address token0, address token1) = abi.decode(data, (address, address)); if (amount0Delta > 0) IErc20(token0).transfer(msg.sender, uint256(amount0Delta)); if (amount1Delta > 0) IErc20(token1).transfer(msg.sender, uint256(amount1Delta)); } function mint( FluxClPool pool, address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256, uint256) { return pool.mint(recipient, tickLower, tickUpper, amount, data); } function swap( FluxClPool pool, address recipient, bool zeroForOne, int256 amountSpecified, uint160 limit, bytes calldata data ) external returns (int256, int256) { return pool.swap(recipient, zeroForOne, amountSpecified, limit, data); } function burn(FluxClPool pool, int24 tickLower, int24 tickUpper, uint128 amount) external returns (uint256, uint256) { return pool.burn(tickLower, tickUpper, amount); } function collect(FluxClPool pool, address recipient, int24 tickLower, int24 tickUpper, uint128 a0, uint128 a1) external returns (uint128, uint128) { return pool.collect(recipient, tickLower, tickUpper, a0, a1); } } contract FluxClPoolTest is Test { uint160 private constant priceOne = 79228162514264337593543950336; FluxClFactory private factory; FluxClPool private pool; TestErc20 private token0; TestErc20 private token1; PoolUser private user; bytes private callbackData; int24 private constant spacing = 60; function setUp() public { factory = new FluxClFactory(); TestErc20 tokenA = new TestErc20("Alpha", "ALPHA", 0); TestErc20 tokenB = new TestErc20("Beta", "BETA", 0); (address a, address b) = address(tokenA) < address(tokenB) ? (address(tokenA), address(tokenB)) : (address(tokenB), address(tokenA)); token0 = TestErc20(a); token1 = TestErc20(b); pool = FluxClPool(factory.createPool(a, b, 3000)); pool.initialize(priceOne); user = new PoolUser(); token0.mint(address(user), 1e30); token1.mint(address(user), 1e30); callbackData = abi.encode(a, b); } function mintRange(int24 tickLower, int24 tickUpper, uint128 amount) private returns (uint256, uint256) { return user.mint(pool, address(user), tickLower, tickUpper, amount, callbackData); } function testFactoryDeterministicPool() public view { assertEq(factory.getPool(address(token0), address(token1), 3000), address(pool)); assertEq(factory.getPool(address(token1), address(token0), 3000), address(pool)); assertEq(pool.tickSpacing(), spacing); assertEq(pool.fee(), 3000); assertEq(factory.allPoolsLength(), 1); } function testCreatePoolRevertsOnUnknownFee() public { vm.expectRevert(bytes("Flux: FEE_TIER")); factory.createPool(address(token0), address(token1), 1234); } function testCreatePoolRevertsOnDuplicate() public { vm.expectRevert(bytes("Flux: POOL_EXISTS")); factory.createPool(address(token0), address(token1), 3000); } function testInitializeSetsTickZero() public view { (uint160 sqrtP, int24 tick,,,,) = pool.slot0(); assertEq(sqrtP, priceOne); assertEq(tick, 0); } function testInitializeRevertsTwice() public { vm.expectRevert(bytes("Flux: INIT")); pool.initialize(priceOne); } function testMintBalancedAtPriceOne() public { (uint256 amount0, uint256 amount1) = mintRange(-600, 600, 1e18); assertGt(amount0, 0); assertGt(amount1, 0); assertApproxEqRel(amount0, amount1, 1e15); assertEq(pool.liquidity(), 1e18); assertEq(token0.balanceOf(address(pool)), amount0); assertEq(token1.balanceOf(address(pool)), amount1); } function testMintRevertsOnBadTickOrder() public { vm.expectRevert(bytes("Flux: TLU")); mintRange(600, -600, 1e18); } function testMintRevertsOnUnalignedTick() public { vm.expectRevert(bytes("Flux: TLS")); mintRange(-599, 600, 1e18); } function testSwapZeroForOneMovesPriceDown() public { mintRange(-6000, 6000, 1e20); (uint160 sqrtBefore, int24 tickBefore,,,,) = pool.slot0(); uint256 out1Before = token1.balanceOf(address(this)); user.swap(pool, address(this), true, 1e17, TickMath.minSqrtRatio + 1, callbackData); uint256 received = token1.balanceOf(address(this)) - out1Before; (uint160 sqrtAfter, int24 tickAfter,,,,) = pool.slot0(); assertLt(sqrtAfter, sqrtBefore); assertLe(tickAfter, tickBefore); assertGt(received, 0); } function testSwapOneForZeroMovesPriceUp() public { mintRange(-6000, 6000, 1e20); (uint160 sqrtBefore,,,,,) = pool.slot0(); uint256 out0Before = token0.balanceOf(address(this)); user.swap(pool, address(this), false, 1e17, TickMath.maxSqrtRatio - 1, callbackData); uint256 received = token0.balanceOf(address(this)) - out0Before; (uint160 sqrtAfter,,,,,) = pool.slot0(); assertGt(sqrtAfter, sqrtBefore); assertGt(received, 0); } function testSwapCrossesTickAndDropsLiquidity() public { mintRange(-6000, 6000, 1e20); mintRange(-120, 120, 5e20); assertEq(pool.liquidity(), 6e20); user.swap(pool, address(this), true, 1e19, TickMath.minSqrtRatio + 1, callbackData); (, int24 tickAfter,,,,) = pool.slot0(); assertLt(tickAfter, -120); assertGt(tickAfter, -6000); assertEq(pool.liquidity(), 1e20); } function testFeesAccrueToInRangePosition() public { mintRange(-6000, 6000, 1e20); user.swap(pool, address(this), true, 1e18, TickMath.minSqrtRatio + 1, callbackData); user.swap(pool, address(this), false, 1e18, TickMath.maxSqrtRatio - 1, callbackData); user.burn(pool, -6000, 6000, 0); (, uint256 fg0,, uint128 owed0, uint128 owed1) = pool.positions(keccak256(abi.encodePacked(address(user), int24(-6000), int24(6000)))); fg0; assertGt(owed0, 0); assertGt(owed1, 0); } function testBurnCollectReturnsPrincipalPlusFees() public { (uint256 minted0, uint256 minted1) = mintRange(-6000, 6000, 1e20); user.swap(pool, address(this), true, 1e18, TickMath.minSqrtRatio + 1, callbackData); user.swap(pool, address(this), false, 1e18, TickMath.maxSqrtRatio - 1, callbackData); user.burn(pool, -6000, 6000, 1e20); (uint128 collected0, uint128 collected1) = user.collect(pool, address(user), -6000, 6000, type(uint128).max, type(uint128).max); assertGe(uint256(collected0) + collected1, minted0 + minted1); } function testSwapExactOutputDeliversRequestedAmount() public { mintRange(-6000, 6000, 1e20); uint256 before = token1.balanceOf(address(this)); user.swap(pool, address(this), true, -1e16, TickMath.minSqrtRatio + 1, callbackData); uint256 received = token1.balanceOf(address(this)) - before; assertEq(received, 1e16); } function testFuzzSwapExactInputNeverExceedsInput(uint128 amountIn, bool zeroForOne) public { mintRange(-60000, 60000, 1e22); amountIn = uint128(bound(amountIn, 1e6, 1e24)); uint160 limit = zeroForOne ? TickMath.minSqrtRatio + 1 : TickMath.maxSqrtRatio - 1; (int256 amount0, int256 amount1) = user.swap(pool, address(this), zeroForOne, int256(uint256(amountIn)), limit, callbackData); if (zeroForOne) { assertLe(uint256(amount0), amountIn); assertLe(amount1, 0); } else { assertLe(uint256(amount1), amountIn); assertLe(amount0, 0); } (uint160 sqrtP,,,,,) = pool.slot0(); assertGe(sqrtP, TickMath.minSqrtRatio); assertLt(sqrtP, TickMath.maxSqrtRatio); } function testFuzzMintThenFullBurnConservesTokens(uint128 amount, int24 lowerSeed, int24 widthSeed) public { amount = uint128(bound(amount, 1e12, 1e24)); int24 lower = int24(bound(lowerSeed, -600, 0)) / spacing * spacing; int24 width = int24(bound(widthSeed, 1, 10)) * spacing; int24 upper = lower + width; uint256 balUser0Before = token0.balanceOf(address(user)); uint256 balUser1Before = token1.balanceOf(address(user)); (uint256 amount0, uint256 amount1) = mintRange(lower, upper, amount); user.burn(pool, lower, upper, amount); user.collect(pool, address(user), lower, upper, type(uint128).max, type(uint128).max); assertLe(token0.balanceOf(address(user)), balUser0Before); assertLe(token1.balanceOf(address(user)), balUser1Before); assertLe(balUser0Before - token0.balanceOf(address(user)), 2); assertLe(balUser1Before - token1.balanceOf(address(user)), 2); amount0; amount1; } }