// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.6.12; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts/math/Math.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; import "./libraries/MathExt.sol"; import "./libraries/FeeFomula.sol"; import "./libraries/ERC20Permit.sol"; import "./interfaces/IDMMFactory.sol"; import "./interfaces/IDMMCallee.sol"; import "./interfaces/IDMMPool.sol"; import "./interfaces/IERC20Metadata.sol"; import "./VolumeTrendRecorder.sol"; contract DMMPool is IDMMPool, ERC20Permit, ReentrancyGuard, VolumeTrendRecorder { using SafeMath for uint256; using SafeERC20 for IERC20; uint256 internal constant MAX_UINT112 = 2**112 - 1; uint256 internal constant BPS = 10000; struct ReserveData { uint256 reserve0; uint256 reserve1; uint256 vReserve0; uint256 vReserve1; // only used when isAmpPool = true } uint256 public constant MINIMUM_LIQUIDITY = 10**3; /// @dev To make etherscan auto-verify new pool, these variables are not immutable IDMMFactory public override factory; IERC20 public override token0; IERC20 public override token1; /// @dev uses single storage slot, accessible via getReservesData uint112 internal reserve0; uint112 internal reserve1; uint32 public override ampBps; /// @dev addition param only when amplification factor > 1 uint112 internal vReserve0; uint112 internal vReserve1; /// @dev vReserve0 * vReserve1, as of immediately after the most recent liquidity event uint256 public override kLast; 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, uint256 feeInPrecision ); event Sync(uint256 vReserve0, uint256 vReserve1, uint256 reserve0, uint256 reserve1); constructor() public ERC20Permit("KyberDMM LP", "DMM-LP", "1") VolumeTrendRecorder(0) { factory = IDMMFactory(msg.sender); } // called once by the factory at time of deployment function initialize( IERC20 _token0, IERC20 _token1, uint32 _ampBps ) external { require(msg.sender == address(factory), "DMM: FORBIDDEN"); token0 = _token0; token1 = _token1; ampBps = _ampBps; } /// @dev this low-level function should be called from a contract /// which performs important safety checks function mint(address to) external override nonReentrant returns (uint256 liquidity) { (bool isAmpPool, ReserveData memory data) = getReservesData(); ReserveData memory _data; _data.reserve0 = token0.balanceOf(address(this)); _data.reserve1 = token1.balanceOf(address(this)); uint256 amount0 = _data.reserve0.sub(data.reserve0); uint256 amount1 = _data.reserve1.sub(data.reserve1); bool feeOn = _mintFee(isAmpPool, data); uint256 _totalSupply = totalSupply(); // gas savings, must be defined here since totalSupply can update in _mintFee if (_totalSupply == 0) { if (isAmpPool) { uint32 _ampBps = ampBps; _data.vReserve0 = _data.reserve0.mul(_ampBps) / BPS; _data.vReserve1 = _data.reserve1.mul(_ampBps) / BPS; } liquidity = MathExt.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(-1), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min( amount0.mul(_totalSupply) / data.reserve0, amount1.mul(_totalSupply) / data.reserve1 ); if (isAmpPool) { uint256 b = liquidity.add(_totalSupply); _data.vReserve0 = Math.max(data.vReserve0.mul(b) / _totalSupply, _data.reserve0); _data.vReserve1 = Math.max(data.vReserve1.mul(b) / _totalSupply, _data.reserve1); } } require(liquidity > 0, "DMM: INSUFFICIENT_LIQUIDITY_MINTED"); _mint(to, liquidity); _update(isAmpPool, _data); if (feeOn) kLast = getK(isAmpPool, _data); emit Mint(msg.sender, amount0, amount1); } /// @dev this low-level function should be called from a contract /// @dev which performs important safety checks /// @dev user must transfer LP token to this contract before call burn function burn(address to) external override nonReentrant returns (uint256 amount0, uint256 amount1) { (bool isAmpPool, ReserveData memory data) = getReservesData(); // gas savings IERC20 _token0 = token0; // gas savings IERC20 _token1 = token1; // gas savings uint256 balance0 = _token0.balanceOf(address(this)); uint256 balance1 = _token1.balanceOf(address(this)); require(balance0 >= data.reserve0 && balance1 >= data.reserve1, "DMM: UNSYNC_RESERVES"); uint256 liquidity = balanceOf(address(this)); bool feeOn = _mintFee(isAmpPool, data); uint256 _totalSupply = totalSupply(); // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution require(amount0 > 0 && amount1 > 0, "DMM: INSUFFICIENT_LIQUIDITY_BURNED"); _burn(address(this), liquidity); _token0.safeTransfer(to, amount0); _token1.safeTransfer(to, amount1); ReserveData memory _data; _data.reserve0 = _token0.balanceOf(address(this)); _data.reserve1 = _token1.balanceOf(address(this)); if (isAmpPool) { uint256 b = Math.min( _data.reserve0.mul(_totalSupply) / data.reserve0, _data.reserve1.mul(_totalSupply) / data.reserve1 ); _data.vReserve0 = Math.max(data.vReserve0.mul(b) / _totalSupply, _data.reserve0); _data.vReserve1 = Math.max(data.vReserve1.mul(b) / _totalSupply, _data.reserve1); } _update(isAmpPool, _data); if (feeOn) kLast = getK(isAmpPool, _data); // data are up-to-date emit Burn(msg.sender, amount0, amount1, to); } /// @dev this low-level function should be called from a contract /// @dev which performs important safety checks function swap( uint256 amount0Out, uint256 amount1Out, address to, bytes calldata callbackData ) external override nonReentrant { require(amount0Out > 0 || amount1Out > 0, "DMM: INSUFFICIENT_OUTPUT_AMOUNT"); (bool isAmpPool, ReserveData memory data) = getReservesData(); // gas savings require( amount0Out < data.reserve0 && amount1Out < data.reserve1, "DMM: INSUFFICIENT_LIQUIDITY" ); ReserveData memory newData; { // scope for _token{0,1}, avoids stack too deep errors IERC20 _token0 = token0; IERC20 _token1 = token1; require(to != address(_token0) && to != address(_token1), "DMM: INVALID_TO"); if (amount0Out > 0) _token0.safeTransfer(to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _token1.safeTransfer(to, amount1Out); // optimistically transfer tokens if (callbackData.length > 0) IDMMCallee(to).dmmSwapCall(msg.sender, amount0Out, amount1Out, callbackData); newData.reserve0 = _token0.balanceOf(address(this)); newData.reserve1 = _token1.balanceOf(address(this)); if (isAmpPool) { newData.vReserve0 = data.vReserve0.add(newData.reserve0).sub(data.reserve0); newData.vReserve1 = data.vReserve1.add(newData.reserve1).sub(data.reserve1); } } uint256 amount0In = newData.reserve0 > data.reserve0 - amount0Out ? newData.reserve0 - (data.reserve0 - amount0Out) : 0; uint256 amount1In = newData.reserve1 > data.reserve1 - amount1Out ? newData.reserve1 - (data.reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, "DMM: INSUFFICIENT_INPUT_AMOUNT"); uint256 feeInPrecision = verifyBalanceAndUpdateEma( amount0In, amount1In, isAmpPool ? data.vReserve0 : data.reserve0, isAmpPool ? data.vReserve1 : data.reserve1, isAmpPool ? newData.vReserve0 : newData.reserve0, isAmpPool ? newData.vReserve1 : newData.reserve1 ); _update(isAmpPool, newData); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to, feeInPrecision); } /// @dev force balances to match reserves function skim(address to) external nonReentrant { token0.safeTransfer(to, token0.balanceOf(address(this)).sub(reserve0)); token1.safeTransfer(to, token1.balanceOf(address(this)).sub(reserve1)); } /// @dev force reserves to match balances function sync() external override nonReentrant { (bool isAmpPool, ReserveData memory data) = getReservesData(); bool feeOn = _mintFee(isAmpPool, data); ReserveData memory newData; newData.reserve0 = IERC20(token0).balanceOf(address(this)); newData.reserve1 = IERC20(token1).balanceOf(address(this)); // update virtual reserves if this is amp pool if (isAmpPool) { uint256 _totalSupply = totalSupply(); uint256 b = Math.min( newData.reserve0.mul(_totalSupply) / data.reserve0, newData.reserve1.mul(_totalSupply) / data.reserve1 ); newData.vReserve0 = Math.max(data.vReserve0.mul(b) / _totalSupply, newData.reserve0); newData.vReserve1 = Math.max(data.vReserve1.mul(b) / _totalSupply, newData.reserve1); } _update(isAmpPool, newData); if (feeOn) kLast = getK(isAmpPool, newData); } /// @dev returns data to calculate amountIn, amountOut function getTradeInfo() external virtual override view returns ( uint112 _reserve0, uint112 _reserve1, uint112 _vReserve0, uint112 _vReserve1, uint256 feeInPrecision ) { // gas saving to read reserve data _reserve0 = reserve0; _reserve1 = reserve1; uint32 _ampBps = ampBps; _vReserve0 = vReserve0; _vReserve1 = vReserve1; if (_ampBps == BPS) { _vReserve0 = _reserve0; _vReserve1 = _reserve1; } uint256 rFactorInPrecision = getRFactor(block.number); feeInPrecision = getFinalFee(FeeFomula.getFee(rFactorInPrecision), _ampBps); } /// @dev returns reserve data to calculate amount to add liquidity function getReserves() external override view returns (uint112 _reserve0, uint112 _reserve1) { _reserve0 = reserve0; _reserve1 = reserve1; } function name() public override view returns (string memory) { IERC20Metadata _token0 = IERC20Metadata(address(token0)); IERC20Metadata _token1 = IERC20Metadata(address(token1)); return string(abi.encodePacked("KyberDMM LP ", _token0.symbol(), "-", _token1.symbol())); } function symbol() public override view returns (string memory) { IERC20Metadata _token0 = IERC20Metadata(address(token0)); IERC20Metadata _token1 = IERC20Metadata(address(token1)); return string(abi.encodePacked("DMM-LP ", _token0.symbol(), "-", _token1.symbol())); } function verifyBalanceAndUpdateEma( uint256 amount0In, uint256 amount1In, uint256 beforeReserve0, uint256 beforeReserve1, uint256 afterReserve0, uint256 afterReserve1 ) internal virtual returns (uint256 feeInPrecision) { // volume = beforeReserve0 * amount1In / beforeReserve1 + amount0In (normalized into amount in token 0) uint256 volume = beforeReserve0.mul(amount1In).div(beforeReserve1).add(amount0In); uint256 rFactorInPrecision = recordNewUpdatedVolume(block.number, volume); feeInPrecision = getFinalFee(FeeFomula.getFee(rFactorInPrecision), ampBps); // verify balance update matches with fomula uint256 balance0Adjusted = afterReserve0.mul(PRECISION); balance0Adjusted = balance0Adjusted.sub(amount0In.mul(feeInPrecision)); balance0Adjusted = balance0Adjusted / PRECISION; uint256 balance1Adjusted = afterReserve1.mul(PRECISION); balance1Adjusted = balance1Adjusted.sub(amount1In.mul(feeInPrecision)); balance1Adjusted = balance1Adjusted / PRECISION; require( balance0Adjusted.mul(balance1Adjusted) >= beforeReserve0.mul(beforeReserve1), "DMM: K" ); } /// @dev update reserves function _update(bool isAmpPool, ReserveData memory data) internal { reserve0 = safeUint112(data.reserve0); reserve1 = safeUint112(data.reserve1); if (isAmpPool) { assert(data.vReserve0 >= data.reserve0 && data.vReserve1 >= data.reserve1); // never happen vReserve0 = safeUint112(data.vReserve0); vReserve1 = safeUint112(data.vReserve1); } emit Sync(data.vReserve0, data.vReserve1, data.reserve0, data.reserve1); } /// @dev if fee is on, mint liquidity equivalent to configured fee of the growth in sqrt(k) function _mintFee(bool isAmpPool, ReserveData memory data) internal returns (bool feeOn) { (address feeTo, uint16 governmentFeeBps) = factory.getFeeConfiguration(); feeOn = feeTo != address(0); uint256 _kLast = kLast; // gas savings if (feeOn) { if (_kLast != 0) { uint256 rootK = MathExt.sqrt(getK(isAmpPool, data)); uint256 rootKLast = MathExt.sqrt(_kLast); if (rootK > rootKLast) { uint256 numerator = totalSupply().mul(rootK.sub(rootKLast)).mul( governmentFeeBps ); uint256 denominator = rootK.add(rootKLast).mul(5000); uint256 liquidity = numerator / denominator; if (liquidity > 0) _mint(feeTo, liquidity); } } } else if (_kLast != 0) { kLast = 0; } } /// @dev gas saving to read reserve data function getReservesData() internal view returns (bool isAmpPool, ReserveData memory data) { data.reserve0 = reserve0; data.reserve1 = reserve1; isAmpPool = ampBps != BPS; if (isAmpPool) { data.vReserve0 = vReserve0; data.vReserve1 = vReserve1; } } function getFinalFee(uint256 feeInPrecision, uint32 _ampBps) internal pure returns (uint256) { if (_ampBps <= 20000) { return feeInPrecision; } else if (_ampBps <= 50000) { return (feeInPrecision * 20) / 30; } else if (_ampBps <= 200000) { return (feeInPrecision * 10) / 30; } else { return (feeInPrecision * 4) / 30; } } function getK(bool isAmpPool, ReserveData memory data) internal pure returns (uint256) { return isAmpPool ? data.vReserve0 * data.vReserve1 : data.reserve0 * data.reserve1; } function safeUint112(uint256 x) internal pure returns (uint112) { require(x <= MAX_UINT112, "DMM: OVERFLOW"); return uint112(x); } }