diff --git a/test/FluxSwapRouter.t.sol b/test/FluxSwapRouter.t.sol new file mode 100644 index 0000000..74cd557 --- /dev/null +++ b/test/FluxSwapRouter.t.sol @@ -0,0 +1,198 @@ +// 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 {FluxSwapRouter} from "../src/FluxSwapRouter.sol"; +import {TestErc20} from "../src/TestErc20.sol"; +import {IErc20} from "../src/interfaces/IErc20.sol"; +import {IFluxMintCallback} from "../src/interfaces/IFluxMintCallback.sol"; + +contract FluxSwapRouterTest is Test, IFluxMintCallback { + uint160 private constant priceOne = 79228162514264337593543950336; + + FluxClFactory private factory; + FluxSwapRouter private router; + TestErc20 private tokenA; + TestErc20 private tokenB; + TestErc20 private tokenC; + + address private constant trader = address(0xF0); + + 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 seed(FluxClPool pool) private { + pool.mint(address(this), -60000, 60000, 1e23, abi.encode(pool.token0(), pool.token1())); + } + + function setUp() public { + factory = new FluxClFactory(); + router = new FluxSwapRouter(address(factory)); + + tokenA = new TestErc20("Aaa", "AAA", 1e30); + tokenB = new TestErc20("Bbb", "BBB", 1e30); + tokenC = new TestErc20("Ccc", "CCC", 1e30); + + FluxClPool poolAB = FluxClPool(factory.createPool(address(tokenA), address(tokenB), 3000)); + FluxClPool poolBC = FluxClPool(factory.createPool(address(tokenB), address(tokenC), 3000)); + poolAB.initialize(priceOne); + poolBC.initialize(priceOne); + seed(poolAB); + seed(poolBC); + + tokenA.mint(trader, 1e24); + tokenB.mint(trader, 1e24); + tokenC.mint(trader, 1e24); + vm.startPrank(trader); + tokenA.approve(address(router), type(uint256).max); + tokenB.approve(address(router), type(uint256).max); + tokenC.approve(address(router), type(uint256).max); + vm.stopPrank(); + } + + function testExactInputSingle() public { + vm.prank(trader); + uint256 out = router.exactInputSingle( + FluxSwapRouter.ExactInputSingleParams({ + tokenIn: address(tokenA), + tokenOut: address(tokenB), + fee: 3000, + recipient: trader, + deadline: block.timestamp + 1, + amountIn: 1e18, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ); + assertGt(out, 0); + assertGt(out, 99e16); + assertLt(out, 1e18); + assertEq(tokenB.balanceOf(trader), 1e24 + out); + } + + function testExactInputMultiHop() public { + bytes memory path = + abi.encodePacked(address(tokenA), uint24(3000), address(tokenB), uint24(3000), address(tokenC)); + uint256 cBefore = tokenC.balanceOf(trader); + vm.prank(trader); + uint256 out = router.exactInput( + FluxSwapRouter.ExactInputParams({ + path: path, recipient: trader, deadline: block.timestamp + 1, amountIn: 1e18, amountOutMinimum: 0 + }) + ); + assertGt(out, 0); + assertGt(out, 98e16); + assertLt(out, 1e18); + assertEq(tokenC.balanceOf(trader), cBefore + out); + } + + function testExactOutputSingle() public { + uint256 aBefore = tokenA.balanceOf(trader); + uint256 bBefore = tokenB.balanceOf(trader); + vm.prank(trader); + uint256 amountIn = router.exactOutputSingle( + FluxSwapRouter.ExactOutputSingleParams({ + tokenIn: address(tokenA), + tokenOut: address(tokenB), + fee: 3000, + recipient: trader, + deadline: block.timestamp + 1, + amountOut: 1e18, + amountInMaximum: 2e18, + sqrtPriceLimitX96: 0 + }) + ); + assertGt(amountIn, 1e18); + assertEq(tokenB.balanceOf(trader), bBefore + 1e18); + assertEq(tokenA.balanceOf(trader), aBefore - amountIn); + } + + function testExactOutputMultiHop() public { + bytes memory path = + abi.encodePacked(address(tokenC), uint24(3000), address(tokenB), uint24(3000), address(tokenA)); + uint256 cBefore = tokenC.balanceOf(trader); + vm.prank(trader); + uint256 amountIn = router.exactOutput( + FluxSwapRouter.ExactOutputParams({ + path: path, recipient: trader, deadline: block.timestamp + 1, amountOut: 1e18, amountInMaximum: 2e18 + }) + ); + assertGt(amountIn, 1e18); + assertEq(tokenC.balanceOf(trader), cBefore + 1e18); + } + + function testRevertsOnExpiredDeadline() public { + vm.warp(1000); + vm.prank(trader); + vm.expectRevert(bytes("FluxRouter: EXPIRED")); + router.exactInputSingle( + FluxSwapRouter.ExactInputSingleParams({ + tokenIn: address(tokenA), + tokenOut: address(tokenB), + fee: 3000, + recipient: trader, + deadline: 999, + amountIn: 1e18, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ); + } + + function testRevertsOnSlippageExactInput() public { + vm.prank(trader); + vm.expectRevert(bytes("FluxRouter: TOO_LITTLE_OUT")); + router.exactInputSingle( + FluxSwapRouter.ExactInputSingleParams({ + tokenIn: address(tokenA), + tokenOut: address(tokenB), + fee: 3000, + recipient: trader, + deadline: block.timestamp + 1, + amountIn: 1e18, + amountOutMinimum: 1e18, + sqrtPriceLimitX96: 0 + }) + ); + } + + function testRevertsOnSlippageExactOutput() public { + vm.prank(trader); + vm.expectRevert(bytes("FluxRouter: TOO_MUCH_IN")); + router.exactOutputSingle( + FluxSwapRouter.ExactOutputSingleParams({ + tokenIn: address(tokenA), + tokenOut: address(tokenB), + fee: 3000, + recipient: trader, + deadline: block.timestamp + 1, + amountOut: 1e18, + amountInMaximum: 1e18, + sqrtPriceLimitX96: 0 + }) + ); + } + + function testFuzzExactInputSingleMonotonic(uint128 amountIn) public { + amountIn = uint128(bound(amountIn, 1e9, 1e22)); + vm.prank(trader); + uint256 out = router.exactInputSingle( + FluxSwapRouter.ExactInputSingleParams({ + tokenIn: address(tokenA), + tokenOut: address(tokenB), + fee: 3000, + recipient: trader, + deadline: block.timestamp + 1, + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ); + assertLt(out, amountIn); + } +}