From d2391b555fc185367314f7b2b6ecc61329b8224d Mon Sep 17 00:00:00 2001 From: 0xstormtrooper <0xstormtrooper@protonmail.com> Date: Mon, 25 Oct 2021 09:59:20 -0400 Subject: [PATCH] merge singleton pool --- README.md | 37 -- contracts/dex-v2/pool/BasePoolV2.sol | 421 ++++++++++++++++++ contracts/dex-v2/pool/VaderPoolV2.sol | 92 ++++ contracts/dex-v2/router/VaderRouterV2.sol | 236 ++++++++++ contracts/dex/pool/BasePool.sol | 143 +++++- contracts/dex/pool/VaderPool.sol | 29 +- contracts/dex/pool/VaderPoolFactory.sol | 41 +- contracts/dex/router/VaderRouter.sol | 145 ++++++ .../interfaces/dex-v2/pool/IBasePoolV2.sol | 97 ++++ .../dex-v2/pool/IVaderPoolFactoryV2.sol | 30 ++ .../interfaces/dex-v2/pool/IVaderPoolV2.sol | 29 ++ .../dex-v2/router/IVaderRouterV2.sol | 50 +++ .../interfaces/dex/pool/IVaderPoolFactory.sol | 2 +- migrations/1_initial_migration.js | 3 +- package.json | 6 - test/utils/index.js | 10 + test/v2/VaderRouterV2.test.js | 81 ++++ truffle-config.js | 31 +- 18 files changed, 1401 insertions(+), 82 deletions(-) create mode 100644 contracts/dex-v2/pool/BasePoolV2.sol create mode 100644 contracts/dex-v2/pool/VaderPoolV2.sol create mode 100644 contracts/dex-v2/router/VaderRouterV2.sol create mode 100644 contracts/interfaces/dex-v2/pool/IBasePoolV2.sol create mode 100644 contracts/interfaces/dex-v2/pool/IVaderPoolFactoryV2.sol create mode 100644 contracts/interfaces/dex-v2/pool/IVaderPoolV2.sol create mode 100644 contracts/interfaces/dex-v2/router/IVaderRouterV2.sol create mode 100644 test/v2/VaderRouterV2.test.js diff --git a/README.md b/README.md index a7cbd21..baa9e5e 100644 --- a/README.md +++ b/README.md @@ -5,40 +5,3 @@ The Vader monorepo (internal) ## Setup Node version that should be utilized is 12.16.2, other versions can show unwarranted errors. For Node.JS version management, `nvm` is recommended. - -```shell -npm i - -# put your wallet seed here -touch .secret - -# put your environment variables here -cp .env.sample .env -``` - -## Deploy - -```shell -# deploy (run migration script from x to y) -npx truffle migrate -f x --to y --network kovan - -# verify contract -npx truffle run verify MyContract --network kovan -``` - -## Networks & Addresses - -### Kovan - -- Vether: [0x438f70Ab08AB3F74833c439643C3fC1939cE2929](https://kovan.etherscan.io/address/0x438f70Ab08AB3F74833c439643C3fC1939cE2929) -- Vader: [0x1E6F42f04D64D55ec08d6D4e6A7CB4a235E1c742](https://kovan.etherscan.io/address/0x1E6F42f04D64D55ec08d6D4e6A7CB4a235E1c742) -- Converter: [0x8e7A48fC00cF9541392FB820628Ca730b6badf3e](https://kovan.etherscan.io/address/0x8e7A48fC00cF9541392FB820628Ca730b6badf3e) -- LinearVesting: [0x0031e708132089B3ed866495d5838273ea27B0ee](https://kovan.etherscan.io/address/0x0031e708132089B3ed866495d5838273ea27B0ee) -- VaderMath: [0x55Bab235490a097653Dd12f10982Dd577705e994](https://kovan.etherscan.io/address/0x55Bab235490a097653Dd12f10982Dd577705e994) -- VaderPoolFactory: [0xa2b26Aa8fE7b5C0D1C9288c372F49f576bae4e4b](https://kovan.etherscan.io/address/0xa2b26Aa8fE7b5C0D1C9288c372F49f576bae4e4b) -- VaderRouter: [0x9C7a0c281Eb192859b41353b1bE682f6F3eD3bEA](https://kovan.etherscan.io/address/0x9C7a0c281Eb192859b41353b1bE682f6F3eD3bEA) -- VaderReserve: [0x3C5d480d3a0CC4e62f557e2A2c546aE9110CB987](https://kovan.etherscan.io/address/0x3C5d480d3a0CC4e62f557e2A2c546aE9110CB987) -- USDV: [0x6b645db074f05775363d9b315c82cbb3A5337C50](https://kovan.etherscan.io/address/0x6b645db074f05775363d9b315c82cbb3A5337C50) -- Vault (Mock): [0x267acDF7EeC7fbD4FE6aD25449B98045180073A8](https://kovan.etherscan.io/address/0x267acDF7EeC7fbD4FE6aD25449B98045180073A8) -- GovernorAlpha: [0x4055F28E1D0dc6b170f5c2D9075aA8e420b6092A](https://kovan.etherscan.io/address/0x4055F28E1D0dc6b170f5c2D9075aA8e420b6092A) -- Timelock: [0xDf6CeAdA2f7cd83C040574362b91EA198Fc6f464](https://kovan.etherscan.io/address/0xDf6CeAdA2f7cd83C040574362b91EA198Fc6f464) diff --git a/contracts/dex-v2/pool/BasePoolV2.sol b/contracts/dex-v2/pool/BasePoolV2.sol new file mode 100644 index 0000000..dd4c3e3 --- /dev/null +++ b/contracts/dex-v2/pool/BasePoolV2.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: Unlicense + +pragma solidity =0.8.9; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +import "../../dex/math/VaderMath.sol"; +import "../../dex/utils/GasThrottle.sol"; + +import "../../external/libraries/UQ112x112.sol"; + +import "../../interfaces/dex-v2/pool/IBasePoolV2.sol"; + +contract BasePoolV2 is + IBasePoolV2, + GasThrottle, + ERC721, + Ownable, + ReentrancyGuard +{ + /* ========== LIBRARIES ========== */ + + // Used for safe token transfers + using SafeERC20 for IERC20; + + // Used by Uniswap-like TWAP mechanism + using UQ112x112 for uint224; + + /* ========== STATE VARIABLES ========== */ + + IERC20 public immutable override nativeAsset; + + // Denotes what tokens are actively supported by the system + mapping(IERC20 => bool) public override supported; + + mapping(IERC20 => PairInfo) public pairInfo; + mapping(uint256 => Position) public positions; + uint256 public positionId; + + // uint112 private _reserveNative; // uses single storage slot, accessible via getReserves + // uint112 private _reserveForeign; // uses single storage slot, accessible via getReserves + // uint32 private _blockTimestampLast; // uses single storage slot, accessible via getReserves + + /* ========== CONSTRUCTOR ========== */ + + constructor(IERC20 _nativeAsset) ERC721("Vader LP", "VLP") { + nativeAsset = IERC20(_nativeAsset); + } + + /* ========== VIEWS ========== */ + + function getReserves(IERC20 foreignAsset) + public + view + returns ( + uint112 reserveNative, + uint112 reserveForeign, + uint32 blockTimestampLast + ) + { + PairInfo storage pair = pairInfo[foreignAsset]; + (reserveNative, reserveForeign, blockTimestampLast) = ( + pair.reserveNative, + pair.reserveForeign, + pair.blockTimestampLast + ); + } + + function positionForeignAsset(uint256 id) + external + view + override + returns (IERC20) + { + return positions[id].foreignAsset; + } + + /* ========== MUTATIVE FUNCTIONS ========== */ + + function mint( + IERC20 foreignAsset, + uint256 nativeDeposit, + uint256 foreignDeposit, + address from, + address to + ) + external + override + nonReentrant + supportedToken(foreignAsset) + returns (uint256 liquidity) + { + (uint112 reserveNative, uint112 reserveForeign, ) = getReserves( + foreignAsset + ); // gas savings + + nativeAsset.safeTransferFrom(from, address(this), nativeDeposit); + foreignAsset.safeTransferFrom(from, address(this), foreignDeposit); + + PairInfo storage pair = pairInfo[foreignAsset]; + uint256 totalLiquidityUnits = pair.totalSupply; + if (totalLiquidityUnits == 0) + liquidity = nativeDeposit; // TODO: Contact ThorChain on proper approach + else + liquidity = VaderMath.calculateLiquidityUnits( + nativeDeposit, + reserveNative, + foreignDeposit, + reserveForeign, + totalLiquidityUnits + ); + + require( + liquidity > 0, + "BasePoolV2::mint: Insufficient Liquidity Provided" + ); + + uint256 id = positionId++; + + pair.totalSupply = totalLiquidityUnits + liquidity; + _mint(to, id); + + positions[id] = Position( + foreignAsset, + block.timestamp, + liquidity, + nativeDeposit, + foreignDeposit + ); + + _update( + foreignAsset, + reserveNative + nativeDeposit, + reserveForeign + foreignDeposit, + reserveNative, + reserveForeign + ); + + emit Mint(from, to, nativeDeposit, foreignDeposit); + emit PositionOpened(from, to, id, liquidity); + } + + function _burn(uint256 id, address to) + internal + nonReentrant + returns (uint256 amountNative, uint256 amountForeign) + { + require( + ownerOf(id) == address(this), + "BasePoolV2::burn: Incorrect Ownership" + ); + + IERC20 foreignAsset = positions[id].foreignAsset; + + (uint112 reserveNative, uint112 reserveForeign, ) = getReserves( + foreignAsset + ); // gas savings + + uint256 liquidity = positions[id].liquidity; + + PairInfo storage pair = pairInfo[foreignAsset]; + uint256 _totalSupply = pair.totalSupply; + amountNative = (liquidity * reserveNative) / _totalSupply; + amountForeign = (liquidity * reserveForeign) / _totalSupply; + + require( + amountNative > 0 && amountForeign > 0, + "BasePoolV2::burn: Insufficient Liquidity Burned" + ); + + pair.totalSupply = _totalSupply - liquidity; + _burn(id); + + nativeAsset.safeTransfer(to, amountNative); + foreignAsset.safeTransfer(to, amountForeign); + + _update( + foreignAsset, + reserveNative - amountNative, + reserveForeign - amountForeign, + reserveNative, + reserveForeign + ); + + emit Burn(msg.sender, amountNative, amountForeign, to); + } + + function doubleSwap( + IERC20 foreignAssetA, + IERC20 foreignAssetB, + uint256 foreignAmountIn, + address to + ) + external + override + supportedToken(foreignAssetA) + supportedToken(foreignAssetB) + nonReentrant + validateGas + returns (uint256 foreignAmountOut) + { + (uint112 nativeReserve, uint112 foreignReserve, ) = getReserves( + foreignAssetA + ); // gas savings + + require( + foreignReserve + foreignAmountIn <= + foreignAssetA.balanceOf(address(this)), + "BasePoolV2::doubleSwap: Insufficient Tokens Provided" + ); + + uint256 nativeAmountOut = VaderMath.calculateSwap( + foreignAmountIn, + foreignReserve, + nativeReserve + ); + + require( + nativeAmountOut > 0 && nativeAmountOut <= nativeReserve, + "BasePoolV2::doubleSwap: Swap Impossible" + ); + + _update( + foreignAssetA, + nativeReserve - nativeAmountOut, + foreignReserve + foreignAmountIn, + nativeReserve, + foreignReserve + ); + + emit Swap( + foreignAssetA, + msg.sender, + 0, + foreignAmountIn, + nativeAmountOut, + 0, + address(this) + ); + + (nativeReserve, foreignReserve, ) = getReserves(foreignAssetB); // gas savings + + foreignAmountOut = VaderMath.calculateSwap( + nativeAmountOut, + nativeReserve, + foreignReserve + ); + + require( + foreignAmountOut > 0 && foreignAmountOut <= foreignReserve, + "BasePoolV2::doubleSwap: Swap Impossible" + ); + + _update( + foreignAssetB, + nativeReserve + nativeAmountOut, + foreignReserve - foreignAmountOut, + nativeReserve, + foreignReserve + ); + + emit Swap( + foreignAssetB, + msg.sender, + nativeAmountOut, + 0, + 0, + foreignAmountOut, + to + ); + + foreignAssetB.safeTransfer(to, foreignAmountOut); + } + + function swap( + IERC20 foreignAsset, + uint256 nativeAmountIn, + uint256 foreignAmountIn, + address to + ) + external + override + supportedToken(foreignAsset) + nonReentrant + validateGas + returns (uint256) + { + require( + (nativeAmountIn > 0 && foreignAmountIn == 0) || + (nativeAmountIn == 0 && foreignAmountIn > 0), + "BasePoolV2::swap: Only One-Sided Swaps Supported" + ); + (uint112 nativeReserve, uint112 foreignReserve, ) = getReserves( + foreignAsset + ); // gas savings + + uint256 nativeAmountOut; + uint256 foreignAmountOut; + { + // scope for _token{0,1}, avoids stack too deep errors + IERC20 _nativeAsset = nativeAsset; + require( + to != address(_nativeAsset) && to != address(foreignAsset), + "BasePoolV2::swap: Invalid Receiver" + ); + + if (foreignAmountIn > 0) { + nativeAmountOut = VaderMath.calculateSwap( + foreignAmountIn, + foreignReserve, + nativeReserve + ); + require( + nativeAmountOut > 0 && nativeAmountOut <= nativeReserve, + "BasePoolV2::swap: Swap Impossible" + ); + _nativeAsset.safeTransfer(to, nativeAmountOut); // optimistically transfer tokens + } else { + foreignAmountOut = VaderMath.calculateSwap( + nativeAmountIn, + nativeReserve, + foreignReserve + ); + require( + foreignAmountOut > 0 && foreignAmountOut <= foreignReserve, + "BasePoolV2::swap: Swap Impossible" + ); + foreignAsset.safeTransfer(to, foreignAmountOut); // optimistically transfer tokens + } + } + + _update( + foreignAsset, + nativeReserve - nativeAmountOut + nativeAmountIn, + foreignReserve - foreignAmountOut + foreignAmountIn, + nativeReserve, + foreignReserve + ); + + emit Swap( + foreignAsset, + msg.sender, + nativeAmountIn, + foreignAmountIn, + nativeAmountOut, + foreignAmountOut, + to + ); + + return nativeAmountOut > 0 ? nativeAmountOut : foreignAmountOut; + } + + function rescue(IERC20 foreignAsset) external { + uint256 foreignBalance = foreignAsset.balanceOf(address(this)); + uint256 reserveForeign = pairInfo[foreignAsset].reserveForeign; + + uint256 unaccounted = foreignBalance - reserveForeign; + + foreignAsset.safeTransfer(msg.sender, unaccounted); + } + + /* ========== RESTRICTED FUNCTIONS ========== */ + + /* ========== INTERNAL FUNCTIONS ========== */ + + function _update( + IERC20 foreignAsset, + uint256 balanceNative, + uint256 balanceForeign, + uint112 reserveNative, + uint112 reserveForeign + ) internal { + require( + balanceNative <= type(uint112).max && + balanceForeign <= type(uint112).max, + "BasePoolV2::_update: Balance Overflow" + ); + uint32 blockTimestamp = uint32(block.timestamp % 2**32); + PairInfo storage pair = pairInfo[foreignAsset]; + unchecked { + uint32 timeElapsed = blockTimestamp - pair.blockTimestampLast; // overflow is desired + if (timeElapsed > 0 && reserveNative != 0 && reserveForeign != 0) { + // * never overflows, and + overflow is desired + pair.priceCumulative.nativeLast += + uint256( + UQ112x112.encode(reserveForeign).uqdiv(reserveNative) + ) * + timeElapsed; + pair.priceCumulative.foreignLast += + uint256( + UQ112x112.encode(reserveNative).uqdiv(reserveForeign) + ) * + timeElapsed; + } + } + pair.reserveNative = uint112(balanceNative); + pair.reserveForeign = uint112(balanceForeign); + pair.blockTimestampLast = blockTimestamp; + emit Sync(foreignAsset, balanceNative, balanceForeign); + } + + /* ========== PRIVATE FUNCTIONS ========== */ + + function _supportedToken(IERC20 token) private view { + require( + supported[token], + "BasePoolV2::_supportedToken: Unsupported Token" + ); + } + + /* ========== MODIFIERS ========== */ + + modifier supportedToken(IERC20 token) { + _supportedToken(token); + _; + } +} diff --git a/contracts/dex-v2/pool/VaderPoolV2.sol b/contracts/dex-v2/pool/VaderPoolV2.sol new file mode 100644 index 0000000..96b50b6 --- /dev/null +++ b/contracts/dex-v2/pool/VaderPoolV2.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense + +pragma solidity =0.8.9; + +import "./BasePoolV2.sol"; + +import "../../interfaces/dex-v2/pool/IVaderPoolV2.sol"; + +contract VaderPoolV2 is IVaderPoolV2, BasePoolV2 { + /* ========== STATE VARIABLES ========== */ + + // Denotes whether the queue system is active + bool public queueActive; + + /* ========== CONSTRUCTOR ========== */ + + constructor(bool _queueActive, IERC20 _nativeAsset) + BasePoolV2(_nativeAsset) + { + queueActive = _queueActive; + } + + /* ========== VIEWS ========== */ + + /* ========== MUTATIVE FUNCTIONS ========== */ + + // NOTE: IL is only covered via router! + function burn(uint256 id, address to) + external + override + returns ( + uint256 amountNative, + uint256 amountForeign, + uint256 coveredLoss + ) + { + (amountNative, amountForeign) = _burn(id, to); + + Position storage position = positions[id]; + + uint256 creation = position.creation; + uint256 originalNative = position.originalNative; + uint256 originalForeign = position.originalForeign; + + delete positions[id]; + + // NOTE: Validate it behaves as expected for non-18 decimal tokens + uint256 loss = VaderMath.calculateLoss( + originalNative, + originalForeign, + amountNative, + amountForeign + ); + + // TODO: Original Implementation Applied 100 Days + coveredLoss = + (loss * _min(block.timestamp - creation, _ONE_YEAR)) / + _ONE_YEAR; + } + + /* ========== RESTRICTED FUNCTIONS ========== */ + + // TODO: Investigate Necessity + function toggleQueue() external override onlyOwner { + bool _queueActive = !queueActive; + queueActive = _queueActive; + emit QueueActive(_queueActive); + } + + function setTokenSupport(IERC20 foreignAsset, bool support) + external + override + onlyOwner + { + require( + supported[foreignAsset] != support, + "VaderPoolV2::supportToken: Already At Desired State" + ); + supported[foreignAsset] = support; + } + + /* ========== INTERNAL FUNCTIONS ========== */ + + /* ========== PRIVATE FUNCTIONS ========== */ + + /** + * @dev Calculates the minimum of the two values + */ + function _min(uint256 a, uint256 b) private pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/contracts/dex-v2/router/VaderRouterV2.sol b/contracts/dex-v2/router/VaderRouterV2.sol new file mode 100644 index 0000000..1d5aaf3 --- /dev/null +++ b/contracts/dex-v2/router/VaderRouterV2.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Unlicense + +pragma solidity =0.8.9; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../../shared/ProtocolConstants.sol"; + +import "../../dex/math/VaderMath.sol"; + +import "../../interfaces/reserve/IVaderReserve.sol"; +import "../../interfaces/dex-v2/router/IVaderRouterV2.sol"; +import "../../interfaces/dex-v2/pool/IVaderPoolV2.sol"; + +contract VaderRouterV2 is IVaderRouterV2, ProtocolConstants, Ownable { + /* ========== LIBRARIES ========== */ + + // Used for safe token transfers + using SafeERC20 for IERC20; + + /* ========== STATE VARIABLES ========== */ + + IVaderPoolV2 public immutable pool; + IERC20 public immutable nativeAsset; + IVaderReserve public reserve; + + /* ========== CONSTRUCTOR ========== */ + + constructor(IVaderPoolV2 _pool) { + require( + _pool != IVaderPoolV2(_ZERO_ADDRESS), + "VaderRouterV2::constructor: Incorrect Arguments" + ); + + pool = _pool; + nativeAsset = pool.nativeAsset(); + } + + /* ========== VIEWS ========== */ + + /* ========== MUTATIVE FUNCTIONS ========== */ + + // NOTE: For Uniswap V2 compliancy, necessary due to stack too deep + function addLiquidity( + IERC20 tokenA, + IERC20 tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256, // amountAMin = unused + uint256, // amountBMin = unused + address to, + uint256 deadline + ) external override returns (uint256 liquidity) { + return + addLiquidity( + tokenA, + tokenB, + amountADesired, + amountBDesired, + to, + deadline + ); + } + + function addLiquidity( + IERC20 tokenA, + IERC20 tokenB, + uint256 amountADesired, + uint256 amountBDesired, + address to, + uint256 deadline + ) public override ensure(deadline) returns (uint256 liquidity) { + IERC20 foreignAsset; + uint256 nativeDeposit; + uint256 foreignDeposit; + + if (tokenA == nativeAsset) { + require( + pool.supported(tokenB), + "VaderRouterV2::addLiquidity: Unsupported Assets Specified" + ); + foreignAsset = tokenB; + foreignDeposit = amountBDesired; + nativeDeposit = amountADesired; + } else { + require( + tokenB == nativeAsset && pool.supported(tokenA), + "VaderRouterV2::addLiquidity: Unsupported Assets Specified" + ); + foreignAsset = tokenA; + foreignDeposit = amountADesired; + nativeDeposit = amountBDesired; + } + + liquidity = pool.mint( + foreignAsset, + nativeDeposit, + foreignDeposit, + msg.sender, + to + ); + } + + function removeLiquidity( + address tokenA, + address tokenB, + uint256 id, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) + public + override + ensure(deadline) + returns (uint256 amountA, uint256 amountB) + { + IERC20 _foreignAsset = pool.positionForeignAsset(id); + IERC20 _nativeAsset = nativeAsset; + + bool isNativeA = _nativeAsset == IERC20(tokenA); + + if (isNativeA) { + require( + IERC20(tokenB) == _foreignAsset, + "VaderRouterV2::removeLiquidity: Incorrect Addresses Specified" + ); + } else { + require( + IERC20(tokenA) == _foreignAsset && + IERC20(tokenB) == _nativeAsset, + "VaderRouterV2::removeLiquidity: Incorrect Addresses Specified" + ); + } + + pool.transferFrom(msg.sender, address(pool), id); + + ( + uint256 amountNative, + uint256 amountForeign, + uint256 coveredLoss + ) = pool.burn(id, to); + + (amountA, amountB) = isNativeA + ? (amountNative, amountForeign) + : (amountForeign, amountNative); + + require( + amountA >= amountAMin, + "VaderRouterV2: INSUFFICIENT_A_AMOUNT" + ); + require( + amountB >= amountBMin, + "VaderRouterV2: INSUFFICIENT_B_AMOUNT" + ); + + reserve.reimburseImpermanentLoss(msg.sender, coveredLoss); + } + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + IERC20[] calldata path, + address to, + uint256 deadline + ) external virtual override ensure(deadline) returns (uint256 amountOut) { + amountOut = _swap(amountIn, path, to); + + require( + amountOut >= amountOutMin, + "VaderRouterV2::swapExactTokensForTokens: Insufficient Trade Output" + ); + } + + /* ========== RESTRICTED FUNCTIONS ========== */ + + function initialize(IVaderReserve _reserve) external onlyOwner { + require( + _reserve != IVaderReserve(_ZERO_ADDRESS), + "VaderRouterV2::initialize: Incorrect Reserve Specified" + ); + + reserve = _reserve; + + renounceOwnership(); + } + + /* ========== INTERNAL FUNCTIONS ========== */ + + /* ========== PRIVATE FUNCTIONS ========== */ + + function _swap( + uint256 amountIn, + IERC20[] calldata path, + address to + ) private returns (uint256 amountOut) { + if (path.length == 3) { + require( + path[0] != path[1] && + path[1] == pool.nativeAsset() && + path[2] != path[1], + "VaderRouterV2::_swap: Incorrect Path" + ); + + path[0].safeTransferFrom(msg.sender, address(pool), amountIn); + + return pool.doubleSwap(path[0], path[2], amountIn, to); + } else { + require( + path.length == 2, + "VaderRouterV2::_swap: Incorrect Path Length" + ); + IERC20 _nativeAsset = nativeAsset; + require(path[0] != path[1], "VaderRouterV2::_swap: Incorrect Path"); + + path[0].safeTransferFrom(msg.sender, address(pool), amountIn); + if (path[0] == _nativeAsset) { + return pool.swap(path[1], amountIn, 0, to); + } else { + require( + path[1] == _nativeAsset, + "VaderRouterV2::_swap: Incorrect Path" + ); + return pool.swap(path[0], 0, amountIn, to); + } + } + } + + /* ========== MODIFIERS ========== */ + + modifier ensure(uint256 deadline) { + require(deadline >= block.timestamp, "VaderRouterV2::ensure: Expired"); + _; + } +} diff --git a/contracts/dex/pool/BasePool.sol b/contracts/dex/pool/BasePool.sol index b327abd..f58004d 100644 --- a/contracts/dex/pool/BasePool.sol +++ b/contracts/dex/pool/BasePool.sol @@ -17,6 +17,22 @@ import "../../external/libraries/UQ112x112.sol"; import "../../interfaces/dex/pool/IBasePool.sol"; import "../../interfaces/shared/IERC20Extended.sol"; +/* + * @dev Implementation of {BasePool} contract. + * + * The BasePool contract represents pool of two assets termed as native and + * foreign assets. The functionality in this contract allows depositing of both + * of these assets to mint liquidity. Minted liquidity is associated with a + * position which is represented by an NFT token. + * + * The contract allows burning of NFT and in turn redeems the associated liquidity, + * transferring out underlying assets to the LP. + * + * The contract allows swapping of both native and foreign assets among themselves. + * + * Keeps track of the cumulative prices for both native and foreign assets and updates + * them after minting and burning of liquidity, and swapping of assets. + **/ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { /* ========== LIBRARIES ========== */ @@ -28,23 +44,53 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { /* ========== STATE VARIABLES ========== */ + // Address of native asset (Vader or USDV). IERC20 public immutable nativeAsset; + + // Address of foreign asset with which the native asset is paired in the pool. IERC20 public immutable foreignAsset; + + // Cumulative price of native asset. uint256 public priceNativeCumulativeLast; + + // Cumulative price of foreign asset. uint256 public priceForeignCumulativeLast; + /* + * @dev A mapping representing positions of liquidity providers. Each position + * is an Non-fungible token that is mapped against amounts of native and foreign assets + * deposited, the timestamp at which the position is created and the amount of + * liquidity assigned to the LP. + * + * Each position in the mapping is mapped against {positionId}. + **/ mapping(uint256 => Position) public positions; + + // A unique id the of the position created when liquidity is added to the pool. uint256 public positionId; + + // Total amount of liquidity units minted. uint256 public totalSupply; + // Name of the contract. string private _name; + // Total amount of the native asset realised by the contract. uint112 private _reserveNative; // uses single storage slot, accessible via getReserves + + // Total amount of the foreign asset realised by the contract. uint112 private _reserveForeign; // uses single storage slot, accessible via getReserves + + // Last timestamp at which the cumulative prices for native and foreign assets were updated. uint32 private _blockTimestampLast; // uses single storage slot, accessible via getReserves /* ========== CONSTRUCTOR ========== */ + /* + * @dev Initialized the contract state setting the addresses for native and foreign assets. + * + * Also computes the name of the contract and stores it in the contract's state. + **/ constructor(IERC20Extended _nativeAsset, IERC20Extended _foreignAsset) ERC721("Vader LP", "VLP") { @@ -59,6 +105,10 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { /* ========== VIEWS ========== */ + /* + * @dev Returns the realised amount of native and foreign assets, and the last timestamp + * at which the cumulative prices for native and foreign assets were updated. + **/ function getReserves() public view @@ -73,12 +123,29 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { blockTimestampLast = _blockTimestampLast; } + // Returns the name of the contract. function name() public view override returns (string memory) { return _name; } /* ========== MUTATIVE FUNCTIONS ========== */ + /* + * @dev Allows depositing of liquidity to the pool by accepting native and foreign assets + * and mints an NFT to the {to} address which records the amounts of the native and foreign + * assets deposited and the liquidity units minted against it in {positions} mapping. + * + * Updates the total supply of liquidity units by adding currently minted liquidity units + * to {totalSupply}. + * + * Updates the cumulative prices of native and foreign assets after minting the appropriate + * liquidity units. + * + * Requirements: + * - Amounts of native and foreign must be sent to the pool prior to calling `mint` such that + * balance of pool for both assets must be greater than their corresponding reserves. + * - The amount of {liquidity} to be minted must be greater than 0. + **/ function mint(address to) external override @@ -126,6 +193,24 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { emit PositionOpened(msg.sender, id, liquidity); } + /* + * @dev Allows redeeming of liquidity units by burning the NFT associated with the liquidity + * position. + * + * Computes the amounts of native and foreign assets depending upon current reserves of assets and + * the liquidity associated with the position, and transfers them to the {to} address. + * + * Burns the redeemed NFT token and decreases {totalSupply} by the {liquidity} + * associated with that NFT token. + * + * Updates the cumulative prices for native and foreign assets after transferring the assets + * to the {to} address. + * + * Requirements: + * - The NFT token being redeemed must be transferred to the pool prior to calling `_burn`. + * - The amount of native and foreign assets computed for transfer to {to} address must be greater + * than 0. + **/ function _burn(uint256 id, address to) internal nonReentrant @@ -167,6 +252,12 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { emit Burn(msg.sender, amountNative, amountForeign, to); } + /* + * @dev Allows swapping between native and foreign assets. It receives the source asset + * and computes the destination asset and transfers it to the {to} address. + * + * Internally calls {swap} function. + **/ function swap( uint256 nativeAmountIn, uint256 foreignAmountIn, @@ -176,6 +267,25 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { return swap(nativeAmountIn, foreignAmountIn, to); } + /* + * @dev Allows swapping between native and foreign assets. It receives the source asset + * and computes the destination asset and transfers it to the {to} address. + * + * Updates the cumulative prices for native and foreign assets after performing swap. + * + * Returns the amount of destination tokens resulting from the swap. + * + * Requirements: + * - Param {nativeAmountIn} must be zero and {foreignAmountIn} must be non-zero + * if the destination asset in swap is native asset. + * - Param {foreignAmountIn} must be zero and {nativeAmountIn} must be non zero + * if the destination asset in swap is foreign asset. + * - Param {to} cannot be the addresses of native or foreign assets. + * - The source asset amount in the swap must be transferred to the pool prior to calling `swap`. + * - The source asset amount in the swap cannot exceed the source asset's reserve. + * - The destination asset's amount in the swap must be greater than 0 and not exceed destination + * asset's reserve. + **/ function swap( uint256 nativeAmountIn, uint256 foreignAmountIn, @@ -272,6 +382,15 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { /* ========== INTERNAL FUNCTIONS ========== */ + /* + * @dev Internally called to update the cumulative prices for native and foreign assets depending + * upon the last reserves and updates the reserves for both of the assets corresponding to their + * current balances along with the {_blockTimestampLast}. + * + * Requirements: + * - Params {balanceNative} and {balanceForeign} must not overflow type `uint112`. + * + **/ function _update( uint256 balanceNative, uint256 balanceForeign, @@ -284,15 +403,21 @@ contract BasePool is IBasePool, GasThrottle, ERC721, Ownable, ReentrancyGuard { "BasePool::_update: Balance Overflow" ); uint32 blockTimestamp = uint32(block.timestamp % 2**32); - uint32 timeElapsed = blockTimestamp - _blockTimestampLast; // overflow is desired - if (timeElapsed > 0 && reserveNative != 0 && reserveForeign != 0) { - // * never overflows, and + overflow is desired - priceNativeCumulativeLast += - uint256(UQ112x112.encode(reserveForeign).uqdiv(reserveNative)) * - timeElapsed; - priceForeignCumulativeLast += - uint256(UQ112x112.encode(reserveNative).uqdiv(reserveForeign)) * - timeElapsed; + unchecked { + uint32 timeElapsed = blockTimestamp - _blockTimestampLast; // overflow is desired + if (timeElapsed > 0 && reserveNative != 0 && reserveForeign != 0) { + // * never overflows, and + overflow is desired + priceNativeCumulativeLast += + uint256( + UQ112x112.encode(reserveForeign).uqdiv(reserveNative) + ) * + timeElapsed; + priceForeignCumulativeLast += + uint256( + UQ112x112.encode(reserveNative).uqdiv(reserveForeign) + ) * + timeElapsed; + } } _reserveNative = uint112(balanceNative); _reserveForeign = uint112(balanceForeign); diff --git a/contracts/dex/pool/VaderPool.sol b/contracts/dex/pool/VaderPool.sol index c10456c..8a714b5 100644 --- a/contracts/dex/pool/VaderPool.sol +++ b/contracts/dex/pool/VaderPool.sol @@ -6,7 +6,17 @@ import "./BasePool.sol"; import "../../interfaces/dex/pool/IVaderPool.sol"; -// TBD +/* + * @dev Implementation of {VaderPool} contract. + * + * The contract VaderPool inherits from {BasePool} contract and implements + * queue system. + * + * Extends on the liquidity redeeming function by introducing the `burn` function + * that internally calls the namesake on `BasePool` contract and computes the + * loss covered by the position being redeemed and returns it along with amounts + * of native and foreign assets sent. + **/ contract VaderPool is IVaderPool, BasePool { /* ========== STATE VARIABLES ========== */ @@ -15,6 +25,11 @@ contract VaderPool is IVaderPool, BasePool { /* ========== CONSTRUCTOR ========== */ + /* + * @dev Initializes contract's state by passing addresses of + * native and foreign assets to {BasePool} contract and setting + * active status of queue. + **/ constructor( bool _queueActive, IERC20Extended _nativeAsset, @@ -27,6 +42,18 @@ contract VaderPool is IVaderPool, BasePool { /* ========== MUTATIVE FUNCTIONS ========== */ + /* + * @dev Allows burning of NFT represented by param {id} for liquidity redeeming. + * + * Deletes the position in {positions} mapping against the burned NFT token. + * + * Internally calls `_burn` function on {BasePool} contract. + * + * Calculates the impermanent loss incurred by the position. + * + * Returns the amounts for native and foreign assets sent to the {to} address + * along with the covered loss. + **/ // NOTE: IL is only covered via router! function burn(uint256 id, address to) external diff --git a/contracts/dex/pool/VaderPoolFactory.sol b/contracts/dex/pool/VaderPoolFactory.sol index 1ca1ec2..c7ec090 100644 --- a/contracts/dex/pool/VaderPoolFactory.sol +++ b/contracts/dex/pool/VaderPoolFactory.sol @@ -9,7 +9,17 @@ import "../../shared/ProtocolConstants.sol"; import "../../interfaces/shared/IERC20Extended.sol"; import "../../interfaces/dex/pool/IVaderPoolFactory.sol"; -// TBD +/* + * @dev Implementation of {VaderPoolFactory} contract. + * + * The VaderPoolFactory contract inherits from {Ownable} and {ProtocolConstants} contracts. + * + * Keeps track of all the created Vader pools through {getPool} mapping and + * {allPools} array. Also stores the address of asset used as native asset + * across all of the Vader pools created through the factory. + * + * Allows creation of new Vader pools. + **/ contract VaderPoolFactory is IVaderPoolFactory, ProtocolConstants, Ownable { /* ========== STATE VARIABLES ========== */ @@ -29,6 +39,17 @@ contract VaderPoolFactory is IVaderPoolFactory, ProtocolConstants, Ownable { /* ========== MUTATIVE FUNCTIONS ========== */ + /* + * @dev Allows creation of a Vader pool of native and foreign assets. + * + * Populates the {getPool} mapping with the newly created Vader pool and + * pushes this pool to {allPools} array. + * + * Requirements: + * - Native and foreign assets cannot be the same. + * - Foreign asset cannot be the zero address. + * - The pool against the specified foreign asset does not already exist. + **/ // NOTE: Between deployment & initialization may be corrupted but chance small function createPool(address tokenA, address tokenB) external @@ -58,8 +79,8 @@ contract VaderPoolFactory is IVaderPoolFactory, ProtocolConstants, Ownable { pool = new VaderPool( queueActive, - IERC20Extended(tokenA), - IERC20Extended(tokenB) + IERC20Extended(token0), + IERC20Extended(token1) ); getPool[token0][token1] = pool; getPool[token1][token0] = pool; // populate mapping in the reverse direction @@ -69,6 +90,14 @@ contract VaderPoolFactory is IVaderPoolFactory, ProtocolConstants, Ownable { /* ========== RESTRICTED FUNCTIONS ========== */ + /* + * @dev Allows initializing of the factory contract by owner by setting the + * address of native asset for all the Vader pool and also transferring the + * contract's ownership to {_dao}. + * + * Requirements: + * - Only onwer can call this function. + **/ function initialize(address _nativeAsset, address _dao) external onlyOwner { require( _nativeAsset != _ZERO_ADDRESS && _dao != _ZERO_ADDRESS, @@ -79,6 +108,12 @@ contract VaderPoolFactory is IVaderPoolFactory, ProtocolConstants, Ownable { transferOwnership(_dao); } + /* + * @dev Allows toggling of queue system of a pool. + * + * Requirements: + * - This function can only be called when DAO is active. + **/ function toggleQueue(address token0, address token1) external onlyDAO { getPool[token0][token1].toggleQueue(); } diff --git a/contracts/dex/router/VaderRouter.sol b/contracts/dex/router/VaderRouter.sol index 86a95b0..5997477 100644 --- a/contracts/dex/router/VaderRouter.sol +++ b/contracts/dex/router/VaderRouter.sol @@ -13,6 +13,24 @@ import "../../interfaces/reserve/IVaderReserve.sol"; import "../../interfaces/dex/router/IVaderRouter.sol"; import "../../interfaces/dex/pool/IVaderPoolFactory.sol"; +/* + @dev Implementation of {VaderRouter} contract. + * + * The contract VaderRouter inherits from {Ownable} and {ProtocolConstants} contracts. + * + * It allows adding of liquidity to Vader pools and facilitate creation of Vader pools if + * it does not already exist when depositing liquidity. + * + * Allows removing of liquidity by the users and claiming the underlying assets from + * the Vader pools. + * + * Allows swapping between native and foreign assets within a single Vader pool. + * + * Allows swapping of foreign assets across two different Vader pools. + * + * Contains helper functions to compute the destination asset amount given the exact source + * asset amount and vice versa. + **/ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { /* ========== LIBRARIES ========== */ @@ -21,11 +39,20 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { /* ========== STATE VARIABLES ========== */ + // The address of Vader pool factory contract. IVaderPoolFactory public immutable factory; + + // The address of Reserve contract. IVaderReserve public reserve; /* ========== CONSTRUCTOR ========== */ + /* + * @dev Initializes contract's state by setting the vader pool factory address. + * + * Requirements: + * - Vader pool factory address must not be zero. + **/ constructor(IVaderPoolFactory _factory) { require( _factory != IVaderPoolFactory(_ZERO_ADDRESS), @@ -39,6 +66,14 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { /* ========== MUTATIVE FUNCTIONS ========== */ + /* + * @dev Allows adding of liquidity to the Vader pools. + * + * Internally calls {addLiquidity} function. + * + * Returns the amounts of assetA and assetB used in liquidity and + * the amount of liquidity units minted. + **/ // NOTE: For Uniswap V2 compliancy, necessary due to stack too deep function addLiquidity( IERC20 tokenA, @@ -69,6 +104,22 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { ); } + /* + * @dev Allows adding of liquidity to the Vader pools. + * + * Internally calls {_addLiquidity} function. + * + * Transfers the amounts of tokenA and tokenB from {msg.sender} to the pool. + * + * Calls the {mint} function on the pool to deposit liquidity on the behalf of + * {to} address. + * + * Returns the amounts of assetA and assetB used in liquidity and + * the amount of liquidity units minted. + * + * Requirements: + * - The current timestamp has not exceeded the param {deadline}. + **/ function addLiquidity( IERC20 tokenA, IERC20 tokenB, @@ -98,6 +149,23 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { liquidity = pool.mint(to); } + /* + * @dev Allows removing of liquidity by {msg.sender} and transfers the + * underlying assets to {to} address. + * + * Transfers the NFT with Id {id} representing user's position, to the pool address, + * so the pool is able to burn it in the `burn` function call. + * + * Calls the `burn` function on the pool contract. + * + * Calls the `reimburseImpermanentLoss` on reserve contract to cover impermanent loss + * for the liquidity being removed. + * + * Requirements: + * - The underlying assets amounts of {amountA} and {amountB} must + * be greater than or equal to {amountAMin} and {amountBMin}, respectively. + * - The current timestamp has not exceeded the param {deadline}. + **/ function removeLiquidity( address tokenA, address tokenB, @@ -138,6 +206,16 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { reserve.reimburseImpermanentLoss(msg.sender, coveredLoss); } + /* + * @dev Allows swapping of exact source token amount to destination + * token amount. + * + * Internally calls {_swap} function. + * + * Requirements: + * - The destination amount {amountOut} must greater than or equal to param {amountOutMin}. + * - The current timestamp has not exceeded the param {deadline}. + **/ function swapExactTokensForTokens( uint256 amountIn, uint256 amountOutMin, @@ -153,6 +231,16 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { ); } + /* + * @dev Allows swapping of source token amount to exact destination token + * amount. + * + * Internally calls {calculateInGivenOut} and {_swap} functions. + * + * Requirements: + * - Param {amountInMax} must be greater than or equal to the source amount computed {amountIn}. + * - The current timestamp has not exceeded the param {deadline}. + **/ function swapTokensForExactTokens( uint256 amountOut, uint256 amountInMax, @@ -172,6 +260,13 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { /* ========== RESTRICTED FUNCTIONS ========== */ + /* + * @dev Sets the reserve address and renounces contract's ownership. + * + * Requirements: + * - Only existing owner can call this function. + * - Param {_reserve} cannot be a zero address. + **/ function initialize(IVaderReserve _reserve) external onlyOwner { require( _reserve != IVaderReserve(_ZERO_ADDRESS), @@ -187,6 +282,24 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { /* ========== PRIVATE FUNCTIONS ========== */ + /* + * @dev Allows swapping of assets from within a single Vader pool or + * across two different Vader pools. + * + * In case of a single Vader pool, the native asset can be swapped for foreign + * asset and vice versa. + * + * In case of two Vader pools, the foreign asset is swapped for native asset from + * the first Vader pool and the native asset retrieved from the first Vader pool is swapped + * for foreign asset from the second Vader pool. + * + * Requirements: + * - Param {path} length can be either 2 or 3. + * - If the {path} length is 3 the index 0 and 1 must contain foreign assets' addresses + * and index 1 must contain native asset's address. + * - If the {path} length is 2 then either of indexes must contain foreign asset's address + * and the other one must contain native asset's address. + **/ // TODO: Refactor with central pool, perhaps diminishes security? would need directSwap & bridgeSwap function _swap( uint256 amountIn, @@ -237,6 +350,11 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { } } + /* + * @dev An internal function that returns Vader pool's address against + * the provided assets of {tokenA} and {tokenB} if it exists, otherwise + * a new Vader pool created against the provided assets. + **/ // NOTE: DEX allows asymmetric deposits function _addLiquidity( address tokenA, @@ -260,6 +378,19 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { (amountA, amountB) = (amountADesired, amountBDesired); } + /* + * @dev Returns the amount of source asset given the amount of destination asset. + * + * Calls the {calculateSwapReverse} on VaderMath library to compute the source + * token amount. + * + * Requirements: + * - Param {path} length can be either 2 or 3. + * - If the {path} length is 3 the index 0 and 1 must contain foreign assets' addresses + * and index 1 must contain native asset's address. + * - If the {path} length is 2 then either of indexes must contain foreign asset's address + * and the other one must contain native asset's address. + **/ function calculateInGivenOut(uint256 amountOut, address[] calldata path) public view @@ -306,6 +437,19 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { } } + /* + * @dev Returns the amount of destination asset given the amount of source asset. + * + * Calls the {calculateSwap} on VaderMath library to compute the destination + * token amount. + * + * Requirements: + * - Param {path} length can be either 2 or 3. + * - If the {path} length is 3 the index 0 and 1 must contain foreign assets' addresses + * and index 1 must contain native asset's address. + * - If the {path} length is 2 then either of indexes must contain foreign asset's address + * and the other one must contain native asset's address. + **/ function calculateOutGivenIn(uint256 amountIn, address[] calldata path) external view @@ -354,6 +498,7 @@ contract VaderRouter is IVaderRouter, ProtocolConstants, Ownable { /* ========== MODIFIERS ========== */ + // Guard ensuring that the current timestamp has not exceeded the param {deadline}. modifier ensure(uint256 deadline) { require(deadline >= block.timestamp, "VaderRouter::ensure: Expired"); _; diff --git a/contracts/interfaces/dex-v2/pool/IBasePoolV2.sol b/contracts/interfaces/dex-v2/pool/IBasePoolV2.sol new file mode 100644 index 0000000..94effad --- /dev/null +++ b/contracts/interfaces/dex-v2/pool/IBasePoolV2.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Unlicense + +pragma solidity =0.8.9; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IBasePoolV2 { + /* ========== STRUCTS ========== */ + + struct Position { + IERC20 foreignAsset; + uint256 creation; + uint256 liquidity; + uint256 originalNative; + uint256 originalForeign; + } + + struct PairInfo { + uint256 totalSupply; + uint112 reserveNative; + uint112 reserveForeign; + uint32 blockTimestampLast; + PriceCumulative priceCumulative; + } + + struct PriceCumulative { + uint256 nativeLast; + uint256 foreignLast; + } + + /* ========== FUNCTIONS ========== */ + + function nativeAsset() external view returns (IERC20); + + function supported(IERC20 token) external view returns (bool); + + function positionForeignAsset(uint256 id) external view returns (IERC20); + + function doubleSwap( + IERC20 foreignAssetA, + IERC20 foreignAssetB, + uint256 foreignAmountIn, + address to + ) external returns (uint256); + + function swap( + IERC20 foreignAsset, + uint256 nativeAmountIn, + uint256 foreignAmountIn, + address to + ) external returns (uint256); + + function mint( + IERC20 foreignAsset, + uint256 nativeDeposit, + uint256 foreignDeposit, + address from, + address to + ) external returns (uint256 liquidity); + + /* ========== EVENTS ========== */ + + event Mint( + address indexed sender, + address indexed to, + uint256 amount0, + uint256 amount1 + ); // Adjustment -> new argument to which didnt exist + event Burn( + address indexed sender, + uint256 amount0, + uint256 amount1, + address indexed to + ); + event Swap( + IERC20 foreignAsset, + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(IERC20 foreignAsset, uint256 reserve0, uint256 reserve1); // Adjustment -> 112 to 256 + event PositionOpened( + address indexed from, + address indexed to, + uint256 id, + uint256 liquidity + ); + event PositionClosed( + address indexed sender, + uint256 id, + uint256 liquidity, + uint256 loss + ); +} diff --git a/contracts/interfaces/dex-v2/pool/IVaderPoolFactoryV2.sol b/contracts/interfaces/dex-v2/pool/IVaderPoolFactoryV2.sol new file mode 100644 index 0000000..a662056 --- /dev/null +++ b/contracts/interfaces/dex-v2/pool/IVaderPoolFactoryV2.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Unlicense + +pragma solidity =0.8.9; + +import "./IVaderPoolV2.sol"; + +interface IVaderPoolFactoryV2 { + /* ========== STRUCTS ========== */ + + /* ========== FUNCTIONS ========== */ + + function createPool(address tokenA, address tokenB) + external + returns (IVaderPoolV2); + + function getPool(address tokenA, address tokenB) + external + returns (IVaderPoolV2); + + function nativeAsset() external view returns (address); + + /* ========== EVENTS ========== */ + + event PoolCreated( + address token0, + address token1, + IVaderPoolV2 pool, + uint256 totalPools + ); +} diff --git a/contracts/interfaces/dex-v2/pool/IVaderPoolV2.sol b/contracts/interfaces/dex-v2/pool/IVaderPoolV2.sol new file mode 100644 index 0000000..77db422 --- /dev/null +++ b/contracts/interfaces/dex-v2/pool/IVaderPoolV2.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Unlicense + +pragma solidity =0.8.9; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./IBasePoolV2.sol"; + +interface IVaderPoolV2 is IBasePoolV2, IERC721 { + /* ========== STRUCTS ========== */ + /* ========== FUNCTIONS ========== */ + + function burn(uint256 id, address to) + external + returns ( + uint256 amountNative, + uint256 amountForeign, + uint256 coveredLoss + ); + + function toggleQueue() external; + + function setTokenSupport(IERC20 foreignAsset, bool support) external; + + /* ========== EVENTS ========== */ + + event QueueActive(bool activated); +} diff --git a/contracts/interfaces/dex-v2/router/IVaderRouterV2.sol b/contracts/interfaces/dex-v2/router/IVaderRouterV2.sol new file mode 100644 index 0000000..974c0ed --- /dev/null +++ b/contracts/interfaces/dex-v2/router/IVaderRouterV2.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense + +pragma solidity =0.8.9; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IVaderRouterV2 { + /* ========== STRUCTS ========== */ + /* ========== FUNCTIONS ========== */ + + function addLiquidity( + IERC20 tokenA, + IERC20 tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256, // amountAMin = unused + uint256, // amountBMin = unused + address to, + uint256 deadline + ) external returns (uint256 liquidity); + + function addLiquidity( + IERC20 tokenA, + IERC20 tokenB, + uint256 amountADesired, + uint256 amountBDesired, + address to, + uint256 deadline + ) external returns (uint256 liquidity); + + function removeLiquidity( + address tokenA, + address tokenB, + uint256 id, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + IERC20[] calldata path, + address to, + uint256 deadline + ) external returns (uint256 amountOut); + + /* ========== EVENTS ========== */ +} diff --git a/contracts/interfaces/dex/pool/IVaderPoolFactory.sol b/contracts/interfaces/dex/pool/IVaderPoolFactory.sol index a33302e..1291439 100644 --- a/contracts/interfaces/dex/pool/IVaderPoolFactory.sol +++ b/contracts/interfaces/dex/pool/IVaderPoolFactory.sol @@ -26,6 +26,6 @@ interface IVaderPoolFactory { address token0, address token1, IVaderPool pool, - uint256 totalPools + uint256 index ); } diff --git a/migrations/1_initial_migration.js b/migrations/1_initial_migration.js index cb5f6d8..16a7ba5 100644 --- a/migrations/1_initial_migration.js +++ b/migrations/1_initial_migration.js @@ -1,6 +1,5 @@ const Migrations = artifacts.require("Migrations"); module.exports = function (deployer) { - // disable Migration contract - // deployer.deploy(Migrations); + deployer.deploy(Migrations); }; diff --git a/package.json b/package.json index 739c7ce..104b6df 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,6 @@ "dependencies": { "@openzeppelin/contracts": "^4.3.2", "@openzeppelin/test-helpers": "^0.5.6", - "@truffle/hdwallet-provider": "^1.5.1", - "bn.js": "^5.2.0", - "dotenv": "^10.0.0", "solidity-coverage": "^0.7.17" - }, - "devDependencies": { - "truffle-plugin-verify": "^0.5.15" } } diff --git a/test/utils/index.js b/test/utils/index.js index ed2c8c8..07d327c 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -22,6 +22,11 @@ module.exports = (artifacts) => { const VaderMath = artifacts.require("VaderMath"); const BasePool = artifacts.require("BasePool"); + // V2 + const VaderRouterV2 = artifacts.require("VaderRouterV2"); + const VaderPoolV2 = artifacts.require("VaderPoolV2"); + const BasePoolV2 = artifacts.require("BasePoolV2"); + // Libraries // Generic Utilities @@ -197,6 +202,8 @@ module.exports = (artifacts) => { governorAlpha.address, big(10 * 60), // default delay of 10 minutes ], + VaderPoolV2: (_, { vader }) => [true, vader.address], + VaderRouterV2: (_, { poolV2 }) => [poolV2.address], }; // Project Utilities @@ -217,6 +224,7 @@ module.exports = (artifacts) => { const vaderMath = await VaderMath.new(); await VaderPoolFactory.link("VaderMath", vaderMath.address); await VaderRouter.link("VaderMath", vaderMath.address); + await VaderPoolV2.link("VaderMath", vaderMath.address); }; // Used to retrieve project constants @@ -306,6 +314,8 @@ module.exports = (artifacts) => { ...configs.Timelock(accounts, cached) ); + cached.poolV2 = await VaderPoolV2.new(...configs.VaderPoolV2(accounts, cached)); + cached.routerV2 = await VaderRouterV2.new(...configs.VaderRouterV2(accounts, cached)); return cached; diff --git a/test/v2/VaderRouterV2.test.js b/test/v2/VaderRouterV2.test.js new file mode 100644 index 0000000..e006c1a --- /dev/null +++ b/test/v2/VaderRouterV2.test.js @@ -0,0 +1,81 @@ +const { + // Deployment Function + deployMock, + + // Testing Utilities + assertBn, + assertErrors, + assertEvents, + UNSET_ADDRESS, + DEFAULT_CONFIGS, + TEN_UNITS, + + // Library Functions + verboseAccounts, + time, + big, + + // Project Specific Constants + PROJECT_CONSTANTS, +} = require("../utils")(artifacts); + + +contract("VaderRouter V2", (accounts) => { + describe("construction", () => { + it("should not allow construction with incorrect arguments", async () => { + if (Array.isArray(accounts)) + accounts = await verboseAccounts(accounts); + + await assertErrors( + deployMock(accounts, { + VaderRouterV2: () => [UNSET_ADDRESS], + }), + "VaderRouterV2::constructor: Incorrect Arguments" + ); + }); + + }); + + describe("add liquidity", () => { + it("should not allow to add liquidity with unsupported assets", async () => { + if (Array.isArray(accounts)) + accounts = await verboseAccounts(accounts); + const { routerV2, dai, mockUsdv } = await deployMock(accounts); + const latestBlock = await web3.eth.getBlock("latest"); + const timestampNow = latestBlock.timestamp + 1000; + await assertErrors(routerV2.addLiquidity(mockUsdv.address, UNSET_ADDRESS, 100, 100, accounts.account0, timestampNow), + "VaderRouterV2::addLiquidity: Unsupported Assets Specified"); + await assertErrors(routerV2.addLiquidity(dai.address, UNSET_ADDRESS, 100, 100, accounts.account0, timestampNow), + "VaderRouterV2::addLiquidity: Unsupported Assets Specified"); + }); + }); + + describe("initialize", () => { + it("should not allow to initialize with zero address", async () => { + if (Array.isArray(accounts)) + accounts = await verboseAccounts(accounts); + const { routerV2 } = await deployMock(accounts); + await assertErrors(routerV2.initialize(UNSET_ADDRESS), + "VaderRouterV2::initialize: Incorrect Reserve Specified"); + }); + + it("should initialize and check if the ownership is renounced", async () => { + if (Array.isArray(accounts)) + accounts = await verboseAccounts(accounts); + const { routerV2, reserve } = await deployMock(accounts); + await routerV2.initialize(reserve.address); + assert.equal(await routerV2.owner(), UNSET_ADDRESS); + }); + }); + + describe("ensure", () => { + it("should not allow to add liquidity with a deadline that is not in the future", async () => { + if (Array.isArray(accounts)) + accounts = await verboseAccounts(accounts); + const { routerV2, mockUsdv, dai } = await deployMock(accounts); + + await assertErrors(routerV2.addLiquidity(mockUsdv.address, dai.address, 100, 100, accounts.account0, 1000), + "VaderRouterV2::ensure: Expired"); + }); + }); +}); \ No newline at end of file diff --git a/truffle-config.js b/truffle-config.js index 968ca58..7c4d2fb 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -18,11 +18,11 @@ * */ -require("dotenv").config(); - -const HDWalletProvider = require("@truffle/hdwallet-provider"); -const fs = require("fs"); -const SEED = fs.readFileSync(".secret").toString().trim(); +// const HDWalletProvider = require('@truffle/hdwallet-provider'); +// const infuraKey = "fj4jll3k....."; +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync(".secret").toString().trim(); module.exports = { /** @@ -59,28 +59,16 @@ module.exports = { // Useful for deploying to a public network. // NB: It's important to wrap the provider as a function. // ropsten: { - // provider: () => new HDWalletProvider(SEED, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), + // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), // network_id: 3, // Ropsten's id // gas: 5500000, // Ropsten has a lower block limit than mainnet // confirmations: 2, // # of confs to wait between deployments. (default: 0) // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) // }, - kovan: { - provider: () => - new HDWalletProvider( - SEED, - `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}` - ), - network_id: 42, - gas: 5500000, - confirmations: 2, - timeoutBlocks: 200, - skipDryRun: true, - }, // Useful for private networks // private: { - // provider: () => new HDWalletProvider(SEED, `https://network.io`), + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), // network_id: 2111, // This network is yours, in the cloud. // production: true // Treats this network as if it was a public net. (default: false) // } @@ -103,8 +91,5 @@ module.exports = { }, }, }, - plugins: ["solidity-coverage", "truffle-plugin-verify"], - api_keys: { - etherscan: process.env.ETHER_SCAN_API_KEY, - }, + plugins: ["solidity-coverage"], };