Files
fluxswap/test/FluxClPool.t.sol

264 lines
9.9 KiB
Solidity

// 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;
}
}