diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index f1242b9bb..e074db3f3 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -18,3 +18,5 @@ jobs: - name: Check Typos uses: crate-ci/typos@v1.12.12 + with: + config: ./_typo.toml diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 4779cffc9..000000000 --- a/.husky/pre-push +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -yarn typechain -yarn lint -yarn size diff --git a/_typo.toml b/_typo.toml new file mode 100644 index 000000000..06d437176 --- /dev/null +++ b/_typo.toml @@ -0,0 +1,2 @@ +[files] +extend-exclude = ["test/helpers/izumi-helper.ts"] diff --git a/contracts/dependencies/izumi/izumi-swap-core/flash.sol b/contracts/dependencies/izumi/izumi-swap-core/flash.sol new file mode 100644 index 000000000..f7eee3213 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/flash.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./interfaces/IiZiSwapPool.sol"; +import "./interfaces/IiZiSwapFlashCallback.sol"; +import "./interfaces/IiZiSwapCallback.sol"; + +import "./libraries/Liquidity.sol"; +import "./libraries/Point.sol"; +import "./libraries/PointBitmap.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LimitOrder.sol"; +import "./libraries/SwapMathY2X.sol"; +import "./libraries/SwapMathX2Y.sol"; +import "./libraries/SwapMathY2XDesire.sol"; +import "./libraries/SwapMathX2YDesire.sol"; +import "./libraries/TokenTransfer.sol"; +import "./libraries/UserEarn.sol"; +import "./libraries/State.sol"; +import "./libraries/Oracle.sol"; +import "./libraries/OrderOrEndpoint.sol"; +import "./libraries/MaxMinMath.sol"; + +contract FlashModule { + + using Liquidity for mapping(bytes32 =>Liquidity.Data); + using Liquidity for Liquidity.Data; + using Point for mapping(int24 =>Point.Data); + using Point for Point.Data; + using PointBitmap for mapping(int16 =>uint256); + using LimitOrder for LimitOrder.Data; + using UserEarn for UserEarn.Data; + using UserEarn for mapping(bytes32 =>UserEarn.Data); + using SwapMathY2X for SwapMathY2X.RangeRetState; + using SwapMathX2Y for SwapMathX2Y.RangeRetState; + using Oracle for Oracle.Observation[65535]; + using OrderOrEndpoint for mapping(int24 =>int24); + + int24 internal constant LEFT_MOST_PT = -800000; + int24 internal constant RIGHT_MOST_PT = 800000; + + /// @notice left most point regularized by pointDelta + int24 public leftMostPt; + /// @notice right most point regularized by pointDelta + int24 public rightMostPt; + /// @notice maximum liquidSum for each point, see points() in IiZiSwapPool or library Point + uint128 public maxLiquidPt; + + /// @notice address of iZiSwapFactory + address public factory; + + /// @notice address of tokenX + address public tokenX; + /// @notice address of tokenY + address public tokenY; + /// @notice fee amount of this swap pool, 3000 means 0.3% + uint24 public fee; + + /// @notice minimum number of distance between initialized or limitorder points + int24 public pointDelta; + + /// @notice The fee growth as a 128-bit fixpoing fees of tokenX collected per 1 liquidity of the pool + uint256 public feeScaleX_128; + /// @notice The fee growth as a 128-bit fixpoing fees of tokenY collected per 1 liquidity of the pool + uint256 public feeScaleY_128; + + uint160 sqrtRate_96; + + /// @notice some values of pool + /// see library State or IiZiSwapPool#state for more infomation + State public state; + + /// @notice the information about a liquidity by the liquidity's key + mapping(bytes32 =>Liquidity.Data) public liquidities; + + /// @notice 256 packed point (orderOrEndpoint>0) boolean values. See PointBitmap for more information + mapping(int16 =>uint256) public pointBitmap; + + /// @notice returns infomation of a point in the pool, see Point library of IiZiSwapPool#poitns for more information + mapping(int24 =>Point.Data) public points; + /// @notice infomation about a point whether has limit order and whether as an liquidity's endpoint + mapping(int24 =>int24) public orderOrEndpoint; + /// @notice limitOrder info on a given point + mapping(int24 =>LimitOrder.Data) public limitOrderData; + /// @notice information about a user's limit order (sell tokenY and earn tokenX) + mapping(bytes32 => UserEarn.Data) public userEarnX; + /// @notice information about a user's limit order (sell tokenX and earn tokenY) + mapping(bytes32 => UserEarn.Data) public userEarnY; + /// @notice observation data array + Oracle.Observation[65535] public observations; + + uint256 public totalFeeXCharged; + uint256 public totalFeeYCharged; + + address private original; + + address private swapModuleX2Y; + address private swapModuleY2X; + address private liquidityModule; + address private limitOrderModule; + address private flashModule; + + /// @notice percent to charge from miner's fee + uint24 public feeChargePercent; + + function balanceX() private view returns (uint256) { + (bool success, bytes memory data) = + tokenX.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + function balanceY() private view returns (uint256) { + (bool success, bytes memory data) = + tokenY.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + + /// Delegate call implementation for IiZiSwapPool#flash. + function flash( + address recipient, + uint256 amountX, + uint256 amountY, + bytes calldata data + ) external returns (uint256 actualAmountX, uint256 actualAmountY, uint256 paidX, uint256 paidY) { + uint128 currentLiquidity = state.liquidity; + require(currentLiquidity > 0, 'L'); + + // even the balance if not enough, the full fees are required to pay + uint256 feeX = MulDivMath.mulDivCeil(amountX, fee, 1e6); + uint256 feeY = MulDivMath.mulDivCeil(amountY, fee, 1e6); + uint256 balanceXBefore = balanceX(); + uint256 balanceYBefore = balanceY(); + + actualAmountX = MaxMinMath.min256(amountX, balanceXBefore); + actualAmountY = MaxMinMath.min256(amountY, balanceYBefore); + + if (actualAmountX > 0) TokenTransfer.transferToken(tokenX, recipient, actualAmountX); + if (actualAmountY > 0) TokenTransfer.transferToken(tokenY, recipient, actualAmountY); + + IiZiSwapFlashCallback(msg.sender).flashCallback(feeX, feeY, data); + uint256 balanceXAfter = balanceX(); + uint256 balanceYAfter = balanceY(); + + require(balanceXBefore + feeX <= balanceXAfter, 'FX'); + require(balanceYBefore + feeY <= balanceYAfter, 'FY'); + + paidX = balanceXAfter - balanceXBefore; + paidY = balanceYAfter - balanceYBefore; + + if (paidX > 0) { + uint256 chargedFeeAmount = paidX * feeChargePercent / 100; + totalFeeXCharged += chargedFeeAmount; + feeScaleX_128 = feeScaleX_128 + MulDivMath.mulDivFloor(paidX - chargedFeeAmount, TwoPower.Pow128, currentLiquidity); + } + if (paidY > 0) { + uint256 chargedFeeAmount = paidY * feeChargePercent / 100; + totalFeeYCharged += chargedFeeAmount; + feeScaleY_128 = feeScaleY_128 + MulDivMath.mulDivFloor(paidY - chargedFeeAmount, TwoPower.Pow128, currentLiquidity); + } + + } +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/iZiSwapFactory.sol b/contracts/dependencies/izumi/izumi-swap-core/iZiSwapFactory.sol new file mode 100644 index 000000000..da2571a0f --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/iZiSwapFactory.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./interfaces/IiZiSwapFactory.sol"; +import "./iZiSwapPool.sol"; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +struct DeployPoolParams { + address tokenX; + address tokenY; + uint24 fee; + int24 currentPoint; + int24 pointDelta; + uint24 feeChargePercent; +} + +contract iZiSwapFactory is Ownable, IiZiSwapFactory { + + /// @notice charge receiver of all pools in this factory + address public override chargeReceiver; + + /// @notice tokenX/tokenY/fee => pool address + mapping(address => mapping(address => mapping(uint24 => address))) public override pool; + + /// @notice mapping from fee amount to pointDelta + mapping(uint24 => int24) public override fee2pointDelta; + + /// @notice mark contract address in constructor to avoid delegate call + address public only_addr_; + + /// @notice address of module to support swapX2Y(DesireY) + address public override swapX2YModule; + + /// @notice address of module to support swapY2X(DesireX) + address public override swapY2XModule; + + /// @notice address of module to support liquidity + address public override liquidityModule; + + /// @notice address of module for user to manage limit orders + address public override limitOrderModule; + + /// @notice address of module to support flash loan + address public override flashModule; + + /// @notice default fee rate from miner's fee gain * 100 + uint24 public override defaultFeeChargePercent; + + DeployPoolParams public override deployPoolParams; + + /// @notice Construct the factory + /// @param _swapX2YModule swap module to support swapX2Y(DesireY) + /// @param _swapY2XModule swap module to support swapY2X(DesireX) + /// @param _liquidityModule liquidity module to support mint/burn/collect + /// @param _limitOrderModule module for user to manage limit orders + /// @param _flashModule module for user to flash + /// @param _defaultFeeChargePercent default fee rate from miner's fee gain * 100 + constructor( + address _chargeReceiver, + address _swapX2YModule, + address _swapY2XModule, + address _liquidityModule, + address _limitOrderModule, + address _flashModule, + uint24 _defaultFeeChargePercent + ) { + only_addr_ = address(this); + fee2pointDelta[100] = 1; + fee2pointDelta[400] = 8; + fee2pointDelta[2000] = 40; + fee2pointDelta[10000] = 200; + swapX2YModule = _swapX2YModule; + swapY2XModule = _swapY2XModule; + liquidityModule = _liquidityModule; + chargeReceiver = _chargeReceiver; + limitOrderModule = _limitOrderModule; + flashModule = _flashModule; + defaultFeeChargePercent = _defaultFeeChargePercent; + } + + modifier noDelegateCall() { + require(address(this) == only_addr_); + _; + } + + /// @inheritdoc IiZiSwapFactory + function enableFeeAmount(uint24 fee, uint24 pointDelta) external override noDelegateCall onlyOwner { + require(pointDelta > 0, "P0"); + require(fee2pointDelta[fee] == 0, "FD0"); + fee2pointDelta[fee] = int24(pointDelta); + } + + /// @inheritdoc IiZiSwapFactory + function newPool( + address tokenX, + address tokenY, + uint24 fee, + int24 currentPoint + ) external override noDelegateCall returns (address addr) { + require(tokenX != tokenY, "SmTK"); + if (tokenX > tokenY) { + (tokenX, tokenY) = (tokenY, tokenX); + } + require(pool[tokenX][tokenY][fee] == address(0)); + int24 pointDelta = fee2pointDelta[fee]; + + require(pointDelta > 0, 'pd'); + // now creating + bytes32 salt = keccak256(abi.encode(tokenX, tokenY, fee)); + + deployPoolParams = DeployPoolParams({ + tokenX: tokenX, + tokenY: tokenY, + fee: fee, + currentPoint: currentPoint, + pointDelta: pointDelta, + feeChargePercent: defaultFeeChargePercent + }); + addr = address(new iZiSwapPool{salt: salt}()); + delete deployPoolParams; + + pool[tokenX][tokenY][fee] = addr; + pool[tokenY][tokenX][fee] = addr; + emit NewPool(tokenX, tokenY, fee, uint24(pointDelta), addr); + } + + /// @inheritdoc IiZiSwapFactory + function modifyChargeReceiver(address _chargeReceiver) external override onlyOwner { + chargeReceiver = _chargeReceiver; + } + + /// @inheritdoc IiZiSwapFactory + function modifyDefaultFeeChargePercent(uint24 _defaultFeeChargePercent) external override onlyOwner { + defaultFeeChargePercent = _defaultFeeChargePercent; + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/iZiSwapPool.sol b/contracts/dependencies/izumi/izumi-swap-core/iZiSwapPool.sol new file mode 100644 index 000000000..66b0e57bf --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/iZiSwapPool.sol @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./interfaces/IiZiSwapPool.sol"; +import "./interfaces/IiZiSwapFactory.sol"; +import "./interfaces/IiZiSwapFlashCallback.sol"; +import "./interfaces/IiZiSwapCallback.sol"; +import "./interfaces/IOwnable.sol"; + +import "./libraries/Liquidity.sol"; +import "./libraries/Point.sol"; +import "./libraries/PointBitmap.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LimitOrder.sol"; +import "./libraries/AmountMath.sol"; +import "./libraries/UserEarn.sol"; +import "./libraries/TokenTransfer.sol"; +import "./libraries/State.sol"; +import "./libraries/Oracle.sol"; +import "./libraries/OrderOrEndpoint.sol"; +import "./libraries/SwapMathY2X.sol"; +import "./libraries/SwapMathX2Y.sol"; + +contract iZiSwapPool is IiZiSwapPool { + + using Liquidity for mapping(bytes32 =>Liquidity.Data); + using Liquidity for Liquidity.Data; + using Point for mapping(int24 =>Point.Data); + using Point for Point.Data; + using PointBitmap for mapping(int16 =>uint256); + using LimitOrder for LimitOrder.Data; + using UserEarn for UserEarn.Data; + using UserEarn for mapping(bytes32 =>UserEarn.Data); + using Oracle for Oracle.Observation[65535]; + using OrderOrEndpoint for mapping(int24 =>int24); + + int24 internal constant LEFT_MOST_PT = -800000; + int24 internal constant RIGHT_MOST_PT = 800000; + + /// @notice left most point regularized by pointDelta + int24 public leftMostPt; + /// @notice right most point regularized by pointDelta + int24 public rightMostPt; + /// @notice maximum liquidSum for each point, see points() in IiZiSwapPool or library Point + uint128 public maxLiquidPt; + + /// @notice address of iZiSwapFactory + address public factory; + + /// @notice address of tokenX + address public tokenX; + /// @notice address of tokenY + address public tokenY; + /// @notice fee amount of this swap pool, 3000 means 0.3% + uint24 public fee; + + /// @notice minimum number of distance between initialized or limitorder points + int24 public pointDelta; + + /// @notice the fee growth as a 128-bit fixpoing fees of tokenX collected per 1 liquidity of the pool + uint256 public feeScaleX_128; + /// @notice the fee growth as a 128-bit fixpoint fees of tokenY collected per 1 liquidity of the pool + uint256 public feeScaleY_128; + + /// @notice sqrt(1.0001), 96 bit fixpoint number + uint160 public override sqrtRate_96; + + /// @notice state of pool, see library State or IiZiSwapPool#state for more infomation + State public override state; + + /// @notice the information about a liquidity by the liquidity's key + mapping(bytes32 =>Liquidity.Data) public override liquidity; + + /// @notice 256 packed point (orderOrEndpoint>0) boolean values. See PointBitmap for more information + mapping(int16 =>uint256) public override pointBitmap; + + /// @notice returns infomation of a point in the pool, see Point library of IiZiSwapPool#poitns for more information + mapping(int24 =>Point.Data) public override points; + /// @notice infomation about a point whether has limit order and whether as an liquidity's endpoint + mapping(int24 =>int24) public override orderOrEndpoint; + /// @notice limitOrder info on a given point + mapping(int24 =>LimitOrder.Data) public override limitOrderData; + /// @notice information about a user's limit order (sell tokenY and earn tokenX) + mapping(bytes32 => UserEarn.Data) public override userEarnX; + /// @notice information about a user's limit order (sell tokenX and earn tokenY) + mapping(bytes32 => UserEarn.Data) public override userEarnY; + + /// @notice observation data array + Oracle.Observation[65535] public override observations; + + uint256 public override totalFeeXCharged; + uint256 public override totalFeeYCharged; + + address private original; + + address private swapModuleX2Y; + address private swapModuleY2X; + address private liquidityModule; + address private limitOrderModule; + address private flashModule; + + /// @notice percent to charge from miner's fee + uint24 public override feeChargePercent; + + modifier lock() { + require(!state.locked, 'LKD'); + state.locked = true; + _; + state.locked = false; + } + + modifier noDelegateCall() { + require(address(this) == original); + _; + } + + function _setRange(int24 pd) private { + rightMostPt = RIGHT_MOST_PT / pd * pd; + leftMostPt = - rightMostPt; + uint24 pointNum = uint24((rightMostPt - leftMostPt) / pd) + 1; + maxLiquidPt = type(uint128).max / pointNum; + } + + /// @notice Construct a pool + constructor() { + (address _tokenX, address _tokenY, uint24 _fee, int24 currentPoint, int24 _pointDelta, uint24 _feeChargePercent) = IiZiSwapFactory(msg.sender).deployPoolParams(); + require(_tokenX < _tokenY, 'x 0, 'pd0'); + original = address(this); + factory = msg.sender; + swapModuleX2Y = IiZiSwapFactory(msg.sender).swapX2YModule(); + swapModuleY2X = IiZiSwapFactory(msg.sender).swapY2XModule(); + liquidityModule = IiZiSwapFactory(msg.sender).liquidityModule(); + limitOrderModule = IiZiSwapFactory(msg.sender).limitOrderModule(); + flashModule = IiZiSwapFactory(msg.sender).flashModule(); + + tokenX = _tokenX; + tokenY = _tokenY; + fee = _fee; + pointDelta = _pointDelta; + _setRange(_pointDelta); + + require(currentPoint >= leftMostPt, "LO"); + require(currentPoint <= rightMostPt, "HO"); + + // current state + state.currentPoint = currentPoint; + state.sqrtPrice_96 = LogPowMath.getSqrtPrice(currentPoint); + state.liquidity = 0; + state.liquidityX = 0; + + sqrtRate_96 = LogPowMath.getSqrtPrice(1); + + (state.observationQueueLen, state.observationNextQueueLen) = observations.init(uint32(block.timestamp)); + state.observationCurrentIndex = 0; + feeChargePercent = _feeChargePercent; + } + + function balanceX() private view returns (uint256) { + (bool success, bytes memory data) = + tokenX.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + function balanceY() private view returns (uint256) { + (bool success, bytes memory data) = + tokenY.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + function revertDCData(bytes memory data) private pure { + if (data.length != 64) { + if (data.length < 68) revert('dc'); + assembly { + data := add(data, 0x04) + } + revert(abi.decode(data, (string))); + } + assembly { + data:= add(data, 0x20) + let w := mload(data) + let t := mload(0x40) + mstore(t, w) + let w2 := mload(add(data, 0x20)) + mstore(add(t, 0x20), w2) + revert(t, 64) + } + } + + /// @inheritdoc IiZiSwapPool + function assignLimOrderEarnY( + int24 point, + uint128 assignY, + bool fromLegacy + ) external override noDelegateCall lock returns (uint128 actualAssignY) { + + (bool success, bytes memory d) = limitOrderModule.delegatecall( + abi.encodeWithSignature("assignLimOrderEarnY(int24,uint128,bool)", point, assignY, fromLegacy) + ); + if (success) { + actualAssignY = abi.decode(d, (uint128)); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function assignLimOrderEarnX( + int24 point, + uint128 assignX, + bool fromLegacy + ) external override noDelegateCall lock returns (uint128 actualAssignX) { + + (bool success, bytes memory d) = limitOrderModule.delegatecall( + abi.encodeWithSignature("assignLimOrderEarnX(int24,uint128,bool)", point, assignX, fromLegacy) + ); + if (success) { + actualAssignX = abi.decode(d, (uint128)); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function decLimOrderWithX( + int24 point, + uint128 deltaX + ) external override noDelegateCall lock returns (uint128 actualDeltaX, uint256 legacyAccEarn) { + + (bool success, bytes memory d) = limitOrderModule.delegatecall( + abi.encodeWithSignature("decLimOrderWithX(int24,uint128)", point, deltaX) + ); + if (success) { + uint128 claimSold; + uint128 claimEarn; + (actualDeltaX, legacyAccEarn, claimSold, claimEarn) = abi.decode(d, (uint128, uint256, uint128, uint128)); + emit DecLimitOrder(msg.sender, actualDeltaX, point, claimSold, claimEarn, true); + } else { + revertDCData(d); + } + + } + + /// @inheritdoc IiZiSwapPool + function decLimOrderWithY( + int24 point, + uint128 deltaY + ) external override noDelegateCall lock returns (uint128 actualDeltaY, uint256 legacyAccEarn) { + + (bool success, bytes memory d) = limitOrderModule.delegatecall( + abi.encodeWithSignature("decLimOrderWithY(int24,uint128)", point, deltaY) + ); + if (success) { + uint128 claimSold; + uint128 claimEarn; + (actualDeltaY, legacyAccEarn, claimSold, claimEarn) = abi.decode(d, (uint128, uint256, uint128, uint128)); + emit DecLimitOrder(msg.sender, actualDeltaY, point, claimSold, claimEarn, false); + } else { + revertDCData(d); + } + + } + + /// @inheritdoc IiZiSwapPool + function addLimOrderWithX( + address recipient, + int24 point, + uint128 amountX, + bytes calldata data + ) external override noDelegateCall lock returns (uint128 orderX, uint128 acquireY) { + + (bool success, bytes memory d) = limitOrderModule.delegatecall( + abi.encodeWithSignature("addLimOrderWithX(address,int24,uint128,bytes)", recipient, point, amountX, data) + ); + if (success) { + uint128 claimSold; + uint128 claimEarn; + (orderX, acquireY, claimSold, claimEarn) = abi.decode(d, (uint128, uint128, uint128, uint128)); + emit AddLimitOrder(recipient, orderX, acquireY, point, claimSold, claimEarn, true); + } else { + revertDCData(d); + } + + } + + /// @inheritdoc IiZiSwapPool + function addLimOrderWithY( + address recipient, + int24 point, + uint128 amountY, + bytes calldata data + ) external override noDelegateCall lock returns (uint128 orderY, uint128 acquireX) { + + (bool success, bytes memory d) = limitOrderModule.delegatecall( + abi.encodeWithSignature("addLimOrderWithY(address,int24,uint128,bytes)", recipient, point, amountY, data) + ); + if (success) { + uint128 claimSold; + uint128 claimEarn; + (orderY, acquireX, claimSold, claimEarn) = abi.decode(d, (uint128, uint128, uint128, uint128)); + emit AddLimitOrder(recipient, orderY, acquireX, point, claimSold, claimEarn, false); + } else { + revertDCData(d); + } + + } + + /// @inheritdoc IiZiSwapPool + function collectLimOrder( + address recipient, int24 point, uint128 collectDec, uint128 collectEarn, bool isEarnY + ) external override noDelegateCall lock returns(uint128 actualCollectDec, uint128 actualCollectEarn) { + (bool success, bytes memory d) = limitOrderModule.delegatecall( + abi.encodeWithSignature("collectLimOrder(address,int24,uint128,uint128,bool)", recipient, point, collectDec, collectEarn, isEarnY) + ); + if (success) { + (actualCollectDec, actualCollectEarn) = abi.decode(d, (uint128, uint128)); + emit CollectLimitOrder(msg.sender, recipient, point, actualCollectDec, actualCollectEarn, isEarnY); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function mint( + address recipient, + int24 leftPt, + int24 rightPt, + uint128 liquidDelta, + bytes calldata data + ) external override noDelegateCall lock returns (uint256 amountX, uint256 amountY) { + (bool success, bytes memory d) = liquidityModule.delegatecall( + abi.encodeWithSignature("mint(address,int24,int24,uint128,bytes)", recipient, leftPt, rightPt,liquidDelta,data) + ); + if (success) { + (amountX, amountY) = abi.decode(d, (uint256, uint256)); + emit Mint(msg.sender, recipient, leftPt, rightPt, liquidDelta, amountX, amountY); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function burn( + int24 leftPt, + int24 rightPt, + uint128 liquidDelta + ) external override noDelegateCall lock returns (uint256 amountX, uint256 amountY) { + (bool success, bytes memory d) = liquidityModule.delegatecall( + abi.encodeWithSignature("burn(int24,int24,uint128)", leftPt, rightPt, liquidDelta) + ); + if (success) { + (amountX, amountY) = abi.decode(d, (uint256, uint256)); + emit Burn(msg.sender, leftPt, rightPt, liquidDelta, amountX, amountY); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function collect( + address recipient, + int24 leftPt, + int24 rightPt, + uint256 amountXLim, + uint256 amountYLim + ) external override noDelegateCall lock returns (uint256 actualAmountX, uint256 actualAmountY) { + (bool success, bytes memory d) = liquidityModule.delegatecall( + abi.encodeWithSignature("collect(address,int24,int24,uint256,uint256)", recipient, leftPt, rightPt, amountXLim, amountYLim) + ); + if (success) { + (actualAmountX, actualAmountY) = abi.decode(d, (uint256, uint256)); + emit CollectLiquidity(msg.sender, recipient, leftPt, rightPt, actualAmountX, actualAmountY); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function swapY2X( + address recipient, + uint128 amount, + int24 highPt, + bytes calldata data + ) external override noDelegateCall lock returns (uint256 amountX, uint256 amountY) { + (bool success, bytes memory d) = swapModuleY2X.delegatecall( + abi.encodeWithSignature("swapY2X(address,uint128,int24,bytes)", + recipient, amount, highPt, data) + ); + if (success) { + (amountX, amountY) = abi.decode(d, (uint256, uint256)); + emit Swap(tokenX, tokenY, fee, false, amountX, amountY, state.currentPoint); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function swapY2XDesireX( + address recipient, + uint128 desireX, + int24 highPt, + bytes calldata data + ) external override noDelegateCall lock returns (uint256 amountX, uint256 amountY) { + (bool success, bytes memory d) = swapModuleY2X.delegatecall( + abi.encodeWithSignature("swapY2XDesireX(address,uint128,int24,bytes)", + recipient, desireX, highPt, data) + ); + if (success) { + (amountX, amountY) = abi.decode(d, (uint256, uint256)); + emit Swap(tokenX, tokenY, fee, false, amountX, amountY, state.currentPoint); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function swapX2Y( + address recipient, + uint128 amount, + int24 lowPt, + bytes calldata data + ) external override noDelegateCall lock returns (uint256 amountX, uint256 amountY) { + (bool success, bytes memory d) = swapModuleX2Y.delegatecall( + abi.encodeWithSignature("swapX2Y(address,uint128,int24,bytes)", + recipient, amount, lowPt, data) + ); + if (success) { + (amountX, amountY) = abi.decode(d, (uint256, uint256)); + emit Swap(tokenX, tokenY, fee, true, amountX, amountY, state.currentPoint); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function swapX2YDesireY( + address recipient, + uint128 desireY, + int24 lowPt, + bytes calldata data + ) external override noDelegateCall lock returns (uint256 amountX, uint256 amountY) { + (bool success, bytes memory d) = swapModuleX2Y.delegatecall( + abi.encodeWithSignature("swapX2YDesireY(address,uint128,int24,bytes)", recipient, desireY, lowPt,data) + ); + if (success) { + (amountX, amountY) = abi.decode(d, (uint256, uint256)); + emit Swap(tokenX, tokenY, fee, true, amountX, amountY, state.currentPoint); + } else { + revertDCData(d); + } + } + + /// @inheritdoc IiZiSwapPool + function observe(uint32[] calldata secondsAgos) + external + view + override + noDelegateCall + returns (int56[] memory accPoints) + { + return + observations.observe( + uint32(block.timestamp), + secondsAgos, + state.currentPoint, + state.observationCurrentIndex, + state.observationQueueLen + ); + } + + /// @inheritdoc IiZiSwapPool + function expandObservationQueue(uint16 newNextQueueLen) external override noDelegateCall lock { + uint16 oldNextQueueLen = state.observationNextQueueLen; + if (newNextQueueLen > oldNextQueueLen) { + observations.expand(oldNextQueueLen, newNextQueueLen); + state.observationNextQueueLen = newNextQueueLen; + } + } + + /// @inheritdoc IiZiSwapPool + function liquiditySnapshot(int24 leftPoint, int24 rightPoint) external override view returns(int128[] memory deltaLiquidities) { + require(leftPoint < rightPoint, "L= leftMostPt, "LO"); + require(rightPoint <= rightMostPt, "RO"); + require(leftPoint % pointDelta == 0, "LD0"); + require(rightPoint % pointDelta == 0, "RD0"); + uint256 len = uint256(int256((rightPoint - leftPoint) / pointDelta)); + deltaLiquidities = new int128[](len); + uint256 idx = 0; + for (int24 i = leftPoint; i < rightPoint; i += pointDelta) { + deltaLiquidities[idx] = points[i].liquidDelta; + idx ++; + } + } + + /// @inheritdoc IiZiSwapPool + function limitOrderSnapshot(int24 leftPoint, int24 rightPoint) external override view returns(LimitOrderStruct[] memory limitOrders) { + require(leftPoint < rightPoint, "L= leftMostPt, "LO"); + require(rightPoint <= rightMostPt, "RO"); + require(leftPoint % pointDelta == 0, "LD0"); + require(rightPoint % pointDelta == 0, "RD0"); + uint256 len = uint256(int256((rightPoint - leftPoint) / pointDelta)); + limitOrders = new LimitOrderStruct[](len); + uint256 idx = 0; + for (int24 i = leftPoint; i < rightPoint; i += pointDelta) { + limitOrders[idx] = LimitOrderStruct({ + sellingX: limitOrderData[i].sellingX, + earnY: limitOrderData[i].earnY, + accEarnY: limitOrderData[i].accEarnY, + sellingY: limitOrderData[i].sellingY, + earnX: limitOrderData[i].earnX, + accEarnX: limitOrderData[i].accEarnX + }); + idx ++; + } + } + + /// @inheritdoc IiZiSwapPool + function collectFeeCharged() external override noDelegateCall lock { + require(msg.sender == IiZiSwapFactory(factory).chargeReceiver(), "NR"); + TokenTransfer.transferToken(tokenX, msg.sender, totalFeeXCharged); + TokenTransfer.transferToken(tokenY, msg.sender, totalFeeYCharged); + totalFeeXCharged = 0; + totalFeeYCharged = 0; + } + + /// @inheritdoc IiZiSwapPool + function modifyFeeChargePercent(uint24 newFeeChargePercent) external override noDelegateCall lock { + require(msg.sender == IOwnable(factory).owner(), "NON"); + require(newFeeChargePercent >= 0, "FP0"); + require(newFeeChargePercent <= 100, "FP0"); + feeChargePercent = newFeeChargePercent; + } + + /// @inheritdoc IiZiSwapPool + function flash( + address recipient, + uint256 amountX, + uint256 amountY, + bytes calldata data + ) external override noDelegateCall lock { + (bool success, bytes memory d) = flashModule.delegatecall( + abi.encodeWithSignature("flash(address,uint256,uint256,bytes)", + recipient, amountX, amountY, data) + ); + if (success) { + (uint256 actualAmountX, uint256 actualAmountY, uint256 paidX, uint256 paidY) = abi.decode(d, (uint256, uint256, uint256, uint256)); + emit Flash(msg.sender, recipient, actualAmountX, actualAmountY, paidX, paidY); + } else { + revertDCData(d); + } + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/interfaces/IERC20Minimal.sol b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IERC20Minimal.sol new file mode 100644 index 000000000..e2e333e41 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IERC20Minimal.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.4; + +/// @title Minimal ERC20 interface for Uniswap +/// @notice Contains a subset of the full ERC20 interface that is used in Uniswap V3 +interface IERC20Minimal { + /// @notice Returns the balance of a token + /// @param account The account for which to look up the number of tokens it has, i.e. its balance + /// @return The number of tokens held by the account + function balanceOf(address account) external view returns (uint256); + + /// @notice Transfers the amount of token from the `msg.sender` to the recipient + /// @param recipient The account that will receive the amount transferred + /// @param amount The number of tokens to send from the sender to the recipient + /// @return Returns true for a successful transfer, false for an unsuccessful transfer + function transfer(address recipient, uint256 amount) external returns (bool); + + /// @notice Returns the current allowance given to a spender by an owner + /// @param owner The account of the token owner + /// @param spender The account of the token spender + /// @return The current allowance granted by `owner` to `spender` + function allowance(address owner, address spender) external view returns (uint256); + + /// @notice Sets the allowance of a spender from the `msg.sender` to the value `amount` + /// @param spender The account which will be allowed to spend a given amount of the owners tokens + /// @param amount The amount of tokens allowed to be used by `spender` + /// @return Returns true for a successful approval, false for unsuccessful + function approve(address spender, uint256 amount) external returns (bool); + + /// @notice Transfers `amount` tokens from `sender` to `recipient` up to the allowance given to the `msg.sender` + /// @param sender The account from which the transfer will be initiated + /// @param recipient The recipient of the transfer + /// @param amount The amount of the transfer + /// @return Returns true for a successful transfer, false for unsuccessful + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /// @notice Event emitted when tokens are transferred from one address to another, either via `#transfer` or `#transferFrom`. + /// @param from The account from which the tokens were sent, i.e. the balance decreased + /// @param to The account to which the tokens were sent, i.e. the balance increased + /// @param value The amount of tokens that were transferred + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @notice Event emitted when the approval amount for the spender of a given owner's tokens changes. + /// @param owner The account that approved spending of its tokens + /// @param spender The account for which the spending allowance was modified + /// @param value The new allowance from the owner to the spender + event Approval(address indexed owner, address indexed spender, uint256 value); + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/interfaces/IOwnable.sol b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IOwnable.sol new file mode 100644 index 000000000..971a47441 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IOwnable.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IOwnable { + /** + * @dev Returns the address of the current owner. + */ + function owner() external view returns (address); +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapCallback.sol b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapCallback.sol new file mode 100644 index 000000000..f194b1bd6 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapCallback.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IiZiSwapMintCallback { + + /// @notice Called to msg.sender in iZiSwapPool#mint call + /// @param x Amount of tokenX need to pay from miner + /// @param y Amount of tokenY need to pay from miner + /// @param data Any data passed through by the msg.sender via the iZiSwapPool#mint call + function mintDepositCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + +} + +interface IiZiSwapCallback { + + /// @notice Called to msg.sender in iZiSwapPool#swapY2X(DesireX) call + /// @param x Amount of tokenX trader will acquire + /// @param y Amount of tokenY trader will pay + /// @param data Any dadta passed though by the msg.sender via the iZiSwapPool#swapY2X(DesireX) call + function swapY2XCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + + /// @notice Called to msg.sender in iZiSwapPool#swapX2Y(DesireY) call + /// @param x Amount of tokenX trader will pay + /// @param y Amount of tokenY trader will require + /// @param data Any dadta passed though by the msg.sender via the iZiSwapPool#swapX2Y(DesireY) call + function swapX2YCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + +} + +interface IiZiSwapAddLimOrderCallback { + + /// @notice Called to msg.sender in iZiSwapPool#addLimOrderWithX(Y) call + /// @param x Amount of tokenX seller will pay + /// @param y Amount of tokenY seller will pay + /// @param data Any dadta passed though by the msg.sender via the iZiSwapPool#addLimOrderWithX(Y) call + function payCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapFactory.sol b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapFactory.sol new file mode 100644 index 000000000..c4fdcfaa3 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapFactory.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IiZiSwapFactory { + + /// @notice emit when successfuly create a new pool (calling iZiSwapFactory#newPool) + /// @param tokenX address of erc-20 tokenX + /// @param tokenY address of erc-20 tokenY + /// @param fee fee amount of swap (3000 means 0.3%) + /// @param pointDelta minimum number of distance between initialized or limitorder points + /// @param pool address of swap pool + event NewPool( + address indexed tokenX, + address indexed tokenY, + uint24 indexed fee, + uint24 pointDelta, + address pool + ); + + /// @notice module to support swap from tokenX to tokenY + /// @return swapX2YModule address + function swapX2YModule() external returns (address); + + /// @notice module to support swap from tokenY to tokenX + /// @return swapY2XModule address + function swapY2XModule() external returns (address); + + /// @notice module to support mint/burn/collect function of pool + /// @return liquidityModule address + function liquidityModule() external returns (address); + + /// @notice address of module for user to manage limit orders + /// @return limitOrderModule address + function limitOrderModule() external returns (address); + + /// @notice address of module for flash loan + /// @return flashModule address + function flashModule() external returns (address); + + /// @notice default fee rate from miner's fee gain + /// @return defaultFeeChargePercent default fee rate * 100 + function defaultFeeChargePercent() external returns (uint24); + + /// @notice Enables a fee amount with the given pointDelta + /// @dev Fee amounts may never be removed once enabled + /// @param fee fee amount (3000 means 0.3%) + /// @param pointDelta The spacing between points to be enforced for all pools created with the given fee amount + function enableFeeAmount(uint24 fee, uint24 pointDelta) external; + + /// @notice Create a new pool which not exists. + /// @param tokenX address of tokenX + /// @param tokenY address of tokenY + /// @param fee fee amount + /// @param currentPoint initial point (log 1.0001 of price) + /// @return address of newly created pool + function newPool( + address tokenX, + address tokenY, + uint24 fee, + int24 currentPoint + ) external returns (address); + + /// @notice Charge receiver of all pools. + /// @return address of charge receiver + function chargeReceiver() external view returns(address); + + /// @notice Get pool of (tokenX, tokenY, fee), address(0) for not exists. + /// @param tokenX address of tokenX + /// @param tokenY address of tokenY + /// @param fee fee amount + /// @return address of pool + function pool( + address tokenX, + address tokenY, + uint24 fee + ) external view returns(address); + + /// @notice Get point delta of a given fee amount. + /// @param fee fee amount + /// @return pointDelta the point delta + function fee2pointDelta(uint24 fee) external view returns (int24 pointDelta); + + /// @notice Change charge receiver, only owner of factory can call. + /// @param _chargeReceiver address of new receiver + function modifyChargeReceiver(address _chargeReceiver) external; + + /// @notice Change defaultFeeChargePercent + /// @param _defaultFeeChargePercent new charge percent + function modifyDefaultFeeChargePercent(uint24 _defaultFeeChargePercent) external; + + function deployPoolParams() external view returns( + address tokenX, + address tokenY, + uint24 fee, + int24 currentPoint, + int24 pointDelta, + uint24 feeChargePercent + ); + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapFlashCallback.sol b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapFlashCallback.sol new file mode 100644 index 000000000..b14101909 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapFlashCallback.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface IiZiSwapFlashCallback { + + /// @notice Call the msg.sender after sending tokens in IiZiSwapPool#flash. + /// @dev Must repay the tokens to the pool within one call. + /// @param feeX the fee amount in tokenX due to the pool by the end of the flash + /// @param feeY the fee amount in tokenY due to the pool by the end of the flash + /// @param data any data passed through by the caller + function flashCallback( + uint256 feeX, + uint256 feeY, + bytes calldata data + ) external; + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapPool.sol b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapPool.sol new file mode 100644 index 000000000..794cd14b9 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapPool.sol @@ -0,0 +1,545 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IiZiSwapPool { + + /// @notice Emitted when miner successfully add liquidity (mint). + /// @param sender the address that minted the liquidity + /// @param owner the owner who will benefit from this liquidity + /// @param leftPoint left endpoint of the liquidity + /// @param rightPoint right endpoint of the liquidity + /// @param liquidity the amount of liquidity minted to the range [leftPoint, rightPoint) + /// @param amountX amount of tokenX deposit + /// @param amountY amount of tokenY deposit + event Mint( + address sender, + address indexed owner, + int24 indexed leftPoint, + int24 indexed rightPoint, + uint128 liquidity, + uint256 amountX, + uint256 amountY + ); + + /// @notice Emitted when miner successfully decrease liquidity (withdraw). + /// @param owner owner address of liquidity + /// @param leftPoint left endpoint of liquidity + /// @param rightPoint right endpoint of liquidity + /// @param liquidity amount of liquidity decreased + /// @param amountX amount of tokenX withdrawed + /// @param amountY amount of tokenY withdrawed + event Burn( + address indexed owner, + int24 indexed leftPoint, + int24 indexed rightPoint, + uint128 liquidity, + uint256 amountX, + uint256 amountY + ); + + /// @notice Emitted when fees and withdrawed liquidity are collected + /// @param owner The owner of the Liquidity + /// @param recipient recipient of those token + /// @param leftPoint The left point of the liquidity + /// @param rightPoint The right point of the liquidity + /// @param amountX The amount of tokenX (fees and withdrawed tokenX from liquidity) + /// @param amountY The amount of tokenY (fees and withdrawed tokenY from liquidity) + event CollectLiquidity( + address indexed owner, + address recipient, + int24 indexed leftPoint, + int24 indexed rightPoint, + uint256 amountX, + uint256 amountY + ); + + /// @notice Emitted when a trader successfully exchange. + /// @param tokenX tokenX of pool + /// @param tokenY tokenY of pool + /// @param fee fee amount of pool + /// @param sellXEarnY true for selling tokenX, false for buying tokenX + /// @param amountX amount of tokenX in this exchange + /// @param amountY amount of tokenY in this exchange + /// @param currentPoint current point after swap + event Swap( + address indexed tokenX, + address indexed tokenY, + uint24 indexed fee, + bool sellXEarnY, + uint256 amountX, + uint256 amountY, + int24 currentPoint + ); + + /// @notice Emitted by the pool for any flashes of tokenX/tokenY. + /// @param sender the address that initiated the swap call, and that received the callback + /// @param recipient the address that received the tokens from flash + /// @param amountX the amount of tokenX that was flashed + /// @param amountY the amount of tokenY that was flashed + /// @param paidX the amount of tokenX paid for the flash, which can exceed the amountX plus the fee + /// @param paidY the amount of tokenY paid for the flash, which can exceed the amountY plus the fee + event Flash( + address indexed sender, + address indexed recipient, + uint256 amountX, + uint256 amountY, + uint256 paidX, + uint256 paidY + ); + + /// @notice Emitted when a seller successfully add a limit order. + /// @param owner owner of limit order + /// @param addAmount amount of token to sell the seller added + /// @param acquireAmount amount of earn-token acquired, if there exists some opposite order before + /// @param point point of limit order + /// @param claimSold claimed sold sell-token, if this owner has order with same direction on this point before + /// @param claimEarn claimed earned earn-token, if this owner has order with same direction on this point before + /// @param sellXEarnY direction of limit order, etc. sell tokenX or sell tokenY + event AddLimitOrder( + address indexed owner, + uint128 addAmount, + uint128 acquireAmount, + int24 indexed point, + uint128 claimSold, + uint128 claimEarn, + bool sellXEarnY + ); + + /// @notice Emitted when a seller successfully decrease a limit order. + /// @param owner owner of limit order + /// @param decreaseAmount amount of token to sell the seller decreased + /// @param point point of limit order + /// @param claimSold claimed sold sell-token + /// @param claimEarn claimed earned earn-token + /// @param sellXEarnY direction of limit order, etc. sell tokenX or sell tokenY + event DecLimitOrder( + address indexed owner, + uint128 decreaseAmount, + int24 indexed point, + uint128 claimSold, + uint128 claimEarn, + bool sellXEarnY + ); + + /// @notice Emitted when collect from a limit order + /// @param owner The owner of the Liquidity + /// @param recipient recipient of those token + /// @param point The point of the limit order + /// @param collectDec The amount of decreased sell token collected + /// @param collectEarn The amount of earn token collected + /// @param sellXEarnY direction of limit order, etc. sell tokenX or sell tokenY + event CollectLimitOrder( + address indexed owner, + address recipient, + int24 indexed point, + uint128 collectDec, + uint128 collectEarn, + bool sellXEarnY + ); + + /// @notice Returns the information about a liquidity by the liquidity's key. + /// @param key the liquidity's key is a hash of a preimage composed by the miner(owner), pointLeft and pointRight + /// @return liquidity the amount of liquidity, + /// @return lastFeeScaleX_128 fee growth of tokenX inside the range as of the last mint/burn/collect, + /// @return lastFeeScaleY_128 fee growth of tokenY inside the range as of the last mint/burn/collect, + /// @return tokenOwedX the computed amount of tokenX miner can collect as of the last mint/burn/collect, + /// @return tokenOwedY the computed amount of tokenY miner can collect as of the last mint/burn/collect + function liquidity(bytes32 key) + external + view + returns ( + uint128 liquidity, + uint256 lastFeeScaleX_128, + uint256 lastFeeScaleY_128, + uint256 tokenOwedX, + uint256 tokenOwedY + ); + + /// @notice Returns the information about a user's limit order (sell tokenY and earn tokenX). + /// @param key the limit order's key is a hash of a preimage composed by the seller, point + /// @return lastAccEarn total amount of tokenX earned by all users at this point as of the last add/dec/collect + /// @return sellingRemain amount of tokenY not selled in this limit order + /// @return sellingDec amount of tokenY decreased by seller from this limit order + /// @return earn amount of unlegacy earned tokenX in this limit order not assigned + /// @return legacyEarn amount of legacy earned tokenX in this limit order not assgined + /// @return earnAssign assigned amount of tokenX earned (both legacy and unlegacy) in this limit order + function userEarnX(bytes32 key) + external + view + returns ( + uint256 lastAccEarn, + uint128 sellingRemain, + uint128 sellingDec, + uint128 earn, + uint128 legacyEarn, + uint128 earnAssign + ); + + /// @notice Returns the information about a user's limit order (sell tokenX and earn tokenY). + /// @param key the limit order's key is a hash of a preimage composed by the seller, point + /// @return lastAccEarn total amount of tokenY earned by all users at this point as of the last add/dec/collect + /// @return sellingRemain amount of tokenX not selled in this limit order + /// @return sellingDec amount of tokenX decreased by seller from this limit order + /// @return earn amount of unlegacy earned tokenY in this limit order not assigned + /// @return legacyEarn amount of legacy earned tokenY in this limit order not assgined + /// @return earnAssign assigned amount of tokenY earned (both legacy and unlegacy) in this limit order + function userEarnY(bytes32 key) + external + view + returns ( + uint256 lastAccEarn, + uint128 sellingRemain, + uint128 sellingDec, + uint128 earn, + uint128 legacyEarn, + uint128 earnAssign + ); + + /// @notice Mark a given amount of tokenY in a limitorder(sellx and earn y) as assigned. + /// @param point point (log Price) of seller's limit order,be sure to be times of pointDelta + /// @param assignY max amount of tokenY to mark assigned + /// @param fromLegacy true for assigning earned token from legacyEarnY + /// @return actualAssignY actual amount of tokenY marked + function assignLimOrderEarnY( + int24 point, + uint128 assignY, + bool fromLegacy + ) external returns(uint128 actualAssignY); + + /// @notice Mark a given amount of tokenX in a limitorder(selly and earn x) as assigned. + /// @param point point (log Price) of seller's limit order,be sure to be times of pointDelta + /// @param assignX max amount of tokenX to mark assigned + /// @param fromLegacy true for assigning earned token from legacyEarnX + /// @return actualAssignX actual amount of tokenX marked + function assignLimOrderEarnX( + int24 point, + uint128 assignX, + bool fromLegacy + ) external returns(uint128 actualAssignX); + + /// @notice Decrease limitorder of selling X. + /// @param point point of seller's limit order, be sure to be times of pointDelta + /// @param deltaX max amount of tokenX seller wants to decrease + /// @return actualDeltaX actual amount of tokenX decreased + /// @return legacyAccEarn legacyAccEarnY of pointOrder at point when calling this interface + function decLimOrderWithX( + int24 point, + uint128 deltaX + ) external returns (uint128 actualDeltaX, uint256 legacyAccEarn); + + /// @notice Decrease limitorder of selling Y. + /// @param point point of seller's limit order, be sure to be times of pointDelta + /// @param deltaY max amount of tokenY seller wants to decrease + /// @return actualDeltaY actual amount of tokenY decreased + /// @return legacyAccEarn legacyAccEarnX of pointOrder at point when calling this interface + function decLimOrderWithY( + int24 point, + uint128 deltaY + ) external returns (uint128 actualDeltaY, uint256 legacyAccEarn); + + /// @notice Add a limit order (selling x) in the pool. + /// @param recipient owner of the limit order + /// @param point point of the order, be sure to be times of pointDelta + /// @param amountX amount of tokenX to sell + /// @param data any data that should be passed through to the callback + /// @return orderX actual added amount of tokenX + /// @return acquireY amount of tokenY acquired if there is a limit order to sell y before adding + function addLimOrderWithX( + address recipient, + int24 point, + uint128 amountX, + bytes calldata data + ) external returns (uint128 orderX, uint128 acquireY); + + /// @notice Add a limit order (selling y) in the pool. + /// @param recipient owner of the limit order + /// @param point point of the order, be sure to be times of pointDelta + /// @param amountY amount of tokenY to sell + /// @param data any data that should be passed through to the callback + /// @return orderY actual added amount of tokenY + /// @return acquireX amount of tokenX acquired if there exists a limit order to sell x before adding + function addLimOrderWithY( + address recipient, + int24 point, + uint128 amountY, + bytes calldata data + ) external returns (uint128 orderY, uint128 acquireX); + + /// @notice Collect earned or decreased token from limit order. + /// @param recipient address to benefit + /// @param point point of limit order, be sure to be times of pointDelta + /// @param collectDec max amount of decreased selling token to collect + /// @param collectEarn max amount of earned token to collect + /// @param isEarnY direction of this limit order, true for sell y, false for sell x + /// @return actualCollectDec actual amount of decresed selling token collected + /// @return actualCollectEarn actual amount of earned token collected + function collectLimOrder( + address recipient, int24 point, uint128 collectDec, uint128 collectEarn, bool isEarnY + ) external returns(uint128 actualCollectDec, uint128 actualCollectEarn); + + /// @notice Add liquidity to the pool. + /// @param recipient newly created liquidity will belong to this address + /// @param leftPt left endpoint of the liquidity, be sure to be times of pointDelta + /// @param rightPt right endpoint of the liquidity, be sure to be times of pointDelta + /// @param liquidDelta amount of liquidity to add + /// @param data any data that should be passed through to the callback + /// @return amountX The amount of tokenX that was paid for the liquidity. Matches the value in the callback + /// @return amountY The amount of tokenY that was paid for the liquidity. Matches the value in the callback + function mint( + address recipient, + int24 leftPt, + int24 rightPt, + uint128 liquidDelta, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Decrease a given amount of liquidity from msg.sender's liquidities. + /// @param leftPt left endpoint of the liquidity + /// @param rightPt right endpoint of the liquidity + /// @param liquidDelta amount of liquidity to burn + /// @return amountX The amount of tokenX should be refund after burn + /// @return amountY The amount of tokenY should be refund after burn + function burn( + int24 leftPt, + int24 rightPt, + uint128 liquidDelta + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Collect tokens (fee or refunded after burn) from a liquidity. + /// @param recipient the address which should receive the collected tokens + /// @param leftPt left endpoint of the liquidity + /// @param rightPt right endpoint of the liquidity + /// @param amountXLim max amount of tokenX the owner wants to collect + /// @param amountYLim max amount of tokenY the owner wants to collect + /// @return actualAmountX the amount tokenX collected + /// @return actualAmountY the amount tokenY collected + function collect( + address recipient, + int24 leftPt, + int24 rightPt, + uint256 amountXLim, + uint256 amountYLim + ) external returns (uint256 actualAmountX, uint256 actualAmountY); + + /// @notice Swap tokenY for tokenX, given max amount of tokenY user willing to pay. + /// @param recipient the address to receive tokenX + /// @param amount the max amount of tokenY user willing to pay + /// @param highPt the highest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX payed + /// @return amountY amount of tokenY acquired + function swapY2X( + address recipient, + uint128 amount, + int24 highPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Swap tokenY for tokenX, given amount of tokenX user desires. + /// @param recipient the address to receive tokenX + /// @param desireX the amount of tokenX user desires + /// @param highPt the highest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX payed + /// @return amountY amount of tokenY acquired + function swapY2XDesireX( + address recipient, + uint128 desireX, + int24 highPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Swap tokenX for tokenY, given max amount of tokenX user willing to pay. + /// @param recipient the address to receive tokenY + /// @param amount the max amount of tokenX user willing to pay + /// @param lowPt the lowest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX acquired + /// @return amountY amount of tokenY payed + function swapX2Y( + address recipient, + uint128 amount, + int24 lowPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Swap tokenX for tokenY, given amount of tokenY user desires. + /// @param recipient the address to receive tokenY + /// @param desireY the amount of tokenY user desires + /// @param lowPt the lowest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX acquired + /// @return amountY amount of tokenY payed + function swapX2YDesireY( + address recipient, + uint128 desireY, + int24 lowPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Returns sqrt(1.0001), in 96 bit fixpoint number. + function sqrtRate_96() external view returns(uint160); + + function feeScaleX_128() external view returns(uint256); + function feeScaleY_128() external view returns(uint256); + + /// @notice State values of pool. + /// @return sqrtPrice_96 a 96 fixpoing number describe the sqrt value of current price(tokenX/tokenY) + /// @return currentPoint the current point of the pool, 1.0001 ^ currentPoint = price + /// @return observationCurrentIndex the index of the last oracle observation that was written, + /// @return observationQueueLen the current maximum number of observations stored in the pool, + /// @return observationNextQueueLen the next maximum number of observations, to be updated when the observation. + /// @return locked whether the pool is locked (only used for checking reentrance) + /// @return liquidity liquidity on the currentPoint (currX * sqrtPrice + currY / sqrtPrice) + /// @return liquidityX liquidity of tokenX + function state() + external view + returns( + uint160 sqrtPrice_96, + int24 currentPoint, + uint16 observationCurrentIndex, + uint16 observationQueueLen, + uint16 observationNextQueueLen, + bool locked, + uint128 liquidity, + uint128 liquidityX + ); + + /// @notice LimitOrder info on a given point. + /// @param point the given point + /// @return sellingX total amount of tokenX selling on the point + /// @return earnY total amount of unclaimed earned tokenY for unlegacy sellingX + /// @return accEarnY total amount of earned tokenY(via selling tokenX) by all users at this point as of the last swap + /// @return legacyAccEarnY latest recorded 'accEarnY' value when sellingX is clear (legacy) + /// @return legacyEarnY total amount of unclaimed earned tokenY for legacy (cleared during swap) sellingX + /// @return sellingY total amount of tokenYselling on the point + /// @return earnX total amount of unclaimed earned tokenX for unlegacy sellingY + /// @return legacyEarnX total amount of unclaimed earned tokenX for legacy (cleared during swap) sellingY + /// @return accEarnX total amount of earned tokenX(via selling tokenY) by all users at this point as of the last swap + /// @return legacyAccEarnX latest recorded 'accEarnX' value when sellingY is clear (legacy) + function limitOrderData(int24 point) + external view + returns( + uint128 sellingX, + uint128 earnY, + uint256 accEarnY, + uint256 legacyAccEarnY, + uint128 legacyEarnY, + uint128 sellingY, + uint128 earnX, + uint128 legacyEarnX, + uint256 accEarnX, + uint256 legacyAccEarnX + ); + + /// @notice Query infomation about a point whether has limit order or is an liquidity's endpoint. + /// @param point point to query + /// @return val endpoint for val&1>0 and has limit order for val&2 > 0 + function orderOrEndpoint(int24 point) external returns(int24 val); + + /// @notice Returns observation data about a specific index. + /// @param index the index of observation array + /// @return timestamp the timestamp of the observation, + /// @return accPoint the point multiplied by seconds elapsed for the life of the pool as of the observation timestamp, + /// @return init whether the observation has been initialized and the above values are safe to use + function observations(uint256 index) + external + view + returns ( + uint32 timestamp, + int56 accPoint, + bool init + ); + + /// @notice Point status in the pool. + /// @param point the point + /// @return liquidSum the total amount of liquidity that uses the point either as left endpoint or right endpoint + /// @return liquidDelta how much liquidity changes when the pool price crosses the point from left to right + /// @return accFeeXOut_128 the fee growth on the other side of the point from the current point in tokenX + /// @return accFeeYOut_128 the fee growth on the other side of the point from the current point in tokenY + /// @return isEndpt whether the point is an endpoint of a some miner's liquidity, true if liquidSum > 0 + function points(int24 point) + external + view + returns ( + uint128 liquidSum, + int128 liquidDelta, + uint256 accFeeXOut_128, + uint256 accFeeYOut_128, + bool isEndpt + ); + + /// @notice Returns 256 packed point (statusVal>0) boolean values. See PointBitmap for more information. + function pointBitmap(int16 wordPosition) external view returns (uint256); + + /// @notice Returns the integral value of point(time) and integral value of 1/liquidity(time) + /// at some target timestamps (block.timestamp - secondsAgo[i]) + /// @dev Reverts if target timestamp is early than oldest observation in the queue + /// @dev If you call this method with secondsAgos = [3600, 0]. the average point of this pool during recent hour is + /// (accPoints[1] - accPoints[0]) / 3600 + /// @param secondsAgos describe the target timestamp , targetTimestimp[i] = block.timestamp - secondsAgo[i] + /// @return accPoints integral value of point(time) from 0 to each target timestamp + function observe(uint32[] calldata secondsAgos) + external + view + returns (int56[] memory accPoints); + + /// @notice Expand max-length of observation queue. + /// @param newNextQueueLen new value of observationNextQueueLen, which should be greater than current observationNextQueueLen + function expandObservationQueue(uint16 newNextQueueLen) external; + + /// @notice Borrow tokenX and/or tokenY and pay it back within a block. + /// @dev The caller needs to implement a IiZiSwapPool#flashCallback callback function + /// @param recipient the address which will receive the tokenY and/or tokenX + /// @param amountX the amount of tokenX to borrow + /// @param amountY the amount of tokenY to borrow + /// @param data Any data to be passed through to the callback + function flash( + address recipient, + uint256 amountX, + uint256 amountY, + bytes calldata data + ) external; + + /// @notice Returns a snapshot infomation of Liquidity in [leftPoint, rightPoint). + /// @param leftPoint left endpoint of range, should be times of pointDelta + /// @param rightPoint right endpoint of range, should be times of pointDelta + /// @return deltaLiquidities an array of delta liquidity for points in the range + /// note 1. delta liquidity here is amount of liquidity changed when cross a point from left to right + /// note 2. deltaLiquidities only contains points which are times of pointDelta + /// note 3. this function may cost a ENORMOUS amount of gas, be careful to call + function liquiditySnapshot(int24 leftPoint, int24 rightPoint) external view returns(int128[] memory deltaLiquidities); + + struct LimitOrderStruct { + uint128 sellingX; + uint128 earnY; + uint256 accEarnY; + uint128 sellingY; + uint128 earnX; + uint256 accEarnX; + } + + /// @notice Returns a snapshot infomation of Limit Order in [leftPoint, rightPoint). + /// @param leftPoint left endpoint of range, should be times of pointDelta + /// @param rightPoint right endpoint of range, should be times of pointDelta + /// @return limitOrders an array of Limit Orders for points in the range + /// note 1. this function may cost a HUGE amount of gas, be careful to call + function limitOrderSnapshot(int24 leftPoint, int24 rightPoint) external view returns(LimitOrderStruct[] memory limitOrders); + + /// @notice Amount of charged fee on tokenX. + function totalFeeXCharged() external view returns(uint256); + + /// @notice Amount of charged fee on tokenY. + function totalFeeYCharged() external view returns(uint256); + + /// @notice Percent to charge from miner's fee. + function feeChargePercent() external view returns(uint24); + + /// @notice Collect charged fee, only factory's chargeReceiver can call. + function collectFeeCharged() external; + + /// @notice modify 'feeChargePercent', only owner has authority. + /// @param newFeeChargePercent new value of feeChargePercent, a nature number range in [0, 100], + function modifyFeeChargePercent(uint24 newFeeChargePercent) external; + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/AmountMath.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/AmountMath.sol new file mode 100644 index 000000000..c83e8b527 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/AmountMath.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./MulDivMath.sol"; +import "./TwoPower.sol"; +import "./LogPowMath.sol"; + +library AmountMath { + + function getAmountY( + uint128 liquidity, + uint160 sqrtPriceL_96, + uint160 sqrtPriceR_96, + uint160 sqrtRate_96, + bool upper + ) internal pure returns (uint256 amount) { + uint160 numerator = sqrtPriceR_96 - sqrtPriceL_96; + uint160 denominator = sqrtRate_96 - uint160(TwoPower.Pow96); + if (!upper) { + amount = MulDivMath.mulDivFloor(liquidity, numerator, denominator); + } else { + amount = MulDivMath.mulDivCeil(liquidity, numerator, denominator); + } + } + + function getAmountX( + uint128 liquidity, + int24 leftPt, + int24 rightPt, + uint160 sqrtPriceR_96, + uint160 sqrtRate_96, + bool upper + ) internal pure returns (uint256 amount) { + // rightPt - (leftPt - 1), pc = leftPt - 1 + uint160 sqrtPricePrPl_96 = LogPowMath.getSqrtPrice(rightPt - leftPt); + // 1. sqrtPriceR_96 * 2^96 < 2^256 + // 2. sqrtRate_96 > 2^96, so sqrtPricePrM1_96 < sqrtPriceR_96 < 2^160 + uint160 sqrtPricePrM1_96 = uint160(uint256(sqrtPriceR_96) * TwoPower.Pow96 / sqrtRate_96); + + uint160 numerator = sqrtPricePrPl_96 - uint160(TwoPower.Pow96); + uint160 denominator = sqrtPriceR_96 - sqrtPricePrM1_96; + if (!upper) { + amount = MulDivMath.mulDivFloor(liquidity, numerator, denominator); + } else { + amount = MulDivMath.mulDivCeil(liquidity, numerator, denominator); + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/Converter.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/Converter.sol new file mode 100644 index 000000000..deeb56adf --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/Converter.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library Converter { + + function toUint128(uint256 a) internal pure returns (uint128 b){ + b = uint128(a); + require(a == b, 'C128'); + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/LimitOrder.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/LimitOrder.sol new file mode 100644 index 000000000..8661d3ca5 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/LimitOrder.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library LimitOrder { + + struct Data { + uint128 sellingX; + uint128 earnY; + uint256 accEarnY; + uint256 legacyAccEarnY; + uint128 legacyEarnY; + uint128 sellingY; + uint128 earnX; + uint128 legacyEarnX; + uint256 accEarnX; + uint256 legacyAccEarnX; + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/Liquidity.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/Liquidity.sol new file mode 100644 index 000000000..c9bba59ac --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/Liquidity.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./MulDivMath.sol"; +import "./TwoPower.sol"; + +library Liquidity { + + struct Data { + uint128 liquidity; + uint256 lastFeeScaleX_128; + uint256 lastFeeScaleY_128; + uint256 tokenOwedX; + uint256 tokenOwedY; + } + + // delta cannot be int128.min and liquidDelta of any one point will not be int128.min + function liquidityAddDelta(uint128 l, int128 delta) internal pure returns (uint128 nl) { + if (delta < 0) { + // in the pool, max(liquidity) < 2 ** 127 + // so, -delta > -2**127, -delta <= int128.max + nl = l - uint128(-delta); + } else { + nl = l + uint128(delta); + } + } + + function get( + mapping(bytes32 => Data) storage self, + address minter, + int24 tl, + int24 tr + ) internal view returns (Liquidity.Data storage data) { + data = self[keccak256(abi.encodePacked(minter, tl, tr))]; + } + + function update( + Liquidity.Data storage self, + int128 delta, + uint256 feeScaleX_128, + uint256 feeScaleY_128 + ) internal { + Data memory data = self; + uint128 liquidity; + if (delta == 0) { + require(data.liquidity > 0, "L>0"); + liquidity = data.liquidity; + } else { + liquidity = liquidityAddDelta(data.liquidity, delta); + } + uint256 deltaScaleX = data.lastFeeScaleX_128; + uint256 deltaScaleY = data.lastFeeScaleY_128; + // use assembly to prevent revert if overflow + // data.lastFeeScaleX(Y)_128 may be "negative" (>=2^255) + assembly { + deltaScaleX := sub(feeScaleX_128, deltaScaleX) + deltaScaleY := sub(feeScaleY_128, deltaScaleY) + } + uint256 feeX = MulDivMath.mulDivFloor(deltaScaleX, data.liquidity, TwoPower.Pow128); + uint256 feeY = MulDivMath.mulDivFloor(deltaScaleY, data.liquidity, TwoPower.Pow128); + data.liquidity = liquidity; + + // update the position + if (delta != 0) self.liquidity = liquidity; + self.lastFeeScaleX_128 = feeScaleX_128; + self.lastFeeScaleY_128 = feeScaleY_128; + if (feeX > 0 || feeY > 0) { + // need to withdraw before overflow + self.tokenOwedX += feeX; + self.tokenOwedY += feeY; + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/LogPowMath.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/LogPowMath.sol new file mode 100644 index 000000000..957a26254 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/LogPowMath.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + + +library LogPowMath { + + int24 internal constant MIN_POINT = -887272; + + int24 internal constant MAX_POINT = -MIN_POINT; + + uint160 internal constant MIN_SQRT_PRICE = 4295128739; + + uint160 internal constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342; + + /// @notice sqrt(1.0001^point) in form oy 96-bit fix point num + function getSqrtPrice(int24 point) internal pure returns (uint160 sqrtPrice_96) { + uint256 absIdx = point < 0 ? uint256(-int256(point)) : uint256(int256(point)); + require(absIdx <= uint256(int256(MAX_POINT)), 'T'); + + uint256 value = absIdx & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000; + if (absIdx & 0x2 != 0) value = (value * 0xfff97272373d413259a46990580e213a) >> 128; + if (absIdx & 0x4 != 0) value = (value * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; + if (absIdx & 0x8 != 0) value = (value * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; + if (absIdx & 0x10 != 0) value = (value * 0xffcb9843d60f6159c9db58835c926644) >> 128; + if (absIdx & 0x20 != 0) value = (value * 0xff973b41fa98c081472e6896dfb254c0) >> 128; + if (absIdx & 0x40 != 0) value = (value * 0xff2ea16466c96a3843ec78b326b52861) >> 128; + if (absIdx & 0x80 != 0) value = (value * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; + if (absIdx & 0x100 != 0) value = (value * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; + if (absIdx & 0x200 != 0) value = (value * 0xf987a7253ac413176f2b074cf7815e54) >> 128; + if (absIdx & 0x400 != 0) value = (value * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; + if (absIdx & 0x800 != 0) value = (value * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; + if (absIdx & 0x1000 != 0) value = (value * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; + if (absIdx & 0x2000 != 0) value = (value * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; + if (absIdx & 0x4000 != 0) value = (value * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; + if (absIdx & 0x8000 != 0) value = (value * 0x31be135f97d08fd981231505542fcfa6) >> 128; + if (absIdx & 0x10000 != 0) value = (value * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; + if (absIdx & 0x20000 != 0) value = (value * 0x5d6af8dedb81196699c329225ee604) >> 128; + if (absIdx & 0x40000 != 0) value = (value * 0x2216e584f5fa1ea926041bedfe98) >> 128; + if (absIdx & 0x80000 != 0) value = (value * 0x48a170391f7dc42444e8fa2) >> 128; + + if (point > 0) value = type(uint256).max / value; + + sqrtPrice_96 = uint160((value >> 32) + (value % (1 << 32) == 0 ? 0 : 1)); + } + + // floor(log1.0001(sqrtPrice_96)) + function getLogSqrtPriceFloor(uint160 sqrtPrice_96) internal pure returns (int24 logValue) { + // second inequality must be < because the price can nevex reach the price at the max tick + require(sqrtPrice_96 >= MIN_SQRT_PRICE && sqrtPrice_96 < MAX_SQRT_PRICE, 'R'); + uint256 sqrtPrice_128 = uint256(sqrtPrice_96) << 32; + + uint256 x = sqrtPrice_128; + uint256 m = 0; + + assembly { + let y := shl(7, gt(x, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(6, gt(x, 0xFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(5, gt(x, 0xFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(4, gt(x, 0xFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(3, gt(x, 0xFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(2, gt(x, 0xF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(1, gt(x, 0x3)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := gt(x, 0x1) + m := or(m, y) + } + + if (m >= 128) x = sqrtPrice_128 >> (m - 127); + else x = sqrtPrice_128 << (127 - m); + + int256 l2 = (int256(m) - 128) << 64; + + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(63, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(62, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(61, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(60, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(59, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(58, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(57, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(56, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(55, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(54, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(53, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(52, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(51, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(50, y)) + } + + int256 ls10001 = l2 * 255738958999603826347141; + + int24 logFloor = int24((ls10001 - 3402992956809132418596140100660247210) >> 128); + int24 logUpper = int24((ls10001 + 291339464771989622907027621153398088495) >> 128); + + logValue = logFloor == logUpper ? logFloor : getSqrtPrice(logUpper) <= sqrtPrice_96 ? logUpper : logFloor; + } + + function getLogSqrtPriceFU(uint160 sqrtPrice_96) internal pure returns (int24 logFloor, int24 logUpper) { + // second inequality must be < because the price can nevex reach the price at the max tick + require(sqrtPrice_96 >= MIN_SQRT_PRICE && sqrtPrice_96 < MAX_SQRT_PRICE, 'R'); + uint256 sqrtPrice_128 = uint256(sqrtPrice_96) << 32; + + uint256 x = sqrtPrice_128; + uint256 m = 0; + + assembly { + let y := shl(7, gt(x, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(6, gt(x, 0xFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(5, gt(x, 0xFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(4, gt(x, 0xFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(3, gt(x, 0xFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(2, gt(x, 0xF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(1, gt(x, 0x3)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := gt(x, 0x1) + m := or(m, y) + } + + if (m >= 128) x = sqrtPrice_128 >> (m - 127); + else x = sqrtPrice_128 << (127 - m); + + int256 l2 = (int256(m) - 128) << 64; + + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(63, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(62, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(61, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(60, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(59, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(58, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(57, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(56, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(55, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(54, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(53, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(52, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(51, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(50, y)) + } + + int256 ls10001 = l2 * 255738958999603826347141; + + logFloor = int24((ls10001 - 3402992956809132418596140100660247210) >> 128); + logUpper = int24((ls10001 + 291339464771989622907027621153398088495) >> 128); + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/MaxMinMath.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/MaxMinMath.sol new file mode 100644 index 000000000..32acd3c0e --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/MaxMinMath.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library MaxMinMath { + + function max(int24 a, int24 b) internal pure returns (int24) { + if (a > b) { + return a; + } + return b; + } + + function min(int24 a, int24 b) internal pure returns (int24) { + if (a < b) { + return a; + } + return b; + } + + function min(uint128 a, uint128 b) internal pure returns (uint128) { + if (a < b) { + return a; + } + return b; + } + + function min256(uint256 a, uint256 b) internal pure returns (uint256) { + if (a < b) { + return a; + } + return b; + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/MulDivMath.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/MulDivMath.sol new file mode 100644 index 000000000..30baad185 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/MulDivMath.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +library MulDivMath { + + // compute res = floor(a * b / c), assuming res < 2^256 + function mulDivFloor( + uint256 a, + uint256 b, + uint256 c + ) internal pure returns (uint256 res) { + + // let prodMod2_256 = a * b % 2^256 + uint256 prodMod2_256; + // let prodDiv2_256 = a * b / 2^256 + uint256 prodDiv2_256; + assembly { + let prodModM1 := mulmod(a, b, not(0)) + prodMod2_256 := mul(a, b) + prodDiv2_256 := sub(sub(prodModM1, prodMod2_256), lt(prodModM1, prodMod2_256)) + } + + if (prodDiv2_256 == 0) { + require(c > 0); + assembly { + res := div(prodMod2_256, c) + } + return res; + } + + // we should ensure that a * b /c < 2^256 before calling + require(c > prodDiv2_256); + + uint256 resMod; + assembly { + resMod := mulmod(a, b, c) + // a * b - resMod + prodDiv2_256 := sub(prodDiv2_256, gt(resMod, prodMod2_256)) + prodMod2_256 := sub(prodMod2_256, resMod) + + // compute lowbit of c + let lowbit := not(c) + lowbit := add(lowbit, 1) + lowbit := and(lowbit, c) + + // c / lowbit + c := div(c, lowbit) + // a * b / lowbit + prodMod2_256 := div(prodMod2_256, lowbit) + lowbit := add(div(sub(0, lowbit), lowbit), 1) + prodDiv2_256 := mul(prodDiv2_256, lowbit) + prodMod2_256 := or(prodMod2_256, prodDiv2_256) + + // get inv of c + // cInv * c = 1 (mod 2^4) + let cInv := xor(mul(3, c), 2) + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^8 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^16 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^32 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^64 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^128 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^256 + + // a * b / c = prodMod2_256 * cInv (mod 2^256) + res := mul(prodMod2_256, cInv) + } + } + + // compute res = ceil(a * b / c), assuming res < 2^256 + function mulDivCeil( + uint256 a, + uint256 b, + uint256 c + ) internal pure returns (uint256 res) { + res = mulDivFloor(a, b, c); + if (mulmod(a, b, c) > 0) { + require(res < type(uint256).max); + res++; + } + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/Oracle.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/Oracle.sol new file mode 100644 index 000000000..9aed68aeb --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/Oracle.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library Oracle { + + struct Observation { + uint32 timestamp; + // sigma (point_i * time_i - time_(i-1)) + int56 accPoint; + // true if this observation is inited + bool init; + } + + /// @notice Record a new observation with a circular queue. + /// @param last the specified observation to be updated + /// @param timestamp the timestamp of the new observation, > last.timestamp + /// @param currentPoint log 1.0001 of price + /// @return observation generated + function newObservation( + Observation memory last, + uint32 timestamp, + int24 currentPoint + ) private pure returns (Observation memory) { + uint56 delta = uint56(timestamp - last.timestamp); + return + Observation({ + timestamp: timestamp, + accPoint: last.accPoint + int56(currentPoint) * int56(delta), + init: true + }); + } + + function init(Observation[65535] storage self, uint32 timestamp) + internal + returns (uint16 queueLen, uint16 nextQueueLen) + { + self[0] = Observation({ + timestamp: timestamp, + accPoint: 0, + init: true + }); + return (1, 1); + } + + /// @notice Append a price oracle observation data in the pool + /// @param self circular-queue of observation data in array form + /// @param currentIndex the index of the last observation in the array + /// @param timestamp timestamp of new observation + /// @param currentPoint current point of new observation (usually we append the point value just-before exchange) + /// @param queueLen max-length of circular queue + /// @param nextQueueLen next max-length of circular queue, if length of queue increase over queueLen, queueLen will become nextQueueLen + /// @return newIndex index of new observation + /// @return newQueueLen queueLen value after appending + function append( + Observation[65535] storage self, + uint16 currentIndex, + uint32 timestamp, + int24 currentPoint, + uint16 queueLen, + uint16 nextQueueLen + ) internal returns (uint16 newIndex, uint16 newQueueLen) { + Observation memory last = self[currentIndex]; + + if (last.timestamp == timestamp) return (currentIndex, queueLen); + + // if the conditions are right, we can bump the cardinality + if (nextQueueLen > queueLen && currentIndex == (queueLen - 1)) { + newQueueLen = nextQueueLen; + } else { + newQueueLen = queueLen; + } + + newIndex = (currentIndex + 1) % newQueueLen; + self[newIndex] = newObservation(last, timestamp, currentPoint); + } + + /// @notice Expand the max-length of observation queue + /// @param queueLen current max-length of queue + /// @param nextQueueLen next max-length + /// @return next max-length + function expand( + Observation[65535] storage self, + uint16 queueLen, + uint16 nextQueueLen + ) internal returns (uint16) { + require(queueLen > 0, 'LEN'); + + if (nextQueueLen <= queueLen) return queueLen; + + for (uint16 i = queueLen; i < nextQueueLen; i++) self[i].timestamp = 1; + return nextQueueLen; + } + + function lte( + uint32 time, + uint32 a, + uint32 b + ) private pure returns (bool) { + if (a <= time && b <= time) return a <= b; + + uint256 aAdjusted = a > time ? a : a + 2**32; + uint256 bAdjusted = b > time ? b : b + 2**32; + + return aAdjusted <= bAdjusted; + } + + /// @notice Binary search to find two neighbor observations for a target timestamp + /// @param self observation queue in array form + /// @param timestamp timestamp of current block + /// @param targetTimestamp target time stamp + /// @param currentIdx The index of the last observation in the array + /// @param queueLen current max-length of queue + /// @return beforeNeighbor before-or-at observation neighbor to target timestamp + /// @return afterNeighbor after-or-at observation neighbor to target timestamp + function findNeighbor( + Observation[65535] storage self, + uint32 timestamp, + uint32 targetTimestamp, + uint16 currentIdx, + uint16 queueLen + ) private view returns (Observation memory beforeNeighbor, Observation memory afterNeighbor) { + uint256 l = (currentIdx + 1) % queueLen; // oldest observation + uint256 r = l + queueLen - 1; // newest observation + uint256 i; + while (true) { + i = (l + r) / 2; + + beforeNeighbor = self[i % queueLen]; + + if (!beforeNeighbor.init) { + l = i + 1; + continue; + } + + afterNeighbor = self[(i + 1) % queueLen]; + + bool leftLessOrEq = lte(timestamp, beforeNeighbor.timestamp, targetTimestamp); + + if (leftLessOrEq && lte(timestamp, targetTimestamp, afterNeighbor.timestamp)) break; + + if (!leftLessOrEq) r = i - 1; + else l = i + 1; + } + } + + /// @notice Find two neighbor observations for a target timestamp + /// @param self observation queue in array form + /// @param timestamp timestamp of current block + /// @param targetTimestamp target time stamp + /// @param currentPoint current point of swap + /// @param currentIndex the index of the last observation in the array + /// @param queueLen current max-length of queue + /// @return beforeNeighbor before-or-at observation neighbor to target timestamp + /// @return afterNeighbor after-or-at observation neighbor to target timestamp, if the targetTimestamp is later than last observation in queue, + /// the afterNeighbor observation does not exist in the queue + function getTwoNeighborObservation( + Observation[65535] storage self, + uint32 timestamp, + uint32 targetTimestamp, + int24 currentPoint, + uint16 currentIndex, + uint16 queueLen + ) private view returns (Observation memory beforeNeighbor, Observation memory afterNeighbor) { + beforeNeighbor = self[currentIndex]; + + if (lte(timestamp, beforeNeighbor.timestamp, targetTimestamp)) { + if (beforeNeighbor.timestamp == targetTimestamp) { + return (beforeNeighbor, beforeNeighbor); + } else { + return (beforeNeighbor, newObservation(beforeNeighbor, targetTimestamp, currentPoint)); + } + } + + beforeNeighbor = self[(currentIndex + 1) % queueLen]; + if (!beforeNeighbor.init) beforeNeighbor = self[0]; + + require(lte(timestamp, beforeNeighbor.timestamp, targetTimestamp), 'OLD'); + + return findNeighbor(self, timestamp, targetTimestamp, currentIndex, queueLen); + } + + /// @dev Revert if secondsAgo too large. + /// @param self the observation circular queue in array form + /// @param timestamp the current block timestamp + /// @param secondsAgo target timestamp is timestamp-secondsAg, 0 to return the current cumulative values. + /// @param currentPoint the current point of pool + /// @param currentIndex the index of the last observation in the array + /// @param queueLen max-length of circular queue + /// @return accPoint integral value of point(time) from 0 to each timestamp + function observeSingle( + Observation[65535] storage self, + uint32 timestamp, + uint32 secondsAgo, + int24 currentPoint, + uint16 currentIndex, + uint16 queueLen + ) internal view returns (int56 accPoint ) { + if (secondsAgo == 0) { + Observation memory last = self[currentIndex]; + if (last.timestamp != timestamp) last = newObservation(last, timestamp, currentPoint); + return last.accPoint; + } + + uint32 targetTimestamp = timestamp - secondsAgo; + + (Observation memory beforeNeighbor, Observation memory afterNeighbor) = + getTwoNeighborObservation(self, timestamp, targetTimestamp, currentPoint, currentIndex, queueLen); + + if (targetTimestamp == beforeNeighbor.timestamp) { + // we're at the left boundary + return beforeNeighbor.accPoint; + } else if (targetTimestamp == afterNeighbor.timestamp) { + // we're at the right boundary + return afterNeighbor.accPoint; + } else { + // we're in the middle + uint56 leftRightTimeDelta = afterNeighbor.timestamp - beforeNeighbor.timestamp; + uint56 targetTimeDelta = targetTimestamp - beforeNeighbor.timestamp; + return beforeNeighbor.accPoint + + (afterNeighbor.accPoint - beforeNeighbor.accPoint) / int56(leftRightTimeDelta) * int56(targetTimeDelta); + } + } + + /// @notice Returns the integral value of point with time + /// @dev Reverts if target timestamp is early than oldest observation in the queue + /// @dev If you call this method with secondsAgos = [3600, 0]. the average point of this pool during recent hour is (accPoints[1] - accPoints[0]) / 3600 + /// @param self the observation circular queue in array form + /// @param timestamp the current block timestamp + /// @param secondsAgos describe the target timestamp , targetTimestimp[i] = block.timestamp - secondsAgo[i] + /// @param currentPoint the current point of pool + /// @param currentIndex the index of the last observation in the array + /// @param queueLen max-length of circular queue + /// @return accPoints integral value of point(time) from 0 to each timestamp + function observe( + Observation[65535] storage self, + uint32 timestamp, + uint32[] memory secondsAgos, + int24 currentPoint, + uint16 currentIndex, + uint16 queueLen + ) internal view returns (int56[] memory accPoints ) { + require(queueLen > 0, 'I'); + + accPoints = new int56[](secondsAgos.length); + for (uint256 i = 0; i < secondsAgos.length; i++) { + accPoints[i] = observeSingle( + self, + timestamp, + secondsAgos[i], + currentPoint, + currentIndex, + queueLen + ); + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/OrderOrEndpoint.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/OrderOrEndpoint.sol new file mode 100644 index 000000000..689d60c35 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/OrderOrEndpoint.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library OrderOrEndpoint { + + function getOrderOrEndptVal(mapping(int24 =>int24) storage self, int24 point, int24 pd) internal view returns(int24 val) { + if (point % pd != 0) { + return 0; + } + val = self[point / pd]; + } + function setOrderOrEndptVal(mapping(int24 =>int24) storage self, int24 point, int24 pd, int24 val) internal { + self[point / pd] = val; + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/Point.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/Point.sol new file mode 100644 index 000000000..d30500cec --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/Point.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library Point { + + struct Data { + uint128 liquidSum; + // value to add when pass this slot from left to right + // value to dec when pass this slot from right to left + int128 liquidDelta; + // if pointPrice < currPrice + // value = sigma(feeScaleX(p)), which p < pointPrice + // if pointPrice >= currPrice + // value = sigma(feeScaleX(p)), which p >= pointPrice + uint256 accFeeXOut_128; + // similar to accFeeXOut_128 + uint256 accFeeYOut_128; + // whether the point is endpoint of a liquid segment + bool isEndpt; + } + + function _getFeeScaleL( + int24 endpt, + int24 currpt, + uint256 feeScale_128, + uint256 feeScaleBeyond_128 + ) internal pure returns (uint256 feeScaleL_128) { + if (endpt <= currpt) { + feeScaleL_128 = feeScaleBeyond_128; + } else { + assembly { + feeScaleL_128:= sub(feeScale_128, feeScaleBeyond_128) + } + } + } + function _getFeeScaleGE( + int24 endpt, + int24 currpt, + uint256 feeScale_128, + uint256 feeScaleBeyond_128 + ) internal pure returns (uint256 feeScaleGE_128) { + if (endpt > currpt) { + feeScaleGE_128 = feeScaleBeyond_128; + } else { + assembly { + feeScaleGE_128:= sub(feeScale_128, feeScaleBeyond_128) + } + } + } + /// @dev Calculate fee scale within range [pl, pr). + /// @param axies collection of points of liquidities + /// @param pl left endpoint of the segment + /// @param pr right endpoint of the segment + /// @param currpt point of the curr price + /// @param feeScaleX_128 total fee scale of token x accummulated of the exchange + /// @param feeScaleY_128 similar to feeScaleX_128 + /// @return accFeeXIn_128 accFeeYIn_128 fee scale of token x and token y within range [pl, pr) + function getSubFeeScale( + mapping(int24 =>Point.Data) storage axies, + int24 pl, + int24 pr, + int24 currpt, + uint256 feeScaleX_128, + uint256 feeScaleY_128 + ) internal view returns (uint256 accFeeXIn_128, uint256 accFeeYIn_128) { + Point.Data storage plData = axies[pl]; + Point.Data storage prData = axies[pr]; + // tot fee scale of token x for price < pl + uint256 feeScaleLX_128 = _getFeeScaleL(pl, currpt, feeScaleX_128, plData.accFeeXOut_128); + // to fee scale of token x for price >= pr + uint256 feeScaleGEX_128 = _getFeeScaleGE(pr, currpt, feeScaleX_128, prData.accFeeXOut_128); + uint256 feeScaleLY_128 = _getFeeScaleL(pl, currpt, feeScaleY_128, plData.accFeeYOut_128); + uint256 feeScaleGEY_128 = _getFeeScaleGE(pr, currpt, feeScaleY_128, prData.accFeeYOut_128); + assembly{ + accFeeXIn_128 := sub(sub(feeScaleX_128, feeScaleLX_128), feeScaleGEX_128) + accFeeYIn_128 := sub(sub(feeScaleY_128, feeScaleLY_128), feeScaleGEY_128) + } + } + + /// @dev Update and endpoint of a liquidity segment. + /// @param axies collections of points + /// @param endpt endpoint of a segment + /// @param isLeft left or right endpoint + /// @param currpt point of current price + /// @param delta >0 for add liquidity and <0 for dec + /// @param liquidLimPt liquid limit per point + /// @param feeScaleX_128 total fee scale of token x + /// @param feeScaleY_128 total fee scale of token y + function updateEndpoint( + mapping(int24 =>Point.Data) storage axies, + int24 endpt, + bool isLeft, + int24 currpt, + int128 delta, + uint128 liquidLimPt, + uint256 feeScaleX_128, + uint256 feeScaleY_128 + ) internal returns (bool) { + Point.Data storage data = axies[endpt]; + uint128 liquidAccBefore = data.liquidSum; + // delta cannot be 0 + require(delta!=0, "D0"); + // liquide acc cannot overflow + uint128 liquidAccAfter; + if (delta > 0) { + liquidAccAfter = liquidAccBefore + uint128(delta); + require(liquidAccAfter > liquidAccBefore, "LAAO"); + } else { + liquidAccAfter = liquidAccBefore - uint128(-delta); + require(liquidAccAfter < liquidAccBefore, "LASO"); + } + require(liquidAccAfter <= liquidLimPt, "L LIM PT"); + data.liquidSum = liquidAccAfter; + + if (isLeft) { + data.liquidDelta = data.liquidDelta + delta; + } else { + data.liquidDelta = data.liquidDelta - delta; + } + bool new_or_erase = false; + if (liquidAccBefore == 0) { + // a new endpoint of certain segment + new_or_erase = true; + data.isEndpt = true; + + // for either left point or right point of the liquide segment + // the feeScaleBeyond can be initialized to arbitrary value + // we here set the initial val to total feeScale to delay overflow + if (endpt >= currpt) { + data.accFeeXOut_128 = feeScaleX_128; + data.accFeeYOut_128 = feeScaleY_128; + } + } else if (liquidAccAfter == 0) { + // no segment use this endpoint + new_or_erase = true; + data.isEndpt = false; + } + return new_or_erase; + } + + /// @dev Pass the endpoint, change the feescale beyond the price. + /// @param endpt endpoint to change + /// @param feeScaleX_128 total fee scale of token x + /// @param feeScaleY_128 total fee scale of token y + function passEndpoint( + Point.Data storage endpt, + uint256 feeScaleX_128, + uint256 feeScaleY_128 + ) internal { + uint256 accFeeXOut_128 = endpt.accFeeXOut_128; + uint256 accFeeYOut_128 = endpt.accFeeYOut_128; + assembly { + accFeeXOut_128 := sub(feeScaleX_128, accFeeXOut_128) + accFeeYOut_128 := sub(feeScaleY_128, accFeeYOut_128) + } + endpt.accFeeXOut_128 = accFeeXOut_128; + endpt.accFeeYOut_128 = accFeeYOut_128; + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/PointBitmap.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/PointBitmap.sol new file mode 100644 index 000000000..e7b649688 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/PointBitmap.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library PointBitmap { + + function MSB(uint256 number) internal pure returns (uint8 msb) { + require(number > 0); + + if (number >= 0x100000000000000000000000000000000) { + number >>= 128; + msb += 128; + } + if (number >= 0x10000000000000000) { + number >>= 64; + msb += 64; + } + if (number >= 0x100000000) { + number >>= 32; + msb += 32; + } + if (number >= 0x10000) { + number >>= 16; + msb += 16; + } + if (number >= 0x100) { + number >>= 8; + msb += 8; + } + if (number >= 0x10) { + number >>= 4; + msb += 4; + } + if (number >= 0x4) { + number >>= 2; + msb += 2; + } + if (number >= 0x2) msb += 1; + } + + function LSB(uint256 number) internal pure returns (uint8 msb) { + require(number > 0); + + msb = 255; + if (number & type(uint128).max > 0) { + msb -= 128; + } else { + number >>= 128; + } + if (number & type(uint64).max > 0) { + msb -= 64; + } else { + number >>= 64; + } + if (number & type(uint32).max > 0) { + msb -= 32; + } else { + number >>= 32; + } + if (number & type(uint16).max > 0) { + msb -= 16; + } else { + number >>= 16; + } + if (number & type(uint8).max > 0) { + msb -= 8; + } else { + number >>= 8; + } + if (number & 0xf > 0) { + msb -= 4; + } else { + number >>= 4; + } + if (number & 0x3 > 0) { + msb -= 2; + } else { + number >>= 2; + } + if (number & 0x1 > 0) msb -= 1; + } + + /// @notice Flips the initialized state for a given point from false to true, or vice versa + /// @param self The mapping in which to flip the point + /// @param point The point to flip + /// @param pointDelta The spacing between usable points + function flipPoint( + mapping(int16 => uint256) storage self, + int24 point, + int24 pointDelta + ) internal { + require(point % pointDelta == 0); + int24 mapPt = point / pointDelta; + int16 wordIdx = int16(mapPt >> 8); + uint8 bitIdx = uint8(uint24(mapPt % 256)); + self[wordIdx] ^= 1 << bitIdx; + } + + function setOne( + mapping(int16 => uint256) storage self, + int24 point, + int24 pointDelta + ) internal { + require(point % pointDelta == 0); + int24 mapPt = point / pointDelta; + int16 wordIdx = int16(mapPt >> 8); + uint8 bitIdx = uint8(uint24(mapPt % 256)); + self[wordIdx] |= 1 << bitIdx; + } + + function setZero( + mapping(int16 => uint256) storage self, + int24 point, + int24 pointDelta + ) internal { + require(point % pointDelta == 0); + int24 mapPt = point / pointDelta; + int16 wordIdx = int16(mapPt >> 8); + uint8 bitIdx = uint8(uint24(mapPt % 256)); + self[wordIdx] &= ~(1 << bitIdx); + } + + // find nearest one from point, or boundary in the same word + function nearestLeftOneOrBoundary( + mapping(int16 => uint256) storage self, + int24 point, + int24 pointDelta + ) internal view returns (int24 left) { + int24 mapPt = point / pointDelta; + if (point < 0 && point % pointDelta != 0) mapPt--; // round towards negative infinity + + int16 wordIdx = int16(mapPt >> 8); + uint8 bitIdx = uint8(uint24(mapPt % 256)); + + uint256 ones = self[wordIdx] & ((1 << bitIdx) - 1 + (1 << bitIdx)); + + left = (ones != 0) + ? (mapPt - int24(uint24(bitIdx - MSB(ones)))) * pointDelta + : (mapPt - int24(uint24(bitIdx))) * pointDelta; + + } + + // find nearest one from point, or boundary in the same word + function nearestRightOneOrBoundary( + mapping(int16 => uint256) storage self, + int24 point, + int24 pointDelta + ) internal view returns (int24 right) { + int24 mapPt = point / pointDelta; + if (point < 0 && point % pointDelta != 0) mapPt--; // round towards negative infinity + + mapPt += 1; + int16 wordIdx = int16(mapPt >> 8); + uint8 bitIdx = uint8(uint24(mapPt % 256)); + + uint256 ones = self[wordIdx] & (~((1 << bitIdx) - 1)); + + right = (ones != 0) + ? (mapPt + int24(uint24(LSB(ones) - bitIdx))) * pointDelta + : (mapPt + int24(uint24(type(uint8).max - bitIdx))) * pointDelta; + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/State.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/State.sol new file mode 100644 index 000000000..0f247dc50 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/State.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +struct State { + // a 96 fixpoing number describe the sqrt value of current price(tokenX/tokenY) + uint160 sqrtPrice_96; + // The current point of the pool, 1.0001 ^ currentPoint = price + int24 currentPoint; + // The index of the last oracle observation that was written, + uint16 observationCurrentIndex; + // The current maximum number of observations stored in the pool, + uint16 observationQueueLen; + // The next maximum number of observations, to be updated when the observation. + uint16 observationNextQueueLen; + // whether the pool is locked (only used for checking reentrance) + bool locked; + // total liquidity on the currentPoint (currX * sqrtPrice + currY / sqrtPrice) + uint128 liquidity; + // liquidity of tokenX, liquidity of tokenY is liquidity - liquidityX + uint128 liquidityX; +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapCache.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapCache.sol new file mode 100644 index 000000000..ad2d6bad4 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapCache.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +// group local vars in swap functions to avoid stack too deep +struct SwapCache { + // fee growth of tokenX during swap + uint256 currFeeScaleX_128; + // fee growth of tokenY during swap + uint256 currFeeScaleY_128; + // whether swap finished + bool finished; + // 96bit-fixpoint of sqrt(1.0001) + uint160 _sqrtRate_96; + // pointDelta + int24 pointDelta; + // whether has limorder and whether as endpt for current point + int24 currentOrderOrEndpt; + // start point of swap, etc. state.currPt before swap loop + int24 startPoint; + // start liquidity of swap, etc. state.liquidity before swap loop + uint128 startLiquidity; + // block time stamp of this swap transaction + uint32 timestamp; +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathX2Y.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathX2Y.sol new file mode 100644 index 000000000..42c445cd1 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathX2Y.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./MulDivMath.sol"; +import "./TwoPower.sol"; +import "./AmountMath.sol"; +import "./State.sol"; +import "./MaxMinMath.sol"; +import "./Converter.sol"; + +library SwapMathX2Y { + + // group returned values of x2YRange to avoid stake too deep + struct RangeRetState { + // whether user run out of amountX + bool finished; + // actual cost of tokenX to buy tokenY + uint128 costX; + // amount of acquired tokenY + uint256 acquireY; + // final point after this swap + int24 finalPt; + // sqrt price on final point + uint160 sqrtFinalPrice_96; + // liquidity of tokenX at finalPt + uint128 liquidityX; + } + + function x2YAtPrice( + uint128 amountX, + uint160 sqrtPrice_96, + uint128 currY + ) internal pure returns (uint128 costX, uint128 acquireY) { + uint256 l = MulDivMath.mulDivFloor(amountX, sqrtPrice_96, TwoPower.Pow96); + acquireY = Converter.toUint128(MulDivMath.mulDivFloor(l, sqrtPrice_96, TwoPower.Pow96)); + if (acquireY > currY) { + acquireY = currY; + } + l = MulDivMath.mulDivCeil(acquireY, TwoPower.Pow96, sqrtPrice_96); + uint256 cost = MulDivMath.mulDivCeil(l, TwoPower.Pow96, sqrtPrice_96); + // costX <= amountX <= uint128.max + costX = uint128(cost); + } + + function mulDivCeil(uint256 a, uint256 b, uint256 c) internal pure returns (uint256) { + uint256 v = a * b; + if (v % c == 0) { + return v / c; + } + return v / c + 1; + } + + function x2YAtPriceLiquidity( + uint128 amountX, + uint160 sqrtPrice_96, + uint128 liquidity, + uint128 liquidityX + ) internal pure returns (uint128 costX, uint256 acquireY, uint128 newLiquidityX) { + uint256 liquidityY = uint256(liquidity - liquidityX); + uint256 maxTransformLiquidityX = MulDivMath.mulDivFloor(amountX, sqrtPrice_96, TwoPower.Pow96); + // transformLiquidityX <= liquidityY <= uint128.max + uint128 transformLiquidityX = uint128(MaxMinMath.min256(maxTransformLiquidityX, liquidityY)); + + // 1. transformLiquidityX * TwoPower.Pow96 < 2^128 * 2^96 < 2^224 < 2^256 + // 2. transformLiquidityX <= floor(amountX * sqrtPrice_96 / TwoPower.Pow96) + // ceil(transformLiquidityX * sqrtPrice_96 / TwoPower.Pow96) <= + // ceil(floor(amountX * sqrtPrice_96 / TwoPower.Pow96) * sqrtPrice_96 / TwoPower.Pow96) <= + // ceil(amountX * sqrtPrice_96 / TwoPower.Pow96 * sqrtPrice_96 / TwoPower.Pow96) = + // ceil(amountX) = amountX <= uint128.max + costX = uint128(mulDivCeil(transformLiquidityX, TwoPower.Pow96, sqrtPrice_96)); + acquireY = MulDivMath.mulDivFloor(transformLiquidityX, sqrtPrice_96, TwoPower.Pow96); + newLiquidityX = liquidityX + transformLiquidityX; + } + + struct Range { + uint128 liquidity; + uint160 sqrtPriceL_96; + int24 leftPt; + uint160 sqrtPriceR_96; + int24 rightPt; + uint160 sqrtRate_96; + } + + struct RangeCompRet { + uint128 costX; + uint256 acquireY; + bool completeLiquidity; + int24 locPt; + uint160 sqrtLoc_96; + } + + /// @dev Move from rightPt to leftPt, the range is [leftPt, rightPt). + function x2YRangeComplete( + Range memory rg, + uint128 amountX + ) internal pure returns ( + RangeCompRet memory ret + ) { + // rg.sqrtPriceR_96 * 2^96 < 2^160 * 2^96 = 2^256 + uint160 sqrtPricePrM1_96 = uint160(mulDivCeil(rg.sqrtPriceR_96, TwoPower.Pow96, rg.sqrtRate_96)); + uint160 sqrtPricePrMl_96 = LogPowMath.getSqrtPrice(rg.rightPt - rg.leftPt); + // rg.rightPt - rg.leftPt <= 256 * 100 + // 1.0001 ** 25600 < 13 + // 13 * 2^96 - 2^96 < 2^100 + // rg.liquidity * (sqrtPricePrMl_96 - TwoPower.Pow96) < 2^228 < 2^256 + uint256 maxX = mulDivCeil(rg.liquidity, sqrtPricePrMl_96 - TwoPower.Pow96, rg.sqrtPriceR_96 - sqrtPricePrM1_96); + if (maxX <= amountX) { + // maxX <= amountX <= uint128.max + ret.costX = uint128(maxX); + ret.acquireY = AmountMath.getAmountY(rg.liquidity, rg.sqrtPriceL_96, rg.sqrtPriceR_96, rg.sqrtRate_96, false); + ret.completeLiquidity = true; + } else { + // we should locate lowest price + // 1. amountX * (rg.sqrtPriceR_96 - sqrtPricePrM1_96) + // < maxX * (rg.sqrtPriceR_96 - sqrtPricePrM1_96) + // < rg.liquidity * (sqrtPricePrMl_96 - TwoPower.Pow96) + (rg.sqrtPriceR_96 - sqrtPricePrM1_96) + // < 2^228 + 2^160 < 2^256 + // 2. sqrtValue_96 = amountX * (rg.sqrtPriceR_96 - sqrtPricePrM1_96) // rg.liquidity + 2^96 + // <= amountX * (rg.sqrtPriceR_96 - sqrtPricePrM1_96) / rg.liquidity + 2^96 + // <= (maxX - 1) * (rg.sqrtPriceR_96 - sqrtPricePrM1_96) / rg.liquidity + 2^96 + // < rg.liquidity * (sqrtPricePrMl_96 - 2^96) / (rg.sqrtPriceR_96 - sqrtPricePrM1_96) * (rg.sqrtPriceR_96 - sqrtPricePrM1_96) / rg.liquidity + 2^96 + // = sqrtPricePrMl_96 < 2^160 + uint160 sqrtValue_96 = uint160(uint256(amountX) * (uint256(rg.sqrtPriceR_96) - sqrtPricePrM1_96) / uint256(rg.liquidity) + TwoPower.Pow96); + + int24 logValue = LogPowMath.getLogSqrtPriceFloor(sqrtValue_96); + + ret.locPt = rg.rightPt - logValue; + + ret.locPt = MaxMinMath.min(ret.locPt, rg.rightPt); + ret.locPt = MaxMinMath.max(ret.locPt, rg.leftPt + 1); + ret.completeLiquidity = false; + + if (ret.locPt == rg.rightPt) { + ret.costX = 0; + ret.acquireY = 0; + ret.locPt = ret.locPt - 1; + ret.sqrtLoc_96 = LogPowMath.getSqrtPrice(ret.locPt); + } else { + uint160 sqrtPricePrMloc_96 = LogPowMath.getSqrtPrice(rg.rightPt - ret.locPt); + // rg.rightPt - ret.locPt <= 256 * 100 + // 1.0001 ** 25600 < 13 + // 13 * 2^96 - 2^96 < 2^100 + // rg.liquidity * (sqrtPricePrMloc_96 - TwoPower.Pow96) < 2^228 < 2^256 + uint256 costX256 = mulDivCeil(rg.liquidity, sqrtPricePrMloc_96 - TwoPower.Pow96, rg.sqrtPriceR_96 - sqrtPricePrM1_96); + // ret.costX <= amountX <= uint128.max + ret.costX = uint128(MaxMinMath.min256(costX256, amountX)); + + ret.locPt = ret.locPt - 1; + ret.sqrtLoc_96 = LogPowMath.getSqrtPrice(ret.locPt); + + uint160 sqrtLocA1_96 = uint160( + uint256(ret.sqrtLoc_96) + + uint256(ret.sqrtLoc_96) * (uint256(rg.sqrtRate_96) - TwoPower.Pow96) / TwoPower.Pow96 + ); + ret.acquireY = AmountMath.getAmountY(rg.liquidity, sqrtLocA1_96, rg.sqrtPriceR_96, rg.sqrtRate_96, false); + } + } + } + + /// @notice Compute amount of tokens exchanged during swapX2Y and some amount values (currX, currY, allX) on final point + /// after this swap. + /// @param currentState state values containing (currX, currY, allX) of start point + /// @param leftPt left most point during this swap + /// @param sqrtRate_96 sqrt(1.0001) + /// @param amountX max amount of tokenX user willing to pay + /// @return retState amount of token acquired and some values on final point + function x2YRange( + State memory currentState, + int24 leftPt, + uint160 sqrtRate_96, + uint128 amountX + ) internal pure returns ( + RangeRetState memory retState + ) { + retState.costX = 0; + retState.acquireY = 0; + retState.finished = false; + + bool currentHasY = (currentState.liquidityX < currentState.liquidity); + if (currentHasY && (currentState.liquidityX > 0 || leftPt == currentState.currentPoint)) { + (retState.costX, retState.acquireY, retState.liquidityX) = x2YAtPriceLiquidity( + amountX, currentState.sqrtPrice_96, currentState.liquidity, currentState.liquidityX + ); + if (retState.liquidityX < currentState.liquidity || retState.costX >= amountX) { + // remaining x is not enough to down current price to price / 1.0001 + // but x may remain, so we cannot simply use (costX == amountX) + retState.finished = true; + retState.finalPt = currentState.currentPoint; + retState.sqrtFinalPrice_96 = currentState.sqrtPrice_96; + } else { + amountX -= retState.costX; + } + } else if (currentHasY) { // all y + currentState.currentPoint = currentState.currentPoint + 1; + // sqrt(price) + sqrt(price) * (1.0001 - 1) == sqrt(price) * 1.0001 + currentState.sqrtPrice_96 = uint160( + uint256(currentState.sqrtPrice_96) + + uint256(currentState.sqrtPrice_96) * (uint256(sqrtRate_96) - TwoPower.Pow96) / TwoPower.Pow96 + ); + } else { + retState.liquidityX = currentState.liquidityX; + } + + if (retState.finished) { + return retState; + } + + if (leftPt < currentState.currentPoint) { + uint160 sqrtPriceL_96 = LogPowMath.getSqrtPrice(leftPt); + RangeCompRet memory ret = x2YRangeComplete( + Range({ + liquidity: currentState.liquidity, + sqrtPriceL_96: sqrtPriceL_96, + leftPt: leftPt, + sqrtPriceR_96: currentState.sqrtPrice_96, + rightPt: currentState.currentPoint, + sqrtRate_96: sqrtRate_96 + }), + amountX + ); + retState.costX += ret.costX; + amountX -= ret.costX; + retState.acquireY += ret.acquireY; + if (ret.completeLiquidity) { + retState.finished = (amountX == 0); + retState.finalPt = leftPt; + retState.sqrtFinalPrice_96 = sqrtPriceL_96; + retState.liquidityX = currentState.liquidity; + } else { + uint128 locCostX; + uint256 locAcquireY; + (locCostX, locAcquireY, retState.liquidityX) = x2YAtPriceLiquidity(amountX, ret.sqrtLoc_96, currentState.liquidity, 0); + retState.costX += locCostX; + retState.acquireY += locAcquireY; + retState.finished = true; + retState.sqrtFinalPrice_96 = ret.sqrtLoc_96; + retState.finalPt = ret.locPt; + } + } else { + // finishd must be false + // retState.finished == false; + // liquidityX has been set + retState.finalPt = currentState.currentPoint; + retState.sqrtFinalPrice_96 = currentState.sqrtPrice_96; + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathX2YDesire.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathX2YDesire.sol new file mode 100644 index 000000000..8f81421b1 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathX2YDesire.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./MulDivMath.sol"; +import "./TwoPower.sol"; +import "./AmountMath.sol"; +import "./State.sol"; +import "./MaxMinMath.sol"; +import "./Converter.sol"; + +library SwapMathX2YDesire { + + // group returned values of x2YRange to avoid stake too deep + struct RangeRetState { + // whether user has acquire enough tokenY + bool finished; + // actual cost of tokenX to buy tokenY + uint256 costX; + // amount of acquired tokenY + uint128 acquireY; + // final point after this swap + int24 finalPt; + // sqrt price on final point + uint160 sqrtFinalPrice_96; + // liquidity of tokenX at finalPt + uint128 liquidityX; + } + + function x2YAtPrice( + uint128 desireY, + uint160 sqrtPrice_96, + uint128 currY + ) internal pure returns (uint128 costX, uint128 acquireY) { + acquireY = desireY; + if (acquireY > currY) { + acquireY = currY; + } + uint256 l = MulDivMath.mulDivCeil(acquireY, TwoPower.Pow96, sqrtPrice_96); + costX = Converter.toUint128(MulDivMath.mulDivCeil(l, TwoPower.Pow96, sqrtPrice_96)); + } + + function mulDivCeil(uint256 a, uint256 b, uint256 c) internal pure returns (uint256) { + uint256 v = a * b; + if (v % c == 0) { + return v / c; + } + return v / c + 1; + } + + function x2YAtPriceLiquidity( + uint128 desireY, + uint160 sqrtPrice_96, + uint128 liquidity, + uint128 liquidityX + ) internal pure returns (uint256 costX, uint128 acquireY, uint128 newLiquidityX) { + uint256 liquidityY = liquidity - liquidityX; + // desireY * 2^96 <= 2^128 * 2^96 <= 2^224 < 2^256 + uint256 maxTransformLiquidityX = mulDivCeil(uint256(desireY), TwoPower.Pow96, sqrtPrice_96); + // transformLiquidityX <= liquidityY <= uint128.max + uint128 transformLiquidityX = uint128(MaxMinMath.min256(maxTransformLiquidityX, liquidityY)); + // transformLiquidityX * 2^96 <= 2^128 * 2^96 <= 2^224 < 2^256 + costX = mulDivCeil(transformLiquidityX, TwoPower.Pow96, sqrtPrice_96); + // acquireY should not > uint128.max + uint256 acquireY256 = MulDivMath.mulDivFloor(transformLiquidityX, sqrtPrice_96, TwoPower.Pow96); + acquireY = Converter.toUint128(acquireY256); + newLiquidityX = liquidityX + transformLiquidityX; + } + + struct Range { + uint128 liquidity; + uint160 sqrtPriceL_96; + int24 leftPt; + uint160 sqrtPriceR_96; + int24 rightPt; + uint160 sqrtRate_96; + } + + struct RangeCompRet { + uint256 costX; + uint128 acquireY; + bool completeLiquidity; + int24 locPt; + uint160 sqrtLoc_96; + } + + function x2YRangeComplete( + Range memory rg, + uint128 desireY + ) internal pure returns ( + RangeCompRet memory ret + ) { + uint256 maxY = AmountMath.getAmountY(rg.liquidity, rg.sqrtPriceL_96, rg.sqrtPriceR_96, rg.sqrtRate_96, false); + if (maxY <= desireY) { + // maxY <= desireY <= uint128.max + ret.acquireY = uint128(maxY); + ret.costX = AmountMath.getAmountX(rg.liquidity, rg.leftPt, rg.rightPt, rg.sqrtPriceR_96, rg.sqrtRate_96, true); + ret.completeLiquidity = true; + return ret; + } + // 1. desireY * (rg.sqrtRate_96 - 2^96) + // < 2^128 * 2^96 + // = 2 ^ 224 < 2 ^ 256 + // 2. desireY < maxY = rg.liquidity * (rg.sqrtPriceR_96 - rg.sqrtPriceL_96) / (rg.sqrtRate_96 - 2^96) + // here, '/' means div of int + // desireY < rg.liquidity * (rg.sqrtPriceR_96 - rg.sqrtPriceL_96) / (rg.sqrtRate_96 - 2^96) + // => desireY * (rg.sqrtRate_96 - TwoPower.Pow96) / rg.liquidity < rg.sqrtPriceR_96 - rg.sqrtPriceL_96 + // => rg.sqrtPriceR_96 - desireY * (rg.sqrtRate_96 - TwoPower.Pow96) / rg.liquidity > rg.sqrtPriceL_96 + uint160 cl = uint160(uint256(rg.sqrtPriceR_96) - uint256(desireY) * (rg.sqrtRate_96 - TwoPower.Pow96) / rg.liquidity); + + ret.locPt = LogPowMath.getLogSqrtPriceFloor(cl) + 1; + + ret.locPt = MaxMinMath.min(ret.locPt, rg.rightPt); + ret.locPt = MaxMinMath.max(ret.locPt, rg.leftPt + 1); + ret.completeLiquidity = false; + + if (ret.locPt == rg.rightPt) { + ret.costX = 0; + ret.acquireY = 0; + ret.locPt = ret.locPt - 1; + ret.sqrtLoc_96 = LogPowMath.getSqrtPrice(ret.locPt); + } else { + // rg.rightPt - ret.locPt <= 256 * 100 + // sqrtPricePrMloc_96 <= 1.0001 ** 25600 * 2 ^ 96 = 13 * 2^96 < 2^100 + uint160 sqrtPricePrMloc_96 = LogPowMath.getSqrtPrice(rg.rightPt - ret.locPt); + // rg.sqrtPriceR_96 * TwoPower.Pow96 < 2^160 * 2^96 = 2^256 + uint160 sqrtPricePrM1_96 = uint160(mulDivCeil(rg.sqrtPriceR_96, TwoPower.Pow96, rg.sqrtRate_96)); + // rg.liquidity * (sqrtPricePrMloc_96 - TwoPower.Pow96) < 2^128 * 2^100 = 2^228 < 2^256 + ret.costX = mulDivCeil(rg.liquidity, sqrtPricePrMloc_96 - TwoPower.Pow96, rg.sqrtPriceR_96 - sqrtPricePrM1_96); + + ret.locPt = ret.locPt - 1; + ret.sqrtLoc_96 = LogPowMath.getSqrtPrice(ret.locPt); + + uint160 sqrtLocA1_96 = uint160( + uint256(ret.sqrtLoc_96) + + uint256(ret.sqrtLoc_96) * (uint256(rg.sqrtRate_96) - TwoPower.Pow96) / TwoPower.Pow96 + ); + uint256 acquireY256 = AmountMath.getAmountY(rg.liquidity, sqrtLocA1_96, rg.sqrtPriceR_96, rg.sqrtRate_96, false); + // ret.acquireY <= desireY <= uint128.max + ret.acquireY = uint128(MaxMinMath.min256(acquireY256, desireY)); + } + } + + /// @notice Compute amount of tokens exchanged during swapX2YDesireY and some amount values (currX, currY, allX) on final point + /// after this swap. + /// @param currentState state values containing (currX, currY, allX) of start point + /// @param leftPt left most point during this swap + /// @param sqrtRate_96 sqrt(1.0001) + /// @param desireY amount of Y user wants to buy + /// @return retState amount of token acquired and some values on final point + function x2YRange( + State memory currentState, + int24 leftPt, + uint160 sqrtRate_96, + uint128 desireY + ) internal pure returns ( + RangeRetState memory retState + ) { + retState.costX = 0; + retState.acquireY = 0; + retState.finished = false; + + bool currentHasY = (currentState.liquidityX < currentState.liquidity); + if (currentHasY && (currentState.liquidityX > 0 || leftPt == currentState.currentPoint)) { + (retState.costX, retState.acquireY, retState.liquidityX) = x2YAtPriceLiquidity( + desireY, currentState.sqrtPrice_96, currentState.liquidity, currentState.liquidityX + ); + if (retState.liquidityX < currentState.liquidity || retState.acquireY >= desireY) { + // remaining desire y is not enough to down current price to price / 1.0001 + // but desire y may remain, so we cannot simply use (retState.acquireY >= desireY) + retState.finished = true; + retState.finalPt = currentState.currentPoint; + retState.sqrtFinalPrice_96 = currentState.sqrtPrice_96; + } else { + desireY -= retState.acquireY; + } + } else if (currentHasY) { // all y + currentState.currentPoint = currentState.currentPoint + 1; + // sqrt(price) + sqrt(price) * (1.0001 - 1) == sqrt(price) * 1.0001 + currentState.sqrtPrice_96 = uint160( + uint256(currentState.sqrtPrice_96) + + uint256(currentState.sqrtPrice_96) * (uint256(sqrtRate_96) - TwoPower.Pow96) / TwoPower.Pow96 + ); + } else { + retState.liquidityX = currentState.liquidityX; + } + if (retState.finished) { + return retState; + } + + if (leftPt < currentState.currentPoint) { + uint160 sqrtPriceL_96 = LogPowMath.getSqrtPrice(leftPt); + RangeCompRet memory ret = x2YRangeComplete( + Range({ + liquidity: currentState.liquidity, + sqrtPriceL_96: sqrtPriceL_96, + leftPt: leftPt, + sqrtPriceR_96: currentState.sqrtPrice_96, + rightPt: currentState.currentPoint, + sqrtRate_96: sqrtRate_96 + }), + desireY + ); + retState.costX += ret.costX; + desireY -= ret.acquireY; + retState.acquireY += ret.acquireY; + if (ret.completeLiquidity) { + retState.finished = (desireY == 0); + retState.finalPt = leftPt; + retState.sqrtFinalPrice_96 = sqrtPriceL_96; + retState.liquidityX = currentState.liquidity; + } else { + // locPt > leftPt + uint256 locCostX; + uint128 locAcquireY; + // trade at locPt + (locCostX, locAcquireY, retState.liquidityX) = x2YAtPriceLiquidity( + desireY, ret.sqrtLoc_96, currentState.liquidity, 0 + ); + + retState.costX += locCostX; + retState.acquireY += locAcquireY; + retState.finished = true; + retState.sqrtFinalPrice_96 = ret.sqrtLoc_96; + retState.finalPt = ret.locPt; + } + } else { + // finishd must be false + // retState.finished == false; + retState.finalPt = currentState.currentPoint; + retState.sqrtFinalPrice_96 = currentState.sqrtPrice_96; + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathY2X.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathY2X.sol new file mode 100644 index 000000000..2999fcb2d --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathY2X.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import './MulDivMath.sol'; +import './TwoPower.sol'; +import './AmountMath.sol'; +import './State.sol'; +import './MaxMinMath.sol'; +import './Converter.sol'; + +library SwapMathY2X { + + struct RangeRetState { + // whether user has run out of tokenY + bool finished; + // actual cost of tokenY to buy tokenX + uint128 costY; + // actual amount of tokenX acquired + uint256 acquireX; + // final point after this swap + int24 finalPt; + // sqrt price on final point + uint160 sqrtFinalPrice_96; + // liquidity of tokenX at finalPt + // if finalPt is not rightPt, liquidityX is meaningless + uint128 liquidityX; + } + + function y2XAtPrice( + uint128 amountY, + uint160 sqrtPrice_96, + uint128 currX + ) internal pure returns (uint128 costY, uint128 acquireX) { + uint256 l = MulDivMath.mulDivFloor(amountY, TwoPower.Pow96, sqrtPrice_96); + // acquireX <= currX <= uint128.max + acquireX = uint128(MaxMinMath.min256(MulDivMath.mulDivFloor(l, TwoPower.Pow96, sqrtPrice_96), currX)); + l = MulDivMath.mulDivCeil(acquireX, sqrtPrice_96, TwoPower.Pow96); + uint256 cost = MulDivMath.mulDivCeil(l, sqrtPrice_96, TwoPower.Pow96); + // costY = cost <= amountY <= uint128.max + costY = uint128(cost); + } + + function y2XAtPriceLiquidity( + uint128 amountY, + uint160 sqrtPrice_96, + uint128 liquidityX + ) internal pure returns (uint128 costY, uint256 acquireX, uint128 newLiquidityX) { + // amountY * TwoPower.Pow96 < 2^128 * 2^96 = 2^224 < 2^256 + uint256 maxTransformLiquidityY = uint256(amountY) * TwoPower.Pow96 / sqrtPrice_96; + // transformLiquidityY <= liquidityX + uint128 transformLiquidityY = uint128(MaxMinMath.min256(maxTransformLiquidityY, liquidityX)); + // costY <= amountY + costY = uint128(MulDivMath.mulDivCeil(transformLiquidityY, sqrtPrice_96, TwoPower.Pow96)); + // transformLiquidityY * 2^96 < 2^224 < 2^256 + acquireX = uint256(transformLiquidityY) * TwoPower.Pow96 / sqrtPrice_96; + newLiquidityX = liquidityX - transformLiquidityY; + } + + struct Range { + uint128 liquidity; + uint160 sqrtPriceL_96; + int24 leftPt; + uint160 sqrtPriceR_96; + int24 rightPt; + uint160 sqrtRate_96; + } + struct RangeCompRet { + uint128 costY; + uint256 acquireX; + bool completeLiquidity; + int24 locPt; + uint160 sqrtLoc_96; + } + + function y2XRangeComplete( + Range memory rg, + uint128 amountY + ) internal pure returns ( + RangeCompRet memory ret + ) { + uint256 maxY = AmountMath.getAmountY(rg.liquidity, rg.sqrtPriceL_96, rg.sqrtPriceR_96, rg.sqrtRate_96, true); + if (maxY <= amountY) { + // ret.costY <= maxY <= uint128.max + ret.costY = uint128(maxY); + ret.acquireX = AmountMath.getAmountX(rg.liquidity, rg.leftPt, rg.rightPt, rg.sqrtPriceR_96, rg.sqrtRate_96, false); + // we complete this liquidity segment + ret.completeLiquidity = true; + } else { + // we should locate highest price + // uint160 is enough for muldiv and adding, because amountY < maxY + uint160 sqrtLoc_96 = uint160(MulDivMath.mulDivFloor( + amountY, + rg.sqrtRate_96 - TwoPower.Pow96, + rg.liquidity + ) + rg.sqrtPriceL_96); + ret.locPt = LogPowMath.getLogSqrtPriceFloor(sqrtLoc_96); + + ret.locPt = MaxMinMath.max(rg.leftPt, ret.locPt); + ret.locPt = MaxMinMath.min(rg.rightPt - 1, ret.locPt); + + ret.completeLiquidity = false; + ret.sqrtLoc_96 = LogPowMath.getSqrtPrice(ret.locPt); + if (ret.locPt == rg.leftPt) { + ret.costY = 0; + ret.acquireX = 0; + return ret; + } + + uint256 costY256 = AmountMath.getAmountY( + rg.liquidity, + rg.sqrtPriceL_96, + ret.sqrtLoc_96, + rg.sqrtRate_96, + true + ); + // ret.costY <= amountY <= uint128.max + ret.costY = uint128(MaxMinMath.min256(costY256, amountY)); + + // costY <= amountY even if the costY is the upperbound of the result + // because amountY is not a real and sqrtLoc_96 <= sqrtLoc256_96 + ret.acquireX = AmountMath.getAmountX( + rg.liquidity, + rg.leftPt, + ret.locPt, + ret.sqrtLoc_96, + rg.sqrtRate_96, + false + ); + + } + } + + /// @notice Compute amount of tokens exchanged during swapY2X and some amount values (currX, currY, allX) on final point + /// after this swap. + /// @param currentState state values containing (currX, currY, allX) of start point + /// @param rightPt right most point during this swap + /// @param sqrtRate_96 sqrt(1.0001) + /// @param amountY max amount of Y user willing to pay + /// @return retState amount of token acquired and some values on final point + function y2XRange( + State memory currentState, + int24 rightPt, + uint160 sqrtRate_96, + uint128 amountY + ) internal pure returns ( + RangeRetState memory retState + ) { + retState.costY = 0; + retState.acquireX = 0; + retState.finished = false; + // first, if current point is not all x, we can not move right directly + bool startHasY = (currentState.liquidityX < currentState.liquidity); + if (startHasY) { + (retState.costY, retState.acquireX, retState.liquidityX) = y2XAtPriceLiquidity( + amountY, + currentState.sqrtPrice_96, + currentState.liquidityX + ); + if (retState.liquidityX > 0 || retState.costY >= amountY) { + // it means remaining y is not enough to rise current price to price*1.0001 + // but y may remain, so we cannot simply use (costY == amountY) + retState.finished = true; + retState.finalPt = currentState.currentPoint; + retState.sqrtFinalPrice_96 = currentState.sqrtPrice_96; + return retState; + } else { + // y not run out + // not finsihed + amountY -= retState.costY; + currentState.currentPoint += 1; + if (currentState.currentPoint == rightPt) { + retState.finalPt = currentState.currentPoint; + // get fixed sqrt price to reduce accumulated error + retState.sqrtFinalPrice_96 = LogPowMath.getSqrtPrice(rightPt); + return retState; + } + // sqrt(price) + sqrt(price) * (1.0001 - 1) == sqrt(price) * 1.0001 + currentState.sqrtPrice_96 = uint160( + uint256(currentState.sqrtPrice_96) + + uint256(currentState.sqrtPrice_96) * (uint256(sqrtRate_96) - TwoPower.Pow96) / TwoPower.Pow96 + ); + } + } + + uint160 sqrtPriceR_96 = LogPowMath.getSqrtPrice(rightPt); + + // (uint128 liquidCostY, uint256 liquidAcquireX, bool liquidComplete, int24 locPt, uint160 sqrtLoc_96) + RangeCompRet memory ret = y2XRangeComplete( + Range({ + liquidity: currentState.liquidity, + sqrtPriceL_96: currentState.sqrtPrice_96, + leftPt: currentState.currentPoint, + sqrtPriceR_96: sqrtPriceR_96, + rightPt: rightPt, + sqrtRate_96: sqrtRate_96 + }), + amountY + ); + + retState.costY += ret.costY; + amountY -= ret.costY; + retState.acquireX += ret.acquireX; + if (ret.completeLiquidity) { + retState.finished = (amountY == 0); + retState.finalPt = rightPt; + retState.sqrtFinalPrice_96 = sqrtPriceR_96; + } else { + // trade at locPt + uint128 locCostY; + uint256 locAcquireX; + + (locCostY, locAcquireX, retState.liquidityX) = y2XAtPriceLiquidity(amountY, ret.sqrtLoc_96, currentState.liquidity); + + retState.costY += locCostY; + retState.acquireX += locAcquireX; + retState.finished = true; + retState.sqrtFinalPrice_96 = ret.sqrtLoc_96; + retState.finalPt = ret.locPt; + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathY2XDesire.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathY2XDesire.sol new file mode 100644 index 000000000..2bf6bce0d --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/SwapMathY2XDesire.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./MulDivMath.sol"; +import "./TwoPower.sol"; +import "./AmountMath.sol"; +import "./State.sol"; +import "./MaxMinMath.sol"; +import "./Converter.sol"; + +library SwapMathY2XDesire { + + struct RangeRetState { + // whether user acquires enough tokenX + bool finished; + // actual cost of tokenY to buy tokenX + uint256 costY; + // actual amount of tokenX acquired + uint128 acquireX; + // final point after this swap + int24 finalPt; + // sqrt price on final point + uint160 sqrtFinalPrice_96; + // liquidity of tokenX at finalPt + // if finalPt is not rightPt, liquidityX is meaningless + uint128 liquidityX; + } + + function y2XAtPrice( + uint128 desireX, + uint160 sqrtPrice_96, + uint128 currX + ) internal pure returns (uint128 costY, uint128 acquireX) { + acquireX = MaxMinMath.min(desireX, currX); + uint256 l = MulDivMath.mulDivCeil(acquireX, sqrtPrice_96, TwoPower.Pow96); + // costY should <= uint128.max + costY = Converter.toUint128(MulDivMath.mulDivCeil(l, sqrtPrice_96, TwoPower.Pow96)); + } + + function y2XAtPriceLiquidity( + uint128 desireX, + uint160 sqrtPrice_96, + uint128 liquidityX + ) internal pure returns (uint256 costY, uint128 acquireX, uint128 newLiquidityX) { + uint256 maxTransformLiquidityY = MulDivMath.mulDivCeil(desireX, sqrtPrice_96, TwoPower.Pow96); + // transformLiquidityY <= liquidityX <= uint128.max + uint128 transformLiquidityY = uint128(MaxMinMath.min256(maxTransformLiquidityY, liquidityX)); + costY = MulDivMath.mulDivCeil(transformLiquidityY, sqrtPrice_96, TwoPower.Pow96); + // transformLiquidityY * TwoPower.Pow96 < 2^128 * 2^96 = 2^224 < 2^256 + acquireX = Converter.toUint128(uint256(transformLiquidityY) * TwoPower.Pow96 / sqrtPrice_96); + newLiquidityX = liquidityX - transformLiquidityY; + } + + struct Range { + uint128 liquidity; + uint160 sqrtPriceL_96; + int24 leftPt; + uint160 sqrtPriceR_96; + int24 rightPt; + uint160 sqrtRate_96; + } + + struct RangeCompRet { + uint256 costY; + uint128 acquireX; + bool completeLiquidity; + int24 locPt; + uint160 sqrtLoc_96; + } + + function y2XRangeComplete( + Range memory rg, + uint128 desireX + ) internal pure returns ( + RangeCompRet memory ret + ) { + uint256 maxX = AmountMath.getAmountX(rg.liquidity, rg.leftPt, rg.rightPt, rg.sqrtPriceR_96, rg.sqrtRate_96, false); + if (maxX <= desireX) { + // maxX <= desireX <= uint128.max + ret.acquireX = uint128(maxX); + ret.costY = AmountMath.getAmountY(rg.liquidity, rg.sqrtPriceL_96, rg.sqrtPriceR_96, rg.sqrtRate_96, true); + ret.completeLiquidity = true; + return ret; + } + + uint256 sqrtPricePrPl_96 = LogPowMath.getSqrtPrice(rg.rightPt - rg.leftPt); + // rg.sqrtPriceR_96 * 2^96 < 2^160 * 2^96 = 2^256 + uint160 sqrtPricePrM1_96 = uint160(uint256(rg.sqrtPriceR_96) * TwoPower.Pow96 / rg.sqrtRate_96); + + // div must be > 2^96 because, if + // div <= 2^96 + // <=> sqrtPricePrPl_96 - desireX * (sqrtPriceR_96 - sqrtPricePrM1_96) / liquidity <= 2^96 (here, '/' is div of int) + // <=> desireX >= (sqrtPricePrPl_96 - 2^96) * liquidity / (sqrtPriceR_96 - sqrtPricePrM1_96) + // <=> desireX >= maxX + // will enter the branch above and return + uint256 div = sqrtPricePrPl_96 - MulDivMath.mulDivFloor(desireX, rg.sqrtPriceR_96 - sqrtPricePrM1_96, rg.liquidity); + + // 1. rg.sqrtPriceR_96 * 2^96 < 2^160 * 2^96 = 2^256 + // 2. sqrtPriceLoc_96 must < rg.sqrtPriceR_96, because div > 2^96 + uint256 sqrtPriceLoc_96 = uint256(rg.sqrtPriceR_96) * TwoPower.Pow96 / div; + + ret.completeLiquidity = false; + ret.locPt = LogPowMath.getLogSqrtPriceFloor(uint160(sqrtPriceLoc_96)); + + ret.locPt = MaxMinMath.max(rg.leftPt, ret.locPt); + ret.locPt = MaxMinMath.min(rg.rightPt - 1, ret.locPt); + ret.sqrtLoc_96 = LogPowMath.getSqrtPrice(ret.locPt); + + if (ret.locPt == rg.leftPt) { + ret.acquireX = 0; + ret.costY = 0; + return ret; + } + + ret.completeLiquidity = false; + // ret.acquireX <= desireX <= uint128.max + ret.acquireX = uint128(MaxMinMath.min256(AmountMath.getAmountX( + rg.liquidity, + rg.leftPt, + ret.locPt, + ret.sqrtLoc_96, + rg.sqrtRate_96, + false + ), desireX)); + + ret.costY = AmountMath.getAmountY( + rg.liquidity, + rg.sqrtPriceL_96, + ret.sqrtLoc_96, + rg.sqrtRate_96, + true + ); + } + + /// @notice Compute amount of tokens exchanged during swapY2XDesireY and some amount values (currX, currY, allX) on final point + /// after this swap. + /// @param currentState state values containing (currX, currY, allX) of start point + /// @param rightPt right most point during this swap + /// @param sqrtRate_96 sqrt(1.0001) + /// @param desireX amount of tokenX user wants to buy + /// @return retState amount of token acquired and some values on final point + function y2XRange( + State memory currentState, + int24 rightPt, + uint160 sqrtRate_96, + uint128 desireX + ) internal pure returns ( + RangeRetState memory retState + ) { + retState.costY = 0; + retState.acquireX = 0; + retState.finished = false; + // first, if current point is not all x, we can not move right directly + bool startHasY = (currentState.liquidityX < currentState.liquidity); + if (startHasY) { + (retState.costY, retState.acquireX, retState.liquidityX) = y2XAtPriceLiquidity(desireX, currentState.sqrtPrice_96, currentState.liquidityX); + if (retState.liquidityX > 0 || retState.acquireX >= desireX) { + // currX remain, means desire runout + retState.finished = true; + retState.finalPt = currentState.currentPoint; + retState.sqrtFinalPrice_96 = currentState.sqrtPrice_96; + return retState; + } else { + // not finished + desireX -= retState.acquireX; + currentState.currentPoint += 1; + if (currentState.currentPoint == rightPt) { + retState.finalPt = currentState.currentPoint; + // get fixed sqrt price to reduce accumulated error + retState.sqrtFinalPrice_96 = LogPowMath.getSqrtPrice(rightPt); + return retState; + } + // sqrt(price) + sqrt(price) * (1.0001 - 1) == sqrt(price) * 1.0001 + currentState.sqrtPrice_96 = uint160( + uint256(currentState.sqrtPrice_96) + + uint256(currentState.sqrtPrice_96) * (uint256(sqrtRate_96) - TwoPower.Pow96) / TwoPower.Pow96 + ); + } + } + + uint160 sqrtPriceR_96 = LogPowMath.getSqrtPrice(rightPt); + RangeCompRet memory ret = y2XRangeComplete( + Range({ + liquidity: currentState.liquidity, + sqrtPriceL_96: currentState.sqrtPrice_96, + leftPt: currentState.currentPoint, + sqrtPriceR_96: sqrtPriceR_96, + rightPt: rightPt, + sqrtRate_96: sqrtRate_96 + }), + desireX + ); + retState.costY += ret.costY; + retState.acquireX += ret.acquireX; + desireX -= ret.acquireX; + + if (ret.completeLiquidity) { + retState.finished = (desireX == 0); + retState.finalPt = rightPt; + retState.sqrtFinalPrice_96 = sqrtPriceR_96; + } else { + uint256 locCostY; + uint128 locAcquireX; + (locCostY, locAcquireX, retState.liquidityX) = y2XAtPriceLiquidity(desireX, ret.sqrtLoc_96, currentState.liquidity); + retState.costY += locCostY; + retState.acquireX += locAcquireX; + retState.finished = true; + retState.finalPt = ret.locPt; + retState.sqrtFinalPrice_96 = ret.sqrtLoc_96; + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/TokenTransfer.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/TokenTransfer.sol new file mode 100644 index 000000000..a8539066e --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/TokenTransfer.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../interfaces/IERC20Minimal.sol"; + +library TokenTransfer { + + function transferToken( + address tokenAddr, + address toAddr, + uint256 amount + ) internal { + (bool ok, bytes memory retData) = + tokenAddr.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, toAddr, amount)); + require(ok && (retData.length == 0 || abi.decode(retData, (bool))), 'TNS'); + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/TwoPower.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/TwoPower.sol new file mode 100644 index 000000000..7445f611e --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/TwoPower.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +library TwoPower { + + uint256 internal constant Pow128 = 0x100000000000000000000000000000000; + uint256 internal constant Pow96 = 0x1000000000000000000000000; + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/libraries/UserEarn.sol b/contracts/dependencies/izumi/izumi-swap-core/libraries/UserEarn.sol new file mode 100644 index 000000000..d0d884d77 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/libraries/UserEarn.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./MulDivMath.sol"; +import "./TwoPower.sol"; +import "./Converter.sol"; +import "./MaxMinMath.sol"; + +library UserEarn { + + // describe user's earning info for a limit order + struct Data { + // total amount of earned token by all users at this point + // with same direction (sell x or sell y) as of the last update(add/dec) + uint256 lastAccEarn; + // remaing amount of token on sale in this limit order + uint128 sellingRemain; + // uncollected decreased token + uint128 sellingDec; + // unassigned and unlegacy earned token + // earned token before collected need to be assigned + uint128 earn; + // unassigned and legacy earned token + uint128 legacyEarn; + // assigned but uncollected earned token (both unlegacy and legacy) + uint128 earnAssign; + } + + function get( + mapping(bytes32 => Data) storage self, + address user, + int24 point + ) internal view returns (UserEarn.Data storage data) { + data = self[keccak256(abi.encodePacked(user, point))]; + } + + /// @notice update UserEarn info for an unlegacy (uncleared during swap) limit order. + /// update strategy is 'first claim first earn', etc, earned token will be transformed for + /// limit orders which is update first + /// @param self UserEarn storage object of target limit order + /// @param currAccEarn 'accEarn' value of corresponding point order on swap pool. + /// accumulate amount of earned token + /// @param sqrtPrice_96 describe price of limit order + /// @param totalEarn remained(undistributed) amount of earned token of unlegacy limit order on corresponding point + /// @param isEarnY direction of corresponding limit order. + function updateUnlegacyOrder( + UserEarn.Data storage self, + uint256 currAccEarn, + uint160 sqrtPrice_96, + uint128 totalEarn, + bool isEarnY + ) internal returns (uint128 totalEarnRemain, uint128 claimSold, uint128 claimEarn) { + Data memory data = self; + + // first, we compute how many earned token remained on the point order + uint256 earn = currAccEarn - data.lastAccEarn; + if (earn > totalEarn) { + earn = totalEarn; + } + // second, compute how many sold token according to the 'first claim first earn' strategy, + // etc, for earnY, sold = min(sellingRemain, earn / price) + // for earnX, sold = min(sellingRemain, earn * price) + uint256 sold; + if (isEarnY) { + uint256 l = MulDivMath.mulDivCeil(earn, TwoPower.Pow96, sqrtPrice_96); + sold = MulDivMath.mulDivCeil(l, TwoPower.Pow96, sqrtPrice_96); + } else { + uint256 l = MulDivMath.mulDivCeil(earn, sqrtPrice_96, TwoPower.Pow96); + sold = MulDivMath.mulDivCeil(l, sqrtPrice_96, TwoPower.Pow96); + } + if (sold > data.sellingRemain) { + sold = data.sellingRemain; + if (isEarnY) { + uint256 l = MulDivMath.mulDivFloor(sold, sqrtPrice_96, TwoPower.Pow96); + earn = MulDivMath.mulDivFloor(l, sqrtPrice_96, TwoPower.Pow96); + } else { + uint256 l = MulDivMath.mulDivFloor(sold, TwoPower.Pow96, sqrtPrice_96); + earn = MulDivMath.mulDivFloor(l, TwoPower.Pow96, sqrtPrice_96); + } + } + // sold1 = ceil(ceil(earn1 * Q / P) * Q / P) + // if sold1 <= data.sellingRemain, earn = earn1 <= totalEarn, sold=sold1 <= data.sellingRemain + // if sold1 > data.sellingRemain, sold = data.sellingRemain + // sold1 - 1 < ceil(ceil(earn1 * Q / P) * Q / P) + // => sold1 - 1 < ceil(earn1 * Q / P) * Q / P + // => floor((sold1 - 1) * P / Q) < ceil(earn1 * Q / P) + // => floor((sold1 - 1) * P / Q) < earn1 * Q / P + // => earn = floor(floor((sold1 - 1) * P / Q) * P / Q) < earn1 <= totalEarn + + // thirdly, update info of userEarn object + + // earn <= totalEarn + data.earn += uint128(earn); + // sold <= data.sellingRemain + data.sellingRemain -= uint128(sold); + self.lastAccEarn = currAccEarn; + if (earn > 0) { + self.earn = data.earn; + } + if (sold > 0) { + self.sellingRemain = data.sellingRemain; + } + claimSold = uint128(sold); + claimEarn = uint128(earn); + // earn <= totalEarn + totalEarnRemain = totalEarn - claimEarn; + } + + /// @notice update UserEarn info for an unlegacy (uncleared during swap) limit order. + /// and then add some amount of selling token + /// update strategy is 'first claim first earn', etc, earned token will be transformed for + /// limit orders which is update first + /// @param self UserEarn storage object of target limit order + /// @param currAccEarn 'accEarn' value of corresponding point order on swap pool. + /// accumulate amount of earned token + /// @param sqrtPrice_96 describe price of limit order + /// @param totalEarn remained(undistributed) amount of earned token of unlegacy limit order on corresponding point + /// @param isEarnY direction of corresponding limit order. + function addUnlegacyOrder( + UserEarn.Data storage self, + uint128 delta, + uint256 currAccEarn, + uint160 sqrtPrice_96, + uint128 totalEarn, + bool isEarnY + ) internal returns(uint128 totalEarnRemain, uint128 claimSold, uint128 claimEarn) { + // first, call `updateUnlegacyOrder` to update unlegacy order + (totalEarnRemain, claimSold, claimEarn) = updateUnlegacyOrder(self, currAccEarn, sqrtPrice_96, totalEarn, isEarnY); + // then, add + self.sellingRemain = self.sellingRemain + delta; + } + + /// @notice update UserEarn info for an unlegacy (uncleared during swap) limit order. + /// and then decrease some amount of selling token (if remain) + /// update strategy is 'first claim first earn', etc, earned token will be transformed for + /// limit orders which is update first + /// @param self UserEarn storage object of target limit order + /// @param currAccEarn 'accEarn' value of corresponding point order on swap pool. + /// accumulate amount of earned token + /// @param sqrtPrice_96 describe price of limit order + /// @param totalEarn remained(undistributed) amount of earned token of unlegacy limit order on corresponding point + /// @param isEarnY direction of corresponding limit order. + function decUnlegacyOrder( + UserEarn.Data storage self, + uint128 delta, + uint256 currAccEarn, + uint160 sqrtPrice_96, + uint128 totalEarn, + bool isEarnY + ) internal returns(uint128 actualDelta, uint128 totalEarnRemain, uint128 claimSold, uint128 claimEarn) { + // first, call `updateUnlegacyOrder` to update unlegacy order + (totalEarnRemain, claimSold, claimEarn) = updateUnlegacyOrder(self, currAccEarn, sqrtPrice_96, totalEarn, isEarnY); + // then decrease + actualDelta = MaxMinMath.min(delta, self.sellingRemain); + self.sellingRemain = self.sellingRemain - actualDelta; + self.sellingDec = self.sellingDec + actualDelta; + } + + /// @notice update UserEarn info for a legacy (cleared during swap) limit order. + /// an limit order we call it 'legacy' if it together with other limit order of same + /// direction and same point on the pool is cleared during one time of exchanging. + /// if an limit order is convinced to be 'legacy', we should mark it as 'sold out', + /// etc, transform all its remained selling token to earned token. + /// @param self UserEarn storage object of target limit order + /// @param addDelta addition of selling amount + /// @param currAccEarn 'accEarn' value of corresponding point order on swap pool. + /// accumulate amount of earned token + /// @param sqrtPrice_96 describe price of limit order + /// @param totalLegacyEarn remained(undistributed) amount of earned token of legacy limit order on corresponding point + /// @param isEarnY direction of corresponding limit order. + function updateLegacyOrder( + UserEarn.Data storage self, + uint128 addDelta, + uint256 currAccEarn, + uint160 sqrtPrice_96, + uint128 totalLegacyEarn, + bool isEarnY + ) internal returns(uint128 totalLegacyEarnRemain, uint128 sold, uint128 claimedEarn) { + sold = self.sellingRemain; + uint256 earn = 0; + if (sold > 0) { + // transform all its remained selling token to earned token. + if (isEarnY) { + uint256 l = MulDivMath.mulDivFloor(sold, sqrtPrice_96, TwoPower.Pow96); + // for earnY, earn = sold * price + earn = MulDivMath.mulDivFloor(l, sqrtPrice_96, TwoPower.Pow96); + } else { + uint256 l = MulDivMath.mulDivFloor(sold, TwoPower.Pow96, sqrtPrice_96); + // for earnX, earn = sold / price + earn = MulDivMath.mulDivFloor(l, TwoPower.Pow96, sqrtPrice_96); + } + if (earn > totalLegacyEarn) { + earn = totalLegacyEarn; + } + self.sellingRemain = 0; + // count earned token into legacyEarn field, not earn field + self.legacyEarn += uint128(earn); + } + uint128 pastEarn = self.earn; + if (pastEarn > 0) { + // add userEarn.earn into legacyEarn + // because this userEarn is legacy + self.legacyEarn += pastEarn; + self.earn = 0; + } + // claimedEarn should not involve pastEarn + claimedEarn = uint128(earn); + self.lastAccEarn = currAccEarn; + // because 1. totalLegacyEarn on pointOrder will minus claimedEarn + // and totalLegacyEarn does not involve pastEarn + // and 2. pastEarn must have been claimed before + totalLegacyEarnRemain = totalLegacyEarn - claimedEarn; + if (addDelta > 0) { + // sellingRemain has been clear to 0 + self.sellingRemain = addDelta; + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/limitOrder.sol b/contracts/dependencies/izumi/izumi-swap-core/limitOrder.sol new file mode 100644 index 000000000..489e8ec22 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/limitOrder.sol @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./interfaces/IiZiSwapPool.sol"; +import "./interfaces/IiZiSwapCallback.sol"; + +import "./libraries/Liquidity.sol"; +import "./libraries/Point.sol"; +import "./libraries/PointBitmap.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LimitOrder.sol"; +import "./libraries/SwapMathY2X.sol"; +import "./libraries/SwapMathX2Y.sol"; +import "./libraries/SwapMathY2XDesire.sol"; +import "./libraries/SwapMathX2YDesire.sol"; +import "./libraries/TokenTransfer.sol"; +import "./libraries/UserEarn.sol"; +import "./libraries/State.sol"; +import "./libraries/Oracle.sol"; +import "./libraries/OrderOrEndpoint.sol"; +import "./libraries/MaxMinMath.sol"; + +contract LimitOrderModule { + + using Liquidity for mapping(bytes32 =>Liquidity.Data); + using Liquidity for Liquidity.Data; + using Point for mapping(int24 =>Point.Data); + using Point for Point.Data; + using PointBitmap for mapping(int16 =>uint256); + using LimitOrder for LimitOrder.Data; + using UserEarn for UserEarn.Data; + using UserEarn for mapping(bytes32 =>UserEarn.Data); + using SwapMathY2X for SwapMathY2X.RangeRetState; + using SwapMathX2Y for SwapMathX2Y.RangeRetState; + using Oracle for Oracle.Observation[65535]; + using OrderOrEndpoint for mapping(int24 =>int24); + + int24 internal constant LEFT_MOST_PT = -800000; + int24 internal constant RIGHT_MOST_PT = 800000; + + /// @notice left most point regularized by pointDelta + int24 public leftMostPt; + /// @notice right most point regularized by pointDelta + int24 public rightMostPt; + /// @notice maximum liquidSum for each point, see points() in IiZiSwapPool or library Point + uint128 public maxLiquidPt; + + /// @notice address of iZiSwapFactory + address public factory; + + /// @notice address of tokenX + address public tokenX; + /// @notice address of tokenY + address public tokenY; + /// @notice fee amount of this swap pool, 3000 means 0.3% + uint24 public fee; + + /// @notice minimum number of distance between initialized or limitorder points + int24 public pointDelta; + + /// @notice the fee growth as a 128-bit fixpoing fees of tokenX collected per 1 liquidity of the pool + uint256 public feeScaleX_128; + /// @notice the fee growth as a 128-bit fixpoing fees of tokenY collected per 1 liquidity of the pool + uint256 public feeScaleY_128; + + uint160 sqrtRate_96; + + /// @notice some values of pool + /// see library State or IiZiSwapPool#state for more infomation + State public state; + + /// @notice the information about a liquidity by the liquidity's key + mapping(bytes32 =>Liquidity.Data) public liquidities; + + /// @notice 256 packed point (orderOrEndpoint>0) boolean values. See PointBitmap for more information + mapping(int16 =>uint256) public pointBitmap; + + /// @notice returns infomation of a point in the pool, see Point library of IiZiSwapPool#poitns for more information + mapping(int24 =>Point.Data) public points; + /// @notice infomation about a point whether has limit order and whether as an liquidity's endpoint + mapping(int24 =>int24) public orderOrEndpoint; + /// @notice limitOrder info on a given point + mapping(int24 =>LimitOrder.Data) public limitOrderData; + /// @notice information about a user's limit order (sell tokenY and earn tokenX) + mapping(bytes32 => UserEarn.Data) public userEarnX; + /// @notice information about a user's limit order (sell tokenX and earn tokenY) + mapping(bytes32 => UserEarn.Data) public userEarnY; + /// @notice observation data array + Oracle.Observation[65535] public observations; + + uint256 public totalFeeXCharged; + uint256 public totalFeeYCharged; + + address private original; + + address private swapModuleX2Y; + address private swapModuleY2X; + address private liquidityModule; + address private limitOrderModule; + address private flashModule; + + /// @notice percent to charge from miner's fee + uint24 public feeChargePercent; + + function balanceX() private view returns (uint256) { + (bool success, bytes memory data) = + tokenX.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + function balanceY() private view returns (uint256) { + (bool success, bytes memory data) = + tokenY.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + /// Delegate call implementation for IiZiSwapPool#assignLimOrderEarnY. + function assignLimOrderEarnY( + int24 point, + uint128 assignY, + bool fromLegacy + ) external returns (uint128 actualAssignY) { + actualAssignY = assignY; + UserEarn.Data storage ue = userEarnY.get(msg.sender, point); + if (fromLegacy) { + if (actualAssignY > ue.legacyEarn) { + actualAssignY = ue.legacyEarn; + } + ue.legacyEarn -= actualAssignY; + } else { + if (actualAssignY > ue.earn) { + actualAssignY = ue.earn; + } + ue.earn -= actualAssignY; + } + ue.earnAssign += actualAssignY; + } + + /// Delegate call implementation for IiZiSwapPool#assignLimOrderEarnX. + function assignLimOrderEarnX( + int24 point, + uint128 assignX, + bool fromLegacy + ) external returns (uint128 actualAssignX) { + actualAssignX = assignX; + UserEarn.Data storage ue = userEarnX.get(msg.sender, point); + if (fromLegacy) { + if (actualAssignX > ue.legacyEarn) { + actualAssignX = ue.legacyEarn; + } + ue.legacyEarn -= actualAssignX; + } else { + if (actualAssignX > ue.earn) { + actualAssignX = ue.earn; + } + ue.earn -= actualAssignX; + } + ue.earnAssign += actualAssignX; + } + + /// Delegate call implementation for IiZiSwapPool#decLimOrderWithX. + function decLimOrderWithX( + int24 point, + uint128 deltaX + ) external returns (uint128 actualDeltaX, uint256 legacyAccEarn, uint128 claimSold, uint128 claimEarn) { + require(point % pointDelta == 0, "PD"); + + UserEarn.Data storage ue = userEarnY.get(msg.sender, point); + LimitOrder.Data storage pointOrder = limitOrderData[point]; + uint160 sqrtPrice_96 = LogPowMath.getSqrtPrice(point); + legacyAccEarn = pointOrder.legacyAccEarnY; + if (legacyAccEarn > ue.lastAccEarn) { + (pointOrder.legacyEarnY, claimSold, claimEarn) = ue.updateLegacyOrder(0, pointOrder.accEarnY, sqrtPrice_96, pointOrder.legacyEarnY, true); + } else { + (actualDeltaX, pointOrder.earnY, claimSold, claimEarn) = ue.decUnlegacyOrder(deltaX, pointOrder.accEarnY, sqrtPrice_96, pointOrder.earnY, true); + pointOrder.sellingX -= actualDeltaX; + + if (actualDeltaX > 0 && pointOrder.sellingX == 0) { + int24 newVal = orderOrEndpoint.getOrderOrEndptVal(point, pointDelta) & 1; + orderOrEndpoint.setOrderOrEndptVal(point, pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(point, pointDelta); + } + } + } + + } + + /// Delegate call implementation for IiZiSwapPool#decLimOrderWithY. + function decLimOrderWithY( + int24 point, + uint128 deltaY + ) external returns (uint128 actualDeltaY, uint256 legacyAccEarn, uint128 claimSold, uint128 claimEarn) { + require(point % pointDelta == 0, "PD"); + + UserEarn.Data storage ue = userEarnX.get(msg.sender, point); + LimitOrder.Data storage pointOrder = limitOrderData[point]; + uint160 sqrtPrice_96 = LogPowMath.getSqrtPrice(point); + legacyAccEarn = pointOrder.legacyAccEarnX; + if (legacyAccEarn > ue.lastAccEarn) { + (pointOrder.legacyEarnX, claimSold, claimEarn) = ue.updateLegacyOrder(0, pointOrder.accEarnX, sqrtPrice_96, pointOrder.legacyEarnX, false); + } else { + (actualDeltaY, pointOrder.earnX, claimSold, claimEarn) = ue.decUnlegacyOrder(deltaY, pointOrder.accEarnX, sqrtPrice_96, pointOrder.earnX, false); + + pointOrder.sellingY -= actualDeltaY; + + if (actualDeltaY > 0 && pointOrder.sellingY == 0) { + int24 newVal = orderOrEndpoint.getOrderOrEndptVal(point, pointDelta) & 1; + orderOrEndpoint.setOrderOrEndptVal(point, pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(point, pointDelta); + } + } + } + + } + + struct AddLimOrderCacheData { + uint128 currX; + uint128 currY; + uint128 costOffset; + } + + /// Delegate call implementation for IiZiSwapPool#allLimOrderWithX. + function addLimOrderWithX( + address recipient, + int24 point, + uint128 amountX, + bytes calldata data + ) external returns (uint128 orderX, uint128 acquireY, uint128 claimSold, uint128 claimEarn) { + require(point % pointDelta == 0, "PD"); + require(point >= state.currentPoint, "PG"); + require(point <= rightMostPt, "HO"); + require(amountX > 0, "XP"); + + // update point order + LimitOrder.Data storage pointOrder = limitOrderData[point]; + + orderX = amountX; + acquireY = 0; + uint160 sqrtPrice_96 = LogPowMath.getSqrtPrice(point); + + AddLimOrderCacheData memory addLimOrderCacheData = AddLimOrderCacheData({ + currY: pointOrder.sellingY, + currX: pointOrder.sellingX, + costOffset: 0 + }); + + if (addLimOrderCacheData.currY > 0) { + (addLimOrderCacheData.costOffset, acquireY) = SwapMathX2Y.x2YAtPrice(amountX, sqrtPrice_96, addLimOrderCacheData.currY); + orderX -= addLimOrderCacheData.costOffset; + addLimOrderCacheData.currY -= acquireY; + pointOrder.accEarnX = pointOrder.accEarnX + addLimOrderCacheData.costOffset; + pointOrder.earnX = pointOrder.earnX + addLimOrderCacheData.costOffset; + pointOrder.sellingY = addLimOrderCacheData.currY; + if (addLimOrderCacheData.currY > 0) { + orderX = 0; + } + } + + if (orderX > 0) { + addLimOrderCacheData.currX += orderX; + pointOrder.sellingX = addLimOrderCacheData.currX; + } + + UserEarn.Data storage ue = userEarnY.get(recipient, point); + if (ue.lastAccEarn < pointOrder.legacyAccEarnY) { + (pointOrder.legacyEarnY, claimSold, claimEarn) = ue.updateLegacyOrder(orderX, pointOrder.accEarnY, sqrtPrice_96, pointOrder.legacyEarnY, true); + } else { + (pointOrder.earnY, claimSold, claimEarn) = ue.addUnlegacyOrder(orderX, pointOrder.accEarnY, sqrtPrice_96, pointOrder.earnY, true); + } + ue.earnAssign = ue.earnAssign + acquireY; + + // update statusval and bitmap + if (addLimOrderCacheData.currX == 0 && addLimOrderCacheData.currY == 0) { + int24 val = orderOrEndpoint.getOrderOrEndptVal(point, pointDelta); + // val & 2 != 0, because currX == 0, but amountX > 0 + int24 newVal = val & 1; + orderOrEndpoint.setOrderOrEndptVal(point, pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(point, pointDelta); + } + } else { + int24 val = orderOrEndpoint.getOrderOrEndptVal(point, pointDelta); + if (val & 2 == 0) { + int24 newVal = val | 2; + orderOrEndpoint.setOrderOrEndptVal(point, pointDelta, newVal); + if (val == 0) { + pointBitmap.setOne(point, pointDelta); + } + } + } + require(orderX + addLimOrderCacheData.costOffset > 0, 'p>0'); + + // trader pay x + uint256 bx = balanceX(); + IiZiSwapAddLimOrderCallback(msg.sender).payCallback(orderX + addLimOrderCacheData.costOffset, 0, data); + require(balanceX() >= bx + orderX + addLimOrderCacheData.costOffset, "XE"); + } + + /// Delegate call implementation for IiZiSwapPool#addLimOrderWithY. + function addLimOrderWithY( + address recipient, + int24 point, + uint128 amountY, + bytes calldata data + ) external returns (uint128 orderY, uint128 acquireX, uint128 claimSold, uint128 claimEarn) { + require(point % pointDelta == 0, "PD"); + require(point <= state.currentPoint, "PL"); + require(point >= leftMostPt, "LO"); + require(amountY > 0, "YP"); + + // update point order + LimitOrder.Data storage pointOrder = limitOrderData[point]; + + orderY = amountY; + acquireX = 0; + uint160 sqrtPrice_96 = LogPowMath.getSqrtPrice(point); + + AddLimOrderCacheData memory addLimOrderCacheData = AddLimOrderCacheData({ + currY: pointOrder.sellingY, + currX: pointOrder.sellingX, + costOffset: 0 + }); + + if (addLimOrderCacheData.currX > 0) { + (addLimOrderCacheData.costOffset, acquireX) = SwapMathY2X.y2XAtPrice(amountY, sqrtPrice_96, addLimOrderCacheData.currX); + orderY -= addLimOrderCacheData.costOffset; + addLimOrderCacheData.currX -= acquireX; + pointOrder.accEarnY = pointOrder.accEarnY + addLimOrderCacheData.costOffset; + pointOrder.earnY = pointOrder.earnY + addLimOrderCacheData.costOffset; + pointOrder.sellingX = addLimOrderCacheData.currX; + if (addLimOrderCacheData.currX > 0) { + orderY = 0; + } + } + + if (orderY > 0) { + addLimOrderCacheData.currY += orderY; + pointOrder.sellingY = addLimOrderCacheData.currY; + } + + UserEarn.Data storage ue = userEarnX.get(recipient, point); + if (pointOrder.legacyAccEarnX > ue.lastAccEarn) { + (pointOrder.legacyEarnX, claimSold, claimEarn) = ue.updateLegacyOrder(orderY, pointOrder.accEarnX, sqrtPrice_96, pointOrder.legacyEarnX, false); + } else { + (pointOrder.earnX, claimSold, claimEarn) = ue.addUnlegacyOrder(orderY, pointOrder.accEarnX, sqrtPrice_96, pointOrder.earnX, false); + } + ue.earnAssign = ue.earnAssign + acquireX; + + // update statusval and bitmap + if (addLimOrderCacheData.currX == 0 && addLimOrderCacheData.currY == 0) { + int24 val = orderOrEndpoint.getOrderOrEndptVal(point, pointDelta); + // val & 2 != 0, because currY == 0, but amountY > 0 + int24 newVal = val & 1; + orderOrEndpoint.setOrderOrEndptVal(point, pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(point, pointDelta); + } + } else { + int24 val = orderOrEndpoint.getOrderOrEndptVal(point, pointDelta); + if (val & 2 == 0) { + int24 newVal = val | 2; + orderOrEndpoint.setOrderOrEndptVal(point, pointDelta, newVal); + if (val == 0) { + pointBitmap.setOne(point, pointDelta); + } + } + } + + require(orderY + addLimOrderCacheData.costOffset > 0, 'p>0'); + + // trader pay y + uint256 by = balanceY(); + IiZiSwapAddLimOrderCallback(msg.sender).payCallback(0, orderY + addLimOrderCacheData.costOffset, data); + require(balanceY() >= by + orderY + addLimOrderCacheData.costOffset, "YE"); + } + + /// Delegate call implementation for IiZiSwapPool#collectLimOrder. + function collectLimOrder( + address recipient, int24 point, uint128 collectDec, uint128 collectEarn, bool isEarnY + ) external returns(uint128 actualCollectDec, uint128 actualCollectEarn) { + UserEarn.Data storage ue = isEarnY? userEarnY.get(msg.sender, point) : userEarnX.get(msg.sender, point); + actualCollectDec = collectDec; + if (actualCollectDec > ue.sellingDec) { + actualCollectDec = ue.sellingDec; + } + ue.sellingDec = ue.sellingDec - actualCollectDec; + actualCollectEarn = collectEarn; + if (actualCollectEarn > ue.earnAssign) { + actualCollectEarn = ue.earnAssign; + } + ue.earnAssign = ue.earnAssign - actualCollectEarn; + (uint256 x, uint256 y) = isEarnY? (actualCollectDec, actualCollectEarn): (actualCollectEarn, actualCollectDec); + x = MaxMinMath.min256(x, balanceX()); + y = MaxMinMath.min256(y, balanceY()); + if (x > 0) { + TokenTransfer.transferToken(tokenX, recipient, x); + } + if (y > 0) { + TokenTransfer.transferToken(tokenY, recipient, y); + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/liquidity.sol b/contracts/dependencies/izumi/izumi-swap-core/liquidity.sol new file mode 100644 index 000000000..b1b5aee59 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/liquidity.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./interfaces/IiZiSwapPool.sol"; +import "./interfaces/IiZiSwapCallback.sol"; + +import "./libraries/Liquidity.sol"; +import "./libraries/Point.sol"; +import "./libraries/PointBitmap.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LimitOrder.sol"; +import "./libraries/SwapMathY2X.sol"; +import "./libraries/SwapMathX2Y.sol"; +import "./libraries/SwapMathY2XDesire.sol"; +import "./libraries/SwapMathX2YDesire.sol"; +import "./libraries/TokenTransfer.sol"; +import "./libraries/UserEarn.sol"; +import "./libraries/State.sol"; +import "./libraries/Oracle.sol"; +import "./libraries/OrderOrEndpoint.sol"; +import "./libraries/MaxMinMath.sol"; + +contract LiquidityModule { + + using Liquidity for mapping(bytes32 =>Liquidity.Data); + using Liquidity for Liquidity.Data; + using Point for mapping(int24 =>Point.Data); + using Point for Point.Data; + using PointBitmap for mapping(int16 =>uint256); + using LimitOrder for LimitOrder.Data; + using UserEarn for UserEarn.Data; + using UserEarn for mapping(bytes32 =>UserEarn.Data); + using SwapMathY2X for SwapMathY2X.RangeRetState; + using SwapMathX2Y for SwapMathX2Y.RangeRetState; + using Oracle for Oracle.Observation[65535]; + using OrderOrEndpoint for mapping(int24 =>int24); + + int24 internal constant LEFT_MOST_PT = -800000; + int24 internal constant RIGHT_MOST_PT = 800000; + + /// @notice left most point regularized by pointDelta + int24 public leftMostPt; + /// @notice right most point regularized by pointDelta + int24 public rightMostPt; + /// @notice maximum liquidSum for each point, see points() in IiZiSwapPool or library Point + uint128 public maxLiquidPt; + + /// @notice address of iZiSwapFactory + address public factory; + + /// @notice address of tokenX + address public tokenX; + /// @notice address of tokenY + address public tokenY; + /// @notice fee amount of this swap pool, 3000 means 0.3% + uint24 public fee; + + /// @notice minimum number of distance between initialized or limitorder points + int24 public pointDelta; + + /// @notice the fee growth as a 128-bit fixpoing fees of tokenX collected per 1 liquidity of the pool + uint256 public feeScaleX_128; + /// @notice the fee growth as a 128-bit fixpoing fees of tokenY collected per 1 liquidity of the pool + uint256 public feeScaleY_128; + + uint160 sqrtRate_96; + + /// @notice some values of pool + /// see library State or IiZiSwapPool#state for more infomation + State public state; + + /// @notice the information about a liquidity by the liquidity's key + mapping(bytes32 =>Liquidity.Data) public liquidities; + + /// @notice 256 packed point (orderOrEndpoint>0) boolean values. See PointBitmap for more information + mapping(int16 =>uint256) public pointBitmap; + + /// @notice returns infomation of a point in the pool, see Point library of IiZiSwapPool#poitns for more information + mapping(int24 =>Point.Data) public points; + /// @notice infomation about a point whether has limit order and whether as an liquidity's endpoint + mapping(int24 =>int24) public orderOrEndpoint; + /// @notice limitOrder info on a given point + mapping(int24 =>LimitOrder.Data) public limitOrderData; + /// @notice information about a user's limit order (sell tokenY and earn tokenX) + mapping(bytes32 => UserEarn.Data) public userEarnX; + /// @notice information about a user's limit order (sell tokenX and earn tokenY) + mapping(bytes32 => UserEarn.Data) public userEarnY; + /// @notice observation data array + Oracle.Observation[65535] public observations; + + uint256 public totalFeeXCharged; + uint256 public totalFeeYCharged; + + address private original; + + address private swapModuleX2Y; + address private swapModuleY2X; + address private liquidityModule; + address private limitOrderModule; + address private flashModule; + + /// @notice percent to charge from miner's fee + uint24 public feeChargePercent; + + // data used when removing liquidity + struct WithdrawRet { + // total amount of tokenX refund after withdraw + uint256 x; + // total amount of tokenY refund after withdraw + uint256 y; + // amount of refund tokenX at current point after withdraw + uint256 xc; + // amount of refund tokenY at current point after withdraw + uint256 yc; + // value of liquidityX at current point after withdraw + uint128 liquidityX; + } + + function balanceX() private view returns (uint256) { + (bool success, bytes memory data) = + tokenX.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + function balanceY() private view returns (uint256) { + (bool success, bytes memory data) = + tokenY.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + /// @dev Add / Dec liquidity + /// @param minter the minter of the liquidity + /// @param leftPoint left endpoint of the segment + /// @param rightPoint right endpoint of the segment, [leftPoint, rightPoint) + /// @param delta delta liquidity, positive for adding + /// @param currentPoint current price point on the axies + function _updateLiquidity( + address minter, + int24 leftPoint, + int24 rightPoint, + int128 delta, + int24 currentPoint + ) private { + int24 pd = pointDelta; + Liquidity.Data storage lq = liquidities.get(minter, leftPoint, rightPoint); + (uint256 mFeeScaleX_128, uint256 mFeeScaleY_128) = (feeScaleX_128, feeScaleY_128); + bool leftFlipped; + bool rightFlipped; + // update points + if (delta != 0) { + // add / dec liquidity + leftFlipped = points.updateEndpoint(leftPoint, true, currentPoint, delta, maxLiquidPt, mFeeScaleX_128, mFeeScaleY_128); + rightFlipped = points.updateEndpoint(rightPoint, false, currentPoint, delta, maxLiquidPt, mFeeScaleX_128, mFeeScaleY_128); + } + // get sub fee scale of the range + (uint256 accFeeXIn_128, uint256 accFeeYIn_128) = + points.getSubFeeScale( + leftPoint, rightPoint, currentPoint, mFeeScaleX_128, mFeeScaleY_128 + ); + lq.update(delta, accFeeXIn_128, accFeeYIn_128); + // update bitmap + if (leftFlipped) { + int24 leftVal = orderOrEndpoint.getOrderOrEndptVal(leftPoint, pd); + if (delta > 0) { + orderOrEndpoint.setOrderOrEndptVal(leftPoint, pd, leftVal | 1); + if (leftVal == 0) { + pointBitmap.setOne(leftPoint, pd); + } + } else { + int24 newVal = leftVal & 2; + orderOrEndpoint.setOrderOrEndptVal(leftPoint, pd, newVal); + if (newVal == 0) { + pointBitmap.setZero(leftPoint, pd); + } + delete points[leftPoint]; + } + } + if (rightFlipped) { + int24 rightVal = orderOrEndpoint.getOrderOrEndptVal(rightPoint, pd); + if (delta > 0) { + orderOrEndpoint.setOrderOrEndptVal(rightPoint, pd, rightVal | 1); + if (rightVal == 0) { + pointBitmap.setOne(rightPoint, pd); + } + } else { + int24 newVal = rightVal & 2; + orderOrEndpoint.setOrderOrEndptVal(rightPoint, pd, newVal); + if (newVal == 0) { + pointBitmap.setZero(rightPoint, pd); + } + delete points[rightPoint]; + } + } + } + + function _computeDepositYc( + uint128 liquidDelta, + uint160 sqrtPrice_96 + ) private pure returns (uint128 y) { + // to simplify computation, + // minter is required to deposit only token y in point of current price + uint256 amount = MulDivMath.mulDivCeil( + liquidDelta, + sqrtPrice_96, + TwoPower.Pow96 + ); + y = uint128(amount); + require (y == amount, "YC OFL"); + } + + /// @dev [leftPoint, rightPoint) + function _computeDepositXY( + uint128 liquidDelta, + int24 leftPoint, + int24 rightPoint, + State memory currentState + ) private view returns (uint128 x, uint128 y, uint128 yc) { + x = 0; + uint256 amountY = 0; + int24 pc = currentState.currentPoint; + uint160 sqrtPrice_96 = currentState.sqrtPrice_96; + uint160 sqrtPriceR_96 = LogPowMath.getSqrtPrice(rightPoint); + uint160 _sqrtRate_96 = sqrtRate_96; + if (leftPoint < pc) { + uint160 sqrtPriceL_96 = LogPowMath.getSqrtPrice(leftPoint); + uint256 yl; + if (rightPoint < pc) { + yl = AmountMath.getAmountY(liquidDelta, sqrtPriceL_96, sqrtPriceR_96, _sqrtRate_96, true); + } else { + yl = AmountMath.getAmountY(liquidDelta, sqrtPriceL_96, sqrtPrice_96, _sqrtRate_96, true); + } + amountY += yl; + } + if (rightPoint > pc) { + // we need compute XR + int24 xrLeft = (leftPoint > pc) ? leftPoint : pc + 1; + uint256 xr = AmountMath.getAmountX( + liquidDelta, + xrLeft, + rightPoint, + sqrtPriceR_96, + _sqrtRate_96, + true + ); + x = uint128(xr); + require(x == xr, "XOFL"); + } + if (leftPoint <= pc && rightPoint > pc) { + // we need compute yc at point of current price + yc = _computeDepositYc( + liquidDelta, + sqrtPrice_96 + ); + amountY += yc; + } else { + yc = 0; + } + y = uint128(amountY); + require(y == amountY, "YOFL"); + } + + /// @notice Compute some values (refund token amount, currX/currY in state) when removing liquidity + /// @param liquidDelta amount of liquidity user wants to withdraw + /// @param leftPoint left endpoint of liquidity + /// @param rightPoint right endpoint of liquidity + /// @param currentState current state values of pool + /// @return withRet a WithdrawRet struct object containing values computed, see WithdrawRet for more information + function _computeWithdrawXY( + uint128 liquidDelta, + int24 leftPoint, + int24 rightPoint, + State memory currentState + ) private view returns (WithdrawRet memory withRet) { + uint256 amountY = 0; + uint256 amountX = 0; + int24 pc = currentState.currentPoint; + uint160 sqrtPrice_96 = currentState.sqrtPrice_96; + uint160 sqrtPriceR_96 = LogPowMath.getSqrtPrice(rightPoint); + uint160 _sqrtRate_96 = sqrtRate_96; + if (leftPoint < pc) { + uint160 sqrtPriceL_96 = LogPowMath.getSqrtPrice(leftPoint); + uint256 yl; + if (rightPoint < pc) { + yl = AmountMath.getAmountY(liquidDelta, sqrtPriceL_96, sqrtPriceR_96, _sqrtRate_96, false); + } else { + yl = AmountMath.getAmountY(liquidDelta, sqrtPriceL_96, sqrtPrice_96, _sqrtRate_96, false); + } + amountY += yl; + } + if (rightPoint > pc) { + // we need compute XR + int24 xrLeft = (leftPoint > pc) ? leftPoint : pc + 1; + uint256 xr = AmountMath.getAmountX( + liquidDelta, + xrLeft, + rightPoint, + sqrtPriceR_96, + _sqrtRate_96, + false + ); + amountX += xr; + } + if (leftPoint <= pc && rightPoint > pc) { + uint128 originLiquidityY = currentState.liquidity - currentState.liquidityX; + uint128 withdrawedLiquidityY = (originLiquidityY < liquidDelta) ? originLiquidityY : liquidDelta; + uint128 withdrawedLiquidityX = liquidDelta - withdrawedLiquidityY; + withRet.yc = MulDivMath.mulDivFloor(withdrawedLiquidityY, sqrtPrice_96, TwoPower.Pow96); + // withdrawedLiquidityX * 2^96 < 2^128*2^96=2^224<2^256 + withRet.xc = uint256(withdrawedLiquidityX) * TwoPower.Pow96 / sqrtPrice_96; + withRet.liquidityX = currentState.liquidityX - withdrawedLiquidityX; + amountY += withRet.yc; + amountX += withRet.xc; + } else { + withRet.yc = 0; + withRet.xc = 0; + withRet.liquidityX = currentState.liquidityX; + } + withRet.y = uint128(amountY); + require(withRet.y == amountY, "YOFL"); + withRet.x = uint128(amountX); + require(withRet.x == amountX, "XOFL"); + } + + /// Delegate call implementation for IiZiSwapPool#mint. + function mint( + address recipient, + int24 leftPt, + int24 rightPt, + uint128 liquidDelta, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY) { + require(leftPt < rightPt, "LR"); + require(leftPt >= leftMostPt, "LO"); + require(rightPt <= rightMostPt, "HO"); + require(int256(rightPt) - int256(leftPt) < RIGHT_MOST_PT, "TL"); + int24 pd = pointDelta; + require(leftPt % pd == 0, "LPD"); + require(rightPt % pd == 0, "RPD"); + int128 ld = int128(liquidDelta); + require(ld > 0, "LP"); + if (recipient == address(0)) { + recipient = msg.sender; + } + State memory currentState = state; + // add a liquidity segment to the pool + _updateLiquidity( + recipient, + leftPt, + rightPt, + ld, + currentState.currentPoint + ); + // compute amount of tokenx and tokeny should be paid from minter + (uint256 x, uint256 y, uint256 yc) = _computeDepositXY( + liquidDelta, + leftPt, + rightPt, + currentState + ); + // update state + if (yc > 0) { + // if (!currentState.allX) { + // state.currY = currentState.currY + yc; + // } else { + // state.allX = false; + // state.currX = MulDivMath.mulDivFloor(currentState.liquidity, TwoPower.Pow96, currentState.sqrtPrice_96); + // state.currY = yc; + // } + state.liquidity = currentState.liquidity + liquidDelta; + } + uint256 bx; + uint256 by; + if (x > 0) { + bx = balanceX(); + } + if (y > 0) { + by = balanceY(); + } + if (x > 0 || y > 0) { + // minter's callback to pay + IiZiSwapMintCallback(msg.sender).mintDepositCallback(x, y, data); + } + if (x > 0) { + require(bx + x <= balanceX(), "NEX"); // not enough x from minter + } + if (y > 0) { + require(by + y <= balanceY(), "NEY"); // not enough y from minter + } + amountX = x; + amountY = y; + } + + /// Delegate call implementation for IiZiSwapPool#burn. + function burn( + int24 leftPt, + int24 rightPt, + uint128 liquidDelta + ) external returns (uint256 amountX, uint256 amountY) { + // it is not necessary to check leftPt rightPt with [leftMostPt, rightMostPt] + // because we haved checked it in the mint(...) + require(leftPt < rightPt, "LR"); + int24 pd = pointDelta; + require(leftPt % pd == 0, "LPD"); + require(rightPt % pd == 0, "RPD"); + State memory currentState = state; + uint128 liquidity = currentState.liquidity; + // add a liquidity segment to the pool + require(liquidDelta <= uint128(type(int128).max), 'LQ127'); + int256 nlDelta = -int256(uint256(liquidDelta)); + require(int128(nlDelta) == nlDelta, "DO"); + _updateLiquidity( + msg.sender, + leftPt, + rightPt, + int128(nlDelta), + currentState.currentPoint + ); + // compute amount of tokenx and tokeny should be paid from minter + WithdrawRet memory withRet = _computeWithdrawXY( + liquidDelta, + leftPt, + rightPt, + currentState + ); + // update state + if (withRet.yc > 0 || withRet.xc > 0) { + state.liquidity = liquidity - liquidDelta; + state.liquidityX = withRet.liquidityX; + } + if (withRet.x > 0 || withRet.y > 0) { + Liquidity.Data storage lq = liquidities.get(msg.sender, leftPt, rightPt); + lq.tokenOwedX += withRet.x; + lq.tokenOwedY += withRet.y; + } + return (withRet.x, withRet.y); + } + + /// Delegate call implementation for IiZiSwapPool#collect. + function collect( + address recipient, + int24 leftPt, + int24 rightPt, + uint256 amountXLim, + uint256 amountYLim + ) external returns (uint256 actualAmountX, uint256 actualAmountY) { + require(amountXLim > 0 || amountYLim > 0, "X+Y>0"); + Liquidity.Data storage lq = liquidities.get(msg.sender, leftPt, rightPt); + actualAmountX = amountXLim; + if (actualAmountX > lq.tokenOwedX) { + actualAmountX = lq.tokenOwedX; + } + actualAmountY = amountYLim; + if (actualAmountY > lq.tokenOwedY) { + actualAmountY = lq.tokenOwedY; + } + lq.tokenOwedX -= actualAmountX; + lq.tokenOwedY -= actualAmountY; + + actualAmountX = MaxMinMath.min256(actualAmountX, balanceX()); + actualAmountY = MaxMinMath.min256(actualAmountY, balanceY()); + if (actualAmountX > 0) { + TokenTransfer.transferToken(tokenX, recipient, actualAmountX); + } + if (actualAmountY > 0) { + TokenTransfer.transferToken(tokenY, recipient, actualAmountY); + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-core/swapX2Y.sol b/contracts/dependencies/izumi/izumi-swap-core/swapX2Y.sol new file mode 100644 index 000000000..ac315586f --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/swapX2Y.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./interfaces/IiZiSwapPool.sol"; +import "./interfaces/IiZiSwapCallback.sol"; + +import "./libraries/Liquidity.sol"; +import "./libraries/Point.sol"; +import "./libraries/PointBitmap.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LimitOrder.sol"; +import "./libraries/SwapMathY2X.sol"; +import "./libraries/SwapMathX2Y.sol"; +import "./libraries/SwapMathY2XDesire.sol"; +import "./libraries/SwapMathX2YDesire.sol"; +import "./libraries/TokenTransfer.sol"; +import "./libraries/UserEarn.sol"; +import "./libraries/State.sol"; +import "./libraries/Oracle.sol"; +import "./libraries/OrderOrEndpoint.sol"; +import "./libraries/MaxMinMath.sol"; +import "./libraries/SwapCache.sol"; + +contract SwapX2YModule { + + using Liquidity for mapping(bytes32 =>Liquidity.Data); + using Liquidity for Liquidity.Data; + using Point for mapping(int24 =>Point.Data); + using Point for Point.Data; + using PointBitmap for mapping(int16 =>uint256); + using LimitOrder for LimitOrder.Data; + using UserEarn for UserEarn.Data; + using UserEarn for mapping(bytes32 =>UserEarn.Data); + using SwapMathY2X for SwapMathY2X.RangeRetState; + using SwapMathX2Y for SwapMathX2Y.RangeRetState; + using Oracle for Oracle.Observation[65535]; + using OrderOrEndpoint for mapping(int24 =>int24); + + int24 internal constant LEFT_MOST_PT = -800000; + int24 internal constant RIGHT_MOST_PT = 800000; + + /// @notice left most point regularized by pointDelta + int24 public leftMostPt; + /// @notice right most point regularized by pointDelta + int24 public rightMostPt; + /// @notice maximum liquidSum for each point, see points() in IiZiSwapPool or library Point + uint128 public maxLiquidPt; + + /// @notice address of iZiSwapFactory + address public factory; + + /// @notice address of tokenX + address public tokenX; + /// @notice address of tokenY + address public tokenY; + /// @notice fee amount of this swap pool, 3000 means 0.3% + uint24 public fee; + + /// @notice minimum number of distance between initialized or limitorder points + int24 public pointDelta; + + /// @notice the fee growth as a 128-bit fixpoing fees of tokenX collected per 1 liquidity of the pool + uint256 public feeScaleX_128; + /// @notice the fee growth as a 128-bit fixpoing fees of tokenY collected per 1 liquidity of the pool + uint256 public feeScaleY_128; + + uint160 sqrtRate_96; + + /// @notice some values of pool + /// see library State or IiZiSwapPool#state for more infomation + State public state; + + /// @notice the information about a liquidity by the liquidity's key + mapping(bytes32 =>Liquidity.Data) public liquidities; + + /// @notice 256 packed point (orderOrEndpoint>0) boolean values. See PointBitmap for more information + mapping(int16 =>uint256) public pointBitmap; + + /// @notice returns infomation of a point in the pool, see Point library of IiZiSwapPool#poitns for more information + mapping(int24 =>Point.Data) public points; + /// @notice infomation about a point whether has limit order and whether as an liquidity's endpoint + mapping(int24 =>int24) public orderOrEndpoint; + /// @notice limitOrder info on a given point + mapping(int24 =>LimitOrder.Data) public limitOrderData; + /// @notice information about a user's limit order (sell tokenY and earn tokenX) + mapping(bytes32 => UserEarn.Data) public userEarnX; + /// @notice information about a user's limit order (sell tokenX and earn tokenY) + mapping(bytes32 => UserEarn.Data) public userEarnY; + /// @notice observation data array + Oracle.Observation[65535] public observations; + + uint256 public totalFeeXCharged; + uint256 public totalFeeYCharged; + + address private original; + + address private swapModuleX2Y; + address private swapModuleY2X; + address private liquidityModule; + address private limitOrderModule; + address private flashModule; + + /// @notice percent to charge from miner's fee + uint24 public feeChargePercent; + + function balanceX() private view returns (uint256) { + (bool success, bytes memory data) = + tokenX.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + function balanceY() private view returns (uint256) { + (bool success, bytes memory data) = + tokenY.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + /// Delegate call implementation for IiZiSwapPool#swapX2Y. + function swapX2Y( + address recipient, + uint128 amount, + int24 lowPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY) { + require(amount > 0, "AP"); + + lowPt = MaxMinMath.max(lowPt, leftMostPt); + amountX = 0; + amountY = 0; + State memory st = state; + SwapCache memory cache; + cache.currFeeScaleX_128 = feeScaleX_128; + cache.currFeeScaleY_128 = feeScaleY_128; + cache.finished = false; + cache._sqrtRate_96 = sqrtRate_96; + cache.pointDelta = pointDelta; + cache.currentOrderOrEndpt = orderOrEndpoint.getOrderOrEndptVal(st.currentPoint, cache.pointDelta); + cache.startPoint = st.currentPoint; + cache.startLiquidity = st.liquidity; + cache.timestamp = uint32(block.timestamp); + + while (lowPt <= st.currentPoint && !cache.finished) { + + // step1: fill limit order + if (cache.currentOrderOrEndpt & 2 > 0) { + // amount <= uint128.max + uint128 amountNoFee = uint128(uint256(amount) * (1e6 - fee) / 1e6); + if (amountNoFee > 0) { + LimitOrder.Data storage od = limitOrderData[st.currentPoint]; + uint128 currY = od.sellingY; + (uint128 costX, uint128 acquireY) = SwapMathX2Y.x2YAtPrice( + amountNoFee, st.sqrtPrice_96, currY + ); + if (acquireY < currY || costX >= amountNoFee) { + cache.finished = true; + } + uint128 feeAmount; + if (costX >= amountNoFee) { + feeAmount = amount - costX; + } else { + // costX <= amountX <= uint128.max + feeAmount = uint128(uint256(costX) * fee / (1e6 - fee)); + uint256 mod = uint256(costX) * fee % (1e6 - fee); + if (mod > 0) { + feeAmount += 1; + } + } + totalFeeXCharged += feeAmount; + amount -= (costX + feeAmount); + amountX = amountX + costX + feeAmount; + amountY += acquireY; + currY -= acquireY; + od.sellingY = currY; + od.earnX += costX; + od.accEarnX += costX; + if (currY == 0) { + od.legacyEarnX += od.earnX; + od.legacyAccEarnX = od.accEarnX; + od.earnX = 0; + if (od.sellingX == 0) { + int24 newVal = cache.currentOrderOrEndpt & 1; + orderOrEndpoint.setOrderOrEndptVal(st.currentPoint, cache.pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(st.currentPoint, cache.pointDelta); + } + } + } + } else { + cache.finished = true; + } + } + if (cache.finished) { + break; + } + int24 searchStart = st.currentPoint - 1; + + // step2: clear the liquidity if the currentPoint is an endpoint + if (cache.currentOrderOrEndpt & 1 > 0) { + // amount <= uint128.max + uint128 amountNoFee = uint128(uint256(amount) * (1e6 - fee) / 1e6); + if (amountNoFee > 0) { + if (st.liquidity > 0) { + SwapMathX2Y.RangeRetState memory retState = SwapMathX2Y.x2YRange( + st, + st.currentPoint, + cache._sqrtRate_96, + amountNoFee + ); + cache.finished = retState.finished; + uint128 feeAmount; + if (retState.costX >= amountNoFee) { + feeAmount = amount - retState.costX; + } else { + // retState.costX <= amount <= uint128.max + feeAmount = uint128(uint256(retState.costX) * fee / (1e6 - fee)); + uint256 mod = uint256(retState.costX) * fee % (1e6 - fee); + if (mod > 0) { + feeAmount += 1; + } + } + uint256 chargedFeeAmount = uint256(feeAmount) * feeChargePercent / 100; + totalFeeXCharged += chargedFeeAmount; + + cache.currFeeScaleX_128 = cache.currFeeScaleX_128 + MulDivMath.mulDivFloor(feeAmount - chargedFeeAmount, TwoPower.Pow128, st.liquidity); + amountX = amountX + retState.costX + feeAmount; + amountY += retState.acquireY; + amount -= (retState.costX + feeAmount); + st.currentPoint = retState.finalPt; + st.sqrtPrice_96 = retState.sqrtFinalPrice_96; + st.liquidityX = retState.liquidityX; + } + if (!cache.finished) { + Point.Data storage pointdata = points[st.currentPoint]; + pointdata.passEndpoint(cache.currFeeScaleX_128, cache.currFeeScaleY_128); + st.liquidity = Liquidity.liquidityAddDelta(st.liquidity, - pointdata.liquidDelta); + st.currentPoint = st.currentPoint - 1; + st.sqrtPrice_96 = LogPowMath.getSqrtPrice(st.currentPoint); + st.liquidityX = 0; + } + } else { + cache.finished = true; + } + } + if (cache.finished || st.currentPoint < lowPt) { + break; + } + int24 nextPt= pointBitmap.nearestLeftOneOrBoundary(searchStart, cache.pointDelta); + if (nextPt < lowPt) { + nextPt = lowPt; + } + int24 nextVal = orderOrEndpoint.getOrderOrEndptVal(nextPt, cache.pointDelta); + + // in [nextPt, st.currentPoint) + if (st.liquidity == 0) { + // no liquidity in the range [nextPt, st.currentPoint] + st.currentPoint = nextPt; + st.sqrtPrice_96 = LogPowMath.getSqrtPrice(st.currentPoint); + // st.liquidityX must be 0 + cache.currentOrderOrEndpt = nextVal; + } else { + // amount > 0 + // amountNoFee <= amount <= uint128.max + uint128 amountNoFee = uint128(uint256(amount) * (1e6 - fee) / 1e6); + if (amountNoFee > 0) { + SwapMathX2Y.RangeRetState memory retState = SwapMathX2Y.x2YRange( + st, nextPt, cache._sqrtRate_96, amountNoFee + ); + cache.finished = retState.finished; + uint128 feeAmount; + if (retState.costX >= amountNoFee) { + feeAmount = amount - retState.costX; + } else { + // feeAmount <= retState.costX <= amount <= uint128.max + feeAmount = uint128(uint256(retState.costX) * fee / (1e6 - fee)); + uint256 mod = uint256(retState.costX) * fee % (1e6 - fee); + if (mod > 0) { + feeAmount += 1; + } + } + amountY += retState.acquireY; + amountX = amountX + retState.costX + feeAmount; + amount -= (retState.costX + feeAmount); + + uint256 chargedFeeAmount = uint256(feeAmount) * feeChargePercent / 100; + totalFeeXCharged += chargedFeeAmount; + + cache.currFeeScaleX_128 = cache.currFeeScaleX_128 + MulDivMath.mulDivFloor(feeAmount - chargedFeeAmount, TwoPower.Pow128, st.liquidity); + st.currentPoint = retState.finalPt; + st.sqrtPrice_96 = retState.sqrtFinalPrice_96; + st.liquidityX = retState.liquidityX; + } else { + cache.finished = true; + } + if (st.currentPoint == nextPt) { + cache.currentOrderOrEndpt = nextVal; + } else { + // not necessary, because finished must be true + cache.currentOrderOrEndpt = 0; + } + } + if (st.currentPoint <= lowPt) { + break; + } + } + + if (cache.startPoint != st.currentPoint) { + (st.observationCurrentIndex, st.observationQueueLen) = observations.append( + st.observationCurrentIndex, + cache.timestamp, + cache.startPoint, + st.observationQueueLen, + st.observationNextQueueLen + ); + } + + // write back fee scale, no fee of y + feeScaleX_128 = cache.currFeeScaleX_128; + // write back state + state = st; + require(amountY > 0, "PR"); + // transfer y to trader + TokenTransfer.transferToken(tokenY, recipient, amountY); + // trader pay x + require(amountX > 0, "PP"); + uint256 bx = balanceX(); + IiZiSwapCallback(msg.sender).swapX2YCallback(amountX, amountY, data); + require(balanceX() >= bx + amountX, "XE"); + + } + + /// Delegate call implementation for IiZiSwapPool#swapX2YDesireY. + function swapX2YDesireY( + address recipient, + uint128 desireY, + int24 lowPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY) { + require(desireY > 0, "AP"); + + lowPt = MaxMinMath.max(lowPt, leftMostPt); + amountX = 0; + amountY = 0; + State memory st = state; + SwapCache memory cache; + cache.currFeeScaleX_128 = feeScaleX_128; + cache.currFeeScaleY_128 = feeScaleY_128; + cache.finished = false; + cache._sqrtRate_96 = sqrtRate_96; + cache.pointDelta = pointDelta; + cache.currentOrderOrEndpt = orderOrEndpoint.getOrderOrEndptVal(st.currentPoint, cache.pointDelta); + cache.startPoint = st.currentPoint; + cache.startLiquidity = st.liquidity; + cache.timestamp = uint32(block.timestamp); + while (lowPt <= st.currentPoint && !cache.finished) { + // clear limit order first + if (cache.currentOrderOrEndpt & 2 > 0) { + LimitOrder.Data storage od = limitOrderData[st.currentPoint]; + uint128 currY = od.sellingY; + (uint128 costX, uint128 acquireY) = SwapMathX2YDesire.x2YAtPrice( + desireY, st.sqrtPrice_96, currY + ); + if (acquireY >= desireY) { + cache.finished = true; + } + + uint256 feeAmount = MulDivMath.mulDivCeil(costX, fee, 1e6 - fee); + totalFeeXCharged += feeAmount; + desireY = (desireY <= acquireY) ? 0 : desireY - acquireY; + amountX += (costX + feeAmount); + amountY += acquireY; + currY -= acquireY; + od.sellingY = currY; + od.earnX += costX; + od.accEarnX += costX; + if (currY == 0) { + od.legacyEarnX += od.earnX; + od.earnX = 0; + od.legacyAccEarnX = od.accEarnX; + if (od.sellingX == 0) { + int24 newVal = cache.currentOrderOrEndpt & 1; + orderOrEndpoint.setOrderOrEndptVal(st.currentPoint, cache.pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(st.currentPoint, cache.pointDelta); + } + } + } + } + if (cache.finished) { + break; + } + int24 searchStart = st.currentPoint - 1; + // second, clear the liquid if the currentPoint is an endpoint + if (cache.currentOrderOrEndpt & 1 > 0) { + if (st.liquidity > 0) { + SwapMathX2YDesire.RangeRetState memory retState = SwapMathX2YDesire.x2YRange( + st, + st.currentPoint, + cache._sqrtRate_96, + desireY + ); + cache.finished = retState.finished; + + uint256 feeAmount = MulDivMath.mulDivCeil(retState.costX, fee, 1e6 - fee); + uint256 chargedFeeAmount = feeAmount * feeChargePercent / 100; + totalFeeXCharged += chargedFeeAmount; + + cache.currFeeScaleX_128 = cache.currFeeScaleX_128 + MulDivMath.mulDivFloor(feeAmount - chargedFeeAmount, TwoPower.Pow128, st.liquidity); + amountX += (retState.costX + feeAmount); + amountY += retState.acquireY; + desireY -= MaxMinMath.min(desireY, retState.acquireY); + st.currentPoint = retState.finalPt; + st.sqrtPrice_96 = retState.sqrtFinalPrice_96; + st.liquidityX = retState.liquidityX; + } + if (!cache.finished) { + Point.Data storage pointdata = points[st.currentPoint]; + pointdata.passEndpoint(cache.currFeeScaleX_128, cache.currFeeScaleY_128); + st.liquidity = Liquidity.liquidityAddDelta(st.liquidity, - pointdata.liquidDelta); + st.currentPoint = st.currentPoint - 1; + st.sqrtPrice_96 = LogPowMath.getSqrtPrice(st.currentPoint); + st.liquidityX = 0; + } + } + if (cache.finished || st.currentPoint < lowPt) { + break; + } + int24 nextPt = pointBitmap.nearestLeftOneOrBoundary(searchStart, cache.pointDelta); + if (nextPt < lowPt) { + nextPt = lowPt; + } + int24 nextVal = orderOrEndpoint.getOrderOrEndptVal(nextPt, cache.pointDelta); + + // in [nextPt, st.currentPoint) + if (st.liquidity == 0) { + // no liquidity in the range [nextPt, st.currentPoint] + st.currentPoint = nextPt; + st.sqrtPrice_96 = LogPowMath.getSqrtPrice(st.currentPoint); + // st.liquidityX must be 0 + cache.currentOrderOrEndpt = nextVal; + } else { + // amount > 0 + SwapMathX2YDesire.RangeRetState memory retState = SwapMathX2YDesire.x2YRange( + st, nextPt, cache._sqrtRate_96, desireY + ); + cache.finished = retState.finished; + + uint256 feeAmount = MulDivMath.mulDivCeil(retState.costX, fee, 1e6 - fee); + uint256 chargedFeeAmount = feeAmount * feeChargePercent / 100; + totalFeeXCharged += chargedFeeAmount; + + amountY += retState.acquireY; + amountX += (retState.costX + feeAmount); + desireY -= MaxMinMath.min(desireY, retState.acquireY); + + cache.currFeeScaleX_128 = cache.currFeeScaleX_128 + MulDivMath.mulDivFloor(feeAmount - chargedFeeAmount, TwoPower.Pow128, st.liquidity); + + st.currentPoint = retState.finalPt; + st.sqrtPrice_96 = retState.sqrtFinalPrice_96; + st.liquidityX = retState.liquidityX; + + if (st.currentPoint == nextPt) { + cache.currentOrderOrEndpt = nextVal; + } else { + // not necessary, because finished must be true + cache.currentOrderOrEndpt = 0; + } + } + if (st.currentPoint <= lowPt) { + break; + } + } + if (cache.startPoint != st.currentPoint) { + (st.observationCurrentIndex, st.observationQueueLen) = observations.append( + st.observationCurrentIndex, + cache.timestamp, + cache.startPoint, + st.observationQueueLen, + st.observationNextQueueLen + ); + } + + // write back fee scale, no fee of y + feeScaleX_128 = cache.currFeeScaleX_128; + // write back state + state = st; + require(amountY > 0, "PR"); + // transfer y to trader + TokenTransfer.transferToken(tokenY, recipient, amountY); + // trader pay x + require(amountX > 0, "PP"); + uint256 bx = balanceX(); + IiZiSwapCallback(msg.sender).swapX2YCallback(amountX, amountY, data); + require(balanceX() >= bx + amountX, "XE"); + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-core/swapY2X.sol b/contracts/dependencies/izumi/izumi-swap-core/swapY2X.sol new file mode 100644 index 000000000..6f4fe8767 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-core/swapY2X.sol @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./interfaces/IiZiSwapPool.sol"; +import "./interfaces/IiZiSwapCallback.sol"; + +import "./libraries/Liquidity.sol"; +import "./libraries/Point.sol"; +import "./libraries/PointBitmap.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LimitOrder.sol"; +import "./libraries/SwapMathY2X.sol"; +import "./libraries/SwapMathX2Y.sol"; +import "./libraries/SwapMathY2XDesire.sol"; +import "./libraries/SwapMathX2YDesire.sol"; +import "./libraries/TokenTransfer.sol"; +import "./libraries/UserEarn.sol"; +import "./libraries/State.sol"; +import "./libraries/Oracle.sol"; +import "./libraries/OrderOrEndpoint.sol"; +import "./libraries/MaxMinMath.sol"; +import "./libraries/SwapCache.sol"; + +contract SwapY2XModule { + + using Liquidity for mapping(bytes32 =>Liquidity.Data); + using Liquidity for Liquidity.Data; + using Point for mapping(int24 =>Point.Data); + using Point for Point.Data; + using PointBitmap for mapping(int16 =>uint256); + using LimitOrder for LimitOrder.Data; + using UserEarn for UserEarn.Data; + using UserEarn for mapping(bytes32 =>UserEarn.Data); + using SwapMathY2X for SwapMathY2X.RangeRetState; + using SwapMathX2Y for SwapMathX2Y.RangeRetState; + using Oracle for Oracle.Observation[65535]; + using OrderOrEndpoint for mapping(int24 =>int24); + + int24 internal constant LEFT_MOST_PT = -800000; + int24 internal constant RIGHT_MOST_PT = 800000; + + /// @notice left most point regularized by pointDelta + int24 public leftMostPt; + /// @notice right most point regularized by pointDelta + int24 public rightMostPt; + /// @notice maximum liquidSum for each point, see points() in IiZiSwapPool or library Point + uint128 public maxLiquidPt; + + /// @notice address of iZiSwapFactory + address public factory; + + /// @notice address of tokenX + address public tokenX; + /// @notice address of tokenY + address public tokenY; + /// @notice fee amount of this swap pool, 3000 means 0.3% + uint24 public fee; + + /// @notice minimum number of distance between initialized or limitorder points + int24 public pointDelta; + + /// @notice the fee growth as a 128-bit fixpoing fees of tokenX collected per 1 liquidity of the pool + uint256 public feeScaleX_128; + /// @notice the fee growth as a 128-bit fixpoing fees of tokenY collected per 1 liquidity of the pool + uint256 public feeScaleY_128; + + uint160 sqrtRate_96; + + /// @notice some values of pool + /// see library State or IiZiSwapPool#state for more infomation + State public state; + + /// @notice the information about a liquidity by the liquidity's key + mapping(bytes32 =>Liquidity.Data) public liquidities; + + /// @notice 256 packed point (orderOrEndpoint>0) boolean values. See PointBitmap for more information + mapping(int16 =>uint256) public pointBitmap; + + /// @notice returns infomation of a point in the pool, see Point library of IiZiSwapPool#poitns for more information + mapping(int24 =>Point.Data) public points; + /// @notice infomation about a point whether has limit order and whether as an liquidity's endpoint + mapping(int24 =>int24) public orderOrEndpoint; + /// @notice limitOrder info on a given point + mapping(int24 =>LimitOrder.Data) public limitOrderData; + /// @notice information about a user's limit order (sell tokenY and earn tokenX) + mapping(bytes32 => UserEarn.Data) public userEarnX; + /// @notice information about a user's limit order (sell tokenX and earn tokenY) + mapping(bytes32 => UserEarn.Data) public userEarnY; + /// @notice observation data array + Oracle.Observation[65535] public observations; + + uint256 public totalFeeXCharged; + uint256 public totalFeeYCharged; + + address private original; + + address private swapModuleX2Y; + address private swapModuleY2X; + address private liquidityModule; + address private limitOrderModule; + address private flashModule; + + /// @notice percent to charge from miner's fee + uint24 public feeChargePercent; + + function balanceX() private view returns (uint256) { + (bool success, bytes memory data) = + tokenX.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + function balanceY() private view returns (uint256) { + (bool success, bytes memory data) = + tokenY.staticcall(abi.encodeWithSelector(IERC20Minimal.balanceOf.selector, address(this))); + require(success && data.length >= 32); + return abi.decode(data, (uint256)); + } + + /// Delegate call implementation for IiZiSwapPool#swapY2X. + function swapY2X( + address recipient, + uint128 amount, + int24 highPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY) { + require(amount > 0, "AP"); + + highPt = MaxMinMath.min(highPt, rightMostPt); + amountX = 0; + amountY = 0; + State memory st = state; + SwapCache memory cache; + cache.currFeeScaleX_128 = feeScaleX_128; + cache.currFeeScaleY_128 = feeScaleY_128; + + cache.finished = false; + cache._sqrtRate_96 = sqrtRate_96; + cache.pointDelta = pointDelta; + cache.currentOrderOrEndpt = orderOrEndpoint.getOrderOrEndptVal(st.currentPoint, cache.pointDelta); + cache.startPoint = st.currentPoint; + cache.startLiquidity = st.liquidity; + cache.timestamp = uint32(block.timestamp); + while (st.currentPoint < highPt && !cache.finished) { + + if (cache.currentOrderOrEndpt & 2 > 0) { + // amount <= uint128.max + uint128 amountNoFee = uint128(uint256(amount) * (1e6 - fee) / 1e6); + if (amountNoFee > 0) { + // clear limit order first + LimitOrder.Data storage od = limitOrderData[st.currentPoint]; + uint128 currX = od.sellingX; + (uint128 costY, uint128 acquireX) = SwapMathY2X.y2XAtPrice( + amountNoFee, st.sqrtPrice_96, currX + ); + if (acquireX < currX || costY >= amountNoFee) { + cache.finished = true; + } + uint128 feeAmount; + if (costY >= amountNoFee) { + feeAmount = amount - costY; + } else { + // amount <= uint128.max + feeAmount = uint128(uint256(costY) * fee / (1e6 - fee)); + uint256 mod = uint256(costY) * fee % (1e6 - fee); + if (mod > 0) { + feeAmount += 1; + } + } + totalFeeYCharged += feeAmount; + amount -= (costY + feeAmount); + amountY = amountY + costY + feeAmount; + amountX += acquireX; + currX -= acquireX; + od.sellingX = currX; + od.earnY += costY; + od.accEarnY += costY; + if (currX == 0) { + od.legacyEarnY += od.earnY; + od.earnY = 0; + od.legacyAccEarnY = od.accEarnY; + if (od.sellingY == 0) { + int24 newVal = cache.currentOrderOrEndpt & 1; + orderOrEndpoint.setOrderOrEndptVal(st.currentPoint, cache.pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(st.currentPoint, cache.pointDelta); + } + } + } + } else { + cache.finished = true; + } + } + + if (cache.finished) { + break; + } + + int24 nextPoint = pointBitmap.nearestRightOneOrBoundary(st.currentPoint, cache.pointDelta); + int24 nextVal = orderOrEndpoint.getOrderOrEndptVal(nextPoint, cache.pointDelta); + if (nextPoint > highPt) { + nextVal = 0; + nextPoint = highPt; + } + + // in [st.currentPoint, nextPoint) + if (st.liquidity == 0) { + // no liquidity in the range [st.currentPoint, nextPoint) + st.currentPoint = nextPoint; + st.sqrtPrice_96 = LogPowMath.getSqrtPrice(st.currentPoint); + if (nextVal & 1 > 0) { + Point.Data storage endPt = points[nextPoint]; + // pass next point from left to right + endPt.passEndpoint(cache.currFeeScaleX_128, cache.currFeeScaleY_128); + // we should add delta liquid of nextPoint + int128 liquidDelta = endPt.liquidDelta; + st.liquidity = Liquidity.liquidityAddDelta(st.liquidity, liquidDelta); + st.liquidityX = st.liquidity; + } + cache.currentOrderOrEndpt = nextVal; + } else { + // amount <= uint128.max + uint128 amountNoFee = uint128(uint256(amount) * (1e6 - fee) / 1e6); + if (amountNoFee > 0) { + SwapMathY2X.RangeRetState memory retState = SwapMathY2X.y2XRange( + st, nextPoint, cache._sqrtRate_96, amountNoFee + ); + + cache.finished = retState.finished; + uint128 feeAmount; + if (retState.costY >= amountNoFee) { + feeAmount = amount - retState.costY; + } else { + // retState.costY <= uint128.max + feeAmount = uint128(uint256(retState.costY) * fee / (1e6 - fee)); + uint256 mod = uint256(retState.costY) * fee % (1e6 - fee); + if (mod > 0) { + feeAmount += 1; + } + } + + amountX += retState.acquireX; + amountY = amountY + retState.costY + feeAmount; + amount -= (retState.costY + feeAmount); + + uint256 chargedFeeAmount = uint256(feeAmount) * feeChargePercent / 100; + totalFeeYCharged += chargedFeeAmount; + + cache.currFeeScaleY_128 = cache.currFeeScaleY_128 + MulDivMath.mulDivFloor(feeAmount - chargedFeeAmount, TwoPower.Pow128, st.liquidity); + + st.currentPoint = retState.finalPt; + st.sqrtPrice_96 = retState.sqrtFinalPrice_96; + st.liquidityX = retState.liquidityX; + } else { + cache.finished = true; + } + + if (st.currentPoint == nextPoint) { + if ((nextVal & 1) > 0) { + Point.Data storage endPt = points[nextPoint]; + // pass next point from left to right + endPt.passEndpoint(cache.currFeeScaleX_128, cache.currFeeScaleY_128); + st.liquidity = Liquidity.liquidityAddDelta(st.liquidity, endPt.liquidDelta); + } + st.liquidityX = st.liquidity; + } + if (st.currentPoint == nextPoint) { + cache.currentOrderOrEndpt = nextVal; + } else { + // not necessary, because finished must be true + cache.currentOrderOrEndpt = 0; + } + } + } + if (cache.startPoint != st.currentPoint) { + (st.observationCurrentIndex, st.observationQueueLen) = observations.append( + st.observationCurrentIndex, + cache.timestamp, + cache.startPoint, + st.observationQueueLen, + st.observationNextQueueLen + ); + } + // write back fee scale, no fee of x + feeScaleY_128 = cache.currFeeScaleY_128; + // write back state + state = st; + require(amountX > 0, "PR"); + // transfer x to trader + TokenTransfer.transferToken(tokenX, recipient, amountX); + // trader pay y + require(amountY > 0, "PP"); + uint256 by = balanceY(); + IiZiSwapCallback(msg.sender).swapY2XCallback(amountX, amountY, data); + require(balanceY() >= by + amountY, "YE"); + } + + /// Delegate call implementation for IiZiSwapPool#swapY2XDesireX. + function swapY2XDesireX( + address recipient, + uint128 desireX, + int24 highPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY) { + require (desireX > 0, "XP"); + + highPt = MaxMinMath.min(highPt, rightMostPt); + amountX = 0; + amountY = 0; + State memory st = state; + SwapCache memory cache; + cache.currFeeScaleX_128 = feeScaleX_128; + cache.currFeeScaleY_128 = feeScaleY_128; + cache.finished = false; + cache._sqrtRate_96 = sqrtRate_96; + cache.pointDelta = pointDelta; + cache.currentOrderOrEndpt = orderOrEndpoint.getOrderOrEndptVal(st.currentPoint, cache.pointDelta); + cache.startPoint = st.currentPoint; + cache.startLiquidity = st.liquidity; + cache.timestamp = uint32(block.timestamp); + while (st.currentPoint < highPt && !cache.finished) { + if (cache.currentOrderOrEndpt & 2 > 0) { + // clear limit order first + LimitOrder.Data storage od = limitOrderData[st.currentPoint]; + uint128 currX = od.sellingX; + (uint128 costY, uint128 acquireX) = SwapMathY2XDesire.y2XAtPrice( + desireX, st.sqrtPrice_96, currX + ); + if (acquireX >= desireX) { + cache.finished = true; + } + uint256 feeAmount = MulDivMath.mulDivCeil(costY, fee, 1e6 - fee); + totalFeeYCharged += feeAmount; + desireX = (desireX <= acquireX) ? 0 : desireX - acquireX; + amountY += (costY + feeAmount); + amountX += acquireX; + currX -= acquireX; + od.sellingX = currX; + od.earnY += costY; + od.accEarnY += costY; + if (currX == 0) { + od.legacyEarnY += od.earnY; + od.earnY = 0; + od.legacyAccEarnY = od.accEarnY; + if (od.sellingY == 0) { + int24 newVal = cache.currentOrderOrEndpt & 1; + orderOrEndpoint.setOrderOrEndptVal(st.currentPoint, cache.pointDelta, newVal); + if (newVal == 0) { + pointBitmap.setZero(st.currentPoint, cache.pointDelta); + } + } + } + } + + if (cache.finished) { + break; + } + int24 nextPoint = pointBitmap.nearestRightOneOrBoundary(st.currentPoint, cache.pointDelta); + int24 nextVal = orderOrEndpoint.getOrderOrEndptVal(nextPoint, cache.pointDelta); + if (nextPoint > highPt) { + nextVal = 0; + nextPoint = highPt; + } + // in [st.currentPoint, nextPoint) + if (st.liquidity == 0) { + // no liquidity in the range [st.currentPoint, nextPoint) + st.currentPoint = nextPoint; + st.sqrtPrice_96 = LogPowMath.getSqrtPrice(st.currentPoint); + if (nextVal & 1 > 0) { + Point.Data storage endPt = points[nextPoint]; + // pass next point from left to right + endPt.passEndpoint(cache.currFeeScaleX_128, cache.currFeeScaleY_128); + // we should add delta liquid of nextPoint + int128 liquidDelta = endPt.liquidDelta; + st.liquidity = Liquidity.liquidityAddDelta(st.liquidity, liquidDelta); + st.liquidityX = st.liquidity; + } + cache.currentOrderOrEndpt = nextVal; + } else { + // desireX > 0 + if (desireX > 0) { + SwapMathY2XDesire.RangeRetState memory retState = SwapMathY2XDesire.y2XRange( + st, nextPoint, cache._sqrtRate_96, desireX + ); + cache.finished = retState.finished; + uint256 feeAmount = MulDivMath.mulDivCeil(retState.costY, fee, 1e6 - fee); + uint256 chargedFeeAmount = feeAmount * feeChargePercent / 100; + totalFeeYCharged += chargedFeeAmount; + + amountX += retState.acquireX; + amountY += (retState.costY + feeAmount); + desireX -= MaxMinMath.min(desireX, retState.acquireX); + + cache.currFeeScaleY_128 = cache.currFeeScaleY_128 + MulDivMath.mulDivFloor(feeAmount - chargedFeeAmount, TwoPower.Pow128, st.liquidity); + + st.currentPoint = retState.finalPt; + st.sqrtPrice_96 = retState.sqrtFinalPrice_96; + st.liquidityX = retState.liquidityX; + } else { + cache.finished = true; + } + + if (st.currentPoint == nextPoint) { + if ((nextVal & 1) > 0) { + Point.Data storage endPt = points[nextPoint]; + // pass next point from left to right + endPt.passEndpoint(cache.currFeeScaleX_128, cache.currFeeScaleY_128); + st.liquidity = Liquidity.liquidityAddDelta(st.liquidity, endPt.liquidDelta); + } + st.liquidityX = st.liquidity; + } + if (st.currentPoint == nextPoint) { + cache.currentOrderOrEndpt = nextVal; + } else { + // not necessary, because finished must be true + cache.currentOrderOrEndpt = 0; + } + } + } + if (cache.startPoint != st.currentPoint) { + (st.observationCurrentIndex, st.observationQueueLen) = observations.append( + st.observationCurrentIndex, + cache.timestamp, + cache.startPoint, + st.observationQueueLen, + st.observationNextQueueLen + ); + } + // write back fee scale, no fee of x + feeScaleY_128 = cache.currFeeScaleY_128; + // write back state + state = st; + // transfer x to trader + require(amountX > 0, "PR"); + TokenTransfer.transferToken(tokenX, recipient, amountX); + // trader pay y + require(amountY > 0, "PP"); + uint256 by = balanceY(); + IiZiSwapCallback(msg.sender).swapY2XCallback(amountX, amountY, data); + require(balanceY() >= by + amountY, "YE"); + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/LimitOrderManager.sol b/contracts/dependencies/izumi/izumi-swap-periphery/LimitOrderManager.sol new file mode 100644 index 000000000..282c71cfa --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/LimitOrderManager.sol @@ -0,0 +1,558 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./core/interfaces/IiZiSwapCallback.sol"; +import "./core/interfaces/IiZiSwapFactory.sol"; +import "./core/interfaces/IiZiSwapPool.sol"; + +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/LimOrder.sol"; +import "./libraries/LimOrderCircularQueue.sol"; + +import "./base/base.sol"; + +contract LimitOrderManager is Base, IiZiSwapAddLimOrderCallback { + + using LimOrderCircularQueue for LimOrderCircularQueue.Queue; + + /// @notice Emitted when user successfully create an limit order + /// @param pool address of swap pool + /// @param point point (price) of this limit order + /// @param user address of user + /// @param amount amount of token ready to sell + /// @param sellingRemain amount of selling token remained after successfully create this limit order + /// @param earn amount of acquired token after successfully create this limit order + /// @param sellXEaryY true if this order sell tokenX, false if sell tokenY + event NewLimitOrder( + address pool, + int24 point, + address user, + uint128 amount, + uint128 sellingRemain, + uint128 earn, + bool sellXEaryY + ); + /// @notice Emitted when user dec or update his limit order + /// @param pool address of swap pool + /// @param point point (price) of this limit order + /// @param user address of user + /// @param sold amount of token sold from last claim to now + /// @param earn amount of token earned from last claim to now + /// @param sellXEaryY true if sell tokenX, false if sell tokenY + event Claim( + address pool, + int24 point, + address user, + uint128 sold, + uint128 earn, + bool sellXEaryY + ); + // max-poolId in poolIds, poolId starts from 1 + uint128 private maxPoolId = 1; + + // owners of limit order + mapping(uint256 =>address) public sellers; + + struct PoolMeta { + address tokenX; + address tokenY; + uint24 fee; + } + + // mapping from pool id to pool's meta info + mapping(uint128 =>PoolMeta) public poolMetas; + + // mapping from pool id to pool address + mapping(uint128 =>address) public poolAddrs; + + // mapping from pool address to poolid + mapping(address =>uint128) public poolIds; + + // seller's active order id + mapping(address => LimOrder[]) private addr2ActiveOrder; + // seller's canceled or finished order id + mapping(address => LimOrderCircularQueue.Queue) private addr2DeactiveOrder; + + // maximum number of active order per user + // TODO: + // currently we used a fixed number of storage space. A better way is to allow user to expand it. + // Otherwise, the first 300 orders need more gas for storage. + uint256 public immutable DEACTIVE_ORDER_LIM = 300; + + // callback data passed through iZiSwapPool#addLimOrderWithX(Y) to the callback + struct LimCallbackData { + // tokenX of swap pool + address tokenX; + // tokenY of swap pool + address tokenY; + // fee amount of swap pool + uint24 fee; + // the address who provides token to sell + address payer; + } + + modifier checkActive(uint256 lIdx) { + require(addr2ActiveOrder[msg.sender].length > lIdx, 'Out Of Length!'); + require(addr2ActiveOrder[msg.sender][lIdx].active, 'Not Active!'); + _; + } + + /// @notice Constructor to create this contract. + /// @param factory address of iZiSwapFactory + /// @param weth address of WETH token + constructor( address factory, address weth ) Base(factory, weth) {} + + /// @notice Callback for add limit order, in order to deposit corresponding tokens + /// @param x amount of tokenX need to pay from miner + /// @param y amount of tokenY need to pay from miner + /// @param data encoded LimCallbackData + function payCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external override { + LimCallbackData memory dt = abi.decode(data, (LimCallbackData)); + verify(dt.tokenX, dt.tokenY, dt.fee); + if (x > 0) { + pay(dt.tokenX, dt.payer, msg.sender, x); + } + if (y > 0) { + pay(dt.tokenY, dt.payer, msg.sender, y); + } + } + + function limOrderKey(address miner, int24 pt) internal pure returns(bytes32) { + return keccak256(abi.encodePacked(miner, pt)); + } + + function cachePoolKey(address pool, PoolMeta memory meta) private returns (uint128 poolId) { + poolId = poolIds[pool]; + if (poolId == 0) { + poolIds[pool] = (poolId = maxPoolId++); + poolMetas[poolId] = meta; + poolAddrs[poolId] = pool; + } + } + + function getEarnX(address pool, bytes32 key) private view returns(uint256, uint128, uint128) { + (uint256 lastAccEarn, , , uint128 earn, uint128 legacyEarn, ) = IiZiSwapPool(pool).userEarnX(key); + return (lastAccEarn, earn, legacyEarn); + } + + function getEarnX(address pool, address miner, int24 pt) private view returns(uint256 accEarn, uint128 earn, uint128 legacyEarn) { + (accEarn, earn, legacyEarn) = getEarnX(pool, limOrderKey(miner, pt)); + } + + function getEarnY(address pool, bytes32 key) private view returns(uint256, uint128, uint128) { + (uint256 lastAccEarn, , , uint128 earn, uint128 legacyEarn, ) = IiZiSwapPool(pool).userEarnY(key); + return (lastAccEarn, earn, legacyEarn); + } + + function getEarnY(address pool, address miner, int24 pt) private view returns(uint256 accEarn, uint128 earn, uint128 legacyEarn) { + (accEarn, earn, legacyEarn) = getEarnY(pool, limOrderKey(miner, pt)); + } + + function getEarn(address pool, address miner, int24 pt, bool sellXEarnY) private view returns(uint256 accEarn, uint128 earn, uint128 legacyEarn) { + if (sellXEarnY) { + (accEarn, earn, legacyEarn) = getEarnY(pool, limOrderKey(miner, pt)); + } else { + (accEarn, earn, legacyEarn) = getEarnX(pool, limOrderKey(miner, pt)); + } + } + + /// parameters when calling newLimOrder, grouped together to avoid stake too deep + struct AddLimOrderParam { + // tokenX of swap pool + address tokenX; + // tokenY of swap pool + address tokenY; + // fee amount of swap pool + uint24 fee; + // on which point to add limit order + int24 pt; + // amount of token to sell + uint128 amount; + // sell tokenX or sell tokenY + bool sellXEarnY; + + uint256 deadline; + } + + function _addLimOrder( + address pool, AddLimOrderParam memory addLimitOrderParam + ) private returns (uint128 order, uint128 acquire) { + if (addLimitOrderParam.sellXEarnY) { + (order, acquire) = IiZiSwapPool(pool).addLimOrderWithX( + address(this), addLimitOrderParam.pt, addLimitOrderParam.amount, + abi.encode(LimCallbackData({tokenX: addLimitOrderParam.tokenX, tokenY: addLimitOrderParam.tokenY, fee: addLimitOrderParam.fee, payer: msg.sender})) + ); + } else { + (order, acquire) = IiZiSwapPool(pool).addLimOrderWithY( + address(this), addLimitOrderParam.pt, addLimitOrderParam.amount, + abi.encode(LimCallbackData({tokenX: addLimitOrderParam.tokenX, tokenY: addLimitOrderParam.tokenY, fee: addLimitOrderParam.fee, payer: msg.sender})) + ); + } + } + + /// @notice Create a limit order for recipient. + /// @param idx slot in the addr2ActiveOrder[msg.sender] + /// @param addLimitOrderParam describe params of added limit order, see AddLimOrderParam for more + /// @return orderAmount actual amount of token added in limit order + /// @return acquire amount of tokenY acquired if there is a limit order to sell the other token before adding + function newLimOrder( + uint256 idx, + AddLimOrderParam calldata addLimitOrderParam + ) external payable checkDeadline(addLimitOrderParam.deadline) returns (uint128 orderAmount, uint128 acquire) { + require(addLimitOrderParam.tokenX < addLimitOrderParam.tokenY, 'x= lastAccEarn, "AEO"); + uint256 earnLim256 = accEarn - lastAccEarn; + if (earnLim256 > earnRemain) { + earnLim256 = earnRemain; + } + earnLim = uint128(earnLim256); + } + + /// @notice Compute amount of earned token and amount of sold token for a limit order as much as possible. + /// @param sqrtPrice_96 a 96 bit fixpoint number to describe sqrt(price) of pool + /// @param earnLim max amount of earned token computed by getEarnLim(...) + /// @param sellingRemain amount of token before exchange in the limit order + /// @param isEarnY direction of the limit order (sell Y or sell tokenY) + /// @return earn amount of earned token this limit order can claim + /// @return sold amount of sold token which will be minused from sellingRemain + function getEarnSold( + uint160 sqrtPrice_96, + uint128 earnLim, + uint128 sellingRemain, + bool isEarnY + ) private pure returns (uint128 earn, uint128 sold) { + earn = earnLim; + uint256 sold256; + if (isEarnY) { + uint256 l = MulDivMath.mulDivCeil(earn, TwoPower.pow96, sqrtPrice_96); + sold256 = MulDivMath.mulDivCeil(l, TwoPower.pow96, sqrtPrice_96); + } else { + uint256 l = MulDivMath.mulDivCeil(earn, sqrtPrice_96, TwoPower.pow96); + sold256 = MulDivMath.mulDivCeil(l, sqrtPrice_96, TwoPower.pow96); + } + if (sold256 > sellingRemain) { + sold256 = sellingRemain; + if (isEarnY) { + uint256 l = MulDivMath.mulDivFloor(sold256, sqrtPrice_96, TwoPower.pow96); + earn = uint128(MulDivMath.mulDivFloor(l, sqrtPrice_96, TwoPower.pow96)); + } else { + uint256 l = MulDivMath.mulDivFloor(sold256, TwoPower.pow96, sqrtPrice_96); + earn = uint128(MulDivMath.mulDivFloor(l, TwoPower.pow96, sqrtPrice_96)); + } + } + sold = uint128(sold256); + } + + /// @notice Compute amount of earned token for a legacy order + /// an limit order we call it 'legacy' if it together with other limit order of same + /// direction and same point on the pool is cleared during one time of exchanging. + /// if an limit order is convinced to be 'legacy', we should mark it as 'sold out', + /// etc, transform all its remained selling token to earned token. + /// @param sqrtPrice_96 a 96 bit fixpoint number to describe sqrt(price) of pool + /// @param earnLim remained amount of legacy part of earnings from corresponding limit order in core contract + /// corresponding limit order is an aggregated limit order owned by this contract at same point + /// @param sellingRemain amount of token before exchange in the limit order + /// @param isEarnY direction of the limit order (sell Y or sell tokenY) + /// @return earn amount of earned token this limit order can claim + function getLegacyEarn( + uint160 sqrtPrice_96, + uint128 earnLim, + uint128 sellingRemain, + bool isEarnY + ) private pure returns (uint128 earn) { + uint256 sold256 = sellingRemain; + if (isEarnY) { + uint256 l = MulDivMath.mulDivFloor(sold256, sqrtPrice_96, TwoPower.pow96); + earn = uint128(MulDivMath.mulDivFloor(l, sqrtPrice_96, TwoPower.pow96)); + } else { + uint256 l = MulDivMath.mulDivFloor(sold256, TwoPower.pow96, sqrtPrice_96); + earn = uint128(MulDivMath.mulDivFloor(l, TwoPower.pow96, sqrtPrice_96)); + } + if (earn > earnLim) { + earn = earnLim; + } + } + + /// @notice assign some amount of earned token from earnings of corresponding limit order in core contract + /// to current user (msg.sender) + /// corresponding limit order is an aggregated limit order owned by this contract at same point + /// @param pool swap pool address + /// @param pt point (price) of limit order + /// @param amount amount of legacy or unlegacy earned token to assgin from core's aggregated limit order + /// @param isEarnY direction of the limit order (sell Y or sell tokenY) + /// @param fromLegacy true for legacy order false for unlegacy + /// @return actualAssign actual earned token assgiend from core + function assignLimOrderEarn( + address pool, int24 pt, uint128 amount, bool isEarnY, bool fromLegacy + ) private returns(uint128 actualAssign) { + if (isEarnY) { + actualAssign = IiZiSwapPool(pool).assignLimOrderEarnY(pt, amount, fromLegacy); + } else { + actualAssign = IiZiSwapPool(pool).assignLimOrderEarnX(pt, amount, fromLegacy); + } + } + + /// @notice Update a limit order to claim earned tokens as much as possible. + /// @param order the order to update, see LimOrder for more + /// @param pool address of swap pool + /// @return earn amount of earned token this limit order can claim + function _updateOrder( + LimOrder storage order, + address pool + ) private returns (uint128 earn) { + uint256 legacyAccEarn; + if (order.sellXEarnY) { + (, legacyAccEarn) = IiZiSwapPool(pool).decLimOrderWithX(order.pt, 0); + } else { + (, legacyAccEarn) = IiZiSwapPool(pool).decLimOrderWithY(order.pt, 0); + } + uint128 sold; + uint160 sqrtPrice_96 = LogPowMath.getSqrtPrice(order.pt); + (uint256 accEarn, uint128 earnLim, uint128 legacyEarnLim) = getEarn(pool, address(this), order.pt, order.sellXEarnY); + if (order.lastAccEarn < legacyAccEarn) { + earn = getLegacyEarn(sqrtPrice_96, legacyEarnLim, order.sellingRemain, order.sellXEarnY); + earn = assignLimOrderEarn(pool, order.pt, earn, order.sellXEarnY, true); + sold = order.sellingRemain; + order.earn = order.earn + earn; + order.sellingRemain = 0; + } else { + earnLim = getEarnLim(order.lastAccEarn, accEarn, earnLim); + (earn, sold) = getEarnSold(sqrtPrice_96, earnLim, order.sellingRemain, order.sellXEarnY); + earn = assignLimOrderEarn(pool, order.pt, earn, order.sellXEarnY, false); + order.earn = order.earn + earn; + order.sellingRemain = order.sellingRemain - sold; + } + order.lastAccEarn = accEarn; + emit Claim(pool, order.pt, msg.sender, sold, earn, order.sellXEarnY); + } + + /// @notice Update a limit order to claim earned tokens as much as possible. + /// @param orderIdx idx of order to update + /// @return earn amount of earned token this limit order can claim + function updateOrder( + uint256 orderIdx + ) external checkActive(orderIdx) returns (uint256 earn) { + LimOrder storage order = addr2ActiveOrder[msg.sender][orderIdx]; + address pool = poolAddrs[order.poolId]; + earn = _updateOrder(order, pool); + } + + /// @notice Decrease amount of selling-token of a limit order. + /// @param orderIdx point of seller's limit order + /// @param amount max amount of selling-token to decrease + /// @param deadline deadline timestamp of transaction + /// @return actualDelta actual amount of selling-token decreased + function decLimOrder( + uint256 orderIdx, + uint128 amount, + uint256 deadline + ) external checkActive(orderIdx) checkDeadline(deadline) returns (uint128 actualDelta) { + require(amount > 0, "A0"); + LimOrder storage order = addr2ActiveOrder[msg.sender][orderIdx]; + address pool = poolAddrs[order.poolId]; + // update order first + _updateOrder(order, pool); + // now dec + actualDelta = amount; + if (actualDelta > order.sellingRemain) { + actualDelta = uint128(order.sellingRemain); + } + uint128 actualDeltaRefund; + if (order.sellXEarnY) { + (actualDeltaRefund, ) = IiZiSwapPool(pool).decLimOrderWithX(order.pt, actualDelta); + } else { + (actualDeltaRefund, ) = IiZiSwapPool(pool).decLimOrderWithY(order.pt, actualDelta); + } + // actualDeltaRefund may be less than actualDelta + // but we still minus actualDelta in sellingRemain, and only add actualDeltaRefund to sellingDec + // because if actualDeltaRefund < actualDelta + // then other users cannot buy from this limit order any more + // and also, the seller cannot fetch back more than actualDeltaRefund from swap pool >_< + // but fortunately, actualDeltaRefund < actualDelta only happens after swap on this limit order + // and also, actualDelta - actualDeltaRefund is a very small deviation + order.sellingRemain -= actualDelta; + order.sellingDec += actualDeltaRefund; + order.accSellingDec += actualDeltaRefund; + } + + /// @notice Collect earned or decreased token from a limit order. + /// @param recipient address to benefit + /// @param orderIdx idx of limit order + /// @param collectDec max amount of decreased selling token to collect + /// @param collectEarn max amount of earned token to collect + /// @return actualCollectDec actual amount of decresed selling token collected + /// @return actualCollectEarn actual amount of earned token collected + function collectLimOrder( + address recipient, + uint256 orderIdx, + uint128 collectDec, + uint128 collectEarn + ) external checkActive(orderIdx) returns (uint128 actualCollectDec, uint128 actualCollectEarn) { + LimOrder storage order = addr2ActiveOrder[msg.sender][orderIdx]; + address pool = poolAddrs[order.poolId]; + // update order first + _updateOrder(order, pool); + // now collect + actualCollectDec = collectDec; + if (actualCollectDec > order.sellingDec) { + actualCollectDec = order.sellingDec; + } + actualCollectEarn = collectEarn; + if (actualCollectEarn > order.earn) { + actualCollectEarn = order.earn; + } + if (recipient == address(0)) { + recipient = address(this); + } + IiZiSwapPool(pool).collectLimOrder(recipient, order.pt, actualCollectDec, actualCollectEarn, order.sellXEarnY); + // collect from core may be less, but we still do not modify actualCollectEarn(Dec) + order.sellingDec -= actualCollectDec; + order.earn -= actualCollectEarn; + + bool noRemain = (order.sellingRemain == 0); + if (order.sellingRemain > 0) { + noRemain = (order.amount / order.sellingRemain > 100000); + } + + if (order.sellingDec == 0 && noRemain && order.earn == 0) { + order.active = false; + // addr2DeactiveOrderID[msg.sender].add(orderId); + addr2DeactiveOrder[msg.sender].add(order, DEACTIVE_ORDER_LIM); + } + } + + /// @notice Returns active orders for the seller. + /// @param user address of the seller + /// @return activeIdx list of active order idx + /// @return activeLimitOrder list of active order + function getActiveOrders(address user) + external + view + returns (uint256[] memory activeIdx, LimOrder[] memory activeLimitOrder) + { + uint256 activeNum = 0; + uint256 length = addr2ActiveOrder[user].length; + for (uint256 i = 0; i < length; i ++) { + if (addr2ActiveOrder[user][i].active) { + activeNum += 1; + } + } + if (activeNum == 0) { + return (activeIdx, activeLimitOrder); + } + activeIdx = new uint256[](activeNum); + activeLimitOrder = new LimOrder[](activeNum); + activeNum = 0; + for (uint256 i = 0; i < length; i ++) { + if (addr2ActiveOrder[user][i].active) { + activeIdx[activeNum] = i; + activeLimitOrder[activeNum] = addr2ActiveOrder[user][i]; + activeNum += 1; + } + } + return (activeIdx, activeLimitOrder); + } + + /// @notice Returns a single active order for the seller. + /// @param user address of the seller + /// @param idx index of the active order list + /// @return limOrder the target active order + function getActiveOrder(address user, uint256 idx) external view returns (LimOrder memory limOrder) { + require(idx < addr2ActiveOrder[user].length, 'Out Of Length'); + return addr2ActiveOrder[user][idx]; + } + + /// @notice Returns a slot in the active order list, which can be replaced with a new order. + /// @param user address of the seller + /// @return slotIdx the first available slot index + function getDeactiveSlot(address user) external view returns (uint256 slotIdx) { + slotIdx = addr2ActiveOrder[user].length; + for (uint256 i = 0; i < addr2ActiveOrder[user].length; i ++) { + if (!addr2ActiveOrder[user][i].active) { + return i; + } + } + return slotIdx; + } + + /// @notice Returns deactived orders for the seller. + /// @param user address of the seller + /// @return deactiveLimitOrder list of deactived orders + function getDeactiveOrders(address user) external view returns (LimOrder[] memory deactiveLimitOrder) { + LimOrderCircularQueue.Queue storage queue = addr2DeactiveOrder[user]; + if (queue.limOrders.length == 0) { + return deactiveLimitOrder; + } + deactiveLimitOrder = new LimOrder[](queue.limOrders.length); + uint256 start = queue.start; + for (uint256 i = 0; i < queue.limOrders.length; i ++) { + deactiveLimitOrder[i] = queue.limOrders[(start + i) % queue.limOrders.length]; + } + return deactiveLimitOrder; + } + + /// @notice Returns a single deactived order for the seller. + /// @param user address of the seller + /// @param idx index of the deactived order list + /// @return limOrder the target deactived order + function getDeactiveOrder(address user, uint256 idx) external view returns (LimOrder memory limOrder) { + LimOrderCircularQueue.Queue storage queue = addr2DeactiveOrder[user]; + require(idx < queue.limOrders.length, 'Out Of Length'); + return queue.limOrders[(queue.start + idx) % queue.limOrders.length]; + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/LimitOrderWithSwapManager.sol b/contracts/dependencies/izumi/izumi-swap-periphery/LimitOrderWithSwapManager.sol new file mode 100644 index 000000000..3e2e26889 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/LimitOrderWithSwapManager.sol @@ -0,0 +1,829 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./core/interfaces/IiZiSwapCallback.sol"; +import "./core/interfaces/IiZiSwapFactory.sol"; +import "./core/interfaces/IiZiSwapPool.sol"; + +import "./libraries/MulDivMath.sol"; +import "./libraries/TwoPower.sol"; +import "./libraries/LogPowMath.sol"; +import "./libraries/Converter.sol"; + +import "./base/base.sol"; +import "./base/Switch.sol"; + +contract LimitOrderWithSwapManager is Switch, Base, IiZiSwapAddLimOrderCallback, IiZiSwapCallback { + + /// @notice Emitted when user preswap AND SWAP OUT or do market swap before adding limit order + /// @param tokenIn address of tokenIn (user payed to swap pool) + /// @param tokenOut address of tokenOut (user acquired from swap pool) + /// @param fee fee amount of swap pool + /// @param amountIn amount of tokenIn during swap + /// @param amountOut amount of tokenOut during swap + event MarketSwap( + address tokenIn, + address tokenOut, + uint24 fee, + uint128 amountIn, + uint128 amountOut + ); + + /// @notice Emitted when user cancel a limit order + /// @param tokenIn address of tokenIn (sell token) + /// @param tokenOut address of tokenOut (earn token) + /// @param fee fee amount of swap pool + /// @param pt point(price) of limit order + /// @param initAmountIn amount of tokenIn(sell token) at begining + /// @param remainAmountIn remain amount of tokenIn(sell token) + /// @param amountOut amount of tokenOut(earn token) of this limit order + event Cancel( + address tokenIn, + address tokenOut, + uint24 fee, + int24 pt, + uint128 initAmountIn, + uint128 remainAmountIn, + uint128 amountOut + ); + + /// @notice Emitted when user collect and finish a limit order + /// @param tokenIn address of tokenIn (sell token) + /// @param tokenOut address of tokenOut (earn token) + /// @param fee fee amount of swap pool + /// @param pt point(price) of limit order + /// @param initAmountIn amount of tokenIn(sell token) at begining + /// @param amountOut amount of tokenOut(earn token) of this limit order + event Finish( + address tokenIn, + address tokenOut, + uint24 fee, + int24 pt, + uint128 initAmountIn, + uint128 amountOut + ); + + + // max-poolId in poolIds, poolId starts from 1 + uint128 private maxPoolId = 1; + + // owners of limit order + mapping(uint256 =>address) public sellers; + + struct PoolMeta { + address tokenX; + address tokenY; + uint24 fee; + } + + // mapping from pool id to pool's meta info + mapping(uint128 =>PoolMeta) public poolMetas; + + // mapping from pool id to pool address + mapping(uint128 =>address) public poolAddrs; + + // mapping from pool address to poolid + mapping(address =>uint128) public poolIds; + + // seller's active order id + mapping(address => LimOrder[]) private addr2ActiveOrder; + + // callback data passed through iZiSwapPool#addLimOrderWithX(Y) to the callback + struct LimCallbackData { + // tokenX of swap pool + address tokenX; + // tokenY of swap pool + address tokenY; + // fee amount of swap pool + uint24 fee; + // the address who provides token to sell + address payer; + } + + // infomation of a limit order + struct LimOrder { + // total amount of earned token by all users at this point + // with same direction (sell x or sell y) as of the last update(add/dec) + uint256 lastAccEarn; + // initial amount of token on sale + uint128 initSellingAmount; + // remaing amount of token on sale + uint128 sellingRemain; + // total earned amount + uint128 earn; + // id of pool in which this liquidity is added + uint128 poolId; + // block.timestamp when add a limit order + uint128 timestamp; + // point (price) of limit order + int24 pt; + // direction of limit order (sellx or sell y) + bool sellXEarnY; + // active or not + bool active; + } + + modifier checkActive(uint256 lIdx) { + require(addr2ActiveOrder[msg.sender].length > lIdx, 'Out Of Length!'); + require(addr2ActiveOrder[msg.sender][lIdx].active, 'Not Active!'); + _; + } + + /// @notice Constructor to create this contract. + /// @param factory address of iZiSwapFactory + /// @param weth address of WETH token + constructor( address factory, address weth ) Base(factory, weth) {} + + /// @notice Callback for add limit order, in order to deposit corresponding tokens + /// @param x amount of tokenX need to pay from miner + /// @param y amount of tokenY need to pay from miner + /// @param data encoded LimCallbackData + function payCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external override notPause { + LimCallbackData memory dt = abi.decode(data, (LimCallbackData)); + verify(dt.tokenX, dt.tokenY, dt.fee); + if (x > 0) { + pay(dt.tokenX, dt.payer, msg.sender, x); + } + if (y > 0) { + pay(dt.tokenY, dt.payer, msg.sender, y); + } + } + + struct SwapCallbackData { + address tokenX; + address tokenY; + uint24 fee; + address payer; + } + + /// @notice Callback for swapY2X and swapY2XDesireX, in order to pay tokenY from trader. + /// @param x amount of tokenX trader acquired + /// @param y amount of tokenY need to pay from trader + /// @param data encoded SwapCallbackData + function swapY2XCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external override notPause { + SwapCallbackData memory dt = abi.decode(data, (SwapCallbackData)); + verify(dt.tokenX, dt.tokenY, dt.fee); + pay(dt.tokenY, dt.payer, msg.sender, y); + } + + /// @notice Callback for swapX2Y and swapX2YDesireY, in order to pay tokenX from trader. + /// @param x amount of tokenX need to pay from trader + /// @param y amount of tokenY trader acquired + /// @param data encoded SwapCallbackData + function swapX2YCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external override notPause { + SwapCallbackData memory dt = abi.decode(data, (SwapCallbackData)); + verify(dt.tokenX, dt.tokenY, dt.fee); + pay(dt.tokenX, dt.payer, msg.sender, x); + } + + function limOrderKey(address miner, int24 pt) internal pure returns(bytes32) { + return keccak256(abi.encodePacked(miner, pt)); + } + + function cachePoolKey(address pool, PoolMeta memory meta) private returns (uint128 poolId) { + poolId = poolIds[pool]; + if (poolId == 0) { + poolIds[pool] = (poolId = maxPoolId++); + poolMetas[poolId] = meta; + poolAddrs[poolId] = pool; + } + } + + function getEarnX(address pool, bytes32 key) private view returns(uint256, uint128, uint128) { + (uint256 lastAccEarn, , , uint128 earn, uint128 legacyEarn, ) = IiZiSwapPool(pool).userEarnX(key); + return (lastAccEarn, earn, legacyEarn); + } + + function getEarnX(address pool, address miner, int24 pt) private view returns(uint256 accEarn, uint128 earn, uint128 legacyEarn) { + (accEarn, earn, legacyEarn) = getEarnX(pool, limOrderKey(miner, pt)); + } + + function getEarnY(address pool, bytes32 key) private view returns(uint256, uint128, uint128) { + (uint256 lastAccEarn, , , uint128 earn, uint128 legacyEarn, ) = IiZiSwapPool(pool).userEarnY(key); + return (lastAccEarn, earn, legacyEarn); + } + + function getEarnY(address pool, address miner, int24 pt) private view returns(uint256 accEarn, uint128 earn, uint128 legacyEarn) { + (accEarn, earn, legacyEarn) = getEarnY(pool, limOrderKey(miner, pt)); + } + + function getEarn(address pool, address miner, int24 pt, bool sellXEarnY) private view returns(uint256 accEarn, uint128 earn, uint128 legacyEarn) { + if (sellXEarnY) { + (accEarn, earn, legacyEarn) = getEarnY(pool, limOrderKey(miner, pt)); + } else { + (accEarn, earn, legacyEarn) = getEarnX(pool, limOrderKey(miner, pt)); + } + } + + /// parameters when calling newLimOrder, grouped together to avoid stake too deep + struct AddLimOrderParam { + // recipient to acquire token during pre-swap + // if and only if user acquire chain token token (like eth/bnb, and not in wrapped form), + // address should be 0, and user should append a unwrapWETH9(...) calling immediately + // (via multicall) + address recipient; + // tokenX of swap pool + address tokenX; + // tokenY of swap pool + address tokenY; + // fee amount of swap pool + uint24 fee; + // on which point to add limit order + int24 pt; + + bool isDesireMode; + // amount of token to sell/acquire + // if isDesireMode is true, acquire amount + // otherwise, sell amount + uint128 amount; + uint256 swapMinAcquired; + // sell tokenX or sell tokenY + bool sellXEarnY; + + uint256 deadline; + } + + function _addLimOrder( + address pool, AddLimOrderParam memory addLimitOrderParam + ) private returns (uint128 order, uint128 acquire) { + if (addLimitOrderParam.sellXEarnY) { + (order, acquire) = IiZiSwapPool(pool).addLimOrderWithX( + address(this), addLimitOrderParam.pt, addLimitOrderParam.amount, + abi.encode(LimCallbackData({tokenX: addLimitOrderParam.tokenX, tokenY: addLimitOrderParam.tokenY, fee: addLimitOrderParam.fee, payer: msg.sender})) + ); + } else { + (order, acquire) = IiZiSwapPool(pool).addLimOrderWithY( + address(this), addLimitOrderParam.pt, addLimitOrderParam.amount, + abi.encode(LimCallbackData({tokenX: addLimitOrderParam.tokenX, tokenY: addLimitOrderParam.tokenY, fee: addLimitOrderParam.fee, payer: msg.sender})) + ); + } + if (acquire > 0) { + IiZiSwapPool(pool).collectLimOrder(addLimitOrderParam.recipient, addLimitOrderParam.pt, 0, acquire, addLimitOrderParam.sellXEarnY); + } + } + + struct SwapBeforeResult { + uint128 remainAmount; + uint128 costBeforeSwap; + uint128 acquireBeforeSwap; + bool swapOut; + } + + function _swapBefore( + address pool, + AddLimOrderParam memory addLimitOrderParam + ) private returns (SwapBeforeResult memory) { + address recipient = addLimitOrderParam.recipient; + SwapBeforeResult memory result = SwapBeforeResult({ + remainAmount: 0, + costBeforeSwap: 0, + acquireBeforeSwap: 0, + swapOut: false + }); + result.remainAmount = addLimitOrderParam.amount; + ( + , + int24 currentPoint, + , + , + , + , + , + ) = IiZiSwapPool(pool).state(); + if (addLimitOrderParam.sellXEarnY) { + if (addLimitOrderParam.pt < currentPoint) { + uint256 costX; + uint256 acquireY; + if (addLimitOrderParam.isDesireMode) { + (costX, acquireY) = IiZiSwapPool(pool).swapX2YDesireY( + recipient, addLimitOrderParam.amount, addLimitOrderParam.pt, + abi.encode(SwapCallbackData({ + tokenX: addLimitOrderParam.tokenX, + fee: addLimitOrderParam.fee, + tokenY: addLimitOrderParam.tokenY, + payer: msg.sender + })) + ); + require(acquireY >= addLimitOrderParam.swapMinAcquired, "X2YDesireYAcquired"); + result.remainAmount = acquireY < uint256(addLimitOrderParam.amount) ? addLimitOrderParam.amount - uint128(acquireY) : 0; + } else { + (costX, acquireY) = IiZiSwapPool(pool).swapX2Y( + recipient, addLimitOrderParam.amount, addLimitOrderParam.pt, + abi.encode(SwapCallbackData({ + tokenX: addLimitOrderParam.tokenX, + fee: addLimitOrderParam.fee, + tokenY: addLimitOrderParam.tokenY, + payer: msg.sender + })) + ); + require(acquireY >= addLimitOrderParam.swapMinAcquired, "X2YAcquired"); + result.remainAmount = costX < uint256(addLimitOrderParam.amount) ? addLimitOrderParam.amount - uint128(costX) : 0; + } + result.acquireBeforeSwap = Converter.toUint128(acquireY); + result.costBeforeSwap = Converter.toUint128(costX); + } + } else { + if (addLimitOrderParam.pt > currentPoint) { + uint256 costY; + uint256 acquireX; + if (addLimitOrderParam.isDesireMode) { + (acquireX, costY) = IiZiSwapPool(pool).swapY2XDesireX( + recipient, addLimitOrderParam.amount, addLimitOrderParam.pt + 1, + abi.encode(SwapCallbackData({ + tokenX: addLimitOrderParam.tokenX, + fee: addLimitOrderParam.fee, + tokenY: addLimitOrderParam.tokenY, + payer: msg.sender + })) + ); + require(acquireX >= addLimitOrderParam.swapMinAcquired, "Y2XDesireXAcquired"); + result.remainAmount = acquireX < uint256(addLimitOrderParam.amount) ? addLimitOrderParam.amount - uint128(acquireX) : 0; + } else { + (acquireX, costY) = IiZiSwapPool(pool).swapY2X( + recipient, addLimitOrderParam.amount, addLimitOrderParam.pt + 1, + abi.encode(SwapCallbackData({ + tokenX: addLimitOrderParam.tokenX, + fee: addLimitOrderParam.fee, + tokenY: addLimitOrderParam.tokenY, + payer: msg.sender + })) + ); + require(acquireX >= addLimitOrderParam.swapMinAcquired, "Y2XAcquired"); + result.remainAmount = costY < uint256(addLimitOrderParam.amount) ? addLimitOrderParam.amount - uint128(costY) : 0; + } + result.acquireBeforeSwap = Converter.toUint128(acquireX); + result.costBeforeSwap = Converter.toUint128(costY); + } + } + result.swapOut = (result.remainAmount <= addLimitOrderParam.amount / 10000); + return result; + } + + /// @notice Create a limit order for recipient. + /// @param idx slot in the addr2ActiveOrder[msg.sender] + /// @param originAddLimitOrderParam describe params of added limit order, see AddLimOrderParam for more + /// @return orderAmount actual amount of token added in limit order + /// @return costBeforeSwap amount of token cost if we need to swap before add limit order + /// @return acquireBeforeSwap amount of token acquired if we need to swap before add limit order + /// @return acquire amount of token acquired if there is a limit order to sell the other token before adding + function newLimOrder( + uint256 idx, + AddLimOrderParam calldata originAddLimitOrderParam + ) external payable notPause checkDeadline(originAddLimitOrderParam.deadline) returns (uint128 orderAmount, uint128 costBeforeSwap, uint128 acquireBeforeSwap, uint128 acquire) { + require(originAddLimitOrderParam.tokenX < originAddLimitOrderParam.tokenY, 'x 0) { + uint256 ethBalance = address(this).balance; + if (addLimitOrderParam.amount > ethBalance) { + addLimitOrderParam.amount = uint128(ethBalance); + } + } + // no need to write following line + addLimitOrderParam.isDesireMode = false; + } + (orderAmount, acquire) = _addLimOrder(pool, addLimitOrderParam); + if (orderAmount == 0) { + // swap out + emit MarketSwap( + originAddLimitOrderParam.sellXEarnY ? originAddLimitOrderParam.tokenX : originAddLimitOrderParam.tokenY, + originAddLimitOrderParam.sellXEarnY ? originAddLimitOrderParam.tokenY : originAddLimitOrderParam.tokenX, + originAddLimitOrderParam.fee, + costBeforeSwap + addLimitOrderParam.amount, + acquireBeforeSwap + acquire + ); + return (0, costBeforeSwap, acquireBeforeSwap, 0); + } + (uint256 accEarn, , ) = getEarn(pool, address(this), addLimitOrderParam.pt, addLimitOrderParam.sellXEarnY); + uint128 poolId = cachePoolKey(pool, PoolMeta({tokenX: addLimitOrderParam.tokenX, tokenY: addLimitOrderParam.tokenY, fee: addLimitOrderParam.fee})); + LimOrder[] storage limOrders = addr2ActiveOrder[msg.sender]; + if (idx < limOrders.length) { + // replace + require(limOrders[idx].active == false, 'active conflict!'); + limOrders[idx] = LimOrder({ + pt: addLimitOrderParam.pt, + initSellingAmount: addLimitOrderParam.amount + costBeforeSwap, + sellingRemain: orderAmount, + earn: acquire + acquireBeforeSwap, + lastAccEarn: accEarn, + poolId: poolId, + sellXEarnY: addLimitOrderParam.sellXEarnY, + timestamp: uint128(block.timestamp), + active: true + }); + } else { + limOrders.push(LimOrder({ + pt: addLimitOrderParam.pt, + initSellingAmount: addLimitOrderParam.amount + costBeforeSwap, + sellingRemain: orderAmount, + earn: acquire + acquireBeforeSwap, + lastAccEarn: accEarn, + poolId: poolId, + sellXEarnY: addLimitOrderParam.sellXEarnY, + timestamp: uint128(block.timestamp), + active: true + })); + } + + } + + /// @notice Compute max amount of earned token the seller can claim. + /// @param lastAccEarn total amount of earned token of all users on this point before last update of this limit order + /// @param accEarn total amount of earned token of all users on this point now + /// @param earnRemain total amount of unclaimed earned token of all users on this point + /// @return earnLim max amount of earned token the seller can claim + function getEarnLim(uint256 lastAccEarn, uint256 accEarn, uint128 earnRemain) private pure returns(uint128 earnLim) { + require(accEarn >= lastAccEarn, "AEO"); + uint256 earnLim256 = accEarn - lastAccEarn; + if (earnLim256 > earnRemain) { + earnLim256 = earnRemain; + } + earnLim = uint128(earnLim256); + } + + /// @notice Compute amount of earned token and amount of sold token for a limit order as much as possible. + /// @param sqrtPrice_96 a 96 bit fixpoint number to describe sqrt(price) of pool + /// @param earnLim max amount of earned token computed by getEarnLim(...) + /// @param sellingRemain amount of token before exchange in the limit order + /// @param isEarnY direction of the limit order (sell Y or sell tokenY) + /// @return earn amount of earned token this limit order can claim + /// @return sold amount of sold token which will be minused from sellingRemain + function getEarnSold( + uint160 sqrtPrice_96, + uint128 earnLim, + uint128 sellingRemain, + bool isEarnY + ) private pure returns (uint128 earn, uint128 sold) { + earn = earnLim; + uint256 sold256; + if (isEarnY) { + uint256 l = MulDivMath.mulDivCeil(earn, TwoPower.pow96, sqrtPrice_96); + sold256 = MulDivMath.mulDivCeil(l, TwoPower.pow96, sqrtPrice_96); + } else { + uint256 l = MulDivMath.mulDivCeil(earn, sqrtPrice_96, TwoPower.pow96); + sold256 = MulDivMath.mulDivCeil(l, sqrtPrice_96, TwoPower.pow96); + } + if (sold256 > sellingRemain) { + sold256 = sellingRemain; + if (isEarnY) { + uint256 l = MulDivMath.mulDivFloor(sold256, sqrtPrice_96, TwoPower.pow96); + earn = uint128(MulDivMath.mulDivFloor(l, sqrtPrice_96, TwoPower.pow96)); + } else { + uint256 l = MulDivMath.mulDivFloor(sold256, TwoPower.pow96, sqrtPrice_96); + earn = uint128(MulDivMath.mulDivFloor(l, TwoPower.pow96, sqrtPrice_96)); + } + } + sold = uint128(sold256); + } + + /// @notice Compute amount of earned token for a legacy order + /// an limit order we call it 'legacy' if it together with other limit order of same + /// direction and same point on the pool is cleared during one time of exchanging. + /// if an limit order is convinced to be 'legacy', we should mark it as 'sold out', + /// etc, transform all its remained selling token to earned token. + /// @param sqrtPrice_96 a 96 bit fixpoint number to describe sqrt(price) of pool + /// @param earnLim remained amount of legacy part of earnings from corresponding limit order in core contract + /// corresponding limit order is an aggregated limit order owned by this contract at same point + /// @param sellingRemain amount of token before exchange in the limit order + /// @param isEarnY direction of the limit order (sell Y or sell tokenY) + /// @return earn amount of earned token this limit order can claim + function getLegacyEarn( + uint160 sqrtPrice_96, + uint128 earnLim, + uint128 sellingRemain, + bool isEarnY + ) private pure returns (uint128 earn) { + uint256 sold256 = sellingRemain; + if (isEarnY) { + uint256 l = MulDivMath.mulDivFloor(sold256, sqrtPrice_96, TwoPower.pow96); + earn = uint128(MulDivMath.mulDivFloor(l, sqrtPrice_96, TwoPower.pow96)); + } else { + uint256 l = MulDivMath.mulDivFloor(sold256, TwoPower.pow96, sqrtPrice_96); + earn = uint128(MulDivMath.mulDivFloor(l, TwoPower.pow96, sqrtPrice_96)); + } + if (earn > earnLim) { + earn = earnLim; + } + } + + /// @notice assign some amount of earned token from earnings of corresponding limit order in core contract + /// to current user (msg.sender) + /// corresponding limit order is an aggregated limit order owned by this contract at same point + /// @param pool swap pool address + /// @param pt point (price) of limit order + /// @param amount amount of legacy or unlegacy earned token to assgin from core's aggregated limit order + /// @param isEarnY direction of the limit order (sell Y or sell tokenY) + /// @param fromLegacy true for legacy order false for unlegacy + /// @return actualAssign actual earned token assgiend from core + function assignLimOrderEarn( + address pool, int24 pt, uint128 amount, bool isEarnY, bool fromLegacy + ) private returns(uint128 actualAssign) { + if (isEarnY) { + actualAssign = IiZiSwapPool(pool).assignLimOrderEarnY(pt, amount, fromLegacy); + } else { + actualAssign = IiZiSwapPool(pool).assignLimOrderEarnX(pt, amount, fromLegacy); + } + } + + /// @notice Update a limit order to claim earned tokens as much as possible. + /// @param order the order to update, see LimOrder for more + /// @param pool address of swap pool + /// @return earn amount of earned token this limit order can claim + function _updateOrder( + LimOrder storage order, + address pool + ) private returns (uint128 earn) { + uint256 legacyAccEarn; + if (order.sellXEarnY) { + (, legacyAccEarn) = IiZiSwapPool(pool).decLimOrderWithX(order.pt, 0); + } else { + (, legacyAccEarn) = IiZiSwapPool(pool).decLimOrderWithY(order.pt, 0); + } + uint128 sold; + uint160 sqrtPrice_96 = LogPowMath.getSqrtPrice(order.pt); + (uint256 accEarn, uint128 earnLim, uint128 legacyEarnLim) = getEarn(pool, address(this), order.pt, order.sellXEarnY); + if (order.lastAccEarn < legacyAccEarn) { + earn = getLegacyEarn(sqrtPrice_96, legacyEarnLim, order.sellingRemain, order.sellXEarnY); + earn = assignLimOrderEarn(pool, order.pt, earn, order.sellXEarnY, true); + sold = order.sellingRemain; + order.earn = order.earn + earn; + order.sellingRemain = 0; + } else { + earnLim = getEarnLim(order.lastAccEarn, accEarn, earnLim); + (earn, sold) = getEarnSold(sqrtPrice_96, earnLim, order.sellingRemain, order.sellXEarnY); + earn = assignLimOrderEarn(pool, order.pt, earn, order.sellXEarnY, false); + order.earn = order.earn + earn; + order.sellingRemain = order.sellingRemain - sold; + } + order.lastAccEarn = accEarn; + } + + /// @notice cancel a limit order + /// @param recipient address to acquire canceled selling token and to acquire earned token + /// @param orderIdx point of seller's limit order + /// @param deadline deadline timestamp of transaction + function cancel( + address recipient, + uint256 orderIdx, + uint256 deadline + ) external notPause checkActive(orderIdx) checkDeadline(deadline) { + if (recipient == address(0)) { + recipient = address(this); + } + LimOrder storage order = addr2ActiveOrder[msg.sender][orderIdx]; + + address pool = poolAddrs[order.poolId]; + // update order first + uint128 earn = _updateOrder(order, pool); + uint128 actualDecrease = order.sellingRemain; + bool sellXEarnY = order.sellXEarnY; + if (actualDecrease > 0) { + if (sellXEarnY) { + IiZiSwapPool(pool).decLimOrderWithX(order.pt, actualDecrease); + } else { + IiZiSwapPool(pool).decLimOrderWithY(order.pt, actualDecrease); + } + } + if (actualDecrease > 0 || earn > 0) { + IiZiSwapPool(pool).collectLimOrder(recipient, order.pt, actualDecrease, earn, sellXEarnY); + } + + PoolMeta memory poolMeta = poolMetas[order.poolId]; + + emit Cancel( + sellXEarnY? poolMeta.tokenX : poolMeta.tokenY, + sellXEarnY? poolMeta.tokenY : poolMeta.tokenX, + poolMeta.fee, + order.pt, + order.initSellingAmount, + order.sellingRemain, + order.earn + ); + order.active = false; + } + + /// @notice Collect earned token from an limit order + /// @param recipient address to benefit + /// @param orderIdx idx of limit order + /// @return earn amount of token collected during this calling (not all earned token of this order) + function collect( + address recipient, + uint256 orderIdx + ) external notPause checkActive(orderIdx) returns (uint128 earn) { + if (recipient == address(0)) { + recipient = address(this); + } + LimOrder storage order = addr2ActiveOrder[msg.sender][orderIdx]; + address pool = poolAddrs[order.poolId]; + // update order first + earn = _updateOrder(order, pool); + + bool noRemain = (order.sellingRemain == 0); + if (order.sellingRemain > 0) { + noRemain = (order.initSellingAmount / order.sellingRemain > 100000); + } + + bool sellXEarnY = order.sellXEarnY; + + if (earn > 0) { + IiZiSwapPool(pool).collectLimOrder(recipient, order.pt, 0, earn, sellXEarnY); + } + + if (noRemain) { + PoolMeta memory poolMeta = poolMetas[order.poolId]; + emit Finish( + sellXEarnY? poolMeta.tokenX : poolMeta.tokenY, + sellXEarnY? poolMeta.tokenY : poolMeta.tokenX, + poolMeta.fee, + order.pt, + order.initSellingAmount, + order.earn + ); + order.active = false; + } + } + + /// @notice Returns active orders for the seller. + /// @param user address of the seller + /// @return activeIdx list of active order idx + /// @return activeLimitOrder list of active order + function getActiveOrders(address user) + external + view + returns (uint256[] memory activeIdx, LimOrder[] memory activeLimitOrder) + { + uint256 activeNum = 0; + uint256 length = addr2ActiveOrder[user].length; + for (uint256 i = 0; i < length; i ++) { + if (addr2ActiveOrder[user][i].active) { + activeNum += 1; + } + } + if (activeNum == 0) { + return (activeIdx, activeLimitOrder); + } + activeIdx = new uint256[](activeNum); + activeLimitOrder = new LimOrder[](activeNum); + activeNum = 0; + for (uint256 i = 0; i < length; i ++) { + if (addr2ActiveOrder[user][i].active) { + activeIdx[activeNum] = i; + activeLimitOrder[activeNum] = addr2ActiveOrder[user][i]; + activeNum += 1; + } + } + return (activeIdx, activeLimitOrder); + } + + /// @notice Returns a single active order for the seller. + /// @param user address of the seller + /// @param idx index of the active order list + /// @return limOrder the target active order + function getActiveOrder(address user, uint256 idx) external view returns (LimOrder memory limOrder) { + require(idx < addr2ActiveOrder[user].length, 'Out Of Length'); + return addr2ActiveOrder[user][idx]; + } + + /// @notice Returns a slot in the active order list, which can be replaced with a new order. + /// @param user address of the seller + /// @return slotIdx the first available slot index + function getDeactiveSlot(address user) external view returns (uint256 slotIdx) { + slotIdx = addr2ActiveOrder[user].length; + for (uint256 i = 0; i < addr2ActiveOrder[user].length; i ++) { + if (!addr2ActiveOrder[user][i].active) { + return i; + } + } + return slotIdx; + } + + + struct SwapParams { + address tokenIn; + address tokenOut; + uint24 fee; + uint128 amount; + uint128 minAcquiredOrMaxPayed; + address recipient; + uint256 deadline; + } + + function swapDesireSingle( + SwapParams calldata params + ) external payable notPause checkDeadline(params.deadline) { + // allow swapping to the router address with address 0 + address recipient = params.recipient == address(0) ? address(this) : params.recipient; + address poolAddr = pool(params.tokenOut, params.tokenIn, params.fee); + uint256 amountIn; + uint256 amountOut; + if (params.tokenOut < params.tokenIn) { + // tokenOut is tokenX, tokenIn is tokenY + // we should call y2XDesireX + (amountOut, amountIn) = IiZiSwapPool(poolAddr).swapY2XDesireX( + recipient, params.amount, 799999, + abi.encode(SwapCallbackData({ + tokenX: params.tokenOut, + fee: params.fee, + tokenY: params.tokenIn, + payer: msg.sender + })) + ); + require (amountIn <= params.minAcquiredOrMaxPayed, "swapY2XDesireX payed too much"); + } else { + // tokenOut is tokenY + // tokenIn is tokenX + (amountIn, amountOut) = IiZiSwapPool(poolAddr).swapX2YDesireY( + recipient, params.amount, -799999, + abi.encode(SwapCallbackData({ + tokenX: params.tokenIn, + fee: params.fee, + tokenY: params.tokenOut, + payer: msg.sender + })) + ); + require (amountIn <= params.minAcquiredOrMaxPayed, "swapX2YDesireY payed too much"); + } + emit MarketSwap(params.tokenIn, params.tokenOut, params.fee, Converter.toUint128(amountIn), Converter.toUint128(amountOut)); + } + + function swapAmountSingle( + SwapParams calldata params + ) external payable notPause checkDeadline(params.deadline) { + // allow swapping to the router address with address 0 + address recipient = params.recipient == address(0) ? address(this) : params.recipient; + address poolAddr = pool(params.tokenOut, params.tokenIn, params.fee); + uint256 amountIn; + uint256 amountOut; + if (params.tokenIn < params.tokenOut) { + // swapX2Y + (amountIn, amountOut) = IiZiSwapPool(poolAddr).swapX2Y( + recipient, params.amount, -799999, + abi.encode(SwapCallbackData({ + tokenX: params.tokenIn, + fee: params.fee, + tokenY: params.tokenOut, + payer: msg.sender + })) + ); + require (amountOut >= params.minAcquiredOrMaxPayed, "swapX2Y acquire too little"); + } else { + // swapY2X + (amountOut, amountIn) = IiZiSwapPool(poolAddr).swapY2X( + recipient, params.amount, 799999, + abi.encode(SwapCallbackData({ + tokenX: params.tokenOut, + fee: params.fee, + tokenY: params.tokenIn, + payer: msg.sender + })) + ); + require (amountOut >= params.minAcquiredOrMaxPayed, "swapY2X acquire too little"); + } + emit MarketSwap(params.tokenIn, params.tokenOut, params.fee, Converter.toUint128(amountIn), Converter.toUint128(amountOut)); + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/LiquidityManager.sol b/contracts/dependencies/izumi/izumi-swap-periphery/LiquidityManager.sol new file mode 100644 index 000000000..acb2eeeb2 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/LiquidityManager.sol @@ -0,0 +1,451 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "./core/interfaces/IiZiSwapCallback.sol"; +import "./core/interfaces/IiZiSwapFactory.sol"; +import "./core/interfaces/IiZiSwapPool.sol"; + +import "./libraries/MintMath.sol"; +import "./libraries/TwoPower.sol"; + +import "./base/base.sol"; + +contract LiquidityManager is Ownable, Base, ERC721Enumerable, IiZiSwapMintCallback { + + /// @notice Emitted when miner successfully add liquidity on an existing liquidity-nft + /// @param nftId id of minted liquidity nft + /// @param pool address of swap pool + /// @param liquidityDelta the amount of liquidity added + /// @param amountX amount of tokenX deposit + /// @param amountY amount of tokenY deposit + event AddLiquidity( + uint256 indexed nftId, + address pool, + uint128 liquidityDelta, + uint256 amountX, + uint256 amountY + ); + /// @notice Emitted when miner successfully add decrease liquidity on an existing liquidity-nft + /// @param nftId id of minted liquidity nft + /// @param pool address of swap pool + /// @param liquidityDelta the amount of liquidity decreased + /// @param amountX amount of tokenX withdrawed + /// @param amountY amount of tokenY withdrawed + event DecLiquidity( + uint256 indexed nftId, + address pool, + uint128 liquidityDelta, + uint256 amountX, + uint256 amountY + ); + + // callback data passed through iZiSwapPool#mint to the callback + struct MintCallbackData { + // tokenX of swap + address tokenX; + // tokenY of swap + address tokenY; + // fee amount of swap + uint24 fee; + // address to pay tokenX and tokenY to iZiSwapPool + address payer; + } + + // max-poolId in poolIds, poolId starts from 1 + uint128 private maxPoolId = 1; + + // infomation of liquidity provided by miner + struct Liquidity { + // left point of liquidity-token, the range is [leftPt, rightPt) + int24 leftPt; + // right point of liquidity-token, the range is [leftPt, rightPt) + int24 rightPt; + // amount of liquidity on each point in [leftPt, rightPt) + uint128 liquidity; + // a 128-fixpoint number, as integral of { fee(pt, t)/L(pt, t) }. + // here fee(pt, t) denotes fee generated on point pt at time t + // L(pt, t) denotes liquidity on point pt at time t + // pt varies in [leftPt, rightPt) + // t moves from pool created until miner last modify this liquidity-token (mint/addLiquidity/decreaseLiquidity/create) + uint256 lastFeeScaleX_128; + uint256 lastFeeScaleY_128; + // remained tokenX miner can collect, including fee and withdrawed token + uint256 remainTokenX; + uint256 remainTokenY; + // id of pool in which this liquidity is added + uint128 poolId; + } + + /// @notice mapping from nftId to Liquidity info + mapping(uint256 =>Liquidity) public liquidities; + + /// @notice num of liquidity token + uint256 public liquidityNum = 0; + + struct PoolMeta { + // tokenX of pool + address tokenX; + // tokenY of pool + address tokenY; + // fee amount of pool + uint24 fee; + } + + /// @notice mapping from poolId to meta info of pool + mapping(uint128 =>PoolMeta) public poolMetas; + + /// @notice mapping from address to poolId within this contract + mapping(address =>uint128) public poolIds; + + modifier checkAuth(uint256 lid) { + require(_isApprovedOrOwner(msg.sender, lid), 'Not approved'); + _; + } + + string public baseURI; + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + function setBaseURI(string calldata newBaseURI) external onlyOwner { + baseURI = newBaseURI; + } + + /// @notice Constructor to create this contract. + /// @param factory address of iZiSwapFactory + /// @param weth address of WETH token + constructor( + address factory, + address weth + ) ERC721("iZiSwap Liquidity NFT", "IZISWAP-LIQUIDITY-NFT") Base(factory, weth) { + } + + /// @notice Callback for mining, in order to deposit tokens. + /// @param x amount of tokenX pay from miner + /// @param y amount of tokenY pay from miner + /// @param data encoded MintCallbackData + function mintDepositCallback( + uint256 x, uint256 y, bytes calldata data + ) external override { + MintCallbackData memory dt = abi.decode(data, (MintCallbackData)); + verify(dt.tokenX, dt.tokenY, dt.fee); + + if (x > 0) { + pay(dt.tokenX, dt.payer, msg.sender, x); + } + if (y > 0) { + pay(dt.tokenY, dt.payer, msg.sender, y); + } + } + + /// @notice Get or create a pool for (tokenX/tokenY/fee) if not exists. + /// @param tokenX tokenX of swap pool + /// @param tokenY tokenY of swap pool + /// @param fee fee amount of swap pool + /// @param initialPoint initial point if need to create a new pool + /// @return corresponding pool address + function createPool(address tokenX, address tokenY, uint24 fee, int24 initialPoint) external returns (address) { + require(tokenX < tokenY, "x= mintParam.amountXMin, "XMIN"); + require(amountY >= mintParam.amountYMin, "YMIN"); + lid = liquidityNum ++; + (uint256 lastFeeScaleX_128, uint256 lastFeeScaleY_128) = getLastFeeScale( + pool, liquidityKey(address(this), mintParam.pl, mintParam.pr) + ); + uint128 poolId = cachePoolKey(pool, PoolMeta({tokenX: mintParam.tokenX, tokenY: mintParam.tokenY, fee: mintParam.fee})); + liquidities[lid] = Liquidity({ + leftPt: mintParam.pl, + rightPt: mintParam.pr, + liquidity: liquidity, + lastFeeScaleX_128: lastFeeScaleX_128, + lastFeeScaleY_128: lastFeeScaleY_128, + remainTokenX: 0, + remainTokenY: 0, + poolId: poolId + }); + _mint(mintParam.miner, lid); + emit AddLiquidity(lid, pool, liquidity, amountX, amountY); + } + + /// @notice Burn a generated nft. + /// @param lid nft (liquidity) id + /// @return success successfully burn or not + function burn(uint256 lid) external checkAuth(lid) returns (bool success) { + Liquidity storage liquid = liquidities[lid]; + require(liquid.liquidity == 0 && liquid.remainTokenX == 0 && liquid.remainTokenY == 0, 'NC'); + delete liquidities[lid]; + _burn(lid); + return true; + } + + /// parameters when calling addLiquidity, grouped together to avoid stake too deep + struct AddLiquidityParam { + // id of nft + uint256 lid; + // amount limit of tokenX user willing to deposit + uint128 xLim; + // amount limit of tokenY user willing to deposit + uint128 yLim; + // min amount of tokenX user willing to deposit + uint128 amountXMin; + // min amount of tokenY user willing to deposit + uint128 amountYMin; + + uint256 deadline; + } + + function updateLiquidity( + Liquidity storage liquid, + address pool, + uint128 newLiquidity, + uint256 amountX, + uint256 amountY + ) private { + (uint256 lastFeeScaleX_128, uint256 lastFeeScaleY_128) = getLastFeeScale( + pool, liquidityKey(address(this), liquid.leftPt, liquid.rightPt) + ); + (uint256 deltaScaleX, uint256 deltaScaleY) = (liquid.lastFeeScaleX_128, liquid.lastFeeScaleY_128); + assembly { + deltaScaleX := sub(lastFeeScaleX_128, deltaScaleX) + deltaScaleY := sub(lastFeeScaleY_128, deltaScaleY) + } + liquid.remainTokenX += amountX + MulDivMath.mulDivFloor(deltaScaleX, liquid.liquidity, TwoPower.pow128); + liquid.remainTokenY += amountY + MulDivMath.mulDivFloor(deltaScaleY, liquid.liquidity, TwoPower.pow128); + liquid.lastFeeScaleX_128 = lastFeeScaleX_128; + liquid.lastFeeScaleY_128 = lastFeeScaleY_128; + liquid.liquidity = newLiquidity; + } + + /// @notice Add liquidity to a existing nft. + /// @param addLiquidityParam see AddLiquidityParam for more + /// @return liquidityDelta amount of added liquidity + /// @return amountX amount of tokenX deposited + /// @return amountY amonut of tokenY deposited + function addLiquidity( + AddLiquidityParam calldata addLiquidityParam + ) external payable checkAuth(addLiquidityParam.lid) checkDeadline(addLiquidityParam.deadline) returns ( + uint128 liquidityDelta, + uint256 amountX, + uint256 amountY + ) { + require(addLiquidityParam.lid < liquidityNum, "LN"); + Liquidity storage liquid = liquidities[addLiquidityParam.lid]; + PoolMeta memory poolMeta = poolMetas[liquid.poolId]; + int24 currPt; + uint160 sqrtPrice_96; + address pool = IiZiSwapFactory(factory).pool(poolMeta.tokenX, poolMeta.tokenY, poolMeta.fee); + // uint160 sqrtRate_96 = IiZiSwapPool(pool).sqrtRate_96(); + require(pool != address(0), "P0"); + (sqrtPrice_96, currPt) = getPoolPrice(pool); + liquidityDelta = MintMath.computeLiquidity( + MintMath.MintMathParam({ + pl: liquid.leftPt, + pr: liquid.rightPt, + xLim: addLiquidityParam.xLim, + yLim: addLiquidityParam.yLim + }), + currPt, + sqrtPrice_96, + // sqrtRate_96 + IiZiSwapPool(pool).sqrtRate_96() + ); + require(int128(liquid.liquidity) == int256(uint256(liquid.liquidity)), "LO"); + uint128 newLiquidity = liquidityDelta + liquid.liquidity; + (amountX, amountY) = IiZiSwapPool(pool).mint(address(this), liquid.leftPt, liquid.rightPt, liquidityDelta, + abi.encode(MintCallbackData({tokenX: poolMeta.tokenX, tokenY: poolMeta.tokenY, fee: poolMeta.fee, payer: msg.sender}))); + require(amountX >= addLiquidityParam.amountXMin, "XMIN"); + require(amountY >= addLiquidityParam.amountYMin, "YMIN"); + updateLiquidity(liquid, pool, newLiquidity, 0, 0); + emit AddLiquidity(addLiquidityParam.lid, pool, liquidityDelta, amountX, amountY); + } + + /// @notice Decrease liquidity from a nft. + /// @param lid id of nft + /// @param liquidDelta amount of liqudity to decrease + /// @param amountXMin min amount of tokenX user want to withdraw + /// @param amountYMin min amount of tokenY user want to withdraw + /// @param deadline deadline timestamp of transaction + /// @return amountX amount of tokenX refund to user + /// @return amountY amount of tokenY refund to user + function decLiquidity( + uint256 lid, + uint128 liquidDelta, + uint256 amountXMin, + uint256 amountYMin, + uint256 deadline + ) external checkAuth(lid) checkDeadline(deadline) returns ( + uint256 amountX, + uint256 amountY + ) { + require(lid < liquidityNum, "LN"); + Liquidity storage liquidity = liquidities[lid]; + if (liquidity.liquidity == 0) { + // no need to call core to update fee + return (0, 0); + } + if (liquidDelta > liquidity.liquidity) { + liquidDelta = liquidity.liquidity; + } + PoolMeta memory poolMeta = poolMetas[liquidity.poolId]; + address pool = IiZiSwapFactory(factory).pool(poolMeta.tokenX, poolMeta.tokenY, poolMeta.fee); + require(pool != address(0), "P0"); + + uint128 newLiquidity = liquidity.liquidity - liquidDelta; + (amountX, amountY) = IiZiSwapPool(pool).burn(liquidity.leftPt, liquidity.rightPt, liquidDelta); + require(amountX >= amountXMin, "XMIN"); + require(amountY >= amountYMin, "YMIN"); + updateLiquidity(liquidity, pool, newLiquidity, amountX, amountY); + emit DecLiquidity(lid, pool, liquidDelta, amountX, amountY); + } + + /// @notice Collect fee gained of token withdrawed from nft. + /// @param recipient address to receive token + /// @param lid id of nft + /// @param amountXLim amount limit of tokenX to collect + /// @param amountYLim amount limit of tokenY to collect + /// @return amountX amount of tokenX actually collect + /// @return amountY amount of tokenY actually collect + function collect( + address recipient, + uint256 lid, + uint128 amountXLim, + uint128 amountYLim + ) external payable checkAuth(lid) returns ( + uint256 amountX, + uint256 amountY + ) { + if (recipient == address(0)) { + recipient = address(this); + } + require(lid < liquidityNum, "LN"); + Liquidity storage liquidity = liquidities[lid]; + PoolMeta memory poolMeta = poolMetas[liquidity.poolId]; + address pool = IiZiSwapFactory(factory).pool(poolMeta.tokenX, poolMeta.tokenY, poolMeta.fee); + require(pool != address(0), "P0"); + if (liquidity.liquidity > 0) { + IiZiSwapPool(pool).burn(liquidity.leftPt, liquidity.rightPt, 0); + updateLiquidity(liquidity, pool, liquidity.liquidity, 0, 0); + } + if (amountXLim > liquidity.remainTokenX) { + amountXLim = uint128(liquidity.remainTokenX); + } + if (amountYLim > liquidity.remainTokenY) { + amountYLim = uint128(liquidity.remainTokenY); + } + (amountX, amountY) = IiZiSwapPool(pool).collect(recipient, liquidity.leftPt, liquidity.rightPt, amountXLim, amountYLim); + // amountX(Y)Lim may be a little greater than actual value + liquidity.remainTokenX -= amountXLim; + liquidity.remainTokenY -= amountYLim; + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/Quoter.sol b/contracts/dependencies/izumi/izumi-swap-periphery/Quoter.sol new file mode 100644 index 000000000..842e6ad58 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/Quoter.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./core/interfaces/IiZiSwapCallback.sol"; +import "./core/interfaces/IiZiSwapFactory.sol"; +import "./core/interfaces/IiZiSwapPool.sol"; + +import "./libraries/Path.sol"; + +import "./base/base.sol"; + +contract Quoter is Base, IiZiSwapCallback { + + using Path for bytes; + + struct SwapCallbackData { + bytes path; + address payer; + } + + uint256 private amountDesireCached; + + /// @notice Construct this contract. + /// @param _factory address iZiSwapFactory + /// @param _weth address of weth token + constructor(address _factory, address _weth) Base(_factory, _weth) {} + + + /// @notice Make multiple function calls in this contract in a single transaction + /// and return the data for each function call, donot revert if any function call fails + /// @param data The encoded function data for each function call + /// @return successes whether catch a revert in the function call of data[i] + /// @return results result of each function call + function multicallNoRevert(bytes[] calldata data) external payable returns (bool[]memory successes, bytes[] memory results) { + results = new bytes[](data.length); + successes = new bool[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + + successes[i] = success; + results[i] = result; + } + } + + function parseRevertReason(bytes memory reason) + private + pure + returns ( + uint256 amount, + int24 currPt + ) + { + if (reason.length != 64) { + if (reason.length < 68) revert('Unexpected error'); + assembly { + reason := add(reason, 0x04) + } + revert(abi.decode(reason, (string))); + } + return abi.decode(reason, (uint256, int24)); + } + + /// @notice Callback for swapY2X and swapY2XDesireX, in order to mark computed-amount of token and point after exchange. + /// @param x amount of tokenX trader acquired + /// @param y amount of tokenY need to pay from trader + /// @param path encoded SwapCallbackData + function swapY2XCallback( + uint256 x, + uint256 y, + bytes calldata path + ) external view override { + (address token0, address token1, uint24 fee) = path.decodeFirstPool(); + verify(token0, token1, fee); + + address poolAddr = pool(token0, token1, fee); + ( + , + int24 currPt, + , + , + , + , + , + ) = IiZiSwapPool(poolAddr).state(); + + if (token0 < token1) { + // token1 is y, amount of token1 is calculated + // called from swapY2XDesireX(...) + require(x >= amountDesireCached, 'x Pool Not Enough'); + assembly { + let ptr := mload(0x40) + mstore(ptr, y) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } else { + // token0 is y, amount of token0 is input param + // called from swapY2X(...) + assembly { + let ptr := mload(0x40) + mstore(ptr, x) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } + } + + /// @notice Callback for swapX2Y and swapX2YDesireY in order to mark computed-amount of token and point after exchange. + /// @param x amount of tokenX need to pay from trader + /// @param y amount of tokenY trader acquired + /// @param path encoded SwapCallbackData + function swapX2YCallback( + uint256 x, + uint256 y, + bytes calldata path + ) external view override { + (address token0, address token1, uint24 fee) = path.decodeFirstPool(); + verify(token0, token1, fee); + + address poolAddr = pool(token0, token1, fee); + ( + , + int24 currPt, + , + , + , + , + , + ) = IiZiSwapPool(poolAddr).state(); + + if (token0 < token1) { + // token0 is x, amount of token0 is input param + // called from swapX2Y(...) + assembly { + let ptr := mload(0x40) + mstore(ptr, y) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } else { + // token1 is x, amount of token1 is calculated param + // called from swapX2YDesireY(...) + require(y >= amountDesireCached, 'y Pool Not Enough'); + assembly { + let ptr := mload(0x40) + mstore(ptr, x) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } + } + + function swapAmountSingleInternal( + address tokenIn, + address tokenOut, + uint24 fee, + uint128 amount + ) private returns (uint256 acquire, int24 currPt) { + address poolAddr = pool(tokenOut, tokenIn, fee); + if (tokenIn < tokenOut) { + int24 boundaryPoint = -799999; + try + IiZiSwapPool(poolAddr).swapX2Y( + address(this), amount, boundaryPoint, + abi.encodePacked(tokenIn, fee, tokenOut) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } else { + int24 boundaryPoint = 799999; + try + IiZiSwapPool(poolAddr).swapY2X( + address(this), amount, boundaryPoint, + abi.encodePacked(tokenIn, fee, tokenOut) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } + } + + function swapAmount( + uint128 amount, + bytes memory path + ) public returns (uint256 acquire, int24[] memory pointAfterList) { + // allow swapping to the router address with address 0 + + uint256 i = 0; + pointAfterList = new int24[](path.numPools()); + + while (true) { + bool hasMultiplePools = path.hasMultiplePools(); + (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); + int24 finalPt; + (acquire, finalPt) = swapAmountSingleInternal(tokenIn, tokenOut, fee, amount); + pointAfterList[i] = finalPt; + i ++; + + // decide whether to continue or terminate + if (hasMultiplePools) { + path = path.skipToken(); + amount = uint128(acquire); + } else { + break; + } + } + } + + function swapDesireSingleInternal( + address tokenIn, + address tokenOut, + uint24 fee, + uint128 desire + ) private returns (uint256 cost, int24 currPt) { + address poolAddr = pool(tokenOut, tokenIn, fee); + amountDesireCached = desire; + if (tokenIn < tokenOut) { + int24 boundaryPoint = -799999; + try + IiZiSwapPool(poolAddr).swapX2YDesireY( + address(this), desire + 1, boundaryPoint, + abi.encodePacked(tokenOut, fee, tokenIn) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } else { + int24 boundaryPoint = 799999; + try + IiZiSwapPool(poolAddr).swapY2XDesireX( + address(this), desire + 1, boundaryPoint, + abi.encodePacked(tokenOut, fee, tokenIn) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } + } + + function swapDesire( + uint128 desire, + bytes memory path + ) public returns (uint256 cost, int24[] memory pointAfterList) { + // allow swapping to the router address with address 0 + + uint256 i = 0; + pointAfterList = new int24[](path.numPools()); + + while (true) { + bool hasMultiplePools = path.hasMultiplePools(); + (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool(); + int24 finalPt; + (cost, finalPt) = swapDesireSingleInternal(tokenIn, tokenOut, fee, desire); + pointAfterList[i] = finalPt; + i ++; + + // decide whether to continue or terminate + if (hasMultiplePools) { + path = path.skipToken(); + desire = uint128(cost); + } else { + break; + } + } + } + + /// @notice Estimate amount of tokenX acquired when user wants to buy tokenX given max amount of tokenY user willing to pay. + /// calling this function will not generate any real exchanges in the pool + /// @param tokenX tokenX of swap pool + /// @param tokenY tokenY of swap pool + /// @param fee fee amount of swap pool + /// @param amount max-amount of tokenY user willing to pay + /// @param highPt highest point during exchange + /// @return amountX estimated amount of tokenX user would acquire + /// @return finalPoint estimated point of pool after swap + function swapY2X( + address tokenX, + address tokenY, + uint24 fee, + uint128 amount, + int24 highPt + ) public returns (uint256 amountX, int24 finalPoint) { + require(tokenX < tokenY, "x 799999) { + boundary = 799999; + } + boundaryPoint = int24(boundary); + } + + function getLowerBoundaryPoint(uint24 fee, int24 currentPoint) internal pure returns (int24 boundaryPoint) { + int256 boundary = currentPoint; + if (fee <= 100) { + boundary -= 10000; + } else { + boundary -= 20000; + } + if (boundary < -799999) { + boundary = -799999; + } + boundaryPoint = int24(boundary); + } + + /// @notice Callback for swapY2X and swapY2XDesireX, in order to mark computed-amount of token and point after exchange. + /// @param x amount of tokenX trader acquired + /// @param y amount of tokenY need to pay from trader + /// @param path encoded SwapCallbackData + function swapY2XCallback( + uint256 x, + uint256 y, + bytes calldata path + ) external view override { + (address token0, address token1, uint24 fee) = path.decodeFirstPool(); + verify(token0, token1, fee); + + address poolAddr = pool(token0, token1, fee); + ( + , + int24 currPt, + , + , + , + , + , + ) = IiZiSwapPool(poolAddr).state(); + + if (token0 < token1) { + // token1 is y, amount of token1 is calculated + // called from swapY2XDesireX(...) + require(x >= amountDesireCached, 'x Pool Not Enough'); + assembly { + let ptr := mload(0x40) + mstore(ptr, y) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } else { + // token0 is y, amount of token0 is input param + // called from swapY2X(...) + assembly { + let ptr := mload(0x40) + mstore(ptr, x) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } + } + + /// @notice Callback for swapX2Y and swapX2YDesireY in order to mark computed-amount of token and point after exchange. + /// @param x amount of tokenX need to pay from trader + /// @param y amount of tokenY trader acquired + /// @param path encoded SwapCallbackData + function swapX2YCallback( + uint256 x, + uint256 y, + bytes calldata path + ) external view override { + (address token0, address token1, uint24 fee) = path.decodeFirstPool(); + verify(token0, token1, fee); + + address poolAddr = pool(token0, token1, fee); + ( + , + int24 currPt, + , + , + , + , + , + ) = IiZiSwapPool(poolAddr).state(); + + if (token0 < token1) { + // token0 is x, amount of token0 is input param + // called from swapX2Y(...) + assembly { + let ptr := mload(0x40) + mstore(ptr, y) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } else { + // token1 is x, amount of token1 is calculated param + // called from swapX2YDesireY(...) + require(y >= amountDesireCached, 'y Pool Not Enough'); + assembly { + let ptr := mload(0x40) + mstore(ptr, x) + mstore(add(ptr, 0x20), currPt) + revert(ptr, 64) + } + } + } + + function swapAmountSingleInternal( + address tokenIn, + address tokenOut, + uint24 fee, + uint128 amount + ) private returns (uint256 acquire, int24 currPt) { + address poolAddr = pool(tokenOut, tokenIn, fee); + int24 currentPoint = getCurrentPoint(poolAddr); + if (tokenIn < tokenOut) { + int24 boundaryPoint = getLowerBoundaryPoint(fee, currentPoint); + try + IiZiSwapPool(poolAddr).swapX2Y( + address(this), amount, boundaryPoint, + abi.encodePacked(tokenIn, fee, tokenOut) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } else { + int24 boundaryPoint = getUpperBoundaryPoint(fee, currentPoint); + try + IiZiSwapPool(poolAddr).swapY2X( + address(this), amount, boundaryPoint, + abi.encodePacked(tokenIn, fee, tokenOut) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } + } + + function swapAmount( + uint128 amount, + bytes memory path + ) public returns (uint256 acquire, int24[] memory pointAfterList) { + // allow swapping to the router address with address 0 + + uint256 i = 0; + pointAfterList = new int24[](path.numPools()); + + while (true) { + bool hasMultiplePools = path.hasMultiplePools(); + (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); + int24 finalPt; + (acquire, finalPt) = swapAmountSingleInternal(tokenIn, tokenOut, fee, amount); + pointAfterList[i] = finalPt; + i ++; + + // decide whether to continue or terminate + if (hasMultiplePools) { + path = path.skipToken(); + amount = uint128(acquire); + } else { + break; + } + } + } + + function swapDesireSingleInternal( + address tokenIn, + address tokenOut, + uint24 fee, + uint128 desire + ) private returns (uint256 cost, int24 currPt) { + address poolAddr = pool(tokenOut, tokenIn, fee); + amountDesireCached = desire; + int24 currentPoint = getCurrentPoint(poolAddr); + if (tokenIn < tokenOut) { + int24 boundaryPoint = getLowerBoundaryPoint(fee, currentPoint); + try + IiZiSwapPool(poolAddr).swapX2YDesireY( + address(this), desire + 1, boundaryPoint, + abi.encodePacked(tokenOut, fee, tokenIn) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } else { + int24 boundaryPoint = getUpperBoundaryPoint(fee, currentPoint); + try + IiZiSwapPool(poolAddr).swapY2XDesireX( + address(this), desire + 1, boundaryPoint, + abi.encodePacked(tokenOut, fee, tokenIn) + ) + {} catch (bytes memory reason) { + return parseRevertReason(reason); + } + } + } + + function swapDesire( + uint128 desire, + bytes memory path + ) public returns (uint256 cost, int24[] memory pointAfterList) { + // allow swapping to the router address with address 0 + + uint256 i = 0; + pointAfterList = new int24[](path.numPools()); + + while (true) { + bool hasMultiplePools = path.hasMultiplePools(); + (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool(); + int24 finalPt; + (cost, finalPt) = swapDesireSingleInternal(tokenIn, tokenOut, fee, desire); + pointAfterList[i] = finalPt; + i ++; + + // decide whether to continue or terminate + if (hasMultiplePools) { + path = path.skipToken(); + desire = uint128(cost); + } else { + break; + } + } + } + + /// @notice Estimate amount of tokenX acquired when user wants to buy tokenX given max amount of tokenY user willing to pay. + /// calling this function will not generate any real exchanges in the pool + /// @param tokenX tokenX of swap pool + /// @param tokenY tokenY of swap pool + /// @param fee fee amount of swap pool + /// @param amount max-amount of tokenY user willing to pay + /// @param highPt highest point during exchange + /// @return amountX estimated amount of tokenX user would acquire + /// @return finalPoint estimated point of pool after swap + function swapY2X( + address tokenX, + address tokenY, + uint24 fee, + uint128 amount, + int24 highPt + ) public returns (uint256 amountX, int24 finalPoint) { + require(tokenX < tokenY, "x= params.desire, 'Too much requested in swapDesire'); + payedCached = DEFAULT_PAYED_CACHED; + } + + struct SwapAmountParams { + bytes path; + address recipient; + // uint256 deadline; + uint128 amount; + uint256 minAcquired; + + uint256 deadline; + } + + /// @notice Swap given amount of input token, usually used in multi-hop case. + function swapAmount(SwapAmountParams calldata params) + external + payable + checkDeadline(params.deadline) + returns (uint256 cost, uint256 acquire) + { + (cost, acquire) = swapAmountInternal( + params.amount, + params.recipient, + SwapCallbackData({path: params.path, payer: msg.sender}) + ); + require(acquire >= params.minAcquired, 'Too much requested in swapAmount'); + } + + /// parameters when calling Swap.swap..., grouped together to avoid stake too deep + struct SwapParams { + // tokenX of swap pool + address tokenX; + // tokenY of swap pool + address tokenY; + // fee amount of swap pool + uint24 fee; + // highPt for y2x, lowPt for x2y + // here y2X is calling swapY2X or swapY2XDesireX + // in swapY2XDesireX, if boundaryPt is 800001, means user wants to get enough X + // in swapX2YDesireY, if boundaryPt is -800001, means user wants to get enough Y + int24 boundaryPt; + // who will receive acquired token + address recipient; + // desired amount for desired mode, paid amount for non-desired mode + // here, desire mode is calling swapX2YDesireY or swapY2XDesireX + uint128 amount; + // max amount of payed token from trader, used in desire mode + uint256 maxPayed; + // min amount of received token trader wanted, used in undesire mode + uint256 minAcquired; + + uint256 deadline; + } + + // amount of exchanged tokens + struct ExchangeAmount { + // amount of tokenX paid or acquired + uint256 amountX; + // amount of tokenY acquired or paid + uint256 amountY; + } + + /// @notice Swap tokenY for tokenX, given max amount of tokenY user willing to pay + /// @param swapParams params(for example: max amount in above line), see SwapParams for more + function swapY2X( + SwapParams calldata swapParams + ) external payable checkDeadline(swapParams.deadline) { + require(swapParams.tokenX < swapParams.tokenY, "x= swapParams.minAcquired, "XMIN"); + } + + /// @notice Swap tokenY for tokenX, given user's desired amount of tokenX. + /// @param swapParams params(for example: desired amount in above line), see SwapParams for more + function swapY2XDesireX( + SwapParams calldata swapParams + ) external payable checkDeadline(swapParams.deadline) { + require(swapParams.tokenX < swapParams.tokenY, "x= swapParams.amount, 'Too much requested in swapY2XDesireX'); + } + require(amount.amountY <= swapParams.maxPayed, "YMAX"); + } + + /// @notice Swap tokenX for tokenY, given max amount of tokenX user willing to pay. + /// @param swapParams params(for example: max amount in above line), see SwapParams for more + function swapX2Y( + SwapParams calldata swapParams + ) external payable checkDeadline(swapParams.deadline) { + require(swapParams.tokenX < swapParams.tokenY, "x= swapParams.minAcquired, "YMIN"); + } + + /// @notice Swap tokenX for tokenY, given amount of tokenY user desires. + /// @param swapParams params(for example: desired amount in above line), see SwapParams for more + function swapX2YDesireY( + SwapParams calldata swapParams + ) external payable checkDeadline(swapParams.deadline) { + require(swapParams.tokenX < swapParams.tokenY, "x= swapParams.amount, 'Too much requested in swapX2YDesireY'); + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/base/Switch.sol b/contracts/dependencies/izumi/izumi-swap-periphery/base/Switch.sol new file mode 100644 index 000000000..f79ea0a51 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/base/Switch.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +abstract contract Switch is Ownable { + + bool public pause = false; + modifier notPause() { + require(!pause, "paused"); + _; + } + function setPause(bool value) external onlyOwner { + pause = value; + } +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/base/base.sol b/contracts/dependencies/izumi/izumi-swap-periphery/base/base.sol new file mode 100644 index 000000000..aa44de053 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/base/base.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "../core/interfaces/IiZiSwapFactory.sol"; + +/// @title Interface for WETH9 +interface IWETH9 is IERC20 { + /// @notice Deposit ether to get wrapped ether + function deposit() external payable; + + /// @notice Withdraw wrapped ether to get ether + function withdraw(uint256) external; +} + +abstract contract Base { + /// @notice address of iZiSwapFactory + address public immutable factory; + + /// @notice address of weth9 token + address public immutable WETH9; + + modifier checkDeadline(uint256 deadline) { + require(block.timestamp <= deadline, 'Out of time'); + _; + } + + receive() external payable {} + + /// @notice Constructor of base. + /// @param _factory address of iZiSwapFactory + /// @param _WETH9 address of weth9 token + constructor(address _factory, address _WETH9) { + factory = _factory; + WETH9 = _WETH9; + } + + /// @notice Make multiple function calls in this contract in a single transaction + /// and return the data for each function call, revert if any function call fails + /// @param data The encoded function data for each function call + /// @return results result of each function call + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + + if (!success) { + if (result.length < 68) revert(); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + + results[i] = result; + } + } + + /// @notice Transfer tokens from the targeted address to the given destination + /// @notice Errors with 'STF' if transfer fails + /// @param token The contract address of the token to be transferred + /// @param from The originating address from which the tokens will be transferred + /// @param to The destination address of the transfer + /// @param value The amount to be transferred + function safeTransferFrom( + address token, + address from, + address to, + uint256 value + ) internal { + (bool success, bytes memory data) = + token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'STF'); + } + + /// @notice Transfer tokens from msg.sender to a recipient + /// @dev Errors with ST if transfer fails + /// @param token The contract address of the token which will be transferred + /// @param to The recipient of the transfer + /// @param value The value of the transfer + function safeTransfer( + address token, + address to, + uint256 value + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'ST'); + } + + /// @notice Approve the stipulated contract to spend the given allowance in the given token + /// @dev Errors with 'SA' if transfer fails + /// @param token The contract address of the token to be approved + /// @param to The target of the approval + /// @param value The amount of the given token the target will be allowed to spend + function safeApprove( + address token, + address to, + uint256 value + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'SA'); + } + + /// @notice Transfer ETH to the recipient address + /// @dev Fails with `STE` + /// @param to The destination of the transfer + /// @param value The value to be transferred + function safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, 'STE'); + } + + /// @notice Withdraw all weth9 token of this contract and send the withdrawed eth to recipient + /// usually used in multicall when mint/swap/update limitorder with eth + /// normally this contract has no any erc20 token or eth after or before a transaction + /// we donot need to worry that some one can steal eth from this contract + /// @param minAmount The minimum amount of WETH9 to withdraw + /// @param recipient The address to receive all withdrawed eth from this contract + function unwrapWETH9(uint256 minAmount, address recipient) external payable { + uint256 all = IWETH9(WETH9).balanceOf(address(this)); + require(all >= minAmount, 'WETH9 Not Enough'); + + if (all > 0) { + IWETH9(WETH9).withdraw(all); + safeTransferETH(recipient, all); + } + } + + /// @notice Send all balance of specified token in this contract to recipient + /// usually used in multicall when mint/swap/update limitorder with eth + /// normally this contract has no any erc20 token or eth after or before a transaction + /// we donot need to worry that some one can steal some token from this contract + /// @param token address of the token + /// @param minAmount balance should >= minAmount + /// @param recipient the address to receive specified token from this contract + function sweepToken( + address token, + uint256 minAmount, + address recipient + ) external payable { + uint256 all = IERC20(token).balanceOf(address(this)); + require(all >= minAmount, 'WETH9 Not Enough'); + + if (all > 0) { + safeTransfer(token, recipient, all); + } + } + + /// @notice Send all balance of eth in this contract to msg.sender + /// usually used in multicall when mint/swap/update limitorder with eth + /// normally this contract has no any erc20 token or eth after or before a transaction + /// we donot need to worry that some one can steal some token from this contract + function refundETH() external payable { + if (address(this).balance > 0) safeTransferETH(msg.sender, address(this).balance); + } + + /// @param token The token to pay + /// @param payer The entity that must pay + /// @param recipient The entity that will receive payment + /// @param value The amount to pay + function pay( + address token, + address payer, + address recipient, + uint256 value + ) internal { + if (token == WETH9 && address(this).balance >= value) { + // pay with WETH9 + IWETH9(WETH9).deposit{value: value}(); // wrap only what is needed to pay + IWETH9(WETH9).transfer(recipient, value); + } else if (payer == address(this)) { + // pay with tokens already in the contract (for the exact input multihop case) + safeTransfer(token, recipient, value); + } else { + // pull payment + safeTransferFrom(token, payer, recipient, value); + } + } + + /// @notice Query pool address from factory by (tokenX, tokenY, fee). + /// @param tokenX tokenX of swap pool + /// @param tokenY tokenY of swap pool + /// @param fee fee amount of swap pool + function pool(address tokenX, address tokenY, uint24 fee) public view returns(address) { + return IiZiSwapFactory(factory).pool(tokenX, tokenY, fee); + } + function verify(address tokenX, address tokenY, uint24 fee) internal view { + require (msg.sender == pool(tokenX, tokenY, fee), "sp"); + } +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/box/Box.sol b/contracts/dependencies/izumi/izumi-swap-periphery/box/Box.sol new file mode 100644 index 000000000..026d221cf --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/box/Box.sol @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +import "../interfaces/ILiquidityManager.sol"; +import "../interfaces/IBase.sol"; +import "../interfaces/IWrapToken.sol"; +import "../interfaces/ISwap.sol"; + +/// @title Interface for WETH9 +interface IWETH9 is IERC20 { + /// @notice Deposit ether to get wrapped ether + function deposit() external payable; + + /// @notice Withdraw wrapped ether to get ether + function withdraw(uint256) external; +} + +contract Box is ReentrancyGuard { + + using SafeERC20 for IERC20; + + uint256 private constant ADDR_SIZE = 20; + bool public isMintOrAddLiquidity = false; + + struct PeripheryAddr { + address weth; + address liquidityManager; + address swap; + } + PeripheryAddr public peripheryAddr; + // mapping(address=>address) public wrap2Token; + + receive() external payable {} + + constructor(PeripheryAddr memory param) { + peripheryAddr = param; + } + + // function setWrap(address wrap, address token) external onlyOwner { + // wrap2Token[wrap] = token; + // } + + function _recvTokenFromUser(address token, bool isWrapToken, uint256 amount) internal returns(uint256 actualAmount) { + if (amount == 0) { + return 0; + } + if (token == peripheryAddr.weth) { + require(msg.value >= amount, '[recvTokenFromUser]: msg.value not enough'); + actualAmount = amount; + } else if (isWrapToken) { + actualAmount = IWrapToken(token).depositFrom(msg.sender, address(this), amount); + } else { + // no need to check, because iziswap core will revert unenough amount + bool ok = IERC20(token).transferFrom(msg.sender, address(this), amount); + require(ok, '[recvTokenFromUser]: erc20 transfer fail'); + actualAmount = amount; + } + } + + /// @notice Transfers ETH to the recipient address + /// @dev Fails with `STE` + /// @param to The destination of the transfer + /// @param value The value to be transferred + function _safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, "STE"); + } + + function _transferTokenToUser(address token, bool isWrapToken, address to, uint256 value) internal returns(uint256 actualAmount) { + if (value > 0) { + if (token == address(peripheryAddr.weth)) { + IWETH9(token).withdraw(value); + _safeTransferETH(to, value); + } else if (isWrapToken) { + return IWrapToken(token).withdraw(to, value); + } else { + IERC20(token).safeTransfer(to, value); + } + } + actualAmount = value; + } + + modifier checkNftOwner(uint256 lid) { + require(ILiquidityManager(peripheryAddr.liquidityManager).ownerOf(lid) == msg.sender, 'not owner'); + _; + } + function mint( + ILiquidityManager.MintParam calldata mintParam, + bool tokenXIsWrap, + bool tokenYIsWrap + ) external payable nonReentrant returns( + uint256 lid, + uint128 liquidity, + uint256 amountX, + uint256 amountY + ) { + isMintOrAddLiquidity = true; + uint256 actualLimX = _recvTokenFromUser(mintParam.tokenX, tokenXIsWrap, mintParam.xLim); + uint256 actualLimY = _recvTokenFromUser(mintParam.tokenY, tokenYIsWrap, mintParam.yLim); + delete isMintOrAddLiquidity; + ILiquidityManager.MintParam memory actualParam = mintParam; + actualParam.xLim = uint128(actualLimX); + actualParam.yLim = uint128(actualLimY); + if (actualParam.miner == address(0)) { + actualParam.miner = msg.sender; + } + if (actualParam.tokenX != peripheryAddr.weth) { + IERC20(actualParam.tokenX).approve(peripheryAddr.liquidityManager, type(uint256).max); + } + if (actualParam.tokenY != peripheryAddr.weth) { + IERC20(actualParam.tokenY).approve(peripheryAddr.liquidityManager, type(uint256).max); + } + + ( + lid, + liquidity, + amountX, + amountY + ) = ILiquidityManager(peripheryAddr.liquidityManager).mint{ + value: msg.value + }(actualParam); + // refund eth to this contract + IBase(peripheryAddr.liquidityManager).refundETH(); + + // refund + if (actualParam.tokenX == peripheryAddr.weth) { + if (amountX < msg.value) { + _safeTransferETH(msg.sender, msg.value - amountX); + } + } else { + if (amountX < actualLimX) { + _transferTokenToUser(actualParam.tokenX, tokenXIsWrap, msg.sender, actualLimX - amountX); + } + } + + if (actualParam.tokenY == peripheryAddr.weth) { + if (amountY < msg.value) { + _safeTransferETH(msg.sender, msg.value - amountY); + } + } else { + if (amountY < actualLimY) { + _transferTokenToUser(actualParam.tokenY, tokenYIsWrap, msg.sender, actualLimY - amountY); + } + } + } + + function addLiquidity( + ILiquidityManager.AddLiquidityParam calldata addParam, + address tokenX, + address tokenY, + bool tokenXIsWrap, + bool tokenYIsWrap + ) external payable checkNftOwner(addParam.lid) nonReentrant returns( + uint128 liquidityDelta, + uint256 amountX, + uint256 amountY + ) { + isMintOrAddLiquidity = true; + // no need to check lid, wrapTokenX, wrapTokenY + // because core will revert unenough deposit + // and this contract will not save any token(including eth) theorily + // so we donot care that some one will steal token from this contract + uint256 actualLimX = _recvTokenFromUser(tokenX, tokenXIsWrap, addParam.xLim); + uint256 actualLimY = _recvTokenFromUser(tokenY, tokenYIsWrap, addParam.yLim); + delete isMintOrAddLiquidity; + ILiquidityManager.AddLiquidityParam memory actualParam = addParam; + actualParam.xLim = uint128(actualLimX); + actualParam.yLim = uint128(actualLimY); + if (tokenX != peripheryAddr.weth) { + IERC20(tokenX).approve(peripheryAddr.liquidityManager, type(uint256).max); + } + if (tokenY != peripheryAddr.weth) { + IERC20(tokenY).approve(peripheryAddr.liquidityManager, type(uint256).max); + } + + ( + liquidityDelta, + amountX, + amountY + ) = ILiquidityManager(peripheryAddr.liquidityManager).addLiquidity{ + value: msg.value + }(actualParam); + // refund eth to this contract + IBase(peripheryAddr.liquidityManager).refundETH(); + + // refund + if (tokenX == peripheryAddr.weth) { + if (amountX < msg.value) { + _safeTransferETH(msg.sender, msg.value - amountX); + } + } else { + if (amountX < actualLimX) { + _transferTokenToUser(tokenX, tokenXIsWrap, msg.sender, actualLimX - amountX); + } + } + + if (tokenY == peripheryAddr.weth) { + if (amountY < msg.value) { + _safeTransferETH(msg.sender, msg.value - amountY); + } + } else { + if (amountY < actualLimY) { + _transferTokenToUser(tokenY, tokenYIsWrap, msg.sender, actualLimY - amountY); + } + } + } + + function _collect( + address recipient, + uint256 lid, + uint128 amountXLim, + uint128 amountYLim, + address tokenX, + address tokenY, + bool tokenXIsWrap, + bool tokenYIsWrap + ) internal returns ( + uint256 amountX, + uint256 amountY + ) { + ( + amountX, + amountY + ) = ILiquidityManager(peripheryAddr.liquidityManager).collect( + address(this), + lid, + amountXLim, + amountYLim + ); + if (amountX > 0) { + amountX = _transferTokenToUser(tokenX, tokenXIsWrap, recipient, amountX); + } + if (amountY > 0) { + amountY = _transferTokenToUser(tokenY, tokenYIsWrap, recipient, amountY); + } + } + + + function collect( + address recipient, + uint256 lid, + uint128 amountXLim, + uint128 amountYLim, + address tokenX, + address tokenY, + bool tokenXIsWrap, + bool tokenYIsWrap + ) external checkNftOwner(lid) nonReentrant returns ( + uint256 amountX, + uint256 amountY + ) { + // no need to check lid, wrapTokenX, wrapTokenY + // because core will revert unenough deposit + // and this contract will not save any token(including eth) theorily + // so we donot care that some one will steal token from this contract + (amountX, amountY) = _collect(recipient, lid, amountXLim, amountYLim, tokenX, tokenY, tokenXIsWrap, tokenYIsWrap); + } + + function decreaseLiquidity( + address recipient, + uint256 lid, + uint128 liquidDelta, + uint128 amountXMin, + uint128 amountYMin, + uint256 deadline, + address tokenX, + address tokenY, + bool tokenXIsWrap, + bool tokenYIsWrap + ) external checkNftOwner(lid) nonReentrant returns ( + uint256 amountX, + uint256 amountY + ) { + ILiquidityManager(peripheryAddr.liquidityManager).decLiquidity( + lid, + liquidDelta, + amountXMin, + amountYMin, + deadline + ); + (amountX, amountY) = _collect(recipient, lid, type(uint128).max, type(uint128).max, tokenX, tokenY, tokenXIsWrap, tokenYIsWrap); + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_start + 20 >= _start, 'toAddress_overflow'); + require(_bytes.length >= _start + 20, 'toAddress_outOfBounds'); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + function decodeFirstLastToken(bytes memory path) + internal + pure + returns ( + address firstToken, + address lastToken + ) + { + firstToken = toAddress(path, 0); + lastToken = toAddress(path, path.length - ADDR_SIZE); + } + function swapAmount( + ISwap.SwapAmountParams calldata params, + bool firstIsWrap, + bool lastIsWrap + ) external payable returns (uint256 cost, uint256 acquire) { + (address firstToken, address lastToken) = decodeFirstLastToken(params.path); + + uint256 actualIn = _recvTokenFromUser(firstToken, firstIsWrap, params.amount); + + if (firstToken != peripheryAddr.weth) { + IERC20(firstToken).approve(peripheryAddr.swap, type(uint256).max); + } + + ISwap.SwapAmountParams memory newParam = params; + newParam.recipient = address(this); + newParam.amount = uint128(actualIn); + (cost, acquire) = ISwap(peripheryAddr.swap).swapAmount{ + value: msg.value + }(newParam); + + // refund eth to this contract, if firstToken is weth + IBase(peripheryAddr.swap).refundETH(); + if (firstToken == peripheryAddr.weth) { + if (cost < msg.value) { + _safeTransferETH(msg.sender, msg.value - cost); + } + } else { + if (cost < actualIn) { + _transferTokenToUser(firstToken, firstIsWrap, msg.sender, actualIn - cost); + } + } + + if (acquire > 0) { + _transferTokenToUser(lastToken, lastIsWrap, params.recipient, acquire); + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/box/TokenWithSwapFee.sol b/contracts/dependencies/izumi/izumi-swap-periphery/box/TokenWithSwapFee.sol new file mode 100644 index 000000000..f30bc1b64 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/box/TokenWithSwapFee.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; + +import "../core/interfaces/IiZiSwapFactory.sol"; +import "./WrapToken.sol"; + +interface IBox { + function isMintOrAddLiquidity() external view returns(bool); +} + +interface IFarmWithWrap { + function isDeposit() external view returns(bool); +} + +contract TokenWithSwapFee is Context, IERC20, IERC20Metadata, Ownable { + uint8 public override decimals; + uint8 public feePercent; + + address public wrapToken; + address public box; + address public farm; + address public feeReceiver; + + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + + uint256 currentAllowance = _allowances[sender][_msgSender()]; + require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance"); + unchecked { + _approve(sender, _msgSender(), currentAllowance - amount); + } + + return true; + } + + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue); + return true; + } + + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + uint256 currentAllowance = _allowances[_msgSender()][spender]; + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(_msgSender(), spender, currentAllowance - subtractedValue); + } + + return true; + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + _totalSupply += amount; + _balances[account] += amount; + emit Transfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + } + _totalSupply -= amount; + + emit Transfer(account, address(0), amount); + } + + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + constructor( + string memory name, + string memory symbol, + uint8 _decimal, + uint8 _feePercent, + address _box, + address _feeReceiver + ) { + _name = name; + _symbol = symbol; + _mint(msg.sender, 10000000000000000000000000000); + decimals = _decimal; + feePercent = _feePercent; + require(feePercent < 100, "f100"); + + box = _box; + + // bytes32 warpTokenSalt = keccak256(abi.encode(address(this), _name, _symbol)); + + // wrapToken = address(new WrapToken{salt: warpTokenSalt}(address(this), _name, _symbol)); + feeReceiver = _feeReceiver; + farm = address(0); + wrapToken = address(0); + } + + function setFarm(address _farm) external onlyOwner { + // restrict owner to set only once + if (farm == address(0)) { + farm = _farm; + } + } + + function setWrapToken(address _wrapToken) external onlyOwner { + // restrict owner to set only once + if (wrapToken == address(0)) { + wrapToken = _wrapToken; + } + } + + function mint(address account, uint256 amount) public onlyOwner { + _mint(account, amount); + } + + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + require(recipient != address(this), "not this!"); + + uint256 senderBalance = _balances[sender]; + require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[sender] = senderBalance - amount; + } + uint256 feeAmount = 0; + if (recipient == wrapToken) { + bool hasFee = true; + if (IBox(box).isMintOrAddLiquidity()) { + hasFee = false; + } + if (farm != address(0)) { + if (IFarmWithWrap(farm).isDeposit()) { + hasFee = false; + } + } + if (hasFee) { + feeAmount = amount * feePercent / 100; + } + } else { + // (bool sx, bytes memory dx) = recipient.staticcall(abi.encodeWithSignature("tokenX()")); + // (bool sy, bytes memory dy) = recipient.staticcall(abi.encodeWithSignature("tokenY()")); + // if (sx && sy) { + + // } + } + _balances[recipient] += amount - feeAmount; + _balances[feeReceiver] += feeAmount; + + emit Transfer(sender, recipient, amount); + } +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/box/WrapToken.sol b/contracts/dependencies/izumi/izumi-swap-periphery/box/WrapToken.sol new file mode 100644 index 000000000..d8cd4e021 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/box/WrapToken.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +import "../interfaces/IWrapToken.sol"; + +interface IERC20WithDecimals { + function decimals() external view returns (uint8); +} + +contract WrapToken is IWrapToken, ERC20 { + using SafeERC20 for IERC20; + + address public override originToken; + mapping(address=>mapping(address=>uint256)) public override depositAllowance; + + uint8 private _decimals; + + constructor(address token, string memory name, string memory symbol) ERC20(name, symbol) { + originToken = token; + _decimals = IERC20WithDecimals(token).decimals(); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + function depositApprove(address spender, uint256 amount) external override { + if (spender != msg.sender) { + depositAllowance[msg.sender][spender] = amount; + } + } + function depositFrom(address from, address to, uint256 amount) external override returns (uint256 actualAmount) { + if (from != msg.sender) { + uint256 allow = depositAllowance[from][msg.sender]; + require(allow >= amount, "deposit allowance not enough"); + depositAllowance[from][msg.sender] = allow - amount; + } + uint256 balanceBefore = IERC20(originToken).balanceOf(address(this)); + IERC20(originToken).safeTransferFrom(from, address(this), amount); + uint256 balanceAfter = IERC20(originToken).balanceOf(address(this)); + actualAmount = balanceAfter - balanceBefore; + _mint(to, actualAmount); + } + function withdraw(address to, uint256 amount) external override returns(uint256 actualAmount) { + _burn(msg.sender, amount); + uint256 originBalanceBefore = IERC20(originToken).balanceOf(to); + IERC20(originToken).safeTransfer(to, amount); + uint256 originBalanceAfter = IERC20(originToken).balanceOf(to); + actualAmount = originBalanceAfter - originBalanceBefore; + } +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IERC20Minimal.sol b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IERC20Minimal.sol new file mode 100644 index 000000000..e2e333e41 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IERC20Minimal.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.4; + +/// @title Minimal ERC20 interface for Uniswap +/// @notice Contains a subset of the full ERC20 interface that is used in Uniswap V3 +interface IERC20Minimal { + /// @notice Returns the balance of a token + /// @param account The account for which to look up the number of tokens it has, i.e. its balance + /// @return The number of tokens held by the account + function balanceOf(address account) external view returns (uint256); + + /// @notice Transfers the amount of token from the `msg.sender` to the recipient + /// @param recipient The account that will receive the amount transferred + /// @param amount The number of tokens to send from the sender to the recipient + /// @return Returns true for a successful transfer, false for an unsuccessful transfer + function transfer(address recipient, uint256 amount) external returns (bool); + + /// @notice Returns the current allowance given to a spender by an owner + /// @param owner The account of the token owner + /// @param spender The account of the token spender + /// @return The current allowance granted by `owner` to `spender` + function allowance(address owner, address spender) external view returns (uint256); + + /// @notice Sets the allowance of a spender from the `msg.sender` to the value `amount` + /// @param spender The account which will be allowed to spend a given amount of the owners tokens + /// @param amount The amount of tokens allowed to be used by `spender` + /// @return Returns true for a successful approval, false for unsuccessful + function approve(address spender, uint256 amount) external returns (bool); + + /// @notice Transfers `amount` tokens from `sender` to `recipient` up to the allowance given to the `msg.sender` + /// @param sender The account from which the transfer will be initiated + /// @param recipient The recipient of the transfer + /// @param amount The amount of the transfer + /// @return Returns true for a successful transfer, false for unsuccessful + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /// @notice Event emitted when tokens are transferred from one address to another, either via `#transfer` or `#transferFrom`. + /// @param from The account from which the tokens were sent, i.e. the balance decreased + /// @param to The account to which the tokens were sent, i.e. the balance increased + /// @param value The amount of tokens that were transferred + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @notice Event emitted when the approval amount for the spender of a given owner's tokens changes. + /// @param owner The account that approved spending of its tokens + /// @param spender The account for which the spending allowance was modified + /// @param value The new allowance from the owner to the spender + event Approval(address indexed owner, address indexed spender, uint256 value); + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IOwnable.sol b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IOwnable.sol new file mode 100644 index 000000000..971a47441 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IOwnable.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IOwnable { + /** + * @dev Returns the address of the current owner. + */ + function owner() external view returns (address); +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapCallback.sol b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapCallback.sol new file mode 100644 index 000000000..f194b1bd6 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapCallback.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IiZiSwapMintCallback { + + /// @notice Called to msg.sender in iZiSwapPool#mint call + /// @param x Amount of tokenX need to pay from miner + /// @param y Amount of tokenY need to pay from miner + /// @param data Any data passed through by the msg.sender via the iZiSwapPool#mint call + function mintDepositCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + +} + +interface IiZiSwapCallback { + + /// @notice Called to msg.sender in iZiSwapPool#swapY2X(DesireX) call + /// @param x Amount of tokenX trader will acquire + /// @param y Amount of tokenY trader will pay + /// @param data Any dadta passed though by the msg.sender via the iZiSwapPool#swapY2X(DesireX) call + function swapY2XCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + + /// @notice Called to msg.sender in iZiSwapPool#swapX2Y(DesireY) call + /// @param x Amount of tokenX trader will pay + /// @param y Amount of tokenY trader will require + /// @param data Any dadta passed though by the msg.sender via the iZiSwapPool#swapX2Y(DesireY) call + function swapX2YCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + +} + +interface IiZiSwapAddLimOrderCallback { + + /// @notice Called to msg.sender in iZiSwapPool#addLimOrderWithX(Y) call + /// @param x Amount of tokenX seller will pay + /// @param y Amount of tokenY seller will pay + /// @param data Any dadta passed though by the msg.sender via the iZiSwapPool#addLimOrderWithX(Y) call + function payCallback( + uint256 x, + uint256 y, + bytes calldata data + ) external; + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapFactory.sol b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapFactory.sol new file mode 100644 index 000000000..c4fdcfaa3 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapFactory.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IiZiSwapFactory { + + /// @notice emit when successfuly create a new pool (calling iZiSwapFactory#newPool) + /// @param tokenX address of erc-20 tokenX + /// @param tokenY address of erc-20 tokenY + /// @param fee fee amount of swap (3000 means 0.3%) + /// @param pointDelta minimum number of distance between initialized or limitorder points + /// @param pool address of swap pool + event NewPool( + address indexed tokenX, + address indexed tokenY, + uint24 indexed fee, + uint24 pointDelta, + address pool + ); + + /// @notice module to support swap from tokenX to tokenY + /// @return swapX2YModule address + function swapX2YModule() external returns (address); + + /// @notice module to support swap from tokenY to tokenX + /// @return swapY2XModule address + function swapY2XModule() external returns (address); + + /// @notice module to support mint/burn/collect function of pool + /// @return liquidityModule address + function liquidityModule() external returns (address); + + /// @notice address of module for user to manage limit orders + /// @return limitOrderModule address + function limitOrderModule() external returns (address); + + /// @notice address of module for flash loan + /// @return flashModule address + function flashModule() external returns (address); + + /// @notice default fee rate from miner's fee gain + /// @return defaultFeeChargePercent default fee rate * 100 + function defaultFeeChargePercent() external returns (uint24); + + /// @notice Enables a fee amount with the given pointDelta + /// @dev Fee amounts may never be removed once enabled + /// @param fee fee amount (3000 means 0.3%) + /// @param pointDelta The spacing between points to be enforced for all pools created with the given fee amount + function enableFeeAmount(uint24 fee, uint24 pointDelta) external; + + /// @notice Create a new pool which not exists. + /// @param tokenX address of tokenX + /// @param tokenY address of tokenY + /// @param fee fee amount + /// @param currentPoint initial point (log 1.0001 of price) + /// @return address of newly created pool + function newPool( + address tokenX, + address tokenY, + uint24 fee, + int24 currentPoint + ) external returns (address); + + /// @notice Charge receiver of all pools. + /// @return address of charge receiver + function chargeReceiver() external view returns(address); + + /// @notice Get pool of (tokenX, tokenY, fee), address(0) for not exists. + /// @param tokenX address of tokenX + /// @param tokenY address of tokenY + /// @param fee fee amount + /// @return address of pool + function pool( + address tokenX, + address tokenY, + uint24 fee + ) external view returns(address); + + /// @notice Get point delta of a given fee amount. + /// @param fee fee amount + /// @return pointDelta the point delta + function fee2pointDelta(uint24 fee) external view returns (int24 pointDelta); + + /// @notice Change charge receiver, only owner of factory can call. + /// @param _chargeReceiver address of new receiver + function modifyChargeReceiver(address _chargeReceiver) external; + + /// @notice Change defaultFeeChargePercent + /// @param _defaultFeeChargePercent new charge percent + function modifyDefaultFeeChargePercent(uint24 _defaultFeeChargePercent) external; + + function deployPoolParams() external view returns( + address tokenX, + address tokenY, + uint24 fee, + int24 currentPoint, + int24 pointDelta, + uint24 feeChargePercent + ); + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapFlashCallback.sol b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapFlashCallback.sol new file mode 100644 index 000000000..b14101909 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapFlashCallback.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface IiZiSwapFlashCallback { + + /// @notice Call the msg.sender after sending tokens in IiZiSwapPool#flash. + /// @dev Must repay the tokens to the pool within one call. + /// @param feeX the fee amount in tokenX due to the pool by the end of the flash + /// @param feeY the fee amount in tokenY due to the pool by the end of the flash + /// @param data any data passed through by the caller + function flashCallback( + uint256 feeX, + uint256 feeY, + bytes calldata data + ) external; + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapPool.sol b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapPool.sol new file mode 100644 index 000000000..b90350188 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/core/interfaces/IiZiSwapPool.sol @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +interface IiZiSwapPool { + + /// @notice Emitted when miner successfully add liquidity (mint). + /// @param sender the address that minted the liquidity + /// @param owner the owner who will benefit from this liquidity + /// @param leftPoint left endpoint of the liquidity + /// @param rightPoint right endpoint of the liquidity + /// @param liquidity the amount of liquidity minted to the range [leftPoint, rightPoint) + /// @param amountX amount of tokenX deposit + /// @param amountY amount of tokenY deposit + event Mint( + address sender, + address indexed owner, + int24 indexed leftPoint, + int24 indexed rightPoint, + uint128 liquidity, + uint256 amountX, + uint256 amountY + ); + + /// @notice Emitted when miner successfully decrease liquidity (withdraw). + /// @param owner owner address of liquidity + /// @param leftPoint left endpoint of liquidity + /// @param rightPoint right endpoint of liquidity + /// @param liquidity amount of liquidity decreased + /// @param amountX amount of tokenX withdrawed + /// @param amountY amount of tokenY withdrawed + event Burn( + address indexed owner, + int24 indexed leftPoint, + int24 indexed rightPoint, + uint128 liquidity, + uint256 amountX, + uint256 amountY + ); + + /// @notice Emitted when fees and withdrawed liquidity are collected + /// @param owner The owner of the Liquidity + /// @param recipient recipient of those token + /// @param leftPoint The left point of the liquidity + /// @param rightPoint The right point of the liquidity + /// @param amountX The amount of tokenX (fees and withdrawed tokenX from liquidity) + /// @param amountY The amount of tokenY (fees and withdrawed tokenY from liquidity) + event CollectLiquidity( + address indexed owner, + address recipient, + int24 indexed leftPoint, + int24 indexed rightPoint, + uint256 amountX, + uint256 amountY + ); + + /// @notice Emitted when a trader successfully exchange. + /// @param tokenX tokenX of pool + /// @param tokenY tokenY of pool + /// @param fee fee amount of pool + /// @param sellXEarnY true for selling tokenX, false for buying tokenX + /// @param amountX amount of tokenX in this exchange + /// @param amountY amount of tokenY in this exchange + event Swap( + address indexed tokenX, + address indexed tokenY, + uint24 indexed fee, + bool sellXEarnY, + uint256 amountX, + uint256 amountY + ); + + /// @notice Emitted by the pool for any flashes of tokenX/tokenY. + /// @param sender the address that initiated the swap call, and that received the callback + /// @param recipient the address that received the tokens from flash + /// @param amountX the amount of tokenX that was flashed + /// @param amountY the amount of tokenY that was flashed + /// @param paidX the amount of tokenX paid for the flash, which can exceed the amountX plus the fee + /// @param paidY the amount of tokenY paid for the flash, which can exceed the amountY plus the fee + event Flash( + address indexed sender, + address indexed recipient, + uint256 amountX, + uint256 amountY, + uint256 paidX, + uint256 paidY + ); + + /// @notice Emitted when a seller successfully add a limit order. + /// @param owner owner of limit order + /// @param addAmount amount of token to sell the seller added + /// @param acquireAmount amount of earn-token acquired, if there exists some opposite order before + /// @param point point of limit order + /// @param claimSold claimed sold sell-token, if this owner has order with same direction on this point before + /// @param claimEarn claimed earned earn-token, if this owner has order with same direction on this point before + /// @param sellXEarnY direction of limit order, etc. sell tokenX or sell tokenY + event AddLimitOrder( + address indexed owner, + uint128 addAmount, + uint128 acquireAmount, + int24 indexed point, + uint128 claimSold, + uint128 claimEarn, + bool sellXEarnY + ); + + /// @notice Emitted when a seller successfully decrease a limit order. + /// @param owner owner of limit order + /// @param decreaseAmount amount of token to sell the seller decreased + /// @param point point of limit order + /// @param claimSold claimed sold sell-token + /// @param claimEarn claimed earned earn-token + /// @param sellXEarnY direction of limit order, etc. sell tokenX or sell tokenY + event DecLimitOrder( + address indexed owner, + uint128 decreaseAmount, + int24 indexed point, + uint128 claimSold, + uint128 claimEarn, + bool sellXEarnY + ); + + /// @notice Emitted when collect from a limit order + /// @param owner The owner of the Liquidity + /// @param recipient recipient of those token + /// @param point The point of the limit order + /// @param collectDec The amount of decreased sell token collected + /// @param collectEarn The amount of earn token collected + /// @param sellXEarnY direction of limit order, etc. sell tokenX or sell tokenY + event CollectLimitOrder( + address indexed owner, + address recipient, + int24 indexed point, + uint128 collectDec, + uint128 collectEarn, + bool sellXEarnY + ); + + /// @notice Returns the information about a liquidity by the liquidity's key. + /// @param key the liquidity's key is a hash of a preimage composed by the miner(owner), pointLeft and pointRight + /// @return liquidity the amount of liquidity, + /// @return lastFeeScaleX_128 fee growth of tokenX inside the range as of the last mint/burn/collect, + /// @return lastFeeScaleY_128 fee growth of tokenY inside the range as of the last mint/burn/collect, + /// @return tokenOwedX the computed amount of tokenX miner can collect as of the last mint/burn/collect, + /// @return tokenOwedY the computed amount of tokenY miner can collect as of the last mint/burn/collect + function liquidity(bytes32 key) + external + view + returns ( + uint128 liquidity, + uint256 lastFeeScaleX_128, + uint256 lastFeeScaleY_128, + uint256 tokenOwedX, + uint256 tokenOwedY + ); + + /// @notice Returns the information about a user's limit order (sell tokenY and earn tokenX). + /// @param key the limit order's key is a hash of a preimage composed by the seller, point + /// @return lastAccEarn total amount of tokenX earned by all users at this point as of the last add/dec/collect + /// @return sellingRemain amount of tokenY not selled in this limit order + /// @return sellingDec amount of tokenY decreased by seller from this limit order + /// @return earn amount of unlegacy earned tokenX in this limit order not assigned + /// @return legacyEarn amount of legacy earned tokenX in this limit order not assgined + /// @return earnAssign assigned amount of tokenX earned (both legacy and unlegacy) in this limit order + function userEarnX(bytes32 key) + external + view + returns ( + uint256 lastAccEarn, + uint128 sellingRemain, + uint128 sellingDec, + uint128 earn, + uint128 legacyEarn, + uint128 earnAssign + ); + + /// @notice Returns the information about a user's limit order (sell tokenX and earn tokenY). + /// @param key the limit order's key is a hash of a preimage composed by the seller, point + /// @return lastAccEarn total amount of tokenY earned by all users at this point as of the last add/dec/collect + /// @return sellingRemain amount of tokenX not selled in this limit order + /// @return sellingDec amount of tokenX decreased by seller from this limit order + /// @return earn amount of unlegacy earned tokenY in this limit order not assigned + /// @return legacyEarn amount of legacy earned tokenY in this limit order not assgined + /// @return earnAssign assigned amount of tokenY earned (both legacy and unlegacy) in this limit order + function userEarnY(bytes32 key) + external + view + returns ( + uint256 lastAccEarn, + uint128 sellingRemain, + uint128 sellingDec, + uint128 earn, + uint128 legacyEarn, + uint128 earnAssign + ); + + /// @notice Mark a given amount of tokenY in a limitorder(sellx and earn y) as assigned. + /// @param point point (log Price) of seller's limit order,be sure to be times of pointDelta + /// @param assignY max amount of tokenY to mark assigned + /// @param fromLegacy true for assigning earned token from legacyEarnY + /// @return actualAssignY actual amount of tokenY marked + function assignLimOrderEarnY( + int24 point, + uint128 assignY, + bool fromLegacy + ) external returns(uint128 actualAssignY); + + /// @notice Mark a given amount of tokenX in a limitorder(selly and earn x) as assigned. + /// @param point point (log Price) of seller's limit order,be sure to be times of pointDelta + /// @param assignX max amount of tokenX to mark assigned + /// @param fromLegacy true for assigning earned token from legacyEarnX + /// @return actualAssignX actual amount of tokenX marked + function assignLimOrderEarnX( + int24 point, + uint128 assignX, + bool fromLegacy + ) external returns(uint128 actualAssignX); + + /// @notice Decrease limitorder of selling X. + /// @param point point of seller's limit order, be sure to be times of pointDelta + /// @param deltaX max amount of tokenX seller wants to decrease + /// @return actualDeltaX actual amount of tokenX decreased + /// @return legacyAccEarn legacyAccEarnY of pointOrder at point when calling this interface + function decLimOrderWithX( + int24 point, + uint128 deltaX + ) external returns (uint128 actualDeltaX, uint256 legacyAccEarn); + + /// @notice Decrease limitorder of selling Y. + /// @param point point of seller's limit order, be sure to be times of pointDelta + /// @param deltaY max amount of tokenY seller wants to decrease + /// @return actualDeltaY actual amount of tokenY decreased + /// @return legacyAccEarn legacyAccEarnX of pointOrder at point when calling this interface + function decLimOrderWithY( + int24 point, + uint128 deltaY + ) external returns (uint128 actualDeltaY, uint256 legacyAccEarn); + + /// @notice Add a limit order (selling x) in the pool. + /// @param recipient owner of the limit order + /// @param point point of the order, be sure to be times of pointDelta + /// @param amountX amount of tokenX to sell + /// @param data any data that should be passed through to the callback + /// @return orderX actual added amount of tokenX + /// @return acquireY amount of tokenY acquired if there is a limit order to sell y before adding + function addLimOrderWithX( + address recipient, + int24 point, + uint128 amountX, + bytes calldata data + ) external returns (uint128 orderX, uint128 acquireY); + + /// @notice Add a limit order (selling y) in the pool. + /// @param recipient owner of the limit order + /// @param point point of the order, be sure to be times of pointDelta + /// @param amountY amount of tokenY to sell + /// @param data any data that should be passed through to the callback + /// @return orderY actual added amount of tokenY + /// @return acquireX amount of tokenX acquired if there exists a limit order to sell x before adding + function addLimOrderWithY( + address recipient, + int24 point, + uint128 amountY, + bytes calldata data + ) external returns (uint128 orderY, uint128 acquireX); + + /// @notice Collect earned or decreased token from limit order. + /// @param recipient address to benefit + /// @param point point of limit order, be sure to be times of pointDelta + /// @param collectDec max amount of decreased selling token to collect + /// @param collectEarn max amount of earned token to collect + /// @param isEarnY direction of this limit order, true for sell y, false for sell x + /// @return actualCollectDec actual amount of decresed selling token collected + /// @return actualCollectEarn actual amount of earned token collected + function collectLimOrder( + address recipient, int24 point, uint128 collectDec, uint128 collectEarn, bool isEarnY + ) external returns(uint128 actualCollectDec, uint128 actualCollectEarn); + + /// @notice Add liquidity to the pool. + /// @param recipient newly created liquidity will belong to this address + /// @param leftPt left endpoint of the liquidity, be sure to be times of pointDelta + /// @param rightPt right endpoint of the liquidity, be sure to be times of pointDelta + /// @param liquidDelta amount of liquidity to add + /// @param data any data that should be passed through to the callback + /// @return amountX The amount of tokenX that was paid for the liquidity. Matches the value in the callback + /// @return amountY The amount of tokenY that was paid for the liquidity. Matches the value in the callback + function mint( + address recipient, + int24 leftPt, + int24 rightPt, + uint128 liquidDelta, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Decrease a given amount of liquidity from msg.sender's liquidities. + /// @param leftPt left endpoint of the liquidity + /// @param rightPt right endpoint of the liquidity + /// @param liquidDelta amount of liquidity to burn + /// @return amountX The amount of tokenX should be refund after burn + /// @return amountY The amount of tokenY should be refund after burn + function burn( + int24 leftPt, + int24 rightPt, + uint128 liquidDelta + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Collect tokens (fee or refunded after burn) from a liquidity. + /// @param recipient the address which should receive the collected tokens + /// @param leftPt left endpoint of the liquidity + /// @param rightPt right endpoint of the liquidity + /// @param amountXLim max amount of tokenX the owner wants to collect + /// @param amountYLim max amount of tokenY the owner wants to collect + /// @return actualAmountX the amount tokenX collected + /// @return actualAmountY the amount tokenY collected + function collect( + address recipient, + int24 leftPt, + int24 rightPt, + uint256 amountXLim, + uint256 amountYLim + ) external returns (uint256 actualAmountX, uint256 actualAmountY); + + /// @notice Swap tokenY for tokenX, given max amount of tokenY user willing to pay. + /// @param recipient the address to receive tokenX + /// @param amount the max amount of tokenY user willing to pay + /// @param highPt the highest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX payed + /// @return amountY amount of tokenY acquired + function swapY2X( + address recipient, + uint128 amount, + int24 highPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Swap tokenY for tokenX, given amount of tokenX user desires. + /// @param recipient the address to receive tokenX + /// @param desireX the amount of tokenX user desires + /// @param highPt the highest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX payed + /// @return amountY amount of tokenY acquired + function swapY2XDesireX( + address recipient, + uint128 desireX, + int24 highPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Swap tokenX for tokenY, given max amount of tokenX user willing to pay. + /// @param recipient the address to receive tokenY + /// @param amount the max amount of tokenX user willing to pay + /// @param lowPt the lowest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX acquired + /// @return amountY amount of tokenY payed + function swapX2Y( + address recipient, + uint128 amount, + int24 lowPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Swap tokenX for tokenY, given amount of tokenY user desires. + /// @param recipient the address to receive tokenY + /// @param desireY the amount of tokenY user desires + /// @param lowPt the lowest point(price) of x/y during swap + /// @param data any data to be passed through to the callback + /// @return amountX amount of tokenX acquired + /// @return amountY amount of tokenY payed + function swapX2YDesireY( + address recipient, + uint128 desireY, + int24 lowPt, + bytes calldata data + ) external returns (uint256 amountX, uint256 amountY); + + /// @notice Returns sqrt(1.0001), in 96 bit fixpoint number. + function sqrtRate_96() external view returns(uint160); + + /// @notice State values of pool. + /// @return sqrtPrice_96 a 96 fixpoing number describe the sqrt value of current price(tokenX/tokenY) + /// @return currentPoint the current point of the pool, 1.0001 ^ currentPoint = price + /// @return observationCurrentIndex the index of the last oracle observation that was written, + /// @return observationQueueLen the current maximum number of observations stored in the pool, + /// @return observationNextQueueLen the next maximum number of observations, to be updated when the observation. + /// @return locked whether the pool is locked (only used for checking reentrance) + /// @return liquidity liquidity on the currentPoint (currX * sqrtPrice + currY / sqrtPrice) + /// @return liquidityX liquidity of tokenX + function state() + external view + returns( + uint160 sqrtPrice_96, + int24 currentPoint, + uint16 observationCurrentIndex, + uint16 observationQueueLen, + uint16 observationNextQueueLen, + bool locked, + uint128 liquidity, + uint128 liquidityX + ); + + /// @notice LimitOrder info on a given point. + /// @param point the given point + /// @return sellingX total amount of tokenX selling on the point + /// @return earnY total amount of unclaimed earned tokenY for unlegacy sellingX + /// @return accEarnY total amount of earned tokenY(via selling tokenX) by all users at this point as of the last swap + /// @return legacyAccEarnY latest recorded 'accEarnY' value when sellingX is clear (legacy) + /// @return legacyEarnY total amount of unclaimed earned tokenY for legacy (cleared during swap) sellingX + /// @return sellingY total amount of tokenYselling on the point + /// @return earnX total amount of unclaimed earned tokenX for unlegacy sellingY + /// @return legacyEarnX total amount of unclaimed earned tokenX for legacy (cleared during swap) sellingY + /// @return accEarnX total amount of earned tokenX(via selling tokenY) by all users at this point as of the last swap + /// @return legacyAccEarnX latest recorded 'accEarnX' value when sellingY is clear (legacy) + function limitOrderData(int24 point) + external view + returns( + uint128 sellingX, + uint128 earnY, + uint256 accEarnY, + uint256 legacyAccEarnY, + uint128 legacyEarnY, + uint128 sellingY, + uint128 earnX, + uint128 legacyEarnX, + uint256 accEarnX, + uint256 legacyAccEarnX + ); + + /// @notice Query infomation about a point whether has limit order or is an liquidity's endpoint. + /// @param point point to query + /// @return val endpoint for val&1>0 and has limit order for val&2 > 0 + function orderOrEndpoint(int24 point) external returns(int24 val); + + /// @notice Returns observation data about a specific index. + /// @param index the index of observation array + /// @return timestamp the timestamp of the observation, + /// @return accPoint the point multiplied by seconds elapsed for the life of the pool as of the observation timestamp, + /// @return init whether the observation has been initialized and the above values are safe to use + function observations(uint256 index) + external + view + returns ( + uint32 timestamp, + int56 accPoint, + bool init + ); + + /// @notice Point status in the pool. + /// @param point the point + /// @return liquidSum the total amount of liquidity that uses the point either as left endpoint or right endpoint + /// @return liquidDelta how much liquidity changes when the pool price crosses the point from left to right + /// @return accFeeXOut_128 the fee growth on the other side of the point from the current point in tokenX + /// @return accFeeYOut_128 the fee growth on the other side of the point from the current point in tokenY + /// @return isEndpt whether the point is an endpoint of a some miner's liquidity, true if liquidSum > 0 + function points(int24 point) + external + view + returns ( + uint128 liquidSum, + int128 liquidDelta, + uint256 accFeeXOut_128, + uint256 accFeeYOut_128, + bool isEndpt + ); + + /// @notice Returns 256 packed point (statusVal>0) boolean values. See PointBitmap for more information. + function pointBitmap(int16 wordPosition) external view returns (uint256); + + /// @notice Returns the integral value of point(time) and integral value of 1/liquidity(time) + /// at some target timestamps (block.timestamp - secondsAgo[i]) + /// @dev Reverts if target timestamp is early than oldest observation in the queue + /// @dev If you call this method with secondsAgos = [3600, 0]. the average point of this pool during recent hour is + /// (accPoints[1] - accPoints[0]) / 3600 + /// @param secondsAgos describe the target timestamp , targetTimestimp[i] = block.timestamp - secondsAgo[i] + /// @return accPoints integral value of point(time) from 0 to each target timestamp + function observe(uint32[] calldata secondsAgos) + external + view + returns (int56[] memory accPoints); + + /// @notice Expand max-length of observation queue. + /// @param newNextQueueLen new value of observationNextQueueLen, which should be greater than current observationNextQueueLen + function expandObservationQueue(uint16 newNextQueueLen) external; + + /// @notice Borrow tokenX and/or tokenY and pay it back within a block. + /// @dev The caller needs to implement a IiZiSwapPool#flashCallback callback function + /// @param recipient the address which will receive the tokenY and/or tokenX + /// @param amountX the amount of tokenX to borrow + /// @param amountY the amount of tokenY to borrow + /// @param data Any data to be passed through to the callback + function flash( + address recipient, + uint256 amountX, + uint256 amountY, + bytes calldata data + ) external; + + /// @notice Returns a snapshot infomation of Liquidity in [leftPoint, rightPoint). + /// @param leftPoint left endpoint of range, should be times of pointDelta + /// @param rightPoint right endpoint of range, should be times of pointDelta + /// @return deltaLiquidities an array of delta liquidity for points in the range + /// note 1. delta liquidity here is amount of liquidity changed when cross a point from left to right + /// note 2. deltaLiquidities only contains points which are times of pointDelta + /// note 3. this function may cost a ENORMOUS amount of gas, be careful to call + function liquiditySnapshot(int24 leftPoint, int24 rightPoint) external view returns(int128[] memory deltaLiquidities); + + struct LimitOrderStruct { + uint128 sellingX; + uint128 earnY; + uint256 accEarnY; + uint128 sellingY; + uint128 earnX; + uint256 accEarnX; + } + + /// @notice Returns a snapshot infomation of Limit Order in [leftPoint, rightPoint). + /// @param leftPoint left endpoint of range, should be times of pointDelta + /// @param rightPoint right endpoint of range, should be times of pointDelta + /// @return limitOrders an array of Limit Orders for points in the range + /// note 1. this function may cost a HUGE amount of gas, be careful to call + function limitOrderSnapshot(int24 leftPoint, int24 rightPoint) external view returns(LimitOrderStruct[] memory limitOrders); + + /// @notice Amount of charged fee on tokenX. + function totalFeeXCharged() external view returns(uint256); + + /// @notice Amount of charged fee on tokenY. + function totalFeeYCharged() external view returns(uint256); + + /// @notice Percent to charge from miner's fee. + function feeChargePercent() external view returns(uint24); + + /// @notice Collect charged fee, only factory's chargeReceiver can call. + function collectFeeCharged() external; + + /// @notice modify 'feeChargePercent', only owner has authority. + /// @param newFeeChargePercent new value of feeChargePercent, a nature number range in [0, 100], + function modifyFeeChargePercent(uint24 newFeeChargePercent) external; + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/IBase.sol b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/IBase.sol new file mode 100644 index 000000000..22b89c949 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/IBase.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface IBase { + function refundETH() external payable; +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/ILiquidityManager.sol b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/ILiquidityManager.sol new file mode 100644 index 000000000..e71cf90b3 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/ILiquidityManager.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + + +/// @title Interface for LiquidityManager +interface ILiquidityManager { + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) external view returns (address); + + /// parameters when calling mint, grouped together to avoid stake too deep + struct MintParam { + // miner address + address miner; + // tokenX of swap pool + address tokenX; + // tokenY of swap pool + address tokenY; + // fee amount of swap pool + uint24 fee; + // left point of added liquidity + int24 pl; + // right point of added liquidity + int24 pr; + // amount limit of tokenX miner willing to deposit + uint128 xLim; + // amount limit tokenY miner willing to deposit + uint128 yLim; + // minimum amount of tokenX miner willing to deposit + uint128 amountXMin; + // minimum amount of tokenY miner willing to deposit + uint128 amountYMin; + + uint256 deadline; + } + /// @notice Add a new liquidity and generate a nft. + /// @param mintParam params, see MintParam for more + /// @return lid id of nft + /// @return liquidity amount of liquidity added + /// @return amountX amount of tokenX deposited + /// @return amountY amount of tokenY depsoited + function mint(MintParam calldata mintParam) external payable returns( + uint256 lid, + uint128 liquidity, + uint256 amountX, + uint256 amountY + ); + + /// parameters when calling addLiquidity, grouped together to avoid stake too deep + struct AddLiquidityParam { + // id of nft + uint256 lid; + // amount limit of tokenX user willing to deposit + uint128 xLim; + // amount limit of tokenY user willing to deposit + uint128 yLim; + // min amount of tokenX user willing to deposit + uint128 amountXMin; + // min amount of tokenY user willing to deposit + uint128 amountYMin; + + uint256 deadline; + } + + /// @notice Add liquidity to a existing nft. + /// @param addLiquidityParam see AddLiquidityParam for more + /// @return liquidityDelta amount of added liquidity + /// @return amountX amount of tokenX deposited + /// @return amountY amonut of tokenY deposited + function addLiquidity( + AddLiquidityParam calldata addLiquidityParam + ) external payable returns ( + uint128 liquidityDelta, + uint256 amountX, + uint256 amountY + ); + + /// @notice Decrease liquidity from a nft. + /// @param lid id of nft + /// @param liquidDelta amount of liqudity to decrease + /// @param amountXMin min amount of tokenX user want to withdraw + /// @param amountYMin min amount of tokenY user want to withdraw + /// @param deadline deadline timestamp of transaction + /// @return amountX amount of tokenX refund to user + /// @return amountY amount of tokenY refund to user + function decLiquidity( + uint256 lid, + uint128 liquidDelta, + uint256 amountXMin, + uint256 amountYMin, + uint256 deadline + ) external returns ( + uint256 amountX, + uint256 amountY + ); + + function liquidities(uint256 lid) external view returns( + int24 leftPt, + // right point of liquidity-token, the range is [leftPt, rightPt) + int24 rightPt, + // amount of liquidity on each point in [leftPt, rightPt) + uint128 liquidity, + // a 128-fixpoint number, as integral of { fee(pt, t)/L(pt, t) }. + // here fee(pt, t) denotes fee generated on point pt at time t + // L(pt, t) denotes liquidity on point pt at time t + // pt varies in [leftPt, rightPt) + // t moves from pool created until miner last modify this liquidity-token (mint/addLiquidity/decreaseLiquidity/create) + uint256 lastFeeScaleX_128, + uint256 lastFeeScaleY_128, + // remained tokenX miner can collect, including fee and withdrawed token + uint256 remainTokenX, + uint256 remainTokenY, + // id of pool in which this liquidity is added + uint128 poolId + ); + + function poolMetas(uint128 poolId) external view returns( + // tokenX of pool + address tokenX, + // tokenY of pool + address tokenY, + // fee amount of pool + uint24 fee + ); + /// @notice Collect fee gained of token withdrawed from nft. + /// @param recipient address to receive token + /// @param lid id of nft + /// @param amountXLim amount limit of tokenX to collect + /// @param amountYLim amount limit of tokenY to collect + /// @return amountX amount of tokenX actually collect + /// @return amountY amount of tokenY actually collect + function collect( + address recipient, + uint256 lid, + uint128 amountXLim, + uint128 amountYLim + ) external payable returns ( + uint256 amountX, + uint256 amountY + ); + + function burn(uint256 lid) external returns (bool success); +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/ISwap.sol b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/ISwap.sol new file mode 100644 index 000000000..a6c755873 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/ISwap.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + + +/// @title Interface for LiquidityManager +interface ISwap { + + struct SwapAmountParams { + bytes path; + address recipient; + uint128 amount; + uint256 minAcquired; + + uint256 deadline; + } + /// @notice Swap given amount of input token, usually used in multi-hop case. + function swapAmount(SwapAmountParams calldata params) + external + payable + returns (uint256 cost, uint256 acquire); +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/IWrapToken.sol b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/IWrapToken.sol new file mode 100644 index 000000000..4852bdaee --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/interfaces/IWrapToken.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Interface for WrapToken +interface IWrapToken is IERC20 { + + function originToken() external view returns(address); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {depositFrom}. This is + * zero by default. + * + * This value changes when {depositApprove} or {depositFrom} are called. + */ + function depositAllowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards + */ + function depositApprove(address spender, uint256 amount) external; + function depositFrom(address from, address to, uint256 amount) external returns(uint256 actualAmount); + function withdraw(address to, uint256 amount) external returns(uint256 actualAmount); +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/AmountMath.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/AmountMath.sol new file mode 100644 index 000000000..1d9949b22 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/AmountMath.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./MulDivMath.sol"; +import "./TwoPower.sol"; +import "./LogPowMath.sol"; + +library AmountMath { + + function getAmountYUnitLiquidity_96( + uint160 sqrtPriceL_96, + uint160 sqrtPriceR_96, + uint160 sqrtRate_96 + ) internal pure returns (uint256 amount_96) { + uint160 numerator = sqrtPriceR_96 - sqrtPriceL_96; + uint160 denominator = sqrtRate_96 - uint160(TwoPower.pow96); + amount_96 = MulDivMath.mulDivCeil(TwoPower.pow96, numerator, denominator); + } + + function getAmountXUnitLiquidity_96( + int24 leftPt, + int24 rightPt, + uint160 sqrtPriceR_96, + uint160 sqrtRate_96 + ) internal pure returns (uint256 amount_96) { + // rightPt - (leftPt - 1), pc = leftPt - 1 + uint160 sqrtPricePrPc_96 = LogPowMath.getSqrtPrice(rightPt - leftPt + 1); + uint160 sqrtPricePrPd_96 = LogPowMath.getSqrtPrice(rightPt + 1); + + uint160 numerator = sqrtPricePrPc_96 - sqrtRate_96; + uint160 denominator = sqrtPricePrPd_96 - sqrtPriceR_96; + amount_96 = MulDivMath.mulDivCeil(TwoPower.pow96, numerator, denominator); + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/BytesLib.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/BytesLib.sol new file mode 100644 index 000000000..b18996b0e --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/BytesLib.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity ^0.8.4; + +library BytesLib { + + function slice( + bytes memory _bytes, + uint256 _start, + uint256 _length + ) internal pure returns (bytes memory) { + require(_length + 31 >= _length, 'slice_overflow'); + require(_start + _length >= _start, 'slice_overflow'); + require(_bytes.length >= _start + _length, 'slice_outOfBounds'); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_start + 20 >= _start, 'toAddress_overflow'); + require(_bytes.length >= _start + 20, 'toAddress_outOfBounds'); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { + require(_start + 3 >= _start, 'toUint24_overflow'); + require(_bytes.length >= _start + 3, 'toUint24_outOfBounds'); + uint24 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x3), _start)) + } + + return tempUint; + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/Converter.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/Converter.sol new file mode 100644 index 000000000..deeb56adf --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/Converter.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library Converter { + + function toUint128(uint256 a) internal pure returns (uint128 b){ + b = uint128(a); + require(a == b, 'C128'); + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LimOrder.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LimOrder.sol new file mode 100644 index 000000000..e585b537f --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LimOrder.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.4; + +// infomation of a limit order +struct LimOrder { + // total amount of earned token by all users at this point + // with same direction (sell x or sell y) as of the last update(add/dec) + uint256 lastAccEarn; + // initial amount of token on sale + uint128 amount; + // remaing amount of token on sale + uint128 sellingRemain; + // accumulated decreased token + uint128 accSellingDec; + // uncollected decreased token + uint128 sellingDec; + // uncollected earned token + uint128 earn; + // id of pool in which this liquidity is added + uint128 poolId; + // block.timestamp when add a limit order + uint128 timestamp; + // point (price) of limit order + int24 pt; + // direction of limit order (sellx or sell y) + bool sellXEarnY; + // active or not + bool active; +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LimOrderCircularQueue.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LimOrderCircularQueue.sol new file mode 100644 index 000000000..90977740b --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LimOrderCircularQueue.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.4; + +import "./LimOrder.sol"; + +library LimOrderCircularQueue { + + struct Queue { + // start, start+1, ..., MAX_LENGTH-1, 0, 1, ..., start-1 + uint256 start; + LimOrder[] limOrders; + } + + function add(Queue storage queue, LimOrder memory limOrder, uint256 capacity) internal { + if (queue.limOrders.length < capacity) { + queue.limOrders.push(limOrder); + } else { + queue.limOrders[queue.start] = limOrder; + queue.start = (queue.start + 1) % queue.limOrders.length; + } + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LogPowMath.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LogPowMath.sol new file mode 100644 index 000000000..c6c2d4364 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/LogPowMath.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +library LogPowMath { + + int24 internal constant MIN_POINT = -887272; + + int24 internal constant MAX_POINT = -MIN_POINT; + + + uint160 internal constant MIN_SQRT_PRICE = 4295128739; + + uint160 internal constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342; + + /// @notice sqrt(1.0001^point) in form oy 96-bit fix point num + function getSqrtPrice(int24 point) internal pure returns (uint160 sqrtPrice_96) { + uint256 absIdx = point < 0 ? uint256(-int256(point)) : uint256(int256(point)); + require(absIdx <= uint256(int256(MAX_POINT)), 'T'); + + uint256 value = absIdx & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000; + if (absIdx & 0x2 != 0) value = (value * 0xfff97272373d413259a46990580e213a) >> 128; + if (absIdx & 0x4 != 0) value = (value * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; + if (absIdx & 0x8 != 0) value = (value * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; + if (absIdx & 0x10 != 0) value = (value * 0xffcb9843d60f6159c9db58835c926644) >> 128; + if (absIdx & 0x20 != 0) value = (value * 0xff973b41fa98c081472e6896dfb254c0) >> 128; + if (absIdx & 0x40 != 0) value = (value * 0xff2ea16466c96a3843ec78b326b52861) >> 128; + if (absIdx & 0x80 != 0) value = (value * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; + if (absIdx & 0x100 != 0) value = (value * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; + if (absIdx & 0x200 != 0) value = (value * 0xf987a7253ac413176f2b074cf7815e54) >> 128; + if (absIdx & 0x400 != 0) value = (value * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; + if (absIdx & 0x800 != 0) value = (value * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; + if (absIdx & 0x1000 != 0) value = (value * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; + if (absIdx & 0x2000 != 0) value = (value * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; + if (absIdx & 0x4000 != 0) value = (value * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; + if (absIdx & 0x8000 != 0) value = (value * 0x31be135f97d08fd981231505542fcfa6) >> 128; + if (absIdx & 0x10000 != 0) value = (value * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; + if (absIdx & 0x20000 != 0) value = (value * 0x5d6af8dedb81196699c329225ee604) >> 128; + if (absIdx & 0x40000 != 0) value = (value * 0x2216e584f5fa1ea926041bedfe98) >> 128; + if (absIdx & 0x80000 != 0) value = (value * 0x48a170391f7dc42444e8fa2) >> 128; + + if (point > 0) value = type(uint256).max / value; + + sqrtPrice_96 = uint160((value >> 32) + (value % (1 << 32) == 0 ? 0 : 1)); + } + + // floor(log1.0001(sqrtPrice_96)) + function getLogSqrtPriceFloor(uint160 sqrtPrice_96) internal pure returns (int24 logValue) { + // second inequality must be < because the price can nevex reach the price at the max tick + require(sqrtPrice_96 >= MIN_SQRT_PRICE && sqrtPrice_96 < MAX_SQRT_PRICE, 'R'); + uint256 sqrtPrice_128 = uint256(sqrtPrice_96) << 32; + + uint256 x = sqrtPrice_128; + uint256 m = 0; + + assembly { + let y := shl(7, gt(x, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(6, gt(x, 0xFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(5, gt(x, 0xFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(4, gt(x, 0xFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(3, gt(x, 0xFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(2, gt(x, 0xF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(1, gt(x, 0x3)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := gt(x, 0x1) + m := or(m, y) + } + + if (m >= 128) x = sqrtPrice_128 >> (m - 127); + else x = sqrtPrice_128 << (127 - m); + + int256 l2 = (int256(m) - 128) << 64; + + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(63, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(62, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(61, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(60, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(59, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(58, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(57, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(56, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(55, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(54, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(53, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(52, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(51, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(50, y)) + } + + int256 ls10001 = l2 * 255738958999603826347141; + + int24 logFloor = int24((ls10001 - 3402992956809132418596140100660247210) >> 128); + int24 logUpper = int24((ls10001 + 291339464771989622907027621153398088495) >> 128); + + logValue = logFloor == logUpper ? logFloor : getSqrtPrice(logUpper) <= sqrtPrice_96 ? logUpper : logFloor; + } + + function getLogSqrtPriceFU(uint160 sqrtPrice_96) internal pure returns (int24 logFloor, int24 logUpper) { + // second inequality must be < because the price can nevex reach the price at the max tick + require(sqrtPrice_96 >= MIN_SQRT_PRICE && sqrtPrice_96 < MAX_SQRT_PRICE, 'R'); + uint256 sqrtPrice_128 = uint256(sqrtPrice_96) << 32; + + uint256 x = sqrtPrice_128; + uint256 m = 0; + + assembly { + let y := shl(7, gt(x, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(6, gt(x, 0xFFFFFFFFFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(5, gt(x, 0xFFFFFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(4, gt(x, 0xFFFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(3, gt(x, 0xFF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(2, gt(x, 0xF)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := shl(1, gt(x, 0x3)) + m := or(m, y) + x := shr(y, x) + } + assembly { + let y := gt(x, 0x1) + m := or(m, y) + } + + if (m >= 128) x = sqrtPrice_128 >> (m - 127); + else x = sqrtPrice_128 << (127 - m); + + int256 l2 = (int256(m) - 128) << 64; + + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(63, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(62, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(61, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(60, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(59, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(58, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(57, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(56, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(55, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(54, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(53, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(52, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(51, y)) + x := shr(y, x) + } + assembly { + x := shr(127, mul(x, x)) + let y := shr(128, x) + l2 := or(l2, shl(50, y)) + } + + int256 ls10001 = l2 * 255738958999603826347141; + + logFloor = int24((ls10001 - 3402992956809132418596140100660247210) >> 128); + logUpper = int24((ls10001 + 291339464771989622907027621153398088495) >> 128); + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/MintMath.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/MintMath.sol new file mode 100644 index 000000000..5ae54baea --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/MintMath.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +import "./TwoPower.sol"; +import "./AmountMath.sol"; + +library MintMath { + + struct MintMathParam { + int24 pl; + int24 pr; + uint128 xLim; + uint128 yLim; + } + + /// @dev [pl, pr) + function _computeDepositXYPerUnit( + int24 pl, + int24 pr, + int24 pc, + uint160 sqrtPrice_96, + uint160 sqrtRate_96 + ) private pure returns (uint256 x, uint256 y) { + x = 0; + y = 0; + uint160 sqrtPriceR_96 = LogPowMath.getSqrtPrice(pr); + if (pl < pc) { + uint160 sqrtPriceL_96 = LogPowMath.getSqrtPrice(pl); + if (pr < pc) { + y += AmountMath.getAmountYUnitLiquidity_96(sqrtPriceL_96, sqrtPriceR_96, sqrtRate_96); + } else { + y += AmountMath.getAmountYUnitLiquidity_96(sqrtPriceL_96, sqrtPrice_96, sqrtRate_96); + } + } + if (pr > pc) { + // we need compute XR + int24 xrLeft = (pl > pc) ? pl : pc + 1; + x = AmountMath.getAmountXUnitLiquidity_96( + xrLeft, + pr, + sqrtPriceR_96, + sqrtRate_96 + ); + } + if (pl <= pc && pr > pc) { + // we nned compute yc at point of current price + y += sqrtPrice_96; + } + } + + function computeLiquidity( + MintMathParam memory mp, int24 currPt, uint160 sqrtPrice_96, uint160 sqrtRate_96 + ) internal pure returns(uint128 liquidity) { + liquidity = type(uint128).max / 2; + (uint256 x, uint256 y) = _computeDepositXYPerUnit(mp.pl, mp.pr, currPt, sqrtPrice_96, sqrtRate_96); + if (x > 0) { + uint256 xl = uint256(mp.xLim) * TwoPower.pow96 / x; + if (liquidity > xl) { + liquidity = uint128(xl); + } + } + if (y > 0) { + // we take yLim - 1, because in the core, the amountY to deposit is + // calculated by range [left, pc) and point at pc respectively + uint256 yl = uint256(mp.yLim - 1) * TwoPower.pow96 / y; + if (liquidity > yl) { + liquidity = uint128(yl); + } + } + } + +} \ No newline at end of file diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/MulDivMath.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/MulDivMath.sol new file mode 100644 index 000000000..271e75fa1 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/MulDivMath.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.4; + +library MulDivMath { + + // compute res = floor(a * b / c), assuming res < 2^256 + function mulDivFloor( + uint256 a, + uint256 b, + uint256 c + ) internal pure returns (uint256 res) { + + // let prodMod2_256 = a * b % 2^256 + uint256 prodMod2_256; + // let prodDiv2_256 = a * b / 2^256 + uint256 prodDiv2_256; + assembly { + let prodModM1 := mulmod(a, b, not(0)) + prodMod2_256 := mul(a, b) + prodDiv2_256 := sub(sub(prodModM1, prodMod2_256), lt(prodModM1, prodMod2_256)) + } + + if (prodDiv2_256 == 0) { + require(c > 0); + assembly { + res := div(prodMod2_256, c) + } + return res; + } + + // we should ensure that a * b /c < 2^256 before calling + require(c > prodDiv2_256); + + uint256 resMod; + assembly { + resMod := mulmod(a, b, c) + // a * b - resMod + prodDiv2_256 := sub(prodDiv2_256, gt(resMod, prodMod2_256)) + prodMod2_256 := sub(prodMod2_256, resMod) + + // compute lowbit of c + let lowbit := not(c) + lowbit := add(lowbit, 1) + lowbit := and(lowbit, c) + + // c / lowbit + c := div(c, lowbit) + // a * b / lowbit + prodMod2_256 := div(prodMod2_256, lowbit) + lowbit := add(div(sub(0, lowbit), lowbit), 1) + prodDiv2_256 := mul(prodDiv2_256, lowbit) + prodMod2_256 := or(prodMod2_256, prodDiv2_256) + + // get inv of c + // cInv * c = 1 (mod 2^4) + let cInv := xor(mul(3, c), 2) + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^8 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^16 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^32 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^64 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^128 + cInv := mul(cInv, sub(2, mul(c, cInv))) // shift to 2^256 + + // a * b / c = prodMod2_256 * cInv (mod 2^256) + res := mul(prodMod2_256, cInv) + } + } + + // compute res = ceil(a * b / c), assuming res < 2^256 + function mulDivCeil( + uint256 a, + uint256 b, + uint256 c + ) internal pure returns (uint256 res) { + res = mulDivFloor(a, b, c); + if (mulmod(a, b, c) > 0) { + require(res < type(uint256).max); + res++; + } + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/Path.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/Path.sol new file mode 100644 index 000000000..5af063ca6 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/Path.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.4; + +import "./BytesLib.sol"; + +/// @title Functions for manipulating path data for multihop swaps +library Path { + + using BytesLib for bytes; + + /// @dev The length of the bytes encoded address + uint256 private constant ADDR_SIZE = 20; + /// @dev The length of the bytes encoded fee + uint256 private constant FEE_SIZE = 3; + + /// @dev The offset of a single token address and pool fee + uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE; + /// @dev The offset of an encoded pool key + uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET; + + /// @notice Returns true iff the path contains two or more pools + /// @param path The encoded swap path + /// @return True if path contains two or more pools, otherwise false + function hasMultiplePools(bytes memory path) internal pure returns (bool) { + return path.length >= MULTIPLE_POOLS_MIN_LENGTH; + } + + /// @notice Returns the number of pools in the path + /// @param path The encoded swap path + /// @return The number of pools in the path + function numPools(bytes memory path) internal pure returns (uint256) { + // Ignore the first token address. From then on every fee and token offset indicates a pool. + return ((path.length - ADDR_SIZE) / NEXT_OFFSET); + } + + /// @notice Decodes the first pool in path + /// @param path The bytes encoded swap path + /// @return tokenA The first token of the given pool + /// @return tokenB The second token of the given pool + /// @return fee The fee level of the pool + function decodeFirstPool(bytes memory path) + internal + pure + returns ( + address tokenA, + address tokenB, + uint24 fee + ) + { + tokenA = path.toAddress(0); + fee = path.toUint24(ADDR_SIZE); + tokenB = path.toAddress(NEXT_OFFSET); + } + + /// @notice Gets the segment corresponding to the first pool in the path + /// @param path The bytes encoded swap path + /// @return The segment containing all data necessary to target the first pool in the path + function getFirstPool(bytes memory path) internal pure returns (bytes memory) { + return path.slice(0, POP_OFFSET); + } + + /// @notice Skips a token + fee element from the buffer and returns the remainder + /// @param path The swap path + /// @return The remaining token + fee elements in the path + function skipToken(bytes memory path) internal pure returns (bytes memory) { + return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); + } + +} diff --git a/contracts/dependencies/izumi/izumi-swap-periphery/libraries/TwoPower.sol b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/TwoPower.sol new file mode 100644 index 000000000..219f11987 --- /dev/null +++ b/contracts/dependencies/izumi/izumi-swap-periphery/libraries/TwoPower.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.4; + +/// @title FixedPoint128 +/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) +library TwoPower { + + uint256 internal constant pow96 = 0x1000000000000000000000000; + uint256 internal constant pow128 = 0x100000000000000000000000000000000; + uint8 internal constant RESOLUTION = 96; + +} \ No newline at end of file diff --git a/contracts/dependencies/uniswapv2/UniswapV2ERC20.sol b/contracts/dependencies/uniswapv2/UniswapV2ERC20.sol new file mode 100644 index 000000000..3cbe6baaf --- /dev/null +++ b/contracts/dependencies/uniswapv2/UniswapV2ERC20.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import './interfaces/IUniswapV2ERC20.sol'; +import './libraries/SafeMath.sol'; + +contract UniswapV2ERC20 is IUniswapV2ERC20 { + using SafeMath for uint; + + string public constant name = 'Uniswap V2'; + string public constant symbol = 'UNI-V2'; + uint8 public constant decimals = 18; + uint public totalSupply; + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + mapping(address => uint) public nonces; + + constructor() { + uint chainId; + assembly { + chainId := chainid() + } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + keccak256(bytes(name)), + keccak256(bytes('1')), + chainId, + address(this) + ) + ); + } + + function _mint(address to, uint value) internal { + totalSupply = totalSupply.add(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(address(0), to, value); + } + + function _burn(address from, uint value) internal { + balanceOf[from] = balanceOf[from].sub(value); + totalSupply = totalSupply.sub(value); + emit Transfer(from, address(0), value); + } + + function _approve(address owner, address spender, uint value) private { + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } + + function _transfer(address from, address to, uint value) private { + balanceOf[from] = balanceOf[from].sub(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(from, to, value); + } + + function approve(address spender, uint value) external returns (bool) { + _approve(msg.sender, spender, value); + return true; + } + + function transfer(address to, uint value) external returns (bool) { + _transfer(msg.sender, to, value); + return true; + } + + function transferFrom(address from, address to, uint value) external returns (bool) { + if (allowance[from][msg.sender] != type(uint256).max) { + allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); + } + _transfer(from, to, value); + return true; + } + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { + require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); + bytes32 digest = keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); + _approve(owner, spender, value); + } +} diff --git a/contracts/dependencies/uniswapv2/UniswapV2Factory.sol b/contracts/dependencies/uniswapv2/UniswapV2Factory.sol new file mode 100644 index 000000000..fa4f8b39b --- /dev/null +++ b/contracts/dependencies/uniswapv2/UniswapV2Factory.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import './interfaces/IUniswapV2Factory.sol'; +import './UniswapV2Pair.sol'; + +contract UniswapV2Factory is IUniswapV2Factory { + address public feeTo; + address public feeToSetter; + + mapping(address => mapping(address => address)) public getPair; + address[] public allPairs; + + constructor(address _feeToSetter) { + feeToSetter = _feeToSetter; + } + + function allPairsLength() external view returns (uint) { + return allPairs.length; + } + + function createPair(address tokenA, address tokenB) external returns (address pair) { + require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); + require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient + bytes memory bytecode = type(UniswapV2Pair).creationCode; + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + assembly { + pair := create2(0, add(bytecode, 32), mload(bytecode), salt) + } + IUniswapV2Pair(pair).initialize(token0, token1); + getPair[token0][token1] = pair; + getPair[token1][token0] = pair; // populate mapping in the reverse direction + allPairs.push(pair); + emit PairCreated(token0, token1, pair, allPairs.length); + } + + function setFeeTo(address _feeTo) external { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeTo = _feeTo; + } + + function setFeeToSetter(address _feeToSetter) external { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeToSetter = _feeToSetter; + } +} diff --git a/contracts/dependencies/uniswapv2/UniswapV2Pair.sol b/contracts/dependencies/uniswapv2/UniswapV2Pair.sol new file mode 100644 index 000000000..d459216cc --- /dev/null +++ b/contracts/dependencies/uniswapv2/UniswapV2Pair.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import './interfaces/IUniswapV2Pair.sol'; +import './UniswapV2ERC20.sol'; +import './libraries/Math.sol'; +import './libraries/UQ112x112.sol'; +import '../openzeppelin/contracts/IERC20.sol'; +import './interfaces/IUniswapV2Factory.sol'; +import './interfaces/IUniswapV2Callee.sol'; + +contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { + using SafeMath for uint; + using UQ112x112 for uint224; + + uint public constant MINIMUM_LIQUIDITY = 10**3; + bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); + + address public factory; + address public token0; + address public token1; + + uint112 private reserve0; // uses single storage slot, accessible via getReserves + uint112 private reserve1; // uses single storage slot, accessible via getReserves + uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves + + uint public price0CumulativeLast; + uint public price1CumulativeLast; + uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event + + uint private unlocked = 1; + modifier lock() { + require(unlocked == 1, 'UniswapV2: LOCKED'); + unlocked = 0; + _; + unlocked = 1; + } + + function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { + _reserve0 = reserve0; + _reserve1 = reserve1; + _blockTimestampLast = blockTimestampLast; + } + + function _safeTransfer(address token, address to, uint value) private { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED'); + } + + constructor() { + factory = msg.sender; + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } + + // update reserves and, on the first call per block, price accumulators + function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { + require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, 'UniswapV2: OVERFLOW'); + uint32 blockTimestamp = uint32(block.timestamp % 2**32); + uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired + if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { + // * never overflows, and + overflow is desired + price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; + price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; + } + reserve0 = uint112(balance0); + reserve1 = uint112(balance1); + blockTimestampLast = blockTimestamp; + emit Sync(reserve0, reserve1); + } + + // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) + function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { + address feeTo = IUniswapV2Factory(factory).feeTo(); + feeOn = feeTo != address(0); + uint _kLast = kLast; // gas savings + if (feeOn) { + if (_kLast != 0) { + uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); + uint rootKLast = Math.sqrt(_kLast); + if (rootK > rootKLast) { + uint numerator = totalSupply.mul(rootK.sub(rootKLast)); + uint denominator = rootK.mul(5).add(rootKLast); + uint liquidity = numerator / denominator; + if (liquidity > 0) _mint(feeTo, liquidity); + } + } + } else if (_kLast != 0) { + kLast = 0; + } + } + + // this low-level function should be called from a contract which performs important safety checks + function mint(address to) external lock returns (uint liquidity) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + uint balance0 = IERC20(token0).balanceOf(address(this)); + uint balance1 = IERC20(token1).balanceOf(address(this)); + uint amount0 = balance0.sub(_reserve0); + uint amount1 = balance1.sub(_reserve1); + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + if (_totalSupply == 0) { + liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); + _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens + } else { + liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); + } + require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); + _mint(to, liquidity); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date + emit Mint(msg.sender, amount0, amount1); + } + + // this low-level function should be called from a contract which performs important safety checks + function burn(address to) external lock returns (uint amount0, uint amount1) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + uint balance0 = IERC20(_token0).balanceOf(address(this)); + uint balance1 = IERC20(_token1).balanceOf(address(this)); + uint liquidity = balanceOf[address(this)]; + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _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, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); + _burn(address(this), liquidity); + _safeTransfer(_token0, to, amount0); + _safeTransfer(_token1, to, amount1); + balance0 = IERC20(_token0).balanceOf(address(this)); + balance1 = IERC20(_token1).balanceOf(address(this)); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date + emit Burn(msg.sender, amount0, amount1, to); + } + + // this low-level function should be called from a contract which performs important safety checks + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { + require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); + + uint balance0; + uint balance1; + { // scope for _token{0,1}, avoids stack too deep errors + address _token0 = token0; + address _token1 = token1; + require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); + if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens + if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); + balance0 = IERC20(_token0).balanceOf(address(this)); + balance1 = IERC20(_token1).balanceOf(address(this)); + } + uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; + uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; + require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); + { // scope for reserve{0,1}Adjusted, avoids stack too deep errors + uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); + uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); + require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); + } + + _update(balance0, balance1, _reserve0, _reserve1); + emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); + } + + // force balances to match reserves + function skim(address to) external lock { + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0)); + _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1)); + } + + // force reserves to match balances + function sync() external lock { + _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1); + } +} diff --git a/contracts/dependencies/uniswapv2/UniswapV2Router02.sol b/contracts/dependencies/uniswapv2/UniswapV2Router02.sol new file mode 100644 index 000000000..556c78d22 --- /dev/null +++ b/contracts/dependencies/uniswapv2/UniswapV2Router02.sol @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import './interfaces/IUniswapV2Factory.sol'; +import './libraries/TransferHelper.sol'; + +import './interfaces/IUniswapV2Router02.sol'; +import './libraries/UniswapV2Library.sol'; +import './interfaces/IUniswapV2Pair.sol'; +import './libraries/SafeMath.sol'; +import '../openzeppelin/contracts/IERC20.sol'; +import './interfaces/IWETH.sol'; + +contract UniswapV2Router02 is IUniswapV2Router02 { + using SafeMath for uint; + + address public immutable override factory; + address public immutable override WETH; + + modifier ensure(uint deadline) { + require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); + _; + } + + constructor(address _factory, address _WETH) { + factory = _factory; + WETH = _WETH; + } + + receive() external payable { + assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract + } + + // **** ADD LIQUIDITY **** + function _addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) internal virtual returns (uint amountA, uint amountB) { + // create the pair if it doesn't exist yet + if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { + IUniswapV2Factory(factory).createPair(tokenA, tokenB); + } + (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); + if (reserveA == 0 && reserveB == 0) { + (amountA, amountB) = (amountADesired, amountBDesired); + } else { + uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); + if (amountBOptimal <= amountBDesired) { + require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + (amountA, amountB) = (amountADesired, amountBOptimal); + } else { + uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); + assert(amountAOptimal <= amountADesired); + require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + (amountA, amountB) = (amountAOptimal, amountBDesired); + } + } + } + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { + (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); + TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); + liquidity = IUniswapV2Pair(pair).mint(to); + } + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { + (amountToken, amountETH) = _addLiquidity( + token, + WETH, + amountTokenDesired, + msg.value, + amountTokenMin, + amountETHMin + ); + address pair = UniswapV2Library.pairFor(factory, token, WETH); + TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); + IWETH(WETH).deposit{value: amountETH}(); + assert(IWETH(WETH).transfer(pair, amountETH)); + liquidity = IUniswapV2Pair(pair).mint(to); + // refund dust eth, if any + if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); + } + + // **** REMOVE LIQUIDITY **** + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair + (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); + (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); + (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); + require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + } + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) { + (amountToken, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, amountToken); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + uint value = approveMax ? type(uint256).max : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); + } + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountToken, uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? type(uint256).max : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); + } + + // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountETH) { + (, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this))); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? type(uint256).max : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + amountETH = removeLiquidityETHSupportingFeeOnTransferTokens( + token, liquidity, amountTokenMin, amountETHMin, to, deadline + ); + } + + // **** SWAP **** + // requires the initial amount to have already been sent to the first pair + function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + uint amountOut = amounts[i + 1]; + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( + amount0Out, amount1Out, to, new bytes(0) + ); + } + } + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + override + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + } + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + virtual + override + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + override + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + virtual + override + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + // refund dust eth, if any + if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); + } + + // **** SWAP (supporting fee-on-transfer tokens) **** + // requires the initial amount to have already been sent to the first pair + function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)); + uint amountInput; + uint amountOutput; + { // scope to avoid stack too deep errors + (uint reserve0, uint reserve1,) = pair.getReserves(); + (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput); + amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); + } + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + pair.swap(amount0Out, amount1Out, to, new bytes(0)); + } + } + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) { + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + override + payable + ensure(deadline) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + uint amountIn = msg.value; + IWETH(WETH).deposit{value: amountIn}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn)); + uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + override + ensure(deadline) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + _swapSupportingFeeOnTransferTokens(path, address(this)); + uint amountOut = IERC20(WETH).balanceOf(address(this)); + require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).withdraw(amountOut); + TransferHelper.safeTransferETH(to, amountOut); + } + + // **** LIBRARY FUNCTIONS **** + function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) { + return UniswapV2Library.quote(amountA, reserveA, reserveB); + } + + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) + public + pure + virtual + override + returns (uint amountOut) + { + return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); + } + + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) + public + pure + virtual + override + returns (uint amountIn) + { + return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut); + } + + function getAmountsOut(uint amountIn, address[] memory path) + public + view + virtual + override + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsOut(factory, amountIn, path); + } + + function getAmountsIn(uint amountOut, address[] memory path) + public + view + virtual + override + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsIn(factory, amountOut, path); + } +} diff --git a/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Callee.sol b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Callee.sol new file mode 100644 index 000000000..5f3b6cba1 --- /dev/null +++ b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Callee.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} diff --git a/contracts/dependencies/uniswapv2/interfaces/IUniswapV2ERC20.sol b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2ERC20.sol new file mode 100644 index 000000000..5bcac2bb9 --- /dev/null +++ b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2ERC20.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +interface IUniswapV2ERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; +} diff --git a/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Factory.sol b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Factory.sol new file mode 100644 index 000000000..6652284bc --- /dev/null +++ b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Factory.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +interface IUniswapV2Factory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function createPair(address tokenA, address tokenB) external returns (address pair); + + function setFeeTo(address) external; + function setFeeToSetter(address) external; +} diff --git a/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Pair.sol b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Pair.sol new file mode 100644 index 000000000..63da921d6 --- /dev/null +++ b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Pair.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import './IUniswapV2ERC20.sol'; + +interface IUniswapV2Pair is IUniswapV2ERC20 { + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} diff --git a/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Router01.sol b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Router01.sol new file mode 100644 index 000000000..3b84dbb12 --- /dev/null +++ b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Router01.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.6.2; + +interface IUniswapV2Router01 { + function factory() external view returns (address); + function WETH() external view returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountETH, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountETH); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} diff --git a/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Router02.sol b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Router02.sol new file mode 100644 index 000000000..aa65427d6 --- /dev/null +++ b/contracts/dependencies/uniswapv2/interfaces/IUniswapV2Router02.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.6.2; + +import './IUniswapV2Router01.sol'; + +interface IUniswapV2Router02 is IUniswapV2Router01 { + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} diff --git a/contracts/dependencies/uniswapv2/interfaces/IWETH.sol b/contracts/dependencies/uniswapv2/interfaces/IWETH.sol new file mode 100644 index 000000000..b0c2c4899 --- /dev/null +++ b/contracts/dependencies/uniswapv2/interfaces/IWETH.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +interface IWETH { + function deposit() external payable; + function transfer(address to, uint value) external returns (bool); + function withdraw(uint) external; +} diff --git a/contracts/dependencies/uniswapv2/libraries/Math.sol b/contracts/dependencies/uniswapv2/libraries/Math.sol new file mode 100644 index 000000000..960c74678 --- /dev/null +++ b/contracts/dependencies/uniswapv2/libraries/Math.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +// a library for performing various math operations + +library Math { + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } +} diff --git a/contracts/dependencies/uniswapv2/libraries/SafeMath.sol b/contracts/dependencies/uniswapv2/libraries/SafeMath.sol new file mode 100644 index 000000000..44dc62b1d --- /dev/null +++ b/contracts/dependencies/uniswapv2/libraries/SafeMath.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) + +library SafeMath { + function add(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, 'ds-math-add-overflow'); + } + + function sub(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, 'ds-math-sub-underflow'); + } + + function mul(uint x, uint y) internal pure returns (uint z) { + require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); + } +} diff --git a/contracts/dependencies/uniswapv2/libraries/TransferHelper.sol b/contracts/dependencies/uniswapv2/libraries/TransferHelper.sol new file mode 100644 index 000000000..0312ca6d5 --- /dev/null +++ b/contracts/dependencies/uniswapv2/libraries/TransferHelper.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import '../../openzeppelin/contracts/IERC20.sol'; + +library TransferHelper { + /// @notice Transfers tokens from the targeted address to the given destination + /// @notice Errors with 'STF' if transfer fails + /// @param token The contract address of the token to be transferred + /// @param from The originating address from which the tokens will be transferred + /// @param to The destination address of the transfer + /// @param value The amount to be transferred + function safeTransferFrom( + address token, + address from, + address to, + uint256 value + ) internal { + (bool success, bytes memory data) = token.call( + abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value) + ); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'STF'); + } + + /// @notice Transfers tokens from msg.sender to a recipient + /// @dev Errors with ST if transfer fails + /// @param token The contract address of the token which will be transferred + /// @param to The recipient of the transfer + /// @param value The value of the transfer + function safeTransfer( + address token, + address to, + uint256 value + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'ST'); + } + + /// @notice Approves the stipulated contract to spend the given allowance in the given token + /// @dev Errors with 'SA' if transfer fails + /// @param token The contract address of the token to be approved + /// @param to The target of the approval + /// @param value The amount of the given token the target will be allowed to spend + function safeApprove( + address token, + address to, + uint256 value + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'SA'); + } + + /// @notice Transfers ETH to the recipient address + /// @dev Fails with `STE` + /// @param to The destination of the transfer + /// @param value The value to be transferred + function safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, 'STE'); + } +} diff --git a/contracts/dependencies/uniswapv2/libraries/UQ112x112.sol b/contracts/dependencies/uniswapv2/libraries/UQ112x112.sol new file mode 100644 index 000000000..0a73d7871 --- /dev/null +++ b/contracts/dependencies/uniswapv2/libraries/UQ112x112.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) + +// range: [0, 2**112 - 1] +// resolution: 1 / 2**112 + +library UQ112x112 { + uint224 constant Q112 = 2**112; + + // encode a uint112 as a UQ112x112 + function encode(uint112 y) internal pure returns (uint224 z) { + z = uint224(y) * Q112; // never overflows + } + + // divide a UQ112x112 by a uint112, returning a UQ112x112 + function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { + z = x / uint224(y); + } +} diff --git a/contracts/dependencies/uniswapv2/libraries/UniswapV2Library.sol b/contracts/dependencies/uniswapv2/libraries/UniswapV2Library.sol new file mode 100644 index 000000000..3454f98e5 --- /dev/null +++ b/contracts/dependencies/uniswapv2/libraries/UniswapV2Library.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../interfaces/IUniswapV2Pair.sol'; + +import "./SafeMath.sol"; + +library UniswapV2Library { + using SafeMath for uint; + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address(uint160(uint(keccak256(abi.encodePacked( + hex'ff', + factory, + keccak256(abi.encodePacked(token0, token1)), + hex'd31b722f1d6217c98453395ca84838aab5b6fb36d007e1992926cd2dac4c6949' // init code hash + ))))); + } + + // fetches and sorts the reserves for a pair + function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { + (address token0,) = sortTokens(tokenA, tokenB); + (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset + function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { + require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); + require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + amountB = amountA.mul(reserveB) / reserveA; + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { + require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint amountInWithFee = amountIn.mul(997); + uint numerator = amountInWithFee.mul(reserveOut); + uint denominator = reserveIn.mul(1000).add(amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { + require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint numerator = reserveIn.mul(amountOut).mul(1000); + uint denominator = reserveOut.sub(amountOut).mul(997); + amountIn = (numerator / denominator).add(1); + } + + // performs chained getAmountOut calculations on any number of pairs + function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[0] = amountIn; + for (uint i; i < path.length - 1; i++) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); + amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); + } + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint i = path.length - 1; i > 0; i--) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} diff --git a/contracts/interfaces/ILiquidityNFTOracleWrapper.sol b/contracts/interfaces/ILiquidityNFTOracleWrapper.sol new file mode 100644 index 000000000..9dee5b36b --- /dev/null +++ b/contracts/interfaces/ILiquidityNFTOracleWrapper.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {IAtomicPriceAggregator} from "../interfaces/IAtomicPriceAggregator.sol"; +import {ILiquidityNFTPositionInfoProvider} from "../interfaces/ILiquidityNFTPositionInfoProvider.sol"; + +interface ILiquidityNFTOracleWrapper is + IAtomicPriceAggregator, + ILiquidityNFTPositionInfoProvider +{} diff --git a/contracts/interfaces/IUniswapV3PositionInfoProvider.sol b/contracts/interfaces/ILiquidityNFTPositionInfoProvider.sol similarity index 54% rename from contracts/interfaces/IUniswapV3PositionInfoProvider.sol rename to contracts/interfaces/ILiquidityNFTPositionInfoProvider.sol index 7efd8ca0f..3936ad929 100644 --- a/contracts/interfaces/IUniswapV3PositionInfoProvider.sol +++ b/contracts/interfaces/ILiquidityNFTPositionInfoProvider.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; -struct UinswapV3PositionData { +struct LiquidityNFTPositionData { address token0; address token1; uint24 fee; @@ -16,20 +16,37 @@ struct UinswapV3PositionData { uint256 tokensOwed1; } +struct OnChainFeeParams { + uint256 feeGrowthOutside0X128Lower; + uint256 feeGrowthOutside1X128Lower; + uint256 feeGrowthOutside0X128Upper; + uint256 feeGrowthOutside1X128Upper; + uint256 feeGrowthGlobal0X128; + uint256 feeGrowthGlobal1X128; +} + +struct PairOracleData { + uint256 token0Price; + uint256 token1Price; + uint8 token0Decimal; + uint8 token1Decimal; + uint160 sqrtPriceX96; +} + /************ @title IUniswapV3PositionInfoProvider interface @notice Interface for UniswapV3 Lp token position info.*/ -interface IUniswapV3PositionInfoProvider { +interface ILiquidityNFTPositionInfoProvider { function getOnchainPositionData( uint256 tokenId - ) external view returns (UinswapV3PositionData memory); + ) external view returns (LiquidityNFTPositionData memory); function getLiquidityAmountFromPositionData( - UinswapV3PositionData memory positionData + LiquidityNFTPositionData memory positionData ) external view returns (uint256 token0Amount, uint256 token1Amount); function getLpFeeAmountFromPositionData( - UinswapV3PositionData memory positionData + LiquidityNFTPositionData memory positionData ) external view returns (uint256 token0Amount, uint256 token1Amount); } diff --git a/contracts/interfaces/INTokenLiquidity.sol b/contracts/interfaces/INTokenLiquidity.sol new file mode 100644 index 000000000..cd24c64cd --- /dev/null +++ b/contracts/interfaces/INTokenLiquidity.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +interface INTokenLiquidity { + /** + * @notice Get underlying ERC20 asset pair. + */ + function underlyingAsset( + uint256 tokenId + ) external view returns (address token0, address token1); + + /** + * @notice Decreases liquidity for underlying NFT LP and validates + * that the user respects liquidation checks. + * @param user The user address decreasing liquidity for + * @param tokenId The id of the erc721 token + * @param liquidityDecrease The amount of liquidity to remove of LP + * @param amount0Min The minimum amount to remove of token0 + * @param amount1Min The minimum amount to remove of token1 + */ + function decreaseLiquidity( + address user, + uint256 tokenId, + uint128 liquidityDecrease, + uint256 amount0Min, + uint256 amount1Min + ) + external + returns ( + address token0, + address token1, + uint256 amount0, + uint256 amount1, + bool isBurned + ); + + /** + * @notice Increases liquidity for underlying NFT LP. + * @param payer The user address pay for the asset to increase liquidity + * @param tokenId The id of the erc721 token + * @param amountAdd0 The amount to add of token0 + * @param amountAdd1 The amount to add of token1 + * @param amount0Min The minimum amount to add of token0 + * @param amount1Min The minimum amount to add of token1 + * @param deadline The time by which the transaction must be included + */ + function increaseLiquidity( + address payer, + uint256 tokenId, + uint256 amountAdd0, + uint256 amountAdd1, + uint256 amount0Min, + uint256 amount1Min, + uint256 deadline + ) external payable; +} diff --git a/contracts/interfaces/INTokenUniswapV3.sol b/contracts/interfaces/INTokenUniswapV3.sol deleted file mode 100644 index 112de9ad5..000000000 --- a/contracts/interfaces/INTokenUniswapV3.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; - -interface INTokenUniswapV3 { - /** - * @notice Decreases liquidity for underlying Uniswap V3 NFT LP and validates - * that the user respects liquidation checks. - * @param user The user address decreasing liquidity for - * @param tokenId The id of the erc721 token - * @param liquidityDecrease The amount of liquidity to remove of LP - * @param amount0Min The minimum amount to remove of token0 - * @param amount1Min The minimum amount to remove of token1 - * @param receiveEthAsWeth If convert weth to ETH - */ - function decreaseUniswapV3Liquidity( - address user, - uint256 tokenId, - uint128 liquidityDecrease, - uint256 amount0Min, - uint256 amount1Min, - bool receiveEthAsWeth - ) external; -} diff --git a/contracts/interfaces/IPool.sol b/contracts/interfaces/IPool.sol index 0839b4c3a..76f50ae07 100644 --- a/contracts/interfaces/IPool.sol +++ b/contracts/interfaces/IPool.sol @@ -6,6 +6,7 @@ import {IPoolMarketplace} from "./IPoolMarketplace.sol"; import {IPoolParameters} from "./IPoolParameters.sol"; import {IParaProxyInterfaces} from "./IParaProxyInterfaces.sol"; import {IPoolPositionMover} from "./IPoolPositionMover.sol"; +import "./IPoolLpOperation.sol"; import "./IPoolApeStaking.sol"; import "./IPoolBorrowAndStake.sol"; @@ -21,7 +22,8 @@ interface IPool is IPoolApeStaking, IParaProxyInterfaces, IPoolPositionMover, - IPoolBorrowAndStake + IPoolBorrowAndStake, + IPoolLpOperation { } diff --git a/contracts/interfaces/IPoolCore.sol b/contracts/interfaces/IPoolCore.sol index 6f409fdd7..3da06dc4c 100644 --- a/contracts/interfaces/IPoolCore.sol +++ b/contracts/interfaces/IPoolCore.sol @@ -324,25 +324,6 @@ interface IPoolCore { address to ) external returns (uint256); - /** - * @notice Decreases liquidity for underlying Uniswap V3 NFT LP and validates - * that the user respects liquidation checks. - * @param asset The asset address of uniswapV3 - * @param tokenId The id of the erc721 token - * @param liquidityDecrease The amount of liquidity to remove of LP - * @param amount0Min The minimum amount to remove of token0 - * @param amount1Min The minimum amount to remove of token1 - * @param receiveEthAsWeth If convert weth to ETH - */ - function decreaseUniswapV3Liquidity( - address asset, - uint256 tokenId, - uint128 liquidityDecrease, - uint256 amount0Min, - uint256 amount1Min, - bool receiveEthAsWeth - ) external; - /** * @notice Allows users to borrow a specific `amount` of the reserve underlying asset, provided that the borrower * already supplied enough collateral, or he was given enough allowance by a credit delegator on the diff --git a/contracts/interfaces/IPoolLpOperation.sol b/contracts/interfaces/IPoolLpOperation.sol new file mode 100644 index 000000000..2cf25bcb7 --- /dev/null +++ b/contracts/interfaces/IPoolLpOperation.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {IPoolAddressesProvider} from "./IPoolAddressesProvider.sol"; +import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; + +/** + * @title IPool + * + * @notice Defines the basic interface for an ParaSpace Pool. + **/ +interface IPoolLpOperation { + /** + * @notice Increases liquidity for underlying NFT LP + * @param tokenId The id of the erc721 token + * @param amountAdd0 The amount to add of token0 + * @param amountAdd1 The amount to add of token1 + * @param amount0Min The minimum amount to add of token0 + * @param amount1Min The minimum amount to add of token1 + * @param deadline The time by which the transaction must be included + */ + function increaseLiquidity( + address asset, + uint256 tokenId, + uint256 amountAdd0, + uint256 amountAdd1, + uint256 amount0Min, + uint256 amount1Min, + uint256 deadline + ) external payable; + + /** + * @notice Adjust liquidity position for underlying NFT LP + * @param assetInfo The underlying erc20 asset info for adjusting position + * @param decreaseLiquidityParam The params for decreasing underlying NFT LP liquidity + * @param mintParams The params for minting new underlying NFT LP + */ + function adjustLpPosition( + DataTypes.AssetInfo calldata assetInfo, + DataTypes.DecreaseLiquidityParam calldata decreaseLiquidityParam, + DataTypes.MintParams calldata mintParams + ) external; +} diff --git a/contracts/interfaces/IUniswapV3OracleWrapper.sol b/contracts/interfaces/IUniswapV3OracleWrapper.sol deleted file mode 100644 index 8e5e2f64e..000000000 --- a/contracts/interfaces/IUniswapV3OracleWrapper.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; - -import {IAtomicPriceAggregator} from "../interfaces/IAtomicPriceAggregator.sol"; -import {IUniswapV3PositionInfoProvider} from "../interfaces/IUniswapV3PositionInfoProvider.sol"; - -interface IUniswapV3OracleWrapper is - IAtomicPriceAggregator, - IUniswapV3PositionInfoProvider -{} diff --git a/contracts/interfaces/IXTokenType.sol b/contracts/interfaces/IXTokenType.sol index 79a6df9fb..79f6dddf1 100644 --- a/contracts/interfaces/IXTokenType.sol +++ b/contracts/interfaces/IXTokenType.sol @@ -25,7 +25,7 @@ enum XTokenType { NTokenOtherdeed, NTokenStakefish, NTokenChromieSquiggle, - PhantomData1, + NTokenIZUMILp, PhantomData2, PhantomData3, PhantomData4, diff --git a/contracts/misc/IZUMIOracleWrapper.sol b/contracts/misc/IZUMIOracleWrapper.sol new file mode 100644 index 000000000..49b92c8a3 --- /dev/null +++ b/contracts/misc/IZUMIOracleWrapper.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ILiquidityNFTOracleWrapper} from "../interfaces/ILiquidityNFTOracleWrapper.sol"; +import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; +import {IPriceOracleGetter} from "../interfaces/IPriceOracleGetter.sol"; +import {IiZiSwapFactory} from "../dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapFactory.sol"; +import {IiZiSwapPool} from "../dependencies/izumi/izumi-swap-core/interfaces/IiZiSwapPool.sol"; +import {ILiquidityManager} from "../dependencies/izumi/izumi-swap-periphery/interfaces/ILiquidityManager.sol"; +import {TickMath} from "../dependencies/uniswapv3-core/libraries/TickMath.sol"; +import {LiquidityNFTPositionData, OnChainFeeParams, PairOracleData} from "../interfaces/ILiquidityNFTPositionInfoProvider.sol"; +import {LogPowMath} from "../dependencies/izumi/izumi-swap-core/libraries/LogPowMath.sol"; +import {AmountMath} from "../dependencies/izumi/izumi-swap-core/libraries/AmountMath.sol"; +import {MulDivMath} from "../dependencies/izumi/izumi-swap-core/libraries/MulDivMath.sol"; +import {TwoPower} from "../dependencies/izumi/izumi-swap-core/libraries/TwoPower.sol"; +import {LiquidityNFTOracleWrapper} from "./LiquidityNFTOracleWrapper.sol"; + +contract IZUMIOracleWrapper is LiquidityNFTOracleWrapper { + constructor( + address _factory, + address _manager, + address _addressProvider + ) LiquidityNFTOracleWrapper(_factory, _manager, _addressProvider) {} + + function _getPoolAddress( + address token0, + address token1, + uint24 fee + ) internal view override returns (address) { + return IiZiSwapFactory(DEX_FACTORY).pool(token0, token1, fee); + } + + function _getOnchainPositionData( + uint256 tokenId + ) + internal + view + override + returns (address pool, LiquidityNFTPositionData memory positionData) + { + uint128 poolId; + ( + positionData.tickLower, + positionData.tickUpper, + positionData.liquidity, + positionData.feeGrowthInside0LastX128, + positionData.feeGrowthInside1LastX128, + positionData.tokensOwed0, + positionData.tokensOwed1, + poolId + ) = ILiquidityManager(POSITION_MANAGER).liquidities(tokenId); + ( + positionData.token0, + positionData.token1, + positionData.fee + ) = ILiquidityManager(POSITION_MANAGER).poolMetas(poolId); + + pool = IiZiSwapFactory(DEX_FACTORY).pool( + positionData.token0, + positionData.token1, + positionData.fee + ); + ( + positionData.currentPrice, + positionData.currentTick, + , + , + , + , + , + + ) = IiZiSwapPool(pool).state(); + } + + function _calculateLiquidityAmount( + int24 tickLower, + int24 tickUpper, + uint160 currentPrice, + uint128 liquidity + ) + internal + pure + override + returns (uint256 token0Amount, uint256 token1Amount) + { + (token0Amount, token1Amount, ) = _computeDepositXY( + liquidity, + tickLower, + tickUpper, + TickMath.getTickAtSqrtRatio(currentPrice), + currentPrice + ); + } + + function _getPendingFeeAmounts( + address pool, + LiquidityNFTPositionData memory positionData + ) + internal + view + override + returns (uint256 token0Amount, uint256 token1Amount) + { + IiZiSwapPool poolState = IiZiSwapPool(pool); + OnChainFeeParams memory feeParams; + + ( + , + , + feeParams.feeGrowthOutside0X128Lower, + feeParams.feeGrowthOutside1X128Lower, + + ) = poolState.points(positionData.tickLower); + ( + , + , + feeParams.feeGrowthOutside0X128Upper, + feeParams.feeGrowthOutside1X128Upper, + + ) = poolState.points(positionData.tickUpper); + + feeParams.feeGrowthGlobal0X128 = poolState.feeScaleX_128(); + feeParams.feeGrowthGlobal1X128 = poolState.feeScaleY_128(); + + (token0Amount, token1Amount) = _calculateTokenFee( + positionData, + feeParams + ); + } + + function _computeDepositXY( + uint128 liquidDelta, + int24 leftPoint, + int24 rightPoint, + int24 currentPoint, + uint160 sqrtPrice_96 + ) private pure returns (uint128 x, uint128 y, uint128 yc) { + x = 0; + uint256 amountY = 0; + uint160 sqrtPriceR_96 = LogPowMath.getSqrtPrice(rightPoint); + uint160 _sqrtRate_96 = LogPowMath.getSqrtPrice(1); + if (leftPoint < currentPoint) { + uint160 sqrtPriceL_96 = LogPowMath.getSqrtPrice(leftPoint); + uint256 yl; + if (rightPoint < currentPoint) { + yl = AmountMath.getAmountY( + liquidDelta, + sqrtPriceL_96, + sqrtPriceR_96, + _sqrtRate_96, + true + ); + } else { + yl = AmountMath.getAmountY( + liquidDelta, + sqrtPriceL_96, + sqrtPrice_96, + _sqrtRate_96, + true + ); + } + amountY += yl; + } + if (rightPoint > currentPoint) { + // we need compute XR + int24 xrLeft = (leftPoint > currentPoint) + ? leftPoint + : currentPoint + 1; + uint256 xr = AmountMath.getAmountX( + liquidDelta, + xrLeft, + rightPoint, + sqrtPriceR_96, + _sqrtRate_96, + true + ); + x = uint128(xr); + require(x == xr, "XOFL"); + } + if (leftPoint <= currentPoint && rightPoint > currentPoint) { + // we need compute yc at point of current price + yc = _computeDepositYc(liquidDelta, sqrtPrice_96); + amountY += yc; + } else { + yc = 0; + } + y = uint128(amountY); + require(y == amountY, "YOFL"); + } + + function _computeDepositYc( + uint128 liquidDelta, + uint160 sqrtPrice_96 + ) private pure returns (uint128 y) { + // to simplify computation, + // minter is required to deposit only token y in point of current price + uint256 amount = MulDivMath.mulDivCeil( + liquidDelta, + sqrtPrice_96, + TwoPower.Pow96 + ); + y = uint128(amount); + require(y == amount, "YC OFL"); + } +} diff --git a/contracts/misc/LiquidityNFTOracleWrapper.sol b/contracts/misc/LiquidityNFTOracleWrapper.sol new file mode 100644 index 000000000..56686546e --- /dev/null +++ b/contracts/misc/LiquidityNFTOracleWrapper.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ILiquidityNFTOracleWrapper} from "../interfaces/ILiquidityNFTOracleWrapper.sol"; +import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; +import {IPriceOracleGetter} from "../interfaces/IPriceOracleGetter.sol"; +import {IUniswapV3Factory} from "../dependencies/uniswapv3-core/interfaces/IUniswapV3Factory.sol"; +import {IUniswapV3PoolState} from "../dependencies/uniswapv3-core/interfaces/pool/IUniswapV3PoolState.sol"; +import {INonfungiblePositionManager} from "../dependencies/uniswapv3-periphery/interfaces/INonfungiblePositionManager.sol"; +import {LiquidityAmounts} from "../dependencies/uniswapv3-periphery/libraries/LiquidityAmounts.sol"; +import {TickMath} from "../dependencies/uniswapv3-core/libraries/TickMath.sol"; +import {FullMath} from "../dependencies/uniswapv3-core/libraries/FullMath.sol"; +import {LiquidityNFTPositionData, OnChainFeeParams, PairOracleData} from "../interfaces/ILiquidityNFTPositionInfoProvider.sol"; +import {SafeCast} from "../dependencies/uniswapv3-core/libraries/SafeCast.sol"; +import {FixedPoint96} from "../dependencies/uniswapv3-core/libraries/FixedPoint96.sol"; +import {IERC20Detailed} from "../dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import {SqrtLib} from "../dependencies/math/SqrtLib.sol"; + +abstract contract LiquidityNFTOracleWrapper is ILiquidityNFTOracleWrapper { + using SafeCast for uint256; + + address immutable DEX_FACTORY; + address immutable POSITION_MANAGER; + IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; + + uint256 internal constant Q128 = 0x100000000000000000000000000000000; + + constructor(address _factory, address _manager, address _addressProvider) { + DEX_FACTORY = _factory; + POSITION_MANAGER = _manager; + ADDRESSES_PROVIDER = IPoolAddressesProvider(_addressProvider); + } + + /** + * @notice get onchain position data from uniswap for the specified tokenId. + */ + function getOnchainPositionData( + uint256 tokenId + ) external view returns (LiquidityNFTPositionData memory positionData) { + (, positionData) = _getOnchainPositionData(tokenId); + } + + /** + * @notice get onchain liquidity amount for the specified tokenId. + */ + function getLiquidityAmount( + uint256 tokenId + ) external view returns (uint256 token0Amount, uint256 token1Amount) { + ( + , + LiquidityNFTPositionData memory positionData + ) = _getOnchainPositionData(tokenId); + (token0Amount, token1Amount) = _calculateLiquidityAmount( + positionData.tickLower, + positionData.tickUpper, + positionData.currentPrice, + positionData.liquidity + ); + } + + /** + * @notice calculate liquidity amount for the position data. + * @param positionData The specified position data + */ + function getLiquidityAmountFromPositionData( + LiquidityNFTPositionData memory positionData + ) external pure returns (uint256 token0Amount, uint256 token1Amount) { + (token0Amount, token1Amount) = _calculateLiquidityAmount( + positionData.tickLower, + positionData.tickUpper, + positionData.currentPrice, + positionData.liquidity + ); + } + + /** + * @notice get liquidity provider fee amount for the specified tokenId. + */ + function getLpFeeAmount( + uint256 tokenId + ) external view returns (uint256 token0Amount, uint256 token1Amount) { + ( + address pool, + LiquidityNFTPositionData memory positionData + ) = _getOnchainPositionData(tokenId); + (token0Amount, token1Amount) = _getLpFeeAmountFromPositionData( + pool, + positionData + ); + } + + /** + * @notice calculate liquidity provider fee amount for the position data. + * @param positionData The specified position data + */ + function getLpFeeAmountFromPositionData( + LiquidityNFTPositionData memory positionData + ) external view returns (uint256 token0Amount, uint256 token1Amount) { + address pool = _getPoolAddress( + positionData.token0, + positionData.token1, + positionData.fee + ); + return _getLpFeeAmountFromPositionData(pool, positionData); + } + + /** + * @notice Returns the price for the specified tokenId. + */ + function getTokenPrice(uint256 tokenId) public view returns (uint256) { + ( + address pool, + LiquidityNFTPositionData memory positionData + ) = _getOnchainPositionData(tokenId); + + PairOracleData memory oracleData = _getOracleData( + IPriceOracleGetter(ADDRESSES_PROVIDER.getPriceOracle()), + positionData + ); + + ( + uint256 liquidityAmount0, + uint256 liquidityAmount1 + ) = _calculateLiquidityAmount( + positionData.tickLower, + positionData.tickUpper, + oracleData.sqrtPriceX96, + positionData.liquidity + ); + + ( + uint256 feeAmount0, + uint256 feeAmount1 + ) = _getLpFeeAmountFromPositionData(pool, positionData); + + return + (((liquidityAmount0 + feeAmount0) * oracleData.token0Price) / + 10 ** oracleData.token0Decimal) + + (((liquidityAmount1 + feeAmount1) * oracleData.token1Price) / + 10 ** oracleData.token1Decimal); + } + + function _getLpFeeAmountFromPositionData( + address pool, + LiquidityNFTPositionData memory positionData + ) internal view returns (uint256 token0Amount, uint256 token1Amount) { + (token0Amount, token1Amount) = _getPendingFeeAmounts( + pool, + positionData + ); + + token0Amount += positionData.tokensOwed0; + token1Amount += positionData.tokensOwed1; + } + + function _getOracleData( + IPriceOracleGetter oracle, + LiquidityNFTPositionData memory positionData + ) internal view returns (PairOracleData memory) { + PairOracleData memory oracleData; + oracleData.token0Price = oracle.getAssetPrice(positionData.token0); + oracleData.token1Price = oracle.getAssetPrice(positionData.token1); + + oracleData.token0Decimal = IERC20Detailed(positionData.token0) + .decimals(); + oracleData.token1Decimal = IERC20Detailed(positionData.token1) + .decimals(); + + oracleData.sqrtPriceX96 = ((SqrtLib.sqrt( + ((oracleData.token0Price * + 10 ** + (36 + + oracleData.token1Decimal - + oracleData.token0Decimal)) / (oracleData.token1Price)) + ) << FixedPoint96.RESOLUTION) / 1E18).toUint160(); + + return oracleData; + } + + function _calculateTokenFee( + LiquidityNFTPositionData memory positionData, + OnChainFeeParams memory feeParams + ) internal pure returns (uint256 token0Amount, uint256 token1Amount) { + unchecked { + // calculate fee growth below + uint256 feeGrowthBelow0X128; + uint256 feeGrowthBelow1X128; + if (positionData.currentTick >= positionData.tickLower) { + feeGrowthBelow0X128 = feeParams.feeGrowthOutside0X128Lower; + feeGrowthBelow1X128 = feeParams.feeGrowthOutside1X128Lower; + } else { + feeGrowthBelow0X128 = + feeParams.feeGrowthGlobal0X128 - + feeParams.feeGrowthOutside0X128Lower; + feeGrowthBelow1X128 = + feeParams.feeGrowthGlobal1X128 - + feeParams.feeGrowthOutside1X128Lower; + } + + // calculate fee growth above + uint256 feeGrowthAbove0X128; + uint256 feeGrowthAbove1X128; + if (positionData.currentTick < positionData.tickUpper) { + feeGrowthAbove0X128 = feeParams.feeGrowthOutside0X128Upper; + feeGrowthAbove1X128 = feeParams.feeGrowthOutside1X128Upper; + } else { + feeGrowthAbove0X128 = + feeParams.feeGrowthGlobal0X128 - + feeParams.feeGrowthOutside0X128Upper; + feeGrowthAbove1X128 = + feeParams.feeGrowthGlobal1X128 - + feeParams.feeGrowthOutside1X128Upper; + } + uint256 feeGrowthInside0X128; + uint256 feeGrowthInside1X128; + + feeGrowthInside0X128 = + feeParams.feeGrowthGlobal0X128 - + feeGrowthBelow0X128 - + feeGrowthAbove0X128; + feeGrowthInside1X128 = + feeParams.feeGrowthGlobal1X128 - + feeGrowthBelow1X128 - + feeGrowthAbove1X128; + + token0Amount = uint128( + FullMath.mulDiv( + feeGrowthInside0X128 - + positionData.feeGrowthInside0LastX128, + positionData.liquidity, + Q128 + ) + ); + + token1Amount = uint128( + FullMath.mulDiv( + feeGrowthInside1X128 - + positionData.feeGrowthInside1LastX128, + positionData.liquidity, + Q128 + ) + ); + } + } + + function _getPoolAddress( + address, + address, + uint24 + ) internal view virtual returns (address) { + return address(0); + } + + function _getPendingFeeAmounts( + address, + LiquidityNFTPositionData memory + ) internal view virtual returns (uint256, uint256) { + return (0, 0); + } + + function _getOnchainPositionData( + uint256 + ) internal view virtual returns (address, LiquidityNFTPositionData memory) { + LiquidityNFTPositionData memory positionData; + return (address(0), positionData); + } + + function _calculateLiquidityAmount( + int24, + int24, + uint160, + uint128 + ) internal pure virtual returns (uint256, uint256) { + return (0, 0); + } +} diff --git a/contracts/misc/UniswapV2OracleWrapper.sol b/contracts/misc/UniswapV2OracleWrapper.sol new file mode 100644 index 000000000..9c33ad21c --- /dev/null +++ b/contracts/misc/UniswapV2OracleWrapper.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; +import {IPriceOracleGetter} from "../interfaces/IPriceOracleGetter.sol"; +import {ICLSynchronicityPriceAdapter} from "../dependencies/chainlink/ICLSynchronicityPriceAdapter.sol"; +import {IUniswapV2Pair} from "../dependencies/uniswapv2/interfaces/IUniswapV2Pair.sol"; +import {IERC20Detailed} from "../dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import {IERC20} from "../dependencies/openzeppelin/contracts/IERC20.sol"; +import {SqrtLib} from "../dependencies/math/SqrtLib.sol"; +import "../dependencies/openzeppelin/contracts/SafeCast.sol"; +import {Errors} from "../protocol/libraries/helpers/Errors.sol"; + +contract UniswapV2OracleWrapper is ICLSynchronicityPriceAdapter { + using SafeCast for uint256; + + IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; + address immutable underlyingAsset; + address immutable token0; + address immutable token1; + uint8 immutable token0Decimal; + uint8 immutable token1Decimal; + uint256 immutable KLIMIT; + + constructor(address _underlyingAsset, address _addressProvider) { + underlyingAsset = _underlyingAsset; + token0 = IUniswapV2Pair(_underlyingAsset).token0(); + token1 = IUniswapV2Pair(_underlyingAsset).token1(); + token0Decimal = IERC20Detailed(token0).decimals(); + token1Decimal = IERC20Detailed(token1).decimals(); + KLIMIT = (10 ** token0Decimal) * (10 ** token1Decimal); + + ADDRESSES_PROVIDER = IPoolAddressesProvider(_addressProvider); + } + + /// @inheritdoc ICLSynchronicityPriceAdapter + function latestAnswer() public view virtual override returns (int256) { + (uint256 _reserve0, uint256 _reserve1, ) = IUniswapV2Pair( + underlyingAsset + ).getReserves(); + uint256 K = _reserve0 * _reserve1; + require(K > KLIMIT, Errors.INVALID_LIQUIDITY); + + IPriceOracleGetter oracle = IPriceOracleGetter( + ADDRESSES_PROVIDER.getPriceOracle() + ); + uint256 token0Price = oracle.getAssetPrice(token0); + uint256 token1Price = oracle.getAssetPrice(token1); + uint256 poolValue = SqrtLib.sqrt( + (K * token0Price * token1Price) / + 10 ** token0Decimal / + 10 ** token1Decimal + ) * 2; + + uint256 totalSupply = IERC20(underlyingAsset).totalSupply(); + uint256 _kLast = IUniswapV2Pair(underlyingAsset).kLast(); + if (_kLast > 0) { + uint rootK = SqrtLib.sqrt(K); + uint rootKLast = SqrtLib.sqrt(_kLast); + if (rootK > rootKLast) { + uint numerator = totalSupply * (rootK - rootKLast); + uint denominator = rootK * 5 + rootKLast; + uint liquidity = numerator / denominator; + totalSupply += liquidity; + } + } + uint256 price = (poolValue * 1e18) / totalSupply; + + return price.toInt256(); + } +} diff --git a/contracts/misc/UniswapV3OracleWrapper.sol b/contracts/misc/UniswapV3OracleWrapper.sol index 445fda85b..c929f9c98 100644 --- a/contracts/misc/UniswapV3OracleWrapper.sol +++ b/contracts/misc/UniswapV3OracleWrapper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import {IUniswapV3OracleWrapper} from "../interfaces/IUniswapV3OracleWrapper.sol"; +import {ILiquidityNFTOracleWrapper} from "../interfaces/ILiquidityNFTOracleWrapper.sol"; import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; import {IPriceOracleGetter} from "../interfaces/IPriceOracleGetter.sol"; import {IUniswapV3Factory} from "../dependencies/uniswapv3-core/interfaces/IUniswapV3Factory.sol"; @@ -9,48 +9,33 @@ import {IUniswapV3PoolState} from "../dependencies/uniswapv3-core/interfaces/poo import {INonfungiblePositionManager} from "../dependencies/uniswapv3-periphery/interfaces/INonfungiblePositionManager.sol"; import {LiquidityAmounts} from "../dependencies/uniswapv3-periphery/libraries/LiquidityAmounts.sol"; import {TickMath} from "../dependencies/uniswapv3-core/libraries/TickMath.sol"; -import {SqrtLib} from "../dependencies/math/SqrtLib.sol"; import {FullMath} from "../dependencies/uniswapv3-core/libraries/FullMath.sol"; -import {IERC20Detailed} from "../dependencies/openzeppelin/contracts/IERC20Detailed.sol"; -import {UinswapV3PositionData} from "../interfaces/IUniswapV3PositionInfoProvider.sol"; -import {SafeCast} from "../dependencies/uniswapv3-core/libraries/SafeCast.sol"; -import {FixedPoint96} from "../dependencies/uniswapv3-core/libraries/FixedPoint96.sol"; - -contract UniswapV3OracleWrapper is IUniswapV3OracleWrapper { - using SafeCast for uint256; - - IUniswapV3Factory immutable UNISWAP_V3_FACTORY; - INonfungiblePositionManager immutable UNISWAP_V3_POSITION_MANAGER; - IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; - uint256 internal constant Q128 = 0x100000000000000000000000000000000; - - constructor(address _factory, address _manager, address _addressProvider) { - UNISWAP_V3_FACTORY = IUniswapV3Factory(_factory); - UNISWAP_V3_POSITION_MANAGER = INonfungiblePositionManager(_manager); - ADDRESSES_PROVIDER = IPoolAddressesProvider(_addressProvider); - } - - struct FeeParams { - uint256 feeGrowthOutside0X128Lower; - uint256 feeGrowthOutside1X128Lower; - uint256 feeGrowthOutside0X128Upper; - uint256 feeGrowthOutside1X128Upper; - } - - struct PairOracleData { - uint256 token0Price; - uint256 token1Price; - uint8 token0Decimal; - uint8 token1Decimal; - uint160 sqrtPriceX96; +import {LiquidityNFTPositionData, OnChainFeeParams, PairOracleData} from "../interfaces/ILiquidityNFTPositionInfoProvider.sol"; +import {LiquidityNFTOracleWrapper} from "./LiquidityNFTOracleWrapper.sol"; + +contract UniswapV3OracleWrapper is LiquidityNFTOracleWrapper { + constructor( + address _factory, + address _manager, + address _addressProvider + ) LiquidityNFTOracleWrapper(_factory, _manager, _addressProvider) {} + + function _getPoolAddress( + address token0, + address token1, + uint24 fee + ) internal view override returns (address) { + return IUniswapV3Factory(DEX_FACTORY).getPool(token0, token1, fee); } - /** - * @notice get onchain position data from uniswap for the specified tokenId. - */ - function getOnchainPositionData( + function _getOnchainPositionData( uint256 tokenId - ) public view returns (UinswapV3PositionData memory) { + ) + internal + view + override + returns (address, LiquidityNFTPositionData memory) + { ( , , @@ -64,153 +49,68 @@ contract UniswapV3OracleWrapper is IUniswapV3OracleWrapper { uint256 feeGrowthInside1LastX128, uint256 tokensOwed0, uint256 tokensOwed1 - ) = UNISWAP_V3_POSITION_MANAGER.positions(tokenId); - - IUniswapV3PoolState pool = IUniswapV3PoolState( - UNISWAP_V3_FACTORY.getPool(token0, token1, fee) - ); - (uint160 currentPrice, int24 currentTick, , , , , ) = pool.slot0(); + ) = INonfungiblePositionManager(POSITION_MANAGER).positions(tokenId); - return - UinswapV3PositionData({ - token0: token0, - token1: token1, - fee: fee, - tickLower: tickLower, - tickUpper: tickUpper, - currentTick: currentTick, - currentPrice: currentPrice, - liquidity: liquidity, - feeGrowthInside0LastX128: feeGrowthInside0LastX128, - feeGrowthInside1LastX128: feeGrowthInside1LastX128, - tokensOwed0: tokensOwed0, - tokensOwed1: tokensOwed1 - }); - } - - /** - * @notice get onchain liquidity amount for the specified tokenId. - */ - function getLiquidityAmount( - uint256 tokenId - ) external view returns (uint256 token0Amount, uint256 token1Amount) { - UinswapV3PositionData memory positionData = getOnchainPositionData( - tokenId + address pool = IUniswapV3Factory(DEX_FACTORY).getPool( + token0, + token1, + fee ); - (token0Amount, token1Amount) = getLiquidityAmountFromPositionData( - positionData - ); - } - - /** - * @notice calculate liquidity amount for the position data. - * @param positionData The specified position data - */ - function getLiquidityAmountFromPositionData( - UinswapV3PositionData memory positionData - ) public pure returns (uint256 token0Amount, uint256 token1Amount) { - (token0Amount, token1Amount) = LiquidityAmounts.getAmountsForLiquidity( - positionData.currentPrice, - TickMath.getSqrtRatioAtTick(positionData.tickLower), - TickMath.getSqrtRatioAtTick(positionData.tickUpper), - positionData.liquidity - ); - } - - /** - * @notice get liquidity provider fee amount for the specified tokenId. - */ - function getLpFeeAmount( - uint256 tokenId - ) external view returns (uint256 token0Amount, uint256 token1Amount) { - UinswapV3PositionData memory positionData = getOnchainPositionData( - tokenId - ); - (token0Amount, token1Amount) = getLpFeeAmountFromPositionData( - positionData - ); - } - - /** - * @notice calculate liquidity provider fee amount for the position data. - * @param positionData The specified position data - */ - function getLpFeeAmountFromPositionData( - UinswapV3PositionData memory positionData - ) public view returns (uint256 token0Amount, uint256 token1Amount) { - (token0Amount, token1Amount) = _getPendingFeeAmounts(positionData); - - token0Amount += positionData.tokensOwed0; - token1Amount += positionData.tokensOwed1; - } - - /** - * @notice Returns the price for the specified tokenId. - */ - function getTokenPrice(uint256 tokenId) public view returns (uint256) { - UinswapV3PositionData memory positionData = getOnchainPositionData( - tokenId - ); - - PairOracleData memory oracleData = _getOracleData(positionData); - - (uint256 liquidityAmount0, uint256 liquidityAmount1) = LiquidityAmounts - .getAmountsForLiquidity( - oracleData.sqrtPriceX96, - TickMath.getSqrtRatioAtTick(positionData.tickLower), - TickMath.getSqrtRatioAtTick(positionData.tickUpper), - positionData.liquidity - ); - ( - uint256 feeAmount0, - uint256 feeAmount1 - ) = getLpFeeAmountFromPositionData(positionData); + uint160 currentPrice, + int24 currentTick, + , + , + , + , - return - (((liquidityAmount0 + feeAmount0) * oracleData.token0Price) / - 10 ** oracleData.token0Decimal) + - (((liquidityAmount1 + feeAmount1) * oracleData.token1Price) / - 10 ** oracleData.token1Decimal); + ) = IUniswapV3PoolState(pool).slot0(); + LiquidityNFTPositionData memory positionData; + positionData.token0 = token0; + positionData.token1 = token1; + positionData.fee = fee; + positionData.tickLower = tickLower; + positionData.tickUpper = tickUpper; + positionData.liquidity = liquidity; + positionData.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + positionData.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + positionData.tokensOwed0 = tokensOwed0; + positionData.tokensOwed1 = tokensOwed1; + positionData.currentPrice = currentPrice; + positionData.currentTick = currentTick; + return (pool, positionData); } - function _getOracleData( - UinswapV3PositionData memory positionData - ) internal view returns (PairOracleData memory) { - PairOracleData memory oracleData; - IPriceOracleGetter oracle = IPriceOracleGetter( - ADDRESSES_PROVIDER.getPriceOracle() + function _calculateLiquidityAmount( + int24 tickLower, + int24 tickUpper, + uint160 currentPrice, + uint128 liquidity + ) + internal + pure + override + returns (uint256 token0Amount, uint256 token1Amount) + { + (token0Amount, token1Amount) = LiquidityAmounts.getAmountsForLiquidity( + currentPrice, + TickMath.getSqrtRatioAtTick(tickLower), + TickMath.getSqrtRatioAtTick(tickUpper), + liquidity ); - oracleData.token0Price = oracle.getAssetPrice(positionData.token0); - oracleData.token1Price = oracle.getAssetPrice(positionData.token1); - - oracleData.token0Decimal = IERC20Detailed(positionData.token0) - .decimals(); - oracleData.token1Decimal = IERC20Detailed(positionData.token1) - .decimals(); - - oracleData.sqrtPriceX96 = ((SqrtLib.sqrt( - ((oracleData.token0Price * - 10 ** - (36 + - oracleData.token1Decimal - - oracleData.token0Decimal)) / (oracleData.token1Price)) - ) << FixedPoint96.RESOLUTION) / 1E18).toUint160(); - - return oracleData; } function _getPendingFeeAmounts( - UinswapV3PositionData memory positionData - ) internal view returns (uint256 token0Amount, uint256 token1Amount) { - IUniswapV3PoolState pool = IUniswapV3PoolState( - UNISWAP_V3_FACTORY.getPool( - positionData.token0, - positionData.token1, - positionData.fee - ) - ); - FeeParams memory feeParams; + address pool, + LiquidityNFTPositionData memory positionData + ) + internal + view + override + returns (uint256 token0Amount, uint256 token1Amount) + { + IUniswapV3PoolState poolState = IUniswapV3PoolState(pool); + OnChainFeeParams memory feeParams; ( , @@ -221,7 +121,7 @@ contract UniswapV3OracleWrapper is IUniswapV3OracleWrapper { , , - ) = pool.ticks(positionData.tickLower); + ) = poolState.ticks(positionData.tickLower); ( , , @@ -231,70 +131,14 @@ contract UniswapV3OracleWrapper is IUniswapV3OracleWrapper { , , - ) = pool.ticks(positionData.tickUpper); + ) = poolState.ticks(positionData.tickUpper); - uint256 feeGrowthGlobal0X128 = pool.feeGrowthGlobal0X128(); - uint256 feeGrowthGlobal1X128 = pool.feeGrowthGlobal1X128(); + feeParams.feeGrowthGlobal0X128 = poolState.feeGrowthGlobal0X128(); + feeParams.feeGrowthGlobal1X128 = poolState.feeGrowthGlobal1X128(); - unchecked { - // calculate fee growth below - uint256 feeGrowthBelow0X128; - uint256 feeGrowthBelow1X128; - if (positionData.currentTick >= positionData.tickLower) { - feeGrowthBelow0X128 = feeParams.feeGrowthOutside0X128Lower; - feeGrowthBelow1X128 = feeParams.feeGrowthOutside1X128Lower; - } else { - feeGrowthBelow0X128 = - feeGrowthGlobal0X128 - - feeParams.feeGrowthOutside0X128Lower; - feeGrowthBelow1X128 = - feeGrowthGlobal1X128 - - feeParams.feeGrowthOutside1X128Lower; - } - - // calculate fee growth above - uint256 feeGrowthAbove0X128; - uint256 feeGrowthAbove1X128; - if (positionData.currentTick < positionData.tickUpper) { - feeGrowthAbove0X128 = feeParams.feeGrowthOutside0X128Upper; - feeGrowthAbove1X128 = feeParams.feeGrowthOutside1X128Upper; - } else { - feeGrowthAbove0X128 = - feeGrowthGlobal0X128 - - feeParams.feeGrowthOutside0X128Upper; - feeGrowthAbove1X128 = - feeGrowthGlobal1X128 - - feeParams.feeGrowthOutside1X128Upper; - } - uint256 feeGrowthInside0X128; - uint256 feeGrowthInside1X128; - - feeGrowthInside0X128 = - feeGrowthGlobal0X128 - - feeGrowthBelow0X128 - - feeGrowthAbove0X128; - feeGrowthInside1X128 = - feeGrowthGlobal1X128 - - feeGrowthBelow1X128 - - feeGrowthAbove1X128; - - token0Amount = uint128( - FullMath.mulDiv( - feeGrowthInside0X128 - - positionData.feeGrowthInside0LastX128, - positionData.liquidity, - Q128 - ) - ); - - token1Amount = uint128( - FullMath.mulDiv( - feeGrowthInside1X128 - - positionData.feeGrowthInside1LastX128, - positionData.liquidity, - Q128 - ) - ); - } + (token0Amount, token1Amount) = _calculateTokenFee( + positionData, + feeParams + ); } } diff --git a/contracts/mocks/DepositContract.sol b/contracts/mocks/DepositContract.sol index 56ac297f9..e0b96ac31 100644 --- a/contracts/mocks/DepositContract.sol +++ b/contracts/mocks/DepositContract.sol @@ -71,7 +71,7 @@ contract DepositContract is IDepositContract, ERC165 { bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] zero_hashes; - constructor() public { + constructor() { // Compute hashes in empty sparse Merkle tree for ( uint256 height = 0; diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index c10efa7b1..f7ec7eb2b 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -135,4 +135,5 @@ library Errors { string public constant TOKEN_NOT_ALLOW_RESCUE = "140"; // token is not allow rescue string public constant INVALID_PARAMETER = "170"; //invalid parameter + string public constant INVALID_LIQUIDITY = "171"; //liquidity not enough } diff --git a/contracts/protocol/libraries/logic/BorrowLogic.sol b/contracts/protocol/libraries/logic/BorrowLogic.sol index 1759bb7fd..c7f7be79a 100644 --- a/contracts/protocol/libraries/logic/BorrowLogic.sol +++ b/contracts/protocol/libraries/logic/BorrowLogic.sol @@ -73,7 +73,8 @@ library BorrowLogic { amount: params.amount, reservesCount: params.reservesCount, oracle: params.oracle, - priceOracleSentinel: params.priceOracleSentinel + priceOracleSentinel: params.priceOracleSentinel, + verifyCollateral: params.verifyCollateral }) ); diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 9453a47a4..93996a5b2 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -16,9 +16,10 @@ import {PercentageMath} from "../math/PercentageMath.sol"; import {WadRayMath} from "../math/WadRayMath.sol"; import {DataTypes} from "../types/DataTypes.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; -import {INonfungiblePositionManager} from "../../../dependencies/uniswapv3-periphery/interfaces/INonfungiblePositionManager.sol"; import {XTokenType, IXTokenType} from "../../../interfaces/IXTokenType.sol"; +import {INTokenLiquidity} from "../../../interfaces/INTokenLiquidity.sol"; import {Helpers} from "../../libraries/helpers/Helpers.sol"; +import {Errors} from "../helpers/Errors.sol"; /** * @title GenericLogic library @@ -203,12 +204,15 @@ library GenericLogic { ) { vars.xTokenType = INToken(vars.xTokenAddress) .getXTokenType(); - if (vars.xTokenType == XTokenType.NTokenUniswapV3) { + if ( + vars.xTokenType == XTokenType.NTokenUniswapV3 || + vars.xTokenType == XTokenType.NTokenIZUMILp + ) { ( vars.userBalanceInBaseCurrency, vars.ltv, vars.liquidationThreshold - ) = _getUserBalanceForUniswapV3( + ) = _getUserBalanceForLiquidityNFT( reservesData, params, vars @@ -380,27 +384,15 @@ library GenericLogic { totalValue = (collateralizedBalance * avgMultiplier).wadMul(assetPrice); } - function getLtvAndLTForUniswapV3( + function getLtvAndLTForLiquidityNFT( mapping(address => DataTypes.ReserveData) storage reservesData, - address uniswapV3Manager, + address nToken, uint256 tokenId, uint256 collectionLTV, uint256 collectionLiquidationThreshold ) internal view returns (uint256 ltv, uint256 liquidationThreshold) { - ( - , - , - address token0, - address token1, - , - , - , - , - , - , - , - - ) = INonfungiblePositionManager(uniswapV3Manager).positions(tokenId); + (address token0, address token1) = INTokenLiquidity(nToken) + .underlyingAsset(tokenId); DataTypes.ReserveConfigurationMap memory token0Configs = reservesData[ token0 @@ -431,7 +423,7 @@ library GenericLogic { ); } - function _getUserBalanceForUniswapV3( + function _getUserBalanceForLiquidityNFT( mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.CalculateUserAccountDataParams memory params, CalculateUserAccountDataVars memory vars @@ -465,9 +457,9 @@ library GenericLogic { ( uint256 tmpLTV, uint256 tmpLiquidationThreshold - ) = getLtvAndLTForUniswapV3( + ) = getLtvAndLTForLiquidityNFT( reservesData, - vars.currentReserveAddress, + vars.xTokenAddress, tokenId, vars.ltv, vars.liquidationThreshold diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 777c9e9a3..2c278127b 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -764,9 +764,11 @@ library LiquidationLogic { ).collateralizedBalanceOf(params.borrower); // price of the asset that is used as collateral + XTokenType xTokenType = IXTokenType(superVars.collateralXToken) + .getXTokenType(); if ( - IXTokenType(superVars.collateralXToken).getXTokenType() == - XTokenType.NTokenUniswapV3 + xTokenType == XTokenType.NTokenUniswapV3 || + xTokenType == XTokenType.NTokenIZUMILp ) { vars.collateralPrice = IPriceOracleGetter(params.priceOracle) .getTokenPrice( diff --git a/contracts/protocol/libraries/logic/MarketplaceLogic.sol b/contracts/protocol/libraries/logic/MarketplaceLogic.sol index e8ce12464..9ad2e4a88 100644 --- a/contracts/protocol/libraries/logic/MarketplaceLogic.sol +++ b/contracts/protocol/libraries/logic/MarketplaceLogic.sol @@ -25,6 +25,7 @@ import {UserConfiguration} from "../configuration/UserConfiguration.sol"; import {ReserveConfiguration} from "../configuration/ReserveConfiguration.sol"; import {IMarketplace} from "../../../interfaces/IMarketplace.sol"; import {Address} from "../../../dependencies/openzeppelin/contracts/Address.sol"; +import {IXTokenType, XTokenType} from "../../../interfaces/IXTokenType.sol"; /** * @title Marketplace library @@ -542,9 +543,10 @@ library MarketplaceLogic { token = underlyingAsset; } + XTokenType xTokenType = INToken(vars.xTokenAddress).getXTokenType(); require( - INToken(vars.xTokenAddress).getXTokenType() != - XTokenType.NTokenUniswapV3, + xTokenType != XTokenType.NTokenUniswapV3 && + xTokenType != XTokenType.NTokenIZUMILp, Errors.XTOKEN_TYPE_NOT_ALLOWED ); @@ -588,7 +590,8 @@ library MarketplaceLogic { releaseUnderlying: false, reservesCount: params.reservesCount, oracle: params.oracle, - priceOracleSentinel: params.priceOracleSentinel + priceOracleSentinel: params.priceOracleSentinel, + verifyCollateral: true }) ); } diff --git a/contracts/protocol/libraries/logic/PoolLogic.sol b/contracts/protocol/libraries/logic/PoolLogic.sol index 8d6bea6e5..4b6bde8a1 100644 --- a/contracts/protocol/libraries/logic/PoolLogic.sol +++ b/contracts/protocol/libraries/logic/PoolLogic.sol @@ -229,11 +229,14 @@ library PoolLogic { .getParams(); XTokenType tokenType = IXTokenType(assetReserve.xTokenAddress) .getXTokenType(); - if (tokenType == XTokenType.NTokenUniswapV3) { + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { return - GenericLogic.getLtvAndLTForUniswapV3( + GenericLogic.getLtvAndLTForLiquidityNFT( ps._reserves, - asset, + assetReserve.xTokenAddress, tokenId, collectionLtv, collectionLT diff --git a/contracts/protocol/libraries/logic/PositionMoverLogic.sol b/contracts/protocol/libraries/logic/PositionMoverLogic.sol index d73f39491..69f975808 100644 --- a/contracts/protocol/libraries/logic/PositionMoverLogic.sol +++ b/contracts/protocol/libraries/logic/PositionMoverLogic.sol @@ -178,7 +178,8 @@ library PositionMoverLogic { reservesCount: ps._reservesCount, oracle: poolAddressProvider.getPriceOracle(), priceOracleSentinel: poolAddressProvider - .getPriceOracleSentinel() + .getPriceOracleSentinel(), + verifyCollateral: true }) ); } @@ -401,7 +402,8 @@ library PositionMoverLogic { releaseUnderlying: false, reservesCount: params.reservesCount, oracle: params.priceOracle, - priceOracleSentinel: params.priceOracleSentinel + priceOracleSentinel: params.priceOracleSentinel, + verifyCollateral: true }) ); } diff --git a/contracts/protocol/libraries/logic/SupplyExtendedLogic.sol b/contracts/protocol/libraries/logic/SupplyExtendedLogic.sol index 000658919..d9a6cac5d 100644 --- a/contracts/protocol/libraries/logic/SupplyExtendedLogic.sol +++ b/contracts/protocol/libraries/logic/SupplyExtendedLogic.sol @@ -115,7 +115,6 @@ library SupplyExtendedLogic { ValidationLogic.validateTransferERC721( reservesData, reserve, - params.asset, params.tokenId ); @@ -235,7 +234,6 @@ library SupplyExtendedLogic { ValidationLogic.validateSetUseERC721AsCollateral( reservesData, reserveCache, - asset, tokenIds ); @@ -281,7 +279,6 @@ library SupplyExtendedLogic { ValidationLogic.validateSetUseERC721AsCollateral( reservesData, reserveCache, - asset, tokenIds ); diff --git a/contracts/protocol/libraries/logic/SupplyLogic.sol b/contracts/protocol/libraries/logic/SupplyLogic.sol index 50d7f1c52..7313f0dc6 100644 --- a/contracts/protocol/libraries/logic/SupplyLogic.sol +++ b/contracts/protocol/libraries/logic/SupplyLogic.sol @@ -5,7 +5,6 @@ import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; import {IERC721} from "../../../dependencies/openzeppelin/contracts/IERC721.sol"; import {GPv2SafeERC20} from "../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol"; import {IPToken} from "../../../interfaces/IPToken.sol"; -import {INonfungiblePositionManager} from "../../../dependencies/uniswapv3-periphery/interfaces/INonfungiblePositionManager.sol"; import {INToken} from "../../../interfaces/INToken.sol"; import {INTokenApeStaking} from "../../../interfaces/INTokenApeStaking.sol"; import {ICollateralizableERC721} from "../../../interfaces/ICollateralizableERC721.sol"; @@ -19,12 +18,13 @@ import {PercentageMath} from "../math/PercentageMath.sol"; import {ValidationLogic} from "./ValidationLogic.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; import {XTokenType} from "../../../interfaces/IXTokenType.sol"; -import {INTokenUniswapV3} from "../../../interfaces/INTokenUniswapV3.sol"; +import {INTokenLiquidity} from "../../../interfaces/INTokenLiquidity.sol"; import {INTokenStakefish} from "../../../interfaces/INTokenStakefish.sol"; import {GenericLogic} from "./GenericLogic.sol"; import {IStakefishNFTManager} from "../../../interfaces/IStakefishNFTManager.sol"; import {IStakefishValidator} from "../../../interfaces/IStakefishValidator.sol"; import {Helpers} from "../helpers/Helpers.sol"; +import {IPriceOracleGetter} from "../../../interfaces/IPriceOracleGetter.sol"; /** * @title SupplyLogic library @@ -193,11 +193,14 @@ library SupplyLogic { XTokenType tokenType = INToken(reserveCache.xTokenAddress) .getXTokenType(); - if (tokenType == XTokenType.NTokenUniswapV3) { + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { for (uint256 index = 0; index < params.tokenData.length; index++) { - ValidationLogic.validateForUniswapV3( + ValidationLogic.validateForLiquidityNFT( reservesData, - params.asset, + reserveCache.xTokenAddress, params.tokenData[index].tokenId, true, true, @@ -398,7 +401,6 @@ library SupplyLogic { ValidationLogic.validateWithdrawERC721( reservesData, reserveCache, - params.asset, params.tokenIds ); uint256 amountToWithdraw = params.tokenIds.length; @@ -445,13 +447,30 @@ library SupplyLogic { DataTypes.ReserveData storage reserve, DataTypes.ExecuteWithdrawERC721Params memory params ) internal returns (uint64, uint64) { + uint256 amount = 0; + INToken nToken = INToken(xTokenAddress); + XTokenType tokenType = nToken.getXTokenType(); + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { + uint256 tokenIdLength = params.tokenIds.length; + for (uint256 index = 0; index < tokenIdLength; index++) { + amount += IPriceOracleGetter(params.oracle).getTokenPrice( + params.asset, + params.tokenIds[index] + ); + } + } else { + amount = params.tokenIds.length; + } DataTypes.TimeLockParams memory timeLockParams = GenericLogic .calculateTimeLockParams( reserve, DataTypes.TimeLockFactorParams({ assetType: DataTypes.AssetType.ERC721, asset: params.asset, - amount: params.tokenIds.length + amount: amount }) ); timeLockParams.actionType = DataTypes.TimeLockActionType.WITHDRAW; @@ -464,58 +483,4 @@ library SupplyLogic { timeLockParams ); } - - function executeDecreaseUniswapV3Liquidity( - mapping(address => DataTypes.ReserveData) storage reservesData, - mapping(uint256 => address) storage reservesList, - DataTypes.UserConfigurationMap storage userConfig, - DataTypes.ExecuteDecreaseUniswapV3LiquidityParams memory params - ) external { - DataTypes.ReserveData storage reserve = reservesData[params.asset]; - DataTypes.ReserveCache memory reserveCache = reserve.cache(); - - //currently don't need to update state for erc721 - //reserve.updateState(reserveCache); - INToken nToken = INToken(reserveCache.xTokenAddress); - require( - nToken.getXTokenType() == XTokenType.NTokenUniswapV3, - Errors.XTOKEN_TYPE_NOT_ALLOWED - ); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = params.tokenId; - ValidationLogic.validateWithdrawERC721( - reservesData, - reserveCache, - params.asset, - tokenIds - ); - - INTokenUniswapV3(reserveCache.xTokenAddress).decreaseUniswapV3Liquidity( - params.user, - params.tokenId, - params.liquidityDecrease, - params.amount0Min, - params.amount1Min, - params.receiveEthAsWeth - ); - - bool isUsedAsCollateral = ICollateralizableERC721( - reserveCache.xTokenAddress - ).isUsedAsCollateral(params.tokenId); - if (isUsedAsCollateral) { - if (userConfig.isBorrowingAny()) { - ValidationLogic.validateHFAndLtvERC721( - reservesData, - reservesList, - userConfig, - params.asset, - tokenIds, - params.user, - params.reservesCount, - params.oracle - ); - } - } - } } diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 94c1c0362..5ed1c4619 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -26,8 +26,8 @@ import {SafeCast} from "../../../dependencies/openzeppelin/contracts/SafeCast.so import {IToken} from "../../../interfaces/IToken.sol"; import {XTokenType, IXTokenType} from "../../../interfaces/IXTokenType.sol"; import {Helpers} from "../helpers/Helpers.sol"; -import {INonfungiblePositionManager} from "../../../dependencies/uniswapv3-periphery/interfaces/INonfungiblePositionManager.sol"; import "../../../interfaces/INTokenApeStaking.sol"; +import "../../../interfaces/INTokenLiquidity.sol"; /** * @title ReserveLogic library @@ -186,10 +186,27 @@ library ValidationLogic { require(!isPaused, Errors.RESERVE_PAUSED); } + function validateAssetStatus( + DataTypes.ReserveConfigurationMap memory reserveConfiguration, + DataTypes.AssetType assetType + ) internal pure { + ( + bool isActive, + bool isFrozen, + , + bool isPaused, + DataTypes.AssetType reserveAssetType + ) = reserveConfiguration.getFlags(); + + require(reserveAssetType == assetType, Errors.INVALID_ASSET_TYPE); + require(isActive, Errors.RESERVE_INACTIVE); + require(!isPaused, Errors.RESERVE_PAUSED); + require(!isFrozen, Errors.RESERVE_FROZEN); + } + function validateWithdrawERC721( mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.ReserveCache memory reserveCache, - address asset, uint256[] memory tokenIds ) internal view { ( @@ -208,11 +225,15 @@ library ValidationLogic { require(!isPaused, Errors.RESERVE_PAUSED); INToken nToken = INToken(reserveCache.xTokenAddress); - if (nToken.getXTokenType() == XTokenType.NTokenUniswapV3) { + XTokenType tokenType = nToken.getXTokenType(); + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { for (uint256 index = 0; index < tokenIds.length; index++) { - ValidationLogic.validateForUniswapV3( + ValidationLogic.validateForLiquidityNFT( reservesData, - asset, + reserveCache.xTokenAddress, tokenIds[index], true, true, @@ -310,6 +331,10 @@ library ValidationLogic { } } + if (!params.verifyCollateral) { + return; + } + ( vars.userCollateralInBaseCurrency, , @@ -442,7 +467,6 @@ library ValidationLogic { function validateSetUseERC721AsCollateral( mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.ReserveCache memory reserveCache, - address asset, uint256[] calldata tokenIds ) internal view { ( @@ -461,11 +485,15 @@ library ValidationLogic { require(!isPaused, Errors.RESERVE_PAUSED); INToken nToken = INToken(reserveCache.xTokenAddress); - if (nToken.getXTokenType() == XTokenType.NTokenUniswapV3) { + XTokenType tokenType = nToken.getXTokenType(); + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { for (uint256 index = 0; index < tokenIds.length; index++) { - ValidationLogic.validateForUniswapV3( + ValidationLogic.validateForLiquidityNFT( reservesData, - asset, + reserveCache.xTokenAddress, tokenIds[index], true, true, @@ -614,10 +642,14 @@ library ValidationLogic { ); INToken nToken = INToken(collateralReserve.xTokenAddress); - if (nToken.getXTokenType() == XTokenType.NTokenUniswapV3) { - ValidationLogic.validateForUniswapV3( + XTokenType tokenType = nToken.getXTokenType(); + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { + ValidationLogic.validateForLiquidityNFT( reservesData, - params.collateralAsset, + address(nToken), params.tokenId, true, true, @@ -911,15 +943,20 @@ library ValidationLogic { if (hasZeroLtvCollateral) { INToken nToken = INToken(reserve.xTokenAddress); - if (nToken.getXTokenType() == XTokenType.NTokenUniswapV3) { + XTokenType tokenType = nToken.getXTokenType(); + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { for (uint256 index = 0; index < tokenIds.length; index++) { - (uint256 assetLTV, ) = GenericLogic.getLtvAndLTForUniswapV3( - reservesData, - asset, - tokenIds[index], - reserve.configuration.getLtv(), - 0 - ); + (uint256 assetLTV, ) = GenericLogic + .getLtvAndLTForLiquidityNFT( + reservesData, + address(nToken), + tokenIds[index], + reserve.configuration.getLtv(), + 0 + ); require(assetLTV == 0, Errors.LTV_VALIDATION_FAILED); } } else { @@ -948,15 +985,18 @@ library ValidationLogic { function validateTransferERC721( mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.ReserveData storage reserve, - address asset, uint256 tokenId ) internal view { require(!reserve.configuration.getPaused(), Errors.RESERVE_PAUSED); INToken nToken = INToken(reserve.xTokenAddress); - if (nToken.getXTokenType() == XTokenType.NTokenUniswapV3) { - ValidationLogic.validateForUniswapV3( + XTokenType tokenType = nToken.getXTokenType(); + if ( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp + ) { + ValidationLogic.validateForLiquidityNFT( reservesData, - asset, + address(nToken), tokenId, false, true, @@ -1010,6 +1050,7 @@ library ValidationLogic { XTokenType tokenType = nToken.getXTokenType(); require( tokenType != XTokenType.NTokenUniswapV3 && + tokenType != XTokenType.NTokenIZUMILp && tokenType != XTokenType.NTokenStakefish, Errors.FLASHCLAIM_NOT_ALLOWED ); @@ -1141,7 +1182,7 @@ library ValidationLogic { ); } - struct ValidateForUniswapV3LocalVars { + struct ValidateForLiquidityNFTLocalVars { bool token0IsActive; bool token0IsFrozen; bool token0IsPaused; @@ -1150,30 +1191,18 @@ library ValidationLogic { bool token1IsPaused; } - function validateForUniswapV3( + function validateForLiquidityNFT( mapping(address => DataTypes.ReserveData) storage reservesData, - address asset, + address nToken, uint256 tokenId, bool checkActive, bool checkNotPaused, bool checkNotFrozen ) internal view { - ( - , - , - address token0, - address token1, - , - , - , - , - , - , - , - - ) = INonfungiblePositionManager(asset).positions(tokenId); + (address token0, address token1) = INTokenLiquidity(nToken) + .underlyingAsset(tokenId); - ValidateForUniswapV3LocalVars memory vars; + ValidateForLiquidityNFTLocalVars memory vars; ( vars.token0IsActive, vars.token0IsFrozen, @@ -1209,4 +1238,35 @@ library ValidationLogic { ); } } + + function validateAddLiquidity( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.ReserveCache memory reserveCache, + uint256 tokenId + ) internal view { + INToken nToken = INToken(reserveCache.xTokenAddress); + XTokenType tokenType = nToken.getXTokenType(); + require( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp, + Errors.XTOKEN_TYPE_NOT_ALLOWED + ); + + (bool isActive, bool isFrozen, , bool isPaused, ) = reserveCache + .reserveConfiguration + .getFlags(); + + require(isActive, Errors.RESERVE_INACTIVE); + require(!isPaused, Errors.RESERVE_PAUSED); + require(!isFrozen, Errors.RESERVE_FROZEN); + + ValidationLogic.validateForLiquidityNFT( + reservesData, + reserveCache.xTokenAddress, + tokenId, + true, + true, + true + ); + } } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index 0da8daba0..1b8604027 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -170,6 +170,7 @@ library DataTypes { uint256 reservesCount; address oracle; address priceOracleSentinel; + bool verifyCollateral; } struct ExecuteRepayParams { @@ -196,7 +197,7 @@ library DataTypes { address oracle; } - struct ExecuteDecreaseUniswapV3LiquidityParams { + struct ExecuteDecreaseLiquidityParams { address user; address asset; uint256 tokenId; @@ -204,8 +205,6 @@ library DataTypes { uint128 liquidityDecrease; uint256 amount0Min; uint256 amount1Min; - bool receiveEthAsWeth; - address oracle; } struct FinalizeTransferParams { @@ -247,6 +246,7 @@ library DataTypes { uint256 reservesCount; address oracle; address priceOracleSentinel; + bool verifyCollateral; } struct ValidateLiquidateERC20Params { @@ -458,4 +458,35 @@ library DataTypes { address priceOracleSentinel; uint256 reservesCount; } + + struct DecreaseLiquidityParam { + bool decreaseLiquidity; + uint256 tokenId; + uint128 liquidityDecrease; + uint256 amount0Min; + uint256 amount1Min; + bool burnNFT; + } + + struct MintParams { + bool mintNewToken; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct AssetInfo { + address asset; + address token0; + address token1; + uint256 token0CashAmount; + uint256 token1CashAmount; + uint256 token0BorrowAmount; + uint256 token1BorrowAmount; + } } diff --git a/contracts/protocol/pool/PoolApeStaking.sol b/contracts/protocol/pool/PoolApeStaking.sol index 487cc19c2..90938a7d7 100644 --- a/contracts/protocol/pool/PoolApeStaking.sol +++ b/contracts/protocol/pool/PoolApeStaking.sol @@ -380,7 +380,8 @@ contract PoolApeStaking is reservesCount: ps._reservesCount, oracle: ADDRESSES_PROVIDER.getPriceOracle(), priceOracleSentinel: ADDRESSES_PROVIDER - .getPriceOracleSentinel() + .getPriceOracleSentinel(), + verifyCollateral: true }) ); } diff --git a/contracts/protocol/pool/PoolBorrowAndStake.sol b/contracts/protocol/pool/PoolBorrowAndStake.sol index 1af514995..c4252b6e8 100644 --- a/contracts/protocol/pool/PoolBorrowAndStake.sol +++ b/contracts/protocol/pool/PoolBorrowAndStake.sol @@ -263,7 +263,8 @@ contract PoolBorrowAndStake is reservesCount: ps._reservesCount, oracle: ADDRESSES_PROVIDER.getPriceOracle(), priceOracleSentinel: ADDRESSES_PROVIDER - .getPriceOracleSentinel() + .getPriceOracleSentinel(), + verifyCollateral: true }) ); } diff --git a/contracts/protocol/pool/PoolCore.sol b/contracts/protocol/pool/PoolCore.sol index c664e09ab..3a1273761 100644 --- a/contracts/protocol/pool/PoolCore.sol +++ b/contracts/protocol/pool/PoolCore.sol @@ -236,35 +236,6 @@ contract PoolCore is ); } - function decreaseUniswapV3Liquidity( - address asset, - uint256 tokenId, - uint128 liquidityDecrease, - uint256 amount0Min, - uint256 amount1Min, - bool receiveEthAsWeth - ) external virtual override nonReentrant { - DataTypes.PoolStorage storage ps = poolStorage(); - - return - SupplyLogic.executeDecreaseUniswapV3Liquidity( - ps._reserves, - ps._reservesList, - ps._usersConfig[msg.sender], - DataTypes.ExecuteDecreaseUniswapV3LiquidityParams({ - user: msg.sender, - asset: asset, - tokenId: tokenId, - reservesCount: ps._reservesCount, - liquidityDecrease: liquidityDecrease, - amount0Min: amount0Min, - amount1Min: amount1Min, - receiveEthAsWeth: receiveEthAsWeth, - oracle: ADDRESSES_PROVIDER.getPriceOracle() - }) - ); - } - /// @inheritdoc IPoolCore function borrow( address asset, @@ -287,7 +258,9 @@ contract PoolCore is releaseUnderlying: true, reservesCount: ps._reservesCount, oracle: ADDRESSES_PROVIDER.getPriceOracle(), - priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel() + priceOracleSentinel: ADDRESSES_PROVIDER + .getPriceOracleSentinel(), + verifyCollateral: true }) ); } diff --git a/contracts/protocol/pool/PoolLpOperation.sol b/contracts/protocol/pool/PoolLpOperation.sol new file mode 100644 index 000000000..1781df3eb --- /dev/null +++ b/contracts/protocol/pool/PoolLpOperation.sol @@ -0,0 +1,548 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ParaVersionedInitializable} from "../libraries/paraspace-upgradeability/ParaVersionedInitializable.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {ReserveConfiguration} from "../libraries/configuration/ReserveConfiguration.sol"; +import {ParaReentrancyGuard} from "../libraries/paraspace-upgradeability/ParaReentrancyGuard.sol"; +import {PoolStorage} from "./PoolStorage.sol"; +import {DataTypes} from "../libraries/types/DataTypes.sol"; +import {INToken} from "../../interfaces/INToken.sol"; +import {INTokenLiquidity} from "../../interfaces/INTokenLiquidity.sol"; +import {XTokenType} from "../../interfaces/IXTokenType.sol"; +import {ILiquidityManager} from "../../dependencies/izumi/izumi-swap-periphery/interfaces/ILiquidityManager.sol"; +import {INonfungiblePositionManager} from "../../dependencies/uniswapv3-periphery/interfaces/INonfungiblePositionManager.sol"; +import {ValidationLogic} from "../libraries/logic/ValidationLogic.sol"; +import {BorrowLogic} from "../libraries/logic/BorrowLogic.sol"; +import {SupplyLogic} from "../libraries/logic/SupplyLogic.sol"; +import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IPToken} from "../../interfaces/IPToken.sol"; +import {ReserveLogic} from "../libraries/logic/ReserveLogic.sol"; +import {ReserveConfiguration} from "../libraries/configuration/ReserveConfiguration.sol"; +import {SafeCast} from "../../dependencies/openzeppelin/contracts/SafeCast.sol"; +import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {UserConfiguration} from "../libraries/configuration/UserConfiguration.sol"; +import {GenericLogic} from "../libraries/logic/GenericLogic.sol"; +import {Math} from "../../dependencies/openzeppelin/contracts/Math.sol"; +import {Helpers} from "../libraries/helpers/Helpers.sol"; +import "../../interfaces/IPoolLpOperation.sol"; + +contract PoolLpOperation is + ParaVersionedInitializable, + ParaReentrancyGuard, + PoolStorage, + IPoolLpOperation +{ + using SafeCast for uint256; + using SafeERC20 for IERC20; + using ReserveLogic for DataTypes.ReserveData; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using UserConfiguration for DataTypes.UserConfigurationMap; + + event ReserveUsedAsCollateralDisabled( + address indexed reserve, + address indexed user + ); + + event SupplyERC721( + address indexed reserve, + address user, + address indexed onBehalfOf, + DataTypes.ERC721SupplyParams[] tokenData, + uint16 indexed referralCode, + bool fromNToken + ); + + uint256 internal constant POOL_REVISION = 200; + IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; + + /** + * @dev Constructor. + * @param provider The address of the PoolAddressesProvider contract + */ + constructor(IPoolAddressesProvider provider) { + ADDRESSES_PROVIDER = provider; + } + + function getRevision() internal pure virtual override returns (uint256) { + return POOL_REVISION; + } + + struct LpPositionVars { + address token0XToken; + address token1XToken; + uint256 token0Before; + uint256 token1Before; + uint256 token0After; + uint256 token1After; + uint256 token0Left; + uint256 token1Left; + uint256 token0RefundBorrow; + uint256 token1RefundBorrow; + uint256 token0Borrow; + uint256 token1Borrow; + uint256 token0RefundCash; + uint256 token1RefundCash; + uint256 token0RefundLiquidity; + uint256 token1RefundLiquidity; + } + + /// @inheritdoc IPoolLpOperation + function increaseLiquidity( + address asset, + uint256 tokenId, + uint256 amountAdd0, + uint256 amountAdd1, + uint256 amount0Min, + uint256 amount1Min, + uint256 deadline + ) external payable virtual override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + DataTypes.ReserveData storage reserve = ps._reserves[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + ValidationLogic.validateAddLiquidity( + ps._reserves, + reserveCache, + tokenId + ); + + INTokenLiquidity(reserveCache.xTokenAddress).increaseLiquidity{ + value: msg.value + }( + msg.sender, + tokenId, + amountAdd0, + amountAdd1, + amount0Min, + amount1Min, + deadline + ); + } + + /// @inheritdoc IPoolLpOperation + function adjustLpPosition( + DataTypes.AssetInfo calldata assetInfo, + DataTypes.DecreaseLiquidityParam calldata decreaseLiquidityParam, + DataTypes.MintParams calldata mintParams + ) external virtual override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + DataTypes.ReserveData storage reserve = ps._reserves[assetInfo.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + //currently don't need to update state for erc721 + //reserve.updateState(reserveCache); + XTokenType tokenType = INToken(reserveCache.xTokenAddress) + .getXTokenType(); + require( + tokenType == XTokenType.NTokenUniswapV3 || + tokenType == XTokenType.NTokenIZUMILp, + Errors.XTOKEN_TYPE_NOT_ALLOWED + ); + + //check asset status + ValidationLogic.validateAssetStatus( + reserveCache.reserveConfiguration, + DataTypes.AssetType.ERC721 + ); + //check underlying asset status + ValidationLogic.validateAssetStatus( + ps._reserves[assetInfo.token0].configuration, + DataTypes.AssetType.ERC20 + ); + ValidationLogic.validateAssetStatus( + ps._reserves[assetInfo.token1].configuration, + DataTypes.AssetType.ERC20 + ); + + LpPositionVars memory vars; + vars.token0Before = IERC20(assetInfo.token0).balanceOf(address(this)); + vars.token1Before = IERC20(assetInfo.token1).balanceOf(address(this)); + if (assetInfo.token0BorrowAmount > 0) { + vars.token0XToken = ps._reserves[assetInfo.token0].xTokenAddress; + } + if (assetInfo.token1BorrowAmount > 0) { + vars.token1XToken = ps._reserves[assetInfo.token1].xTokenAddress; + } + + //handle cash and borrow part asset + _transferInAsset( + assetInfo.token0, + msg.sender, + assetInfo.token0CashAmount + ); + _transferInAsset( + assetInfo.token1, + msg.sender, + assetInfo.token1CashAmount + ); + _flashBorrowAsset(vars.token0XToken, assetInfo.token0BorrowAmount); + _flashBorrowAsset(vars.token1XToken, assetInfo.token1BorrowAmount); + + //decrease liquidity + if (decreaseLiquidityParam.decreaseLiquidity) { + ( + address token0, + address token1, + , + , + bool isBurned + ) = INTokenLiquidity(reserveCache.xTokenAddress).decreaseLiquidity( + msg.sender, //check owner here + decreaseLiquidityParam.tokenId, + decreaseLiquidityParam.liquidityDecrease, + decreaseLiquidityParam.amount0Min, + decreaseLiquidityParam.amount1Min + ); + require(token0 == assetInfo.token0, Errors.INVALID_PARAMETER); + require(token1 == assetInfo.token1, Errors.INVALID_PARAMETER); + require( + decreaseLiquidityParam.burnNFT == isBurned, + Errors.INVALID_PARAMETER + ); + if (isBurned) { + DataTypes.TimeLockParams memory timeLockParams; + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = decreaseLiquidityParam.tokenId; + ( + uint256 oldCollateralizedBalance, + uint256 newCollateralizedBalance + ) = INToken(reserveCache.xTokenAddress).burn( + msg.sender, + reserveCache.xTokenAddress, + tokenIds, + timeLockParams + ); + if (!mintParams.mintNewToken) { + if ( + oldCollateralizedBalance > 0 && + newCollateralizedBalance == 0 + ) { + ps._usersConfig[msg.sender].setUsingAsCollateral( + reserve.id, + false + ); + emit ReserveUsedAsCollateralDisabled( + assetInfo.asset, + msg.sender + ); + } + } + } + } + + //mint and supply new nft + if (mintParams.mintNewToken) { + require( + mintParams.amount0Desired > 0 || mintParams.amount1Desired > 0, + Errors.INVALID_PARAMETER + ); + if (mintParams.amount0Desired > 0) { + IERC20(assetInfo.token0).safeApprove( + assetInfo.asset, + mintParams.amount0Desired + ); + } + if (mintParams.amount1Desired > 0) { + IERC20(assetInfo.token1).safeApprove( + assetInfo.asset, + mintParams.amount1Desired + ); + } + uint256 newTokenId; + if (tokenType == XTokenType.NTokenUniswapV3) { + INonfungiblePositionManager.MintParams + memory params = INonfungiblePositionManager.MintParams({ + token0: assetInfo.token0, + token1: assetInfo.token1, + fee: mintParams.fee, + tickLower: mintParams.tickLower, + tickUpper: mintParams.tickUpper, + amount0Desired: mintParams.amount0Desired, + amount1Desired: mintParams.amount1Desired, + amount0Min: mintParams.amount0Min, + amount1Min: mintParams.amount1Min, + recipient: reserveCache.xTokenAddress, + deadline: mintParams.deadline + }); + (newTokenId, , , ) = INonfungiblePositionManager( + assetInfo.asset + ).mint(params); + } else { + // izumi + ILiquidityManager.MintParam memory params = ILiquidityManager + .MintParam({ + miner: reserveCache.xTokenAddress, + tokenX: assetInfo.token0, + tokenY: assetInfo.token1, + fee: mintParams.fee, + pl: mintParams.tickLower, + pr: mintParams.tickUpper, + xLim: mintParams.amount0Desired.toUint128(), + yLim: mintParams.amount1Desired.toUint128(), + amountXMin: mintParams.amount0Min.toUint128(), + amountYMin: mintParams.amount1Min.toUint128(), + deadline: mintParams.deadline + }); + (newTokenId, , , ) = ILiquidityManager(assetInfo.asset).mint( + params + ); + } + if (mintParams.amount0Desired > 0) { + IERC20(assetInfo.token0).safeApprove(assetInfo.asset, 0); + } + if (mintParams.amount1Desired > 0) { + IERC20(assetInfo.token1).safeApprove(assetInfo.asset, 0); + } + + //supply new mint nft + { + DataTypes.ERC721SupplyParams[] + memory tokenData = new DataTypes.ERC721SupplyParams[](1); + tokenData[0] = DataTypes.ERC721SupplyParams({ + tokenId: newTokenId, + useAsCollateral: true + }); + ( + uint64 oldCollateralizedBalance, + uint64 newCollateralizedBalance + ) = INToken(reserveCache.xTokenAddress).mint( + msg.sender, + tokenData + ); + bool isFirstSupplyCollateral = (oldCollateralizedBalance == 0 && + newCollateralizedBalance > 0); + if (isFirstSupplyCollateral) { + Helpers.setAssetUsedAsCollateral( + ps._usersConfig[msg.sender], + ps._reserves, + assetInfo.asset, + msg.sender + ); + } + + uint256 supplyCap = reserveCache + .reserveConfiguration + .getSupplyCap(); + require( + supplyCap == 0 || + (INToken(reserveCache.xTokenAddress).totalSupply() <= + supplyCap), + Errors.SUPPLY_CAP_EXCEEDED + ); + + emit SupplyERC721( + assetInfo.asset, + msg.sender, + msg.sender, + tokenData, + 0, + false + ); + } + } + + vars.token0After = IERC20(assetInfo.token0).balanceOf(address(this)); + vars.token1After = IERC20(assetInfo.token1).balanceOf(address(this)); + require( + vars.token0After >= vars.token0Before, + Errors.INVALID_PARAMETER + ); + require( + vars.token1After >= vars.token1Before, + Errors.INVALID_PARAMETER + ); + + //calculate refund borrow + vars.token0Left = vars.token0After - vars.token0Before; + vars.token1Left = vars.token1After - vars.token1Before; + if (vars.token0Left > 0) { + vars.token0RefundBorrow = Math.min( + vars.token0Left, + assetInfo.token0BorrowAmount + ); + + //calculate refund cash + vars.token0Left = vars.token0Left - vars.token0RefundBorrow; + if (vars.token0Left > 0) { + vars.token0RefundCash = Math.min( + vars.token0Left, + assetInfo.token0CashAmount + ); + + //calculate refund decreaseLiquidity + vars.token0RefundLiquidity = + vars.token0Left - + vars.token0RefundCash; + } + } + if (vars.token1Left > 0) { + vars.token1RefundBorrow = Math.min( + vars.token1Left, + assetInfo.token1BorrowAmount + ); + + vars.token1Left = vars.token1Left - vars.token1RefundBorrow; + if (vars.token1Left > 0) { + vars.token1RefundCash = Math.min( + vars.token1Left, + assetInfo.token1CashAmount + ); + vars.token1RefundLiquidity = + vars.token1Left - + vars.token1RefundCash; + } + } + vars.token0Borrow = + assetInfo.token0BorrowAmount - + vars.token0RefundBorrow; + vars.token1Borrow = + assetInfo.token1BorrowAmount - + vars.token1RefundBorrow; + + //do refund + { + //refund borrow + _refundAsset( + assetInfo.token0, + vars.token0XToken, + vars.token0RefundBorrow + ); + _refundAsset( + assetInfo.token1, + vars.token1XToken, + vars.token1RefundBorrow + ); + + //refund cash + _refundAsset(assetInfo.token0, msg.sender, vars.token0RefundCash); + _refundAsset(assetInfo.token1, msg.sender, vars.token1RefundCash); + + //refund decreaseLiquidity + _supplyLiquidity( + ps._reserves, + ps._usersConfig[msg.sender], + msg.sender, + assetInfo.token0, + vars.token0RefundLiquidity + ); + _supplyLiquidity( + ps._reserves, + ps._usersConfig[msg.sender], + msg.sender, + assetInfo.token1, + vars.token1RefundLiquidity + ); + } + + //execute borrow + if (vars.token0Borrow > 0) { + _executeBorrow( + ps, + msg.sender, + assetInfo.token0, + vars.token0Borrow, + !(vars.token1Borrow > 0) + ); + } + if (vars.token1Borrow > 0) { + _executeBorrow( + ps, + msg.sender, + assetInfo.token1, + vars.token1Borrow, + true + ); + } + } + + function _transferInAsset( + address asset, + address user, + uint256 amount + ) internal { + if (amount > 0) { + IERC20(asset).safeTransferFrom(user, address(this), amount); + } + } + + function _flashBorrowAsset(address assetPToken, uint256 amount) internal { + if (amount > 0) { + DataTypes.TimeLockParams memory timeLockParams; + IPToken(assetPToken).transferUnderlyingTo( + address(this), + amount, + timeLockParams + ); + } + } + + function _executeBorrow( + DataTypes.PoolStorage storage ps, + address user, + address asset, + uint256 amount, + bool verifyCollateral + ) internal { + BorrowLogic.executeBorrow( + ps._reserves, + ps._reservesList, + ps._usersConfig[user], + DataTypes.ExecuteBorrowParams({ + asset: asset, + user: user, + onBehalfOf: user, + amount: amount, + referralCode: 0, + releaseUnderlying: false, + reservesCount: ps._reservesCount, + oracle: ADDRESSES_PROVIDER.getPriceOracle(), + priceOracleSentinel: ADDRESSES_PROVIDER + .getPriceOracleSentinel(), + verifyCollateral: verifyCollateral + }) + ); + } + + function _refundAsset( + address asset, + address user, + uint256 amount + ) internal { + if (amount > 0) { + IERC20(asset).safeTransfer(user, amount); + } + } + + function _supplyLiquidity( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap storage userConfig, + address user, + address asset, + uint256 amount + ) internal { + if (amount > 0) { + SupplyLogic.executeSupply( + reservesData, + userConfig, + DataTypes.ExecuteSupplyParams({ + asset: asset, + amount: amount, + onBehalfOf: user, + payer: address(this), + referralCode: 0 + }) + ); + + //for simplicity, we don't check where the erc20 came from and just set it as collateral + Helpers.setAssetUsedAsCollateral( + userConfig, + reservesData, + asset, + user + ); + } + } +} diff --git a/contracts/protocol/tokenization/NTokenIzumi.sol b/contracts/protocol/tokenization/NTokenIzumi.sol new file mode 100644 index 000000000..31f5d4635 --- /dev/null +++ b/contracts/protocol/tokenization/NTokenIzumi.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC721} from "../../dependencies/openzeppelin/contracts/IERC721.sol"; +import {IERC1155} from "../../dependencies/openzeppelin/contracts/IERC1155.sol"; +import {IERC721Metadata} from "../../dependencies/openzeppelin/contracts/IERC721Metadata.sol"; +import {Address} from "../../dependencies/openzeppelin/contracts/Address.sol"; +import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {SafeCast} from "../../dependencies/openzeppelin/contracts/SafeCast.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {WadRayMath} from "../libraries/math/WadRayMath.sol"; +import {IPool} from "../../interfaces/IPool.sol"; +import {NToken} from "./NToken.sol"; +import {DataTypes} from "../libraries/types/DataTypes.sol"; +import {ILiquidityManager} from "../../dependencies/izumi/izumi-swap-periphery/interfaces/ILiquidityManager.sol"; +import {IBase} from "../../dependencies/izumi/izumi-swap-periphery/interfaces/IBase.sol"; +import {IWETH} from "../../misc/interfaces/IWETH.sol"; +import {Helpers} from "../../protocol/libraries/helpers/Helpers.sol"; +import {NTokenLiquidity} from "./NTokenLiquidity.sol"; +import {XTokenType} from "../../interfaces/IXTokenType.sol"; + +/** + * @title IZUMI NToken + * + * @notice Implementation of the interest bearing token for the ParaSpace protocol + */ +contract NTokenIzumi is NTokenLiquidity { + using SafeERC20 for IERC20; + using SafeCast for uint256; + + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor( + IPool pool, + address delegateRegistry + ) NTokenLiquidity(pool, delegateRegistry) {} + + function getXTokenType() external pure override returns (XTokenType) { + return XTokenType.NTokenIZUMILp; + } + + function _underlyingAsset( + address positionManager, + uint256 tokenId + ) internal view override returns (address token0, address token1) { + ILiquidityManager POSITION_MANAGER = ILiquidityManager(positionManager); + (, , , , , , , uint128 poolId) = POSITION_MANAGER.liquidities(tokenId); + (token0, token1, ) = POSITION_MANAGER.poolMetas(poolId); + } + + function _collect( + address positionManager, + uint256 tokenId + ) internal override returns (uint256 amount0, uint256 amount1) { + ILiquidityManager POSITION_MANAGER = ILiquidityManager(positionManager); + + (amount0, amount1) = POSITION_MANAGER.collect( + address(POOL), + tokenId, + type(uint128).max, + type(uint128).max + ); + } + + function _increaseLiquidity( + address positionManager, + uint256 tokenId, + uint256 amountAdd0, + uint256 amountAdd1, + uint256 amount0Min, + uint256 amount1Min, + uint256 deadline + ) internal override returns (uint256 amount0, uint256 amount1) { + ILiquidityManager POSITION_MANAGER = ILiquidityManager(positionManager); + + ILiquidityManager.AddLiquidityParam memory params = ILiquidityManager + .AddLiquidityParam({ + lid: tokenId, + xLim: amountAdd0.toUint128(), + yLim: amountAdd1.toUint128(), + amountXMin: amount0Min.toUint128(), + amountYMin: amount1Min.toUint128(), + deadline: deadline + }); + + (, amount0, amount1) = POSITION_MANAGER.addLiquidity{value: msg.value}( + params + ); + } + + function _decreaseLiquidity( + address positionManager, + uint256 tokenId, + uint128 liquidityDecrease, + uint256 amount0Min, + uint256 amount1Min + ) internal override { + ILiquidityManager POSITION_MANAGER = ILiquidityManager(positionManager); + POSITION_MANAGER.decLiquidity( + tokenId, + liquidityDecrease, + amount0Min, + amount1Min, + block.timestamp + ); + } + + function _liquidity( + address positionManager, + uint256 tokenId + ) internal view override returns (uint256) { + (, , uint256 liquidity, , , , , ) = ILiquidityManager(positionManager) + .liquidities(tokenId); + return liquidity; + } + + function _burn(address positionManager, uint256 tokenId) internal override { + ILiquidityManager(positionManager).burn(tokenId); + } + + function _refundETH(address positionManager) internal override { + IBase(positionManager).refundETH(); + } +} diff --git a/contracts/protocol/tokenization/NTokenLiquidity.sol b/contracts/protocol/tokenization/NTokenLiquidity.sol new file mode 100644 index 000000000..92f7fa3eb --- /dev/null +++ b/contracts/protocol/tokenization/NTokenLiquidity.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC721} from "../../dependencies/openzeppelin/contracts/IERC721.sol"; +import {IERC1155} from "../../dependencies/openzeppelin/contracts/IERC1155.sol"; +import {IERC721Metadata} from "../../dependencies/openzeppelin/contracts/IERC721Metadata.sol"; +import {Address} from "../../dependencies/openzeppelin/contracts/Address.sol"; +import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {SafeCast} from "../../dependencies/openzeppelin/contracts/SafeCast.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {IPool} from "../../interfaces/IPool.sol"; +import {NToken} from "./NToken.sol"; +import {DataTypes} from "../libraries/types/DataTypes.sol"; +import {IWETH} from "../../misc/interfaces/IWETH.sol"; +import {INTokenLiquidity} from "../../interfaces/INTokenLiquidity.sol"; +import {Helpers} from "../../protocol/libraries/helpers/Helpers.sol"; + +/** + * @title NTokenLiquidity + * + * @notice Implementation of the interest bearing token for the ParaSpace protocol + */ +abstract contract NTokenLiquidity is NToken, INTokenLiquidity { + using SafeERC20 for IERC20; + + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor( + IPool pool, + address delegateRegistry + ) NToken(pool, true, delegateRegistry) { + _ERC721Data.balanceLimit = 30; + } + + function _underlyingAsset( + address, + uint256 + ) internal view virtual returns (address, address) { + return (address(0), address(0)); + } + + function _decreaseLiquidity( + address, + uint256, + uint128, + uint256, + uint256 + ) internal virtual {} + + function _increaseLiquidity( + address, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256 + ) internal virtual returns (uint256, uint256) { + return (0, 0); + } + + function _collect( + address, + uint256 + ) internal virtual returns (uint256, uint256) { + return (0, 0); + } + + function _liquidity( + address, + uint256 + ) internal view virtual returns (uint256) { + return 0; + } + + function _burn(address, uint256) internal virtual {} + + function _refundETH(address) internal virtual {} + + function underlyingAsset( + uint256 tokenId + ) external view returns (address token0, address token1) { + return _underlyingAsset(_ERC721Data.underlyingAsset, tokenId); + } + + /// @inheritdoc INTokenLiquidity + function decreaseLiquidity( + address user, + uint256 tokenId, + uint128 liquidityDecrease, + uint256 amount0Min, + uint256 amount1Min + ) + external + onlyPool + nonReentrant + returns ( + address token0, + address token1, + uint256 amount0, + uint256 amount1, + bool isBurned + ) + { + require(user == ownerOf(tokenId), Errors.NOT_THE_OWNER); + + address positionManager = _ERC721Data.underlyingAsset; + if (liquidityDecrease > 0) { + _decreaseLiquidity( + positionManager, + tokenId, + liquidityDecrease, + amount0Min, + amount1Min + ); + } + + (token0, token1) = _underlyingAsset(positionManager, tokenId); + + (amount0, amount1) = _collect(positionManager, tokenId); + + uint256 currentLiquidity = _liquidity(positionManager, tokenId); + if (currentLiquidity == 0) { + _burn(positionManager, tokenId); + isBurned = true; + } + } + + /// @inheritdoc INTokenLiquidity + function increaseLiquidity( + address payer, + uint256 tokenId, + uint256 amountAdd0, + uint256 amountAdd1, + uint256 amount0Min, + uint256 amount1Min, + uint256 deadline + ) external payable onlyPool nonReentrant { + address positionManager = _ERC721Data.underlyingAsset; + + (address token0, address token1) = _underlyingAsset( + positionManager, + tokenId + ); + + // move underlying into this contract + bool token0IsETH; + bool token1IsETH; + if (msg.value > 0) { + address weth = _addressesProvider.getWETH(); + require(token0 == weth || token1 == weth, Errors.INVALID_AMOUNT); + token0IsETH = (token0 == weth); + token1IsETH = (token1 == weth); + } + if (token0IsETH) { + require(msg.value == amountAdd0, Errors.INVALID_AMOUNT); + } else { + IERC20(token0).safeTransferFrom(payer, address(this), amountAdd0); + checkAllownance(token0, positionManager); + } + if (token1IsETH) { + require(msg.value == amountAdd1, Errors.INVALID_AMOUNT); + } else { + IERC20(token1).safeTransferFrom(payer, address(this), amountAdd1); + checkAllownance(token1, positionManager); + } + + (uint256 amount0, uint256 amount1) = _increaseLiquidity( + positionManager, + tokenId, + amountAdd0, + amountAdd1, + amount0Min, + amount1Min, + deadline + ); + + // refund unused tokens + if (amount0 < amountAdd0) { + uint256 refund0 = amountAdd0 - amount0; + if (token0IsETH) { + _refundETH(positionManager); + Helpers.safeTransferETH(payer, refund0); + } else { + IERC20(token0).safeTransfer(payer, refund0); + } + } + if (amount1 < amountAdd1) { + uint256 refund1 = amountAdd1 - amount1; + if (token1IsETH) { + _refundETH(positionManager); + Helpers.safeTransferETH(payer, refund1); + } else { + IERC20(token1).safeTransfer(payer, refund1); + } + } + } + + function checkAllownance(address token, address spender) internal { + uint256 allownance = IERC20(token).allowance(address(this), spender); + if (allownance == 0) { + IERC20(token).safeApprove(spender, type(uint256).max); + } + } + + function setTraitsMultipliers( + uint256[] calldata, + uint256[] calldata + ) external override onlyPoolAdmin nonReentrant { + revert(); + } + + receive() external payable {} +} diff --git a/contracts/protocol/tokenization/NTokenUniswapV3.sol b/contracts/protocol/tokenization/NTokenUniswapV3.sol index a7cc8cfc6..4a930d78a 100644 --- a/contracts/protocol/tokenization/NTokenUniswapV3.sol +++ b/contracts/protocol/tokenization/NTokenUniswapV3.sol @@ -15,16 +15,16 @@ import {NToken} from "./NToken.sol"; import {DataTypes} from "../libraries/types/DataTypes.sol"; import {INonfungiblePositionManager} from "../../dependencies/uniswapv3-periphery/interfaces/INonfungiblePositionManager.sol"; import {IWETH} from "../../misc/interfaces/IWETH.sol"; -import {XTokenType} from "../../interfaces/IXTokenType.sol"; -import {INTokenUniswapV3} from "../../interfaces/INTokenUniswapV3.sol"; import {Helpers} from "../../protocol/libraries/helpers/Helpers.sol"; +import {NTokenLiquidity} from "./NTokenLiquidity.sol"; +import {XTokenType} from "../../interfaces/IXTokenType.sol"; /** * @title UniswapV3 NToken * * @notice Implementation of the interest bearing token for the ParaSpace protocol */ -contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { +contract NTokenUniswapV3 is NTokenLiquidity { using SafeERC20 for IERC20; /** @@ -34,125 +34,96 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { constructor( IPool pool, address delegateRegistry - ) NToken(pool, true, delegateRegistry) { - _ERC721Data.balanceLimit = 30; - } + ) NTokenLiquidity(pool, delegateRegistry) {} function getXTokenType() external pure override returns (XTokenType) { return XTokenType.NTokenUniswapV3; } - /** - * @notice A function that decreases the current liquidity. - * @param tokenId The id of the erc721 token - * @param liquidityDecrease The amount of liquidity to remove of LP - * @param amount0Min The minimum amount to remove of token0 - * @param amount1Min The minimum amount to remove of token1 - * @param receiveEthAsWeth If convert weth to ETH - * @return amount0 The amount received back in token0 - * @return amount1 The amount returned back in token1 - */ - function _decreaseLiquidity( - address user, - uint256 tokenId, - uint128 liquidityDecrease, - uint256 amount0Min, - uint256 amount1Min, - bool receiveEthAsWeth - ) internal returns (uint256 amount0, uint256 amount1) { - if (liquidityDecrease > 0) { - // amount0Min and amount1Min are price slippage checks - // if the amount received after burning is not greater than these minimums, transaction will fail - INonfungiblePositionManager.DecreaseLiquidityParams - memory params = INonfungiblePositionManager - .DecreaseLiquidityParams({ - tokenId: tokenId, - liquidity: liquidityDecrease, - amount0Min: amount0Min, - amount1Min: amount1Min, - deadline: block.timestamp - }); - - INonfungiblePositionManager(_ERC721Data.underlyingAsset) - .decreaseLiquidity(params); - } - - ( - , - , - address token0, - address token1, - , - , - , - , - , - , - , - - ) = INonfungiblePositionManager(_ERC721Data.underlyingAsset).positions( - tokenId - ); - - address weth = _addressesProvider.getWETH(); - receiveEthAsWeth = (receiveEthAsWeth && - (token0 == weth || token1 == weth)); + function _underlyingAsset( + address positionManager, + uint256 tokenId + ) internal view override returns (address token0, address token1) { + (, , token0, token1, , , , , , , , ) = INonfungiblePositionManager( + positionManager + ).positions(tokenId); + } + function _collect( + address positionManager, + uint256 tokenId + ) internal override returns (uint256 amount0, uint256 amount1) { INonfungiblePositionManager.CollectParams memory collectParams = INonfungiblePositionManager.CollectParams({ tokenId: tokenId, - recipient: receiveEthAsWeth ? address(this) : user, + recipient: address(POOL), amount0Max: type(uint128).max, amount1Max: type(uint128).max }); - (amount0, amount1) = INonfungiblePositionManager( - _ERC721Data.underlyingAsset - ).collect(collectParams); - - if (receiveEthAsWeth) { - uint256 balanceWeth = IERC20(weth).balanceOf(address(this)); - if (balanceWeth > 0) { - IWETH(weth).withdraw(balanceWeth); - Helpers.safeTransferETH(user, balanceWeth); - } - - address pairToken = (token0 == weth) ? token1 : token0; - uint256 balanceToken = IERC20(pairToken).balanceOf(address(this)); - if (balanceToken > 0) { - IERC20(pairToken).safeTransfer(user, balanceToken); - } - } + (amount0, amount1) = INonfungiblePositionManager(positionManager) + .collect(collectParams); } - /// @inheritdoc INTokenUniswapV3 - function decreaseUniswapV3Liquidity( - address user, + function _increaseLiquidity( + address positionManager, uint256 tokenId, - uint128 liquidityDecrease, + uint256 amountAdd0, + uint256 amountAdd1, uint256 amount0Min, uint256 amount1Min, - bool receiveEthAsWeth - ) external onlyPool nonReentrant { - require(user == ownerOf(tokenId), Errors.NOT_THE_OWNER); + uint256 deadline + ) internal override returns (uint256 amount0, uint256 amount1) { + INonfungiblePositionManager.IncreaseLiquidityParams + memory params = INonfungiblePositionManager + .IncreaseLiquidityParams({ + tokenId: tokenId, + amount0Desired: amountAdd0, + amount1Desired: amountAdd1, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline + }); + + (, amount0, amount1) = INonfungiblePositionManager(positionManager) + .increaseLiquidity{value: msg.value}(params); + } - // interact with Uniswap V3 - _decreaseLiquidity( - user, - tokenId, - liquidityDecrease, - amount0Min, - amount1Min, - receiveEthAsWeth - ); + function _liquidity( + address positionManager, + uint256 tokenId + ) internal view override returns (uint256) { + (, , , , , , , uint256 liquidity, , , , ) = INonfungiblePositionManager( + positionManager + ).positions(tokenId); + return liquidity; } - function setTraitsMultipliers( - uint256[] calldata, - uint256[] calldata - ) external override onlyPoolAdmin nonReentrant { - revert(); + function _burn(address positionManager, uint256 tokenId) internal override { + INonfungiblePositionManager(positionManager).burn(tokenId); } - receive() external payable {} + function _decreaseLiquidity( + address positionManager, + uint256 tokenId, + uint128 liquidityDecrease, + uint256 amount0Min, + uint256 amount1Min + ) internal override { + INonfungiblePositionManager.DecreaseLiquidityParams + memory params = INonfungiblePositionManager + .DecreaseLiquidityParams({ + tokenId: tokenId, + liquidity: liquidityDecrease, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: block.timestamp + }); + + INonfungiblePositionManager(positionManager).decreaseLiquidity(params); + } + + function _refundETH(address positionManager) internal override { + INonfungiblePositionManager(positionManager).refundETH(); + } } diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index f75f1dc63..bea1bf3da 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -27,8 +27,8 @@ import {IEACAggregatorProxy} from "./interfaces/IEACAggregatorProxy.sol"; import {IERC20DetailedBytes} from "./interfaces/IERC20DetailedBytes.sol"; import {ProtocolDataProvider} from "../misc/ProtocolDataProvider.sol"; import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; -import {IUniswapV3OracleWrapper} from "../interfaces/IUniswapV3OracleWrapper.sol"; -import {UinswapV3PositionData} from "../interfaces/IUniswapV3PositionInfoProvider.sol"; +import {ILiquidityNFTOracleWrapper} from "../interfaces/ILiquidityNFTOracleWrapper.sol"; +import {LiquidityNFTPositionData} from "../interfaces/ILiquidityNFTPositionInfoProvider.sol"; import {Helpers} from "../protocol/libraries/helpers/Helpers.sol"; import {IStakefishValidator} from "../interfaces/IStakefishValidator.sol"; import {INTokenStakefish} from "../interfaces/INTokenStakefish.sol"; @@ -321,14 +321,14 @@ contract UiPoolDataProvider is IUiPoolDataProvider { return (tokenData); } - function getUniswapV3LpTokenData( + function getLiquidityTokenData( IPoolAddressesProvider provider, address lpTokenAddress, uint256 tokenId - ) external view override returns (UniswapV3LpTokenInfo memory) { - UniswapV3LpTokenInfo memory lpTokenInfo; + ) external view override returns (LiquidityTokenInfo memory) { + LiquidityTokenInfo memory lpTokenInfo; - IUniswapV3OracleWrapper source; + ILiquidityNFTOracleWrapper source; //avoid stack too deep { IParaSpaceOracle oracle = IParaSpaceOracle( @@ -338,7 +338,7 @@ contract UiPoolDataProvider is IUiPoolDataProvider { if (sourceAddress == address(0)) { return lpTokenInfo; } - source = IUniswapV3OracleWrapper(sourceAddress); + source = ILiquidityNFTOracleWrapper(sourceAddress); IPool pool = IPool(provider.getPool()); ( @@ -351,7 +351,7 @@ contract UiPoolDataProvider is IUiPoolDataProvider { try source.getTokenPrice(tokenId) returns (uint256 tokenPrice) { lpTokenInfo.tokenPrice = tokenPrice; - UinswapV3PositionData memory positionData = source + LiquidityNFTPositionData memory positionData = source .getOnchainPositionData(tokenId); lpTokenInfo.token0 = positionData.token0; lpTokenInfo.token1 = positionData.token1; diff --git a/contracts/ui/interfaces/IUiPoolDataProvider.sol b/contracts/ui/interfaces/IUiPoolDataProvider.sol index c496d8ae7..3a930ac70 100644 --- a/contracts/ui/interfaces/IUiPoolDataProvider.sol +++ b/contracts/ui/interfaces/IUiPoolDataProvider.sol @@ -74,7 +74,7 @@ interface IUiPoolDataProvider { uint8 networkBaseTokenPriceDecimals; } - struct UniswapV3LpTokenInfo { + struct LiquidityTokenInfo { address token0; address token1; uint24 feeRate; @@ -143,11 +143,11 @@ interface IUiPoolDataProvider { uint256[][] memory tokenIds ) external view returns (DataTypes.AuctionData[][] memory); - function getUniswapV3LpTokenData( + function getLiquidityTokenData( IPoolAddressesProvider provider, address lpTokenAddress, uint256 tokenId - ) external view returns (UniswapV3LpTokenInfo memory); + ) external view returns (LiquidityTokenInfo memory); function getUserInLiquidationNFTData( IPoolAddressesProvider provider, diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 16cb563ae..7b0745b13 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -153,8 +153,23 @@ import { WstETHMocked, X2Y2Adapter, X2Y2R1, + NTokenIzumi, + IZUMIOracleWrapper, + SwapX2YModule, + SwapY2XModule, + LiquidityModule, + LimitOrderModule, + FlashModule, + IZiSwapFactory, + LiquidityManager, + Swap, PoolBorrowAndStake__factory, PoolBorrowAndStake, + PoolLpOperation__factory, + UniswapV2Factory, + UniswapV2Router02, + UniswapV2Pair, + UniswapV2OracleWrapper, } from "../types"; import { getACLManager, @@ -171,6 +186,8 @@ import { getProtocolDataProvider, getPunks, getTimeLockProxy, + getUniswapV2Factory, + getUniswapV2Pair, getUniswapV3SwapRouter, getWETH, } from "./contracts-getters"; @@ -203,6 +220,7 @@ import {pick, upperFirst} from "lodash"; import shell from "shelljs"; import {ZERO_ADDRESS} from "./constants"; import {GLOBAL_OVERRIDES, ZK_LIBRARIES_PATH} from "./hardhat-constants"; +import {zeroAddress} from "ethereumjs-util"; export const deployAllLibraries = async (verify?: boolean) => { const supplyLogic = await deploySupplyLogic(verify); @@ -780,6 +798,10 @@ export const getPoolSignatures = () => { PoolPositionMover__factory.abi ); + const poolLpOperationSelectors = getFunctionSignatures( + PoolLpOperation__factory.abi + ); + const poolProxySelectors = getFunctionSignatures(ParaProxy__factory.abi); const poolParaProxyInterfacesSelectors = getFunctionSignatures( @@ -796,6 +818,7 @@ export const getPoolSignatures = () => { ...poolProxySelectors, ...poolParaProxyInterfacesSelectors, ...poolPositionMoverSelectors, + ...poolLpOperationSelectors, ]; for (const selector of poolSelectors) { if (!allSelectors[selector.signature]) { @@ -817,6 +840,7 @@ export const getPoolSignatures = () => { poolBorrowAndStakeSelectors, poolParaProxyInterfacesSelectors, poolPositionMoverSelectors, + poolLpOperationSelectors, }; }; @@ -872,6 +896,11 @@ export const deployPoolComponents = async ( "contracts/protocol/libraries/logic/SupplyLogic.sol:SupplyLogic", ]); + const lpOperationLibraries = pick(coreLibraries, [ + "contracts/protocol/libraries/logic/BorrowLogic.sol:BorrowLogic", + "contracts/protocol/libraries/logic/SupplyLogic.sol:SupplyLogic", + ]); + const allTokens = await getAllTokens(); const APE_WETH_FEE = 3000; @@ -883,6 +912,7 @@ export const deployPoolComponents = async ( poolMarketplaceSelectors, poolApeStakingSelectors, poolBorrowAndStakeSelectors, + poolLpOperationSelectors, } = getPoolSignatures(); const poolCore = (await withSaveAndVerify( @@ -921,6 +951,16 @@ export const deployPoolComponents = async ( poolMarketplaceSelectors )) as PoolMarketplace; + const poolLpOperation = (await withSaveAndVerify( + await getContractFactory("PoolLpOperation", lpOperationLibraries), + eContractid.PoolLpOperationImpl, + [provider], + verify, + false, + lpOperationLibraries, + poolLpOperationSelectors + )) as PoolMarketplace; + const config = getParaSpaceConfig(); const treasuryAddress = config.Treasury; const cApe = await getAutoCompoundApe(); @@ -967,6 +1007,7 @@ export const deployPoolComponents = async ( poolMarketplace, poolApeStaking, poolBorrowAndStake, + poolLpOperation, poolCoreSelectors: poolCoreSelectors.map((s) => s.signature), poolParametersSelectors: poolParametersSelectors.map((s) => s.signature), poolMarketplaceSelectors: poolMarketplaceSelectors.map((s) => s.signature), @@ -974,6 +1015,7 @@ export const deployPoolComponents = async ( poolBorrowAndStakeSelectors: poolBorrowAndStakeSelectors.map( (s) => s.signature ), + poolLpOperationSelectors: poolLpOperationSelectors.map((s) => s.signature), }; }; @@ -1171,6 +1213,29 @@ export const deployUniswapV3NTokenImpl = async ( ) as Promise; }; +export const deployIZUMILpNTokenImpl = async ( + poolAddress: tEthereumAddress, + delegationRegistry: tEthereumAddress, + verify?: boolean +) => { + const mintableERC721Logic = + (await getContractAddressInDb(eContractid.MintableERC721Logic)) || + (await deployMintableERC721Logic(verify)).address; + + const libraries = { + ["contracts/protocol/tokenization/libraries/MintableERC721Logic.sol:MintableERC721Logic"]: + mintableERC721Logic, + }; + return withSaveAndVerify( + await getContractFactory("NTokenIzumi", libraries), + eContractid.NTokenIZUMILpImpl, + [poolAddress, delegationRegistry], + verify, + false, + libraries + ) as Promise; +}; + export const deployGenericMoonbirdNTokenImpl = async ( poolAddress: tEthereumAddress, delegationRegistry: tEthereumAddress, @@ -1218,12 +1283,15 @@ export const deployAllERC20Tokens = async (verify?: boolean) => { | WstETHMocked | MockAToken | AutoYieldApe + | UniswapV2Pair | AutoCompoundApe; } = {}; const paraSpaceConfig = getParaSpaceConfig(); const reservesConfig = paraSpaceConfig.ReservesConfig; const tokensConfig = paraSpaceConfig.Tokens; + const deployer = await getFirstSigner(); + const deployerAddress = await deployer.getAddress(); for (const tokenSymbol of Object.keys(ERC20TokenContractId)) { const db = getDb(); @@ -1339,6 +1407,87 @@ export const deployAllERC20Tokens = async (verify?: boolean) => { ); continue; } + if (tokenSymbol === ERC20TokenContractId.UNIV2DAIWETH) { + if (!(await getContractAddressInDb(eContractid.UniswapV2Factory))) { + const factory = await deployUniswapV2Factory( + [deployerAddress], + false + ); + await deployUniswapV2Router02( + [factory.address, tokens["WETH"].address], + false + ); + } + const factory = await getUniswapV2Factory(); + await factory.createPair(tokens["WETH"].address, tokens["DAI"].address); + const address = await factory.getPair( + tokens["WETH"].address, + tokens["DAI"].address + ); + tokens[tokenSymbol] = await getUniswapV2Pair(address); + await insertContractAddressInDb( + eContractid.UNIV2DAIWETH, + address, + false + ); + continue; + } + if (tokenSymbol === ERC20TokenContractId.UNIV2USDCWETH) { + if (!(await getContractAddressInDb(eContractid.UniswapV2Factory))) { + const factory = await deployUniswapV2Factory( + [deployerAddress], + false + ); + await deployUniswapV2Router02( + [factory.address, tokens["WETH"].address], + false + ); + } + const factory = await getUniswapV2Factory(); + await factory.createPair( + tokens["WETH"].address, + tokens["USDC"].address + ); + const address = await factory.getPair( + tokens["WETH"].address, + tokens["USDC"].address + ); + tokens[tokenSymbol] = await getUniswapV2Pair(address); + await insertContractAddressInDb( + eContractid.UNIV2USDCWETH, + address, + false + ); + continue; + } + if (tokenSymbol === ERC20TokenContractId.UNIV2WETHUSDT) { + if (!(await getContractAddressInDb(eContractid.UniswapV2Factory))) { + const factory = await deployUniswapV2Factory( + [deployerAddress], + false + ); + await deployUniswapV2Router02( + [factory.address, tokens["WETH"].address], + false + ); + } + const factory = await getUniswapV2Factory(); + await factory.createPair( + tokens["WETH"].address, + tokens["USDT"].address + ); + const address = await factory.getPair( + tokens["WETH"].address, + tokens["USDT"].address + ); + tokens[tokenSymbol] = await getUniswapV2Pair(address); + await insertContractAddressInDb( + eContractid.UNIV2WETHUSDT, + address, + false + ); + continue; + } tokens[tokenSymbol] = await deployMintableERC20( [tokenSymbol, tokenSymbol, reserveConfig.reserveDecimals], @@ -1554,6 +1703,18 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { continue; } + if (tokenSymbol === ERC721TokenContractId.IZUMILp) { + const weth = await getWETH(); + const factory = await deployIZUMIFactory(verify); + await deployIZUMISwapRouter([factory.address, weth.address], verify); + const positionManager = await deployIZUMIPositionManager( + [factory.address, weth.address], + verify + ); + tokens[tokenSymbol] = positionManager; + continue; + } + if (tokenSymbol === ERC721TokenContractId.SFVLDR) { const depositContract = await deployDepositContract(verify); const {paraSpaceAdminAddress} = await getParaSpaceAdmins(); @@ -2052,6 +2213,28 @@ export const deployERC721Delegate = async (verify?: boolean) => verify ) as Promise; +export const deployUniswapV2Factory = async ( + args: [string], + verify?: boolean +) => + withSaveAndVerify( + await getContractFactory("UniswapV2Factory"), + eContractid.UniswapV2Factory, + [...args], + verify + ) as Promise; + +export const deployUniswapV2Router02 = async ( + args: [string, string], + verify?: boolean +) => + withSaveAndVerify( + await getContractFactory("UniswapV2Router02"), + eContractid.UniswapV2Router02, + [...args], + verify + ) as Promise; + export const deployUniswapV3Factory = async (args: [], verify?: boolean) => withSaveAndVerify( await getContractFactory("UniswapV3Factory"), @@ -2114,6 +2297,19 @@ export const deployUniswapV3OracleWrapper = async ( verify ) as Promise; +export const deployUniswapV2OracleWrapper = async ( + asset: string, + contractId: string, + addressProvider: string, + verify?: boolean +) => + withSaveAndVerify( + await getContractFactory("UniswapV2OracleWrapper"), + eContractid.Aggregator.concat(upperFirst(contractId)), + [asset, addressProvider], + verify + ) as Promise; + export const deployUniswapV3TwapOracleWrapper = async ( pool: string, baseCurrency: string, @@ -2150,6 +2346,103 @@ export const deployUniswapSwapRouter = async ( verify ); +export const deployIZUMISwapX2YModule = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("SwapX2YModule"), + eContractid.IZUMISwapX2Y, + [], + verify + ) as Promise; + +export const deployIZUMISwapY2XModule = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("SwapY2XModule"), + eContractid.IZUMISwapY2X, + [], + verify + ) as Promise; + +export const deployIZUMILiquidity = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("LiquidityModule"), + eContractid.IZUMILiquidity, + [], + verify + ) as Promise; + +export const deployIZUMILimitOrder = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("LimitOrderModule"), + eContractid.IZUMILimitOrder, + [], + verify + ) as Promise; + +export const deployIZUMIFlashModule = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("FlashModule"), + eContractid.IZUMIFlashModule, + [], + verify + ) as Promise; + +export const deployIZUMIFactory = async (verify?: boolean) => { + const X2YModule = await deployIZUMISwapX2YModule(); + const Y2XModule = await deployIZUMISwapY2XModule(); + const LiquidityModule = await deployIZUMILiquidity(); + const LimitOrderModule = await deployIZUMILimitOrder(); + const FlashModule = await deployIZUMIFlashModule(); + return withSaveAndVerify( + await getContractFactory("iZiSwapFactory"), + eContractid.IZUMIPoolFactory, + [ + zeroAddress(), + X2YModule.address, + Y2XModule.address, + LiquidityModule.address, + LimitOrderModule.address, + FlashModule.address, + 0, + ], + verify + ) as Promise; +}; + +export const deployIZUMIOracleWrapper = async ( + factory: string, + manager: string, + addressProvider: string, + verify?: boolean +) => + withSaveAndVerify( + await getContractFactory("IZUMIOracleWrapper"), + eContractid.Aggregator.concat(upperFirst(eContractid.IZUMILp)), + [factory, manager, addressProvider], + verify + ) as Promise; + +export const deployIZUMIPositionManager = async ( + args: [string, string], + verify?: boolean +) => + withSaveAndVerify( + await getContractFactory("LiquidityManager"), + eContractid.IZUMILp, + [...args], + verify + ) as Promise; + +export const deployIZUMISwapRouter = async ( + args: [string, string], + verify?: boolean +) => + withSaveAndVerify( + await getContractFactory("Swap"), + eContractid.IZUMISwapRouter, + [...args], + verify + ) as Promise; + export const deployStETH = async (verify?: boolean): Promise => withSaveAndVerify( await getContractFactory("StETHMocked"), diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 033947d2c..590292ea0 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -98,7 +98,15 @@ import { MockLendPool__factory, NTokenChromieSquiggle__factory, Account__factory, + NTokenIzumi__factory, + IZiSwapFactory__factory, + LiquidityManager__factory, + Swap__factory, + IZUMIOracleWrapper__factory, AccountRegistry__factory, + UniswapV2Pair__factory, + UniswapV2Factory__factory, + UniswapV2Router02__factory, } from "../types"; import { getEthersSigners, @@ -397,6 +405,31 @@ export const getUiIncentiveDataProviderV3 = async ( await getFirstSigner() ); +export const getUniswapV2Factory = async (address?: tEthereumAddress) => + await UniswapV2Factory__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.UniswapV2Factory}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getUniswapV2Router = async (address?: tEthereumAddress) => + await UniswapV2Router02__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.UniswapV2Router02}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getUniswapV2Pair = async (address: tEthereumAddress) => + await UniswapV2Pair__factory.connect(address, await getFirstSigner()); + export const getPToken = async (address?: tEthereumAddress) => await PToken__factory.connect( address || @@ -785,6 +818,63 @@ export const getNTokenUniswapV3 = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getIZUMIFactory = async (address?: tEthereumAddress) => + await IZiSwapFactory__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.IZUMIPoolFactory}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getIZUMIPositionManager = async (address?: tEthereumAddress) => + await LiquidityManager__factory.connect( + address || + ( + await getDb().get(`${eContractid.IZUMILp}.${DRE.network.name}`).value() + ).address, + await getFirstSigner() + ); + +export const getIZUMISwapRouter = async (address?: tEthereumAddress) => + await Swap__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.IZUMISwapRouter}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getIZUMIOracleWrapper = async (address?: tEthereumAddress) => + await IZUMIOracleWrapper__factory.connect( + address || + ( + await getDb() + .get( + `${eContractid.Aggregator.concat( + upperFirst(eContractid.IZUMILp) + )}.${DRE.network.name}` + ) + .value() + ).address, + await getFirstSigner() + ); + +export const getNTokenIZUMI = async (address?: tEthereumAddress) => + await NTokenIzumi__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.NTokenIZUMILpImpl}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + export const getChainId = async () => (await DRE.ethers.provider.getNetwork()).chainId; diff --git a/helpers/hardhat-constants.ts b/helpers/hardhat-constants.ts index 30dd2c731..dd47d62e3 100644 --- a/helpers/hardhat-constants.ts +++ b/helpers/hardhat-constants.ts @@ -342,6 +342,7 @@ export const eContractidToContractName = { NTokenImpl: "NToken", NTokenMoonBirdsImpl: "NTokenMoonBirds", NTokenUniswapV3Impl: "NTokenUniswapV3", + NTokenIZUMILpImpl: "NTokenIZUMILpImpl", NTokenBAYCImpl: "NTokenBAYC", NTokenMAYCImpl: "NTokenMAYC", NTokenBAKCImpl: "NTokenBAKC", diff --git a/helpers/init-helpers.ts b/helpers/init-helpers.ts index 4be05e313..350fbb0e0 100644 --- a/helpers/init-helpers.ts +++ b/helpers/init-helpers.ts @@ -53,6 +53,7 @@ import { deployAutoCompoundApeImplAndAssignItToProxy, deployPTokenStKSM, deployStKSMDebtToken, + deployIZUMILpNTokenImpl, } from "./contracts-deployments"; import {ZERO_ADDRESS} from "./constants"; @@ -324,6 +325,7 @@ export const initReservesByHelper = async ( eContractid.NTokenImpl, eContractid.NTokenMoonBirdsImpl, eContractid.NTokenUniswapV3Impl, + eContractid.NTokenIZUMILpImpl, eContractid.NTokenBAYCImpl, eContractid.NTokenMAYCImpl, eContractid.NTokenBAKCImpl, @@ -521,6 +523,14 @@ export const initReservesByHelper = async ( ).address; } xTokenToUse = nTokenUniSwapV3ImplementationAddress; + } else if (reserveSymbol === ERC721TokenContractId.IZUMILp) { + xTokenToUse = ( + await deployIZUMILpNTokenImpl( + pool.address, + delegationRegistryAddress, + verify + ) + ).address; } else if (reserveSymbol === ERC721TokenContractId.BAYC) { const apeCoinStaking = (await getContractAddressInDb(eContractid.ApeCoinStaking)) || diff --git a/helpers/oracles-helpers.ts b/helpers/oracles-helpers.ts index 89ca14cf8..1e6097d03 100644 --- a/helpers/oracles-helpers.ts +++ b/helpers/oracles-helpers.ts @@ -12,8 +12,10 @@ import { CLFixedPriceSynchronicityPriceAdapter, CLwstETHSynchronicityPriceAdapter, ERC721OracleWrapper, + IZUMIOracleWrapper, MockAggregator, PriceOracle, + UniswapV2OracleWrapper, UniswapV3OracleWrapper, } from "../types"; import { @@ -24,11 +26,14 @@ import { deployFixedPriceSynchronicityPriceAdapter, deployExchangeRateSynchronicityPriceAdapter, deployCTokenSynchronicityPriceAdapter, + deployIZUMIOracleWrapper, + deployUniswapV2OracleWrapper, } from "./contracts-deployments"; import {getParaSpaceConfig, waitForTx} from "./misc-utils"; import { getAggregator, getAllTokens, + getIZUMIFactory, getPoolAddressesProvider, getUniswapV3Factory, } from "./contracts-getters"; @@ -66,6 +71,8 @@ export const deployAllAggregators = async ( [tokenSymbol: string]: | MockAggregator | UniswapV3OracleWrapper + | UniswapV2OracleWrapper + | IZUMIOracleWrapper | CLwstETHSynchronicityPriceAdapter | ERC721OracleWrapper | CLExchangeRateSynchronicityPriceAdapter @@ -134,6 +141,27 @@ export const deployAllAggregators = async ( tokenSymbol, verify ); + } else if (tokenSymbol === ERC20TokenContractId.UNIV2DAIWETH) { + aggregators[tokenSymbol] = await deployUniswapV2OracleWrapper( + tokens[ERC20TokenContractId.UNIV2DAIWETH].address, + ERC20TokenContractId.UNIV2DAIWETH, + addressesProvider.address, + verify + ); + } else if (tokenSymbol === ERC20TokenContractId.UNIV2USDCWETH) { + aggregators[tokenSymbol] = await deployUniswapV2OracleWrapper( + tokens[ERC20TokenContractId.UNIV2USDCWETH].address, + ERC20TokenContractId.UNIV2USDCWETH, + addressesProvider.address, + verify + ); + } else if (tokenSymbol === ERC20TokenContractId.UNIV2WETHUSDT) { + aggregators[tokenSymbol] = await deployUniswapV2OracleWrapper( + tokens[ERC20TokenContractId.UNIV2WETHUSDT].address, + ERC20TokenContractId.UNIV2WETHUSDT, + addressesProvider.address, + verify + ); } else if (tokenSymbol === ERC721TokenContractId.UniswapV3) { const univ3Factory = await getUniswapV3Factory(); const univ3Token = tokens[ERC721TokenContractId.UniswapV3]; @@ -143,6 +171,15 @@ export const deployAllAggregators = async ( addressesProvider.address, verify ); + } else if (tokenSymbol === ERC721TokenContractId.IZUMILp) { + const factory = await getIZUMIFactory(); + const iZumiToken = tokens[ERC721TokenContractId.IZUMILp]; + aggregators[tokenSymbol] = await deployIZUMIOracleWrapper( + factory.address, + iZumiToken.address, + addressesProvider.address, + verify + ); } else if (!initialPrices) { aggregators[tokenSymbol] = await deployERC721OracleWrapper( addressesProvider.address, diff --git a/helpers/types.ts b/helpers/types.ts index 309d65b2f..5f8c02bfa 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -1,5 +1,6 @@ import {BigNumber, BigNumberish, BytesLike} from "ethers"; import {Libraries} from "hardhat/types"; +import {strategyUniv2DaiWeth} from "../market-config/reservesConfigs"; export enum AssetType { ERC20 = 0, @@ -55,7 +56,7 @@ export enum XTokenType { NTokenOtherdeed = 15, NTokenStakefish = 16, NTokenChromieSquiggle = 17, - PhantomData1 = 18, + NTokenIZUMILp = 18, PhantomData2 = 19, PhantomData3 = 20, PhantomData4 = 21, @@ -143,6 +144,7 @@ export enum eContractid { NTokenImpl = "NTokenImpl", NTokenMoonBirdsImpl = "NTokenMoonBirdsImpl", NTokenUniswapV3Impl = "NTokenUniswapV3Impl", + NTokenIZUMILpImpl = "NTokenIZUMILpImpl", NTokenBAYCImpl = "NTokenBAYCImpl", NTokenMAYCImpl = "NTokenMAYCImpl", NTokenBAKCImpl = "NTokenBAKCImpl", @@ -223,9 +225,22 @@ export enum eContractid { UniswapV3 = "UniswapV3", UniswapV3Factory = "UniswapV3Factory", UniswapV3SwapRouter = "UniswapV3SwapRouter", + UniswapV2Factory = "UniswapV2Factory", + UniswapV2Router02 = "UniswapV2Router02", + UNIV2DAIWETH = "UNIV2DAIWETH", + UNIV2USDCWETH = "UNIV2USDCWETH", + UNIV2WETHUSDT = "UNIV2WETHUSDT", NFTDescriptor = "NFTDescriptor", NonfungibleTokenPositionDescriptor = "NonfungibleTokenPositionDescriptor", - NonfungiblePositionManager = "NonfungiblePositionManager", + IZUMILp = "IZUMILp", + IZUMISwapX2Y = "IZUMISwapX2Y", + IZUMISwapY2X = "IZUMISwapY2X", + IZUMILiquidity = "IZUMILiquidity", + IZUMILimitOrder = "IZUMILimitOrder", + IZUMIFlashModule = "IZUMIFlashModule", + IZUMIPoolFactory = "IZUMIPoolFactory", + IZUMISwapRouter = "IZUMISwapRouter", + IZUMIPositionManager = "IZUMIPositionManager", StETH = "stETH", WStETH = "wstETH", MockAToken = "MockAToken", @@ -236,6 +251,7 @@ export enum eContractid { PoolParametersImpl = "PoolParametersImpl", PoolApeStakingImpl = "PoolApeStakingImpl", PoolBorrowAndStakeImpl = "PoolBorrowAndStakeImpl", + PoolLpOperationImpl = "PoolLpOperationImpl", ApeCoinStaking = "ApeCoinStaking", ATokenDebtToken = "ATokenDebtToken", StETHDebtToken = "StETHDebtToken", @@ -619,6 +635,9 @@ export enum ERC20TokenContractId { stMATIC = "stMATIC", CRV = "CRV", WMATIC = "WMATIC", + UNIV2DAIWETH = "UNIV2DAIWETH", + UNIV2USDCWETH = "UNIV2USDCWETH", + UNIV2WETHUSDT = "UNIV2WETHUSDT", } export enum ERC721TokenContractId { @@ -632,6 +651,7 @@ export enum ERC721TokenContractId { MEEBITS = "MEEBITS", OTHR = "OTHR", UniswapV3 = "UniswapV3", + IZUMILp = "IZUMILp", BAKC = "BAKC", SEWER = "SEWER", PPG = "PPG", @@ -653,6 +673,7 @@ export enum NTokenContractId { nWPUNKS = "nWPUNKS", nMOONBIRD = "nMOONBIRD", nUniswapV3 = "nUniswapV3", + nIZUMI = "nIZUMI", nBAKC = "nBAKC", nSEWER = "nSEWER", nPPG = "nPPG", diff --git a/market-config/auctionStrategies.ts b/market-config/auctionStrategies.ts index 79df5d8dd..831b020ab 100644 --- a/market-config/auctionStrategies.ts +++ b/market-config/auctionStrategies.ts @@ -101,6 +101,16 @@ export const auctionStrategyUniswapV3: IAuctionStrategyParams = { tickLength: "900", }; +export const auctionStrategyIZUMI: IAuctionStrategyParams = { + name: "auctionStrategyIZUMI", + maxPriceMultiplier: utils.parseUnits("1.2", 18).toString(), + minExpPriceMultiplier: utils.parseUnits("1", 18).toString(), + minPriceMultiplier: utils.parseUnits("0.8", 18).toString(), + stepLinear: utils.parseUnits("0.003102276665", 18).toString(), + stepExp: utils.parseUnits("0.2022592736", 18).toString(), + tickLength: "900", +}; + export const auctionStrategyBAKC: IAuctionStrategyParams = { name: "auctionStrategyBAKC", maxPriceMultiplier: utils.parseUnits("2", 18).toString(), diff --git a/market-config/index.ts b/market-config/index.ts index 69bb652ac..8e2b0a254 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -34,6 +34,7 @@ import { strategyAzuki, strategyOthr, strategyUniswapV3, + strategyIZUMI, strategyClonex, strategyMeebits, strategySAPE, @@ -76,6 +77,9 @@ import { strategyWBTCWH, strategySTDOT, strategyEXRP, + strategyUniv2DaiWeth, + strategyUniv2UsdcWeth, + strategyUniv2WethUsdt, } from "./reservesConfigs"; export const CommonConfig: Pick< @@ -180,6 +184,7 @@ export const HardhatConfig: IParaSpaceConfiguration = { OTHR: strategyOthr, CLONEX: strategyClonex, UniswapV3: strategyUniswapV3, + IZUMILp: strategyIZUMI, sAPE: strategySAPE, cAPE: strategyCAPE, yAPE: strategyYAPE, @@ -188,6 +193,9 @@ export const HardhatConfig: IParaSpaceConfiguration = { SEWER: strategySEWER, PPG: strategyPudgyPenguins, SFVLDR: strategyStakefishValidator, + UNIV2DAIWETH: strategyUniv2DaiWeth, + UNIV2USDCWETH: strategyUniv2UsdcWeth, + UNIV2WETHUSDT: strategyUniv2WethUsdt, }, }; @@ -650,6 +658,7 @@ export const ZkSyncGoerliConfig: IParaSpaceConfiguration = { FRAX: strategyFRAX, WETH: strategyWETH, WBTC: strategyWBTC, + IZUMILp: strategyIZUMI, }, Mocks: MocksUSDConfig, Oracle: ZkSyncOracleConfig, diff --git a/market-config/reservesConfigs.ts b/market-config/reservesConfigs.ts index e66535b74..1bf61058f 100644 --- a/market-config/reservesConfigs.ts +++ b/market-config/reservesConfigs.ts @@ -23,6 +23,7 @@ import { auctionStrategyZero, auctionStrategyEXRP, auctionStrategyUniswapV3, + auctionStrategyIZUMI, } from "./auctionStrategies"; import { rateStrategyAAVE, @@ -112,8 +113,57 @@ import { timeLockStrategyWBTCWH, timeLockStrategySTDOT, timeLockStrategyEXRP, + timeLockStrategyIZUMI, } from "./timeLockStrategies"; +export const strategyUniv2DaiWeth: IReserveParams = { + strategy: rateStrategyDAI, + auctionStrategy: auctionStrategyZero, + timeLockStrategy: timeLockStrategyDAI, + baseLTVAsCollateral: "7000", + liquidationThreshold: "9000", + liquidationProtocolFeePercentage: "0", + liquidationBonus: "10400", + borrowingEnabled: true, + reserveDecimals: "18", + xTokenImpl: eContractid.PTokenImpl, + reserveFactor: "1000", + borrowCap: "0", + supplyCap: "0", +}; + +export const strategyUniv2UsdcWeth: IReserveParams = { + strategy: rateStrategyDAI, + auctionStrategy: auctionStrategyZero, + timeLockStrategy: timeLockStrategyDAI, + baseLTVAsCollateral: "7000", + liquidationThreshold: "9000", + liquidationProtocolFeePercentage: "0", + liquidationBonus: "10400", + borrowingEnabled: true, + reserveDecimals: "18", + xTokenImpl: eContractid.PTokenImpl, + reserveFactor: "1000", + borrowCap: "0", + supplyCap: "0", +}; + +export const strategyUniv2WethUsdt: IReserveParams = { + strategy: rateStrategyDAI, + auctionStrategy: auctionStrategyZero, + timeLockStrategy: timeLockStrategyDAI, + baseLTVAsCollateral: "7000", + liquidationThreshold: "9000", + liquidationProtocolFeePercentage: "0", + liquidationBonus: "10400", + borrowingEnabled: true, + reserveDecimals: "18", + xTokenImpl: eContractid.PTokenImpl, + reserveFactor: "1000", + borrowCap: "0", + supplyCap: "0", +}; + export const strategyDAI: IReserveParams = { strategy: rateStrategyDAI, auctionStrategy: auctionStrategyZero, @@ -902,6 +952,22 @@ export const strategyUniswapV3: IReserveParams = { supplyCap: "0", }; +export const strategyIZUMI: IReserveParams = { + strategy: rateStrategyNFT, + auctionStrategy: auctionStrategyIZUMI, + timeLockStrategy: timeLockStrategyIZUMI, + baseLTVAsCollateral: "3000", + liquidationProtocolFeePercentage: "0", + liquidationThreshold: "7000", + liquidationBonus: "10500", + borrowingEnabled: false, + reserveDecimals: "0", + xTokenImpl: eContractid.NTokenIZUMILpImpl, + reserveFactor: "0", + borrowCap: "0", + supplyCap: "0", +}; + export const strategySEWER: IReserveParams = { strategy: rateStrategyNFT, auctionStrategy: auctionStrategySEWER, // same as BAKC, it won't be used as collateral so it's ok diff --git a/market-config/timeLockStrategies.ts b/market-config/timeLockStrategies.ts index 5fc4d246a..bb9896524 100644 --- a/market-config/timeLockStrategies.ts +++ b/market-config/timeLockStrategies.ts @@ -605,6 +605,18 @@ export const timeLockStrategyUniswapV3: ITimeLockStrategyParams = { period: "86400", }; +export const timeLockStrategyIZUMI: ITimeLockStrategyParams = { + name: "timeLockStrategyIZUMI", + minThreshold: "5", + midThreshold: "10", + minWaitTime: "12", + midWaitTime: "7200", + maxWaitTime: "21600", + poolPeriodWaitTime: "600", + poolPeriodLimit: "50", + period: "86400", +}; + export const timeLockStrategySEWER: ITimeLockStrategyParams = { name: "timeLockStrategySEWER", minThreshold: "5", diff --git a/package.json b/package.json index 2b9547652..a347115e1 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "coverage": "hardhat coverage --testfiles 'test/*.ts'", "format": "prettier --write 'contracts/**/*.sol' 'scripts/**/*.ts' 'helpers/**/*.ts' 'tasks/**/*.ts' 'test/**/*.ts' 'hardhat.config.ts' 'helper-hardhat-config.ts' 'market-config/**/*.ts'", "doc": "hardhat docgen", - "test": "hardhat test ./test/*.ts", + "test": "hardhat test test/_uniswap_v2_*.spec.ts", "clean": "hardhat clean" }, "resolutions": { diff --git a/scripts/deployments/steps/06_pool.ts b/scripts/deployments/steps/06_pool.ts index 3348f9d8b..fdab1c726 100644 --- a/scripts/deployments/steps/06_pool.ts +++ b/scripts/deployments/steps/06_pool.ts @@ -37,11 +37,13 @@ export const step_06 = async (verify = false) => { poolMarketplace, poolApeStaking, poolBorrowAndStake, + poolLpOperation, poolCoreSelectors, poolParametersSelectors, poolMarketplaceSelectors, poolApeStakingSelectors, poolBorrowAndStakeSelectors, + poolLpOperationSelectors, } = await deployPoolComponents(addressesProvider.address, verify); const {poolParaProxyInterfaces, poolParaProxyInterfacesSelectors} = @@ -77,6 +79,21 @@ export const step_06 = async (verify = false) => { ) ); + await waitForTx( + await addressesProvider.updatePoolImpl( + [ + { + implAddress: poolLpOperation.address, + action: 0, + functionSelectors: poolLpOperationSelectors, + }, + ], + ZERO_ADDRESS, + "0x", + GLOBAL_OVERRIDES + ) + ); + if ( paraSpaceConfig.BendDAO.LendingPoolLoan || paraSpaceConfig.ParaSpaceV1 || diff --git a/scripts/deployments/steps/11_allReserves.ts b/scripts/deployments/steps/11_allReserves.ts index a8043e0f9..24976a998 100644 --- a/scripts/deployments/steps/11_allReserves.ts +++ b/scripts/deployments/steps/11_allReserves.ts @@ -89,6 +89,7 @@ export const step_11 = async (verify = false) => { xTokenImpl === eContractid.NTokenMAYCImpl || xTokenImpl === eContractid.NTokenMoonBirdsImpl || xTokenImpl === eContractid.NTokenUniswapV3Impl || + xTokenImpl === eContractid.NTokenIZUMILpImpl || xTokenImpl === eContractid.PTokenStETHImpl || xTokenImpl === eContractid.PTokenStKSMImpl || xTokenImpl === eContractid.PTokenATokenImpl || diff --git a/scripts/upgrade/ntoken.ts b/scripts/upgrade/ntoken.ts index bdeea10f2..645ef2e72 100644 --- a/scripts/upgrade/ntoken.ts +++ b/scripts/upgrade/ntoken.ts @@ -3,6 +3,7 @@ import { deployChromieSquiggleNTokenImpl, deployGenericMoonbirdNTokenImpl, deployGenericNTokenImpl, + deployIZUMILpNTokenImpl, deployNTokenBAKCImpl, deployNTokenBAYCImpl, deployNTokenMAYCImpl, @@ -46,6 +47,7 @@ export const upgradeNToken = async (verify = false) => { let nTokenBAKCImplementationAddress = ""; let nTokenMoonBirdImplementationAddress = ""; let nTokenUniSwapV3ImplementationAddress = ""; + let nTokenIZUMILpImplementationAddress = ""; let nTokenOTHRImplementationAddress = ""; let nTokenStakefishImplementationAddress = ""; let nTokenBlocksImplementationAddress = ""; @@ -64,6 +66,7 @@ export const upgradeNToken = async (verify = false) => { XTokenType.NToken, XTokenType.NTokenMoonBirds, XTokenType.NTokenUniswapV3, + XTokenType.NTokenIZUMILp, XTokenType.NTokenBAYC, XTokenType.NTokenMAYC, XTokenType.NTokenBAKC, @@ -141,6 +144,14 @@ export const upgradeNToken = async (verify = false) => { ).address; } newImpl = nTokenUniSwapV3ImplementationAddress; + } else if (xTokenType == XTokenType.NTokenIZUMILp) { + if (!nTokenIZUMILpImplementationAddress) { + console.log("deploy NTokenIZUMILp implementation"); + nTokenIZUMILpImplementationAddress = ( + await deployIZUMILpNTokenImpl(poolAddress, delegationRegistry, verify) + ).address; + } + newImpl = nTokenIZUMILpImplementationAddress; } else if (xTokenType == XTokenType.NTokenMoonBirds) { if (!nTokenMoonBirdImplementationAddress) { console.log("deploy NTokenMoonBirds implementation"); diff --git a/test/_data_providers.spec.ts b/test/_data_providers.spec.ts index e2df50677..94f356c5a 100644 --- a/test/_data_providers.spec.ts +++ b/test/_data_providers.spec.ts @@ -292,7 +292,7 @@ describe("UI Contracts Tests", () => { const uniV3Oracle = await getUniswapV3OracleWrapper(); const positionData = await uniV3Oracle.getOnchainPositionData(tokenId); - const data = await poolDataProvider.getUniswapV3LpTokenData( + const data = await poolDataProvider.getLiquidityTokenData( addressesProvider.address, nftPositionManager.address, tokenId diff --git a/test/_pool_lp_operation.spec.ts b/test/_pool_lp_operation.spec.ts new file mode 100644 index 000000000..0b321865b --- /dev/null +++ b/test/_pool_lp_operation.spec.ts @@ -0,0 +1,910 @@ +import {expect} from "chai"; +import {waitForTx} from "../helpers/misc-utils"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import { + createNewPool, + mintNewPosition, + fund, + approveTo, +} from "./helpers/uniswapv3-helper"; +import {encodeSqrtRatioX96, TickMath} from "@uniswap/v3-sdk"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {parseEther} from "ethers/lib/utils"; +import {supplyAndValidate} from "./helpers/validated-steps"; +import {VariableDebtToken} from "../types"; +import { + getIZUMIPositionManager, + getNTokenIZUMI, + getVariableDebtToken, +} from "../helpers/contracts-getters"; +import {createIzumiNewPool, mintIzumiNewPosition} from "./helpers/izumi-helper"; + +describe("Pool LP Operation", () => { + let variableDebtDai: VariableDebtToken; + let variableDebtWeth: VariableDebtToken; + + const uniFixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + + const { + users: [user1], + dai, + weth, + nftPositionManager, + nUniswapV3, + pool, + protocolDataProvider, + } = testEnv; + + const {variableDebtTokenAddress: variableDebtDaiAddress} = + await protocolDataProvider.getReserveTokensAddresses(dai.address); + const {variableDebtTokenAddress: variableDebtWethAddress} = + await protocolDataProvider.getReserveTokensAddresses(weth.address); + variableDebtDai = await getVariableDebtToken(variableDebtDaiAddress); + variableDebtWeth = await getVariableDebtToken(variableDebtWethAddress); + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + const nft = nftPositionManager.connect(user1.signer); + await approveTo({ + target: nftPositionManager.address, + token: dai, + user: user1, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: user1, + }); + const fee = 3000; + const tickSpacing = fee / 50; + const initialPrice = encodeSqrtRatioX96(1, 1000); + const lowerPrice = encodeSqrtRatioX96(1, 10000); + const upperPrice = encodeSqrtRatioX96(1, 100); + await createNewPool({ + positionManager: nft, + token0: dai, + token1: weth, + fee: fee, + initialSqrtPrice: initialPrice.toString(), + }); + await mintNewPosition({ + nft: nft, + token0: dai, + token1: weth, + fee: fee, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userDaiAmount, + token1Amount: userWethAmount, + }); + expect(await nftPositionManager.balanceOf(user1.address)).to.eq(1); + + await nft.setApprovalForAll(pool.address, true); + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 1, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + + expect(await nUniswapV3.balanceOf(user1.address)).to.eq(1); + + return testEnv; + }; + + const izumiFixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + + const { + users: [user1], + dai, + weth, + pool, + protocolDataProvider, + } = testEnv; + + const {variableDebtTokenAddress: variableDebtDaiAddress} = + await protocolDataProvider.getReserveTokensAddresses(dai.address); + const {variableDebtTokenAddress: variableDebtWethAddress} = + await protocolDataProvider.getReserveTokensAddresses(weth.address); + variableDebtDai = await getVariableDebtToken(variableDebtDaiAddress); + variableDebtWeth = await getVariableDebtToken(variableDebtWethAddress); + + const izumiPositionManager = await getIZUMIPositionManager(); + const nIzumiAddress = await pool.getReserveXToken( + izumiPositionManager.address + ); + const nTokenIzumi = await getNTokenIZUMI(nIzumiAddress); + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + const nft = izumiPositionManager.connect(user1.signer); + await approveTo({ + target: izumiPositionManager.address, + token: dai, + user: user1, + }); + await approveTo({ + target: izumiPositionManager.address, + token: weth, + user: user1, + }); + const fee = 2000; + const tickSpacing = fee / 50; + const initialPrice = encodeSqrtRatioX96(1, 1000); + const lowerPrice = encodeSqrtRatioX96(1, 10000); + const upperPrice = encodeSqrtRatioX96(1, 100); + await createIzumiNewPool({ + positionManager: nft, + token0: dai, + token1: weth, + fee: fee, + initialSqrtPrice: initialPrice, + }); + await mintIzumiNewPosition({ + nft: nft, + token0: dai, + token1: weth, + fee: fee, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userDaiAmount, + token1Amount: userWethAmount, + }); + expect(await izumiPositionManager.balanceOf(user1.address)).to.eq(1); + + await nft.setApprovalForAll(pool.address, true); + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + izumiPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + + expect(await nTokenIzumi.balanceOf(user1.address)).to.eq(1); + + return {...testEnv, izumiPositionManager, nTokenIzumi}; + }; + + it("uni: borrow and mint new position [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(uniFixture); + const { + users: [user1, supplier], + dai, + weth, + nftPositionManager, + pool, + } = testEnv; + + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const fee = 3000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: parseEther("1000"), + token1BorrowAmount: parseEther("1"), + }, + { + decreaseLiquidity: false, + tokenId: 1, + liquidityDecrease: 0, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 3000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("1000"), + amount1Desired: parseEther("1"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiDebt = await variableDebtDai.balanceOf(user1.address); + const wethDebt = await variableDebtWeth.balanceOf(user1.address); + expect(daiDebt).to.be.closeTo(parseEther("1000"), parseEther("1")); + expect(wethDebt).to.be.closeTo(parseEther("1"), parseEther("0.01")); + }); + + it("uni: adjust existing position: refund borrow [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(uniFixture); + const { + users: [user1, supplier], + dai, + weth, + nftPositionManager, + pool, + } = testEnv; + + await fund({token: dai, user: user1, amount: parseEther("10000")}); + await fund({token: weth, user: user1, amount: parseEther("10")}); + await approveTo({ + target: pool.address, + token: dai, + user: user1, + }); + await approveTo({ + target: pool.address, + token: weth, + user: user1, + }); + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; + + const fee = 3000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: parseEther("10000"), + token1CashAmount: parseEther("10"), + token0BorrowAmount: parseEther("10000"), + token1BorrowAmount: parseEther("10"), + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 3000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("25000"), + amount1Desired: parseEther("25"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiDebt = await variableDebtDai.balanceOf(user1.address); + const wethDebt = await variableDebtWeth.balanceOf(user1.address); + expect(daiDebt).to.be.closeTo(parseEther("5000"), parseEther("10")); + expect(wethDebt).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); + + it("uni: adjust existing position: refund cash [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(uniFixture); + const { + users: [user1, supplier], + dai, + weth, + nftPositionManager, + pool, + } = testEnv; + + await fund({token: dai, user: user1, amount: parseEther("20000")}); + await fund({token: weth, user: user1, amount: parseEther("20")}); + await approveTo({ + target: pool.address, + token: dai, + user: user1, + }); + await approveTo({ + target: pool.address, + token: weth, + user: user1, + }); + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; + + const fee = 3000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: parseEther("20000"), + token1CashAmount: parseEther("20"), + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 3000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("25000"), + amount1Desired: parseEther("25"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiBalance = await dai.balanceOf(user1.address); + const ethBalance = await weth.balanceOf(user1.address); + expect(daiBalance).to.be.closeTo(parseEther("5000"), parseEther("10")); + expect(ethBalance).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); + + it("uni: adjust existing position: refund liquidity [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(uniFixture); + const { + users: [user1, supplier], + dai, + pDai, + weth, + pWETH, + nftPositionManager, + pool, + } = testEnv; + + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; + + const fee = 3000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 3000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("5000"), + amount1Desired: parseEther("5"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const pDaiBalance = await pDai.balanceOf(user1.address); + const pEthBalance = await pWETH.balanceOf(user1.address); + expect(pDaiBalance).to.be.closeTo(parseEther("5000"), parseEther("10")); + expect(pEthBalance).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); + + it("uni: adjust existing position: refund all [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(uniFixture); + const { + users: [user1, supplier], + dai, + pDai, + weth, + pWETH, + nftPositionManager, + pool, + } = testEnv; + + await fund({token: dai, user: user1, amount: parseEther("10000")}); + await fund({token: weth, user: user1, amount: parseEther("10")}); + await approveTo({ + target: pool.address, + token: dai, + user: user1, + }); + await approveTo({ + target: pool.address, + token: weth, + user: user1, + }); + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; + + const fee = 3000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: parseEther("10000"), + token1CashAmount: parseEther("10"), + token0BorrowAmount: parseEther("10000"), + token1BorrowAmount: parseEther("10"), + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 3000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("5000"), + amount1Desired: parseEther("5"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiDebt = await variableDebtDai.balanceOf(user1.address); + const wethDebt = await variableDebtWeth.balanceOf(user1.address); + expect(daiDebt).to.be.eq("0"); + expect(wethDebt).to.be.eq("0"); + const daiBalance = await dai.balanceOf(user1.address); + const ethBalance = await weth.balanceOf(user1.address); + expect(daiBalance).to.be.closeTo(parseEther("10000"), parseEther("10")); + expect(ethBalance).to.be.closeTo(parseEther("10"), parseEther("1")); + const pDaiBalance = await pDai.balanceOf(user1.address); + const pEthBalance = await pWETH.balanceOf(user1.address); + expect(pDaiBalance).to.be.closeTo(parseEther("5000"), parseEther("10")); + expect(pEthBalance).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); + + it("izumi: borrow and mint new position [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(izumiFixture); + const { + users: [user1, supplier], + dai, + weth, + izumiPositionManager, + pool, + } = testEnv; + + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const fee = 2000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: izumiPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: parseEther("1000"), + token1BorrowAmount: parseEther("1"), + }, + { + decreaseLiquidity: false, + tokenId: 0, + liquidityDecrease: 0, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 2000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("1000"), + amount1Desired: parseEther("1"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiDebt = await variableDebtDai.balanceOf(user1.address); + const wethDebt = await variableDebtWeth.balanceOf(user1.address); + expect(daiDebt).to.be.closeTo(parseEther("1000"), parseEther("1")); + expect(wethDebt).to.be.closeTo(parseEther("1"), parseEther("0.01")); + }); + + it("izumi: adjust existing position: refund borrow [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(izumiFixture); + const { + users: [user1, supplier], + dai, + weth, + izumiPositionManager, + pool, + } = testEnv; + + await fund({token: dai, user: user1, amount: parseEther("10000")}); + await fund({token: weth, user: user1, amount: parseEther("10")}); + await approveTo({ + target: pool.address, + token: dai, + user: user1, + }); + await approveTo({ + target: pool.address, + token: weth, + user: user1, + }); + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await izumiPositionManager.liquidities(0)) + .liquidity; + + const fee = 2000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: izumiPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: parseEther("10000"), + token1CashAmount: parseEther("10"), + token0BorrowAmount: parseEther("10000"), + token1BorrowAmount: parseEther("10"), + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 2000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("25000"), + amount1Desired: parseEther("25"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiDebt = await variableDebtDai.balanceOf(user1.address); + const wethDebt = await variableDebtWeth.balanceOf(user1.address); + expect(daiDebt).to.be.closeTo(parseEther("5000"), parseEther("15")); + expect(wethDebt).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); + + it("izumi: adjust existing position: refund cash [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(izumiFixture); + const { + users: [user1, supplier], + dai, + weth, + izumiPositionManager, + pool, + } = testEnv; + + await fund({token: dai, user: user1, amount: parseEther("20000")}); + await fund({token: weth, user: user1, amount: parseEther("20")}); + await approveTo({ + target: pool.address, + token: dai, + user: user1, + }); + await approveTo({ + target: pool.address, + token: weth, + user: user1, + }); + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await izumiPositionManager.liquidities(0)) + .liquidity; + + const fee = 2000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: izumiPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: parseEther("20000"), + token1CashAmount: parseEther("20"), + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 2000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("25000"), + amount1Desired: parseEther("25"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiBalance = await dai.balanceOf(user1.address); + const ethBalance = await weth.balanceOf(user1.address); + expect(daiBalance).to.be.closeTo(parseEther("5000"), parseEther("15")); + expect(ethBalance).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); + + it("izumi: adjust existing position: refund liquidity [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(izumiFixture); + const { + users: [user1, supplier], + dai, + pDai, + weth, + pWETH, + izumiPositionManager, + pool, + } = testEnv; + + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await izumiPositionManager.liquidities(0)) + .liquidity; + + const fee = 2000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: izumiPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 2000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("5000"), + amount1Desired: parseEther("5"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const pDaiBalance = await pDai.balanceOf(user1.address); + const pEthBalance = await pWETH.balanceOf(user1.address); + expect(pDaiBalance).to.be.closeTo(parseEther("5000"), parseEther("15")); + expect(pEthBalance).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); + + it("izumi: adjust existing position: refund all [ @skip-on-coverage ]", async () => { + const testEnv = await loadFixture(izumiFixture); + const { + users: [user1, supplier], + dai, + pDai, + weth, + pWETH, + izumiPositionManager, + pool, + } = testEnv; + + await fund({token: dai, user: user1, amount: parseEther("10000")}); + await fund({token: weth, user: user1, amount: parseEther("10")}); + await approveTo({ + target: pool.address, + token: dai, + user: user1, + }); + await approveTo({ + target: pool.address, + token: weth, + user: user1, + }); + await supplyAndValidate(dai, "1000000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + const beforeLiquidity = (await izumiPositionManager.liquidities(0)) + .liquidity; + + const fee = 2000; + const tickSpacing = fee / 50; + const lowerPrice = encodeSqrtRatioX96(1, 20000); + const upperPrice = encodeSqrtRatioX96(1, 50); + let tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + let tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + (tickLower = Math.ceil(tickLower / tickSpacing) * tickSpacing), + (tickUpper = Math.floor(tickUpper / tickSpacing) * tickSpacing), + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: izumiPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: parseEther("10000"), + token1CashAmount: parseEther("10"), + token0BorrowAmount: parseEther("10000"), + token1BorrowAmount: parseEther("10"), + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity, + amount0Min: 0, + amount1Min: 1, + burnNFT: true, + }, + { + mintNewToken: true, + fee: 2000, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: parseEther("5000"), + amount1Desired: parseEther("5"), + amount0Min: 0, + amount1Min: 0, + deadline: 2659537628, + } + ) + ); + + const daiDebt = await variableDebtDai.balanceOf(user1.address); + const wethDebt = await variableDebtWeth.balanceOf(user1.address); + expect(daiDebt).to.be.eq("0"); + expect(wethDebt).to.be.eq("0"); + const daiBalance = await dai.balanceOf(user1.address); + const ethBalance = await weth.balanceOf(user1.address); + expect(daiBalance).to.be.closeTo(parseEther("10000"), parseEther("10")); + expect(ethBalance).to.be.closeTo(parseEther("10"), parseEther("1")); + const pDaiBalance = await pDai.balanceOf(user1.address); + const pEthBalance = await pWETH.balanceOf(user1.address); + expect(pDaiBalance).to.be.closeTo(parseEther("5000"), parseEther("15")); + expect(pEthBalance).to.be.closeTo(parseEther("5"), parseEther("0.1")); + }); +}); diff --git a/test/_timelock.spec.ts b/test/_timelock.spec.ts index 1191a4018..3354b9e0f 100644 --- a/test/_timelock.spec.ts +++ b/test/_timelock.spec.ts @@ -13,7 +13,14 @@ import {eContractid, ProtocolErrors} from "../helpers/types"; import {testEnvFixture} from "./helpers/setup-env"; import {supplyAndValidate} from "./helpers/validated-steps"; import {parseEther} from "ethers/lib/utils"; -import {almostEqual} from "./helpers/uniswapv3-helper"; +import { + almostEqual, + approveTo, + createNewPool, + fund, + mintNewPosition, +} from "./helpers/uniswapv3-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; describe("TimeLock functionality tests", () => { const minTime = 5; @@ -32,6 +39,7 @@ describe("TimeLock functionality tests", () => { wPunk, users: [user1, user2], poolAdmin, + nftPositionManager, } = testEnv; // User 1 - Deposit dai @@ -43,6 +51,28 @@ describe("TimeLock functionality tests", () => { await supplyAndValidate(weth, "1", user1, true); + //uniswap V3 + const nft = nftPositionManager.connect(user1.signer); + await approveTo({ + target: nftPositionManager.address, + token: dai, + user: user1, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: user1, + }); + const fee = 3000; + const initialPrice = encodeSqrtRatioX96(1, 1000); + await createNewPool({ + positionManager: nft, + token0: dai, + token1: weth, + fee: fee, + initialSqrtPrice: initialPrice.toString(), + }); + const minThreshold = await convertToCurrencyDecimals(usdc.address, "1000"); const midThreshold = await convertToCurrencyDecimals(usdc.address, "2000"); const minThresholdNFT = 2; @@ -74,6 +104,19 @@ describe("TimeLock functionality tests", () => { (24 * 3600).toString() ); + const UniswapV3Strategy = await deployReserveTimeLockStrategy( + eContractid.DefaultTimeLockStrategy + "UniV3", + pool.address, + parseEther("1").toString(), + parseEther("10").toString(), + minTime.toString(), + midTime.toString(), + maxTime.toString(), + parseEther("100").toString(), + (12 * 3600).toString(), + (24 * 3600).toString() + ); + const poolConfigurator = await getPoolConfiguratorProxy(); await waitForTx( await poolConfigurator @@ -99,6 +142,14 @@ describe("TimeLock functionality tests", () => { defaultStrategyNFT.address ) ); + await waitForTx( + await poolConfigurator + .connect(poolAdmin.signer) + .setReserveTimeLockStrategyAddress( + nftPositionManager.address, + UniswapV3Strategy.address + ) + ); await waitForTx( await poolConfigurator .connect(poolAdmin.signer) @@ -506,6 +557,140 @@ describe("TimeLock functionality tests", () => { await expect(balanceAfter).to.be.eq(balanceBefore.add(3)); }); + it("withdraw UniswapV3 tokens below minThreshold should be time locked for 1 block only", async () => { + const { + pool, + users: [user1], + dai, + weth, + nftPositionManager, + } = await loadFixture(fixture); + + const userDaiAmount = parseEther("1"); + const userWethAmount = parseEther("0.1"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + + const tickSpacing = 3000 / 50; + const lowerPrice = encodeSqrtRatioX96(1, 10000); + const upperPrice = encodeSqrtRatioX96(1, 100); + const nft = nftPositionManager.connect(user1.signer); + await mintNewPosition({ + nft: nft, + token0: dai, + token1: weth, + fee: 3000, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userDaiAmount, + token1Amount: userWethAmount, + }); + expect(await nftPositionManager.balanceOf(user1.address)).to.eq(1); + + await nft.setApprovalForAll(pool.address, true); + + let uniswapV3Balance = await nftPositionManager.balanceOf(user1.address); + expect(uniswapV3Balance).to.be.eq(1); + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 1, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [1], user1.address, { + gasLimit: 5000000, + }) + ); + + await advanceTimeAndBlock(10); + await waitForTx(await timeLockProxy.connect(user1.signer).claim(["0"])); + uniswapV3Balance = await nftPositionManager.balanceOf(user1.address); + expect(uniswapV3Balance).to.be.eq(1); + }); + + it("withdraw UniswapV3 tokens above midThreshold should be time locked for 3600 seconds", async () => { + const { + pool, + users: [user1], + dai, + weth, + nftPositionManager, + } = await loadFixture(fixture); + + const userDaiAmount = parseEther("10000"); + const userWethAmount = parseEther("10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + + const tickSpacing = 3000 / 50; + const lowerPrice = encodeSqrtRatioX96(1, 10000); + const upperPrice = encodeSqrtRatioX96(1, 100); + const nft = nftPositionManager.connect(user1.signer); + await mintNewPosition({ + nft: nft, + token0: dai, + token1: weth, + fee: 3000, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userDaiAmount, + token1Amount: userWethAmount, + }); + expect(await nftPositionManager.balanceOf(user1.address)).to.eq(1); + + await nft.setApprovalForAll(pool.address, true); + + let uniswapV3Balance = await nftPositionManager.balanceOf(user1.address); + expect(uniswapV3Balance).to.be.eq(1); + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 1, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [1], user1.address, { + gasLimit: 5000000, + }) + ); + + await advanceTimeAndBlock(10); + await expect(timeLockProxy.connect(user1.signer).claim(["0"])).to.be + .reverted; + + await advanceTimeAndBlock(4000); + await waitForTx(await timeLockProxy.connect(user1.signer).claim(["0"])); + uniswapV3Balance = await nftPositionManager.balanceOf(user1.address); + expect(uniswapV3Balance).to.be.eq(1); + }); + it("non-pool admin cannot update timeLockWhiteList", async () => { const { users: [user1], diff --git a/test/_uniswap_v2_oracle_test.spec.ts b/test/_uniswap_v2_oracle_test.spec.ts new file mode 100644 index 000000000..cd50893fb --- /dev/null +++ b/test/_uniswap_v2_oracle_test.spec.ts @@ -0,0 +1,314 @@ +import {expect} from "chai"; +import {fund, approveTo} from "./helpers/uniswapv3-helper"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {parseEther} from "ethers/lib/utils"; +import {changePriceAndValidate} from "./helpers/validated-steps"; +import {getERC20} from "../helpers/contracts-getters"; +import {BigNumber, BigNumberish} from "ethers"; + +function almostEqual(value0: BigNumberish, value1: BigNumberish) { + const maxDiff = BigNumber.from(value0.toString()).mul(3).div("1000").abs(); + const abs = BigNumber.from(value0.toString()).sub(value1.toString()).abs(); + if (!abs.lte(maxDiff)) { + console.log("---------value0=" + value0 + ", --------value1=" + value1); + } + expect(abs.lte(maxDiff)).to.be.equal(true); +} + +describe("UniswapV2 Oracle Wrapper Test", () => { + it("test with dai and weth:(token0 decimal equals token1 decimal)", async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + users: [user1, swapper], + dai, + weth, + uniswapv2Factory, + uniswapv2Router, + paraspaceOracle, + } = testEnv; + + const userDaiAmount = parseEther("1000"); + const userWethAmount = parseEther("1"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: dai, + user: user1, + }); + await approveTo({ + target: uniswapv2Router.address, + token: weth, + user: user1, + }); + + const weth_dai_address = await uniswapv2Factory.getPair( + weth.address, + dai.address + ); + await uniswapv2Router + .connect(user1.signer) + .addLiquidity( + weth.address, + dai.address, + userWethAmount, + userDaiAmount, + 0, + 0, + user1.address, + 2659537628 + ); + const weth_dai = await getERC20(weth_dai_address); + + await changePriceAndValidate(dai, "0.001"); + let lp_price = await paraspaceOracle.getAssetPrice(weth_dai_address); + const total_supply = await weth_dai.totalSupply(); + const expectedPrice = parseEther("1") + .mul(2) + .mul(parseEther("1")) + .div(total_supply); + expect(lp_price).to.be.eq(expectedPrice); + + const swapAmount = parseEther("200"); + await fund({token: dai, user: swapper, amount: swapAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: dai, + user: swapper, + }); + await uniswapv2Router + .connect(swapper.signer) + .swapExactTokensForTokens( + swapAmount, + 0, + [dai.address, weth.address], + swapper.address, + 2659537628 + ); + lp_price = await paraspaceOracle.getAssetPrice(weth_dai_address); + almostEqual(lp_price, expectedPrice); + }); + + it("test with usdc and weth:(token0 decimal less than token1 decimal)", async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + users: [user1, swapper], + usdc, + weth, + uniswapv2Factory, + uniswapv2Router, + paraspaceOracle, + } = testEnv; + + const userUsdcAmount = "1000000000"; + const userWethAmount = parseEther("1"); + await fund({token: usdc, user: user1, amount: userUsdcAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: usdc, + user: user1, + }); + await approveTo({ + target: uniswapv2Router.address, + token: weth, + user: user1, + }); + + const weth_usdc_address = await uniswapv2Factory.getPair( + weth.address, + usdc.address + ); + await uniswapv2Router + .connect(user1.signer) + .addLiquidity( + weth.address, + usdc.address, + userWethAmount, + userUsdcAmount, + 0, + 0, + user1.address, + 2659537628 + ); + const weth_usdc = await getERC20(weth_usdc_address); + + await changePriceAndValidate(usdc, "0.001"); + let lp_price = await paraspaceOracle.getAssetPrice(weth_usdc_address); + const total_supply = await weth_usdc.totalSupply(); + const expectedPrice = parseEther("1") + .mul(2) + .mul(parseEther("1")) + .div(total_supply); + expect(lp_price).to.be.eq(expectedPrice); + + const swapAmount = "2000000000"; + await fund({token: usdc, user: swapper, amount: swapAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: usdc, + user: swapper, + }); + await uniswapv2Router + .connect(swapper.signer) + .swapExactTokensForTokens( + swapAmount, + 0, + [usdc.address, weth.address], + swapper.address, + 2659537628 + ); + lp_price = await paraspaceOracle.getAssetPrice(weth_usdc_address); + almostEqual(lp_price, expectedPrice); + }); + + it("test with weth and usdt:(token0 decimal greater than token1 decimal)", async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + users: [user1, swapper], + usdt, + weth, + uniswapv2Factory, + uniswapv2Router, + paraspaceOracle, + } = testEnv; + + const userUsdtAmount = "1000000000"; + const userWethAmount = parseEther("1"); + await fund({token: usdt, user: user1, amount: userUsdtAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: usdt, + user: user1, + }); + await approveTo({ + target: uniswapv2Router.address, + token: weth, + user: user1, + }); + + const weth_usdt_address = await uniswapv2Factory.getPair( + weth.address, + usdt.address + ); + await uniswapv2Router + .connect(user1.signer) + .addLiquidity( + weth.address, + usdt.address, + userWethAmount, + userUsdtAmount, + 0, + 0, + user1.address, + 2659537628 + ); + const weth_usdt = await getERC20(weth_usdt_address); + + await changePriceAndValidate(usdt, "0.001"); + let lp_price = await paraspaceOracle.getAssetPrice(weth_usdt_address); + const total_supply = await weth_usdt.totalSupply(); + const expectedPrice = parseEther("1") + .mul(2) + .mul(parseEther("1")) + .div(total_supply); + expect(lp_price).to.be.eq(expectedPrice); + + const swapAmount = "2000000000"; + await fund({token: usdt, user: swapper, amount: swapAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: usdt, + user: swapper, + }); + await uniswapv2Router + .connect(swapper.signer) + .swapExactTokensForTokens( + swapAmount, + 0, + [usdt.address, weth.address], + swapper.address, + 2659537628 + ); + lp_price = await paraspaceOracle.getAssetPrice(weth_usdt_address); + almostEqual(lp_price, expectedPrice); + }); + + it("test with fee on", async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + users: [user1, swapper], + dai, + weth, + uniswapv2Factory, + uniswapv2Router, + paraspaceOracle, + deployer, + } = testEnv; + + await uniswapv2Factory.connect(deployer.signer).setFeeTo(deployer.address); + + const userDaiAmount = parseEther("1000"); + const userWethAmount = parseEther("1"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: dai, + user: user1, + }); + await approveTo({ + target: uniswapv2Router.address, + token: weth, + user: user1, + }); + + const weth_dai_address = await uniswapv2Factory.getPair( + weth.address, + dai.address + ); + await uniswapv2Router + .connect(user1.signer) + .addLiquidity( + weth.address, + dai.address, + userWethAmount, + userDaiAmount, + 0, + 0, + user1.address, + 2659537628 + ); + const weth_dai = await getERC20(weth_dai_address); + + await changePriceAndValidate(dai, "0.001"); + let lp_price = await paraspaceOracle.getAssetPrice(weth_dai_address); + const total_supply = await weth_dai.totalSupply(); + const expectedPrice = parseEther("1") + .mul(2) + .mul(parseEther("1")) + .div(total_supply); + expect(lp_price).to.be.eq(expectedPrice); + + const swapAmount = parseEther("200"); + await fund({token: dai, user: swapper, amount: swapAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: dai, + user: swapper, + }); + await uniswapv2Router + .connect(swapper.signer) + .swapExactTokensForTokens( + swapAmount, + 0, + [dai.address, weth.address], + swapper.address, + 2659537628 + ); + lp_price = await paraspaceOracle.getAssetPrice(weth_dai_address); + almostEqual(lp_price, expectedPrice); + }); +}); diff --git a/test/_uniswap_v2_pool_operation.spec.ts b/test/_uniswap_v2_pool_operation.spec.ts new file mode 100644 index 000000000..d69cf8bc8 --- /dev/null +++ b/test/_uniswap_v2_pool_operation.spec.ts @@ -0,0 +1,238 @@ +import {expect} from "chai"; +import {fund, approveTo} from "./helpers/uniswapv3-helper"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {parseEther} from "ethers/lib/utils"; +import { + borrowAndValidate, + changePriceAndValidate, + supplyAndValidate, +} from "./helpers/validated-steps"; +import {getUniswapV2Pair} from "../helpers/contracts-getters"; +import {BigNumber, BigNumberish} from "ethers"; +import {waitForTx} from "../helpers/misc-utils"; +import {MAX_UINT_AMOUNT} from "../helpers/constants"; + +function almostEqual(value0: BigNumberish, value1: BigNumberish) { + const maxDiff = BigNumber.from(value0.toString()).mul(3).div("1000").abs(); + const abs = BigNumber.from(value0.toString()).sub(value1.toString()).abs(); + if (!abs.lte(maxDiff)) { + console.log("---------value0=" + value0 + ", --------value1=" + value1); + } + expect(abs.lte(maxDiff)).to.be.equal(true); +} + +describe("UniswapV2 Lending Test", () => { + it("lp token can be supplied to borrow other asset", async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + users: [user1, supplier, liquidator], + dai, + weth, + uniswapv2Factory, + uniswapv2Router, + pool, + } = testEnv; + + await supplyAndValidate(dai, "10000000", supplier, true); + + const userDaiAmount = parseEther("1000"); + const userWethAmount = parseEther("1"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: dai, + user: user1, + }); + await approveTo({ + target: uniswapv2Router.address, + token: weth, + user: user1, + }); + + const weth_dai_address = await uniswapv2Factory.getPair( + weth.address, + dai.address + ); + await uniswapv2Router + .connect(user1.signer) + .addLiquidity( + weth.address, + dai.address, + userWethAmount, + userDaiAmount, + 0, + 0, + user1.address, + 2659537628 + ); + const weth_dai = await getUniswapV2Pair(weth_dai_address); + const lp_balance = await weth_dai.balanceOf(user1.address); + await waitForTx( + await weth_dai + .connect(user1.signer) + .approve(pool.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await pool + .connect(user1.signer) + .supply(weth_dai_address, lp_balance, user1.address, 0) + ); + + await changePriceAndValidate(dai, "0.001"); + let userData = await pool.getUserAccountData(user1.address); + almostEqual(userData.totalCollateralBase, parseEther("2")); + almostEqual(userData.availableBorrowsBase, parseEther("1.4")); + almostEqual(userData.totalDebtBase, 0); + + await borrowAndValidate(dai, "1300", user1); + userData = await pool.getUserAccountData(user1.address); + almostEqual(userData.totalCollateralBase, parseEther("2")); + almostEqual(userData.availableBorrowsBase, parseEther("0.1")); + almostEqual(userData.totalDebtBase, parseEther("1.3")); + + await changePriceAndValidate(dai, "0.01"); + userData = await pool.getUserAccountData(user1.address); + almostEqual(userData.availableBorrowsBase, 0); + almostEqual(userData.totalDebtBase, parseEther("13")); + expect(userData.healthFactor).to.be.lt(parseEther("1")); + + //liquidation + await fund({token: dai, user: liquidator, amount: parseEther("1300")}); + await approveTo({ + target: pool.address, + token: dai, + user: liquidator, + }); + await pool + .connect(liquidator.signer) + .liquidateERC20( + weth_dai_address, + dai.address, + user1.address, + parseEther("1300"), + false + ); + expect(await weth_dai.balanceOf(liquidator.address)).to.be.eq(lp_balance); + }); + + it("lp token can be borrowed", async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + users: [user1, supplier, liquidator], + dai, + weth, + uniswapv2Factory, + uniswapv2Router, + pool, + } = testEnv; + + const userDaiAmount = parseEther("10000"); + const userWethAmount = parseEther("10"); + await fund({token: dai, user: supplier, amount: userDaiAmount}); + await fund({token: weth, user: supplier, amount: userWethAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: dai, + user: supplier, + }); + await approveTo({ + target: uniswapv2Router.address, + token: weth, + user: supplier, + }); + + const weth_dai_address = await uniswapv2Factory.getPair( + weth.address, + dai.address + ); + await uniswapv2Router + .connect(supplier.signer) + .addLiquidity( + weth.address, + dai.address, + userWethAmount, + userDaiAmount, + 0, + 0, + supplier.address, + 2659537628 + ); + const weth_dai = await getUniswapV2Pair(weth_dai_address); + const lp_balance = await weth_dai.balanceOf(supplier.address); + await waitForTx( + await weth_dai + .connect(supplier.signer) + .approve(pool.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await pool + .connect(supplier.signer) + .supply(weth_dai_address, lp_balance, supplier.address, 0) + ); + + await supplyAndValidate(dai, "3000", user1, true); + await changePriceAndValidate(dai, "0.001"); + let userData = await pool.getUserAccountData(user1.address); + almostEqual(userData.totalCollateralBase, parseEther("3")); + almostEqual(userData.availableBorrowsBase, parseEther("2.31")); + almostEqual(userData.totalDebtBase, 0); + + //borrow 1/10 of the lp, value 2ETH + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth_dai_address, lp_balance.div(10), 0, user1.address) + ); + userData = await pool.getUserAccountData(user1.address); + almostEqual(userData.totalCollateralBase, parseEther("3")); + almostEqual(userData.availableBorrowsBase, parseEther("0.31")); + almostEqual(userData.totalDebtBase, parseEther("2")); + + await changePriceAndValidate(dai, "0.0001"); + userData = await pool.getUserAccountData(user1.address); + almostEqual(userData.availableBorrowsBase, 0); + expect(userData.healthFactor).to.be.lt(parseEther("1")); + + //liquidation + await fund({token: dai, user: liquidator, amount: userDaiAmount}); + await fund({token: weth, user: liquidator, amount: userWethAmount}); + await approveTo({ + target: uniswapv2Router.address, + token: dai, + user: liquidator, + }); + await approveTo({ + target: uniswapv2Router.address, + token: weth, + user: liquidator, + }); + await uniswapv2Router + .connect(liquidator.signer) + .addLiquidity( + weth.address, + dai.address, + userWethAmount, + userDaiAmount, + 0, + 0, + liquidator.address, + 2659537628 + ); + await waitForTx( + await weth_dai + .connect(liquidator.signer) + .approve(pool.address, MAX_UINT_AMOUNT) + ); + await pool + .connect(liquidator.signer) + .liquidateERC20( + dai.address, + weth_dai_address, + user1.address, + lp_balance.div(10), + false + ); + }); +}); diff --git a/test/_uniswapv3_pool_operation.spec.ts b/test/_uniswapv3_pool_operation.spec.ts index 3b289bc35..65b7c07b6 100644 --- a/test/_uniswapv3_pool_operation.spec.ts +++ b/test/_uniswapv3_pool_operation.spec.ts @@ -434,6 +434,7 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf const { users: [user1], configurator, + dai, weth, pool, nftPositionManager, @@ -444,25 +445,43 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; await expect( - pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.div(2), - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) + pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); }); it("decreaseUniswapV3Liquidity success if underlying erc20 was active [ @skip-on-coverage ]", async () => { const { users: [user1], + dai, weth, pool, configurator, @@ -476,19 +495,36 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; await waitForTx( - await pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.div(2), - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) ); await snapshot.revert(preLiquidationSnapshot); @@ -498,6 +534,7 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf const { users: [user1], configurator, + dai, weth, pool, nftPositionManager, @@ -508,25 +545,43 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; await expect( - pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.div(2), - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) + pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); }); it("decreaseUniswapV3Liquidity success if underlying erc20 was not paused [ @skip-on-coverage ]", async () => { const { users: [user1], + dai, weth, pool, nftPositionManager, @@ -540,43 +595,77 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; await waitForTx( - await pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.div(2), - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) ); await snapshot.revert(preLiquidationSnapshot); }); it("decreaseUniswapV3Liquidity failed if not owner [ @skip-on-coverage ]", async () => { - const {users, pool, nftPositionManager} = testEnv; + const {users, dai, weth, pool, nftPositionManager} = testEnv; const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; await expect( - pool - .connect(users[1].signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.div(2), - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) + pool.connect(users[1].signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) ).to.be.revertedWith(ProtocolErrors.NOT_THE_OWNER); }); @@ -675,34 +764,6 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf ); }); - it("decreaseUniswapV3Liquidity failed if hf < 1 [ @skip-on-coverage ]", async () => { - const { - users: [user1], - pool, - nftPositionManager, - } = testEnv; - // get current liquidity - const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; - - await expect( - pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.mul(3).div(4), - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) - ).to.be.revertedWith( - ProtocolErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD - ); - }); - it("transfer failed if hf < 1 [ @skip-on-coverage ]", async () => { const { users: [user1, user2], diff --git a/test/_uniswapv3_position_control.spec.ts b/test/_uniswapv3_position_control.spec.ts index b84badfae..94f02ba98 100644 --- a/test/_uniswapv3_position_control.spec.ts +++ b/test/_uniswapv3_position_control.spec.ts @@ -12,7 +12,6 @@ import { approveTo, } from "./helpers/uniswapv3-helper"; import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; -import {DRE} from "../helpers/misc-utils"; import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {testEnvFixture} from "./helpers/setup-env"; @@ -91,16 +90,17 @@ describe("Uniswap V3 NFT position control", () => { expect(await nUniswapV3.balanceOf(user1.address)).to.eq(1); }); - it("increaseLiquidity by nftPositionManager [ @skip-on-coverage ]", async () => { + it("increaseLiquidity by nUniswapV3 [ @skip-on-coverage ]", async () => { const { users: [user1], dai, weth, nftPositionManager, nUniswapV3, + pool, } = testEnv; - const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "20000"); const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); await fund({token: dai, user: user1, amount: userDaiAmount}); await fund({token: weth, user: user1, amount: userWethAmount}); @@ -109,79 +109,61 @@ describe("Uniswap V3 NFT position control", () => { const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; - const encodedData0 = nftPositionManager.interface.encodeFunctionData( - "increaseLiquidity", - [ - { - tokenId: 1, - amount0Desired: userDaiAmount, - amount1Desired: userWethAmount, - amount0Min: 0, - amount1Min: 0, - deadline: 2659537628, - }, - ] - ); - - const Multicall = await DRE.ethers.getContractAt( - "IMulticall", - nftPositionManager.address - ); await waitForTx( - await Multicall.connect(user1.signer).multicall([encodedData0], { - gasLimit: 12_450_000, - }) + await pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 1, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628 + ) ); const afterLiquidity = (await nftPositionManager.positions(1)).liquidity; almostEqual(afterLiquidity, beforeLiquidity.mul(2)); + //test refund. fund userDaiAmount , should left userDaiAmount. + almostEqual(await dai.balanceOf(user1.address), userDaiAmount.div(2)); }); - it("increaseLiquidity with ETH by nftPositionManager [ @skip-on-coverage ]", async () => { + it("increaseLiquidity with ETH by nUniswapV3 [ @skip-on-coverage ]", async () => { const { users: [user1], + pool, dai, weth, nftPositionManager, + nUniswapV3, } = testEnv; const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); const userWethAmount = await convertToCurrencyDecimals(weth.address, "20"); await fund({token: dai, user: user1, amount: userDaiAmount}); + await approveTo({target: nUniswapV3.address, token: dai, user: user1}); const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; const beforeBalance = await user1.signer.getBalance(); - const encodedData0 = nftPositionManager.interface.encodeFunctionData( - "increaseLiquidity", - [ - { - tokenId: 1, - amount0Desired: userDaiAmount, - amount1Desired: userWethAmount, - amount0Min: 0, - amount1Min: 0, - deadline: 2659537628, - }, - ] - ); - const encodedData1 = - nftPositionManager.interface.encodeFunctionData("refundETH"); - - const Multicall = await DRE.ethers.getContractAt( - "IMulticall", - nftPositionManager.address - ); - await waitForTx( - await Multicall.connect(user1.signer).multicall( - [encodedData0, encodedData1], - { - gasLimit: 12_450_000, - value: userWethAmount, - } - ) + await pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 1, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628, + { + gasLimit: 12_450_000, + value: userWethAmount, + } + ) ); const afterLiquidity = (await nftPositionManager.positions(1)).liquidity; @@ -196,9 +178,12 @@ describe("Uniswap V3 NFT position control", () => { const { users: [user1], dai, + pDai, weth, + pWETH, nftPositionManager, pool, + protocolDataProvider, } = testEnv; const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); @@ -208,76 +193,80 @@ describe("Uniswap V3 NFT position control", () => { const beforeEthBalance = await weth.balanceOf(user1.address); const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; + let userReserveData = await protocolDataProvider.getUserReserveData( + dai.address, + user1.address + ); + expect(userReserveData.usageAsCollateralEnabled).to.be.false; + userReserveData = await protocolDataProvider.getUserReserveData( + weth.address, + user1.address + ); + expect(userReserveData.usageAsCollateralEnabled).to.be.false; + await waitForTx( - await pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.div(3), - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: beforeLiquidity.div(3), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) ); - const afterDaiBalance = await dai.balanceOf(user1.address); - const afterEthBalance = await weth.balanceOf(user1.address); + const afterDaiBalance = (await dai.balanceOf(user1.address)).add( + await pDai.balanceOf(user1.address) + ); + const afterEthBalance = (await weth.balanceOf(user1.address)).add( + await pWETH.balanceOf(user1.address) + ); const afterLiquidity = (await nftPositionManager.positions(1)).liquidity; almostEqual(afterDaiBalance.sub(beforeDaiBalance), userDaiAmount); almostEqual(afterEthBalance.sub(beforeEthBalance), userWethAmount); almostEqual(afterLiquidity, beforeLiquidity.div(3).mul(2)); - }); - - it("decreaseLiquidity with ETH by NTokenUniswapV3 [ @skip-on-coverage ]", async () => { - const { - users: [user1], - dai, - weth, - nftPositionManager, - pool, - } = testEnv; - const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); - const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); - - const beforeDaiBalance = await dai.balanceOf(user1.address); - const beforeBalance = await user1.signer.getBalance(); - const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; - - await waitForTx( - await pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - beforeLiquidity.div(2), - 0, - 0, - true, - { - gasLimit: 12_450_000, - } - ) + userReserveData = await protocolDataProvider.getUserReserveData( + dai.address, + user1.address ); - const afterDaiBalance = await dai.balanceOf(user1.address); - const afterBalance = await user1.signer.getBalance(); - const afterLiquidity = (await nftPositionManager.positions(1)).liquidity; - - almostEqual(afterDaiBalance.sub(beforeDaiBalance), userDaiAmount); - almostEqual(afterBalance.sub(beforeBalance), userWethAmount); - almostEqual(afterLiquidity, beforeLiquidity.div(2)); + expect(userReserveData.usageAsCollateralEnabled).to.be.true; + userReserveData = await protocolDataProvider.getUserReserveData( + weth.address, + user1.address + ); + expect(userReserveData.usageAsCollateralEnabled).to.be.true; }); it("collect fee by decreaseLiquidity by NTokenUniswapV3 [ @skip-on-coverage ]", async () => { const { users: [user1, trader], dai, + pDai, weth, nftPositionManager, pool, @@ -301,33 +290,53 @@ describe("Uniswap V3 NFT position control", () => { }); const beforeDaiBalance = await dai.balanceOf(user1.address); + const beforepDaiBalance = await pDai.balanceOf(user1.address); const beforeEthBalance = await weth.balanceOf(user1.address); const beforeLiquidity = (await nftPositionManager.positions(1)).liquidity; await waitForTx( - await pool - .connect(user1.signer) - .decreaseUniswapV3Liquidity( - nftPositionManager.address, - 1, - 0, - 0, - 0, - false, - { - gasLimit: 12_450_000, - } - ) + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 1, + liquidityDecrease: 0, + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) ); const afterDaiBalance = await dai.balanceOf(user1.address); + const afterpDaiBalance = await pDai.balanceOf(user1.address); const afterEthBalance = await weth.balanceOf(user1.address); const afterLiquidity = (await nftPositionManager.positions(1)).liquidity; expect(afterEthBalance).to.eq(beforeEthBalance); + expect(afterDaiBalance).to.eq(beforeDaiBalance); expect(afterLiquidity).to.eq(beforeLiquidity); almostEqual( - afterDaiBalance.sub(beforeDaiBalance), + afterpDaiBalance.sub(beforepDaiBalance), await convertToCurrencyDecimals(dai.address, "3") ); }); diff --git a/test/_xtoken_ntoken_uniswapv3.spec.ts b/test/_xtoken_ntoken_uniswapv3.spec.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/helpers/izumi-helper.ts b/test/helpers/izumi-helper.ts new file mode 100644 index 000000000..d853f243d --- /dev/null +++ b/test/helpers/izumi-helper.ts @@ -0,0 +1,151 @@ +import { + getIZUMISwapRouter, + getIZUMIFactory, +} from "../../helpers/contracts-getters"; +import {TickMath} from "@uniswap/v3-sdk"; +import {BigNumber, BigNumberish} from "ethers"; +import {expect} from "chai"; +import {waitForTx} from "../../helpers/misc-utils"; +import {MAX_UINT_AMOUNT} from "../../helpers/constants"; +import {IZiSwapPool__factory} from "../../types"; +import {VERBOSE} from "../../helpers/hardhat-constants"; + +export function almostEqual(value0: BigNumberish, value1: BigNumberish) { + const maxDiff = BigNumber.from(value0.toString()).div("1000").abs(); + const abs = BigNumber.from(value0.toString()).sub(value1.toString()).abs(); + if (!abs.lte(maxDiff) && VERBOSE) { + console.log("---------value0=" + value0 + ", --------value1=" + value1); + } + expect(abs.lte(maxDiff)).to.be.equal(true); +} + +export const fund = async ({token, user, amount}) => { + await waitForTx(await token.connect(user.signer)["mint(uint256)"](amount)); +}; + +// Give user dai and weth +export const approveTo = async ({token, target, user}) => { + await waitForTx( + await token.connect(user.signer).approve(target, MAX_UINT_AMOUNT) + ); +}; + +export const createIzumiNewPool = async ({ + positionManager, + token0, + token1, + fee, + initialSqrtPrice, +}) => { + await waitForTx( + await positionManager.createPool( + token0.address, + token1.address, + fee, + TickMath.getTickAtSqrtRatio(initialSqrtPrice), + { + gasLimit: 12_450_000, + } + ) + ); +}; + +export const mintIzumiNewPosition = async ({ + nft, + token0, + token1, + fee, + user, + tickSpacing, + lowerPrice, + upperPrice, + token0Amount, + token1Amount, +}) => { + const tickLower = TickMath.getTickAtSqrtRatio(lowerPrice); + const tickUpper = TickMath.getTickAtSqrtRatio(upperPrice); + const minter = await nft.mint( + { + miner: user.address, + tokenX: token0.address, + tokenY: token1.address, + fee, + pl: Math.ceil(tickLower / tickSpacing) * tickSpacing, + pr: Math.floor(tickUpper / tickSpacing) * tickSpacing, + xLim: token0Amount, + yLim: token1Amount, + amountXMin: 0, + amountYMin: 0, + deadline: 2659537628, + }, + { + gasLimit: 12_450_000, + } + ); + await waitForTx(minter); +}; + +export const approveSwapRouter = async ({token, user}) => { + const swapRouter = await getIZUMISwapRouter(); + await waitForTx( + await token + .connect(user.signer) + .approve(swapRouter.address, MAX_UINT_AMOUNT) + ); +}; + +export const swapToken = async ({ + tokenIn, + tokenOut, + fee, + amountIn, + trader, + zeroForOne, +}) => { + const swapRouter = await getIZUMISwapRouter(); + if (zeroForOne) { + await waitForTx( + await swapRouter.connect(trader.signer).swapX2Y( + { + tokenX: tokenIn.address, + tokenY: tokenOut.address, + fee: fee, + boundaryPt: -800001, + recipient: trader.address, + amount: amountIn, + maxPayed: 0, + minAcquired: 0, + deadline: 2659537628, + }, + { + gasLimit: 12_450_000, + } + ) + ); + } else { + await waitForTx( + await swapRouter.connect(trader.signer).swapY2X( + { + tokenX: tokenOut.address, + tokenY: tokenIn.address, + fee: fee, + boundaryPt: 800001, + recipient: trader.address, + amount: amountIn, + maxPayed: 0, + minAcquired: 0, + deadline: 2659537628, + }, + { + gasLimit: 12_450_000, + } + ) + ); + } +}; + +export const getIZUMIPool = async ({token0, token1, fee}) => { + const factory = await getIZUMIFactory(); + const poolAddress = await factory.pool(token0.address, token1.address, fee); + return IZiSwapPool__factory.connect(poolAddress, factory.provider); +}; diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index d77760b17..8855e665d 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -56,6 +56,8 @@ import { getStakefishNFTManager, getNTokenStakefish, getTimeLockProxy, + getUniswapV2Factory, + getUniswapV2Router, } from "../../helpers/contracts-getters"; import { eContractid, @@ -91,6 +93,8 @@ import { StETHDebtToken, TimeLock, UiPoolDataProvider, + UniswapV2Factory, + UniswapV2Router02, WstETHMocked, X2Y2Adapter, X2Y2R1, @@ -221,6 +225,8 @@ export interface TestEnv { blurExchange: BlurExchange; blurAdapter: BlurAdapter; timeLock: TimeLock; + uniswapv2Factory: UniswapV2Factory; + uniswapv2Router: UniswapV2Router02; } export async function initializeMakeSuite() { @@ -294,6 +300,8 @@ export async function initializeMakeSuite() { blurExchange: {} as BlurExchange, blurAdapter: {} as BlurAdapter, timeLock: {} as TimeLock, + uniswapv2Factory: {} as UniswapV2Factory, + uniswapv2Router: {} as UniswapV2Router02, } as TestEnv; const paraSpaceConfig = getParaSpaceConfig(); const signers = await Promise.all( @@ -572,5 +580,8 @@ export async function initializeMakeSuite() { testEnv.blurAdapter = await getBlurAdapter(); testEnv.timeLock = await getTimeLockProxy(); + testEnv.uniswapv2Factory = await getUniswapV2Factory(); + testEnv.uniswapv2Router = await getUniswapV2Router(); + return testEnv; } diff --git a/test/helpers/validated-steps.ts b/test/helpers/validated-steps.ts index b4f5923bf..0e1b81cf9 100644 --- a/test/helpers/validated-steps.ts +++ b/test/helpers/validated-steps.ts @@ -33,6 +33,7 @@ import { MintableERC721, NToken, StETHMocked, + UniswapV2Pair, WETH9Mocked, } from "../../types"; import {SignerWithAddress} from "./make-suite"; @@ -50,6 +51,7 @@ type SupportedAsset = | AutoCompoundApe | AutoYieldApe | StETHMocked + | UniswapV2Pair | INonfungiblePositionManager; function isERC20(token: SupportedAsset): token is MintableERC20 | WETH9Mocked { diff --git a/test/izumi_nft_oracle.spec.ts b/test/izumi_nft_oracle.spec.ts new file mode 100644 index 000000000..37439cb82 --- /dev/null +++ b/test/izumi_nft_oracle.spec.ts @@ -0,0 +1,536 @@ +import {expect} from "chai"; +import {TestEnv} from "./helpers/make-suite"; +import {waitForTx, evmSnapshot, evmRevert} from "../helpers/misc-utils"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import { + getIZUMIOracleWrapper, + getIZUMIPositionManager, + getIZUMISwapRouter, +} from "../helpers/contracts-getters"; +import { + almostEqual, + createIzumiNewPool, + mintIzumiNewPosition, + swapToken, + fund, + approveTo, + getIZUMIPool, +} from "./helpers/izumi-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {LiquidityManager} from "../types"; + +describe("IZUMI NFT Oracle", () => { + let snap: string; + let testEnv: TestEnv; + let nftPositionManager: LiquidityManager; + + beforeEach(async () => { + snap = await evmSnapshot(); + }); + afterEach(async () => { + await evmRevert(snap); + }); + + before(async () => { + testEnv = await loadFixture(testEnvFixture); + nftPositionManager = await getIZUMIPositionManager(); + const {addressesProvider, oracle} = testEnv; + await waitForTx(await addressesProvider.setPriceOracle(oracle.address)); + }); + + after(async () => { + const {paraspaceOracle, addressesProvider} = testEnv; + await waitForTx( + await addressesProvider.setPriceOracle(paraspaceOracle.address) + ); + }); + + it("test with dai and weth:(token0 decimal equals token1 decimal) [ @skip-on-coverage ]", async () => { + const { + users: [user1, trader], + dai, + weth, + oracle, + } = testEnv; + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + const nft = nftPositionManager.connect(user1.signer); + await approveTo({token: dai, user: user1, target: nft.address}); + await approveTo({token: weth, user: user1, target: nft.address}); + + const fee = 2000; + const tickSpacing = fee / 50; + const initialPrice = encodeSqrtRatioX96(1, 1000); + const lowerPrice = encodeSqrtRatioX96(1, 10000); + const upperPrice = encodeSqrtRatioX96(1, 100); + await createIzumiNewPool({ + positionManager: nft, + token0: dai, + token1: weth, + fee: fee, + initialSqrtPrice: initialPrice, + }); + await mintIzumiNewPosition({ + nft: nft, + token0: dai, + token1: weth, + fee: fee, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userDaiAmount, + token1Amount: userWethAmount, + }); + const daiBalance = await dai.balanceOf(user1.address); + const wethBalance = await weth.balanceOf(user1.address); + const liquidityDaiAmount = userDaiAmount.sub(daiBalance); + const liquidityWethAmount = userWethAmount.sub(wethBalance); + + expect(await nft.balanceOf(user1.address)).to.eq(1); + const tokenId = await nft.tokenOfOwnerByIndex(user1.address, 0); + + const izumiOracle = await getIZUMIOracleWrapper(); + const liquidityAmount = await izumiOracle.getLiquidityAmount(tokenId); + almostEqual(liquidityAmount.token0Amount, liquidityDaiAmount); + almostEqual(liquidityAmount.token1Amount, liquidityWethAmount); + + let lpFee = await izumiOracle.getLpFeeAmount(tokenId); + expect(lpFee.token0Amount).to.eq(0); + expect(lpFee.token1Amount).to.eq(0); + + await oracle.setAssetPrice(dai.address, "1000000000000000"); //weth = 1000 dai + let tokenPrice = await izumiOracle.getTokenPrice(tokenId); + almostEqual( + tokenPrice, + liquidityWethAmount.add(liquidityDaiAmount.div(1000)) + ); + + const izumiPool = await getIZUMIPool({token0: dai, token1: weth, fee}); + //check lp fee in range + let traderDaiAmount = await convertToCurrencyDecimals(dai.address, "1000"); + let traderWethAmount = await convertToCurrencyDecimals(weth.address, "1"); + await fund({token: dai, user: trader, amount: traderDaiAmount}); + await fund({token: weth, user: trader, amount: traderWethAmount}); + const swapRouter = await getIZUMISwapRouter(); + await approveTo({token: dai, user: trader, target: swapRouter.address}); + await approveTo({token: weth, user: trader, target: swapRouter.address}); + + await swapToken({ + tokenIn: dai, + tokenOut: weth, + fee, + amountIn: traderDaiAmount, + trader, + zeroForOne: true, + }); + await swapToken({ + tokenIn: weth, + tokenOut: dai, + fee, + amountIn: traderWethAmount, + trader, + zeroForOne: false, + }); + + let currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.gt(lowerPrice.toString()); + expect(currentPrice).to.lt(upperPrice.toString()); + + const targetDaiFee = await convertToCurrencyDecimals(dai.address, "2"); + const targetEthFee = await convertToCurrencyDecimals(weth.address, "0.002"); + const targetTokenPrice = liquidityWethAmount + .add(targetEthFee) + .add(liquidityDaiAmount.div(1000)) + .add(targetDaiFee.div(1000)); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + almostEqual(lpFee.token0Amount, targetDaiFee); + almostEqual(lpFee.token1Amount, targetEthFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + almostEqual(tokenPrice, targetTokenPrice); + + traderDaiAmount = await convertToCurrencyDecimals(dai.address, "100000"); + traderWethAmount = await convertToCurrencyDecimals(weth.address, "10000"); + await fund({token: dai, user: trader, amount: traderDaiAmount}); + await fund({token: weth, user: trader, amount: traderWethAmount}); + + //check lp fee above range + await swapToken({ + tokenIn: dai, + tokenOut: weth, + fee, + amountIn: traderDaiAmount, + trader, + zeroForOne: true, + }); + currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.lt(lowerPrice.toString()); + + const addDaiFeeCap = await convertToCurrencyDecimals(dai.address, "200"); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + expect(lpFee.token0Amount).to.lt(targetDaiFee.add(addDaiFeeCap)); + expect(lpFee.token0Amount).to.gt(targetDaiFee); + almostEqual(lpFee.token1Amount, targetEthFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + expect(tokenPrice).to.lt(targetTokenPrice.add(addDaiFeeCap.div(1000))); + expect(tokenPrice).to.gt(targetTokenPrice); + + const lastToken0LpFee = lpFee.token0Amount; + const lastTokenPrice = tokenPrice; + + //check lp fee upper range + await swapToken({ + tokenIn: weth, + tokenOut: dai, + fee, + amountIn: traderWethAmount, + trader, + zeroForOne: false, + }); + currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.gt(upperPrice.toString()); + + const addEthFeeCap = await convertToCurrencyDecimals(weth.address, "20"); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + almostEqual(lpFee.token0Amount, lastToken0LpFee); + expect(lpFee.token1Amount).to.lt(targetEthFee.add(addEthFeeCap)); + expect(lpFee.token1Amount).to.gt(targetEthFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + expect(tokenPrice).to.lt(lastTokenPrice.add(addEthFeeCap)); + expect(tokenPrice).to.gt(lastTokenPrice); + }); + + it("test with usdc and weth:(token0 decimal less than token1 decimal) [ @skip-on-coverage ]", async () => { + const { + users: [user1, trader], + usdc, + weth, + oracle, + } = testEnv; + const userUsdcAmount = await convertToCurrencyDecimals( + usdc.address, + "10000" + ); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: usdc, user: user1, amount: userUsdcAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + const nft = nftPositionManager.connect(user1.signer); + await approveTo({token: usdc, user: user1, target: nft.address}); + await approveTo({token: weth, user: user1, target: nft.address}); + + const fee = 2000; + const tickSpacing = fee / 50; + const initialPrice = encodeSqrtRatioX96("1000000000", 1); + const lowerPrice = encodeSqrtRatioX96("100000000", 1); + const upperPrice = encodeSqrtRatioX96("10000000000", 1); + await createIzumiNewPool({ + positionManager: nft, + token0: usdc, + token1: weth, + fee: fee, + initialSqrtPrice: initialPrice, + }); + await mintIzumiNewPosition({ + nft: nft, + token0: usdc, + token1: weth, + fee: fee, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userUsdcAmount, + token1Amount: userWethAmount, + }); + const usdcBalance = await usdc.balanceOf(user1.address); + const wethBalance = await weth.balanceOf(user1.address); + const liquidityUsdcAmount = userUsdcAmount.sub(usdcBalance); + const liquidityWethAmount = userWethAmount.sub(wethBalance); + + expect(await nft.balanceOf(user1.address)).to.eq(1); + const tokenId = await nft.tokenOfOwnerByIndex(user1.address, 0); + + const izumiOracle = await getIZUMIOracleWrapper(); + const liquidityAmount = await izumiOracle.getLiquidityAmount(tokenId); + almostEqual(liquidityAmount.token0Amount, liquidityUsdcAmount); + almostEqual(liquidityAmount.token1Amount, liquidityWethAmount); + + let lpFee = await izumiOracle.getLpFeeAmount(tokenId); + expect(lpFee.token0Amount).to.eq(0); + expect(lpFee.token1Amount).to.eq(0); + + await oracle.setAssetPrice(usdc.address, "1000000000000000"); //weth = 1000 usdc + let tokenPrice = await izumiOracle.getTokenPrice(tokenId); + almostEqual( + tokenPrice, + liquidityWethAmount.add(liquidityUsdcAmount.mul("1000000000")) + ); + + const izumiPool = await getIZUMIPool({token0: usdc, token1: weth, fee}); + //check lp fee in range + let traderUsdcAmount = await convertToCurrencyDecimals( + usdc.address, + "1000" + ); + let traderWethAmount = await convertToCurrencyDecimals(weth.address, "1"); + await fund({token: usdc, user: trader, amount: traderUsdcAmount}); + await fund({token: weth, user: trader, amount: traderWethAmount}); + const swapRouter = await getIZUMISwapRouter(); + await approveTo({token: usdc, user: trader, target: swapRouter.address}); + await approveTo({token: weth, user: trader, target: swapRouter.address}); + + await swapToken({ + tokenIn: usdc, + tokenOut: weth, + fee, + amountIn: traderUsdcAmount, + trader, + zeroForOne: true, + }); + await swapToken({ + tokenIn: weth, + tokenOut: usdc, + fee, + amountIn: traderWethAmount, + trader, + zeroForOne: false, + }); + + let currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.gt(lowerPrice.toString()); + expect(currentPrice).to.lt(upperPrice.toString()); + + const targetUsdcFee = await convertToCurrencyDecimals(usdc.address, "2"); + const targetEthFee = await convertToCurrencyDecimals(weth.address, "0.002"); + const targetTokenPrice = liquidityWethAmount + .add(targetEthFee) + .add(liquidityUsdcAmount.mul("1000000000")) + .add(targetUsdcFee.mul("1000000000")); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + almostEqual(lpFee.token0Amount, targetUsdcFee); + almostEqual(lpFee.token1Amount, targetEthFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + almostEqual(tokenPrice, targetTokenPrice); + + traderUsdcAmount = await convertToCurrencyDecimals(usdc.address, "100000"); + traderWethAmount = await convertToCurrencyDecimals(weth.address, "10000"); + await fund({token: usdc, user: trader, amount: traderUsdcAmount}); + await fund({token: weth, user: trader, amount: traderWethAmount}); + + //check lp fee above range + await swapToken({ + tokenIn: usdc, + tokenOut: weth, + fee, + amountIn: traderUsdcAmount, + trader, + zeroForOne: true, + }); + currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.lt(lowerPrice.toString()); + + const addUsdcFeeCap = await convertToCurrencyDecimals(usdc.address, "200"); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + expect(lpFee.token0Amount).to.lt(targetUsdcFee.add(addUsdcFeeCap)); + expect(lpFee.token0Amount).to.gt(targetUsdcFee); + almostEqual(lpFee.token1Amount, targetEthFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + expect(tokenPrice).to.lt( + targetTokenPrice.add(addUsdcFeeCap.mul("1000000000")) + ); + expect(tokenPrice).to.gt(targetTokenPrice); + + const lastToken0LpFee = lpFee.token0Amount; + const lastTokenPrice = tokenPrice; + + //check lp fee upper range + await swapToken({ + tokenIn: weth, + tokenOut: usdc, + fee, + amountIn: traderWethAmount, + trader, + zeroForOne: false, + }); + currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.gt(upperPrice.toString()); + + const addEthFeeCap = await convertToCurrencyDecimals(weth.address, "20"); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + almostEqual(lpFee.token0Amount, lastToken0LpFee); + expect(lpFee.token1Amount).to.lt(targetEthFee.add(addEthFeeCap)); + expect(lpFee.token1Amount).to.gt(targetEthFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + expect(tokenPrice).to.lt(lastTokenPrice.add(addEthFeeCap)); + expect(tokenPrice).to.gt(lastTokenPrice); + }); + + it("test with weth and usdt:(token0 decimal greater than token1 decimal) [ @skip-on-coverage ]", async () => { + const { + users: [user1, trader], + usdt, + weth, + oracle, + } = testEnv; + const userUsdtAmount = await convertToCurrencyDecimals( + usdt.address, + "10000" + ); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: usdt, user: user1, amount: userUsdtAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + const nft = nftPositionManager.connect(user1.signer); + await approveTo({token: usdt, user: user1, target: nft.address}); + await approveTo({token: weth, user: user1, target: nft.address}); + + const fee = 2000; + const tickSpacing = fee / 50; + const initialPrice = encodeSqrtRatioX96(1, "1000000000"); + const lowerPrice = encodeSqrtRatioX96(1, "10000000000"); + const upperPrice = encodeSqrtRatioX96(1, "100000000"); + await createIzumiNewPool({ + positionManager: nft, + token0: weth, + token1: usdt, + fee: fee, + initialSqrtPrice: initialPrice, + }); + await mintIzumiNewPosition({ + nft: nft, + token0: weth, + token1: usdt, + fee: fee, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userWethAmount, + token1Amount: userUsdtAmount, + }); + const usdtBalance = await usdt.balanceOf(user1.address); + const wethBalance = await weth.balanceOf(user1.address); + const liquidityUsdtAmount = userUsdtAmount.sub(usdtBalance); + const liquidityWethAmount = userWethAmount.sub(wethBalance); + + expect(await nft.balanceOf(user1.address)).to.eq(1); + const tokenId = await nft.tokenOfOwnerByIndex(user1.address, 0); + + const izumiOracle = await getIZUMIOracleWrapper(); + const liquidityAmount = await izumiOracle.getLiquidityAmount(tokenId); + almostEqual(liquidityAmount.token0Amount, liquidityWethAmount); + almostEqual(liquidityAmount.token1Amount, liquidityUsdtAmount); + + let lpFee = await izumiOracle.getLpFeeAmount(tokenId); + expect(lpFee.token0Amount).to.eq(0); + expect(lpFee.token1Amount).to.eq(0); + + await oracle.setAssetPrice(usdt.address, "1000000000000000"); //weth = 1000 usdt + let tokenPrice = await izumiOracle.getTokenPrice(tokenId); + almostEqual( + tokenPrice, + liquidityWethAmount.add(liquidityUsdtAmount.mul("1000000000")) + ); + + const izumiPool = await getIZUMIPool({token0: usdt, token1: weth, fee}); + //check lp fee in range + let traderUsdtAmount = await convertToCurrencyDecimals( + usdt.address, + "1000" + ); + let traderWethAmount = await convertToCurrencyDecimals(weth.address, "1"); + await fund({token: usdt, user: trader, amount: traderUsdtAmount}); + await fund({token: weth, user: trader, amount: traderWethAmount}); + const swapRouter = await getIZUMISwapRouter(); + await approveTo({token: usdt, user: trader, target: swapRouter.address}); + await approveTo({token: weth, user: trader, target: swapRouter.address}); + + await swapToken({ + tokenIn: usdt, + tokenOut: weth, + fee, + amountIn: traderUsdtAmount, + trader, + zeroForOne: false, + }); + await swapToken({ + tokenIn: weth, + tokenOut: usdt, + fee, + amountIn: traderWethAmount, + trader, + zeroForOne: true, + }); + + let currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.gt(lowerPrice.toString()); + expect(currentPrice).to.lt(upperPrice.toString()); + + const targetUsdtFee = await convertToCurrencyDecimals(usdt.address, "2"); + const targetEthFee = await convertToCurrencyDecimals(weth.address, "0.002"); + const targetTokenPrice = liquidityWethAmount + .add(targetEthFee) + .add(liquidityUsdtAmount.mul("1000000000")) + .add(targetUsdtFee.mul("1000000000")); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + almostEqual(lpFee.token0Amount, targetEthFee); + almostEqual(lpFee.token1Amount, targetUsdtFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + almostEqual(tokenPrice, targetTokenPrice); + + traderUsdtAmount = await convertToCurrencyDecimals(usdt.address, "100000"); + traderWethAmount = await convertToCurrencyDecimals(weth.address, "10000"); + await fund({token: usdt, user: trader, amount: traderUsdtAmount}); + await fund({token: weth, user: trader, amount: traderWethAmount}); + + //check lp fee above range + await swapToken({ + tokenIn: usdt, + tokenOut: weth, + fee, + amountIn: traderUsdtAmount, + trader, + zeroForOne: false, + }); + currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.gt(upperPrice.toString()); + + const addUsdtFeeCap = await convertToCurrencyDecimals(usdt.address, "200"); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + almostEqual(lpFee.token0Amount, targetEthFee); + expect(lpFee.token1Amount).to.lt(targetUsdtFee.add(addUsdtFeeCap)); + expect(lpFee.token1Amount).to.gt(targetUsdtFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + expect(tokenPrice).to.lt( + targetTokenPrice.add(addUsdtFeeCap.mul("1000000000")) + ); + expect(tokenPrice).to.gt(targetTokenPrice); + + const lastToken1LpFee = lpFee.token1Amount; + const lastTokenPrice = tokenPrice; + + //check lp fee below range + await swapToken({ + tokenIn: weth, + tokenOut: usdt, + fee, + amountIn: traderWethAmount, + trader, + zeroForOne: true, + }); + currentPrice = (await izumiPool.state()).sqrtPrice_96; + expect(currentPrice).to.lt(lowerPrice.toString()); + + const addEthFeeCap = await convertToCurrencyDecimals(weth.address, "20"); + lpFee = await izumiOracle.getLpFeeAmount(tokenId); + almostEqual(lpFee.token1Amount, lastToken1LpFee); + expect(lpFee.token0Amount).to.lt(targetEthFee.add(addEthFeeCap)); + expect(lpFee.token0Amount).to.gt(targetEthFee); + tokenPrice = await izumiOracle.getTokenPrice(tokenId); + expect(tokenPrice).to.lt(lastTokenPrice.add(addEthFeeCap)); + expect(tokenPrice).to.gt(lastTokenPrice); + }); +}); diff --git a/test/izumi_pool_operation.spec.ts b/test/izumi_pool_operation.spec.ts new file mode 100644 index 000000000..dc18223a0 --- /dev/null +++ b/test/izumi_pool_operation.spec.ts @@ -0,0 +1,1093 @@ +import {expect} from "chai"; +import {TestEnv} from "./helpers/make-suite"; +import {waitForTx} from "../helpers/misc-utils"; +import {ZERO_ADDRESS} from "../helpers/constants"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import { + almostEqual, + createIzumiNewPool, + mintIzumiNewPosition, + fund, + approveTo, +} from "./helpers/izumi-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; +import { + getIZUMIOracleWrapper, + getIZUMIPositionManager, + getNTokenIZUMI, +} from "../helpers/contracts-getters"; +import {ProtocolErrors} from "../helpers/types"; +import {snapshot} from "./helpers/snapshot-manager"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {LiquidityManager, NTokenIzumi} from "../types"; + +describe("IZUMI LP NFT supply, withdraw, setCollateral, liquidation and transfer test", () => { + let testEnv: TestEnv; + let nftPositionManager: LiquidityManager; + let nTokenIzumi: NTokenIzumi; + + before(async () => { + testEnv = await loadFixture(testEnvFixture); + + const { + users: [user1], + dai, + weth, + pool, + } = testEnv; + + nftPositionManager = await getIZUMIPositionManager(); + const nIzumiAddress = await pool.getReserveXToken( + nftPositionManager.address + ); + nTokenIzumi = await getNTokenIZUMI(nIzumiAddress); + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + const nft = nftPositionManager.connect(user1.signer); + await approveTo({ + target: nftPositionManager.address, + token: dai, + user: user1, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: user1, + }); + const fee = 2000; + const tickSpacing = fee / 50; + const initialPrice = encodeSqrtRatioX96(1, 1000); + const lowerPrice = encodeSqrtRatioX96(1, 10000); + const upperPrice = encodeSqrtRatioX96(1, 100); + await createIzumiNewPool({ + positionManager: nft, + token0: dai, + token1: weth, + fee: fee, + initialSqrtPrice: initialPrice, + }); + await mintIzumiNewPosition({ + nft: nft, + token0: dai, + token1: weth, + fee: fee, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userDaiAmount, + token1Amount: userWethAmount, + }); + expect(await nftPositionManager.balanceOf(user1.address)).to.eq(1); + + await nft.setApprovalForAll(pool.address, true); + }); + + it("supply IZUMI LP failed if underlying erc20 was inactive[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, false)); + await expect( + pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); + }); + + it("supply IZUMI LP success if underlying erc20 was active[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, true)); + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + expect(await nTokenIzumi.balanceOf(user1.address)).to.eq(1); + }); + + it("supply IZUMI LP failed if underlying erc20 was paused[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [0], user1.address, { + gasLimit: 12_450_000, + }) + ); + + await waitForTx(await configurator.pauseReserve(weth.address)); + await expect( + pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); + }); + + it("supply IZUMI LP success if underlying erc20 was not paused[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.unpauseReserve(weth.address)); + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + expect(await nTokenIzumi.balanceOf(user1.address)).to.eq(1); + }); + + it("supply IZUMI LP failed if underlying erc20 was frozen[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [0], user1.address, { + gasLimit: 12_450_000, + }) + ); + + await waitForTx(await configurator.setReserveFreeze(weth.address, true)); + await expect( + pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_FROZEN); + }); + + it("supply IZUMI LP success if underlying erc20 was not frozen[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveFreeze(weth.address, false)); + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + expect(await nTokenIzumi.balanceOf(user1.address)).to.eq(1); + }); + + it("withdraw IZUMI LP failed if underlying erc20 was not active[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, false)); + await expect( + pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [0], user1.address, { + gasLimit: 12_450_000, + }) + ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); + }); + + it("withdraw IZUMI LP success if underlying erc20 was active[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, true)); + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [0], user1.address, { + gasLimit: 12_450_000, + }) + ); + expect(await nftPositionManager.balanceOf(user1.address)).to.eq(1); + }); + + it("withdraw IZUMI LP failed if underlying erc20 was paused[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + await waitForTx(await configurator.pauseReserve(weth.address)); + await expect( + pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [0], user1.address, { + gasLimit: 12_450_000, + }) + ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); + }); + + it("withdraw IZUMI LP success if underlying erc20 was not paused[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.unpauseReserve(weth.address)); + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(nftPositionManager.address, [0], user1.address, { + gasLimit: 12_450_000, + }) + ); + expect(await nftPositionManager.balanceOf(user1.address)).to.eq(1); + }); + + it("setAsCollateral failed if underlying erc20 was not active[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + await waitForTx(await configurator.setReserveActive(weth.address, false)); + await expect( + pool + .connect(user1.signer) + .setUserUseERC721AsCollateral(nftPositionManager.address, [0], false, { + gasLimit: 12_450_000, + }) + ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); + }); + + it("setAsCollateral success if underlying erc20 was active[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, true)); + await waitForTx( + await pool + .connect(user1.signer) + .setUserUseERC721AsCollateral(nftPositionManager.address, [0], false, { + gasLimit: 12_450_000, + }) + ); + expect(await nTokenIzumi.collateralizedBalanceOf(user1.address)).to.eq(0); + }); + + it("setAsCollateral failed if underlying erc20 was paused[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.pauseReserve(weth.address)); + await expect( + pool + .connect(user1.signer) + .setUserUseERC721AsCollateral(nftPositionManager.address, [0], true, { + gasLimit: 12_450_000, + }) + ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); + }); + + it("setAsCollateral success if underlying erc20 was not paused[ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.unpauseReserve(weth.address)); + await waitForTx( + await pool + .connect(user1.signer) + .setUserUseERC721AsCollateral(nftPositionManager.address, [0], true, { + gasLimit: 12_450_000, + }) + ); + expect(await nTokenIzumi.collateralizedBalanceOf(user1.address)).to.eq(1); + }); + + it("increaseLiquidity failed if underlying erc20 was not active [ @skip-on-coverage ]", async () => { + const { + users: [user1], + configurator, + dai, + weth, + pool, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, false)); + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "20000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + + await expect( + pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 0, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628, + { + gasLimit: 12_450_000, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); + }); + + it("increaseLiquidity success if underlying erc20 was active [ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + pool, + configurator, + dai, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, true)); + + const preLiquidationSnapshot = await snapshot.take(); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "20000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({target: nTokenIzumi.address, token: dai, user: user1}); + await approveTo({target: nTokenIzumi.address, token: weth, user: user1}); + + await waitForTx( + await pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 0, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628, + { + gasLimit: 12_450_000, + } + ) + ); + + const afterLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + almostEqual(afterLiquidity, beforeLiquidity.mul(2)); + + await snapshot.revert(preLiquidationSnapshot); + }); + + it("increaseLiquidity failed if underlying erc20 was paused [ @skip-on-coverage ]", async () => { + const { + users: [user1], + configurator, + weth, + dai, + pool, + } = testEnv; + + await waitForTx(await configurator.pauseReserve(weth.address)); + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "20000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + + await expect( + pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 0, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628, + { + gasLimit: 12_450_000, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); + }); + + it("increaseLiquidity success if underlying erc20 was not paused [ @skip-on-coverage ]", async () => { + const { + users: [user1], + weth, + dai, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.unpauseReserve(weth.address)); + + const preLiquidationSnapshot = await snapshot.take(); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "20000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({target: nTokenIzumi.address, token: dai, user: user1}); + await approveTo({target: nTokenIzumi.address, token: weth, user: user1}); + + await waitForTx( + await pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 0, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628, + { + gasLimit: 12_450_000, + } + ) + ); + + const afterLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + almostEqual(afterLiquidity, beforeLiquidity.mul(2)); + + await snapshot.revert(preLiquidationSnapshot); + }); + + it("decreaseLiquidity failed if underlying erc20 was not active [ @skip-on-coverage ]", async () => { + const { + users: [user1], + configurator, + dai, + weth, + pool, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, false)); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + await expect( + pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); + }); + + it("decreaseLiquidity success if underlying erc20 was active [ @skip-on-coverage ]", async () => { + const { + users: [user1], + dai, + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(weth.address, true)); + + const preLiquidationSnapshot = await snapshot.take(); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) + ); + + await snapshot.revert(preLiquidationSnapshot); + }); + + it("decreaseLiquidity failed if underlying erc20 was paused [ @skip-on-coverage ]", async () => { + const { + users: [user1], + configurator, + dai, + weth, + pool, + } = testEnv; + + await waitForTx(await configurator.pauseReserve(weth.address)); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + await expect( + pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); + }); + + it("decreaseLiquidity success if underlying erc20 was not paused [ @skip-on-coverage ]", async () => { + const { + users: [user1], + dai, + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.unpauseReserve(weth.address)); + + const preLiquidationSnapshot = await snapshot.take(); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) + ); + + await snapshot.revert(preLiquidationSnapshot); + }); + + it("decreaseLiquidity failed if not owner [ @skip-on-coverage ]", async () => { + const {users, dai, weth, pool} = testEnv; + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + await expect( + pool.connect(users[1].signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity.div(2), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) + ).to.be.revertedWith(ProtocolErrors.NOT_THE_OWNER); + }); + + it("transfer failed if underlying erc20 was paused [ @skip-on-coverage ]", async () => { + const { + users: [user1, user2], + configurator, + weth, + } = testEnv; + + await waitForTx(await configurator.pauseReserve(weth.address)); + + await expect( + nTokenIzumi + .connect(user1.signer) + .transferFrom(user1.address, user2.address, 0, { + gasLimit: 12_450_000, + }) + ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); + }); + + it("transfer success if underlying erc20 was not paused [ @skip-on-coverage ]", async () => { + const { + users: [user1, user2], + weth, + configurator, + } = testEnv; + await waitForTx(await configurator.unpauseReserve(weth.address)); + + const preLiquidationSnapshot = await snapshot.take(); + + await waitForTx( + await nTokenIzumi + .connect(user1.signer) + .transferFrom(user1.address, user2.address, 0, { + gasLimit: 12_450_000, + }) + ); + + expect(await nTokenIzumi.balanceOf(user1.address)).to.eq(0); + expect(await nTokenIzumi.balanceOf(user2.address)).to.eq(1); + + await snapshot.revert(preLiquidationSnapshot); + }); + + it("borrow asset by using IZUMI Lp as collateral [ @skip-on-coverage ]", async () => { + const { + users: [user1, depositor], + weth, + dai, + pool, + paraspaceOracle, + oracle, + } = testEnv; + + const ethAmount = await convertToCurrencyDecimals(weth.address, "30"); + await fund({token: weth, user: depositor, amount: ethAmount}); + await approveTo({ + target: pool.address, + token: weth, + user: depositor, + }); + await waitForTx( + await pool + .connect(depositor.signer) + .supply(weth.address, ethAmount, depositor.address, "0") + ); + + await waitForTx( + await paraspaceOracle.setAssetSources([dai.address], [ZERO_ADDRESS]) + ); + await oracle.setAssetPrice(dai.address, "1000000000000000"); //weth = 1000 dai + + const nftValue = await convertToCurrencyDecimals(weth.address, "20"); + const borrowableValue = await convertToCurrencyDecimals(weth.address, "6"); + + const izumiOracle = await getIZUMIOracleWrapper(); + const tokenPrice = await izumiOracle.getTokenPrice(0); + almostEqual(tokenPrice, nftValue); + + const userAccountData = await pool.getUserAccountData(user1.address); + expect(userAccountData.ltv).to.eq(3000); + almostEqual(userAccountData.availableBorrowsBase, borrowableValue); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow( + weth.address, + userAccountData.availableBorrowsBase.sub(1), + "0", + user1.address + ) + ); + }); + + it("transfer failed if hf < 1 [ @skip-on-coverage ]", async () => { + const { + users: [user1, user2], + } = testEnv; + + await expect( + nTokenIzumi + .connect(user1.signer) + .transferFrom(user1.address, user2.address, 0, { + gasLimit: 12_450_000, + }) + ).to.be.revertedWith( + ProtocolErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + }); + + it("IZUMI LP asset can be auctioned [ @skip-on-coverage ]", async () => { + const { + users: [borrower, liquidator], + pool, + oracle, + dai, + weth, + } = testEnv; + await oracle.setAssetPrice(dai.address, "100000000000000"); //weth = 10000 dai + + const ethAmount = await convertToCurrencyDecimals(weth.address, "20"); + await fund({token: weth, user: liquidator, amount: ethAmount}); + await approveTo({ + target: pool.address, + token: weth, + user: liquidator, + }); + + const user1Balance = await nTokenIzumi.balanceOf(borrower.address); + const liquidatorBalance = await nTokenIzumi.balanceOf(liquidator.address); + expect(user1Balance).to.eq(1); + expect(liquidatorBalance).to.eq(0); + + // try to start auction + await waitForTx( + await pool + .connect(liquidator.signer) + .startAuction(borrower.address, nftPositionManager.address, 0) + ); + + expect(await nTokenIzumi.isAuctioned(0)).to.be.true; + }); + + it("liquidation failed if underlying erc20 was not active [ @skip-on-coverage ]", async () => { + const { + users: [user1, liquidator], + configurator, + weth, + dai, + pool, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(dai.address, false)); + + const liquidationValue = await convertToCurrencyDecimals( + weth.address, + "20" + ); + + await expect( + pool + .connect(liquidator.signer) + .liquidateERC721( + nftPositionManager.address, + user1.address, + 0, + liquidationValue, + true, + { + gasLimit: 12_450_000, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); + }); + + it("liquidation success if underlying erc20 was active [ @skip-on-coverage ]", async () => { + const { + users: [user1, liquidator], + weth, + dai, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.setReserveActive(dai.address, true)); + + const preLiquidationSnapshot = await snapshot.take(); + + const liquidationValue = await convertToCurrencyDecimals( + weth.address, + "20" + ); + + await waitForTx( + await pool + .connect(liquidator.signer) + .liquidateERC721( + nftPositionManager.address, + user1.address, + 0, + liquidationValue, + true, + { + gasLimit: 12_450_000, + } + ) + ); + + await snapshot.revert(preLiquidationSnapshot); + }); + + it("liquidation failed if underlying erc20 was paused [ @skip-on-coverage ]", async () => { + const { + users: [user1, liquidator], + configurator, + weth, + pool, + } = testEnv; + + await waitForTx(await configurator.pauseReserve(weth.address)); + + const liquidationValue = await convertToCurrencyDecimals( + weth.address, + "20" + ); + + await expect( + pool + .connect(liquidator.signer) + .liquidateERC721( + nftPositionManager.address, + user1.address, + 0, + liquidationValue, + true, + { + gasLimit: 12_450_000, + } + ) + ).to.be.revertedWith(ProtocolErrors.RESERVE_PAUSED); + }); + + it("liquidation success if underlying erc20 was not paused [ @skip-on-coverage ]", async () => { + const { + users: [user1, liquidator], + weth, + pool, + configurator, + } = testEnv; + + await waitForTx(await configurator.unpauseReserve(weth.address)); + + const preLiquidationSnapshot = await snapshot.take(); + + const liquidationValue = await convertToCurrencyDecimals( + weth.address, + "20" + ); + + await waitForTx( + await pool + .connect(liquidator.signer) + .liquidateERC721( + nftPositionManager.address, + user1.address, + 0, + liquidationValue, + true, + { + gasLimit: 12_450_000, + } + ) + ); + + await snapshot.revert(preLiquidationSnapshot); + }); +}); diff --git a/test/izumi_position_control.spec.ts b/test/izumi_position_control.spec.ts new file mode 100644 index 000000000..2606f3b31 --- /dev/null +++ b/test/izumi_position_control.spec.ts @@ -0,0 +1,354 @@ +import {expect} from "chai"; +import {TestEnv} from "./helpers/make-suite"; +import {waitForTx} from "../helpers/misc-utils"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import { + almostEqual, + createIzumiNewPool, + mintIzumiNewPosition, + approveSwapRouter, + swapToken, + fund, + approveTo, +} from "./helpers/izumi-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {LiquidityManager, NTokenIzumi} from "../types"; +import { + getIZUMIPositionManager, + getNTokenIZUMI, +} from "../helpers/contracts-getters"; +import {parseEther} from "ethers/lib/utils"; + +describe("IZUMI LP NFT position control", () => { + let testEnv: TestEnv; + let nftPositionManager: LiquidityManager; + let nTokenIzumi: NTokenIzumi; + + before(async () => { + testEnv = await loadFixture(testEnvFixture); + + const { + users: [user1], + dai, + weth, + pool, + } = testEnv; + + nftPositionManager = await getIZUMIPositionManager(); + const nIzumiAddress = await pool.getReserveXToken( + nftPositionManager.address + ); + nTokenIzumi = await getNTokenIZUMI(nIzumiAddress); + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + const nft = nftPositionManager.connect(user1.signer); + await approveTo({ + target: nftPositionManager.address, + token: dai, + user: user1, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: user1, + }); + const fee = 2000; + const tickSpacing = fee / 50; + const initialPrice = encodeSqrtRatioX96(1, 1000); + const lowerPrice = encodeSqrtRatioX96(1, 10000); + const upperPrice = encodeSqrtRatioX96(1, 100); + await createIzumiNewPool({ + positionManager: nft, + token0: dai, + token1: weth, + fee: fee, + initialSqrtPrice: initialPrice, + }); + await mintIzumiNewPosition({ + nft: nft, + token0: dai, + token1: weth, + fee: fee, + user: user1, + tickSpacing: tickSpacing, + lowerPrice, + upperPrice, + token0Amount: userDaiAmount, + token1Amount: userWethAmount, + }); + expect(await nftPositionManager.balanceOf(user1.address)).to.eq(1); + + await nft.setApprovalForAll(pool.address, true); + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + nftPositionManager.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + 0, + { + gasLimit: 12_450_000, + } + ) + ); + + expect(await nTokenIzumi.balanceOf(user1.address)).to.eq(1); + + await waitForTx( + await weth.connect(user1.signer).deposit({ + value: parseEther("100"), + }) + ); + }); + + it("increaseLiquidity by NTokenIZUMI [ @skip-on-coverage ]", async () => { + const { + users: [user1], + dai, + weth, + pool, + } = testEnv; + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "20000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await fund({token: weth, user: user1, amount: userWethAmount}); + await approveTo({target: nTokenIzumi.address, token: dai, user: user1}); + await approveTo({target: nTokenIzumi.address, token: weth, user: user1}); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + await waitForTx( + await pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 0, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628 + ) + ); + + const afterLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + almostEqual(afterLiquidity, beforeLiquidity.mul(2)); + //test refund. fund userDaiAmount , should left userDaiAmount. + almostEqual(await dai.balanceOf(user1.address), userDaiAmount.div(2)); + }); + + it("increaseLiquidity with ETH by NTokenIZUMI [ @skip-on-coverage ]", async () => { + const { + users: [user1], + dai, + weth, + pool, + } = testEnv; + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "20"); + await fund({token: dai, user: user1, amount: userDaiAmount}); + await approveTo({target: nTokenIzumi.address, token: dai, user: user1}); + + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + const beforeBalance = await user1.signer.getBalance(); + + await waitForTx( + await pool + .connect(user1.signer) + .increaseLiquidity( + nftPositionManager.address, + 0, + userDaiAmount, + userWethAmount, + 0, + 0, + 2659537628, + { + gasLimit: 12_450_000, + value: userWethAmount, + } + ) + ); + + const afterLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + const afterBalance = await user1.signer.getBalance(); + + almostEqual(afterLiquidity, beforeLiquidity.div(2).mul(3)); + // user sent 20, so the remaining 10 are refunded back to the user + almostEqual(beforeBalance.sub(afterBalance), userWethAmount.div(2)); + }); + + it("decreaseLiquidity by NTokenIZUMI [ @skip-on-coverage ]", async () => { + const { + users: [user1], + dai, + pDai, + weth, + pWETH, + pool, + protocolDataProvider, + } = testEnv; + + const userDaiAmount = await convertToCurrencyDecimals(dai.address, "10000"); + const userWethAmount = await convertToCurrencyDecimals(weth.address, "10"); + + const beforeDaiBalance = await dai.balanceOf(user1.address); + const beforeEthBalance = await weth.balanceOf(user1.address); + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + let userReserveData = await protocolDataProvider.getUserReserveData( + dai.address, + user1.address + ); + expect(userReserveData.usageAsCollateralEnabled).to.be.false; + userReserveData = await protocolDataProvider.getUserReserveData( + weth.address, + user1.address + ); + expect(userReserveData.usageAsCollateralEnabled).to.be.false; + + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: beforeLiquidity.div(3), + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) + ); + + const afterDaiBalance = (await dai.balanceOf(user1.address)).add( + await pDai.balanceOf(user1.address) + ); + const afterEthBalance = (await weth.balanceOf(user1.address)).add( + await pWETH.balanceOf(user1.address) + ); + const afterLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + almostEqual(afterDaiBalance.sub(beforeDaiBalance), userDaiAmount); + almostEqual(afterEthBalance.sub(beforeEthBalance), userWethAmount); + almostEqual(afterLiquidity, beforeLiquidity.div(3).mul(2)); + + userReserveData = await protocolDataProvider.getUserReserveData( + dai.address, + user1.address + ); + expect(userReserveData.usageAsCollateralEnabled).to.be.true; + userReserveData = await protocolDataProvider.getUserReserveData( + weth.address, + user1.address + ); + expect(userReserveData.usageAsCollateralEnabled).to.be.true; + }); + + it("collect fee by decreaseLiquidity by NTokenIZUMI [ @skip-on-coverage ]", async () => { + const { + users: [user1, trader], + dai, + pDai, + weth, + pool, + } = testEnv; + + const traderDaiAmount = await convertToCurrencyDecimals( + dai.address, + "1000" + ); + await fund({token: dai, user: trader, amount: traderDaiAmount}); + await approveSwapRouter({token: dai, user: trader}); + + const fee = 2000; + await swapToken({ + tokenIn: dai, + tokenOut: weth, + fee, + amountIn: traderDaiAmount, + trader, + zeroForOne: true, + }); + + const beforeDaiBalance = await dai.balanceOf(user1.address); + const beforepDaiBalance = await pDai.balanceOf(user1.address); + const beforeEthBalance = await weth.balanceOf(user1.address); + const beforeLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + await waitForTx( + await pool.connect(user1.signer).adjustLpPosition( + { + asset: nftPositionManager.address, + token0: dai.address, + token1: weth.address, + token0CashAmount: 0, + token1CashAmount: 0, + token0BorrowAmount: 0, + token1BorrowAmount: 0, + }, + { + decreaseLiquidity: true, + tokenId: 0, + liquidityDecrease: 0, + amount0Min: 0, + amount1Min: 0, + burnNFT: false, + }, + { + mintNewToken: false, + fee: 2000, + tickLower: 0, + tickUpper: 0, + amount0Desired: 0, + amount1Desired: 0, + amount0Min: 0, + amount1Min: 0, + deadline: 0, + } + ) + ); + + const afterDaiBalance = await dai.balanceOf(user1.address); + const afterpDaiBalance = await pDai.balanceOf(user1.address); + const afterEthBalance = await weth.balanceOf(user1.address); + const afterLiquidity = (await nftPositionManager.liquidities(0)).liquidity; + + expect(afterEthBalance).to.eq(beforeEthBalance); + expect(afterDaiBalance).to.eq(beforeDaiBalance); + expect(afterLiquidity).to.eq(beforeLiquidity); + almostEqual( + afterpDaiBalance.sub(beforepDaiBalance), + await convertToCurrencyDecimals(dai.address, "2") + ); + }); +});