diff --git a/src/FluxPair.sol b/src/FluxPair.sol new file mode 100644 index 0000000..af2883e --- /dev/null +++ b/src/FluxPair.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.36; + +import {Erc20} from "./Erc20.sol"; +import {IErc20} from "./interfaces/IErc20.sol"; +import {IFluxCallee} from "./interfaces/IFluxCallee.sol"; +import {Math} from "./libraries/Math.sol"; +import {UQ112x112} from "./libraries/UQ112x112.sol"; + +/// @title Constant-product pair holding reserves of two tokens and the LP token. +contract FluxPair is Erc20 { + using UQ112x112 for uint224; + + uint256 public constant minimumLiquidity = 10 ** 3; + + address public factory; + address public token0; + address public token1; + + uint112 private reserve0; + uint112 private reserve1; + uint32 private blockTimestampLast; + + uint256 public price0CumulativeLast; + uint256 public price1CumulativeLast; + + uint256 private unlocked = 1; + + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + modifier lock() { + require(unlocked == 1, "Flux: LOCKED"); + unlocked = 0; + _; + unlocked = 1; + } + + constructor() Erc20("FluxSwap LP", "FLUX-LP") { + factory = msg.sender; + } + + function initialize(address firstToken, address secondToken) external { + require(msg.sender == factory, "Flux: FORBIDDEN"); + token0 = firstToken; + token1 = secondToken; + } + + function getReserves() + public + view + returns (uint112 currentReserve0, uint112 currentReserve1, uint32 timestampLast) + { + currentReserve0 = reserve0; + currentReserve1 = reserve1; + timestampLast = blockTimestampLast; + } + + function _safeTransfer(address token, address to, uint256 value) private { + (bool success, bytes memory returndata) = + token.call(abi.encodeWithSelector(IErc20.transfer.selector, to, value)); + require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "Flux: TRANSFER_FAILED"); + } + + function _update(uint256 balance0, uint256 balance1, uint112 oldReserve0, uint112 oldReserve1) private { + require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "Flux: OVERFLOW"); + uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32); + unchecked { + uint32 timeElapsed = blockTimestamp - blockTimestampLast; + if (timeElapsed > 0 && oldReserve0 != 0 && oldReserve1 != 0) { + price0CumulativeLast += uint256(UQ112x112.encode(oldReserve1).uqdiv(oldReserve0)) * timeElapsed; + price1CumulativeLast += uint256(UQ112x112.encode(oldReserve0).uqdiv(oldReserve1)) * timeElapsed; + } + } + reserve0 = uint112(balance0); + reserve1 = uint112(balance1); + blockTimestampLast = blockTimestamp; + emit Sync(reserve0, reserve1); + } + + function mint(address to) external lock returns (uint256 liquidity) { + (uint112 oldReserve0, uint112 oldReserve1,) = getReserves(); + uint256 balance0 = IErc20(token0).balanceOf(address(this)); + uint256 balance1 = IErc20(token1).balanceOf(address(this)); + uint256 amount0 = balance0 - oldReserve0; + uint256 amount1 = balance1 - oldReserve1; + + uint256 supply = totalSupply; + if (supply == 0) { + liquidity = Math.sqrt(amount0 * amount1) - minimumLiquidity; + _mint(address(0), minimumLiquidity); + } else { + liquidity = Math.min(amount0 * supply / oldReserve0, amount1 * supply / oldReserve1); + } + require(liquidity > 0, "Flux: INSUFFICIENT_LIQUIDITY_MINTED"); + _mint(to, liquidity); + + _update(balance0, balance1, oldReserve0, oldReserve1); + emit Mint(msg.sender, amount0, amount1); + } + + function burn(address to) external lock returns (uint256 amount0, uint256 amount1) { + address firstToken = token0; + address secondToken = token1; + uint256 balance0 = IErc20(firstToken).balanceOf(address(this)); + uint256 balance1 = IErc20(secondToken).balanceOf(address(this)); + uint256 liquidity = balanceOf[address(this)]; + + uint256 supply = totalSupply; + amount0 = liquidity * balance0 / supply; + amount1 = liquidity * balance1 / supply; + require(amount0 > 0 && amount1 > 0, "Flux: INSUFFICIENT_LIQUIDITY_BURNED"); + _burn(address(this), liquidity); + _safeTransfer(firstToken, to, amount0); + _safeTransfer(secondToken, to, amount1); + balance0 = IErc20(firstToken).balanceOf(address(this)); + balance1 = IErc20(secondToken).balanceOf(address(this)); + + (uint112 oldReserve0, uint112 oldReserve1,) = getReserves(); + _update(balance0, balance1, oldReserve0, oldReserve1); + emit Burn(msg.sender, amount0, amount1, to); + } + + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external lock { + require(amount0Out > 0 || amount1Out > 0, "Flux: INSUFFICIENT_OUTPUT_AMOUNT"); + (uint112 oldReserve0, uint112 oldReserve1,) = getReserves(); + require(amount0Out < oldReserve0 && amount1Out < oldReserve1, "Flux: INSUFFICIENT_LIQUIDITY"); + + uint256 balance0; + uint256 balance1; + { + address firstToken = token0; + address secondToken = token1; + require(to != firstToken && to != secondToken, "Flux: INVALID_TO"); + if (amount0Out > 0) _safeTransfer(firstToken, to, amount0Out); + if (amount1Out > 0) _safeTransfer(secondToken, to, amount1Out); + if (data.length > 0) IFluxCallee(to).fluxCall(msg.sender, amount0Out, amount1Out, data); + balance0 = IErc20(firstToken).balanceOf(address(this)); + balance1 = IErc20(secondToken).balanceOf(address(this)); + } + uint256 amount0In = balance0 > oldReserve0 - amount0Out ? balance0 - (oldReserve0 - amount0Out) : 0; + uint256 amount1In = balance1 > oldReserve1 - amount1Out ? balance1 - (oldReserve1 - amount1Out) : 0; + require(amount0In > 0 || amount1In > 0, "Flux: INSUFFICIENT_INPUT_AMOUNT"); + { + uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3; + uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3; + require(balance0Adjusted * balance1Adjusted >= uint256(oldReserve0) * oldReserve1 * (1000 ** 2), "Flux: K"); + } + _update(balance0, balance1, oldReserve0, oldReserve1); + emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); + } + + function skim(address to) external lock { + address firstToken = token0; + address secondToken = token1; + _safeTransfer(firstToken, to, IErc20(firstToken).balanceOf(address(this)) - reserve0); + _safeTransfer(secondToken, to, IErc20(secondToken).balanceOf(address(this)) - reserve1); + } + + function sync() external lock { + _update(IErc20(token0).balanceOf(address(this)), IErc20(token1).balanceOf(address(this)), reserve0, reserve1); + } +}