uniswap-v2-contracts/test/UniswapV2Pair.t.sol
Uncle Fatso c3e0d54be4
additional tests added and extra mocks added
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2025-05-17 16:07:39 +03:00

441 lines
14 KiB
Solidity

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