// SPDX-License-Identifier: MIT pragma solidity =0.8.20; import {Test} from "forge-std/Test.sol"; import "../src/mock/MockERC20.sol"; import "../src/UniswapV2Pair.sol"; import "../src/UniswapV2Factory.sol"; import "../src/libraries/UQ112x112.sol"; contract MockUser { function addLiquidity( address pair, address _token0, address _token1, uint256 _amount0, uint256 _amount1 ) public returns (uint256) { MockERC20(_token0).transfer(pair, _amount0); MockERC20(_token1).transfer(pair, _amount1); return UniswapV2Pair(pair).mint(address(this)); } function removeLiquidity(address pair, uint256 liquidity) public returns (uint256, uint256) { UniswapV2Pair(pair).transfer(pair, liquidity); return UniswapV2Pair(pair).burn(address(this)); } } contract TestUniswapV2Pair is Test { MockERC20 public token0; MockERC20 public token1; UniswapV2Pair public pair; UniswapV2Factory public factory; MockUser public user; function setUp() public { token0 = new MockERC20("SomeToken0", "ST0", 18); token1 = new MockERC20("SomeToken1", "ST1", 9); factory = new UniswapV2Factory(address(0)); address tokenAddress = factory.createPair(address(token0), address(token1)); pair = UniswapV2Pair(tokenAddress); // pair.initialize(address(token0), address(token1)); token0.mint(address(this), 10 ether); token1.mint(address(this), 10 ether); user = new MockUser(); token0.mint(address(user), 10 ether); token1.mint(address(user), 10 ether); } function assertBlockTimestampLast(uint256 timestamp) public view { (, , uint32 lastBlockTimestamp) = pair.getReserves(); assertEq(timestamp, lastBlockTimestamp); } function getCurrentMarginalPrices() public view returns (uint256 price0, uint256 price1) { (uint112 reserve0, uint112 reserve1, ) = pair.getReserves(); price0 = reserve0 > 0 ? uint256(UQ112x112.encode(reserve1)) / reserve0 : 0; price1 = reserve1 > 0 ? uint256(UQ112x112.encode(reserve0)) / reserve1 : 0; } function sortAmountsByTokens(uint256 amount0, uint256 amount1) public view returns (uint256, uint256) { if (token0 < token1) { return (amount0, amount1); } else { return (amount1, amount0); } } function assertCumulativePrices(uint256 price0, uint256 price1) public view { assertEq( price0, pair.price0CumulativeLast(), "unexpected cumulative price 0" ); assertEq( price1, pair.price1CumulativeLast(), "unexpected cumulative price 1" ); } function assertPairReserves(uint256 _reserve0, uint256 _reserve1) public view { (uint256 reserve0, uint256 reserve1,) = pair.getReserves(); // (reserve0, reserve1) = sortAmountsByTokens(reserve0, reserve1); assertEq(reserve0, _reserve0); assertEq(reserve1, _reserve1); } function testMintNewPair() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 liquidity = pair.mint(address(this)); assertPairReserves(1 ether, 1 ether); assertEq(pair.balanceOf(address(0)), pair.MINIMUM_LIQUIDITY()); assertEq(pair.balanceOf(address(this)), liquidity); assertEq(pair.totalSupply(), liquidity + pair.MINIMUM_LIQUIDITY()); } function testMintWithReserve() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 l1 = pair.mint(address(this)); token0.transfer(address(pair), 2 ether); token1.transfer(address(pair), 2 ether); uint256 l2 = pair.mint(address(this)); assertPairReserves(3 ether, 3 ether); assertEq(pair.balanceOf(address(this)), l1 + l2); assertEq(pair.totalSupply(), l1 + l2 + pair.MINIMUM_LIQUIDITY()); } function testMintUnequalBalance() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 l1 = pair.mint(address(this)); token0.transfer(address(pair), 4 ether); token1.transfer(address(pair), 1 ether); uint256 l2 = pair.mint(address(this)); assertPairReserves(2 ether, 5 ether); assertEq(pair.balanceOf(address(this)), l1 + l2); assertEq(pair.totalSupply(), l1 + l2 + pair.MINIMUM_LIQUIDITY()); } function testMintArithmeticUnderflow() public { // 0x11: Arithmetic over/underflow vm.expectRevert(); pair.mint(address(this)); } function testMintInsufficientLiquidity() public { token0.transfer(address(pair), 1000); token1.transfer(address(pair), 1000); vm.expectRevert(); pair.mint(address(this)); } function testMintMultipleUsers() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 l1 = pair.mint(address(this)); uint256 l2 = user.addLiquidity( address(pair), address(token0), address(token1), 2 ether, 3 ether ); assertPairReserves(4 ether, 3 ether); assertEq(pair.balanceOf(address(this)), l1); assertEq(pair.balanceOf(address(user)), l2); assertEq(pair.totalSupply(), l1 + l2 + pair.MINIMUM_LIQUIDITY()); } function testBurn() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 liquidity = pair.mint(address(this)); pair.transfer(address(pair), liquidity); pair.burn(address(this)); assertEq(pair.balanceOf(address(this)), 0); assertEq(pair.totalSupply(), pair.MINIMUM_LIQUIDITY()); assertPairReserves(pair.MINIMUM_LIQUIDITY(), pair.MINIMUM_LIQUIDITY()); assertEq( token0.balanceOf(address(this)), 10 ether - pair.MINIMUM_LIQUIDITY() ); assertEq( token1.balanceOf(address(this)), 10 ether - pair.MINIMUM_LIQUIDITY() ); } function testBurnUnequal() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 l0 = pair.mint(address(this)); token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 2 ether); uint256 l1 = pair.mint(address(this)); pair.transfer(address(pair), l0 + l1); (uint256 amount0, uint256 amount1) = pair.burn(address(this)); (amount0, amount1) = sortAmountsByTokens(amount0, amount1); assertEq(pair.balanceOf(address(this)), 0); assertEq(pair.totalSupply(), pair.MINIMUM_LIQUIDITY()); assertEq(token0.balanceOf(address(this)), 10 ether - 2 ether + amount0); assertEq(token1.balanceOf(address(this)), 10 ether - 3 ether + amount1); } function testBurnNoLiquidity() public { // 0x12: divide/modulo by zero vm.expectRevert(); pair.burn(address(this)); } function testBurnInsufficientLiquidityBurned() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); vm.expectRevert(); pair.burn(address(this)); } function testBurnMultipleUsers() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 l1 = pair.mint(address(this)); uint256 l2 = user.addLiquidity( address(pair), address(token0), address(token1), 2 ether, 3 ether ); pair.transfer(address(pair), l1); (uint256 amount0, uint256 amount1) = pair.burn(address(this)); (amount0, amount1) = sortAmountsByTokens(amount0, amount1); assertEq(pair.balanceOf(address(this)), 0); assertEq(pair.balanceOf(address(user)), l2); assertEq(pair.totalSupply(), l2 + pair.MINIMUM_LIQUIDITY()); assertPairReserves(4 ether - amount1, 3 ether - amount0); assertEq(token0.balanceOf(address(this)), 10 ether - 1 ether + amount0); assertEq(token1.balanceOf(address(this)), 10 ether - 1 ether + amount1); } function testBurnUnbalancedMultipleUsers() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); uint256 l1 = pair.mint(address(this)); uint256 l2 = user.addLiquidity( address(pair), address(token0), address(token1), 2 ether, 3 ether ); (uint256 a00, uint256 a01) = user.removeLiquidity(address(pair), l2); (a00, a01) = sortAmountsByTokens(a00, a01); pair.transfer(address(pair), l1); (uint256 a10, uint256 a11) = pair.burn(address(this)); (a10, a11) = sortAmountsByTokens(a10, a11); assertEq(pair.balanceOf(address(this)), 0); assertEq(pair.balanceOf(address(user)), 0); assertEq(pair.totalSupply(), pair.MINIMUM_LIQUIDITY()); // second user penalised for unbalanced liquidity, hence reserves unbalanced assertPairReserves( pair.MINIMUM_LIQUIDITY() * 4 / 3 + 1, pair.MINIMUM_LIQUIDITY() ); assertEq(token0.balanceOf(address(this)), 10 ether - 1 ether + a10); assertEq(token1.balanceOf(address(this)), 10 ether - 1 ether + a11); assertEq(token0.balanceOf(address(user)), 10 ether - 2 ether + a00); assertEq(token1.balanceOf(address(user)), 10 ether - 3 ether + a01); } function testSwap() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); // transfer to maintain K token1.transfer(address(pair), 1 ether); pair.swap(0.5 ether, 0 ether, address(user), ""); assertPairReserves(1.5 ether, 1 ether); assertEq(token1.balanceOf(address(user)), 10 ether + 0.5 ether); } function testSwapMultipleUserLiquidity() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); user.addLiquidity( address(pair), address(token0), address(token1), 2 ether, 3 ether ); // transfer to maintain K token0.transfer(address(pair), 2 ether); pair.swap(0 ether, 1 ether, address(user), ""); assertPairReserves(4 ether, 4 ether); } function testSwapUnderpriced() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); // transfer to maintain K token1.transfer(address(pair), 1 ether); pair.swap(0, 0.4 ether, address(user), ""); assertPairReserves(2 ether, 0.6 ether); } function testSwapInvalidAmount() public { vm.expectRevert(); pair.swap(0 ether, 0 ether, address(user), ""); } function testSwapInsufficientLiquidity() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); vm.expectRevert(); pair.swap(3 ether, 0 ether, address(user), ""); vm.expectRevert(); pair.swap(0 ether, 3 ether, address(user), ""); } function testSwapSwapToSelf() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); vm.expectRevert(); pair.swap(1 ether, 0 ether, address(token0), ""); vm.expectRevert(); pair.swap(0 ether, 1 ether, address(token1), ""); } function testSwapInvalidConstantProductFormula() public { token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); vm.expectRevert(); pair.swap(1 ether, 0 ether, address(user), ""); vm.expectRevert(); pair.swap(0 ether, 1 ether, address(user), ""); } function testCumulativePrices() public { vm.warp(0); token0.transfer(address(pair), 1 ether); token1.transfer(address(pair), 1 ether); pair.mint(address(this)); pair.sync(); assertCumulativePrices(0, 0); ( uint256 currentPrice0, uint256 currentPrice1 ) = getCurrentMarginalPrices(); vm.warp(1); pair.sync(); assertBlockTimestampLast(1); assertCumulativePrices(currentPrice0, currentPrice1); vm.warp(2); pair.sync(); assertBlockTimestampLast(2); assertCumulativePrices(currentPrice0 * 2, currentPrice1 * 2); vm.warp(3); pair.sync(); assertBlockTimestampLast(3); assertCumulativePrices(currentPrice0 * 3, currentPrice1 * 3); user.addLiquidity( address(pair), address(token0), address(token1), 2 ether, 3 ether ); (uint256 newPrice0, uint256 newPrice1) = getCurrentMarginalPrices(); vm.warp(4); pair.sync(); assertBlockTimestampLast(4); assertCumulativePrices( currentPrice0 * 3 + newPrice0, currentPrice1 * 3 + newPrice1 ); vm.warp(5); pair.sync(); assertBlockTimestampLast(5); assertCumulativePrices( currentPrice0 * 3 + newPrice0 * 2, currentPrice1 * 3 + newPrice1 * 2 ); vm.warp(6); pair.sync(); assertBlockTimestampLast(6); assertCumulativePrices( currentPrice0 * 3 + newPrice0 * 3, currentPrice1 * 3 + newPrice1 * 3 ); } }