diff --git a/test/FluxPair.t.sol b/test/FluxPair.t.sol new file mode 100644 index 0000000..4d6d80a --- /dev/null +++ b/test/FluxPair.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.36; + +import {Test} from "forge-std/Test.sol"; +import {FluxFactory} from "../src/FluxFactory.sol"; +import {FluxPair} from "../src/FluxPair.sol"; +import {TestErc20} from "../src/TestErc20.sol"; +import {IFluxCallee} from "../src/interfaces/IFluxCallee.sol"; + +contract ReentrantSwapper is IFluxCallee { + FluxPair private pair; + + constructor(FluxPair pairAddress) { + pair = pairAddress; + } + + function fluxCall(address, uint256, uint256, bytes calldata) external { + pair.swap(0, 1, address(this), ""); + } +} + +contract FluxPairTest is Test { + address private constant receiver = address(0xBEEF); + + FluxFactory private factory; + FluxPair private pair; + TestErc20 private token0; + TestErc20 private token1; + + uint256 private initialLp; + + function setUp() public { + factory = new FluxFactory(address(this)); + TestErc20 tokenA = new TestErc20("Alpha", "ALPHA", 1_000_000e18); + TestErc20 tokenB = new TestErc20("Beta", "BETA", 1_000_000e18); + address pairAddress = factory.createPair(address(tokenA), address(tokenB)); + pair = FluxPair(pairAddress); + token0 = TestErc20(pair.token0()); + token1 = TestErc20(pair.token1()); + + token0.transfer(pairAddress, 100e18); + token1.transfer(pairAddress, 400e18); + initialLp = pair.mint(address(this)); + } + + function testInitialMint() public view { + assertEq(initialLp, 199999999999999999000); + assertEq(pair.balanceOf(address(this)), 199999999999999999000); + assertEq(pair.totalSupply(), 200e18); + assertEq(pair.balanceOf(address(0)), 1000); + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + assertEq(reserve0, 100e18); + assertEq(reserve1, 400e18); + } + + function testProportionalMint() public { + token0.transfer(address(pair), 50e18); + token1.transfer(address(pair), 200e18); + uint256 minted = pair.mint(receiver); + assertEq(minted, 100e18); + assertEq(pair.balanceOf(receiver), 100e18); + assertEq(pair.totalSupply(), 300e18); + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + assertEq(reserve0, 150e18); + assertEq(reserve1, 600e18); + } + + function testSwapMovesReservesAndKeepsInvariant() public { + uint256 expectedOut = 36264435755205965263; + uint256 kBefore = uint256(100e18) * 400e18; + + token0.transfer(address(pair), 10e18); + pair.swap(0, expectedOut, receiver, ""); + + assertEq(token1.balanceOf(receiver), expectedOut); + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + assertEq(reserve0, 110e18); + assertEq(reserve1, 363735564244794034737); + assertGt(uint256(reserve0) * uint256(reserve1), kBefore); + } + + function testSwapRevertsWhenInvariantBroken() public { + token0.transfer(address(pair), 10e18); + vm.expectRevert(bytes("Flux: K")); + pair.swap(0, 36264435755205965263 + 1e15, receiver, ""); + } + + function testSwapRevertsWithoutInput() public { + vm.expectRevert(bytes("Flux: INSUFFICIENT_INPUT_AMOUNT")); + pair.swap(0, 1e18, receiver, ""); + } + + function testBurnReturnsProportional() public { + uint256 lp = pair.balanceOf(address(this)); + pair.transfer(address(pair), lp); + (uint256 amount0, uint256 amount1) = pair.burn(receiver); + + assertEq(amount0, 99999999999999999500); + assertEq(amount1, 399999999999999998000); + assertEq(token0.balanceOf(receiver), 99999999999999999500); + assertEq(token1.balanceOf(receiver), 399999999999999998000); + assertEq(pair.totalSupply(), 1000); + } + + function testSkimSendsExcessAndKeepsReserves() public { + token0.transfer(address(pair), 5e18); + pair.skim(receiver); + assertEq(token0.balanceOf(receiver), 5e18); + (uint112 reserve0,,) = pair.getReserves(); + assertEq(reserve0, 100e18); + } + + function testSyncAdoptsBalances() public { + token0.transfer(address(pair), 5e18); + pair.sync(); + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + assertEq(reserve0, 105e18); + assertEq(reserve1, 400e18); + } + + function testPriceAccumulatorGrowsOverTime() public { + assertEq(pair.price0CumulativeLast(), 0); + vm.warp(block.timestamp + 3600); + pair.sync(); + assertGt(pair.price0CumulativeLast(), 0); + assertGt(pair.price1CumulativeLast(), 0); + } + + function testReentrancyLockBlocksSwap() public { + ReentrantSwapper attacker = new ReentrantSwapper(pair); + token0.transfer(address(pair), 10e18); + vm.expectRevert(bytes("Flux: LOCKED")); + pair.swap(0, 1e18, address(attacker), "0x01"); + } +}