441 lines
14 KiB
Solidity
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
|
|
);
|
|
}
|
|
}
|