test(pool): add concentrated-liquidity pool tests
This commit is contained in:
1 parent
da99c14cdd
commit
ec7e6b4b46
1 file changed
+263
@@ -0,0 +1,263 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user