264 lines
9.9 KiB
Solidity
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;
|
|
}
|
|
}
|