├── .gitattributes ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── audits ├── REP-final-20220915T000103Z.pdf ├── REP-final-20230527T214448Z.pdf └── VirtuSwap_Security_Analysis_by_Pessimistic.pdf ├── contracts ├── interfaces │ ├── IvExchangeReserves.sol │ ├── IvFlashSwapCallback.sol │ ├── IvPair.sol │ ├── IvPairFactory.sol │ ├── IvPoolManager.sol │ ├── IvRouter.sol │ ├── IvSwapPoolDeployer.sol │ └── external │ │ ├── IWETH9.sol │ │ └── WETH9.sol ├── libraries │ ├── PoolAddress.sol │ └── vSwapLibrary.sol ├── test │ └── ReentrancyExploiter.sol ├── types.sol ├── vExchangeReserves.sol ├── vPair.sol ├── vPairFactory.sol ├── vPoolManager.sol ├── vRouter.sol └── vSwapERC20.sol ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── tasks └── compile.ts ├── test ├── 1_base.test.ts ├── 2_vPair.test.ts ├── 3_vRouter.test.ts ├── 4_reserveRatio.test.ts ├── 5_exchangeReserves.test.ts ├── 6_reserveRatioManipulations.test.ts ├── ReentrancyExploiter.sol ├── fixtures │ ├── deployPools.ts │ ├── reserveRatioManipulation.ts │ └── sameValues.ts └── utils.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | artifacts/ 10 | cache/ 11 | typechain-types/ 12 | 13 | # Compiled Java class files 14 | *.class 15 | 16 | # Compiled Python bytecode 17 | *.py[cod] 18 | 19 | # Log files 20 | *.log 21 | 22 | # Package files 23 | *.jar 24 | 25 | # Maven 26 | target/ 27 | dist/ 28 | 29 | # JetBrains IDE 30 | .idea/ 31 | 32 | # Unit test reports 33 | TEST*.xml 34 | 35 | # Generated by MacOS 36 | .DS_Store 37 | 38 | # Generated by Windows 39 | Thumbs.db 40 | 41 | # Applications 42 | *.app 43 | *.exe 44 | *.war 45 | 46 | # Large media files 47 | *.mp4 48 | *.tiff 49 | *.avi 50 | *.flv 51 | *.mov 52 | *.wmv 53 | 54 | /.vscode 55 | /build 56 | bin/contracts/VirtuPool-solc-output.json 57 | /bin 58 | 59 | .env 60 | coverage 61 | coverage.json 62 | typechain 63 | typechain-types 64 | 65 | #Hardhat files 66 | cache 67 | artifacts 68 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README # 2 | 3 | Implementation of the [Virtuswap Whitepaper]( https://virtuswap.io/docs/whitepaper.pdf) using Solidity for EVM-compatible networks. 4 | More in-depth documentation is available at [docs.virtuswap.io](https://docs.virtuswap.io). 5 | 6 | # Security # 7 | 8 | ### Security Audits 9 | - [CertiK](https://github.com/Virtuswap/v1-core/blob/main/audits/REP-final-20230527T214448Z.pdf) 10 | - [Pessimistic](https://github.com/Virtuswap/v1-core/blob/main/audits/VirtuSwap_Security_Analysis_by_Pessimistic.pdf) 11 | 12 | 13 | ### Bug Bounty 14 | The repository is part of VirtuSwap Bug Bounty program run by Hacken. See the details [here](https://hackenproof.com/virtuswap). 15 | 16 | # Local development # 17 | 18 | ``` 19 | git clone git@github.com:Virtuswap/v1-core.git 20 | cd v1-core 21 | npm i 22 | ``` 23 | 24 | 25 | # Compilation # 26 | ``` 27 | npx hardhat compile 28 | ``` 29 | 30 | # Running tests # 31 | ``` 32 | npx hardhat test 33 | ``` 34 | -------------------------------------------------------------------------------- /audits/REP-final-20220915T000103Z.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Virtuswap/v1-core/e6336ef80949d748f16e0c2580c5cc60bc386c98/audits/REP-final-20220915T000103Z.pdf -------------------------------------------------------------------------------- /audits/REP-final-20230527T214448Z.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Virtuswap/v1-core/e6336ef80949d748f16e0c2580c5cc60bc386c98/audits/REP-final-20230527T214448Z.pdf -------------------------------------------------------------------------------- /audits/VirtuSwap_Security_Analysis_by_Pessimistic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Virtuswap/v1-core/e6336ef80949d748f16e0c2580c5cc60bc386c98/audits/VirtuSwap_Security_Analysis_by_Pessimistic.pdf -------------------------------------------------------------------------------- /contracts/interfaces/IvExchangeReserves.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import './IvFlashSwapCallback.sol'; 6 | 7 | interface IvExchangeReserves is IvFlashSwapCallback { 8 | event ReservesExchanged( 9 | address jkPair1, 10 | address ikPair1, 11 | address jkPair2, 12 | address ikPair2, 13 | uint256 requiredBackAmount, 14 | uint256 flashAmountOut, 15 | address leftOverToken, 16 | uint leftOverAmount 17 | ); 18 | 19 | event NewIncentivesLimit(uint256 newLimit); 20 | 21 | function factory() external view returns (address); 22 | 23 | function exchange( 24 | address jkPair1, 25 | address ikPair1, 26 | address jkPair2, 27 | address ikPair2, 28 | uint256 flashAmountOut 29 | ) external; 30 | 31 | function changeIncentivesLimitPct(uint256 newLimit) external; 32 | } 33 | -------------------------------------------------------------------------------- /contracts/interfaces/IvFlashSwapCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | interface IvFlashSwapCallback { 6 | function vFlashSwapCallback( 7 | address tokenIn, 8 | address tokenOut, 9 | uint256 requiredBackAmount, 10 | bytes calldata data 11 | ) external; 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/IvPair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import '../types.sol'; 6 | 7 | interface IvPair { 8 | event Mint( 9 | address indexed sender, 10 | uint256 amount0, 11 | uint256 amount1, 12 | uint lpTokens, 13 | uint poolLPTokens 14 | ); 15 | 16 | event Burn( 17 | address indexed sender, 18 | uint256 amount0, 19 | uint256 amount1, 20 | address indexed to, 21 | uint256 totalSupply 22 | ); 23 | 24 | event Swap( 25 | address indexed sender, 26 | address tokenIn, 27 | address tokenOut, 28 | uint256 amountIn, 29 | uint256 amountOut, 30 | address indexed to 31 | ); 32 | 33 | event SwapReserve( 34 | address indexed sender, 35 | address tokenIn, 36 | address tokenOut, 37 | uint256 amountIn, 38 | uint256 amountOut, 39 | address ikPool, 40 | address indexed to 41 | ); 42 | 43 | event AllowListChanged(address[] tokens); 44 | 45 | event vSync(uint112 balance0, uint112 balance1); 46 | 47 | event ReserveSync(address asset, uint256 balance, uint256 rRatio); 48 | 49 | event FeeChanged(uint16 fee, uint16 vFee); 50 | 51 | event ReserveThresholdChanged(uint256 newThreshold); 52 | 53 | event BlocksDelayChanged(uint256 _newBlocksDelay); 54 | 55 | event ReserveRatioWarningThresholdChanged( 56 | uint256 _newReserveRatioWarningThreshold 57 | ); 58 | 59 | function fee() external view returns (uint16); 60 | 61 | function vFee() external view returns (uint16); 62 | 63 | function setFee(uint16 _fee, uint16 _vFee) external; 64 | 65 | function swapNative( 66 | uint256 amountOut, 67 | address tokenOut, 68 | address to, 69 | bytes calldata data 70 | ) external returns (uint256 _amountIn); 71 | 72 | function swapReserveToNative( 73 | uint256 amountOut, 74 | address ikPair, 75 | address to, 76 | bytes calldata data 77 | ) external returns (uint256 _amountIn); 78 | 79 | function swapNativeToReserve( 80 | uint256 amountOut, 81 | address ikPair, 82 | address to, 83 | uint256 incentivesLimitPct, 84 | bytes calldata data 85 | ) external returns (address _token, uint256 _leftovers); 86 | 87 | function liquidateReserve( 88 | address reserveToken, 89 | address nativePool 90 | ) external; 91 | 92 | function mint(address to) external returns (uint256 liquidity); 93 | 94 | function burn( 95 | address to 96 | ) external returns (uint256 amount0, uint256 amount1); 97 | 98 | function setAllowList(address[] memory _allowList) external; 99 | 100 | function allowListMap(address _token) external view returns (bool allowed); 101 | 102 | function calculateReserveRatio() external view returns (uint256 rRatio); 103 | 104 | function setMaxReserveThreshold(uint256 threshold) external; 105 | 106 | function setReserveRatioWarningThreshold(uint256 threshold) external; 107 | 108 | function setBlocksDelay(uint128 _newBlocksDelay) external; 109 | 110 | function emergencyToggle() external; 111 | 112 | function allowListLength() external view returns (uint); 113 | 114 | function token0() external view returns (address); 115 | 116 | function token1() external view returns (address); 117 | 118 | function pairBalance0() external view returns (uint112); 119 | 120 | function pairBalance1() external view returns (uint112); 121 | 122 | function maxReserveRatio() external view returns (uint256); 123 | 124 | function getBalances() external view returns (uint112, uint112); 125 | 126 | function lastSwapBlock() external view returns (uint128); 127 | 128 | function blocksDelay() external view returns (uint128); 129 | 130 | function getTokens() external view returns (address, address); 131 | 132 | function reservesBaseValue( 133 | address reserveAddress 134 | ) external view returns (uint256); 135 | 136 | function reserves(address reserveAddress) external view returns (uint256); 137 | 138 | function reservesBaseValueSum() external view returns (uint256); 139 | 140 | function reserveRatioFactor() external pure returns (uint256); 141 | } 142 | -------------------------------------------------------------------------------- /contracts/interfaces/IvPairFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | interface IvPairFactory { 6 | event PairCreated( 7 | address poolAddress, 8 | address factory, 9 | address token0, 10 | address token1, 11 | uint16 fee, 12 | uint16 vFee, 13 | uint256 maxReserveRatio 14 | ); 15 | 16 | event DefaultAllowListChanged(address[] allowList); 17 | 18 | event FactoryNewAdmin(address newAdmin); 19 | event FactoryNewPendingAdmin(address newPendingAdmin); 20 | 21 | event FactoryNewEmergencyAdmin(address newEmergencyAdmin); 22 | event FactoryNewPendingEmergencyAdmin(address newPendingEmergencyAdmin); 23 | 24 | event ExchangeReserveAddressChanged(address newExchangeReserve); 25 | 26 | event FactoryVPoolManagerChanged(address newVPoolManager); 27 | 28 | function createPair( 29 | address tokenA, 30 | address tokenB 31 | ) external returns (address); 32 | 33 | function pairs( 34 | address tokenA, 35 | address tokenB 36 | ) external view returns (address); 37 | 38 | function setDefaultAllowList(address[] calldata _defaultAllowList) external; 39 | 40 | function allPairs(uint256 index) external view returns (address); 41 | 42 | function allPairsLength() external view returns (uint256); 43 | 44 | function vPoolManager() external view returns (address); 45 | 46 | function admin() external view returns (address); 47 | 48 | function emergencyAdmin() external view returns (address); 49 | 50 | function pendingEmergencyAdmin() external view returns (address); 51 | 52 | function setPendingEmergencyAdmin(address newEmergencyAdmin) external; 53 | 54 | function acceptEmergencyAdmin() external; 55 | 56 | function pendingAdmin() external view returns (address); 57 | 58 | function setPendingAdmin(address newAdmin) external; 59 | 60 | function setVPoolManagerAddress(address _vPoolManager) external; 61 | 62 | function acceptAdmin() external; 63 | 64 | function exchangeReserves() external view returns (address); 65 | 66 | function setExchangeReservesAddress(address _exchangeReserves) external; 67 | } 68 | -------------------------------------------------------------------------------- /contracts/interfaces/IvPoolManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import '../types.sol'; 6 | 7 | interface IvPoolManager { 8 | function pairFactory() external view returns (address); 9 | 10 | function getVirtualPool( 11 | address jkPair, 12 | address ikPair 13 | ) external view returns (VirtualPoolModel memory vPool); 14 | 15 | function getVirtualPools( 16 | address token0, 17 | address token1 18 | ) external view returns (VirtualPoolModel[] memory vPools); 19 | 20 | function updateVirtualPoolBalances( 21 | address jkPair, 22 | address ikPair, 23 | uint256 balance0, 24 | uint256 balance1 25 | ) external; 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/IvRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | import '../types.sol'; 5 | 6 | interface IvRouter { 7 | event RouterFactoryChanged(address newFactoryAddress); 8 | 9 | function changeFactory(address _factory) external; 10 | 11 | function factory() external view returns (address); 12 | 13 | function WETH9() external view returns (address); 14 | 15 | function swapExactETHForTokens( 16 | address[] memory path, 17 | uint256 amountIn, 18 | uint256 minAmountOut, 19 | address to, 20 | uint256 deadline 21 | ) external payable; 22 | 23 | function swapExactTokensForETH( 24 | address[] memory path, 25 | uint256 amountIn, 26 | uint256 minAmountOut, 27 | address to, 28 | uint256 deadline 29 | ) external; 30 | 31 | function swapETHForExactTokens( 32 | address[] memory path, 33 | uint256 amountOut, 34 | uint256 maxAmountIn, 35 | address to, 36 | uint256 deadline 37 | ) external payable; 38 | 39 | function swapTokensForExactETH( 40 | address[] memory path, 41 | uint256 amountOut, 42 | uint256 maxAmountIn, 43 | address to, 44 | uint256 deadline 45 | ) external; 46 | 47 | function swapReserveETHForExactTokens( 48 | address tokenOut, 49 | address commonToken, 50 | address ikPair, 51 | uint256 amountOut, 52 | uint256 maxAmountIn, 53 | address to, 54 | uint256 deadline 55 | ) external payable; 56 | 57 | function swapReserveTokensForExactETH( 58 | address tokenOut, 59 | address commonToken, 60 | address ikPair, 61 | uint256 amountOut, 62 | uint256 maxAmountIn, 63 | address to, 64 | uint256 deadline 65 | ) external; 66 | 67 | function swapReserveExactTokensForETH( 68 | address tokenOut, 69 | address commonToken, 70 | address ikPair, 71 | uint256 amountIn, 72 | uint256 minAmountOut, 73 | address to, 74 | uint256 deadline 75 | ) external; 76 | 77 | function swapReserveExactETHForTokens( 78 | address tokenOut, 79 | address commonToken, 80 | address ikPair, 81 | uint256 amountIn, 82 | uint256 minAmountOut, 83 | address to, 84 | uint256 deadline 85 | ) external payable; 86 | 87 | function swapTokensForExactTokens( 88 | address[] memory path, 89 | uint256 amountOut, 90 | uint256 maxAmountIn, 91 | address to, 92 | uint256 deadline 93 | ) external; 94 | 95 | function swapExactTokensForTokens( 96 | address[] memory path, 97 | uint256 amountIn, 98 | uint256 minAmountOut, 99 | address to, 100 | uint256 deadline 101 | ) external; 102 | 103 | function swapReserveTokensForExactTokens( 104 | address tokenOut, 105 | address commonToken, 106 | address ikPair, 107 | uint256 amountOut, 108 | uint256 maxAmountIn, 109 | address to, 110 | uint256 deadline 111 | ) external; 112 | 113 | function swapReserveExactTokensForTokens( 114 | address tokenOut, 115 | address commonToken, 116 | address ikPair, 117 | uint256 amountIn, 118 | uint256 minAmountOut, 119 | address to, 120 | uint256 deadline 121 | ) external; 122 | 123 | function addLiquidity( 124 | address tokenA, 125 | address tokenB, 126 | uint256 amountADesired, 127 | uint256 amountBDesired, 128 | uint256 amountAMin, 129 | uint256 amountBMin, 130 | address to, 131 | uint256 deadline 132 | ) 133 | external 134 | returns ( 135 | uint256 amountA, 136 | uint256 amountB, 137 | address pairAddress, 138 | uint256 liquidity 139 | ); 140 | 141 | function removeLiquidity( 142 | address tokenA, 143 | address tokenB, 144 | uint256 liquidity, 145 | uint256 amountAMin, 146 | uint256 amountBMin, 147 | address to, 148 | uint256 deadline 149 | ) external returns (uint256 amountA, uint256 amountB); 150 | 151 | function getAmountOut( 152 | address tokenA, 153 | address tokenB, 154 | uint256 amountIn 155 | ) external view returns (uint256 amountOut); 156 | 157 | function getAmountIn( 158 | address tokenA, 159 | address tokenB, 160 | uint256 amountOut 161 | ) external view returns (uint256 amountIn); 162 | 163 | function quote( 164 | address inputToken, 165 | address outputToken, 166 | uint256 amountIn 167 | ) external view returns (uint256 amountOut); 168 | 169 | function getVirtualAmountIn( 170 | address jkPair, 171 | address ikPair, 172 | uint256 amountOut 173 | ) external view returns (uint256 amountIn); 174 | 175 | function getVirtualAmountOut( 176 | address jkPair, 177 | address ikPair, 178 | uint256 amountIn 179 | ) external view returns (uint256 amountOut); 180 | 181 | function getVirtualPool( 182 | address jkPair, 183 | address ikPair 184 | ) external view returns (VirtualPoolModel memory vPool); 185 | 186 | function getVirtualPools( 187 | address token0, 188 | address token1 189 | ) external view returns (VirtualPoolModel[] memory vPools); 190 | 191 | function getMaxVirtualTradeAmountRtoN( 192 | address jkPair, 193 | address ikPair 194 | ) external view returns (uint256 maxAmountIn); 195 | } 196 | -------------------------------------------------------------------------------- /contracts/interfaces/IvSwapPoolDeployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | /// @title An interface for a contract that is capable of deploying Uniswap V3 Pools 6 | /// @notice A contract that constructs a pool must implement this to pass arguments to the pool 7 | /// @dev This is used to avoid having constructor arguments in the pool contract, which results in the init code hash 8 | /// of the pool being constant allowing the CREATE2 address of the pool to be cheaply computed on-chain 9 | interface IvSwapPoolDeployer { 10 | /// @notice Get the parameters to be used in constructing the pool, set transiently during pool creation. 11 | /// @dev Called by the pool constructor to fetch the parameters of the pool 12 | /// Returns factory The factory address 13 | /// Returns token0 The first token of the pool by address sort order 14 | /// Returns token1 The second token of the pool by address sort order 15 | /// Returns fee The fee collected upon every swap in the pool, denominated in hundredths of a bip 16 | /// Returns tickSpacing The minimum number of ticks between initialized ticks 17 | function poolCreationDefaults() 18 | external 19 | view 20 | returns ( 21 | address factory, 22 | address token0, 23 | address token1, 24 | uint16 fee, 25 | uint16 vFee, 26 | uint256 maxReserveRatio 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IWETH9.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.18; 2 | 3 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 4 | 5 | /// @title Interface for WETH9 6 | interface IWETH9 is IERC20 { 7 | /// @notice Deposit ether to get wrapped ether 8 | function deposit() external payable; 9 | 10 | /// @notice Withdraw wrapped ether to get ether 11 | function withdraw(uint256) external; 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/external/WETH9.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.18; 2 | 3 | contract WETH9 { 4 | string public name = 'Wrapped Ether'; 5 | string public symbol = 'WETH'; 6 | uint8 public decimals = 18; 7 | 8 | event Approval(address indexed src, address indexed guy, uint256 wad); 9 | event Transfer(address indexed src, address indexed dst, uint256 wad); 10 | event Deposit(address indexed dst, uint256 wad); 11 | event Withdrawal(address indexed src, uint256 wad); 12 | 13 | mapping(address => uint256) public balanceOf; 14 | mapping(address => mapping(address => uint256)) public allowance; 15 | 16 | receive() external payable { 17 | deposit(); 18 | } 19 | 20 | function deposit() public payable { 21 | balanceOf[msg.sender] += msg.value; 22 | emit Deposit(msg.sender, msg.value); 23 | } 24 | 25 | function withdraw(uint256 wad) public { 26 | require(balanceOf[msg.sender] >= wad); 27 | balanceOf[msg.sender] -= wad; 28 | payable(msg.sender).transfer(wad); 29 | emit Withdrawal(msg.sender, wad); 30 | } 31 | 32 | function totalSupply() public view returns (uint256) { 33 | return address(this).balance; 34 | } 35 | 36 | function approve(address guy, uint256 wad) public returns (bool) { 37 | allowance[msg.sender][guy] = wad; 38 | emit Approval(msg.sender, guy, wad); 39 | return true; 40 | } 41 | 42 | function transfer(address dst, uint256 wad) public returns (bool) { 43 | return transferFrom(msg.sender, dst, wad); 44 | } 45 | 46 | function transferFrom( 47 | address src, 48 | address dst, 49 | uint256 wad 50 | ) public returns (bool) { 51 | require(balanceOf[src] >= wad); 52 | 53 | if (src != msg.sender && allowance[src][msg.sender] != 0) { 54 | require(allowance[src][msg.sender] >= wad); 55 | allowance[src][msg.sender] -= wad; 56 | } 57 | 58 | balanceOf[src] -= wad; 59 | balanceOf[dst] += wad; 60 | 61 | emit Transfer(src, dst, wad); 62 | 63 | return true; 64 | } 65 | 66 | // used for overflow testing 67 | function testSetBalance(address account, uint256 amount) external { 68 | balanceOf[account] = amount; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /contracts/libraries/PoolAddress.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | /// @title Provides functions for deriving a pool address from the factory and token 6 | library PoolAddress { 7 | bytes32 internal constant POOL_INIT_CODE_HASH = 8 | 0x637bc1e6555f050fef1c3804f2f03647a960ac0a39ac52c519c3c6d9da312ae0; 9 | 10 | function orderAddresses( 11 | address tokenA, 12 | address tokenB 13 | ) internal pure returns (address token0, address token1) { 14 | return (tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA)); 15 | } 16 | 17 | function getSalt( 18 | address tokenA, 19 | address tokenB 20 | ) internal pure returns (bytes32 salt) { 21 | (address token0, address token1) = orderAddresses(tokenA, tokenB); 22 | salt = keccak256(abi.encode(token0, token1)); 23 | } 24 | 25 | function computeAddress( 26 | address factory, 27 | address token0, 28 | address token1 29 | ) internal pure returns (address pool) { 30 | bytes32 _salt = getSalt(token0, token1); 31 | 32 | pool = address( 33 | uint160( 34 | uint256( 35 | keccak256( 36 | abi.encodePacked( 37 | bytes1(0xff), 38 | factory, 39 | _salt, 40 | POOL_INIT_CODE_HASH 41 | ) 42 | ) 43 | ) 44 | ) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/libraries/vSwapLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import '@openzeppelin/contracts/utils/math/Math.sol'; 6 | import '@openzeppelin/contracts/utils/math/SafeCast.sol'; 7 | import '../types.sol'; 8 | import '../interfaces/IvPair.sol'; 9 | 10 | library vSwapLibrary { 11 | uint24 internal constant PRICE_FEE_FACTOR = 10 ** 3; 12 | 13 | //find common token and assign to ikToken1 and jkToken1 14 | function findCommonToken( 15 | address ikToken0, 16 | address ikToken1, 17 | address jkToken0, 18 | address jkToken1 19 | ) internal pure returns (VirtualPoolTokens memory vPoolTokens) { 20 | ( 21 | vPoolTokens.ik0, 22 | vPoolTokens.ik1, 23 | vPoolTokens.jk0, 24 | vPoolTokens.jk1 25 | ) = (ikToken0 == jkToken0) 26 | ? (ikToken1, ikToken0, jkToken1, jkToken0) 27 | : (ikToken0 == jkToken1) 28 | ? (ikToken1, ikToken0, jkToken0, jkToken1) 29 | : (ikToken1 == jkToken0) 30 | ? (ikToken0, ikToken1, jkToken1, jkToken0) 31 | : (ikToken0, ikToken1, jkToken0, jkToken1); //default 32 | } 33 | 34 | function calculateVPool( 35 | uint256 ikTokenABalance, 36 | uint256 ikTokenBBalance, 37 | uint256 jkTokenABalance, 38 | uint256 jkTokenBBalance 39 | ) internal pure returns (VirtualPoolModel memory vPool) { 40 | vPool.balance0 = 41 | (ikTokenABalance * Math.min(ikTokenBBalance, jkTokenBBalance)) / 42 | Math.max(ikTokenBBalance, 1); 43 | 44 | vPool.balance1 = 45 | (jkTokenABalance * Math.min(ikTokenBBalance, jkTokenBBalance)) / 46 | Math.max(jkTokenBBalance, 1); 47 | } 48 | 49 | function getAmountIn( 50 | uint256 amountOut, 51 | uint256 pairBalanceIn, 52 | uint256 pairBalanceOut, 53 | uint256 fee 54 | ) internal pure returns (uint256 amountIn) { 55 | uint256 numerator = (pairBalanceIn * amountOut) * PRICE_FEE_FACTOR; 56 | uint256 denominator = (pairBalanceOut - amountOut) * fee; 57 | amountIn = (numerator / denominator) + 1; 58 | } 59 | 60 | function getAmountOut( 61 | uint256 amountIn, 62 | uint256 pairBalanceIn, 63 | uint256 pairBalanceOut, 64 | uint256 fee 65 | ) internal pure returns (uint256 amountOut) { 66 | uint256 amountInWithFee = amountIn * fee; 67 | uint256 numerator = amountInWithFee * pairBalanceOut; 68 | uint256 denominator = (pairBalanceIn * PRICE_FEE_FACTOR) + 69 | amountInWithFee; 70 | amountOut = numerator / denominator; 71 | } 72 | 73 | function quote( 74 | uint256 amountA, 75 | uint256 balanceA, 76 | uint256 balanceB 77 | ) internal pure returns (uint256 amountB) { 78 | require(amountA > 0, 'VSWAP: INSUFFICIENT_AMOUNT'); 79 | require(balanceA > 0 && balanceB > 0, 'VSWAP: INSUFFICIENT_LIQUIDITY'); 80 | amountB = (amountA * balanceB) / balanceA; 81 | } 82 | 83 | function sortBalances( 84 | address tokenIn, 85 | address baseToken, 86 | uint256 pairBalance0, 87 | uint256 pairBalance1 88 | ) internal pure returns (uint256 _balance0, uint256 _balance1) { 89 | (_balance0, _balance1) = baseToken == tokenIn 90 | ? (pairBalance0, pairBalance1) 91 | : (pairBalance1, pairBalance0); 92 | } 93 | 94 | function getVirtualPool( 95 | address jkPair, 96 | address ikPair 97 | ) internal view returns (VirtualPoolModel memory vPool) { 98 | require( 99 | block.number >= 100 | IvPair(ikPair).lastSwapBlock() + IvPair(ikPair).blocksDelay(), 101 | 'VSWAP: LOCKED_VPOOL' 102 | ); 103 | 104 | (address jk0, address jk1) = IvPair(jkPair).getTokens(); 105 | (address ik0, address ik1) = IvPair(ikPair).getTokens(); 106 | 107 | VirtualPoolTokens memory vPoolTokens = findCommonToken( 108 | ik0, 109 | ik1, 110 | jk0, 111 | jk1 112 | ); 113 | 114 | require( 115 | (vPoolTokens.ik0 != vPoolTokens.jk0) && 116 | (vPoolTokens.ik1 == vPoolTokens.jk1), 117 | 'VSWAP: INVALID_VPOOL' 118 | ); 119 | 120 | (uint256 ikBalance0, uint256 ikBalance1) = IvPair(ikPair).getBalances(); 121 | 122 | (uint256 jkBalance0, uint256 jkBalance1) = IvPair(jkPair).getBalances(); 123 | 124 | vPool = calculateVPool( 125 | vPoolTokens.ik0 == ik0 ? ikBalance0 : ikBalance1, 126 | vPoolTokens.ik0 == ik0 ? ikBalance1 : ikBalance0, 127 | vPoolTokens.jk0 == jk0 ? jkBalance0 : jkBalance1, 128 | vPoolTokens.jk0 == jk0 ? jkBalance1 : jkBalance0 129 | ); 130 | 131 | vPool.token0 = vPoolTokens.ik0; 132 | vPool.token1 = vPoolTokens.jk0; 133 | vPool.commonToken = vPoolTokens.ik1; 134 | 135 | require( 136 | IvPair(jkPair).allowListMap(vPool.token0), 137 | 'VSWAP: NOT_ALLOWED' 138 | ); 139 | 140 | vPool.fee = IvPair(jkPair).vFee(); 141 | 142 | vPool.jkPair = jkPair; 143 | vPool.ikPair = ikPair; 144 | } 145 | 146 | /** @dev The function is used to calculate maximum virtual trade amount for 147 | * swapReserveToNative. The maximum amount that can be traded is such that 148 | * after the swap reserveRatio will be equal to maxReserveRatio: 149 | * 150 | * (reserveBaseValueSum + newReserveBaseValue(vPool.token0)) * reserveRatioFactor / (2 * balance0) = maxReserveRatio, 151 | * where balance0 is the balance of token0 after the swap (i.e. oldBalance0 + amountOut), 152 | * reserveBaseValueSum is SUM(reserveBaseValue[i]) without reserveBaseValue(vPool.token0) 153 | * newReserveBaseValue(vPool.token0) is reserveBaseValue(vPool.token0) after the swap 154 | * 155 | * amountOut can be expressed through amountIn: 156 | * amountOut = (amountIn * fee * vBalance1) / (amountIn * fee + vBalance0 * priceFeeFactor) 157 | * 158 | * reserveBaseValue(vPool.token0) can be expessed as: 159 | * if vPool.token1 == token0: 160 | * reserveBaseValue(vPool.token0) = reserves[vPool.token0] * vBalance1 / vBalance0 161 | * else: 162 | * reserveBaseValue(vPool.token0) = (reserves[vPool.token0] * vBalance1 * balance0) / (vBalance0 * balance1) 163 | * 164 | * Given all that we have two equations for finding maxAmountIn: 165 | * if vPool.token1 == token0: 166 | * Ax^2 + Bx + C = 0, 167 | * where A = fee * reserveRatioFactor * vBalance1, 168 | * B = vBalance0 * (-2 * balance0 * fee * maxReserveRatio + vBalance1 * 169 | * (2 * fee * maxReserveRatio + priceFeeFactor * reserveRatioFactor) + 170 | * fee * reserveRatioFactor * reservesBaseValueSum) + 171 | * fee * reserves * reserveRatioFactor * vBalance1, 172 | * C = -priceFeeFactor * balance0 * (2 * balance0 * maxReserveRatio * vBalance0 - 173 | * reserveRatioFactor * (reserves * vBalance1 + reservesBaseValueSum * vBalance0)); 174 | * if vPool.token1 == token1: 175 | * x = balance1 * vBalance0 * (2 * balance0 * maxReserveRatio - reserveRatioFactor * reservesBaseValueSum) / 176 | * (balance0 * reserveRatioFactor * vBalance1) 177 | * 178 | * In the first case, we solve quadratic equation using Newton method. 179 | */ 180 | function getMaxVirtualTradeAmountRtoN( 181 | VirtualPoolModel memory vPool 182 | ) internal view returns (uint256) { 183 | // The function works if and only if the following constraints are 184 | // satisfied: 185 | // 1. all balances are positive and less than or equal to 10^32 186 | // 2. reserves are non-negative and less than or equal to 10^32 187 | // 3. 0 < vBalance1 <= balance0 (or balance1 depending on trade) 188 | // 4. priceFeeFactor == 10^3 189 | // 5. reserveRatioFactor == 10^5 190 | // 6. 0 < fee <= priceFeeFactor 191 | // 7. 0 < maxReserveRatio <= reserveRatioFactor 192 | // 8. reserveBaseValueSum <= 2 * balance0 * maxReserveRatio (see 193 | // reserve ratio formula in vPair.calculateReserveRatio()) 194 | MaxTradeAmountParams memory params; 195 | 196 | params.fee = uint256(vPool.fee); 197 | params.balance0 = IvPair(vPool.jkPair).pairBalance0(); 198 | params.balance1 = IvPair(vPool.jkPair).pairBalance1(); 199 | params.vBalance0 = vPool.balance0; 200 | params.vBalance1 = vPool.balance1; 201 | params.reserveRatioFactor = IvPair(vPool.jkPair).reserveRatioFactor(); 202 | params.priceFeeFactor = uint256(PRICE_FEE_FACTOR); 203 | params.maxReserveRatio = IvPair(vPool.jkPair).maxReserveRatio(); 204 | params.reserves = IvPair(vPool.jkPair).reserves(vPool.token0); 205 | params.reservesBaseValueSum = 206 | IvPair(vPool.jkPair).reservesBaseValueSum() - 207 | IvPair(vPool.jkPair).reservesBaseValue(vPool.token0); 208 | 209 | require( 210 | params.balance0 > 0 && params.balance0 <= 10 ** 32, 211 | 'invalid balance0' 212 | ); 213 | require( 214 | params.balance1 > 0 && params.balance1 <= 10 ** 32, 215 | 'invalid balance1' 216 | ); 217 | require( 218 | params.vBalance0 > 0 && params.vBalance0 <= 10 ** 32, 219 | 'invalid vBalance0' 220 | ); 221 | require( 222 | params.vBalance1 > 0 && params.vBalance1 <= 10 ** 32, 223 | 'invalid vBalance1' 224 | ); 225 | require(params.priceFeeFactor == 10 ** 3, 'invalid priceFeeFactor'); 226 | require( 227 | params.reserveRatioFactor == 10 ** 5, 228 | 'invalid reserveRatioFactor' 229 | ); 230 | require( 231 | params.fee > 0 && params.fee <= params.priceFeeFactor, 232 | 'invalid fee' 233 | ); 234 | require( 235 | params.maxReserveRatio > 0 && 236 | params.maxReserveRatio <= params.reserveRatioFactor, 237 | 'invalid maxReserveRatio' 238 | ); 239 | 240 | // reserves are full, the answer is 0 241 | if ( 242 | params.reservesBaseValueSum > 243 | 2 * params.balance0 * params.maxReserveRatio 244 | ) return 0; 245 | 246 | int256 maxAmountIn; 247 | if (IvPair(vPool.jkPair).token0() == vPool.token1) { 248 | require(params.vBalance1 <= params.balance0, 'invalid vBalance1'); 249 | unchecked { 250 | // a = R * v1 <= 10^5 * v1 = 10^5 * v1 <= 10^37 251 | uint256 a = params.vBalance1 * params.reserveRatioFactor; 252 | // b = v0 * (-2 * b0 * M + v1 * (2 * M + R * F / f) + R * s) + r * R * v1 <= 253 | // <= v0 * (-2 * b0 * M + b0 * (2 * M + 10^8) + 10^5 * s) + 10^5 * r * v1 = 254 | // = v0 * (10^8 * b0 + 10^5 * s) + 10^5 * r * v1 = 255 | // = 10^5 * (v0 * (10^3 * b0 + s) + r * v1) <= 256 | // <= 10^5 * (v0 * (10^3 * b0 + 2 * b0 * M) + r * v1) <= 257 | // <= 10^5 * (v0 * (10^3 * b0 + 2 * 10^5 * b0) + r * v1) = 258 | // = 10^5 * (v0 * b0 * (2 * 10^5 + 10^3) + r * v1) <= 259 | // <= 10^5 * (10^64 * 2 * 10^5 + 10^64) <= 2 * 10^74 260 | int256 b = int256(params.vBalance0) * 261 | (-2 * 262 | int256(params.balance0 * params.maxReserveRatio) + 263 | int256( 264 | params.vBalance1 * 265 | (2 * 266 | params.maxReserveRatio + 267 | (params.priceFeeFactor * 268 | params.reserveRatioFactor) / 269 | params.fee) + 270 | params.reserveRatioFactor * 271 | params.reservesBaseValueSum 272 | )) + 273 | int256( 274 | params.reserves * 275 | params.reserveRatioFactor * 276 | params.vBalance1 277 | ); 278 | // we split C into c1 * c2 to fit in uint256 279 | // c1 = F * v0 / f <= 10^3 * v0 <= 10^35 280 | uint256 c1 = (params.priceFeeFactor * params.vBalance0) / 281 | params.fee; 282 | // c2 = 2 * b0 * M * v0 - R * (r * v1 + s * v0) <= 283 | // <= [r and s can be zero] <= 284 | // <= 2 * 10^5 * b0 * v0 - 0 <= 2 * 10^69 285 | // 286 | // -c2 = R * (r * v1 + s * v0) - 2 * b0 * M * v0 <= 287 | // <= 10^5 * (r * v1 + 2 * b0 * M * v0) - 2 * b0 * M * v0 = 288 | // = 10^5 * r * v1 + 2 * b0 * M * v0 * (10^5 - 1) <= 289 | // <= 10^5 * 10^32 * 10^32 + 2 * 10^32 * 10^5 * 10^32 * 10^5 <= 290 | // <= 10^69 + 2 * 10^74 <= 2 * 10^74 291 | // 292 | // |c2| <= 2 * 10^74 293 | int256 c2 = 2 * 294 | int256( 295 | params.balance0 * 296 | params.maxReserveRatio * 297 | params.vBalance0 298 | ) - 299 | int256( 300 | params.reserveRatioFactor * 301 | (params.reserves * 302 | params.vBalance1 + 303 | params.reservesBaseValueSum * 304 | params.vBalance0) 305 | ); 306 | 307 | (bool negativeC, uint256 uc2) = ( 308 | c2 < 0 ? (false, uint256(-c2)) : (true, uint256(c2)) 309 | ); 310 | 311 | // according to Newton's method: 312 | // x_{n+1} = x_n - f(x_n) / f'(x_n) = 313 | // = x_n - (Ax_n^2 + Bx_n + c1 * c2) / (2Ax_n + B) = 314 | // = (2Ax_n^2 + Bx_n - Ax_n^2 - Bx_n - c1 * c2) / (2Ax_n + B) = 315 | // = (Ax_n^2 - c1 * c2) / (2Ax_n + B) = 316 | // = Ax_n^2 / (2Ax_n + B) - c1 * c2 / (2Ax_n + B) 317 | // initial approximation: maxAmountIn always <= vb0 318 | maxAmountIn = int256(params.vBalance0); 319 | // derivative = 2 * a * x + b = 320 | // = 2 * R * f * v1 * x + v0 * (-2 * b0 * f * M + v1 * (2 * f * M + R * F) + f * R * s) + f * r * R * v1 <= 321 | // <= 2 * 10^40 * 10^32 + 2 * 10^76 <= 2 * 10^76 322 | int256 derivative = int256(2 * a) * maxAmountIn + b; 323 | 324 | (bool negativeDerivative, uint256 uDerivative) = ( 325 | derivative < 0 326 | ? (true, uint256(-derivative)) 327 | : (false, uint256(derivative)) 328 | ); 329 | 330 | // maxAmountIn * maxAmountIn <= vb0 * vb0 <= 10^64 331 | maxAmountIn = ( 332 | negativeC 333 | ? SafeCast.toInt256( 334 | Math.mulDiv( 335 | a, 336 | uint256(maxAmountIn * maxAmountIn), 337 | uDerivative 338 | ) 339 | ) + SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 340 | : SafeCast.toInt256( 341 | Math.mulDiv( 342 | a, 343 | uint256(maxAmountIn * maxAmountIn), 344 | uDerivative 345 | ) 346 | ) - SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 347 | ); 348 | 349 | if (negativeDerivative) maxAmountIn = -maxAmountIn; 350 | 351 | derivative = int256(2 * a) * maxAmountIn + b; 352 | 353 | (negativeDerivative, uDerivative) = ( 354 | derivative < 0 355 | ? (true, uint256(-derivative)) 356 | : (false, uint256(derivative)) 357 | ); 358 | 359 | maxAmountIn = ( 360 | negativeC 361 | ? SafeCast.toInt256( 362 | Math.mulDiv( 363 | a, 364 | uint256(maxAmountIn * maxAmountIn), 365 | uDerivative 366 | ) 367 | ) + SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 368 | : SafeCast.toInt256( 369 | Math.mulDiv( 370 | a, 371 | uint256(maxAmountIn * maxAmountIn), 372 | uDerivative 373 | ) 374 | ) - SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 375 | ); 376 | 377 | if (negativeDerivative) maxAmountIn = -maxAmountIn; 378 | 379 | derivative = int256(2 * a) * maxAmountIn + b; 380 | 381 | (negativeDerivative, uDerivative) = ( 382 | derivative < 0 383 | ? (true, uint256(-derivative)) 384 | : (false, uint256(derivative)) 385 | ); 386 | 387 | maxAmountIn = ( 388 | negativeC 389 | ? SafeCast.toInt256( 390 | Math.mulDiv( 391 | a, 392 | uint256(maxAmountIn * maxAmountIn), 393 | uDerivative 394 | ) 395 | ) + SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 396 | : SafeCast.toInt256( 397 | Math.mulDiv( 398 | a, 399 | uint256(maxAmountIn * maxAmountIn), 400 | uDerivative 401 | ) 402 | ) - SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 403 | ); 404 | 405 | if (negativeDerivative) maxAmountIn = -maxAmountIn; 406 | 407 | derivative = int256(2 * a) * maxAmountIn + b; 408 | 409 | (negativeDerivative, uDerivative) = ( 410 | derivative < 0 411 | ? (true, uint256(-derivative)) 412 | : (false, uint256(derivative)) 413 | ); 414 | 415 | maxAmountIn = ( 416 | negativeC 417 | ? SafeCast.toInt256( 418 | Math.mulDiv( 419 | a, 420 | uint256(maxAmountIn * maxAmountIn), 421 | uDerivative 422 | ) 423 | ) + SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 424 | : SafeCast.toInt256( 425 | Math.mulDiv( 426 | a, 427 | uint256(maxAmountIn * maxAmountIn), 428 | uDerivative 429 | ) 430 | ) - SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 431 | ); 432 | 433 | if (negativeDerivative) maxAmountIn = -maxAmountIn; 434 | 435 | derivative = int256(2 * a) * maxAmountIn + b; 436 | 437 | (negativeDerivative, uDerivative) = ( 438 | derivative < 0 439 | ? (true, uint256(-derivative)) 440 | : (false, uint256(derivative)) 441 | ); 442 | 443 | maxAmountIn = ( 444 | negativeC 445 | ? SafeCast.toInt256( 446 | Math.mulDiv( 447 | a, 448 | uint256(maxAmountIn * maxAmountIn), 449 | uDerivative 450 | ) 451 | ) + SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 452 | : SafeCast.toInt256( 453 | Math.mulDiv( 454 | a, 455 | uint256(maxAmountIn * maxAmountIn), 456 | uDerivative 457 | ) 458 | ) - SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 459 | ); 460 | 461 | if (negativeDerivative) maxAmountIn = -maxAmountIn; 462 | 463 | derivative = int256(2 * a) * maxAmountIn + b; 464 | 465 | (negativeDerivative, uDerivative) = ( 466 | derivative < 0 467 | ? (true, uint256(-derivative)) 468 | : (false, uint256(derivative)) 469 | ); 470 | 471 | maxAmountIn = ( 472 | negativeC 473 | ? SafeCast.toInt256( 474 | Math.mulDiv( 475 | a, 476 | uint256(maxAmountIn * maxAmountIn), 477 | uDerivative 478 | ) 479 | ) + SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 480 | : SafeCast.toInt256( 481 | Math.mulDiv( 482 | a, 483 | uint256(maxAmountIn * maxAmountIn), 484 | uDerivative 485 | ) 486 | ) - SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 487 | ); 488 | 489 | if (negativeDerivative) maxAmountIn = -maxAmountIn; 490 | 491 | derivative = int256(2 * a) * maxAmountIn + b; 492 | 493 | (negativeDerivative, uDerivative) = ( 494 | derivative < 0 495 | ? (true, uint256(-derivative)) 496 | : (false, uint256(derivative)) 497 | ); 498 | 499 | maxAmountIn = ( 500 | negativeC 501 | ? SafeCast.toInt256( 502 | Math.mulDiv( 503 | a, 504 | uint256(maxAmountIn * maxAmountIn), 505 | uDerivative 506 | ) 507 | ) + SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 508 | : SafeCast.toInt256( 509 | Math.mulDiv( 510 | a, 511 | uint256(maxAmountIn * maxAmountIn), 512 | uDerivative 513 | ) 514 | ) - SafeCast.toInt256(Math.mulDiv(c1, uc2, uDerivative)) 515 | ); 516 | 517 | if (negativeDerivative) maxAmountIn = -maxAmountIn; 518 | } 519 | } else { 520 | unchecked { 521 | require( 522 | params.vBalance1 <= params.balance1, 523 | 'invalid vBalance1' 524 | ); 525 | maxAmountIn = 526 | SafeCast.toInt256( 527 | Math.mulDiv( 528 | params.balance1 * params.vBalance0, 529 | 2 * 530 | params.balance0 * 531 | params.maxReserveRatio - 532 | params.reserveRatioFactor * 533 | params.reservesBaseValueSum, 534 | params.balance0 * 535 | params.reserveRatioFactor * 536 | params.vBalance1 537 | ) 538 | ) - 539 | SafeCast.toInt256(params.reserves); 540 | } 541 | } 542 | assert(maxAmountIn >= 0); 543 | return uint256(maxAmountIn); 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /contracts/test/ReentrancyExploiter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import '../interfaces/IvFlashSwapCallback.sol'; 6 | import '../interfaces/IvPair.sol'; 7 | import '../types.sol'; 8 | 9 | contract ReentrancyExploiter is IvFlashSwapCallback { 10 | uint256 public constant SOME_AMOUNT = 100; 11 | 12 | struct MyCallbackData { 13 | address caller; 14 | address pool; 15 | bytes4 selector; 16 | } 17 | 18 | function vFlashSwapCallback( 19 | address tokenIn, 20 | address tokenOut, 21 | uint256 requiredBackAmount, 22 | bytes calldata data 23 | ) external override { 24 | MyCallbackData memory decodedData = abi.decode(data, (MyCallbackData)); 25 | (bool success, bytes memory result) = decodedData.pool.call( 26 | abi.encodeWithSelector( 27 | decodedData.selector, 28 | SOME_AMOUNT, 29 | tokenOut, 30 | decodedData.caller, 31 | new bytes(0) 32 | ) 33 | ); 34 | if (!success) { 35 | if (result.length == 0) revert(); 36 | assembly { 37 | revert(add(32, result), mload(result)) 38 | } 39 | } 40 | } 41 | 42 | function exploitSwapNative( 43 | address pool, 44 | address tokenOut, 45 | uint256 amountOut, 46 | address to 47 | ) external { 48 | IvPair(pool).swapNative( 49 | amountOut, 50 | tokenOut, 51 | to, 52 | abi.encode( 53 | MyCallbackData({ 54 | caller: msg.sender, 55 | pool: pool, 56 | selector: IvPair.swapNative.selector 57 | }) 58 | ) 59 | ); 60 | } 61 | 62 | function exploitSwapNativeToReserve( 63 | address pool, 64 | address tokenOut, 65 | uint256 amountOut, 66 | address to 67 | ) external { 68 | IvPair(pool).swapNativeToReserve( 69 | amountOut, 70 | tokenOut, 71 | to, 72 | 1, 73 | abi.encode( 74 | MyCallbackData({ 75 | caller: msg.sender, 76 | pool: pool, 77 | selector: IvPair.swapNativeToReserve.selector 78 | }) 79 | ) 80 | ); 81 | } 82 | 83 | function exploitSwapReserveToNative( 84 | address pool, 85 | address ikAddress, 86 | uint256 amountOut, 87 | address to 88 | ) external { 89 | IvPair(pool).swapReserveToNative( 90 | amountOut, 91 | ikAddress, 92 | to, 93 | abi.encode( 94 | MyCallbackData({ 95 | caller: msg.sender, 96 | pool: pool, 97 | selector: IvPair.swapReserveToNative.selector 98 | }) 99 | ) 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /contracts/types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | struct MaxTradeAmountParams { 6 | uint256 fee; 7 | uint256 balance0; 8 | uint256 balance1; 9 | uint256 vBalance0; 10 | uint256 vBalance1; 11 | uint256 reserveRatioFactor; 12 | uint256 priceFeeFactor; 13 | uint256 maxReserveRatio; 14 | uint256 reserves; 15 | uint256 reservesBaseValueSum; 16 | } 17 | 18 | struct VirtualPoolModel { 19 | uint24 fee; 20 | address token0; 21 | address token1; 22 | uint256 balance0; 23 | uint256 balance1; 24 | address commonToken; 25 | address jkPair; 26 | address ikPair; 27 | } 28 | 29 | struct VirtualPoolTokens { 30 | address jk0; 31 | address jk1; 32 | address ik0; 33 | address ik1; 34 | } 35 | 36 | struct ExchangeReserveCallbackParams { 37 | address jkPair1; 38 | address ikPair1; 39 | address jkPair2; 40 | address ikPair2; 41 | address caller; 42 | uint256 flashAmountOut; 43 | } 44 | 45 | struct SwapCallbackData { 46 | address caller; 47 | uint256 tokenInMax; 48 | uint ETHValue; 49 | address jkPool; 50 | } 51 | 52 | struct PoolCreationDefaults { 53 | address factory; 54 | address token0; 55 | address token1; 56 | uint16 fee; 57 | uint16 vFee; 58 | uint256 maxReserveRatio; 59 | } 60 | -------------------------------------------------------------------------------- /contracts/vExchangeReserves.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.18; 3 | 4 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 5 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 6 | import '@openzeppelin/contracts/utils/Multicall.sol'; 7 | 8 | import './types.sol'; 9 | import './libraries/PoolAddress.sol'; 10 | import './interfaces/IvPair.sol'; 11 | import './interfaces/IvExchangeReserves.sol'; 12 | import './interfaces/IvPairFactory.sol'; 13 | 14 | contract vExchangeReserves is IvExchangeReserves, Multicall { 15 | address public immutable factory; 16 | uint256 public incentivesLimitPct; 17 | 18 | constructor(address _factory) { 19 | factory = _factory; 20 | incentivesLimitPct = 1; 21 | } 22 | 23 | function changeIncentivesLimitPct(uint256 newLimit) external override { 24 | require(msg.sender == IvPairFactory(factory).admin(), 'Admin only'); 25 | require(newLimit <= 100, 'Invalid limit'); 26 | incentivesLimitPct = newLimit; 27 | emit NewIncentivesLimit(newLimit); 28 | } 29 | 30 | function vFlashSwapCallback( 31 | address, 32 | address, 33 | uint256 requiredBackAmount, 34 | bytes calldata data 35 | ) external override { 36 | ExchangeReserveCallbackParams memory decodedData = abi.decode( 37 | data, 38 | (ExchangeReserveCallbackParams) 39 | ); 40 | 41 | (address jk0, address jk1) = IvPair(decodedData.jkPair1).getTokens(); 42 | require( 43 | msg.sender == PoolAddress.computeAddress(factory, jk0, jk1), 44 | 'IC' 45 | ); 46 | 47 | (address _leftoverToken, uint256 _leftoverAmount) = IvPair( 48 | decodedData.jkPair2 49 | ).swapNativeToReserve( 50 | requiredBackAmount, 51 | decodedData.ikPair2, 52 | decodedData.jkPair1, 53 | incentivesLimitPct, 54 | new bytes(0) 55 | ); 56 | 57 | if (_leftoverAmount > 0) 58 | SafeERC20.safeTransfer( 59 | IERC20(_leftoverToken), 60 | decodedData.caller, 61 | _leftoverAmount 62 | ); 63 | 64 | emit ReservesExchanged( 65 | decodedData.jkPair1, 66 | decodedData.ikPair1, 67 | decodedData.jkPair2, 68 | decodedData.ikPair2, 69 | requiredBackAmount, 70 | decodedData.flashAmountOut, 71 | _leftoverToken, 72 | _leftoverAmount 73 | ); 74 | } 75 | 76 | function exchange( 77 | address jkPair1, 78 | address ikPair1, 79 | address jkPair2, 80 | address ikPair2, 81 | uint256 flashAmountOut 82 | ) external override { 83 | (address _jkToken0, address _jkToken1) = IvPair(jkPair1).getTokens(); 84 | require( 85 | PoolAddress.computeAddress(factory, _jkToken0, _jkToken1) == 86 | jkPair1, 87 | 'IJKP1' 88 | ); 89 | (_jkToken0, _jkToken1) = IvPair(jkPair2).getTokens(); 90 | require( 91 | PoolAddress.computeAddress(factory, _jkToken0, _jkToken1) == 92 | jkPair2, 93 | 'IJKP2' 94 | ); 95 | 96 | IvPair(jkPair1).swapNativeToReserve( 97 | flashAmountOut, 98 | ikPair1, 99 | jkPair2, 100 | incentivesLimitPct, 101 | abi.encode( 102 | ExchangeReserveCallbackParams({ 103 | jkPair1: jkPair1, 104 | ikPair1: ikPair1, 105 | jkPair2: jkPair2, 106 | ikPair2: ikPair2, 107 | flashAmountOut: flashAmountOut, 108 | caller: msg.sender 109 | }) 110 | ) 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /contracts/vPair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; 6 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 7 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 8 | import '@openzeppelin/contracts/utils/math/Math.sol'; 9 | 10 | import './interfaces/IvPair.sol'; 11 | import './interfaces/IvSwapPoolDeployer.sol'; 12 | import './interfaces/IvPairFactory.sol'; 13 | import './interfaces/IvPoolManager.sol'; 14 | import './interfaces/IvFlashSwapCallback.sol'; 15 | import './libraries/vSwapLibrary.sol'; 16 | import './vSwapERC20.sol'; 17 | 18 | contract vPair is IvPair, vSwapERC20, ReentrancyGuard { 19 | uint24 internal constant BASE_FACTOR = 1000; 20 | uint24 internal constant MINIMUM_LIQUIDITY = BASE_FACTOR; 21 | uint24 internal constant RESERVE_RATIO_FACTOR = BASE_FACTOR * 100; 22 | 23 | address public immutable factory; 24 | address public immutable override token0; 25 | address public immutable override token1; 26 | 27 | uint112 public override pairBalance0; 28 | uint112 public override pairBalance1; 29 | uint16 public override fee; 30 | uint16 public override vFee; 31 | 32 | uint128 public override lastSwapBlock; 33 | uint128 public override blocksDelay; 34 | 35 | uint256 public override reservesBaseValueSum; 36 | uint256 public override maxReserveRatio; 37 | uint256 public reserveRatioWarningThreshold; 38 | 39 | address[] public allowList; 40 | mapping(address => bool) public override allowListMap; 41 | bool public closed; 42 | 43 | mapping(address => uint256) public override reservesBaseValue; 44 | mapping(address => uint256) public override reserves; 45 | 46 | function _onlyFactoryAdmin() internal view { 47 | require( 48 | msg.sender == IvPairFactory(factory).admin() || 49 | msg.sender == factory, 50 | 'OA' 51 | ); 52 | } 53 | 54 | modifier onlyFactoryAdmin() { 55 | _onlyFactoryAdmin(); 56 | _; 57 | } 58 | 59 | modifier onlyEmergencyAdmin() { 60 | require(msg.sender == IvPairFactory(factory).emergencyAdmin(), 'OE'); 61 | _; 62 | } 63 | 64 | modifier isOpen() { 65 | require(!closed, 'C'); 66 | _; 67 | } 68 | 69 | /// @dev This function is gas optimized to avoid a redundant extcodesize check in addition to the returndatasize 70 | function fetchBalance(address token) internal view returns (uint256) { 71 | (bool success, bytes memory data) = token.staticcall( 72 | abi.encodeWithSignature('balanceOf(address)', address(this)) 73 | ); 74 | require(success && data.length >= 32, 'FBF'); 75 | return abi.decode(data, (uint256)); 76 | } 77 | 78 | constructor() { 79 | ( 80 | factory, 81 | token0, 82 | token1, 83 | fee, 84 | vFee, 85 | maxReserveRatio 86 | ) = IvSwapPoolDeployer(msg.sender).poolCreationDefaults(); 87 | reserveRatioWarningThreshold = 1900; 88 | blocksDelay = 40; 89 | } 90 | 91 | function _update(uint112 balance0, uint112 balance1) internal { 92 | lastSwapBlock = uint128(block.number); 93 | 94 | (pairBalance0, pairBalance1) = (balance0, balance1); 95 | 96 | emit vSync(balance0, balance1); 97 | } 98 | 99 | function getBalances() 100 | external 101 | view 102 | override 103 | returns (uint112 _balance0, uint112 _balance1) 104 | { 105 | return (pairBalance0, pairBalance1); 106 | } 107 | 108 | function getTokens() 109 | external 110 | view 111 | override 112 | returns (address _token0, address _token1) 113 | { 114 | return (token0, token1); 115 | } 116 | 117 | function swapNative( 118 | uint256 amountOut, 119 | address tokenOut, 120 | address to, 121 | bytes calldata data 122 | ) external override nonReentrant isOpen returns (uint256 _amountIn) { 123 | require(to > address(0) && to != token0 && to != token1, 'IT'); 124 | require(tokenOut == token0 || tokenOut == token1, 'NNT'); 125 | require(amountOut > 0, 'IAO'); 126 | 127 | address _tokenIn = tokenOut == token0 ? token1 : token0; 128 | 129 | (uint256 _balanceIn, uint256 _balanceOut) = vSwapLibrary.sortBalances( 130 | _tokenIn, 131 | token0, 132 | pairBalance0, 133 | pairBalance1 134 | ); 135 | 136 | require(amountOut < _balanceOut, 'AOE'); 137 | 138 | SafeERC20.safeTransfer(IERC20(tokenOut), to, amountOut); 139 | 140 | uint256 requiredAmountIn = vSwapLibrary.getAmountIn( 141 | amountOut, 142 | _balanceIn, 143 | _balanceOut, 144 | fee 145 | ); 146 | 147 | if (data.length > 0) { 148 | IvFlashSwapCallback(msg.sender).vFlashSwapCallback( 149 | _tokenIn, 150 | tokenOut, 151 | requiredAmountIn, 152 | data 153 | ); 154 | } 155 | 156 | _amountIn = fetchBalance(_tokenIn) - _balanceIn; 157 | 158 | require(_amountIn > 0 && _amountIn >= requiredAmountIn, 'IIA'); 159 | 160 | { 161 | //avoid stack too deep 162 | bool _isTokenIn0 = _tokenIn == token0; 163 | 164 | _update( 165 | uint112( 166 | _isTokenIn0 167 | ? _balanceIn + _amountIn 168 | : _balanceOut - amountOut 169 | ), 170 | uint112( 171 | _isTokenIn0 172 | ? _balanceOut - amountOut 173 | : _balanceIn + _amountIn 174 | ) 175 | ); 176 | } 177 | 178 | emit Swap( 179 | msg.sender, 180 | _tokenIn, 181 | tokenOut, 182 | requiredAmountIn, 183 | amountOut, 184 | to 185 | ); 186 | } 187 | 188 | function swapNativeToReserve( 189 | uint256 amountOut, 190 | address ikPair, 191 | address to, 192 | uint256 incentivesLimitPct, 193 | bytes calldata data 194 | ) 195 | external 196 | override 197 | nonReentrant 198 | isOpen 199 | returns (address _leftoverToken, uint256 _leftoverAmount) 200 | { 201 | require(msg.sender == IvPairFactory(factory).exchangeReserves(), 'OA'); 202 | require(to > address(0) && to != token0 && to != token1, 'IT'); 203 | 204 | VirtualPoolModel memory vPool = IvPoolManager( 205 | IvPairFactory(factory).vPoolManager() 206 | ).getVirtualPool(ikPair, address(this)); 207 | 208 | // validate ikPair with factory 209 | require( 210 | IvPairFactory(factory).pairs(vPool.token1, vPool.commonToken) == 211 | ikPair, 212 | 'IIKP' 213 | ); 214 | require( 215 | amountOut <= vPool.balance1 && amountOut <= reserves[vPool.token1], 216 | 'AOE' 217 | ); 218 | require(allowListMap[vPool.token1], 'TNW'); 219 | require(vPool.token0 == token0 || vPool.token0 == token1, 'NNT'); 220 | 221 | SafeERC20.safeTransfer(IERC20(vPool.token1), to, amountOut); 222 | uint256 requiredAmountIn = vSwapLibrary.quote( 223 | amountOut, 224 | vPool.balance1, 225 | vPool.balance0 226 | ); 227 | 228 | if (data.length > 0) 229 | IvFlashSwapCallback(msg.sender).vFlashSwapCallback( 230 | vPool.token0, 231 | vPool.token1, 232 | requiredAmountIn, 233 | data 234 | ); 235 | 236 | { 237 | // scope to avoid stack too deep errors 238 | uint256 balanceDiff = fetchBalance(vPool.token0) - 239 | (vPool.token0 == token0 ? pairBalance0 : pairBalance1); 240 | require(balanceDiff >= requiredAmountIn, 'IBD'); 241 | (_leftoverAmount, _leftoverToken) = ( 242 | Math.min( 243 | balanceDiff - requiredAmountIn, 244 | (balanceDiff * incentivesLimitPct) / 100 245 | ), 246 | vPool.token0 247 | ); 248 | if (_leftoverAmount > 0) { 249 | SafeERC20.safeTransfer( 250 | IERC20(_leftoverToken), 251 | msg.sender, 252 | _leftoverAmount 253 | ); 254 | } 255 | IvPoolManager(IvPairFactory(factory).vPoolManager()) 256 | .updateVirtualPoolBalances( 257 | ikPair, 258 | address(this), 259 | vPool.balance0 + balanceDiff - _leftoverAmount, 260 | vPool.balance1 - amountOut 261 | ); 262 | } 263 | 264 | { 265 | // scope to avoid stack too deep errors 266 | // //update reserve balance in the equivalent of token0 value 267 | uint256 reserveTokenBalance = fetchBalance(vPool.token1); 268 | // //re-calculate price of reserve asset in token0 for the whole pool balance 269 | uint256 _reserveBaseValue = reserveTokenBalance > 0 270 | ? vSwapLibrary.quote( 271 | reserveTokenBalance, 272 | vPool.balance1, 273 | vPool.balance0 274 | ) 275 | : 0; 276 | 277 | if (_reserveBaseValue > 0 && vPool.token0 == token1) { 278 | //if tokenOut is not token0 we should quote it to token0 value 279 | _reserveBaseValue = vSwapLibrary.quote( 280 | _reserveBaseValue, 281 | pairBalance1, 282 | pairBalance0 283 | ); 284 | } 285 | unchecked { 286 | reservesBaseValueSum += _reserveBaseValue; 287 | reservesBaseValueSum -= reservesBaseValue[vPool.token1]; 288 | } 289 | reservesBaseValue[vPool.token1] = _reserveBaseValue; 290 | //update reserve balance 291 | reserves[vPool.token1] = reserveTokenBalance; 292 | } 293 | 294 | _update(uint112(fetchBalance(token0)), uint112(fetchBalance(token1))); 295 | 296 | emit ReserveSync( 297 | vPool.token1, 298 | reserves[vPool.token1], 299 | calculateReserveRatio() 300 | ); 301 | emit SwapReserve( 302 | msg.sender, 303 | vPool.token0, 304 | vPool.token1, 305 | requiredAmountIn, 306 | amountOut, 307 | ikPair, 308 | to 309 | ); 310 | } 311 | 312 | function allowListLength() external view returns (uint) { 313 | return allowList.length; 314 | } 315 | 316 | function liquidateReserve( 317 | address reserveToken, 318 | address nativePool 319 | ) external override nonReentrant { 320 | require( 321 | (msg.sender == IvPairFactory(factory).admin() && 322 | calculateReserveRatio() >= reserveRatioWarningThreshold) || 323 | msg.sender == IvPairFactory(factory).emergencyAdmin(), 324 | 'OA' 325 | ); 326 | require(allowListMap[reserveToken], 'TNW'); 327 | 328 | (address nativeToken0, address nativeToken1) = IvPair(nativePool) 329 | .getTokens(); 330 | (uint256 nativeBalance0, uint256 nativeBalance1) = IvPair(nativePool) 331 | .getBalances(); 332 | if (nativeToken0 != reserveToken) { 333 | (nativeToken0, nativeToken1) = (nativeToken1, nativeToken0); 334 | (nativeBalance0, nativeBalance1) = (nativeBalance1, nativeBalance0); 335 | } 336 | uint256 reserveAmount = reserves[reserveToken]; 337 | 338 | require( 339 | (nativeToken1 == token0 || nativeToken1 == token1) && 340 | IvPairFactory(factory).pairs(reserveToken, nativeToken1) == 341 | nativePool, 342 | 'INP' 343 | ); 344 | 345 | unchecked { 346 | reservesBaseValueSum -= reservesBaseValue[reserveToken]; 347 | } 348 | reservesBaseValue[reserveToken] = 0; 349 | reserves[reserveToken] = 0; 350 | 351 | SafeERC20.safeTransfer(IERC20(reserveToken), nativePool, reserveAmount); 352 | IvPair(nativePool).swapNative( 353 | vSwapLibrary.getAmountOut( 354 | reserveAmount, 355 | nativeBalance0, 356 | nativeBalance1, 357 | IvPair(nativePool).fee() 358 | ), 359 | nativeToken1, 360 | address(this), 361 | new bytes(0) 362 | ); 363 | 364 | _update(uint112(fetchBalance(token0)), uint112(fetchBalance(token1))); 365 | 366 | emit ReserveSync(reserveToken, 0, calculateReserveRatio()); 367 | } 368 | 369 | function swapReserveToNative( 370 | uint256 amountOut, 371 | address ikPair, 372 | address to, 373 | bytes calldata data 374 | ) external override nonReentrant isOpen returns (uint256 amountIn) { 375 | require(amountOut > 0, 'IAO'); 376 | require(to > address(0) && to != token0 && to != token1, 'IT'); 377 | 378 | VirtualPoolModel memory vPool = IvPoolManager( 379 | IvPairFactory(factory).vPoolManager() 380 | ).getVirtualPool(address(this), ikPair); 381 | 382 | // validate ikPair with factory 383 | require( 384 | IvPairFactory(factory).pairs(vPool.token0, vPool.commonToken) == 385 | ikPair, 386 | 'IIKP' 387 | ); 388 | 389 | require(amountOut < vPool.balance1, 'AOE'); 390 | 391 | uint256 requiredAmountIn = vSwapLibrary.getAmountIn( 392 | amountOut, 393 | vPool.balance0, 394 | vPool.balance1, 395 | vFee 396 | ); 397 | 398 | SafeERC20.safeTransfer(IERC20(vPool.token1), to, amountOut); 399 | 400 | if (data.length > 0) 401 | IvFlashSwapCallback(msg.sender).vFlashSwapCallback( 402 | vPool.token0, 403 | vPool.token1, 404 | requiredAmountIn, 405 | data 406 | ); 407 | 408 | uint256 tokenInBalance = fetchBalance(vPool.token0); 409 | amountIn = tokenInBalance - reserves[vPool.token0]; 410 | 411 | require(amountIn >= requiredAmountIn, 'IIA'); 412 | 413 | { 414 | //update reserve balance in the equivalent of token0 value 415 | //re-calculate price of reserve asset in token0 for the whole pool blance 416 | uint256 _reserveBaseValue = vSwapLibrary.quote( 417 | tokenInBalance, 418 | vPool.balance0, 419 | vPool.balance1 420 | ); 421 | 422 | if (vPool.token1 == token1) { 423 | //if tokenOut is not token0 we should quote it to token0 value 424 | _reserveBaseValue = vSwapLibrary.quote( 425 | _reserveBaseValue, 426 | pairBalance1, 427 | pairBalance0 428 | ); 429 | } 430 | 431 | unchecked { 432 | reservesBaseValueSum += _reserveBaseValue; 433 | reservesBaseValueSum -= reservesBaseValue[vPool.token0]; 434 | } 435 | reservesBaseValue[vPool.token0] = _reserveBaseValue; 436 | } 437 | 438 | //update reserve balance 439 | reserves[vPool.token0] = tokenInBalance; 440 | 441 | _update(uint112(fetchBalance(token0)), uint112(fetchBalance(token1))); 442 | 443 | uint256 reserveRatio = calculateReserveRatio(); 444 | require(reserveRatio <= maxReserveRatio, 'TBPT'); // reserve amount goes beyond pool threshold 445 | 446 | IvPoolManager(IvPairFactory(factory).vPoolManager()) 447 | .updateVirtualPoolBalances( 448 | address(this), 449 | ikPair, 450 | vPool.balance0 + amountIn, 451 | vPool.balance1 - amountOut 452 | ); 453 | 454 | emit ReserveSync(vPool.token0, tokenInBalance, reserveRatio); 455 | 456 | emit SwapReserve( 457 | msg.sender, 458 | vPool.token0, 459 | vPool.token1, 460 | requiredAmountIn, 461 | amountOut, 462 | ikPair, 463 | to 464 | ); 465 | } 466 | 467 | function calculateReserveRatio() 468 | public 469 | view 470 | override 471 | returns (uint256 rRatio) 472 | { 473 | uint256 _pairBalance0 = pairBalance0; 474 | rRatio = _pairBalance0 > 0 475 | ? (reservesBaseValueSum * RESERVE_RATIO_FACTOR) / 476 | (_pairBalance0 << 1) 477 | : 0; 478 | } 479 | 480 | function mint( 481 | address to 482 | ) external override nonReentrant isOpen returns (uint256 liquidity) { 483 | (uint256 _pairBalance0, uint256 _pairBalance1) = ( 484 | pairBalance0, 485 | pairBalance1 486 | ); 487 | uint256 currentBalance0 = fetchBalance(token0); 488 | uint256 currentBalance1 = fetchBalance(token1); 489 | uint256 amount0 = currentBalance0 - _pairBalance0; 490 | uint256 amount1 = currentBalance1 - _pairBalance1; 491 | 492 | uint256 totalSupply_ = totalSupply(); 493 | if (totalSupply_ == 0) { 494 | liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY; 495 | _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens 496 | } else { 497 | liquidity = Math.min( 498 | (amount0 * totalSupply_) / _pairBalance0, 499 | (amount1 * totalSupply_) / _pairBalance1 500 | ); 501 | } 502 | 503 | //substract reserve ratio PCT from minted liquidity tokens amount 504 | uint256 reserveRatio = calculateReserveRatio(); 505 | 506 | liquidity = 507 | (liquidity * RESERVE_RATIO_FACTOR) / 508 | (RESERVE_RATIO_FACTOR + reserveRatio); 509 | 510 | require(liquidity > 0, 'ILM'); 511 | 512 | _mint(to, liquidity); 513 | 514 | _update(uint112(currentBalance0), uint112(currentBalance1)); 515 | emit Mint(to, amount0, amount1, liquidity, totalSupply()); 516 | } 517 | 518 | function burn( 519 | address to 520 | ) 521 | external 522 | override 523 | nonReentrant 524 | returns (uint256 amount0, uint256 amount1) 525 | { 526 | address _token0 = token0; // gas savings 527 | address _token1 = token1; // gas savings 528 | uint256 balance0 = fetchBalance(_token0); 529 | uint256 balance1 = fetchBalance(_token1); 530 | uint256 liquidity = fetchBalance(address(this)); 531 | 532 | uint256 totalSupply_ = totalSupply(); 533 | amount0 = (balance0 * liquidity) / totalSupply_; 534 | amount1 = (balance1 * liquidity) / totalSupply_; 535 | 536 | require(amount0 > 0 && amount1 > 0, 'ILB'); 537 | 538 | _burn(address(this), liquidity); 539 | SafeERC20.safeTransfer(IERC20(_token0), to, amount0); 540 | SafeERC20.safeTransfer(IERC20(_token1), to, amount1); 541 | 542 | //distribute reserve tokens and update reserve ratios 543 | uint256 _currentReserveRatio = calculateReserveRatio(); 544 | if (_currentReserveRatio > 0) { 545 | for (uint256 i = 0; i < allowList.length; ++i) { 546 | address _wlI = allowList[i]; 547 | uint256 reserveBalance = reserves[_wlI]; 548 | 549 | if (reserveBalance > 0) { 550 | uint256 reserveAmountOut = (reserveBalance * liquidity) / 551 | totalSupply_; 552 | 553 | SafeERC20.safeTransfer(IERC20(_wlI), to, reserveAmountOut); 554 | 555 | uint256 reserveBaseValuewlI = reservesBaseValue[_wlI]; //gas saving 556 | 557 | reservesBaseValue[_wlI] = 558 | reserveBaseValuewlI - 559 | ((reserveBaseValuewlI * liquidity) / totalSupply_); 560 | 561 | unchecked { 562 | reservesBaseValueSum += reservesBaseValue[_wlI]; 563 | reservesBaseValueSum -= reserveBaseValuewlI; 564 | } 565 | 566 | reserves[_wlI] = reserveBalance - reserveAmountOut; 567 | } 568 | } 569 | } 570 | 571 | balance0 = fetchBalance(_token0); 572 | balance1 = fetchBalance(_token1); 573 | 574 | _update(uint112(balance0), uint112(balance1)); 575 | emit Burn(msg.sender, amount0, amount1, to, totalSupply()); 576 | } 577 | 578 | function setAllowList(address[] memory _allowList) external override { 579 | require( 580 | msg.sender == factory || 581 | msg.sender == IvPairFactory(factory).admin() || 582 | msg.sender == IvPairFactory(factory).emergencyAdmin(), 583 | 'OA' 584 | ); 585 | for (uint i = 1; i < _allowList.length; ++i) { 586 | require( 587 | _allowList[i] > _allowList[i - 1], 588 | 'allow list must be unique and sorted' 589 | ); 590 | } 591 | 592 | address[] memory _oldWL = allowList; 593 | for (uint256 i = 0; i < _oldWL.length; ++i) 594 | allowListMap[_oldWL[i]] = false; 595 | 596 | //set new allowList 597 | allowList = _allowList; 598 | address token0_ = token0; 599 | address token1_ = token1; 600 | uint256 newReservesBaseValueSum; 601 | for (uint256 i = 0; i < _allowList.length; ++i) 602 | if (_allowList[i] != token0_ && _allowList[i] != token1_) { 603 | allowListMap[_allowList[i]] = true; 604 | newReservesBaseValueSum += reservesBaseValue[_allowList[i]]; 605 | } 606 | reservesBaseValueSum = newReservesBaseValueSum; 607 | 608 | emit AllowListChanged(_allowList); 609 | } 610 | 611 | function setFee( 612 | uint16 _fee, 613 | uint16 _vFee 614 | ) external override onlyFactoryAdmin { 615 | require(_fee > 0 && _vFee > 0 && _fee < 1000 && _vFee < 1000, 'IFC'); 616 | fee = _fee; 617 | vFee = _vFee; 618 | 619 | emit FeeChanged(_fee, _vFee); 620 | } 621 | 622 | function setMaxReserveThreshold( 623 | uint256 threshold 624 | ) external override onlyFactoryAdmin { 625 | require(threshold > 0, 'IRT'); 626 | maxReserveRatio = threshold; 627 | emit ReserveThresholdChanged(threshold); 628 | } 629 | 630 | function setReserveRatioWarningThreshold( 631 | uint256 _reserveRatioWarningThreshold 632 | ) external override onlyEmergencyAdmin { 633 | require(_reserveRatioWarningThreshold <= maxReserveRatio, 'IRWT'); 634 | reserveRatioWarningThreshold = _reserveRatioWarningThreshold; 635 | emit ReserveRatioWarningThresholdChanged(_reserveRatioWarningThreshold); 636 | } 637 | 638 | function emergencyToggle() external override onlyEmergencyAdmin { 639 | closed = !closed; 640 | } 641 | 642 | function setBlocksDelay(uint128 _newBlocksDelay) external override { 643 | require( 644 | msg.sender == IvPairFactory(factory).emergencyAdmin() || 645 | msg.sender == IvPairFactory(factory).admin(), 646 | 'OA' 647 | ); 648 | blocksDelay = _newBlocksDelay; 649 | emit BlocksDelayChanged(_newBlocksDelay); 650 | } 651 | 652 | function reserveRatioFactor() external pure override returns (uint256) { 653 | return RESERVE_RATIO_FACTOR; 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /contracts/vPairFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import './vPair.sol'; 6 | import './interfaces/IvPair.sol'; 7 | import './interfaces/IvPairFactory.sol'; 8 | import './interfaces/IvExchangeReserves.sol'; 9 | import './interfaces/IvSwapPoolDeployer.sol'; 10 | import './libraries/PoolAddress.sol'; 11 | import './types.sol'; 12 | 13 | contract vPairFactory is IvPairFactory, IvSwapPoolDeployer { 14 | mapping(address => mapping(address => address)) public override pairs; 15 | address[] public override allPairs; 16 | 17 | address public override admin; 18 | address public override pendingAdmin; 19 | address public override emergencyAdmin; 20 | address public override pendingEmergencyAdmin; 21 | address public override exchangeReserves; 22 | address public override vPoolManager; 23 | 24 | address[] defaultAllowList; 25 | 26 | PoolCreationDefaults public override poolCreationDefaults; 27 | 28 | modifier onlyAdmin() { 29 | require(msg.sender == admin, 'OA'); 30 | _; 31 | } 32 | 33 | modifier onlyEmergencyAdmin() { 34 | require(msg.sender == emergencyAdmin, 'OEA'); 35 | _; 36 | } 37 | 38 | constructor() { 39 | admin = msg.sender; 40 | emergencyAdmin = msg.sender; 41 | } 42 | 43 | function createPair( 44 | address tokenA, 45 | address tokenB 46 | ) external override returns (address pair) { 47 | require(tokenA != tokenB, 'IA'); 48 | 49 | (address token0, address token1) = PoolAddress.orderAddresses( 50 | tokenA, 51 | tokenB 52 | ); 53 | 54 | require(token0 != address(0), 'ZA'); 55 | 56 | require(pairs[token0][token1] == address(0), 'PE'); 57 | 58 | poolCreationDefaults = PoolCreationDefaults({ 59 | factory: address(this), 60 | token0: token0, 61 | token1: token1, 62 | fee: 997, 63 | vFee: 997, 64 | maxReserveRatio: 2000 65 | }); 66 | 67 | bytes32 _salt = PoolAddress.getSalt(token0, token1); 68 | pair = address(new vPair{salt: _salt}()); 69 | 70 | delete poolCreationDefaults; 71 | 72 | IvPair(pair).setAllowList(defaultAllowList); 73 | 74 | pairs[token0][token1] = pair; 75 | pairs[token1][token0] = pair; 76 | allPairs.push(pair); 77 | 78 | emit PairCreated(pair, address(this), token0, token1, 997, 997, 2000); 79 | 80 | return pair; 81 | } 82 | 83 | function setExchangeReservesAddress( 84 | address _exchangeReserves 85 | ) external override onlyAdmin { 86 | require(_exchangeReserves > address(0), 'IERA'); 87 | require( 88 | IvExchangeReserves(_exchangeReserves).factory() == address(this), 89 | 'IER' 90 | ); 91 | exchangeReserves = _exchangeReserves; 92 | 93 | emit ExchangeReserveAddressChanged(_exchangeReserves); 94 | } 95 | 96 | function setVPoolManagerAddress( 97 | address _vPoolManager 98 | ) external override onlyAdmin { 99 | require(_vPoolManager > address(0), 'IVPMA'); 100 | require( 101 | IvPoolManager(_vPoolManager).pairFactory() == address(this), 102 | 'IVPM' 103 | ); 104 | vPoolManager = _vPoolManager; 105 | emit FactoryVPoolManagerChanged(_vPoolManager); 106 | } 107 | 108 | function setPendingAdmin( 109 | address newPendingAdmin 110 | ) external override onlyAdmin { 111 | pendingAdmin = newPendingAdmin; 112 | emit FactoryNewPendingAdmin(newPendingAdmin); 113 | } 114 | 115 | function acceptAdmin() external override { 116 | require(msg.sender == pendingAdmin, 'OPA'); 117 | admin = pendingAdmin; 118 | pendingAdmin = address(0); 119 | emit FactoryNewAdmin(admin); 120 | } 121 | 122 | function setPendingEmergencyAdmin( 123 | address newPendingEmergencyAdmin 124 | ) external override onlyEmergencyAdmin { 125 | pendingEmergencyAdmin = newPendingEmergencyAdmin; 126 | emit FactoryNewPendingEmergencyAdmin(newPendingEmergencyAdmin); 127 | } 128 | 129 | function acceptEmergencyAdmin() external override { 130 | require(msg.sender == pendingEmergencyAdmin, 'OPA'); 131 | emergencyAdmin = pendingEmergencyAdmin; 132 | pendingEmergencyAdmin = address(0); 133 | emit FactoryNewEmergencyAdmin(emergencyAdmin); 134 | } 135 | 136 | function setDefaultAllowList( 137 | address[] calldata _defaultAllowList 138 | ) external override onlyAdmin { 139 | require(_defaultAllowList.length <= 2 ** 24 - 1, 'ATL'); 140 | for (uint i = 1; i < _defaultAllowList.length; ++i) { 141 | require(_defaultAllowList[i] > _defaultAllowList[i - 1], 'ALU'); 142 | } 143 | defaultAllowList = _defaultAllowList; 144 | emit DefaultAllowListChanged(_defaultAllowList); 145 | } 146 | 147 | function allPairsLength() external view override returns (uint256) { 148 | return allPairs.length; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /contracts/vPoolManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import './types.sol'; 6 | import './libraries/vSwapLibrary.sol'; 7 | import './libraries/PoolAddress.sol'; 8 | import './interfaces/IvPairFactory.sol'; 9 | import './interfaces/IvPair.sol'; 10 | import './interfaces/IvPoolManager.sol'; 11 | 12 | contract vPoolManager is IvPoolManager { 13 | struct VBalancesWithBlock { 14 | uint112 balance0; 15 | uint112 balance1; 16 | uint32 blockLastUpdated; 17 | } 18 | 19 | mapping(address => mapping(address => VBalancesWithBlock)) vPoolsBalancesCache; 20 | 21 | address public immutable pairFactory; 22 | 23 | constructor(address _pairFactory) { 24 | pairFactory = _pairFactory; 25 | } 26 | 27 | function getVirtualPool( 28 | address jkPair, 29 | address ikPair 30 | ) public view override returns (VirtualPoolModel memory vPool) { 31 | VBalancesWithBlock memory vBalancesWithBlock = vPoolsBalancesCache[ 32 | jkPair 33 | ][ikPair]; 34 | vPool = vSwapLibrary.getVirtualPool(jkPair, ikPair); 35 | if (block.number == vBalancesWithBlock.blockLastUpdated) { 36 | (vPool.balance0, vPool.balance1) = ( 37 | vBalancesWithBlock.balance0, 38 | vBalancesWithBlock.balance1 39 | ); 40 | _reduceBalances(vPool); 41 | } 42 | } 43 | 44 | function getVirtualPools( 45 | address token0, 46 | address token1 47 | ) external view override returns (VirtualPoolModel[] memory vPools) { 48 | uint256 allPairsLength = IvPairFactory(pairFactory).allPairsLength(); 49 | uint256 vPoolsNumber; 50 | address jk0; 51 | address jk1; 52 | address jkPair; 53 | for (uint256 i = 0; i < allPairsLength; ++i) { 54 | jkPair = IvPairFactory(pairFactory).allPairs(i); 55 | (jk0, jk1) = IvPair(jkPair).getTokens(); 56 | if ( 57 | (jk0 == token1 || jk1 == token1) && 58 | jk0 != token0 && 59 | jk1 != token0 && 60 | IvPair(jkPair).allowListMap(token0) && 61 | IvPairFactory(pairFactory).pairs( 62 | token0, 63 | jk0 == token1 ? jk1 : jk0 64 | ) != 65 | address(0) 66 | ) { 67 | ++vPoolsNumber; 68 | } 69 | } 70 | vPools = new VirtualPoolModel[](vPoolsNumber); 71 | address ikPair; 72 | for (uint256 i = 0; i < allPairsLength; ++i) { 73 | jkPair = IvPairFactory(pairFactory).allPairs(i); 74 | (jk0, jk1) = IvPair(jkPair).getTokens(); 75 | if ( 76 | (jk0 == token1 || jk1 == token1) && 77 | jk0 != token0 && 78 | jk1 != token0 && 79 | IvPair(jkPair).allowListMap(token0) 80 | ) { 81 | ikPair = IvPairFactory(pairFactory).pairs( 82 | token0, 83 | jk0 == token1 ? jk1 : jk0 84 | ); 85 | if (ikPair != address(0)) { 86 | vPools[--vPoolsNumber] = getVirtualPool(jkPair, ikPair); 87 | } 88 | } 89 | } 90 | } 91 | 92 | function updateVirtualPoolBalances( 93 | address jkPair, 94 | address ikPair, 95 | uint256 balance0, 96 | uint256 balance1 97 | ) external override { 98 | (address token0, address token1) = IvPair(msg.sender).getTokens(); 99 | require( 100 | msg.sender == 101 | PoolAddress.computeAddress(pairFactory, token0, token1), 102 | 'Only pools' 103 | ); 104 | vPoolsBalancesCache[jkPair][ikPair] = VBalancesWithBlock( 105 | uint112(balance0), 106 | uint112(balance1), 107 | uint32(block.number) 108 | ); 109 | } 110 | 111 | function _reduceBalances(VirtualPoolModel memory vPool) private view { 112 | (uint256 ikBalance0, uint256 ikBalance1) = IvPair(vPool.ikPair) 113 | .getBalances(); 114 | 115 | if (vPool.token0 == IvPair(vPool.ikPair).token1()) 116 | (ikBalance0, ikBalance1) = (ikBalance1, ikBalance0); 117 | 118 | (uint256 jkBalance0, uint256 jkBalance1) = IvPair(vPool.jkPair) 119 | .getBalances(); 120 | 121 | if (vPool.token1 == IvPair(vPool.jkPair).token1()) 122 | (jkBalance0, jkBalance1) = (jkBalance1, jkBalance0); 123 | 124 | // Make sure vPool balances are less or equal than real pool balances 125 | if (vPool.balance0 >= ikBalance0) { 126 | vPool.balance1 = (vPool.balance1 * ikBalance0) / vPool.balance0; 127 | vPool.balance0 = ikBalance0; 128 | } 129 | if (vPool.balance1 >= jkBalance0) { 130 | vPool.balance0 = (vPool.balance0 * jkBalance0) / vPool.balance1; 131 | vPool.balance1 = jkBalance0; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /contracts/vRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 6 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 7 | import '@openzeppelin/contracts/utils/Multicall.sol'; 8 | import '@uniswap/lib/contracts/libraries/TransferHelper.sol'; 9 | 10 | import './types.sol'; 11 | import './vPair.sol'; 12 | import './libraries/PoolAddress.sol'; 13 | import './libraries/vSwapLibrary.sol'; 14 | import './interfaces/IvRouter.sol'; 15 | import './interfaces/IvPairFactory.sol'; 16 | import './interfaces/IvPoolManager.sol'; 17 | import './interfaces/IvPair.sol'; 18 | import './interfaces/external/IWETH9.sol'; 19 | 20 | contract vRouter is IvRouter, Multicall { 21 | address public override factory; 22 | address public immutable override WETH9; 23 | 24 | modifier _onlyFactoryAdmin() { 25 | require( 26 | msg.sender == IvPairFactory(factory).admin(), 27 | 'VSWAP:ONLY_ADMIN' 28 | ); 29 | _; 30 | } 31 | 32 | modifier notAfter(uint256 deadline) { 33 | require(deadline >= block.timestamp, 'VSWAP:EXPIRED'); 34 | _; 35 | } 36 | 37 | constructor(address _factory, address _WETH9) { 38 | WETH9 = _WETH9; 39 | factory = _factory; 40 | } 41 | 42 | receive() external payable { 43 | require(msg.sender == WETH9, 'Not WETH9'); 44 | } 45 | 46 | function getPairAddress( 47 | address tokenA, 48 | address tokenB 49 | ) internal view returns (address) { 50 | return PoolAddress.computeAddress(factory, tokenA, tokenB); 51 | } 52 | 53 | function getPair( 54 | address tokenA, 55 | address tokenB 56 | ) internal view returns (IvPair) { 57 | return IvPair(getPairAddress(tokenA, tokenB)); 58 | } 59 | 60 | function unwrapTransferETH(address to, uint256 amount) internal { 61 | IWETH9(WETH9).withdraw(amount); 62 | (bool success, ) = to.call{value: amount}(''); 63 | require(success, 'VSWAP: TRANSFER FAILED'); 64 | } 65 | 66 | function getAmountsIn( 67 | address[] memory path, 68 | uint256 amountOut 69 | ) public view returns (uint[] memory amountsIn) { 70 | amountsIn = new uint[](path.length); 71 | amountsIn[amountsIn.length - 1] = amountOut; 72 | for (uint i = path.length - 1; i > 0; --i) { 73 | amountsIn[i - 1] = getAmountIn(path[i - 1], path[i], amountsIn[i]); 74 | } 75 | } 76 | 77 | function getAmountsOut( 78 | address[] memory path, 79 | uint256 amountIn 80 | ) public view returns (uint[] memory amountsOut) { 81 | amountsOut = new uint[](path.length); 82 | amountsOut[0] = amountIn; 83 | for (uint i = 1; i < amountsOut.length; ++i) { 84 | amountsOut[i] = getAmountOut( 85 | path[i - 1], 86 | path[i], 87 | amountsOut[i - 1] 88 | ); 89 | } 90 | } 91 | 92 | function swapExactETHForTokens( 93 | address[] memory path, 94 | uint256 amountIn, 95 | uint256 minAmountOut, 96 | address to, 97 | uint256 deadline 98 | ) external payable override notAfter(deadline) { 99 | require(path[0] == WETH9, 'VSWAP: INPUT TOKEN MUST BE WETH9'); 100 | uint[] memory amountsOut = getAmountsOut(path, amountIn); 101 | require( 102 | amountsOut[amountsOut.length - 1] >= minAmountOut, 103 | 'VSWAP: INSUFFICIENT_INPUT_AMOUNT' 104 | ); 105 | transferETHInput(amountsOut[0], getPairAddress(path[0], path[1])); 106 | swap(path, amountsOut, to); 107 | } 108 | 109 | function swapExactTokensForETH( 110 | address[] memory path, 111 | uint256 amountIn, 112 | uint256 minAmountOut, 113 | address to, 114 | uint256 deadline 115 | ) external override notAfter(deadline) { 116 | require( 117 | path[path.length - 1] == WETH9, 118 | 'VSWAP: OUTPUT TOKEN MUST BE WETH9' 119 | ); 120 | uint[] memory amountsOut = getAmountsOut(path, amountIn); 121 | require( 122 | amountsOut[amountsOut.length - 1] >= minAmountOut, 123 | 'VSWAP: INSUFFICIENT_INPUT_AMOUNT' 124 | ); 125 | transferInput(path[0], amountsOut[0], getPairAddress(path[0], path[1])); 126 | swap(path, amountsOut, address(this)); 127 | unwrapTransferETH(to, amountsOut[amountsOut.length - 1]); 128 | } 129 | 130 | function swapETHForExactTokens( 131 | address[] memory path, 132 | uint256 amountOut, 133 | uint256 maxAmountIn, 134 | address to, 135 | uint256 deadline 136 | ) external payable override notAfter(deadline) { 137 | require(path[0] == WETH9, 'VSWAP: INPUT TOKEN MUST BE WETH9'); 138 | uint[] memory amountsIn = getAmountsIn(path, amountOut); 139 | require(amountsIn[0] <= maxAmountIn, 'VSWAP: REQUIRED_AMOUNT_EXCEEDS'); 140 | transferETHInput(amountsIn[0], getPairAddress(path[0], path[1])); 141 | swap(path, amountsIn, to); 142 | } 143 | 144 | function swapTokensForExactETH( 145 | address[] memory path, 146 | uint256 amountOut, 147 | uint256 maxAmountIn, 148 | address to, 149 | uint256 deadline 150 | ) external override notAfter(deadline) { 151 | require( 152 | path[path.length - 1] == WETH9, 153 | 'VSWAP: OUTPUT TOKEN MUST BE WETH9' 154 | ); 155 | uint[] memory amountsIn = getAmountsIn(path, amountOut); 156 | require(amountsIn[0] <= maxAmountIn, 'VSWAP: REQUIRED_AMOUNT_EXCEEDS'); 157 | transferInput(path[0], amountsIn[0], getPairAddress(path[0], path[1])); 158 | swap(path, amountsIn, address(this)); 159 | unwrapTransferETH(to, amountsIn[amountsIn.length - 1]); 160 | } 161 | 162 | function swapReserveETHForExactTokens( 163 | address tokenOut, 164 | address commonToken, 165 | address ikPair, 166 | uint256 amountOut, 167 | uint256 maxAmountIn, 168 | address to, 169 | uint256 deadline 170 | ) external payable override notAfter(deadline) { 171 | (address ik0, address ik1) = IvPair(ikPair).getTokens(); 172 | address tokenIn = ik0 == commonToken ? ik1 : ik0; 173 | require(tokenIn == WETH9, 'VSWAP: INPUT TOKEN MUST BE WETH9'); 174 | address jkAddress = getPairAddress(tokenOut, commonToken); 175 | uint256 amountIn = getVirtualAmountIn(jkAddress, ikPair, amountOut); 176 | require(amountIn <= maxAmountIn, 'VSWAP: REQUIRED_VINPUT_EXCEED'); 177 | transferETHInput(amountIn, jkAddress); 178 | swapReserve(amountOut, jkAddress, ikPair, to); 179 | } 180 | 181 | function swapReserveTokensForExactETH( 182 | address tokenOut, 183 | address commonToken, 184 | address ikPair, 185 | uint256 amountOut, 186 | uint256 maxAmountIn, 187 | address to, 188 | uint256 deadline 189 | ) external override notAfter(deadline) { 190 | require(tokenOut == WETH9, 'VSWAP: OUTPUT TOKEN MUST BE WETH9'); 191 | (address ik0, address ik1) = IvPair(ikPair).getTokens(); 192 | address tokenIn = ik0 == commonToken ? ik1 : ik0; 193 | address jkAddress = getPairAddress(tokenOut, commonToken); 194 | uint256 amountIn = getVirtualAmountIn(jkAddress, ikPair, amountOut); 195 | require(amountIn <= maxAmountIn, 'VSWAP: REQUIRED_VINPUT_EXCEED'); 196 | transferInput(tokenIn, amountIn, jkAddress); 197 | swapReserve(amountOut, jkAddress, ikPair, address(this)); 198 | unwrapTransferETH(to, amountOut); 199 | } 200 | 201 | function swapReserveExactTokensForETH( 202 | address tokenOut, 203 | address commonToken, 204 | address ikPair, 205 | uint256 amountIn, 206 | uint256 minAmountOut, 207 | address to, 208 | uint256 deadline 209 | ) external override notAfter(deadline) { 210 | require(tokenOut == WETH9, 'VSWAP: OUTPUT TOKEN MUST BE WETH9'); 211 | (address ik0, address ik1) = IvPair(ikPair).getTokens(); 212 | address tokenIn = ik0 == commonToken ? ik1 : ik0; 213 | address jkAddress = getPairAddress(tokenOut, commonToken); 214 | uint256 amountOut = getVirtualAmountOut(jkAddress, ikPair, amountIn); 215 | require( 216 | amountOut >= minAmountOut, 217 | 'VSWAP: INSUFFICIENT_VOUTPUT_AMOUNT' 218 | ); 219 | transferInput(tokenIn, amountIn, jkAddress); 220 | swapReserve(amountOut, jkAddress, ikPair, address(this)); 221 | unwrapTransferETH(to, amountOut); 222 | } 223 | 224 | function swapReserveExactETHForTokens( 225 | address tokenOut, 226 | address commonToken, 227 | address ikPair, 228 | uint256 amountIn, 229 | uint256 minAmountOut, 230 | address to, 231 | uint256 deadline 232 | ) external payable override notAfter(deadline) { 233 | (address ik0, address ik1) = IvPair(ikPair).getTokens(); 234 | address tokenIn = ik0 == commonToken ? ik1 : ik0; 235 | require(tokenIn == WETH9, 'VSWAP: INPUT TOKEN MUST BE WETH9'); 236 | address jkAddress = getPairAddress(tokenOut, commonToken); 237 | uint256 amountOut = getVirtualAmountOut(jkAddress, ikPair, amountIn); 238 | require( 239 | amountOut >= minAmountOut, 240 | 'VSWAP: INSUFFICIENT_VOUTPUT_AMOUNT' 241 | ); 242 | transferETHInput(amountIn, jkAddress); 243 | swapReserve(amountOut, jkAddress, ikPair, to); 244 | } 245 | 246 | function swapTokensForExactTokens( 247 | address[] memory path, 248 | uint256 amountOut, 249 | uint256 maxAmountIn, 250 | address to, 251 | uint256 deadline 252 | ) external override notAfter(deadline) { 253 | uint[] memory amountsIn = getAmountsIn(path, amountOut); 254 | require(amountsIn[0] <= maxAmountIn, 'VSWAP: REQUIRED_AMOUNT_EXCEEDS'); 255 | transferInput(path[0], amountsIn[0], getPairAddress(path[0], path[1])); 256 | swap(path, amountsIn, to); 257 | } 258 | 259 | function swapExactTokensForTokens( 260 | address[] memory path, 261 | uint256 amountIn, 262 | uint256 minAmountOut, 263 | address to, 264 | uint256 deadline 265 | ) external override notAfter(deadline) { 266 | uint[] memory amountsOut = getAmountsOut(path, amountIn); 267 | require( 268 | amountsOut[amountsOut.length - 1] >= minAmountOut, 269 | 'VSWAP: INSUFFICIENT_INPUT_AMOUNT' 270 | ); 271 | transferInput(path[0], amountsOut[0], getPairAddress(path[0], path[1])); 272 | swap(path, amountsOut, to); 273 | } 274 | 275 | function swapReserveTokensForExactTokens( 276 | address tokenOut, 277 | address commonToken, 278 | address ikPair, 279 | uint256 amountOut, 280 | uint256 maxAmountIn, 281 | address to, 282 | uint256 deadline 283 | ) external override notAfter(deadline) { 284 | (address ik0, address ik1) = IvPair(ikPair).getTokens(); 285 | address tokenIn = ik0 == commonToken ? ik1 : ik0; 286 | address jkAddress = getPairAddress(tokenOut, commonToken); 287 | uint256 amountIn = getVirtualAmountIn(jkAddress, ikPair, amountOut); 288 | require(amountIn <= maxAmountIn, 'VSWAP: REQUIRED_VINPUT_EXCEED'); 289 | transferInput(tokenIn, amountIn, jkAddress); 290 | swapReserve(amountOut, jkAddress, ikPair, to); 291 | } 292 | 293 | function swapReserveExactTokensForTokens( 294 | address tokenOut, 295 | address commonToken, 296 | address ikPair, 297 | uint256 amountIn, 298 | uint256 minAmountOut, 299 | address to, 300 | uint256 deadline 301 | ) external override notAfter(deadline) { 302 | (address ik0, address ik1) = IvPair(ikPair).getTokens(); 303 | address tokenIn = ik0 == commonToken ? ik1 : ik0; 304 | address jkAddress = getPairAddress(tokenOut, commonToken); 305 | uint256 amountOut = getVirtualAmountOut(jkAddress, ikPair, amountIn); 306 | require( 307 | amountOut >= minAmountOut, 308 | 'VSWAP: INSUFFICIENT_VOUTPUT_AMOUNT' 309 | ); 310 | transferInput(tokenIn, amountIn, jkAddress); 311 | swapReserve(amountOut, jkAddress, ikPair, to); 312 | } 313 | 314 | function transferETHInput(uint amountIn, address pair) internal { 315 | require( 316 | address(this).balance >= amountIn, 317 | 'VSWAP: INSUFFICIENT_ETH_INPUT_AMOUNT' 318 | ); 319 | IWETH9(WETH9).deposit{value: amountIn}(); 320 | SafeERC20.safeTransfer(IERC20(WETH9), pair, amountIn); 321 | (bool success, ) = msg.sender.call{value: address(this).balance}(''); 322 | require(success, 'VSWAP: TRANSFER FAILED'); 323 | } 324 | 325 | function transferInput( 326 | address token, 327 | uint amountIn, 328 | address pair 329 | ) internal { 330 | SafeERC20.safeTransferFrom(IERC20(token), msg.sender, pair, amountIn); 331 | } 332 | 333 | function swap( 334 | address[] memory path, 335 | uint[] memory amounts, 336 | address to 337 | ) internal { 338 | for (uint i = 0; i < path.length - 1; ++i) { 339 | getPair(path[i], path[i + 1]).swapNative( 340 | amounts[i + 1], 341 | path[i + 1], 342 | i == path.length - 2 343 | ? to 344 | : getPairAddress(path[i + 1], path[i + 2]), 345 | new bytes(0) 346 | ); 347 | } 348 | } 349 | 350 | function swapReserve( 351 | uint amountOut, 352 | address jkAddress, 353 | address ikAddress, 354 | address to 355 | ) internal { 356 | IvPair(jkAddress).swapReserveToNative( 357 | amountOut, 358 | ikAddress, 359 | to, 360 | new bytes(0) 361 | ); 362 | } 363 | 364 | function _addLiquidity( 365 | address tokenA, 366 | address tokenB, 367 | uint256 amountADesired, 368 | uint256 amountBDesired, 369 | uint256 amountAMin, 370 | uint256 amountBMin 371 | ) internal returns (uint256 amountA, uint256 amountB, address pairAddress) { 372 | pairAddress = IvPairFactory(factory).pairs(tokenA, tokenB); 373 | // create the pair if it doesn't exist yet 374 | if (pairAddress == address(0)) 375 | pairAddress = IvPairFactory(factory).createPair(tokenA, tokenB); 376 | 377 | (uint256 reserve0, uint256 reserve1) = IvPair(pairAddress) 378 | .getBalances(); 379 | 380 | (reserve0, reserve1) = vSwapLibrary.sortBalances( 381 | IvPair(pairAddress).token0(), 382 | tokenA, 383 | reserve0, 384 | reserve1 385 | ); 386 | 387 | if (reserve0 == 0 && reserve1 == 0) { 388 | (amountA, amountB) = (amountADesired, amountBDesired); 389 | } else { 390 | uint256 amountBOptimal = vSwapLibrary.quote( 391 | amountADesired, 392 | reserve0, 393 | reserve1 394 | ); 395 | 396 | if (amountBOptimal <= amountBDesired) { 397 | require( 398 | amountBOptimal >= amountBMin, 399 | 'VSWAP: INSUFFICIENT_B_AMOUNT' 400 | ); 401 | (amountA, amountB) = (amountADesired, amountBOptimal); 402 | } else { 403 | uint256 amountAOptimal = vSwapLibrary.quote( 404 | amountBDesired, 405 | reserve1, 406 | reserve0 407 | ); 408 | 409 | assert(amountAOptimal <= amountADesired); 410 | require( 411 | amountAOptimal >= amountAMin, 412 | 'VSWAP: INSUFFICIENT_A_AMOUNT' 413 | ); 414 | (amountA, amountB) = (amountAOptimal, amountBDesired); 415 | } 416 | } 417 | } 418 | 419 | function addLiquidity( 420 | address tokenA, 421 | address tokenB, 422 | uint256 amountADesired, 423 | uint256 amountBDesired, 424 | uint256 amountAMin, 425 | uint256 amountBMin, 426 | address to, 427 | uint256 deadline 428 | ) 429 | external 430 | override 431 | notAfter(deadline) 432 | returns ( 433 | uint256 amountA, 434 | uint256 amountB, 435 | address pairAddress, 436 | uint256 liquidity 437 | ) 438 | { 439 | (amountA, amountB, pairAddress) = _addLiquidity( 440 | tokenA, 441 | tokenB, 442 | amountADesired, 443 | amountBDesired, 444 | amountAMin, 445 | amountBMin 446 | ); 447 | 448 | SafeERC20.safeTransferFrom( 449 | IERC20(tokenA), 450 | msg.sender, 451 | pairAddress, 452 | amountA 453 | ); 454 | SafeERC20.safeTransferFrom( 455 | IERC20(tokenB), 456 | msg.sender, 457 | pairAddress, 458 | amountB 459 | ); 460 | 461 | liquidity = IvPair(pairAddress).mint(to); 462 | } 463 | 464 | function removeLiquidity( 465 | address tokenA, 466 | address tokenB, 467 | uint256 liquidity, 468 | uint256 amountAMin, 469 | uint256 amountBMin, 470 | address to, 471 | uint256 deadline 472 | ) 473 | external 474 | override 475 | notAfter(deadline) 476 | returns (uint256 amountA, uint256 amountB) 477 | { 478 | address pairAddress = getPairAddress(tokenA, tokenB); 479 | 480 | SafeERC20.safeTransferFrom( 481 | IERC20(pairAddress), 482 | msg.sender, 483 | pairAddress, 484 | liquidity 485 | ); 486 | 487 | (amountA, amountB) = IvPair(pairAddress).burn(to); 488 | 489 | require(amountA >= amountAMin, 'VSWAP: INSUFFICIENT_A_AMOUNT'); 490 | require(amountB >= amountBMin, 'VSWAP: INSUFFICIENT_B_AMOUNT'); 491 | } 492 | 493 | function getVirtualAmountIn( 494 | address jkPair, 495 | address ikPair, 496 | uint256 amountOut 497 | ) public view override returns (uint256 amountIn) { 498 | VirtualPoolModel memory vPool = getVirtualPool(jkPair, ikPair); 499 | 500 | amountIn = vSwapLibrary.getAmountIn( 501 | amountOut, 502 | vPool.balance0, 503 | vPool.balance1, 504 | vPool.fee 505 | ); 506 | } 507 | 508 | function getVirtualAmountOut( 509 | address jkPair, 510 | address ikPair, 511 | uint256 amountIn 512 | ) public view override returns (uint256 amountOut) { 513 | VirtualPoolModel memory vPool = getVirtualPool(jkPair, ikPair); 514 | 515 | amountOut = vSwapLibrary.getAmountOut( 516 | amountIn, 517 | vPool.balance0, 518 | vPool.balance1, 519 | vPool.fee 520 | ); 521 | } 522 | 523 | function getVirtualPools( 524 | address token0, 525 | address token1 526 | ) external view override returns (VirtualPoolModel[] memory vPools) { 527 | vPools = IvPoolManager(IvPairFactory(factory).vPoolManager()) 528 | .getVirtualPools(token0, token1); 529 | } 530 | 531 | function getVirtualPool( 532 | address jkPair, 533 | address ikPair 534 | ) public view override returns (VirtualPoolModel memory vPool) { 535 | vPool = IvPoolManager(IvPairFactory(factory).vPoolManager()) 536 | .getVirtualPool(jkPair, ikPair); 537 | } 538 | 539 | function quote( 540 | address inputToken, 541 | address outputToken, 542 | uint256 amountIn 543 | ) external view override returns (uint256 amountOut) { 544 | IvPair pair = getPair(inputToken, outputToken); 545 | 546 | (uint256 balance0, uint256 balance1) = pair.getBalances(); 547 | 548 | (balance0, balance1) = vSwapLibrary.sortBalances( 549 | inputToken, 550 | pair.token0(), 551 | balance0, 552 | balance1 553 | ); 554 | 555 | amountOut = vSwapLibrary.quote(amountIn, balance0, balance1); 556 | } 557 | 558 | function getAmountOut( 559 | address tokenIn, 560 | address tokenOut, 561 | uint256 amountIn 562 | ) public view virtual override returns (uint256 amountOut) { 563 | IvPair pair = getPair(tokenIn, tokenOut); 564 | 565 | (uint256 balance0, uint256 balance1) = pair.getBalances(); 566 | 567 | (balance0, balance1) = vSwapLibrary.sortBalances( 568 | tokenIn, 569 | pair.token0(), 570 | balance0, 571 | balance1 572 | ); 573 | 574 | amountOut = vSwapLibrary.getAmountOut( 575 | amountIn, 576 | balance0, 577 | balance1, 578 | pair.fee() 579 | ); 580 | } 581 | 582 | function getAmountIn( 583 | address tokenIn, 584 | address tokenOut, 585 | uint256 amountOut 586 | ) public view virtual override returns (uint256 amountIn) { 587 | IvPair pair = getPair(tokenIn, tokenOut); 588 | (uint256 balance0, uint256 balance1) = IvPair(pair).getBalances(); 589 | 590 | (balance0, balance1) = vSwapLibrary.sortBalances( 591 | tokenIn, 592 | pair.token0(), 593 | balance0, 594 | balance1 595 | ); 596 | 597 | amountIn = vSwapLibrary.getAmountIn( 598 | amountOut, 599 | balance0, 600 | balance1, 601 | pair.fee() 602 | ); 603 | } 604 | 605 | function getMaxVirtualTradeAmountRtoN( 606 | address jkPair, 607 | address ikPair 608 | ) external view override returns (uint256 maxAmountIn) { 609 | VirtualPoolModel memory vPool = getVirtualPool(jkPair, ikPair); 610 | maxAmountIn = vSwapLibrary.getMaxVirtualTradeAmountRtoN(vPool); 611 | } 612 | 613 | function changeFactory( 614 | address _factory 615 | ) external override _onlyFactoryAdmin { 616 | require( 617 | _factory > address(0) && _factory != factory, 618 | 'VSWAP:INVALID_FACTORY' 619 | ); 620 | factory = _factory; 621 | 622 | emit RouterFactoryChanged(_factory); 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /contracts/vSwapERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/ERC20.sol) 3 | 4 | pragma solidity 0.8.18; 5 | 6 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 7 | import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; 8 | import '@openzeppelin/contracts/utils/Context.sol'; 9 | 10 | import '@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol'; //for test 11 | 12 | /** 13 | * @dev Implementation of the {IERC20} interface. 14 | * 15 | * This implementation is agnostic to the way tokens are created. This means 16 | * that a supply mechanism has to be added in a derived contract using {_mint}. 17 | * For a generic mechanism see {ERC20PresetMinterPauser}. 18 | * 19 | * TIP: For a detailed writeup see our guide 20 | * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How 21 | * to implement supply mechanisms]. 22 | * 23 | * We have followed general OpenZeppelin Contracts guidelines: functions revert 24 | * instead returning `false` on failure. This behavior is nonetheless 25 | * conventional and does not conflict with the expectations of ERC20 26 | * applications. 27 | * 28 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 29 | * This allows applications to reconstruct the allowance for all accounts just 30 | * by listening to said events. Other implementations of the EIP may not emit 31 | * these events, as it isn't required by the specification. 32 | * 33 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 34 | * functions have been added to mitigate the well-known issues around setting 35 | * allowances. See {IERC20-approve}. 36 | */ 37 | contract vSwapERC20 is Context, IERC20, IERC20Metadata { 38 | mapping(address => uint256) private _balances; 39 | 40 | mapping(address => mapping(address => uint256)) private _allowances; 41 | 42 | uint256 private _totalSupply; 43 | 44 | string private constant _name = 'Virtuswap-LP'; 45 | string private constant _symbol = 'VSWAPLP'; 46 | 47 | /** 48 | * @dev Returns the name of the token. 49 | */ 50 | function name() public view virtual override returns (string memory) { 51 | return _name; 52 | } 53 | 54 | /** 55 | * @dev Returns the symbol of the token, usually a shorter version of the 56 | * name. 57 | */ 58 | function symbol() public view virtual override returns (string memory) { 59 | return _symbol; 60 | } 61 | 62 | /** 63 | * @dev Returns the number of decimals used to get its user representation. 64 | * For example, if `decimals` equals `2`, a balance of `505` tokens should 65 | * be displayed to a user as `5.05` (`505 / 10 ** 2`). 66 | * 67 | * Tokens usually opt for a value of 18, imitating the relationship between 68 | * Ether and Wei. This is the value {ERC20} uses, unless this function is 69 | * overridden; 70 | * 71 | * NOTE: This information is only used for _display_ purposes: it in 72 | * no way affects any of the arithmetic of the contract, including 73 | * {IERC20-balanceOf} and {IERC20-transfer}. 74 | */ 75 | function decimals() public view virtual override returns (uint8) { 76 | return 18; 77 | } 78 | 79 | /** 80 | * @dev See {IERC20-totalSupply}. 81 | */ 82 | function totalSupply() public view virtual override returns (uint256) { 83 | return _totalSupply; 84 | } 85 | 86 | /** 87 | * @dev See {IERC20-balanceOf}. 88 | */ 89 | function balanceOf( 90 | address account 91 | ) public view virtual override returns (uint256) { 92 | return _balances[account]; 93 | } 94 | 95 | /** 96 | * @dev See {IERC20-transfer}. 97 | * 98 | * Requirements: 99 | * 100 | * - `to` cannot be the zero address. 101 | * - the caller must have a balance of at least `amount`. 102 | */ 103 | function transfer( 104 | address to, 105 | uint256 amount 106 | ) public virtual override returns (bool) { 107 | address owner = _msgSender(); 108 | _transfer(owner, to, amount); 109 | return true; 110 | } 111 | 112 | /** 113 | * @dev See {IERC20-allowance}. 114 | */ 115 | function allowance( 116 | address owner, 117 | address spender 118 | ) public view virtual override returns (uint256) { 119 | return _allowances[owner][spender]; 120 | } 121 | 122 | /** 123 | * @dev See {IERC20-approve}. 124 | * 125 | * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on 126 | * `transferFrom`. This is semantically equivalent to an infinite approval. 127 | * 128 | * Requirements: 129 | * 130 | * - `spender` cannot be the zero address. 131 | */ 132 | function approve( 133 | address spender, 134 | uint256 amount 135 | ) external virtual override returns (bool) { 136 | address owner = _msgSender(); 137 | _approve(owner, spender, amount); 138 | return true; 139 | } 140 | 141 | /** 142 | * @dev See {IERC20-transferFrom}. 143 | * 144 | * Emits an {Approval} event indicating the updated allowance. This is not 145 | * required by the EIP. See the note at the beginning of {ERC20}. 146 | * 147 | * NOTE: Does not update the allowance if the current allowance 148 | * is the maximum `uint256`. 149 | * 150 | * Requirements: 151 | * 152 | * - `from` and `to` cannot be the zero address. 153 | * - `from` must have a balance of at least `amount`. 154 | * - the caller must have allowance for ``from``'s tokens of at least 155 | * `amount`. 156 | */ 157 | function transferFrom( 158 | address from, 159 | address to, 160 | uint256 amount 161 | ) public virtual override returns (bool) { 162 | address spender = _msgSender(); 163 | _spendAllowance(from, spender, amount); 164 | _transfer(from, to, amount); 165 | return true; 166 | } 167 | 168 | /** 169 | * @dev Atomically increases the allowance granted to `spender` by the caller. 170 | * 171 | * This is an alternative to {approve} that can be used as a mitigation for 172 | * problems described in {IERC20-approve}. 173 | * 174 | * Emits an {Approval} event indicating the updated allowance. 175 | * 176 | * Requirements: 177 | * 178 | * - `spender` cannot be the zero address. 179 | */ 180 | function increaseAllowance( 181 | address spender, 182 | uint256 addedValue 183 | ) external virtual returns (bool) { 184 | address owner = _msgSender(); 185 | _approve(owner, spender, allowance(owner, spender) + addedValue); 186 | return true; 187 | } 188 | 189 | /** 190 | * @dev Atomically decreases the allowance granted to `spender` by the caller. 191 | * 192 | * This is an alternative to {approve} that can be used as a mitigation for 193 | * problems described in {IERC20-approve}. 194 | * 195 | * Emits an {Approval} event indicating the updated allowance. 196 | * 197 | * Requirements: 198 | * 199 | * - `spender` cannot be the zero address. 200 | * - `spender` must have allowance for the caller of at least 201 | * `subtractedValue`. 202 | */ 203 | function decreaseAllowance( 204 | address spender, 205 | uint256 subtractedValue 206 | ) external virtual returns (bool) { 207 | address owner = _msgSender(); 208 | uint256 currentAllowance = allowance(owner, spender); 209 | require( 210 | currentAllowance >= subtractedValue, 211 | 'ERC20: decreased allowance below zero' 212 | ); 213 | unchecked { 214 | _approve(owner, spender, currentAllowance - subtractedValue); 215 | } 216 | 217 | return true; 218 | } 219 | 220 | /** 221 | * @dev Moves `amount` of tokens from `from` to `to`. 222 | * 223 | * This internal function is equivalent to {transfer}, and can be used to 224 | * e.g. implement automatic token fees, slashing mechanisms, etc. 225 | * 226 | * Emits a {Transfer} event. 227 | * 228 | * Requirements: 229 | * 230 | * - `from` cannot be the zero address. 231 | * - `to` cannot be the zero address. 232 | * - `from` must have a balance of at least `amount`. 233 | */ 234 | function _transfer( 235 | address from, 236 | address to, 237 | uint256 amount 238 | ) internal virtual { 239 | require(from != address(0), 'ERC20: transfer from the zero address'); 240 | require(to != address(0), 'ERC20: transfer to the zero address'); 241 | 242 | _beforeTokenTransfer(from, to, amount); 243 | 244 | uint256 fromBalance = _balances[from]; 245 | require( 246 | fromBalance >= amount, 247 | 'ERC20: transfer amount exceeds balance' 248 | ); 249 | unchecked { 250 | _balances[from] = fromBalance - amount; 251 | // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by 252 | // decrementing then incrementing. 253 | _balances[to] += amount; 254 | } 255 | 256 | emit Transfer(from, to, amount); 257 | 258 | _afterTokenTransfer(from, to, amount); 259 | } 260 | 261 | /** @dev Creates `amount` tokens and assigns them to `account`, increasing 262 | * the total supply. 263 | * 264 | * Emits a {Transfer} event with `from` set to the zero address. 265 | * 266 | * Requirements: 267 | * 268 | * - `account` cannot be the zero address. 269 | */ 270 | function _mint(address account, uint256 amount) internal virtual { 271 | _beforeTokenTransfer(address(0), account, amount); 272 | 273 | _totalSupply += amount; 274 | unchecked { 275 | // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. 276 | _balances[account] += amount; 277 | } 278 | emit Transfer(address(0), account, amount); 279 | 280 | _afterTokenTransfer(address(0), account, amount); 281 | } 282 | 283 | /** 284 | * @dev Destroys `amount` tokens from `account`, reducing the 285 | * total supply. 286 | * 287 | * Emits a {Transfer} event with `to` set to the zero address. 288 | * 289 | * Requirements: 290 | * 291 | * - `account` cannot be the zero address. 292 | * - `account` must have at least `amount` tokens. 293 | */ 294 | function _burn(address account, uint256 amount) internal virtual { 295 | require(account != address(0), 'ERC20: burn from the zero address'); 296 | 297 | _beforeTokenTransfer(account, address(0), amount); 298 | 299 | uint256 accountBalance = _balances[account]; 300 | require(accountBalance >= amount, 'ERC20: burn amount exceeds balance'); 301 | unchecked { 302 | _balances[account] = accountBalance - amount; 303 | // Overflow not possible: amount <= accountBalance <= totalSupply. 304 | _totalSupply -= amount; 305 | } 306 | 307 | emit Transfer(account, address(0), amount); 308 | 309 | _afterTokenTransfer(account, address(0), amount); 310 | } 311 | 312 | /** 313 | * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. 314 | * 315 | * This internal function is equivalent to `approve`, and can be used to 316 | * e.g. set automatic allowances for certain subsystems, etc. 317 | * 318 | * Emits an {Approval} event. 319 | * 320 | * Requirements: 321 | * 322 | * - `owner` cannot be the zero address. 323 | * - `spender` cannot be the zero address. 324 | */ 325 | function _approve( 326 | address owner, 327 | address spender, 328 | uint256 amount 329 | ) internal virtual { 330 | require(owner != address(0), 'ERC20: approve from the zero address'); 331 | require(spender != address(0), 'ERC20: approve to the zero address'); 332 | 333 | _allowances[owner][spender] = amount; 334 | emit Approval(owner, spender, amount); 335 | } 336 | 337 | /** 338 | * @dev Updates `owner` s allowance for `spender` based on spent `amount`. 339 | * 340 | * Does not update the allowance amount in case of infinite allowance. 341 | * Revert if not enough allowance is available. 342 | * 343 | * Might emit an {Approval} event. 344 | */ 345 | function _spendAllowance( 346 | address owner, 347 | address spender, 348 | uint256 amount 349 | ) internal virtual { 350 | uint256 currentAllowance = allowance(owner, spender); 351 | if (currentAllowance != type(uint256).max) { 352 | require( 353 | currentAllowance >= amount, 354 | 'ERC20: insufficient allowance' 355 | ); 356 | unchecked { 357 | _approve(owner, spender, currentAllowance - amount); 358 | } 359 | } 360 | } 361 | 362 | /** 363 | * @dev Hook that is called before any transfer of tokens. This includes 364 | * minting and burning. 365 | * 366 | * Calling conditions: 367 | * 368 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 369 | * will be transferred to `to`. 370 | * - when `from` is zero, `amount` tokens will be minted for `to`. 371 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned. 372 | * - `from` and `to` are never both zero. 373 | * 374 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 375 | */ 376 | function _beforeTokenTransfer( 377 | address from, 378 | address to, 379 | uint256 amount 380 | ) internal virtual {} 381 | 382 | /** 383 | * @dev Hook that is called after any transfer of tokens. This includes 384 | * minting and burning. 385 | * 386 | * Calling conditions: 387 | * 388 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 389 | * has been transferred to `to`. 390 | * - when `from` is zero, `amount` tokens have been minted for `to`. 391 | * - when `to` is zero, `amount` of ``from``'s tokens have been burned. 392 | * - `from` and `to` are never both zero. 393 | * 394 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 395 | */ 396 | function _afterTokenTransfer( 397 | address from, 398 | address to, 399 | uint256 amount 400 | ) internal virtual {} 401 | } 402 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config'; 2 | import '@nomicfoundation/hardhat-toolbox'; 3 | import './tasks/compile'; 4 | 5 | import 'hardhat-contract-sizer'; 6 | 7 | const config: HardhatUserConfig = { 8 | contractSizer: { 9 | alphaSort: true, 10 | runOnCompile: true, 11 | disambiguatePaths: false, 12 | }, 13 | solidity: { 14 | version: '0.8.18', 15 | settings: { 16 | optimizer: { 17 | enabled: true, 18 | runs: 833, 19 | }, 20 | 21 | metadata: { 22 | // do not include the metadata hash, since this is machine dependent 23 | // and we want all generated code to be deterministic 24 | // https://docs.soliditylang.org/en/v0.7.6/metadata.html 25 | bytecodeHash: 'none', 26 | }, 27 | }, 28 | }, 29 | }; 30 | 31 | export default config; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Virtuswap", 3 | "version": "1.0.0", 4 | "description": "Virtuswap protocol smart contracts", 5 | "license": "ISC", 6 | "devDependencies": { 7 | "@nomicfoundation/hardhat-network-helpers": "1.0.4", 8 | "@nomicfoundation/hardhat-toolbox": "1.0.2", 9 | "@openzeppelin/contracts": "4.8.1", 10 | "@openzeppelin/test-helpers": "0.5.15", 11 | "@typechain/hardhat": "6.1.2", 12 | "@types/chai": "4.3.1", 13 | "@types/lodash": "4.14.182", 14 | "@types/mocha": "9.1.1", 15 | "@types/node": "18.6.3", 16 | "@uniswap/lib": "4.0.1-alpha", 17 | "@uniswap/v2-periphery": "1.1.0-beta.0", 18 | "@uniswap/v3-periphery": "1.4.1", 19 | "chai": "4.3.6", 20 | "ethers": "5.6.9", 21 | "hardhat": "2.11.0", 22 | "hardhat-contract-sizer": "2.6.1", 23 | "replace-in-file": "^6.3.5", 24 | "ts-node": "10.9.1", 25 | "typechain": "8.1.0", 26 | "typescript": "4.7.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tasks/compile.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import * as path from 'path'; 3 | import * as replace from 'replace-in-file'; 4 | 5 | export default task( 6 | 'compile', 7 | 'Compiles the entire project, building all artifacts', 8 | async (_taskArgs, hre, runSuper) => { 9 | await runSuper(_taskArgs); 10 | const vPairContractFactory = await hre.ethers.getContractFactory( 11 | 'vPair' 12 | ); 13 | const init_hash = await hre.ethers.utils.keccak256( 14 | vPairContractFactory.bytecode 15 | ); 16 | const path_to_pool_address = path.join( 17 | hre.config.paths.sources, 18 | 'libraries', 19 | 'PoolAddress.sol' 20 | ); 21 | const options = { 22 | files: path_to_pool_address, 23 | from: new RegExp('POOL_INIT_CODE_HASH.*\n?.*0x.*;'), 24 | to: `POOL_INIT_CODE_HASH =\n ${init_hash};`, 25 | }; 26 | if (replace.sync(options)[0].hasChanged) { 27 | await runSuper(_taskArgs); 28 | } 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /test/1_base.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from 'chai'; 3 | import { deployPools } from './fixtures/deployPools'; 4 | 5 | describe('Base actions', function () { 6 | it('Should deploy fixture', async function () { 7 | const fixture = await loadFixture(deployPools); 8 | 9 | expect(fixture.tokenA.address.length > 0); 10 | expect(fixture.tokenB.address.length > 0); 11 | expect(fixture.tokenC.address.length > 0); 12 | expect(fixture.vRouterInstance.address.length > 0); 13 | expect(fixture.vPairFactoryInstance.address.length > 0); 14 | expect(fixture.abPool.address.length > 0); 15 | expect(fixture.bcPool.address.length > 0); 16 | expect(fixture.acPool.address.length > 0); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/4_reserveRatio.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from 'chai'; 3 | import { ethers } from 'hardhat'; 4 | import { deployPools } from './fixtures/deployPools'; 5 | import { sameValues } from './fixtures/sameValues'; 6 | import _ from 'lodash'; 7 | import utils from './utils'; 8 | 9 | describe('Reserve Ratio 1', () => { 10 | let fixture: any = {}; 11 | 12 | before(async function () { 13 | fixture = await loadFixture(deployPools); 14 | await fixture.abPool.setBlocksDelay(0); 15 | await fixture.acPool.setBlocksDelay(0); 16 | await fixture.bcPool.setBlocksDelay(0); 17 | await fixture.bdPool.setBlocksDelay(0); 18 | }); 19 | 20 | it('Should increase reserveRatio and reservesBaseValue of C after adding C for A to pool A/B', async () => { 21 | const abPool = fixture.abPool; 22 | const bcPool = fixture.bcPool; 23 | const tokenA = fixture.tokenA; 24 | const tokenB = fixture.tokenB; 25 | const tokenC = fixture.tokenC; 26 | const owner = fixture.owner; 27 | const vRouterInstance = fixture.vRouterInstance; 28 | 29 | let amountOut = ethers.utils.parseEther('100'); 30 | 31 | let amountIn = await vRouterInstance.getVirtualAmountIn( 32 | abPool.address, 33 | bcPool.address, 34 | amountOut 35 | ); 36 | 37 | let reserveRatioBefore = await abPool.calculateReserveRatio(); 38 | let tokenCReserve = await abPool.reservesBaseValue(tokenC.address); 39 | 40 | const futureTs = await utils.getFutureBlockTimestamp(); 41 | 42 | await vRouterInstance.swapReserveTokensForExactTokens( 43 | tokenA.address, 44 | tokenB.address, 45 | bcPool.address, 46 | amountOut, 47 | amountIn, 48 | owner.address, 49 | futureTs 50 | ); 51 | 52 | let reserveRatioAfter = await abPool.calculateReserveRatio(); 53 | 54 | expect(reserveRatioBefore).to.lessThan(reserveRatioAfter); 55 | 56 | let tokenCReserveAfter = await abPool.reservesBaseValue(tokenC.address); 57 | expect(tokenCReserve).to.lessThan(tokenCReserveAfter); 58 | }); 59 | 60 | it('Should increase reserveRatio and reservesBaseValue of C after adding C for B to pool A/B', async () => { 61 | const abPool = fixture.abPool; 62 | const bcPool = fixture.bcPool; 63 | const acPool = fixture.acPool; 64 | const tokenA = fixture.tokenA; 65 | const tokenB = fixture.tokenB; 66 | const tokenC = fixture.tokenC; 67 | const owner = fixture.owner; 68 | const vRouterInstance = fixture.vRouterInstance; 69 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 70 | 71 | let amountOut = ethers.utils.parseEther('100'); 72 | 73 | let amountIn = await vRouterInstance.getVirtualAmountIn( 74 | acPool.address, 75 | bcPool.address, 76 | amountOut 77 | ); 78 | 79 | let reserveRatioBefore = await abPool.calculateReserveRatio(); 80 | let tokenCReserve = await abPool.reservesBaseValue(tokenC.address); 81 | 82 | const futureTs = await utils.getFutureBlockTimestamp(); 83 | 84 | await vRouterInstance.swapReserveTokensForExactTokens( 85 | tokenB.address, 86 | tokenA.address, 87 | acPool.address, 88 | amountOut, 89 | amountIn, 90 | owner.address, 91 | futureTs 92 | ); 93 | 94 | let reserveRatioAfter = await abPool.calculateReserveRatio(); 95 | 96 | expect(reserveRatioBefore).to.lessThan(reserveRatioAfter); 97 | 98 | let tokenCReserveAfter = await abPool.reservesBaseValue(tokenC.address); 99 | expect(tokenCReserve).to.lessThan(tokenCReserveAfter); 100 | }); 101 | 102 | it('Should increase reserveRatio and reservesBaseValue of C after adding C for A on pool A/B #2', async () => { 103 | const abPool = fixture.abPool; 104 | const bcPool = fixture.bcPool; 105 | const tokenA = fixture.tokenA; 106 | const tokenB = fixture.tokenB; 107 | const tokenC = fixture.tokenC; 108 | const owner = fixture.owner; 109 | const vRouterInstance = fixture.vRouterInstance; 110 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 111 | const ikPair = await vPairFactoryInstance.pairs( 112 | tokenC.address, 113 | tokenB.address 114 | ); 115 | 116 | const jkPair = await vPairFactoryInstance.pairs( 117 | tokenB.address, 118 | tokenA.address 119 | ); 120 | 121 | let amountOut = ethers.utils.parseEther('100'); 122 | 123 | let amountIn = await vRouterInstance.getVirtualAmountIn( 124 | jkPair, 125 | ikPair, 126 | amountOut 127 | ); 128 | 129 | let reserveRatioBefore = await abPool.calculateReserveRatio(); 130 | let tokenCReserve = await abPool.reservesBaseValue(tokenC.address); 131 | 132 | const futureTs = await utils.getFutureBlockTimestamp(); 133 | 134 | await vRouterInstance.swapReserveTokensForExactTokens( 135 | tokenA.address, 136 | tokenB.address, 137 | ikPair, 138 | amountOut, 139 | amountIn, 140 | owner.address, 141 | futureTs 142 | ); 143 | 144 | let reserveRatioAfter = await abPool.calculateReserveRatio(); 145 | 146 | expect(reserveRatioBefore).to.lessThan(reserveRatioAfter); 147 | 148 | let tokenCReserveAfter = await abPool.reservesBaseValue(tokenC.address); 149 | expect(tokenCReserve).to.lessThan(tokenCReserveAfter); 150 | }); 151 | 152 | it('Should increase reserveRatio and reservesBaseValue of C after adding C for B on pool A/B', async () => { 153 | const abPool = fixture.abPool; 154 | const bcPool = fixture.bcPool; 155 | const tokenA = fixture.tokenA; 156 | const tokenB = fixture.tokenB; 157 | const tokenC = fixture.tokenC; 158 | const owner = fixture.owner; 159 | const vRouterInstance = fixture.vRouterInstance; 160 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 161 | const ikPair = await vPairFactoryInstance.pairs( 162 | tokenC.address, 163 | tokenA.address 164 | ); 165 | 166 | const jkPair = await vPairFactoryInstance.pairs( 167 | tokenB.address, 168 | tokenA.address 169 | ); 170 | 171 | let amountOut = ethers.utils.parseEther('1'); 172 | 173 | let amountIn = await vRouterInstance.getVirtualAmountIn( 174 | jkPair, 175 | ikPair, 176 | amountOut 177 | ); 178 | 179 | let reserveRatioBefore = await abPool.calculateReserveRatio(); 180 | let tokenCReserve = await abPool.reservesBaseValue(tokenC.address); 181 | 182 | const futureTs = await utils.getFutureBlockTimestamp(); 183 | 184 | await vRouterInstance.swapReserveTokensForExactTokens( 185 | tokenB.address, 186 | tokenA.address, 187 | ikPair, 188 | amountOut, 189 | amountIn, 190 | owner.address, 191 | futureTs 192 | ); 193 | 194 | let reserveRatioAfter = await abPool.calculateReserveRatio(); 195 | 196 | expect(reserveRatioBefore).to.lessThan(reserveRatioAfter); 197 | 198 | let tokenCReserveAfter = await abPool.reservesBaseValue(tokenC.address); 199 | expect(tokenCReserve).to.lessThan(tokenCReserveAfter); 200 | }); 201 | 202 | // it("Should update price after a 50% drop in price of C in pool A/B", async () => { 203 | // const ikPair = await vPairFactoryInstance.pairs( 204 | // tokenC.address, 205 | // tokenB.address 206 | // ); 207 | 208 | // const jkPair = await vPairFactoryInstance.pairs( 209 | // tokenB.address, 210 | // tokenA.address 211 | // ); 212 | 213 | // let amountIn = web3.utils.toWei("120000", "ether"); 214 | 215 | // const pool = await vPair.at(jkPair); 216 | // const ikpool = await vPair.at(ikPair); 217 | 218 | // // 219 | // let reserves = await ikpool.getReserves(); 220 | 221 | // console.log("Pool B/C - reserves 0 " + (reserves["0"])); 222 | // console.log("Pool B/C - reserves 1 " + (reserves["1"])); 223 | 224 | // //get quote 225 | // let amountOutOne = await vRouterInstance.getAmountOut( 226 | // tokenC.address, 227 | // tokenB.address, 228 | // web3.utils.toWei("1", "ether") 229 | // ); 230 | 231 | // console.log( 232 | // "Pool B/C - for 1C gets " + (amountOutOne) + "B" 233 | // ); 234 | 235 | // //get quote 236 | // let amountOut = await vRouterInstance.getAmountOut( 237 | // tokenC.address, 238 | // tokenB.address, 239 | // amountIn 240 | // ); 241 | 242 | // console.log( 243 | // "Pool B/C - for " + 244 | // (amountIn) + 245 | // "C gets " + 246 | // (amountOut) + 247 | // "B" 248 | // ); 249 | 250 | // //reserve of C in pool JK 251 | // let reserveBaseBalance = await pool.reservesBaseValue(tokenC.address); 252 | // let reserveBalance = await pool.reserves(tokenC.address); 253 | 254 | // console.log( 255 | // "Pool A/B - C reserve balance: " + (reserveBalance) 256 | // ); 257 | // console.log( 258 | // "Pool A/B - C reserve balance in token0: " + 259 | // (reserveBaseBalance) 260 | // ); 261 | 262 | // console.log( 263 | // "-------------------\nPool B/C - swapping " + 264 | // (amountIn) + 265 | // "C for " + 266 | // (amountOut) + 267 | // "B" 268 | // ); 269 | 270 | // let data = getEncodedSwapData( 271 | // accounts[0], 272 | // tokenC.address, 273 | // tokenA.address, 274 | // tokenB.address, 275 | // amountIn 276 | // ); 277 | 278 | // const futureTs = await getFutureBlockTimestamp(); 279 | 280 | // await vRouterInstance.swapToExactNative( 281 | // tokenA.address, 282 | // tokenB.address, 283 | // amountOut, 284 | // accounts[0], 285 | // data, 286 | // futureTs 287 | // ); 288 | 289 | // let reservesAfterSwap = await ikpool.getReserves(); 290 | 291 | // console.log( 292 | // "Pool B/C - reserves 0 " + (reservesAfterSwap["0"]) 293 | // ); 294 | // console.log( 295 | // "Pool B/C - reserves 1 " + (reservesAfterSwap["1"]) 296 | // ); 297 | 298 | // //get quote 299 | // let amountOutOneAfter = await vRouterInstance.getAmountOut( 300 | // tokenC.address, 301 | // tokenB.address, 302 | // web3.utils.toWei("1", "ether") 303 | // ); 304 | 305 | // console.log( 306 | // "Pool B/C - for 1C gets " + (amountOutOneAfter) + "B" 307 | // ); 308 | 309 | // console.log("-------------------\nSwap C for A in vPool A/C "); 310 | 311 | // let amountOutA = web3.utils.toWei("1", "ether"); 312 | 313 | // const ikPair2 = await vPairFactoryInstance.pairs( 314 | // tokenC.address, 315 | // tokenB.address 316 | // ); 317 | 318 | // const jkPair2 = await vPairFactoryInstance.pairs( 319 | // tokenB.address, 320 | // tokenA.address 321 | // ); 322 | 323 | // //get amountIn 324 | // let amountInC = await vRouterInstance.getVirtualAmountIn( 325 | // jkPair2, 326 | // ikPair2, 327 | // amountOutA 328 | // ); 329 | 330 | // console.log( 331 | // "vPool A/C - for " + 332 | // (amountInC) + 333 | // "C gets " + 334 | // (amountOutA) + 335 | // "A" 336 | // ); 337 | 338 | // //add C and get B from pool AB 339 | // await vRouterInstance.swap( 340 | // [jkPair2], 341 | // [amountInC], 342 | // [amountOutA], 343 | // [ikPair2], 344 | // tokenC.address, 345 | // tokenA.address, 346 | // accounts[0], 347 | // futureTs 348 | // ); 349 | 350 | // //reserve of C in pool JK 351 | // let reserveBaseBalanceAfter = await pool.reservesBaseValue(tokenC.address); 352 | // let reserveBalanceAfter = await pool.reserves(tokenC.address); 353 | 354 | // console.log( 355 | // "Pool A/B - C reserve balance: " + (reserveBalanceAfter) 356 | // ); 357 | // console.log( 358 | // "Pool A/B - C reserve balance in token0: " + 359 | // (reserveBaseBalanceAfter) 360 | // ); 361 | // }); 362 | 363 | // it("Should increase reserveRatio and reservesBaseValue of D after adding D to pool A/B", async () => { 364 | // const ikPair = await vPairFactoryInstance.pairs( 365 | // tokenD.address, 366 | // tokenB.address 367 | // ); 368 | 369 | // const jkPair = await vPairFactoryInstance.pairs( 370 | // tokenB.address, 371 | // tokenA.address 372 | // ); 373 | 374 | // let amountOut = web3.utils.toWei("2", "ether"); 375 | 376 | // const amountIn = await vRouterInstance.getVirtualAmountIn( 377 | // jkPair, 378 | // ikPair, 379 | // amountOut 380 | // ); 381 | 382 | // const pool = await vPair.at(jkPair); 383 | 384 | // const futureTs = await getFutureBlockTimestamp(); 385 | 386 | // let reserveRatioBefore = await pool.calculateReserveRatio(); 387 | 388 | // let tokenDReserve = await pool.reservesBaseValue(tokenD.address); 389 | 390 | // await vRouterInstance.swap( 391 | // [jkPair], 392 | // [amountIn], 393 | // [amountOut], 394 | // [ikPair], 395 | // tokenD.address, 396 | // tokenA.address, 397 | // accounts[0], 398 | // futureTs 399 | // ); 400 | 401 | // let tokenDReserveAfter = await pool.reservesBaseValue(tokenD.address); 402 | // let reserveRatioAfter = await pool.calculateReserveRatio(); 403 | 404 | // expect((reserveRatioBefore)).to.lessThan( 405 | // (reserveRatioAfter) 406 | // ); 407 | 408 | // expect((tokenDReserve)).to.lessThan( 409 | // (tokenDReserveAfter) 410 | // ); 411 | // }); 412 | 413 | // it("Assert pool A/B calculateReserveRatio is correct ", async () => { 414 | // const abPool = fixture.abPool; 415 | // const bcPool = fixture.bcPool; 416 | // const tokenA = fixture.tokenA; 417 | // const tokenB = fixture.tokenB; 418 | // const tokenC = fixture.tokenC; 419 | // const tokenD = fixture.tokenD; 420 | 421 | // const owner = fixture.owner; 422 | // const vRouterInstance = fixture.vRouterInstance; 423 | // const vPairFactoryInstance = fixture.vPairFactoryInstance; 424 | 425 | // const jkPair = await vPairFactoryInstance.pairs( 426 | // tokenB.address, 427 | // tokenA.address 428 | // ); 429 | 430 | // let poolReserveRatio = await abPool.calculateReserveRatio(); 431 | 432 | // let poolCReserves = await abPool.reservesBaseValue(tokenC.address); 433 | // let poolDReserves = await abPool.reservesBaseValue(tokenD.address); 434 | 435 | // poolCReserves = poolCReserves; 436 | // poolDReserves = poolDReserves; 437 | 438 | // let totalReserves = poolCReserves.add(poolDReserves); 439 | // console.log('totalReserves ' + totalReserves); 440 | 441 | // let reserve0 = await abPool.reserve0(); 442 | // reserve0 = reserve0; 443 | // let poolLiquidity = reserve0.mul(2); 444 | // console.log('poolLiquidity ' + poolLiquidity); 445 | 446 | // let reserveRatioPCT = totalReserves.div(poolLiquidity); 447 | // console.log('reserveRatioPCT ' + reserveRatioPCT); 448 | 449 | // poolReserveRatio = poolReserveRatio; 450 | 451 | // let maxReserveRatio = await abPool.max_reserve_ratio(); 452 | 453 | // expect(parseInt(poolReserveRatio)).to.equal(reserveRatioPCT * 1000); 454 | // }); 455 | 456 | it('Should revert swap that goes beyond reserve ratio', async () => { 457 | const tokenA = fixture.tokenA; 458 | const tokenB = fixture.tokenB; 459 | const tokenC = fixture.tokenC; 460 | 461 | const owner = fixture.owner; 462 | const vRouterInstance = fixture.vRouterInstance; 463 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 464 | 465 | const ikPair = await vPairFactoryInstance.pairs( 466 | tokenC.address, 467 | tokenB.address 468 | ); 469 | 470 | const jkPair = await vPairFactoryInstance.pairs( 471 | tokenB.address, 472 | tokenA.address 473 | ); 474 | 475 | let amountOut = ethers.utils.parseEther('40'); 476 | 477 | const amountIn = await vRouterInstance.getVirtualAmountIn( 478 | jkPair, 479 | ikPair, 480 | amountOut 481 | ); 482 | 483 | const futureTs = await utils.getFutureBlockTimestamp(); 484 | 485 | let reverted = false; 486 | try { 487 | await vRouterInstance.swap( 488 | [jkPair], 489 | [amountIn], 490 | [amountOut], 491 | [ikPair], 492 | tokenC.address, 493 | tokenA.address, 494 | owner.address, 495 | futureTs 496 | ); 497 | } catch { 498 | reverted = true; 499 | } 500 | 501 | expect(reverted, 'EXPECTED SWAP TO REVERT'); 502 | }); 503 | 504 | it('Withdrawal from pool A/B and check reserves and reserveRatio', async () => { 505 | const abPool = fixture.abPool; 506 | const bcPool = fixture.bcPool; 507 | const tokenA = fixture.tokenA; 508 | const tokenB = fixture.tokenB; 509 | const tokenC = fixture.tokenC; 510 | const owner = fixture.owner; 511 | const vRouterInstance = fixture.vRouterInstance; 512 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 513 | 514 | const poolAddress = await vPairFactoryInstance.pairs( 515 | tokenB.address, 516 | tokenA.address 517 | ); 518 | 519 | let balance = await abPool.balanceOf(owner.address); 520 | 521 | //get 30% of balance out 522 | let balanceOut = balance.div(3); 523 | 524 | await abPool.approve(vRouterInstance.address, balanceOut); 525 | 526 | let reserves = await abPool.getBalances(); 527 | 528 | let amountADesired = reserves._balance0.mul(290).div(1000); 529 | let amountBDesired = reserves._balance1.mul(290).div(1000); 530 | 531 | let amountCInBalance = await tokenC.balanceOf(abPool.address); 532 | let amountCInReserve = await abPool.reserves(tokenC.address); 533 | let amountCInReserveBaseValue = await abPool.reservesBaseValue( 534 | tokenC.address 535 | ); 536 | 537 | const futureTs = await utils.getFutureBlockTimestamp(); 538 | await vRouterInstance.removeLiquidity( 539 | tokenA.address, 540 | tokenB.address, 541 | balanceOut, 542 | amountADesired, 543 | amountBDesired, 544 | owner.address, 545 | futureTs 546 | ); 547 | 548 | let amountCInBalanceAfter = await tokenC.balanceOf(abPool.address); 549 | let amountCInReserveAfter = await abPool.reserves(tokenC.address); 550 | let amountCInReserveBaseValueAfter = await abPool.reservesBaseValue( 551 | tokenC.address 552 | ); 553 | }); 554 | }); 555 | 556 | describe('Reserve Ratio 2', () => { 557 | let fixture: any = {}; 558 | 559 | before(async () => { 560 | fixture = await loadFixture(sameValues); 561 | await fixture.vPairFactoryInstance.setExchangeReservesAddress( 562 | fixture.exchangeReserveInstance.address 563 | ); 564 | // TODO 565 | await fixture.abPool.setBlocksDelay(0); 566 | await fixture.acPool.setBlocksDelay(0); 567 | await fixture.bcPool.setBlocksDelay(0); 568 | await fixture.bdPool.setBlocksDelay(0); 569 | }); 570 | 571 | it('Exchange 500000 C to B in pool AB', async () => { 572 | const abPool = fixture.abPool; 573 | const tokenA = fixture.tokenA; 574 | const owner = fixture.owner; 575 | const tokenB = fixture.tokenB; 576 | const tokenC = fixture.tokenC; 577 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 578 | const vRouterInstance = fixture.vRouterInstance; 579 | 580 | let amountCIn = ethers.utils.parseEther('500000'); 581 | const ikPair = await vPairFactoryInstance.pairs( 582 | tokenA.address, 583 | tokenC.address 584 | ); 585 | 586 | const jkPair = await vPairFactoryInstance.pairs( 587 | tokenA.address, 588 | tokenB.address 589 | ); 590 | 591 | let amountBOut = await vRouterInstance.getVirtualAmountOut( 592 | jkPair, 593 | ikPair, 594 | amountCIn 595 | ); 596 | 597 | let ABRRBefore = (await abPool.calculateReserveRatio()).toString(); 598 | console.log(`Reserve ratio of AB pool before = ${ABRRBefore}`); 599 | 600 | await tokenC.transfer(abPool.address, amountCIn); 601 | 602 | await abPool.swapReserveToNative(amountBOut, ikPair, owner.address, []); 603 | 604 | let ABRRAfter = (await abPool.calculateReserveRatio()).toString(); 605 | console.log(`Reserve ratio of AB pool after = ${ABRRAfter}`); 606 | 607 | expect(ABRRAfter).to.equal('504'); 608 | 609 | console.log(`Exchanged ${amountCIn} of C for ${amountBOut} of B`); 610 | console.log( 611 | `Reserve base value of token C = ${( 612 | await abPool.reservesBaseValue(tokenC.address) 613 | ).toString()}` 614 | ); 615 | console.log( 616 | `Reserve of token C = ${( 617 | await abPool.reserves(tokenC.address) 618 | ).toString()}` 619 | ); 620 | console.log(`A balance = ${(await abPool.pairBalance0()).toString()}`); 621 | console.log(`B balance = ${(await abPool.pairBalance1()).toString()}`); 622 | }); 623 | 624 | it('Exchange 500000 D to A in pool AB', async () => { 625 | const abPool = fixture.abPool; 626 | const tokenA = fixture.tokenA; 627 | const owner = fixture.owner; 628 | const tokenB = fixture.tokenB; 629 | const tokenD = fixture.tokenD; 630 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 631 | const vRouterInstance = fixture.vRouterInstance; 632 | 633 | let amountDIn = ethers.utils.parseEther('500000'); 634 | const ikPair = await vPairFactoryInstance.pairs( 635 | tokenB.address, 636 | tokenD.address 637 | ); 638 | 639 | const jkPair = await vPairFactoryInstance.pairs( 640 | tokenA.address, 641 | tokenB.address 642 | ); 643 | 644 | let amountAOut = await vRouterInstance.getVirtualAmountOut( 645 | jkPair, 646 | ikPair, 647 | amountDIn 648 | ); 649 | 650 | let ABRRBefore = (await abPool.calculateReserveRatio()).toString(); 651 | console.log(`Reserve ratio of AB pool before = ${ABRRBefore}`); 652 | 653 | await tokenD.transfer(abPool.address, amountDIn); 654 | 655 | await abPool.swapReserveToNative(amountAOut, ikPair, owner.address, []); 656 | 657 | let ABRRAfter = (await abPool.calculateReserveRatio()).toString(); 658 | console.log(`Reserve ratio of AB pool after = ${ABRRAfter}`); 659 | 660 | expect(ABRRAfter).to.equal('1009'); 661 | 662 | console.log(`Exchanged ${amountDIn} of D for ${amountAOut} of A`); 663 | console.log( 664 | `Reserve base value of token D = ${( 665 | await abPool.reservesBaseValue(tokenD.address) 666 | ).toString()}` 667 | ); 668 | console.log(`A balance = ${(await abPool.pairBalance0()).toString()}`); 669 | console.log(`B balance = ${(await abPool.pairBalance1()).toString()}`); 670 | }); 671 | 672 | it('Exchange 300000 C to A in pool AB', async () => { 673 | const abPool = fixture.abPool; 674 | const tokenA = fixture.tokenA; 675 | const owner = fixture.owner; 676 | const tokenB = fixture.tokenB; 677 | const tokenC = fixture.tokenC; 678 | const tokenD = fixture.tokenD; 679 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 680 | const vRouterInstance = fixture.vRouterInstance; 681 | 682 | let amountCIn = ethers.utils.parseEther('300000'); 683 | const ikPair = await vPairFactoryInstance.pairs( 684 | tokenB.address, 685 | tokenC.address 686 | ); 687 | 688 | const jkPair = await vPairFactoryInstance.pairs( 689 | tokenA.address, 690 | tokenB.address 691 | ); 692 | 693 | let amountAOut = await vRouterInstance.getVirtualAmountOut( 694 | jkPair, 695 | ikPair, 696 | amountCIn 697 | ); 698 | 699 | let ABRRBefore = (await abPool.calculateReserveRatio()).toString(); 700 | console.log(`Reserve ratio of AB pool before = ${ABRRBefore}`); 701 | 702 | await tokenC.transfer(abPool.address, amountCIn); 703 | 704 | await abPool.swapReserveToNative(amountAOut, ikPair, owner.address, []); 705 | 706 | let ABRRAfter = (await abPool.calculateReserveRatio()).toString(); 707 | console.log(`Reserve ratio of AB pool after = ${ABRRAfter}`); 708 | 709 | expect(ABRRAfter).to.equal('1312'); 710 | 711 | console.log(`Exchanged ${amountCIn} of C for ${amountAOut} of A`); 712 | console.log( 713 | `Reserve base value of token C = ${( 714 | await abPool.reservesBaseValue(tokenC.address) 715 | ).toString()}` 716 | ); 717 | console.log( 718 | `Reserve base value of token D = ${( 719 | await abPool.reservesBaseValue(tokenD.address) 720 | ).toString()}` 721 | ); 722 | console.log(`A balance = ${(await abPool.pairBalance0()).toString()}`); 723 | console.log(`B balance = ${(await abPool.pairBalance1()).toString()}`); 724 | }); 725 | 726 | it('Exchange 1000000 A to D in pool BD', async () => { 727 | const bdPool = fixture.bdPool; 728 | const tokenA = fixture.tokenA; 729 | const owner = fixture.owner; 730 | const tokenB = fixture.tokenB; 731 | const tokenD = fixture.tokenD; 732 | const vPairFactoryInstance = fixture.vPairFactoryInstance; 733 | const vRouterInstance = fixture.vRouterInstance; 734 | 735 | let amountAIn = ethers.utils.parseEther('1000000'); 736 | const ikPair = await vPairFactoryInstance.pairs( 737 | tokenA.address, 738 | tokenB.address 739 | ); 740 | 741 | const jkPair = await vPairFactoryInstance.pairs( 742 | tokenB.address, 743 | tokenD.address 744 | ); 745 | 746 | let amountDOut = await vRouterInstance.getVirtualAmountOut( 747 | jkPair, 748 | ikPair, 749 | amountAIn 750 | ); 751 | 752 | await tokenA.transfer(bdPool.address, amountAIn); 753 | 754 | let BDRRBefore = (await bdPool.calculateReserveRatio()).toString(); 755 | console.log(`Reserve ratio of BD pool before = ${BDRRBefore}`); 756 | 757 | await bdPool.swapReserveToNative(amountDOut, ikPair, owner.address, []); 758 | 759 | let BDRRAfter = (await bdPool.calculateReserveRatio()).toString(); 760 | console.log(`Reserve ratio of BD pool after = ${BDRRAfter}`); 761 | 762 | expect(BDRRAfter).to.equal('1006'); 763 | 764 | console.log( 765 | `Reserve base value of token A = ${( 766 | await bdPool.reservesBaseValue(tokenA.address) 767 | ).toString()}` 768 | ); 769 | console.log(`D balance = ${(await bdPool.pairBalance0()).toString()}`); 770 | console.log(`B balance = ${(await bdPool.pairBalance1()).toString()}`); 771 | }); 772 | 773 | it('Exchange reserves between AB and BD pools (A<>D)', async () => { 774 | const abPool = fixture.abPool; 775 | const bdPool = fixture.bdPool; 776 | const tokenA = fixture.tokenA; 777 | const tokenC = fixture.tokenC; 778 | const tokenD = fixture.tokenD; 779 | 780 | let amountDInReserve = await abPool.reserves(tokenD.address); 781 | 782 | let BDRRBefore = await bdPool.calculateReserveRatio(); 783 | console.log( 784 | `Reserve ratio of BD pool before = ${BDRRBefore.toString()}` 785 | ); 786 | 787 | let ABRRBefore = await abPool.calculateReserveRatio(); 788 | console.log( 789 | `Reserve ratio of AB pool before = ${ABRRBefore.toString()}` 790 | ); 791 | 792 | let reservedAinBDBefore = await bdPool.reservesBaseValue( 793 | tokenA.address 794 | ); 795 | 796 | await fixture.exchangeReserveInstance.exchange( 797 | abPool.address, //jk1 798 | bdPool.address, // ik1 799 | bdPool.address, //jk2 800 | abPool.address, //ik2 801 | amountDInReserve 802 | ); 803 | 804 | let BDRRAfter = await bdPool.calculateReserveRatio(); 805 | console.log(`Reserve ratio of BD pool after = ${BDRRAfter.toString()}`); 806 | 807 | let ABRRAfter = await abPool.calculateReserveRatio(); 808 | console.log(`Reserve ratio of AB pool after = ${ABRRAfter.toString()}`); 809 | 810 | console.log(`D balance = ${(await bdPool.pairBalance0()).toString()}`); 811 | console.log(`B balance = ${(await bdPool.pairBalance1()).toString()}`); 812 | console.log(`A balance = ${(await abPool.pairBalance0()).toString()}`); 813 | console.log(`B balance = ${(await abPool.pairBalance1()).toString()}`); 814 | 815 | let reservedAinBDAfter = await bdPool.reservesBaseValue(tokenA.address); 816 | let reservedDinAB = await abPool.reservesBaseValue(tokenD.address); 817 | 818 | console.log( 819 | `Reserve base value of token A (BD) = ${reservedAinBDAfter.toString()}` 820 | ); 821 | console.log( 822 | `Reserve base value of token D (AB) = ${reservedDinAB.toString()}` 823 | ); 824 | console.log( 825 | `Reserve base value of token C (AB) = ${( 826 | await abPool.reservesBaseValue(tokenC.address) 827 | ).toString()}` 828 | ); 829 | 830 | expect(BDRRAfter).to.lessThan(BDRRBefore); 831 | expect(ABRRAfter).to.lessThan(ABRRBefore); 832 | expect(reservedDinAB).equals('0'); 833 | expect(reservedAinBDAfter).to.lessThan(reservedAinBDBefore); 834 | }); 835 | }); 836 | -------------------------------------------------------------------------------- /test/5_exchangeReserves.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from 'chai'; 3 | import { ethers } from 'hardhat'; 4 | import { deployPools } from './fixtures/deployPools'; 5 | import _ from 'lodash'; 6 | import utils from './utils'; 7 | 8 | describe('ExchangeReserves', () => { 9 | let fixture: any = {}; 10 | 11 | before(async function () { 12 | fixture = await loadFixture(deployPools); 13 | 14 | await fixture.vPairFactoryInstance.setExchangeReservesAddress( 15 | fixture.exchageReserveInstance.address 16 | ); 17 | await fixture.bcPool.setBlocksDelay(0); 18 | await fixture.abPool.setBlocksDelay(0); 19 | }); 20 | 21 | it('Should change incentives limit', async () => { 22 | const exchangeReserves = fixture.exchageReserveInstance; 23 | const incentivesLimitBefore = 24 | await exchangeReserves.incentivesLimitPct(); 25 | await exchangeReserves.changeIncentivesLimitPct(50); 26 | const incentivesLimitAfter = 27 | await exchangeReserves.incentivesLimitPct(); 28 | expect(incentivesLimitBefore).to.equal(1); 29 | expect(incentivesLimitAfter).to.equal(50); 30 | }); 31 | 32 | it('Incentives limit can be changed only by factory admin', async () => { 33 | const exchangeReserves = fixture.exchageReserveInstance; 34 | await expect( 35 | exchangeReserves 36 | .connect(fixture.accounts[1]) 37 | .changeIncentivesLimitPct(50) 38 | ).to.revertedWith('Admin only'); 39 | }); 40 | 41 | it('Should revert when callback caller is not jkPair1', async () => { 42 | const abPool = fixture.abPool; 43 | const bdPool = fixture.bdPool; 44 | const tokenA = fixture.tokenA; 45 | const tokenB = fixture.tokenB; 46 | const owner = fixture.owner; 47 | 48 | let amountOut = ethers.utils.parseEther('100'); 49 | let data = utils.getEncodedExchangeReserveCallbackParams( 50 | abPool.address, //jk1 51 | bdPool.address, //ik1 52 | bdPool.address, //jk2 53 | abPool.address, //ik2 54 | owner.address, 55 | amountOut 56 | ); 57 | 58 | await expect( 59 | fixture.exchageReserveInstance.vFlashSwapCallback( 60 | tokenA.address, 61 | tokenB.address, 62 | amountOut, 63 | data 64 | ) 65 | ).to.revertedWith('IC'); 66 | }); 67 | 68 | it('Should add C to pool A/B', async () => { 69 | const abPool = fixture.abPool; 70 | const bcPool = fixture.bcPool; 71 | const acPool = fixture.acPool; 72 | const tokenA = fixture.tokenA; 73 | const tokenB = fixture.tokenB; 74 | const tokenC = fixture.tokenC; 75 | const owner = fixture.owner; 76 | const vRouterInstance = fixture.vRouterInstance; 77 | 78 | let amountOut = ethers.utils.parseEther('100'); 79 | 80 | let amountIn = await vRouterInstance.getVirtualAmountIn( 81 | abPool.address, 82 | bcPool.address, 83 | amountOut 84 | ); 85 | 86 | let reserveRatioBefore = await abPool.calculateReserveRatio(); 87 | let tokenCReserve = await abPool.reservesBaseValue(tokenC.address); 88 | 89 | const futureTs = await utils.getFutureBlockTimestamp(); 90 | 91 | await vRouterInstance.swapReserveTokensForExactTokens( 92 | tokenA.address, 93 | tokenB.address, 94 | bcPool.address, 95 | amountOut, 96 | amountIn, 97 | owner.address, 98 | futureTs 99 | ); 100 | 101 | let reserveRatioAfter = await abPool.calculateReserveRatio(); 102 | 103 | expect(reserveRatioBefore).to.lessThan(reserveRatioAfter); 104 | 105 | let tokenCReserveAfter = await abPool.reservesBaseValue(tokenC.address); 106 | expect(tokenCReserve).to.lessThan(tokenCReserveAfter); 107 | }); 108 | 109 | it('Should add A to pool B/C', async () => { 110 | const abPool = fixture.abPool; 111 | const bcPool = fixture.bcPool; 112 | const tokenA = fixture.tokenA; 113 | const tokenB = fixture.tokenB; 114 | const tokenC = fixture.tokenC; 115 | const owner = fixture.owner; 116 | const vRouterInstance = fixture.vRouterInstance; 117 | 118 | let amountOut = ethers.utils.parseEther('500'); 119 | 120 | let amountIn = await vRouterInstance.getVirtualAmountIn( 121 | bcPool.address, 122 | abPool.address, 123 | amountOut 124 | ); 125 | 126 | let reserveRatioBefore = await bcPool.calculateReserveRatio(); 127 | let tokenAReserve = await bcPool.reservesBaseValue(tokenA.address); 128 | const futureTs = await utils.getFutureBlockTimestamp(); 129 | 130 | await vRouterInstance.swapReserveTokensForExactTokens( 131 | tokenB.address, 132 | tokenC.address, 133 | abPool.address, 134 | amountOut, 135 | amountIn, 136 | owner.address, 137 | futureTs 138 | ); 139 | 140 | let reserveRatioAfter = await bcPool.calculateReserveRatio(); 141 | 142 | expect(reserveRatioBefore).to.lessThan(reserveRatioAfter); 143 | 144 | let tokenAReserveAfter = await bcPool.reservesBaseValue(tokenA.address); 145 | expect(tokenAReserve).to.lessThan(tokenAReserveAfter); 146 | }); 147 | 148 | it('Should exchange reserves A<>C -> A goes from B/C to A/B, C goes from A/B to B/C', async () => { 149 | const abPool = fixture.abPool; 150 | const bcPool = fixture.bcPool; 151 | const tokenA = fixture.tokenA; 152 | const tokenC = fixture.tokenC; 153 | const owner = fixture.owner; 154 | 155 | let amountAInReserve = await bcPool.reserves(tokenA.address); 156 | 157 | await tokenC.transfer(bcPool.address, ethers.utils.parseEther('10')); 158 | 159 | let aReserveInBC = await bcPool.reserves(tokenA.address); 160 | let cReserveInAB = await abPool.reserves(tokenC.address); 161 | let poolABRR = await abPool.calculateReserveRatio(); 162 | 163 | let tokenAReserveBaseValue = await bcPool.reservesBaseValue( 164 | tokenA.address 165 | ); 166 | let tokenCReserveBaseValue = await abPool.reservesBaseValue( 167 | tokenC.address 168 | ); 169 | 170 | let poolBCRR = await bcPool.calculateReserveRatio(); 171 | 172 | let balanceABefore = await tokenA.balanceOf(owner.address); 173 | 174 | //get flash swap of amount required amount C from pool BC. 175 | await fixture.exchageReserveInstance.exchange( 176 | bcPool.address, //jk1 177 | abPool.address, // ik1 178 | abPool.address, //jk2 179 | bcPool.address, // ik2 180 | amountAInReserve 181 | ); 182 | 183 | let balanceAAfter = await tokenA.balanceOf(owner.address); 184 | 185 | let tokenAReserveBaseValueAfter = await bcPool.reservesBaseValue( 186 | tokenA.address 187 | ); 188 | let tokenCReserveBaseValueAfter = await abPool.reservesBaseValue( 189 | tokenC.address 190 | ); 191 | 192 | let aReserveInBCAfter = await bcPool.reserves(tokenA.address); 193 | let cReserveInABAfter = await abPool.reserves(tokenC.address); 194 | let poolABRRAfter = await abPool.calculateReserveRatio(); 195 | 196 | let poolBCRRAfter = await bcPool.calculateReserveRatio(); 197 | 198 | // incentives received 199 | expect(balanceAAfter).to.be.above(balanceABefore); 200 | 201 | expect(aReserveInBCAfter).to.equal(0); 202 | 203 | expect(poolABRRAfter).to.lessThan(poolABRR); 204 | 205 | expect(poolBCRRAfter).to.lessThan(poolBCRR); 206 | 207 | expect(tokenAReserveBaseValueAfter).to.lessThan(tokenAReserveBaseValue); 208 | 209 | expect(aReserveInBCAfter).to.lessThan(aReserveInBC); 210 | 211 | expect(cReserveInABAfter).to.lessThan(cReserveInAB); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /test/6_reserveRatioManipulations.test.ts: -------------------------------------------------------------------------------- 1 | //#### 2 | // ## Based on https://docs.google.com/spreadsheets/d/1OW2c76WO-FvI4dp-5HB0LGUbDfv_YzVw/edit?usp=sharing&ouid=100308376099877825660&rtpof=true&sd=true 3 | //#### 4 | 5 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 6 | import { ethers } from 'hardhat'; 7 | import { reserveRatioManipulation } from './fixtures/reserveRatioManipulation'; 8 | import _ from 'lodash'; 9 | import utils from './utils'; 10 | 11 | describe('ExchangeReserves manipulation scenarios', () => { 12 | let fixture: any = {}; 13 | 14 | before(async () => { 15 | fixture = await loadFixture(reserveRatioManipulation); 16 | await fixture.abPool.setBlocksDelay(0); 17 | await fixture.bcPool.setBlocksDelay(0); 18 | }); 19 | 20 | it('Manipulation 1: manipulating pool AB in order to reduce reserve ratio (i.e. making A more expensive)', async () => { 21 | console.log('==========================================='); 22 | console.log('STEP1: Buying A and paying 300 B in pool AB'); 23 | console.log('==========================================='); 24 | 25 | await fixture.acPool.setAllowList( 26 | [fixture.tokenB.address, fixture.tokenD.address].sort((a, b) => { 27 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 28 | return -1; 29 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 30 | return 0; 31 | else return 1; 32 | }) 33 | ); 34 | 35 | let amountIn = ethers.utils.parseEther('300'); 36 | let amountOut = await fixture.vRouterInstance.getAmountOut( 37 | fixture.tokenB.address, 38 | fixture.tokenA.address, 39 | amountIn 40 | ); 41 | 42 | const futureTs = await utils.getFutureBlockTimestamp(); 43 | await fixture.vRouterInstance.swapTokensForExactTokens( 44 | [fixture.tokenB.address, fixture.tokenA.address], 45 | amountOut, 46 | amountIn, 47 | fixture.owner.address, 48 | futureTs 49 | ); 50 | 51 | console.log('==========================================='); 52 | console.log('STEP2: send 1B to pool AC'); 53 | console.log('==========================================='); 54 | 55 | const ikPair = await fixture.vPairFactoryInstance.pairs( 56 | fixture.tokenB.address, 57 | fixture.tokenA.address 58 | ); 59 | 60 | const jkPair = await fixture.vPairFactoryInstance.pairs( 61 | fixture.tokenA.address, 62 | fixture.tokenC.address 63 | ); 64 | 65 | let amountBIn = ethers.utils.parseEther('1'); 66 | let amountCOut = await fixture.vRouterInstance.getVirtualAmountOut( 67 | jkPair, 68 | ikPair, 69 | amountBIn 70 | ); 71 | 72 | console.log( 73 | 'Amount B Sent to pool: ' + utils.fromWeiToNumber(amountBIn) 74 | ); 75 | console.log( 76 | 'Amount C Received from pool ' + utils.fromWeiToNumber(amountCOut) 77 | ); 78 | const futureTs2 = await utils.getFutureBlockTimestamp(); 79 | let jkPairInstance = fixture.acPool; 80 | 81 | let rrBefore = await jkPairInstance.calculateReserveRatio(); 82 | console.log( 83 | 'reserveRatio before ' + 84 | utils.fromWeiToNumber(rrBefore) / 1000 + 85 | '%' 86 | ); 87 | 88 | await fixture.vRouterInstance.swapReserveTokensForExactTokens( 89 | fixture.tokenC.address, 90 | fixture.tokenA.address, 91 | ikPair, 92 | amountCOut, 93 | amountBIn, 94 | fixture.owner.address, 95 | futureTs2 96 | ); 97 | 98 | let reserveRatioAfter = await jkPairInstance.calculateReserveRatio(); 99 | console.log( 100 | 'reserveRatio after ' + 101 | utils.fromWeiToNumber(reserveRatioAfter) / 1000 + 102 | '%' 103 | ); 104 | }); 105 | 106 | it('Manipulation 2: manipulating pool AB in order to make B more expensive (and get more C in the trade)', async () => { 107 | console.log('==========================================='); 108 | console.log('STEP1: Buying B and paying 300A in pool AB'); 109 | console.log('==========================================='); 110 | 111 | let amountIn = ethers.utils.parseEther('300'); 112 | let amountOut = await fixture.vRouterInstance.getAmountOut( 113 | fixture.tokenA.address, 114 | fixture.tokenB.address, 115 | amountIn 116 | ); 117 | 118 | const futureTs = await utils.getFutureBlockTimestamp(); 119 | await fixture.vRouterInstance.swapTokensForExactTokens( 120 | [fixture.tokenA.address, fixture.tokenB.address], 121 | amountOut, 122 | amountIn, 123 | fixture.owner.address, 124 | futureTs 125 | ); 126 | 127 | console.log('==========================================='); 128 | console.log('STEP2: send 1B to pool AC'); 129 | console.log('==========================================='); 130 | 131 | const ikPair = await fixture.vPairFactoryInstance.pairs( 132 | fixture.tokenB.address, 133 | fixture.tokenA.address 134 | ); 135 | 136 | const jkPair = await fixture.vPairFactoryInstance.pairs( 137 | fixture.tokenA.address, 138 | fixture.tokenC.address 139 | ); 140 | 141 | let amountBIn = ethers.utils.parseEther('1'); 142 | let amountCOut = await fixture.vRouterInstance.getVirtualAmountOut( 143 | jkPair, 144 | ikPair, 145 | amountBIn 146 | ); 147 | 148 | console.log( 149 | 'Amount B Sent to pool: ' + utils.fromWeiToNumber(amountBIn) 150 | ); 151 | console.log( 152 | 'Amount C Received from pool ' + utils.fromWeiToNumber(amountCOut) 153 | ); 154 | const futureTs2 = await utils.getFutureBlockTimestamp(); 155 | let jkPairInstance = fixture.acPool; 156 | 157 | let rrBefore = await jkPairInstance.calculateReserveRatio(); 158 | console.log( 159 | 'reserveRatio before ' + 160 | utils.fromWeiToNumber(rrBefore) / 1000 + 161 | '%' 162 | ); 163 | 164 | let cBalance = await fixture.tokenC.balanceOf(jkPair); 165 | let aBalance = await fixture.tokenA.balanceOf(jkPair); 166 | let bBalance = await fixture.tokenB.balanceOf(jkPair); 167 | 168 | console.log('cBalance ' + cBalance); 169 | console.log('aBalance ' + aBalance); 170 | console.log('bBalance ' + bBalance); 171 | 172 | let vPool = await fixture.vRouterInstance.getVirtualPool( 173 | jkPair, 174 | ikPair 175 | ); 176 | console.log(JSON.stringify(vPool)); 177 | 178 | await fixture.vRouterInstance.swapReserveTokensForExactTokens( 179 | fixture.tokenC.address, 180 | fixture.tokenA.address, 181 | ikPair, 182 | amountCOut, 183 | amountBIn, 184 | fixture.owner.address, 185 | futureTs2 186 | ); 187 | 188 | let cBalanceAfter = await fixture.tokenC.balanceOf(jkPair); 189 | let aBalanceAfter = await fixture.tokenA.balanceOf(jkPair); 190 | let bBalanceAfter = await fixture.tokenB.balanceOf(jkPair); 191 | 192 | console.log('cBalanceAfter ' + cBalanceAfter); 193 | console.log('aBalanceAfter ' + aBalanceAfter); 194 | console.log('bBalanceAfter ' + bBalanceAfter); 195 | 196 | let reserveRatioAfter = await jkPairInstance.calculateReserveRatio(); 197 | console.log( 198 | 'reserveRatio after ' + 199 | utils.fromWeiToNumber(reserveRatioAfter) / 1000 + 200 | '%' 201 | ); 202 | }); 203 | 204 | it('Manipulation 3: manipulating pool AC in order to make C cheaper (and get more C in the trade)', async () => { 205 | console.log('==========================================='); 206 | console.log('STEP1: Buying A and paying 300C in pool AC'); 207 | console.log('==========================================='); 208 | 209 | let amountIn = ethers.utils.parseEther('300'); 210 | let amountOut = await fixture.vRouterInstance.getAmountOut( 211 | fixture.tokenC.address, 212 | fixture.tokenA.address, 213 | amountIn 214 | ); 215 | 216 | const futureTs = await utils.getFutureBlockTimestamp(); 217 | await fixture.vRouterInstance.swapTokensForExactTokens( 218 | [fixture.tokenC.address, fixture.tokenA.address], 219 | amountOut, 220 | amountIn, 221 | fixture.owner.address, 222 | futureTs 223 | ); 224 | 225 | console.log('==========================================='); 226 | console.log('STEP2: send 1B to pool AC'); 227 | console.log('==========================================='); 228 | 229 | const ikPair = await fixture.vPairFactoryInstance.pairs( 230 | fixture.tokenB.address, 231 | fixture.tokenA.address 232 | ); 233 | 234 | const jkPair = await fixture.vPairFactoryInstance.pairs( 235 | fixture.tokenA.address, 236 | fixture.tokenC.address 237 | ); 238 | 239 | let amountBIn = ethers.utils.parseEther('1'); 240 | let amountCOut = await fixture.vRouterInstance.getVirtualAmountOut( 241 | jkPair, 242 | ikPair, 243 | amountBIn 244 | ); 245 | 246 | console.log( 247 | 'Amount B Sent to pool: ' + utils.fromWeiToNumber(amountBIn) 248 | ); 249 | console.log( 250 | 'Amount C Received from pool ' + utils.fromWeiToNumber(amountCOut) 251 | ); 252 | const futureTs2 = await utils.getFutureBlockTimestamp(); 253 | let jkPairInstance = fixture.acPool; 254 | 255 | let rrBefore = await jkPairInstance.calculateReserveRatio(); 256 | console.log( 257 | 'reserveRatio before ' + 258 | utils.fromWeiToNumber(rrBefore) / 1000 + 259 | '%' 260 | ); 261 | 262 | let cBalance = await fixture.tokenC.balanceOf(jkPair); 263 | let aBalance = await fixture.tokenA.balanceOf(jkPair); 264 | let bBalance = await fixture.tokenB.balanceOf(jkPair); 265 | 266 | console.log('cBalance ' + cBalance); 267 | console.log('aBalance ' + aBalance); 268 | console.log('bBalance ' + bBalance); 269 | 270 | let vPool = await fixture.vRouterInstance.getVirtualPool( 271 | jkPair, 272 | ikPair 273 | ); 274 | console.log(JSON.stringify(vPool)); 275 | 276 | await fixture.vRouterInstance.swapReserveTokensForExactTokens( 277 | fixture.tokenC.address, 278 | fixture.tokenA.address, 279 | ikPair, 280 | amountCOut, 281 | amountBIn, 282 | fixture.owner.address, 283 | futureTs2 284 | ); 285 | 286 | let cBalanceAfter = await fixture.tokenC.balanceOf(jkPair); 287 | let aBalanceAfter = await fixture.tokenA.balanceOf(jkPair); 288 | let bBalanceAfter = await fixture.tokenB.balanceOf(jkPair); 289 | 290 | console.log('cBalanceAfter ' + cBalanceAfter); 291 | console.log('aBalanceAfter ' + aBalanceAfter); 292 | console.log('bBalanceAfter ' + bBalanceAfter); 293 | 294 | let reserveRatioAfter = await jkPairInstance.calculateReserveRatio(); 295 | console.log( 296 | 'reserveRatio after ' + 297 | utils.fromWeiToNumber(reserveRatioAfter) / 1000 + 298 | '%' 299 | ); 300 | }); 301 | 302 | it('Manipulation 4: manipulating pool AC in order to make C cheaper (and get more C in the trade)', async () => { 303 | console.log('==========================================='); 304 | console.log('STEP1: Buying C and paying 300A in pool AC'); 305 | console.log('==========================================='); 306 | 307 | let amountIn = ethers.utils.parseEther('300'); 308 | let amountOut = await fixture.vRouterInstance.getAmountOut( 309 | fixture.tokenA.address, 310 | fixture.tokenC.address, 311 | amountIn 312 | ); 313 | 314 | const futureTs = await utils.getFutureBlockTimestamp(); 315 | await fixture.vRouterInstance.swapTokensForExactTokens( 316 | [fixture.tokenA.address, fixture.tokenC.address], 317 | amountOut, 318 | amountIn, 319 | fixture.owner.address, 320 | futureTs 321 | ); 322 | 323 | console.log('==========================================='); 324 | console.log('STEP2: send 1B to pool AC'); 325 | console.log('==========================================='); 326 | 327 | const ikPair = await fixture.vPairFactoryInstance.pairs( 328 | fixture.tokenB.address, 329 | fixture.tokenA.address 330 | ); 331 | 332 | const jkPair = await fixture.vPairFactoryInstance.pairs( 333 | fixture.tokenA.address, 334 | fixture.tokenC.address 335 | ); 336 | 337 | let amountBIn = ethers.utils.parseEther('1'); 338 | let amountCOut = await fixture.vRouterInstance.getVirtualAmountOut( 339 | jkPair, 340 | ikPair, 341 | amountBIn 342 | ); 343 | 344 | console.log( 345 | 'Amount B Sent to pool: ' + utils.fromWeiToNumber(amountBIn) 346 | ); 347 | console.log( 348 | 'Amount C Received from pool ' + utils.fromWeiToNumber(amountCOut) 349 | ); 350 | const futureTs2 = await utils.getFutureBlockTimestamp(); 351 | let jkPairInstance = fixture.acPool; 352 | 353 | let rrBefore = await jkPairInstance.calculateReserveRatio(); 354 | console.log( 355 | 'reserveRatio before ' + 356 | utils.fromWeiToNumber(rrBefore) / 1000 + 357 | '%' 358 | ); 359 | 360 | let cBalance = await fixture.tokenC.balanceOf(jkPair); 361 | let aBalance = await fixture.tokenA.balanceOf(jkPair); 362 | let bBalance = await fixture.tokenB.balanceOf(jkPair); 363 | 364 | console.log('cBalance ' + cBalance); 365 | console.log('aBalance ' + aBalance); 366 | console.log('bBalance ' + bBalance); 367 | 368 | let vPool = await fixture.vRouterInstance.getVirtualPool( 369 | jkPair, 370 | ikPair 371 | ); 372 | console.log(JSON.stringify(vPool)); 373 | 374 | await fixture.vRouterInstance.swapReserveTokensForExactTokens( 375 | fixture.tokenC.address, 376 | fixture.tokenA.address, 377 | ikPair, 378 | amountCOut, 379 | amountBIn, 380 | fixture.owner.address, 381 | futureTs2 382 | ); 383 | 384 | let cBalanceAfter = await fixture.tokenC.balanceOf(jkPair); 385 | let aBalanceAfter = await fixture.tokenA.balanceOf(jkPair); 386 | let bBalanceAfter = await fixture.tokenB.balanceOf(jkPair); 387 | 388 | console.log('cBalanceAfter ' + cBalanceAfter); 389 | console.log('aBalanceAfter ' + aBalanceAfter); 390 | console.log('bBalanceAfter ' + bBalanceAfter); 391 | 392 | let reserveRatioAfter = await jkPairInstance.calculateReserveRatio(); 393 | console.log( 394 | 'reserveRatio after ' + 395 | utils.fromWeiToNumber(reserveRatioAfter) / 1000 + 396 | '%' 397 | ); 398 | }); 399 | }); 400 | -------------------------------------------------------------------------------- /test/ReentrancyExploiter.sol: -------------------------------------------------------------------------------- 1 | // SPDF-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import '../interfaces/IvFlashSwapCallback.sol'; 6 | import '../interfaces/IvPair.sol'; 7 | import '../types.sol'; 8 | 9 | contract ReentrancyExploiter is IvFlashSwapCallback { 10 | uint256 public constant SOME_AMOUNT = 100; 11 | 12 | struct MyCallbackData { 13 | address caller; 14 | address pool; 15 | bytes4 selector; 16 | } 17 | 18 | function vFlashSwapCallback( 19 | address tokenIn, 20 | address tokenOut, 21 | uint256 requiredBackAmount, 22 | bytes calldata data 23 | ) external override { 24 | MyCallbackData memory decodedData = abi.decode(data, (MyCallbackData)); 25 | (bool success, bytes memory result) = decodedData.pool.call( 26 | abi.encodeWithSelector( 27 | decodedData.selector, 28 | SOME_AMOUNT, 29 | tokenOut, 30 | decodedData.caller, 31 | new bytes(0) 32 | ) 33 | ); 34 | if (!success) { 35 | if (result.length == 0) revert(); 36 | assembly { 37 | revert(add(32, result), mload(result)) 38 | } 39 | } 40 | } 41 | 42 | function exploitSwapNative( 43 | address pool, 44 | address tokenOut, 45 | uint256 amountOut, 46 | address to 47 | ) external { 48 | IvPair(pool).swapNative( 49 | amountOut, 50 | tokenOut, 51 | to, 52 | abi.encode( 53 | MyCallbackData({ 54 | caller: msg.sender, 55 | pool: pool, 56 | selector: IvPair.swapNative.selector 57 | }) 58 | ) 59 | ); 60 | } 61 | 62 | function exploitSwapNativeToReserve( 63 | address pool, 64 | address tokenOut, 65 | uint256 amountOut, 66 | address to 67 | ) external { 68 | IvPair(pool).swapNativeToReserve( 69 | amountOut, 70 | tokenOut, 71 | to, 72 | abi.encode( 73 | MyCallbackData({ 74 | caller: msg.sender, 75 | pool: pool, 76 | selector: IvPair.swapNativeToReserve.selector 77 | }) 78 | ) 79 | ); 80 | } 81 | 82 | function exploitSwapReserveToNative( 83 | address pool, 84 | address ikAddress, 85 | uint256 amountOut, 86 | address to 87 | ) external { 88 | IvPair(pool).swapReserveToNative( 89 | amountOut, 90 | ikAddress, 91 | to, 92 | abi.encode( 93 | MyCallbackData({ 94 | caller: msg.sender, 95 | pool: pool, 96 | selector: IvPair.swapReserveToNative.selector 97 | }) 98 | ) 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/fixtures/deployPools.ts: -------------------------------------------------------------------------------- 1 | import { time } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { ethers } from 'hardhat'; 3 | import { 4 | ERC20PresetFixedSupply__factory, 5 | VPairFactory__factory, 6 | VPair__factory, 7 | VRouter__factory, 8 | VExchangeReserves__factory, 9 | WETH9__factory, 10 | } from '../../typechain-types/index'; 11 | 12 | // We define a fixture to reuse the same setup in every test. 13 | // We use loadFixture to run this setup once, snapshot that state, 14 | // and reset Hardhat Network to that snapshot in every test. 15 | export async function deployPools() { 16 | console.log('=================='); 17 | console.log('deployPool Fixture'); 18 | console.log('=================='); 19 | 20 | const issueAmount = ethers.utils.parseEther( 21 | '100000000000000000000000000000000000' 22 | ); 23 | 24 | // Contracts are deployed using the first signer/account by default 25 | const [ 26 | owner, 27 | account1, 28 | account2, 29 | account3, 30 | account4, 31 | account5, 32 | account6, 33 | account7, 34 | account8, 35 | account9, 36 | account10, 37 | ] = await ethers.getSigners(); 38 | 39 | const A_PRICE = 1; 40 | const B_PRICE = 3; 41 | const C_PRICE = 6; 42 | const D_PRICE = 9; 43 | 44 | const erc20ContractFactory = await new ERC20PresetFixedSupply__factory( 45 | owner 46 | ); 47 | const tokenA = await erc20ContractFactory.deploy( 48 | 'tokenA', 49 | 'A', 50 | issueAmount, 51 | owner.address 52 | ); 53 | const tokenB = await erc20ContractFactory.deploy( 54 | 'tokenB', 55 | 'B', 56 | issueAmount, 57 | owner.address 58 | ); 59 | const tokenC = await erc20ContractFactory.deploy( 60 | 'tokenC', 61 | 'C', 62 | issueAmount, 63 | owner.address 64 | ); 65 | 66 | const tokenD = await erc20ContractFactory.deploy( 67 | 'tokenD', 68 | 'D', 69 | issueAmount, 70 | owner.address 71 | ); 72 | 73 | const WETH9ContractFactory = await ethers.getContractFactory('WETH9'); 74 | const WETH9Instance = await WETH9ContractFactory.deploy(); 75 | 76 | await WETH9Instance.deposit({ value: ethers.utils.parseEther('1000') }); 77 | 78 | const vPairContractFactory = await ethers.getContractFactory( 79 | 'vPairFactory' 80 | ); 81 | const vPairFactoryInstance = await vPairContractFactory.deploy(); 82 | 83 | const vRouterContractFactory = await ethers.getContractFactory('vRouter'); 84 | const vRouterInstance = await vRouterContractFactory.deploy( 85 | vPairFactoryInstance.address, 86 | WETH9Instance.address 87 | ); 88 | 89 | const vExchangeReserveContractFactory = await ethers.getContractFactory( 90 | 'vExchangeReserves' 91 | ); 92 | const exchageReserveInstance = await vExchangeReserveContractFactory.deploy( 93 | vPairFactoryInstance.address 94 | ); 95 | 96 | const vPoolManagerFactory = await ethers.getContractFactory('vPoolManager'); 97 | const vPoolManagerInstance = await vPoolManagerFactory.deploy( 98 | vPairFactoryInstance.address 99 | ); 100 | 101 | await vPairFactoryInstance.setVPoolManagerAddress( 102 | vPoolManagerInstance.address 103 | ); 104 | 105 | await vPairFactoryInstance.setDefaultAllowList( 106 | [tokenA.address, tokenB.address, tokenC.address, tokenD.address, WETH9Instance.address].sort( 107 | (a, b) => { 108 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 109 | return -1; 110 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 111 | return 0; 112 | else return 1; 113 | } 114 | ) 115 | ); 116 | 117 | await tokenA.approve(vRouterInstance.address, issueAmount); 118 | await tokenB.approve(vRouterInstance.address, issueAmount); 119 | await tokenC.approve(vRouterInstance.address, issueAmount); 120 | await tokenD.approve(vRouterInstance.address, issueAmount); 121 | 122 | const futureTs = (await time.latest()) + 1000000; 123 | 124 | // create pool A/B with 10,000 A and equivalent B 125 | let AInput = 10000 * A_PRICE; 126 | let BInput = (B_PRICE / A_PRICE) * AInput; 127 | 128 | await vRouterInstance.addLiquidity( 129 | tokenA.address, 130 | tokenB.address, 131 | ethers.utils.parseEther(AInput.toString()), 132 | ethers.utils.parseEther(BInput.toString()), 133 | ethers.utils.parseEther(AInput.toString()), 134 | ethers.utils.parseEther(BInput.toString()), 135 | owner.address, 136 | futureTs 137 | ); 138 | 139 | // create pool A/C 140 | // create pool A/C with 10,000 A and equivalent C 141 | let CInput = (C_PRICE / A_PRICE) * AInput; 142 | await vRouterInstance.addLiquidity( 143 | tokenA.address, 144 | tokenC.address, 145 | ethers.utils.parseEther(AInput.toString()), 146 | ethers.utils.parseEther(CInput.toString()), 147 | ethers.utils.parseEther(AInput.toString()), 148 | ethers.utils.parseEther(CInput.toString()), 149 | owner.address, 150 | futureTs 151 | ); 152 | 153 | // create pool B/C 154 | // create pool B/C with 20,000 B and equivalent C 155 | BInput = 20000 * B_PRICE; 156 | CInput = (C_PRICE / B_PRICE) * BInput; 157 | await vRouterInstance.addLiquidity( 158 | tokenB.address, 159 | tokenC.address, 160 | ethers.utils.parseEther(BInput.toString()), 161 | ethers.utils.parseEther(CInput.toString()), 162 | ethers.utils.parseEther(BInput.toString()), 163 | ethers.utils.parseEther(CInput.toString()), 164 | owner.address, 165 | futureTs 166 | ); 167 | 168 | // create pool B/D 169 | // create pool B/D with 20,000 B and equivalent C 170 | BInput = 20000 * B_PRICE; 171 | let DInput = (D_PRICE / B_PRICE) * BInput; 172 | await vRouterInstance.addLiquidity( 173 | tokenB.address, 174 | tokenD.address, 175 | ethers.utils.parseEther(BInput.toString()), 176 | ethers.utils.parseEther(DInput.toString()), 177 | ethers.utils.parseEther(BInput.toString()), 178 | ethers.utils.parseEther(DInput.toString()), 179 | owner.address, 180 | futureTs 181 | ); 182 | 183 | // create pool WETH9/B 184 | let WETH9Input = 1000; 185 | BInput = 2000; 186 | 187 | await vRouterInstance.addLiquidity( 188 | WETH9Instance.address, 189 | tokenB.address, 190 | ethers.utils.parseEther(WETH9Input.toString()), 191 | ethers.utils.parseEther(BInput.toString()), 192 | ethers.utils.parseEther(WETH9Input.toString()), 193 | ethers.utils.parseEther(BInput.toString()), 194 | owner.address, 195 | futureTs 196 | ); 197 | 198 | // whitelist tokens in pools 199 | 200 | // pool 1 201 | const address1 = await vPairFactoryInstance.pairs( 202 | tokenA.address, 203 | tokenB.address 204 | ); 205 | console.log('AB address: ' + address1); 206 | const abPool = VPair__factory.connect(address1, owner); 207 | 208 | // whitelist token C 209 | // await abPool.setAllowList([tokenC.address]); 210 | 211 | const reserve0Pool1 = await abPool.pairBalance0(); 212 | const reserve1Pool1 = await abPool.pairBalance1(); 213 | 214 | const pool1Reserve0 = ethers.utils.formatEther(reserve0Pool1); 215 | const pool1Reserve1 = ethers.utils.formatEther(reserve1Pool1); 216 | 217 | console.log('pool1: A/B: ' + pool1Reserve0 + '/' + pool1Reserve1); 218 | 219 | // pool 2 220 | const address2 = await vPairFactoryInstance.pairs( 221 | tokenA.address, 222 | tokenC.address 223 | ); 224 | console.log('AC address: ' + address2); 225 | const acPool = VPair__factory.connect(address2, owner); 226 | 227 | // whitelist token B 228 | // await acPool.setAllowList([tokenB.address]); 229 | 230 | const reserve0Pool2 = await acPool.pairBalance0(); 231 | const reserve1Pool2 = await acPool.pairBalance1(); 232 | 233 | const pool2Reserve0 = ethers.utils.formatEther(reserve0Pool2); 234 | const pool2Reserve1 = ethers.utils.formatEther(reserve1Pool2); 235 | 236 | console.log('pool2: A/C: ' + pool2Reserve0 + '/' + pool2Reserve1); 237 | 238 | // pool 3 239 | const address3 = await vPairFactoryInstance.pairs( 240 | tokenB.address, 241 | tokenC.address 242 | ); 243 | console.log('BC address: ' + address3); 244 | const bcPool = VPair__factory.connect(address3, owner); 245 | 246 | // whitelist token A 247 | // await bcPool.setAllowList([tokenA.address]); 248 | 249 | const reserve0Pool3 = await bcPool.pairBalance0(); 250 | const reserve1Pool3 = await bcPool.pairBalance1(); 251 | 252 | const pool3Reserve0 = ethers.utils.formatEther(reserve0Pool3); 253 | const pool3Reserve1 = ethers.utils.formatEther(reserve1Pool3); 254 | 255 | console.log('pool3: B/C: ' + pool3Reserve0 + '/' + pool3Reserve1); 256 | 257 | // pool 4 258 | const address4 = await vPairFactoryInstance.pairs( 259 | tokenD.address, 260 | tokenB.address 261 | ); 262 | console.log('AB address: ' + address1); 263 | const bdPool = VPair__factory.connect(address1, owner); 264 | 265 | // whitelist token C 266 | //await bdPool.setAllowList([tokenC.address]); 267 | 268 | const reserve0Pool4 = await bdPool.pairBalance0(); 269 | const reserve1Pool4 = await bdPool.pairBalance1(); 270 | 271 | const pool4Reserve0 = ethers.utils.formatEther(reserve0Pool4); 272 | const pool4Reserve1 = ethers.utils.formatEther(reserve1Pool4); 273 | 274 | console.log('pool4: B/D: ' + pool4Reserve0 + '/' + pool4Reserve1); 275 | 276 | const address5 = await vPairFactoryInstance.pairs( 277 | WETH9Instance.address, 278 | tokenB.address 279 | ); 280 | console.log('WETH9B address: ' + address5); 281 | const wbPool = VPair__factory.connect(address5, owner); 282 | const reserve0Pool5 = await wbPool.pairBalance0(); 283 | const reserve1Pool5 = await wbPool.pairBalance1(); 284 | 285 | const pool5Reserve0 = ethers.utils.formatEther(reserve0Pool5); 286 | const pool5Reserve1 = ethers.utils.formatEther(reserve1Pool5); 287 | 288 | console.log('pool5: WETH9/B: ' + pool5Reserve0 + '/' + pool5Reserve1); 289 | 290 | return { 291 | tokenA, 292 | tokenB, 293 | tokenC, 294 | tokenD, 295 | A_PRICE, 296 | B_PRICE, 297 | C_PRICE, 298 | D_PRICE, 299 | abPool, 300 | bcPool, 301 | acPool, 302 | bdPool, 303 | wbPool, 304 | pool1Reserve0, 305 | pool1Reserve1, 306 | pool2Reserve0, 307 | pool2Reserve1, 308 | pool3Reserve0, 309 | pool3Reserve1, 310 | vRouterInstance, 311 | owner, 312 | accounts: [ 313 | account1, 314 | account2, 315 | account3, 316 | account4, 317 | account5, 318 | account6, 319 | account7, 320 | account8, 321 | account9, 322 | account10, 323 | ], 324 | vPairFactoryInstance, 325 | exchageReserveInstance, 326 | WETH9Instance, 327 | }; 328 | } 329 | -------------------------------------------------------------------------------- /test/fixtures/reserveRatioManipulation.ts: -------------------------------------------------------------------------------- 1 | import { time } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { ethers } from 'hardhat'; 3 | import { 4 | ERC20PresetFixedSupply__factory, 5 | VPairFactory__factory, 6 | VPair__factory, 7 | VRouter__factory, 8 | VExchangeReserves__factory, 9 | WETH9__factory, 10 | } from '../../typechain-types/index'; 11 | 12 | // We define a fixture to reuse the same setup in every test. 13 | // We use loadFixture to run this setup once, snapshot that state, 14 | // and reset Hardhat Network to that snapshot in every test. 15 | export async function reserveRatioManipulation() { 16 | console.log('=================='); 17 | console.log('Reserve ratio manipulation fixture'); 18 | console.log('=================='); 19 | 20 | const issueAmount = ethers.utils.parseEther( 21 | '100000000000000000000000000000000000' 22 | ); 23 | 24 | // Contracts are deployed using the first signer/account by default 25 | const [owner] = await ethers.getSigners(); 26 | 27 | const A_PRICE = 1; 28 | const B_PRICE = 3; 29 | const C_PRICE = 6; 30 | const D_PRICE = 9; 31 | 32 | const erc20ContractFactory = await new ERC20PresetFixedSupply__factory( 33 | owner 34 | ); 35 | const tokenA = await erc20ContractFactory.deploy( 36 | 'tokenA', 37 | 'A', 38 | issueAmount, 39 | owner.address 40 | ); 41 | const tokenB = await erc20ContractFactory.deploy( 42 | 'tokenB', 43 | 'B', 44 | issueAmount, 45 | owner.address 46 | ); 47 | const tokenC = await erc20ContractFactory.deploy( 48 | 'tokenC', 49 | 'C', 50 | issueAmount, 51 | owner.address 52 | ); 53 | 54 | const tokenD = await erc20ContractFactory.deploy( 55 | 'tokenD', 56 | 'D', 57 | issueAmount, 58 | owner.address 59 | ); 60 | 61 | const WETH9ContractFactory = await ethers.getContractFactory('WETH9'); 62 | const WETH9Instance = await WETH9ContractFactory.deploy(); 63 | 64 | const vPairContractFactory = await ethers.getContractFactory( 65 | 'vPairFactory' 66 | ); 67 | const vPairFactoryInstance = await vPairContractFactory.deploy(); 68 | 69 | const vPoolManagerFactory = await ethers.getContractFactory('vPoolManager'); 70 | const vPoolManagerInstance = await vPoolManagerFactory.deploy( 71 | vPairFactoryInstance.address 72 | ); 73 | 74 | await vPairFactoryInstance.setVPoolManagerAddress( 75 | vPoolManagerInstance.address 76 | ); 77 | 78 | await vPairFactoryInstance.setDefaultAllowList( 79 | [tokenA.address, tokenB.address, tokenC.address, tokenD.address].sort( 80 | (a, b) => { 81 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 82 | return -1; 83 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 84 | return 0; 85 | else return 1; 86 | } 87 | ) 88 | ); 89 | 90 | const vRouterContractFactory = await ethers.getContractFactory('vRouter'); 91 | const vRouterInstance = await vRouterContractFactory.deploy( 92 | vPairFactoryInstance.address, 93 | WETH9Instance.address 94 | ); 95 | 96 | await tokenA.approve(vRouterInstance.address, issueAmount); 97 | await tokenB.approve(vRouterInstance.address, issueAmount); 98 | await tokenC.approve(vRouterInstance.address, issueAmount); 99 | await tokenD.approve(vRouterInstance.address, issueAmount); 100 | 101 | const futureTs = (await time.latest()) + 1000000; 102 | 103 | await vRouterInstance.addLiquidity( 104 | tokenA.address, 105 | tokenB.address, 106 | ethers.utils.parseEther('100'), 107 | ethers.utils.parseEther('100'), 108 | ethers.utils.parseEther('100'), 109 | ethers.utils.parseEther('100'), 110 | owner.address, 111 | futureTs 112 | ); 113 | 114 | //create pool A/C 115 | //create pool A/B with 10,000 A and equivalent C 116 | 117 | await vRouterInstance.addLiquidity( 118 | tokenA.address, 119 | tokenC.address, 120 | ethers.utils.parseEther('50'), 121 | ethers.utils.parseEther('200'), 122 | ethers.utils.parseEther('50'), 123 | ethers.utils.parseEther('200'), 124 | owner.address, 125 | futureTs 126 | ); 127 | 128 | //create pool B/C 129 | //create pool B/C with 10,000 B and equivalent C 130 | 131 | await vRouterInstance.addLiquidity( 132 | tokenB.address, 133 | tokenC.address, 134 | ethers.utils.parseEther('50'), 135 | ethers.utils.parseEther('200'), 136 | ethers.utils.parseEther('50'), 137 | ethers.utils.parseEther('200'), 138 | owner.address, 139 | futureTs 140 | ); 141 | 142 | //whitelist tokens in pools 143 | 144 | //pool 1 145 | const abAddress = await vPairFactoryInstance.pairs( 146 | tokenA.address, 147 | tokenB.address 148 | ); 149 | 150 | const abPool = VPair__factory.connect(abAddress, owner); 151 | 152 | // whitelist token C 153 | await abPool.setMaxReserveThreshold(ethers.utils.parseEther('100000')); 154 | //whitelist token C 155 | await abPool.setAllowList( 156 | [tokenC.address, tokenD.address].sort((a, b) => { 157 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 158 | return -1; 159 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 160 | return 0; 161 | else return 1; 162 | }) 163 | ); 164 | 165 | //pool 2 166 | const acAddress = await vPairFactoryInstance.pairs( 167 | tokenA.address, 168 | tokenC.address 169 | ); 170 | const acPool = VPair__factory.connect(acAddress, owner); 171 | 172 | //whitelist token B 173 | await acPool.setAllowList( 174 | [tokenB.address, tokenD.address].sort((a, b) => { 175 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 176 | return -1; 177 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 178 | return 0; 179 | else return 1; 180 | }) 181 | ); 182 | await acPool.setMaxReserveThreshold(ethers.utils.parseEther('100000')); 183 | 184 | //pool 3 185 | const bcAddress = await vPairFactoryInstance.pairs( 186 | tokenB.address, 187 | tokenC.address 188 | ); 189 | const bcPool = VPair__factory.connect(acAddress, owner); 190 | 191 | //whitelist token A 192 | await bcPool.setAllowList( 193 | [tokenA.address, tokenD.address].sort((a, b) => { 194 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 195 | return -1; 196 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 197 | return 0; 198 | else return 1; 199 | }) 200 | ); 201 | await bcPool.setMaxReserveThreshold(ethers.utils.parseEther('100000')); 202 | 203 | // console.log("pool3: B/C: " + reserve0Pool3 + "/" + reserve1Pool3); 204 | 205 | return { 206 | tokenA, 207 | tokenB, 208 | tokenC, 209 | tokenD, 210 | A_PRICE, 211 | B_PRICE, 212 | C_PRICE, 213 | D_PRICE, 214 | abPool, 215 | bcPool, 216 | acPool, 217 | vRouterInstance, 218 | owner, 219 | vPairFactoryInstance, 220 | }; 221 | } 222 | -------------------------------------------------------------------------------- /test/fixtures/sameValues.ts: -------------------------------------------------------------------------------- 1 | import { time } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { ethers } from 'hardhat'; 3 | import { 4 | ERC20PresetFixedSupply__factory, 5 | VPairFactory__factory, 6 | VPair__factory, 7 | VRouter__factory, 8 | VExchangeReserves__factory, 9 | WETH9__factory, 10 | } from '../../typechain-types/index'; 11 | 12 | // We define a fixture to reuse the same setup in every test. 13 | // We use loadFixture to run this setup once, snapshot that state, 14 | // and reset Hardhat Network to that snapshot in every test. 15 | export async function sameValues() { 16 | console.log('=================='); 17 | console.log('Reserve ratio manipulation fixture'); 18 | console.log('=================='); 19 | 20 | const issueAmount = ethers.utils.parseEther( 21 | '10000000000000000000000000000000000000000000000' 22 | ); 23 | 24 | // Contracts are deployed using the first signer/account by default 25 | const [owner] = await ethers.getSigners(); 26 | 27 | const A_PRICE = 1; 28 | const B_PRICE = 1; 29 | const C_PRICE = 1; 30 | const D_PRICE = 1; 31 | 32 | const erc20ContractFactory = await new ERC20PresetFixedSupply__factory( 33 | owner 34 | ); 35 | const tokenA = await erc20ContractFactory.deploy( 36 | 'tokenA', 37 | 'A', 38 | issueAmount, 39 | owner.address 40 | ); 41 | const tokenB = await erc20ContractFactory.deploy( 42 | 'tokenB', 43 | 'B', 44 | issueAmount, 45 | owner.address 46 | ); 47 | const tokenC = await erc20ContractFactory.deploy( 48 | 'tokenC', 49 | 'C', 50 | issueAmount, 51 | owner.address 52 | ); 53 | 54 | const tokenD = await erc20ContractFactory.deploy( 55 | 'tokenD', 56 | 'D', 57 | issueAmount, 58 | owner.address 59 | ); 60 | 61 | const WETH9ContractFactory = await ethers.getContractFactory('WETH9'); 62 | const WETH9Instance = await WETH9ContractFactory.deploy(); 63 | 64 | const vPairContractFactory = await ethers.getContractFactory( 65 | 'vPairFactory' 66 | ); 67 | const vPairFactoryInstance = await vPairContractFactory.deploy(); 68 | 69 | const vPoolManagerFactory = await ethers.getContractFactory('vPoolManager'); 70 | const vPoolManagerInstance = await vPoolManagerFactory.deploy( 71 | vPairFactoryInstance.address 72 | ); 73 | 74 | await vPairFactoryInstance.setVPoolManagerAddress( 75 | vPoolManagerInstance.address 76 | ); 77 | 78 | await vPairFactoryInstance.setDefaultAllowList( 79 | [tokenA.address, tokenB.address, tokenC.address, tokenD.address].sort( 80 | (a, b) => { 81 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 82 | return -1; 83 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 84 | return 0; 85 | else return 1; 86 | } 87 | ) 88 | ); 89 | 90 | const vRouterContractFactory = await ethers.getContractFactory('vRouter'); 91 | const vRouterInstance = await vRouterContractFactory.deploy( 92 | vPairFactoryInstance.address, 93 | WETH9Instance.address 94 | ); 95 | 96 | const vExchangeReserveContractFactory = await ethers.getContractFactory( 97 | 'vExchangeReserves' 98 | ); 99 | const exchangeReserveInstance = 100 | await vExchangeReserveContractFactory.deploy( 101 | vPairFactoryInstance.address 102 | ); 103 | 104 | await tokenA.approve(vRouterInstance.address, issueAmount); 105 | await tokenB.approve(vRouterInstance.address, issueAmount); 106 | await tokenC.approve(vRouterInstance.address, issueAmount); 107 | await tokenD.approve(vRouterInstance.address, issueAmount); 108 | 109 | const futureTs = (await time.latest()) + 1000000; 110 | 111 | await vRouterInstance.addLiquidity( 112 | tokenA.address, 113 | tokenB.address, 114 | ethers.utils.parseEther('50000000'), 115 | ethers.utils.parseEther('50000000'), 116 | ethers.utils.parseEther('50000000'), 117 | ethers.utils.parseEther('50000000'), 118 | owner.address, 119 | futureTs 120 | ); 121 | 122 | //create pool A/C 123 | //create pool A/B with 10,000 A and equivalent C 124 | 125 | await vRouterInstance.addLiquidity( 126 | tokenA.address, 127 | tokenC.address, 128 | ethers.utils.parseEther('50000000'), 129 | ethers.utils.parseEther('50000000'), 130 | ethers.utils.parseEther('50000000'), 131 | ethers.utils.parseEther('50000000'), 132 | owner.address, 133 | futureTs 134 | ); 135 | 136 | //create pool B/C 137 | //create pool B/C with 10,000 B and equivalent C 138 | 139 | await vRouterInstance.addLiquidity( 140 | tokenB.address, 141 | tokenC.address, 142 | ethers.utils.parseEther('50000000'), 143 | ethers.utils.parseEther('50000000'), 144 | ethers.utils.parseEther('50000000'), 145 | ethers.utils.parseEther('50000000'), 146 | owner.address, 147 | futureTs 148 | ); 149 | 150 | await vRouterInstance.addLiquidity( 151 | tokenB.address, 152 | tokenD.address, 153 | ethers.utils.parseEther('50000000'), 154 | ethers.utils.parseEther('50000000'), 155 | ethers.utils.parseEther('50000000'), 156 | ethers.utils.parseEther('50000000'), 157 | owner.address, 158 | futureTs 159 | ); 160 | 161 | //whitelist tokens in pools 162 | 163 | //pool 1 164 | const abAddress = await vPairFactoryInstance.pairs( 165 | tokenA.address, 166 | tokenB.address 167 | ); 168 | 169 | const abPool = VPair__factory.connect(abAddress, owner); 170 | 171 | await abPool.setMaxReserveThreshold(2000); 172 | //whitelist token C 173 | await abPool.setAllowList( 174 | [tokenC.address, tokenD.address].sort((a, b) => { 175 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 176 | return -1; 177 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 178 | return 0; 179 | else return 1; 180 | }) 181 | ); 182 | 183 | //pool 2 184 | const acAddress = await vPairFactoryInstance.pairs( 185 | tokenA.address, 186 | tokenC.address 187 | ); 188 | const acPool = VPair__factory.connect(acAddress, owner); 189 | 190 | //whitelist token B 191 | await acPool.setAllowList( 192 | [tokenB.address, tokenD.address].sort((a, b) => { 193 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 194 | return -1; 195 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 196 | return 0; 197 | else return 1; 198 | }) 199 | ); 200 | await acPool.setMaxReserveThreshold(2000); 201 | 202 | //pool 3 203 | const bcAddress = await vPairFactoryInstance.pairs( 204 | tokenB.address, 205 | tokenC.address 206 | ); 207 | const bcPool = VPair__factory.connect(bcAddress, owner); 208 | 209 | //whitelist token A 210 | await bcPool.setAllowList( 211 | [tokenA.address, tokenD.address].sort((a, b) => { 212 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 213 | return -1; 214 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 215 | return 0; 216 | else return 1; 217 | }) 218 | ); 219 | await bcPool.setMaxReserveThreshold(2000); 220 | 221 | // pool 4 222 | const bdAddress = await vPairFactoryInstance.pairs( 223 | tokenB.address, 224 | tokenD.address 225 | ); 226 | const bdPool = VPair__factory.connect(bdAddress, owner); 227 | 228 | //whitelist token A 229 | await bdPool.setAllowList( 230 | [tokenA.address, tokenC.address].sort((a, b) => { 231 | if (ethers.BigNumber.from(a).lt(ethers.BigNumber.from(b))) 232 | return -1; 233 | else if (ethers.BigNumber.from(a).eq(ethers.BigNumber.from(b))) 234 | return 0; 235 | else return 1; 236 | }) 237 | ); 238 | await bdPool.setMaxReserveThreshold(2000); 239 | 240 | // console.log("pool3: B/C: " + reserve0Pool3 + "/" + reserve1Pool3); 241 | 242 | return { 243 | tokenA, 244 | tokenB, 245 | tokenC, 246 | tokenD, 247 | A_PRICE, 248 | B_PRICE, 249 | C_PRICE, 250 | D_PRICE, 251 | abPool, 252 | bcPool, 253 | acPool, 254 | bdPool, 255 | vRouterInstance, 256 | owner, 257 | vPairFactoryInstance, 258 | exchangeReserveInstance, 259 | }; 260 | } 261 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | const abi = ethers.utils.defaultAbiCoder; 3 | import { time } from '@nomicfoundation/hardhat-network-helpers'; 4 | 5 | export default { 6 | fromWeiToNumber: function (number: any) { 7 | return parseFloat( 8 | parseFloat(ethers.utils.formatEther(number.toString())).toFixed(6) 9 | ); 10 | }, 11 | getFutureBlockTimestamp: async function () { 12 | return (await time.latest()) + 1000000; 13 | }, 14 | getEncodedExchangeReserveCallbackParams: function ( 15 | jkPair1: any, 16 | ikPair1: any, 17 | jkPair2: any, 18 | ikPair2: any, 19 | caller: any, 20 | flashAmountOut: any 21 | ) { 22 | return abi.encode( 23 | ['address', 'address', 'address', 'address', 'address', 'uint256'], 24 | [jkPair1, ikPair1, jkPair2, ikPair2, caller, flashAmountOut] 25 | ); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | }, 10 | "include": ["./test", "./scripts"],//only the "scripts" part. 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | --------------------------------------------------------------------------------