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