├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── README.md ├── audits ├── Certik-Audit.pdf ├── Halborn-Audit.pdf ├── Salusec-Audit.pdf ├── Veridise-Audit.pdf └── range-veridise-vault-diff-audit.pdf ├── contracts ├── RangeProtocolFactory.sol ├── RangeProtocolVault.sol ├── RangeProtocolVaultStorage.sol ├── access │ └── OwnableUpgradeable.sol ├── errors │ ├── FactoryErrors.sol │ └── VaultErrors.sol ├── interfaces │ ├── IRangeProtocolFactory.sol │ └── IRangeProtocolVault.sol ├── mock │ ├── FixedPoint96.sol │ ├── FullMath.sol │ ├── LowGasSafeMath.sol │ ├── MockERC20.sol │ ├── MockLiquidityAmounts.sol │ ├── MockSqrtPriceMath.sol │ ├── SafeCast.sol │ ├── SwapTest.sol │ └── UnsafeMath.sol └── uniswap │ ├── FullMath.sol │ ├── LiquidityAmounts.sol │ └── TickMath.sol ├── deploy ├── config.json ├── hot-deployment │ ├── factory.ts │ ├── implementation.ts │ └── vault.ts ├── ledger │ ├── RangeProtocolFactory.deploy.ts │ ├── RangeProtocolVault.implementation.deploy.ts │ └── RangeProtocolVault.proxy.deploy.ts └── mock │ └── dummy-token.ts ├── hardhat.config.ts ├── package.json ├── scripts ├── UpgradeImplementation.ts ├── deploy-vault.ts ├── timelock-execute-update-owner.ts ├── timelock-schedule-update-owner.ts └── updateOwner.ts ├── test ├── RangeProtocolFactory.test.ts ├── RangeProtocolVault.exposure.test.ts ├── RangeProtocolVault.test.ts └── common.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | cache 2 | coverage 3 | coverage 4 | dist 5 | typechain 6 | 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": ["eslint:recommended", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": 12 11 | }, 12 | // Typescript config 13 | "overrides": [ 14 | { 15 | "files": ["*.ts"], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { "project": "./tsconfig.json" }, 18 | "plugins": ["@typescript-eslint", "prettier"], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:@typescript-eslint/eslint-recommended", 23 | "prettier" 24 | ], 25 | "rules": { 26 | "prettier/prettier": "error", 27 | "@typescript-eslint/no-unused-vars": "error", 28 | "@typescript-eslint/no-explicit-any": "warn", 29 | "@typescript-eslint/naming-convention": [ 30 | "error", 31 | { 32 | "selector": "default", 33 | "format": ["camelCase"] 34 | }, 35 | { 36 | "selector": "variable", 37 | "format": ["camelCase", "UPPER_CASE"] 38 | }, 39 | { 40 | "selector": "parameter", 41 | "format": ["camelCase"], 42 | "leadingUnderscore": "allow" 43 | }, 44 | { 45 | "selector": ["objectLiteralProperty"], 46 | "format": ["camelCase", "PascalCase"] 47 | }, 48 | { 49 | "selector": ["classProperty"], 50 | "modifiers": ["private", "static"], 51 | "format": ["PascalCase", "UPPER_CASE"] 52 | }, 53 | { 54 | "selector": ["classProperty", "classMethod"], 55 | "modifiers": ["private"], 56 | "format": ["camelCase"], 57 | "leadingUnderscore": "require" 58 | }, 59 | { 60 | "selector": ["classProperty", "classMethod"], 61 | "modifiers": ["protected"], 62 | "format": ["camelCase"], 63 | "leadingUnderscore": "require" 64 | }, 65 | { 66 | "selector": "typeLike", 67 | "format": ["PascalCase"] 68 | }, 69 | { 70 | "selector": ["enumMember"], 71 | "format": ["camelCase", "PascalCase"] 72 | }, 73 | { 74 | "selector": "variable", 75 | "types": ["boolean"], 76 | "format": ["PascalCase"], 77 | "prefix": ["is", "should", "has", "can", "did", "will"] 78 | } 79 | ] 80 | } 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # local env variables 5 | .env 6 | 7 | # coverage 8 | coverage 9 | .nyc_output 10 | coverage.json 11 | 12 | # cache 13 | .eslintcache 14 | 15 | # hardhat 16 | artifacts 17 | cache 18 | 19 | # macOS 20 | .DS_Store 21 | *.icloud 22 | 23 | # redis 24 | dump.rdb 25 | 26 | # typechain 27 | typechain 28 | 29 | # typescript 30 | dist 31 | 32 | # VS Code 33 | .vscode 34 | 35 | # idea 36 | .idea 37 | .husky 38 | 39 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | dist 3 | cache 4 | coverage 5 | coverage.json 6 | typechain 7 | uniswap 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.sol", 5 | "options": { 6 | "printWidth": 100, 7 | "tabWidth": 4, 8 | "useTabs": false, 9 | "singleQuote": false, 10 | "bracketSpacing": false 11 | } 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | "__mocks__", 4 | "interfaces", 5 | "uniswap", 6 | "node_modules", 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "code-complexity": ["error", 5], 7 | "function-max-lines": ["error", 100], 8 | "max-line-length": ["error", 130], 9 | "max-states-count": ["warn", 20], 10 | "no-empty-blocks": "off", 11 | "no-unused-vars": "error", 12 | "payable-fallback": "off", 13 | "reason-string": ["off", { "maxLength": 32 }], 14 | "constructor-syntax": "off", 15 | "comprehensive-interface": "off", 16 | "quotes": ["error", "double"], 17 | "const-name-snakecase": "off", 18 | "contract-name-camelcase": "error", 19 | "event-name-camelcase": "error", 20 | "func-name-mixedcase": "error", 21 | "func-param-name-mixedcase": "error", 22 | "modifier-name-mixedcase": "error", 23 | "private-vars-leading-underscore": "off", 24 | "var-name-mixedcase": "error", 25 | "imports-on-top": "error", 26 | "ordering": "off", 27 | "func-order": "error", 28 | "visibility-modifier-order": "error", 29 | "avoid-call-value": "off", 30 | "avoid-low-level-calls": "off", 31 | "avoid-sha3": "error", 32 | "avoid-suicide": "error", 33 | "avoid-throw": "error", 34 | "avoid-tx-origin": "off", 35 | "check-send-result": "error", 36 | "compiler-version": ["error", "0.8.4"], 37 | "mark-callable-contracts": "off", 38 | "func-visibility": ["error", { "ignoreConstructors": true }], 39 | "multiple-sends": "error", 40 | "no-complex-fallback": "error", 41 | "no-inline-assembly": "off", 42 | "not-rely-on-block-hash": "error", 43 | "not-rely-on-time": "error", 44 | "reentrancy": "error", 45 | "state-visibility": "error" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Range Protocol 2 | 3 | # Overview 4 | 5 | Range Protocol is a Uniswap V2-like interface which enables providing fungible liquidity to Uniswap V3 for arbitrary liquidity provision: one-sided, lop-sided, and balanced 6 | 7 | [Range Protocol](https://www.rangeprotocol.com/) is a Uniswap V3 liquidity provision system consisting of: 8 | - [RangeProtocolFactory.sol](https://github.com/Range-Protocol/contracts/blob/master/contracts/RangeProtocolFactory.sol) contract that allows creating of Range Protocol vaults. It creates `ERC1967` proxies in front of provided implementation contracts. 9 | - [RangeProtocolVault.sol](https://github.com/Range-Protocol/contracts/blob/master/contracts/RangeProtocolVault.sol) contract that allows Uniswap liquidity provision through `mint` and `addLiquidity` functions. It's an upgradeable contract and implements Openzeppelin's `UUPSUpgradeable` proxy pattern. 10 | - [RangeProtocolVaultStorage.sol](https://github.com/Range-Protocol/contracts/blob/master/contracts/RangeProtocolVaultStorage.sol) contract for storing storage variables for `RangeProtocolVault` contract. 11 | - [Ownable.sol](https://github.com/Range-Protocol/range-protocol-vault/blob/main/contracts/abstract/Ownable.sol) contract for managing the `manager` role. 12 | The Range Protocol operates as follows: 13 | - A factory contract is deployed by a factory manager. 14 | - The factory manager creates a vault for Uniswap V3 pair providing `token0`, `token1` and `fee` for the pair along with the `implementation` and `initialize data` specific to implementation. 15 | - The minting on the vault contract is not started until the vault manager calls `updateTicks` on the vault and provides the tick range for liquidity provision. Updating the ticks starts the minting process and changes pool status to `in the position`. 16 | - Anyone wanting to mint calls `getMintAmounts` with `token0` and `token1` they want to provide liquidity with and the function returns the `mintAmount` to be provided to `mint` function for liquidity into the vault's current ticks. This mints fungible vault shares to mint representing their share of the vault. 17 | - Anyone wanting to exit from vault can call `burn` function with the amount of owned vault shares they want to burn. This burns portion of active liquidity from Uniswap V3 pool equivalent to user's share of the pool and returns user the resulting token amounts along with the user's share from inactive liquidity (fees collected + unprovisioned liquidity) from the vault. 18 | - At the times of high volatility, vault manager can remove liquidity from current tick range making all the vault liquidity inactive. The vault's status is changed to `out of the position` yet minting continues based on the `token0` and `token1` ratio in the pool and users are minted vault shares based on this ratio. If the total supply goes to zero while the pool is `out of the position` then minting is stopped since at that point there will be no reference ratio to mint vault shares based upon. The vault must update the ticks to start accepting liquidity into a newer tick range. 19 | - Vault manager can perform swap between `token0` and `token1` to convert assets to a specific ratio using `swap` function for providing liquidity to newer tick range through `addLiquidity` function. 20 | - Part of collected fee from Uniswap V3 pool is provided to vault manager as performance fee and part of notional amount is deducted from redeeming user as managing fee. 21 | - Vault manager can update the managing and performance fee, managing fee is capped at 1% and performance fee is capped at 20%. 22 | - Vault manager can pause and unpause the mint and burn function of the vault contract. 23 | 24 | ### Fee Mechanism 25 | There are two types of fees i.e. performance fee and managing fee. Performance fee will be capped at 10% (1000 BPS) and at the time of vault initialisation, it will be set to 250 BPS (2.5%). The managing fee at the time of vault initialisation will be set to 0%, but it can be set up to 1% (100 BPS). Both of these fees are credited to state variables of `managerBalance0` and `managerBalance1`. 26 | 27 | The performance fee will be applied to directly all the fees collected from Uniswap v3 pool. For example, if 1000 of token0 and 500 of token1 are collected in fees and performance fee is 250 BPS (2.5%) then the fee credited to manager in token0 is 1000 * (250 / 10000) = 25 and in token1 is 500 * (250 / 1000) = 12.5. 28 | 29 | The managing fee will be applied on the notional value of the equity tokens being burned. For example, after burning equity tokens the amount of token0 and token1 shares calculated for the exiting user is 2000 of token0 and 1500 of token1, and the managing fee is 0.5% (50 BPS) then the fee credited to manager in token0 is 2000 * (50 / 10000) = 10 and in token1 is 1500 * (50 / 10000) = 7.5 30 | 31 | # Tests 32 | 33 | To build the project from a fresh `git clone`, perform the following. 34 | 1. Install dependencies using `npm install`. 35 | 2. Run the test cases using `npx hardhat test`. -------------------------------------------------------------------------------- /audits/Certik-Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Range-Protocol/contracts/4fdade9941fcd4e9d441f2fc3c51a852e4783936/audits/Certik-Audit.pdf -------------------------------------------------------------------------------- /audits/Halborn-Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Range-Protocol/contracts/4fdade9941fcd4e9d441f2fc3c51a852e4783936/audits/Halborn-Audit.pdf -------------------------------------------------------------------------------- /audits/Salusec-Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Range-Protocol/contracts/4fdade9941fcd4e9d441f2fc3c51a852e4783936/audits/Salusec-Audit.pdf -------------------------------------------------------------------------------- /audits/Veridise-Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Range-Protocol/contracts/4fdade9941fcd4e9d441f2fc3c51a852e4783936/audits/Veridise-Audit.pdf -------------------------------------------------------------------------------- /audits/range-veridise-vault-diff-audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Range-Protocol/contracts/4fdade9941fcd4e9d441f2fc3c51a852e4783936/audits/range-veridise-vault-diff-audit.pdf -------------------------------------------------------------------------------- /contracts/RangeProtocolFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 6 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 7 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 8 | import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; 9 | import {IRangeProtocolFactory} from "./interfaces/IRangeProtocolFactory.sol"; 10 | import {FactoryErrors} from "./errors/FactoryErrors.sol"; 11 | 12 | /** 13 | * @dev Mars@RangeProtocol 14 | * @notice RangeProtocolFactory deploys and upgrades proxies for Range Protocol vault contracts. 15 | * Owner can deploy and upgrade vault contracts. 16 | */ 17 | contract RangeProtocolFactory is IRangeProtocolFactory, Ownable { 18 | bytes4 public constant INIT_SELECTOR = 19 | bytes4(keccak256(bytes("initialize(address,int24,bytes)"))); 20 | 21 | bytes4 public constant UPGRADE_SELECTOR = bytes4(keccak256(bytes("upgradeTo(address)"))); 22 | 23 | /// @notice Uniswap v3 factory 24 | address public immutable factory; 25 | 26 | /// @notice all deployed vault instances 27 | address[] private _vaultsList; 28 | 29 | constructor(address _uniswapV3Factory) Ownable() { 30 | factory = _uniswapV3Factory; 31 | } 32 | 33 | // @notice createVault creates a ERC1967 proxy instance for the given implementation of vault contract 34 | // @param tokenA one of the tokens in the uniswap pair 35 | // @param tokenB the other token in the uniswap pair 36 | // @param fee fee tier of the uniswap pair 37 | // @param implementation address of the implementation 38 | // @param configData additional data associated with the specific implementation of vault 39 | function createVault( 40 | address tokenA, 41 | address tokenB, 42 | uint24 fee, 43 | address implementation, 44 | bytes memory data 45 | ) external override onlyOwner { 46 | address pool = IUniswapV3Factory(factory).getPool(tokenA, tokenB, fee); 47 | if (pool == address(0x0)) revert FactoryErrors.ZeroPoolAddress(); 48 | address vault = _createVault(tokenA, tokenB, fee, pool, implementation, data); 49 | 50 | emit VaultCreated(pool, vault); 51 | } 52 | 53 | /** 54 | * @notice upgradeVaults it allows upgrading the implementation contracts for deployed vault proxies. 55 | * only owner of the factory contract can call it. Internally calls _upgradeVault. 56 | * @param _vaults list of vaults to upgrade 57 | * @param _impls new implementation contracts of corresponding vaults 58 | */ 59 | function upgradeVaults( 60 | address[] calldata _vaults, 61 | address[] calldata _impls 62 | ) external override onlyOwner { 63 | if (_vaults.length != _impls.length) revert FactoryErrors.MismatchedVaultsAndImplsLength(); 64 | 65 | for (uint256 i = 0; i < _vaults.length; i++) { 66 | _upgradeVault(_vaults[i], _impls[i]); 67 | } 68 | } 69 | 70 | /** 71 | * @notice upgradeVault it allows upgrading the implementation contract for deployed vault proxy. 72 | * only owner of the factory contract can call it. Internally calls _upgradeVault. 73 | * @param _vault a vault to upgrade 74 | * @param _impl new implementation contract of corresponding vault 75 | */ 76 | function upgradeVault(address _vault, address _impl) public override onlyOwner { 77 | _upgradeVault(_vault, _impl); 78 | } 79 | 80 | /** 81 | * @notice returns the vaults addresses based on the provided indexes 82 | * @param startIdx the index in vaults to start retrieval from. 83 | * @param endIdx the index in vaults to end retrieval from. 84 | * @return vaultList list of fetched vault addresses 85 | */ 86 | function getVaultAddresses( 87 | uint256 startIdx, 88 | uint256 endIdx 89 | ) external view returns (address[] memory vaultList) { 90 | vaultList = new address[](endIdx - startIdx + 1); 91 | for (uint256 i = startIdx; i <= endIdx; i++) { 92 | vaultList[i - startIdx] = _vaultsList[i]; 93 | } 94 | } 95 | 96 | /// @notice vaultCount counts the total number of vaults in existence 97 | /// @return total count of vaults 98 | function vaultCount() public view returns (uint256) { 99 | return _vaultsList.length; 100 | } 101 | 102 | /** 103 | * @dev Internal function to create vault proxy. 104 | */ 105 | function _createVault( 106 | address tokenA, 107 | address tokenB, 108 | uint24 fee, 109 | address pool, 110 | address implementation, 111 | bytes memory data 112 | ) internal returns (address vault) { 113 | if (data.length == 0) revert FactoryErrors.NoVaultInitDataProvided(); 114 | if (tokenA == tokenB) revert FactoryErrors.SameTokensAddresses(); 115 | address token0 = tokenA < tokenB ? tokenA : tokenB; 116 | if (token0 == address(0x0)) revert("token cannot be a zero address"); 117 | 118 | int24 tickSpacing = IUniswapV3Factory(factory).feeAmountTickSpacing(fee); 119 | vault = address( 120 | new ERC1967Proxy( 121 | implementation, 122 | abi.encodeWithSelector(INIT_SELECTOR, pool, tickSpacing, data) 123 | ) 124 | ); 125 | _vaultsList.push(vault); 126 | } 127 | 128 | /** 129 | * @dev Internal function to upgrade a vault's implementation. 130 | */ 131 | function _upgradeVault(address _vault, address _impl) internal { 132 | if (!Address.isContract(_impl)) revert FactoryErrors.ImplIsNotAContract(); 133 | (bool success, ) = _vault.call(abi.encodeWithSelector(UPGRADE_SELECTOR, _impl)); 134 | 135 | if (!success) revert FactoryErrors.VaultUpgradeFailed(); 136 | emit VaultImplUpgraded(_vault, _impl); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /contracts/RangeProtocolVault.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 6 | import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 7 | import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; 8 | import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; 9 | import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 10 | import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 11 | import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 12 | import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 13 | 14 | import {TickMath} from "./uniswap/TickMath.sol"; 15 | import {LiquidityAmounts} from "./uniswap/LiquidityAmounts.sol"; 16 | import {FullMath} from "./uniswap/FullMath.sol"; 17 | import {IRangeProtocolVault} from "./interfaces/IRangeProtocolVault.sol"; 18 | import {RangeProtocolVaultStorage} from "./RangeProtocolVaultStorage.sol"; 19 | import {OwnableUpgradeable} from "./access/OwnableUpgradeable.sol"; 20 | import {VaultErrors} from "./errors/VaultErrors.sol"; 21 | 22 | /** 23 | * @dev Mars@RangeProtocol 24 | * @notice RangeProtocolVault is fungible vault shares contract that accepts uniswap pool tokens for liquidity 25 | * provision to the corresponding uniswap v3 pool. This contract is configurable to work with any uniswap v3 26 | * pool and is initialized through RangeProtocolFactory contract's createVault function which determines 27 | * the pool address based provided tokens addresses and fee tier. 28 | * 29 | * The contract allows minting and burning of vault shares where minting involves providing token0 and/or token1 30 | * for the current set ticks (or based on ratio of token0 and token1 amounts in the vault when vault does not have an 31 | * active position in the uniswap v3 pool) and burning involves removing liquidity from the uniswap v3 pool along with 32 | * the vault's fee. 33 | * 34 | * The manager of the contract can remove liquidity from uniswap v3 pool and deposit into a newer take range to maximise 35 | * the profit by keeping liquidity out of the pool under high volatility periods. 36 | * 37 | * Part of the fee earned from uniswap v3 position is paid to manager as performance fee and fee is charged on the LP's 38 | * notional amount as managing fee. 39 | */ 40 | contract RangeProtocolVault is 41 | Initializable, 42 | UUPSUpgradeable, 43 | ReentrancyGuardUpgradeable, 44 | OwnableUpgradeable, 45 | ERC20Upgradeable, 46 | PausableUpgradeable, 47 | IRangeProtocolVault, 48 | RangeProtocolVaultStorage 49 | { 50 | using SafeERC20Upgradeable for IERC20Upgradeable; 51 | using TickMath for int24; 52 | 53 | /// Performance fee cannot be set more than 20% of the fee earned from uniswap v3 pool. 54 | uint16 private constant MAX_PERFORMANCE_FEE_BPS = 2000; 55 | /// Managing fee cannot be set more than 1% of the total fee earned. 56 | uint16 private constant MAX_MANAGING_FEE_BPS = 100; 57 | 58 | constructor() { 59 | _disableInitializers(); 60 | } 61 | 62 | /** 63 | * @notice initialize initializes the vault contract and is called right after proxy deployment 64 | * by the factory contract. 65 | * @param _pool address of the uniswap v3 pool associated with vault 66 | * @param _tickSpacing tick spacing of the uniswap pool 67 | * @param data additional config data associated with the implementation. The data type chosen is bytes 68 | * to keep the initialize function implementation contract generic to be compatible with factory contract 69 | */ 70 | function initialize( 71 | address _pool, 72 | int24 _tickSpacing, 73 | bytes memory data 74 | ) external override initializer { 75 | (address manager, string memory _name, string memory _symbol) = abi.decode( 76 | data, 77 | (address, string, string) 78 | ); 79 | 80 | // reverts if manager address provided is zero. 81 | if (manager == address(0x0)) revert VaultErrors.ZeroManagerAddress(); 82 | __UUPSUpgradeable_init(); 83 | __ReentrancyGuard_init(); 84 | __Ownable_init(); 85 | __ERC20_init(_name, _symbol); 86 | __Pausable_init(); 87 | 88 | _transferOwnership(manager); 89 | 90 | pool = IUniswapV3Pool(_pool); 91 | token0 = IERC20Upgradeable(pool.token0()); 92 | token1 = IERC20Upgradeable(pool.token1()); 93 | tickSpacing = _tickSpacing; 94 | factory = msg.sender; 95 | 96 | // Managing fee is 0% and performanceFee is 10% at the time vault initialization. 97 | _updateFees(0, 1000); 98 | } 99 | 100 | /** 101 | * @notice updateTicks it is called by the contract manager to update the ticks. 102 | * It can only be called once total supply is zero and the vault has not active position 103 | * in the uniswap pool 104 | * @param _lowerTick lowerTick to set 105 | * @param _upperTick upperTick to set 106 | */ 107 | function updateTicks(int24 _lowerTick, int24 _upperTick) external override onlyManager { 108 | if (totalSupply() != 0 || inThePosition) revert VaultErrors.NotAllowedToUpdateTicks(); 109 | _updateTicks(_lowerTick, _upperTick); 110 | 111 | if (!mintStarted) { 112 | mintStarted = true; 113 | emit MintStarted(); 114 | } 115 | } 116 | 117 | /** 118 | * @notice allows pausing of minting and burning features of the contract in the event 119 | * any security risk is seen in the vault. 120 | */ 121 | function pause() external onlyManager { 122 | _pause(); 123 | } 124 | 125 | /** 126 | * @notice allows unpausing of minting and burning features of the contract if they paused. 127 | */ 128 | function unpause() external onlyManager { 129 | _unpause(); 130 | } 131 | 132 | /// @notice uniswapV3MintCallback Uniswap V3 callback fn, called back on pool.mint 133 | function uniswapV3MintCallback( 134 | uint256 amount0Owed, 135 | uint256 amount1Owed, 136 | bytes calldata 137 | ) external override { 138 | if (msg.sender != address(pool)) revert VaultErrors.OnlyPoolAllowed(); 139 | 140 | if (amount0Owed > 0) { 141 | token0.safeTransfer(msg.sender, amount0Owed); 142 | } 143 | 144 | if (amount1Owed > 0) { 145 | token1.safeTransfer(msg.sender, amount1Owed); 146 | } 147 | } 148 | 149 | /// @notice uniswapV3SwapCallback Uniswap v3 callback fn, called back on pool.swap 150 | function uniswapV3SwapCallback( 151 | int256 amount0Delta, 152 | int256 amount1Delta, 153 | bytes calldata 154 | ) external override { 155 | if (msg.sender != address(pool)) revert VaultErrors.OnlyPoolAllowed(); 156 | 157 | if (amount0Delta > 0) { 158 | token0.safeTransfer(msg.sender, uint256(amount0Delta)); 159 | } else if (amount1Delta > 0) { 160 | token1.safeTransfer(msg.sender, uint256(amount1Delta)); 161 | } 162 | } 163 | 164 | /** 165 | * @notice mint mints range vault shares, fractional shares of a Uniswap V3 position/strategy 166 | * to compute the amount of tokens necessary to mint `mintAmount` see getMintAmounts 167 | * @param mintAmount The number of shares to mint 168 | * @param maxAmountsIn max amounts to add in token0 and token1. 169 | * @return amount0 amount of token0 transferred from msg.sender to mint `mintAmount` 170 | * @return amount1 amount of token1 transferred from msg.sender to mint `mintAmount` 171 | */ 172 | function mint( 173 | uint256 mintAmount, 174 | uint256[2] calldata maxAmountsIn 175 | ) external override nonReentrant whenNotPaused returns (uint256 amount0, uint256 amount1) { 176 | if (!mintStarted) revert VaultErrors.MintNotStarted(); 177 | if (mintAmount == 0) revert VaultErrors.InvalidMintAmount(); 178 | uint256 totalSupply = totalSupply(); 179 | bool _inThePosition = inThePosition; 180 | (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); 181 | 182 | if (totalSupply > 0) { 183 | (uint256 amount0Current, uint256 amount1Current) = getUnderlyingBalances(); 184 | amount0 = FullMath.mulDivRoundingUp(amount0Current, mintAmount, totalSupply); 185 | amount1 = FullMath.mulDivRoundingUp(amount1Current, mintAmount, totalSupply); 186 | } else if (_inThePosition) { 187 | // If total supply is zero then inThePosition must be set to accept token0 and token1 based on currently set ticks. 188 | // This branch will be executed for the first mint and as well as each time total supply is to be changed from zero to non-zero. 189 | (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity( 190 | sqrtRatioX96, 191 | lowerTick.getSqrtRatioAtTick(), 192 | upperTick.getSqrtRatioAtTick(), 193 | SafeCastUpgradeable.toUint128(mintAmount) 194 | ); 195 | } else { 196 | // If total supply is zero and the vault is not in the position then mint cannot be accepted based on the assumptions 197 | // that being out of the pool renders currently set ticks unusable and totalSupply being zero does not allow 198 | // calculating correct amounts of amount0 and amount1 to be accepted from the user. 199 | // This branch will be executed if all users remove their liquidity from the vault i.e. total supply is zero from non-zero and 200 | // the vault is out of the position i.e. no valid tick range to calculate the vault's mint shares. 201 | // Manager must call initialize function with valid tick ranges to enable the minting again. 202 | revert VaultErrors.MintNotAllowed(); 203 | } 204 | 205 | if (amount0 > maxAmountsIn[0] || amount1 > maxAmountsIn[1]) 206 | revert VaultErrors.SlippageExceedThreshold(); 207 | 208 | if (!userVaults[msg.sender].exists) { 209 | userVaults[msg.sender].exists = true; 210 | users.push(msg.sender); 211 | } 212 | if (amount0 > 0) { 213 | userVaults[msg.sender].token0 += amount0; 214 | token0.safeTransferFrom(msg.sender, address(this), amount0); 215 | } 216 | if (amount1 > 0) { 217 | userVaults[msg.sender].token1 += amount1; 218 | token1.safeTransferFrom(msg.sender, address(this), amount1); 219 | } 220 | 221 | _mint(msg.sender, mintAmount); 222 | if (_inThePosition) { 223 | uint128 liquidityMinted = LiquidityAmounts.getLiquidityForAmounts( 224 | sqrtRatioX96, 225 | lowerTick.getSqrtRatioAtTick(), 226 | upperTick.getSqrtRatioAtTick(), 227 | amount0, 228 | amount1 229 | ); 230 | pool.mint(address(this), lowerTick, upperTick, liquidityMinted, ""); 231 | } 232 | 233 | emit Minted(msg.sender, mintAmount, amount0, amount1); 234 | } 235 | 236 | struct BurnLocalVars { 237 | uint256 totalSupply; 238 | uint256 balanceBefore; 239 | } 240 | 241 | /** 242 | * @notice burn burns range vault shares (shares of a Uniswap V3 position) and receive underlying 243 | * @param burnAmount The number of shares to burn 244 | * @param minAmountsOut the min desired amounts to be out from burn. 245 | * @return amount0 amount of token0 transferred to msg.sender for burning {burnAmount} 246 | * @return amount1 amount of token1 transferred to msg.sender for burning {burnAmount} 247 | */ 248 | function burn( 249 | uint256 burnAmount, 250 | uint256[2] calldata minAmountsOut 251 | ) external override nonReentrant returns (uint256 amount0, uint256 amount1) { 252 | if (burnAmount == 0) revert VaultErrors.InvalidBurnAmount(); 253 | 254 | BurnLocalVars memory vars; 255 | vars.totalSupply = totalSupply(); 256 | vars.balanceBefore = balanceOf(msg.sender); 257 | _burn(msg.sender, burnAmount); 258 | 259 | if (inThePosition) { 260 | (uint128 liquidity, , , , ) = pool.positions(getPositionID()); 261 | uint256 liquidityBurned_ = FullMath.mulDiv(burnAmount, liquidity, vars.totalSupply); 262 | uint128 liquidityBurned = SafeCastUpgradeable.toUint128(liquidityBurned_); 263 | (uint256 burn0, uint256 burn1, uint256 fee0, uint256 fee1) = _withdraw(liquidityBurned); 264 | 265 | _applyPerformanceFee(fee0, fee1); 266 | (fee0, fee1) = _netPerformanceFees(fee0, fee1); 267 | emit FeesEarned(fee0, fee1); 268 | 269 | uint256 passiveBalance0 = token0.balanceOf(address(this)) - burn0; 270 | uint256 passiveBalance1 = token1.balanceOf(address(this)) - burn1; 271 | if (passiveBalance0 > managerBalance0) passiveBalance0 -= managerBalance0; 272 | if (passiveBalance1 > managerBalance1) passiveBalance1 -= managerBalance1; 273 | 274 | amount0 = burn0 + FullMath.mulDiv(passiveBalance0, burnAmount, vars.totalSupply); 275 | amount1 = burn1 + FullMath.mulDiv(passiveBalance1, burnAmount, vars.totalSupply); 276 | } else { 277 | (uint256 amount0Current, uint256 amount1Current) = getUnderlyingBalances(); 278 | amount0 = FullMath.mulDiv(amount0Current, burnAmount, vars.totalSupply); 279 | amount1 = FullMath.mulDiv(amount1Current, burnAmount, vars.totalSupply); 280 | } 281 | 282 | if (amount0 < minAmountsOut[0] || amount1 < minAmountsOut[1]) 283 | revert VaultErrors.SlippageExceedThreshold(); 284 | 285 | _applyManagingFee(amount0, amount1); 286 | (amount0, amount1) = _netManagingFees(amount0, amount1); 287 | 288 | userVaults[msg.sender].token0 = 289 | (userVaults[msg.sender].token0 * (vars.balanceBefore - burnAmount)) / 290 | vars.balanceBefore; 291 | if (amount0 > 0) token0.safeTransfer(msg.sender, amount0); 292 | 293 | userVaults[msg.sender].token1 = 294 | (userVaults[msg.sender].token1 * (vars.balanceBefore - burnAmount)) / 295 | vars.balanceBefore; 296 | if (amount1 > 0) token1.safeTransfer(msg.sender, amount1); 297 | 298 | emit Burned(msg.sender, burnAmount, amount0, amount1); 299 | } 300 | 301 | /** 302 | * @notice removeLiquidity removes liquidity from uniswap pool and receives underlying tokens 303 | * in the vault contract. 304 | * @param minAmountsOut minimum amounts to get from the pool upon removal of liquidity. 305 | */ 306 | function removeLiquidity(uint256[2] calldata minAmountsOut) external override onlyManager { 307 | (uint128 liquidity, , , , ) = pool.positions(getPositionID()); 308 | 309 | if (liquidity > 0) { 310 | int24 _lowerTick = lowerTick; 311 | int24 _upperTick = upperTick; 312 | (uint256 amount0, uint256 amount1, uint256 fee0, uint256 fee1) = _withdraw(liquidity); 313 | 314 | if (amount0 < minAmountsOut[0] || amount1 < minAmountsOut[1]) 315 | revert VaultErrors.SlippageExceedThreshold(); 316 | 317 | emit LiquidityRemoved(liquidity, _lowerTick, _upperTick, amount0, amount1); 318 | 319 | _applyPerformanceFee(fee0, fee1); 320 | (fee0, fee1) = _netPerformanceFees(fee0, fee1); 321 | emit FeesEarned(fee0, fee1); 322 | } 323 | 324 | // TicksSet event is not emitted here since the emitting would create a new position on subgraph but 325 | // the following statement is to only disallow any liquidity provision through the vault unless done 326 | // by manager (taking into account any features added in future). 327 | lowerTick = upperTick; 328 | inThePosition = false; 329 | emit InThePositionStatusSet(false); 330 | } 331 | 332 | /** 333 | * @dev Mars@RangeProtocol 334 | * @notice swap swaps token0 for token1 (token0 in, token1 out), or token1 for token0 (token1 in token0 out). 335 | * Zero for one will cause the price: amount1 / amount0 lower, otherwise it will cause the price higher 336 | * @param zeroForOne The direction of the swap, true is swap token0 for token1, false is swap token1 to token0 337 | * @param swapAmount The exact input token amount of the swap 338 | * @param sqrtPriceLimitX96 threshold price ratio after the swap. 339 | * If zero for one, the price cannot be lower (swap make price lower) than this threshold value after the swap 340 | * If one for zero, the price cannot be greater (swap make price higher) than this threshold value after the swap 341 | * @param minAmountOut minimum amount to protect against slippage. 342 | * @return amount0 If positive represents exact input token0 amount after this swap, msg.sender paid amount, 343 | * or exact output token0 amount (negative), msg.sender received amount 344 | * @return amount1 If positive represents exact input token1 amount after this swap, msg.sender paid amount, 345 | * or exact output token1 amount (negative), msg.sender received amount 346 | */ 347 | function swap( 348 | bool zeroForOne, 349 | int256 swapAmount, 350 | uint160 sqrtPriceLimitX96, 351 | uint256 minAmountOut 352 | ) external override onlyManager returns (int256 amount0, int256 amount1) { 353 | (amount0, amount1) = pool.swap( 354 | address(this), 355 | zeroForOne, 356 | swapAmount, 357 | sqrtPriceLimitX96, 358 | "" 359 | ); 360 | if ( 361 | (zeroForOne && uint256(-amount1) < minAmountOut) || 362 | (!zeroForOne && uint256(-amount0) < minAmountOut) 363 | ) revert VaultErrors.SlippageExceedThreshold(); 364 | 365 | emit Swapped(zeroForOne, amount0, amount1); 366 | } 367 | 368 | /** 369 | * @dev Mars@RangeProtocol 370 | * @notice addLiquidity allows manager to add liquidity into uniswap pool into newer tick ranges. 371 | * @param newLowerTick new lower tick to deposit liquidity into 372 | * @param newUpperTick new upper tick to deposit liquidity into 373 | * @param amount0 max amount of amount0 to use 374 | * @param amount1 max amount of amount1 to use 375 | * @param minAmountsIn minimum amounts to add for slippage protection 376 | * @param maxAmountsIn minimum amounts to add for slippage protection 377 | * @return remainingAmount0 remaining amount from amount0 378 | * @return remainingAmount1 remaining amount from amount1 379 | */ 380 | function addLiquidity( 381 | int24 newLowerTick, 382 | int24 newUpperTick, 383 | uint256 amount0, 384 | uint256 amount1, 385 | uint256[2] calldata minAmountsIn, 386 | uint256[2] calldata maxAmountsIn 387 | ) external override onlyManager returns (uint256 remainingAmount0, uint256 remainingAmount1) { 388 | if (inThePosition) revert VaultErrors.LiquidityAlreadyAdded(); 389 | 390 | (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); 391 | uint128 baseLiquidity = LiquidityAmounts.getLiquidityForAmounts( 392 | sqrtRatioX96, 393 | newLowerTick.getSqrtRatioAtTick(), 394 | newUpperTick.getSqrtRatioAtTick(), 395 | amount0, 396 | amount1 397 | ); 398 | 399 | if (baseLiquidity > 0) { 400 | (uint256 amountDeposited0, uint256 amountDeposited1) = pool.mint( 401 | address(this), 402 | newLowerTick, 403 | newUpperTick, 404 | baseLiquidity, 405 | "" 406 | ); 407 | 408 | if ( 409 | amountDeposited0 < minAmountsIn[0] 410 | || amountDeposited0 > maxAmountsIn[0] 411 | || amountDeposited1 < minAmountsIn[1] 412 | || amountDeposited1 > maxAmountsIn[1] 413 | ) 414 | revert VaultErrors.SlippageExceedThreshold(); 415 | 416 | _updateTicks(newLowerTick, newUpperTick); 417 | emit LiquidityAdded( 418 | baseLiquidity, 419 | newLowerTick, 420 | newUpperTick, 421 | amountDeposited0, 422 | amountDeposited1 423 | ); 424 | 425 | // Should return remaining token number for swap 426 | remainingAmount0 = amount0 - amountDeposited0; 427 | remainingAmount1 = amount1 - amountDeposited1; 428 | } 429 | } 430 | 431 | /** 432 | * @dev pullFeeFromPool pulls accrued fee from uniswap v3 pool that position has accrued since 433 | * last collection. 434 | */ 435 | function pullFeeFromPool() external onlyManager { 436 | _pullFeeFromPool(); 437 | } 438 | 439 | /// @notice collectManager collects manager fees accrued 440 | function collectManager() external override onlyManager { 441 | uint256 amount0 = managerBalance0; 442 | uint256 amount1 = managerBalance1; 443 | managerBalance0 = 0; 444 | managerBalance1 = 0; 445 | 446 | if (amount0 > 0) { 447 | token0.safeTransfer(manager(), amount0); 448 | } 449 | if (amount1 > 0) { 450 | token1.safeTransfer(manager(), amount1); 451 | } 452 | } 453 | 454 | /** 455 | * @notice updateFees allows updating of managing and performance fees 456 | */ 457 | function updateFees( 458 | uint16 newManagingFee, 459 | uint16 newPerformanceFee 460 | ) external override onlyManager { 461 | _updateFees(newManagingFee, newPerformanceFee); 462 | } 463 | 464 | /** 465 | * @notice compute maximum shares that can be minted from `amount0Max` and `amount1Max` 466 | * @param amount0Max The maximum amount of token0 to forward on mint 467 | * @param amount1Max The maximum amount of token1 to forward on mint 468 | * @return amount0 actual amount of token0 to forward when minting `mintAmount` 469 | * @return amount1 actual amount of token1 to forward when minting `mintAmount` 470 | * @return mintAmount maximum number of shares mintable 471 | */ 472 | function getMintAmounts( 473 | uint256 amount0Max, 474 | uint256 amount1Max 475 | ) external view override returns (uint256 amount0, uint256 amount1, uint256 mintAmount) { 476 | if (!mintStarted) revert VaultErrors.MintNotStarted(); 477 | uint256 totalSupply = totalSupply(); 478 | if (totalSupply > 0) { 479 | (amount0, amount1, mintAmount) = _calcMintAmounts(totalSupply, amount0Max, amount1Max); 480 | } else if (inThePosition) { 481 | (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); 482 | uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts( 483 | sqrtRatioX96, 484 | lowerTick.getSqrtRatioAtTick(), 485 | upperTick.getSqrtRatioAtTick(), 486 | amount0Max, 487 | amount1Max 488 | ); 489 | mintAmount = uint256(newLiquidity); 490 | (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity( 491 | sqrtRatioX96, 492 | lowerTick.getSqrtRatioAtTick(), 493 | upperTick.getSqrtRatioAtTick(), 494 | newLiquidity 495 | ); 496 | } 497 | } 498 | 499 | /** 500 | * @notice getCurrentFees returns the current uncollected fees 501 | * @return fee0 uncollected fee in token0 502 | * @return fee1 uncollected fee in token1 503 | */ 504 | function getCurrentFees() external view override returns (uint256 fee0, uint256 fee1) { 505 | (, int24 tick, , , , , ) = pool.slot0(); 506 | ( 507 | uint128 liquidity, 508 | uint256 feeGrowthInside0Last, 509 | uint256 feeGrowthInside1Last, 510 | uint128 tokensOwed0, 511 | uint128 tokensOwed1 512 | ) = pool.positions(getPositionID()); 513 | fee0 = _feesEarned(true, feeGrowthInside0Last, tick, liquidity) + uint256(tokensOwed0); 514 | fee1 = _feesEarned(false, feeGrowthInside1Last, tick, liquidity) + uint256(tokensOwed1); 515 | (fee0, fee1) = _netPerformanceFees(fee0, fee1); 516 | } 517 | 518 | /** 519 | * @notice returns array of current user vaults. This function is only intended to be called off-chain. 520 | * @param fromIdx start index to fetch the user vaults info from. 521 | * @param toIdx end index to fetch the user vault to. 522 | */ 523 | function getUserVaults( 524 | uint256 fromIdx, 525 | uint256 toIdx 526 | ) external view override returns (UserVaultInfo[] memory) { 527 | if (fromIdx == 0 && toIdx == 0) { 528 | toIdx = users.length; 529 | } 530 | UserVaultInfo[] memory usersVaultInfo = new UserVaultInfo[](toIdx - fromIdx); 531 | uint256 count; 532 | for (uint256 i = fromIdx; i < toIdx; i++) { 533 | UserVault memory userVault = userVaults[users[i]]; 534 | usersVaultInfo[count++] = UserVaultInfo({ 535 | user: users[i], 536 | token0: userVault.token0, 537 | token1: userVault.token1 538 | }); 539 | } 540 | return usersVaultInfo; 541 | } 542 | 543 | /** 544 | * @dev returns the length of users array. 545 | */ 546 | function userCount() external view returns (uint256) { 547 | return users.length; 548 | } 549 | 550 | /** 551 | * @notice getPositionID returns the position id of the vault in uniswap pool 552 | * @return positionID position id of the vault in uniswap pool 553 | */ 554 | function getPositionID() public view override returns (bytes32 positionID) { 555 | return keccak256(abi.encodePacked(address(this), lowerTick, upperTick)); 556 | } 557 | 558 | /** 559 | * @notice compute total underlying token0 and token1 token supply at current price 560 | * includes current liquidity invested in uniswap position, current fees earned 561 | * and any uninvested leftover (but does not include manager fees accrued) 562 | * @return amount0Current current total underlying balance of token0 563 | * @return amount1Current current total underlying balance of token1 564 | */ 565 | function getUnderlyingBalances() 566 | public 567 | view 568 | override 569 | returns (uint256 amount0Current, uint256 amount1Current) 570 | { 571 | (uint160 sqrtRatioX96, int24 tick, , , , , ) = pool.slot0(); 572 | return _getUnderlyingBalances(sqrtRatioX96, tick); 573 | } 574 | 575 | function getUnderlyingBalancesByShare( 576 | uint256 shares 577 | ) external view returns (uint256 amount0, uint256 amount1) { 578 | uint256 _totalSupply = totalSupply(); 579 | if (_totalSupply != 0) { 580 | // getUnderlyingBalances already applies performanceFee 581 | (uint256 amount0Current, uint256 amount1Current) = getUnderlyingBalances(); 582 | amount0 = (shares * amount0Current) / _totalSupply; 583 | amount1 = (shares * amount1Current) / _totalSupply; 584 | // apply managing fee 585 | (amount0, amount1) = _netManagingFees(amount0, amount1); 586 | } 587 | } 588 | 589 | /** 590 | * @notice _getUnderlyingBalances internal function to calculate underlying balances 591 | * @param sqrtRatioX96 price to calculate underlying balances at 592 | * @param tick tick at the given price 593 | * @return amount0Current current amount of token0 594 | * @return amount1Current current amount of token1 595 | */ 596 | function _getUnderlyingBalances( 597 | uint160 sqrtRatioX96, 598 | int24 tick 599 | ) internal view returns (uint256 amount0Current, uint256 amount1Current) { 600 | ( 601 | uint128 liquidity, 602 | uint256 feeGrowthInside0Last, 603 | uint256 feeGrowthInside1Last, 604 | uint128 tokensOwed0, 605 | uint128 tokensOwed1 606 | ) = pool.positions(getPositionID()); 607 | 608 | uint256 fee0; 609 | uint256 fee1; 610 | if (liquidity != 0) { 611 | (amount0Current, amount1Current) = LiquidityAmounts.getAmountsForLiquidity( 612 | sqrtRatioX96, 613 | lowerTick.getSqrtRatioAtTick(), 614 | upperTick.getSqrtRatioAtTick(), 615 | liquidity 616 | ); 617 | fee0 = _feesEarned(true, feeGrowthInside0Last, tick, liquidity) + uint256(tokensOwed0); 618 | fee1 = _feesEarned(false, feeGrowthInside1Last, tick, liquidity) + uint256(tokensOwed1); 619 | (fee0, fee1) = _netPerformanceFees(fee0, fee1); 620 | amount0Current += fee0; 621 | amount1Current += fee1; 622 | } 623 | 624 | uint256 passiveBalance0 = token0.balanceOf(address(this)); 625 | uint256 passiveBalance1 = token1.balanceOf(address(this)); 626 | amount0Current += passiveBalance0 > managerBalance0 627 | ? passiveBalance0 - managerBalance0 628 | : passiveBalance0; 629 | amount1Current += passiveBalance1 > managerBalance1 630 | ? passiveBalance1 - managerBalance1 631 | : passiveBalance1; 632 | } 633 | 634 | /** 635 | * @notice _authorizeUpgrade internally called by UUPS contract to validate the upgrading operation of 636 | * the contract. 637 | */ 638 | function _authorizeUpgrade(address) internal override { 639 | if (msg.sender != factory) revert VaultErrors.OnlyFactoryAllowed(); 640 | } 641 | 642 | /** 643 | * @notice The userVault mapping is updated before the vault share tokens are transferred between the users. 644 | * The data from this mapping is used by off-chain strategy manager. The data in this mapping does not impact 645 | * the on-chain behaviour of vault or users' funds. 646 | * @dev transfers userVault amounts based on the transferring user vault shares 647 | * @param from address to transfer userVault amount from 648 | * @param to address to transfer userVault amount to 649 | */ 650 | function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { 651 | super._beforeTokenTransfer(from, to, amount); 652 | 653 | // for mint and burn the user vaults adjustment are handled in the respective functions 654 | if (from == address(0x0) || to == address(0x0)) return; 655 | if (!userVaults[to].exists) { 656 | userVaults[to].exists = true; 657 | users.push(to); 658 | } 659 | uint256 senderBalance = balanceOf(from); 660 | uint256 token0Amount = userVaults[from].token0 - 661 | (userVaults[from].token0 * (senderBalance - amount)) / 662 | senderBalance; 663 | 664 | uint256 token1Amount = userVaults[from].token1 - 665 | (userVaults[from].token1 * (senderBalance - amount)) / 666 | senderBalance; 667 | 668 | userVaults[from].token0 -= token0Amount; 669 | userVaults[from].token1 -= token1Amount; 670 | 671 | userVaults[to].token0 += token0Amount; 672 | userVaults[to].token1 += token1Amount; 673 | } 674 | 675 | /** 676 | * @notice _withdraw internal function to withdraw liquidity from uniswap pool 677 | * @param liquidity liquidity to remove from the uniswap pool 678 | */ 679 | function _withdraw( 680 | uint128 liquidity 681 | ) private returns (uint256 burn0, uint256 burn1, uint256 fee0, uint256 fee1) { 682 | int24 _lowerTick = lowerTick; 683 | int24 _upperTick = upperTick; 684 | uint256 preBalance0 = token0.balanceOf(address(this)); 685 | uint256 preBalance1 = token1.balanceOf(address(this)); 686 | (burn0, burn1) = pool.burn(_lowerTick, _upperTick, liquidity); 687 | pool.collect(address(this), _lowerTick, _upperTick, type(uint128).max, type(uint128).max); 688 | fee0 = token0.balanceOf(address(this)) - preBalance0 - burn0; 689 | fee1 = token1.balanceOf(address(this)) - preBalance1 - burn1; 690 | } 691 | 692 | /** 693 | * @notice _calcMintAmounts internal function to calculate the amount based on the max supply of token0 and token1 694 | * and current supply of RangeVault shares. 695 | * @param totalSupply current total supply of range vault shares 696 | * @param amount0Max max amount of token0 to compute mint amount 697 | * @param amount1Max max amount of token1 to compute mint amount 698 | */ 699 | function _calcMintAmounts( 700 | uint256 totalSupply, 701 | uint256 amount0Max, 702 | uint256 amount1Max 703 | ) private view returns (uint256 amount0, uint256 amount1, uint256 mintAmount) { 704 | (uint256 amount0Current, uint256 amount1Current) = getUnderlyingBalances(); 705 | if (amount0Current == 0 && amount1Current > 0) { 706 | mintAmount = FullMath.mulDiv(amount1Max, totalSupply, amount1Current); 707 | } else if (amount1Current == 0 && amount0Current > 0) { 708 | mintAmount = FullMath.mulDiv(amount0Max, totalSupply, amount0Current); 709 | } else if (amount0Current == 0 && amount1Current == 0) { 710 | revert VaultErrors.ZeroUnderlyingBalance(); 711 | } else { 712 | uint256 amount0Mint = FullMath.mulDiv(amount0Max, totalSupply, amount0Current); 713 | uint256 amount1Mint = FullMath.mulDiv(amount1Max, totalSupply, amount1Current); 714 | if (amount0Mint == 0 || amount1Mint == 0) revert VaultErrors.ZeroMintAmount(); 715 | mintAmount = amount0Mint < amount1Mint ? amount0Mint : amount1Mint; 716 | } 717 | 718 | amount0 = FullMath.mulDivRoundingUp(mintAmount, amount0Current, totalSupply); 719 | amount1 = FullMath.mulDivRoundingUp(mintAmount, amount1Current, totalSupply); 720 | } 721 | 722 | /** 723 | * @notice _feesEarned internal function to return the fees accrued 724 | * @param isZero true to compute fee for token0 and false to compute fee for token1 725 | * @param feeGrowthInsideLast last time the fee was realized for the vault in uniswap pool 726 | */ 727 | function _feesEarned( 728 | bool isZero, 729 | uint256 feeGrowthInsideLast, 730 | int24 tick, 731 | uint128 liquidity 732 | ) private view returns (uint256 fee) { 733 | uint256 feeGrowthOutsideLower; 734 | uint256 feeGrowthOutsideUpper; 735 | uint256 feeGrowthGlobal; 736 | if (isZero) { 737 | feeGrowthGlobal = pool.feeGrowthGlobal0X128(); 738 | (, , feeGrowthOutsideLower, , , , , ) = pool.ticks(lowerTick); 739 | (, , feeGrowthOutsideUpper, , , , , ) = pool.ticks(upperTick); 740 | } else { 741 | feeGrowthGlobal = pool.feeGrowthGlobal1X128(); 742 | (, , , feeGrowthOutsideLower, , , , ) = pool.ticks(lowerTick); 743 | (, , , feeGrowthOutsideUpper, , , , ) = pool.ticks(upperTick); 744 | } 745 | 746 | unchecked { 747 | uint256 feeGrowthBelow; 748 | if (tick >= lowerTick) { 749 | feeGrowthBelow = feeGrowthOutsideLower; 750 | } else { 751 | feeGrowthBelow = feeGrowthGlobal - feeGrowthOutsideLower; 752 | } 753 | 754 | uint256 feeGrowthAbove; 755 | if (tick < upperTick) { 756 | feeGrowthAbove = feeGrowthOutsideUpper; 757 | } else { 758 | feeGrowthAbove = feeGrowthGlobal - feeGrowthOutsideUpper; 759 | } 760 | uint256 feeGrowthInside = feeGrowthGlobal - feeGrowthBelow - feeGrowthAbove; 761 | 762 | fee = FullMath.mulDiv( 763 | liquidity, 764 | feeGrowthInside - feeGrowthInsideLast, 765 | 0x100000000000000000000000000000000 766 | ); 767 | } 768 | } 769 | 770 | /** 771 | * @notice _applyManagingFee applies the managing fee to the notional value of the redeeming user. 772 | * @param amount0 user's notional value in token0 773 | * @param amount1 user's notional value in token1 774 | */ 775 | function _applyManagingFee(uint256 amount0, uint256 amount1) private { 776 | uint256 _managingFee = managingFee; 777 | managerBalance0 += (amount0 * _managingFee) / 10_000; 778 | managerBalance1 += (amount1 * _managingFee) / 10_000; 779 | } 780 | 781 | /** 782 | * @notice _applyPerformanceFee applies the performance fee to the fees earned from uniswap v3 pool. 783 | * @param fee0 fee earned in token0 784 | * @param fee1 fee earned in token1 785 | */ 786 | function _applyPerformanceFee(uint256 fee0, uint256 fee1) private { 787 | uint256 _performanceFee = performanceFee; 788 | managerBalance0 += (fee0 * _performanceFee) / 10_000; 789 | managerBalance1 += (fee1 * _performanceFee) / 10_000; 790 | } 791 | 792 | /** 793 | * @notice _netManagingFees computes the fee share for manager from notional value of the redeeming user. 794 | * @param amount0 user's notional value in token0 795 | * @param amount1 user's notional value in token1 796 | * @return amount0AfterFee user's notional value in token0 after managing fee deduction 797 | * @return amount1AfterFee user's notional value in token1 after managing fee deduction 798 | */ 799 | function _netManagingFees( 800 | uint256 amount0, 801 | uint256 amount1 802 | ) private view returns (uint256 amount0AfterFee, uint256 amount1AfterFee) { 803 | uint256 _managingFee = managingFee; 804 | uint256 deduct0 = (amount0 * _managingFee) / 10_000; 805 | uint256 deduct1 = (amount1 * _managingFee) / 10_000; 806 | amount0AfterFee = amount0 - deduct0; 807 | amount1AfterFee = amount1 - deduct1; 808 | } 809 | 810 | /** 811 | * @notice _netPerformanceFees computes the fee share for manager as performance fee from the fee earned from uniswap v3 pool. 812 | * @param rawFee0 fee earned in token0 from uniswap v3 pool. 813 | * @param rawFee1 fee earned in token1 from uniswap v3 pool. 814 | * @return fee0AfterDeduction fee in token0 earned after deducting performance fee from earned fee. 815 | * @return fee1AfterDeduction fee in token1 earned after deducting performance fee from earned fee. 816 | */ 817 | function _netPerformanceFees( 818 | uint256 rawFee0, 819 | uint256 rawFee1 820 | ) private view returns (uint256 fee0AfterDeduction, uint256 fee1AfterDeduction) { 821 | uint256 _performanceFee = performanceFee; 822 | uint256 deduct0 = (rawFee0 * _performanceFee) / 10_000; 823 | uint256 deduct1 = (rawFee1 * _performanceFee) / 10_000; 824 | fee0AfterDeduction = rawFee0 - deduct0; 825 | fee1AfterDeduction = rawFee1 - deduct1; 826 | } 827 | 828 | /** 829 | * @notice _updateTicks internal function to validate and update ticks 830 | * _lowerTick lower tick to update 831 | * _upperTick upper tick to update 832 | */ 833 | function _updateTicks(int24 _lowerTick, int24 _upperTick) private { 834 | _validateTicks(_lowerTick, _upperTick); 835 | lowerTick = _lowerTick; 836 | upperTick = _upperTick; 837 | 838 | // Upon updating ticks inThePosition status is set to true. 839 | inThePosition = true; 840 | emit InThePositionStatusSet(true); 841 | emit TicksSet(_lowerTick, _upperTick); 842 | } 843 | 844 | /** 845 | * @notice _validateTicks validates the upper and lower ticks 846 | * @param _lowerTick lower tick to validate 847 | * @param _upperTick upper tick to validate 848 | */ 849 | function _validateTicks(int24 _lowerTick, int24 _upperTick) private view { 850 | if (_lowerTick < TickMath.MIN_TICK || _upperTick > TickMath.MAX_TICK) 851 | revert VaultErrors.TicksOutOfRange(); 852 | 853 | if ( 854 | _lowerTick >= _upperTick || 855 | _lowerTick % tickSpacing != 0 || 856 | _upperTick % tickSpacing != 0 857 | ) revert VaultErrors.InvalidTicksSpacing(); 858 | } 859 | 860 | /** 861 | * @notice internal function that pulls fee from the pool 862 | */ 863 | function _pullFeeFromPool() private { 864 | (, , uint256 fee0, uint256 fee1) = _withdraw(0); 865 | _applyPerformanceFee(fee0, fee1); 866 | (fee0, fee1) = _netPerformanceFees(fee0, fee1); 867 | emit FeesEarned(fee0, fee1); 868 | } 869 | 870 | /** 871 | * @notice internal function that updates the fee percentages for both performance 872 | * and managing fee. 873 | * @param newManagingFee new managing fee to set. 874 | * @param newPerformanceFee new performance fee to set. 875 | */ 876 | function _updateFees(uint16 newManagingFee, uint16 newPerformanceFee) private { 877 | if (newManagingFee > MAX_MANAGING_FEE_BPS) revert VaultErrors.InvalidManagingFee(); 878 | if (newPerformanceFee > MAX_PERFORMANCE_FEE_BPS) revert VaultErrors.InvalidPerformanceFee(); 879 | 880 | if (inThePosition) _pullFeeFromPool(); 881 | managingFee = newManagingFee; 882 | performanceFee = newPerformanceFee; 883 | emit FeesUpdated(newManagingFee, newPerformanceFee); 884 | } 885 | } 886 | -------------------------------------------------------------------------------- /contracts/RangeProtocolVaultStorage.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 5 | import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 6 | 7 | /** 8 | * @notice RangeProtocolVaultStorage a storage contract for RangeProtocolVault 9 | */ 10 | abstract contract RangeProtocolVaultStorage { 11 | int24 public lowerTick; 12 | int24 public upperTick; 13 | bool public inThePosition; 14 | bool public mintStarted; 15 | 16 | int24 public tickSpacing; 17 | IUniswapV3Pool public pool; 18 | IERC20Upgradeable public token0; 19 | IERC20Upgradeable public token1; 20 | 21 | address public factory; 22 | uint16 public managingFee; 23 | uint16 public performanceFee; 24 | uint256 public managerBalance0; 25 | uint256 public managerBalance1; 26 | 27 | struct UserVault { 28 | bool exists; 29 | uint256 token0; 30 | uint256 token1; 31 | } 32 | mapping(address => UserVault) public userVaults; 33 | address[] public users; 34 | // NOTE: Only add more state variable below it and do not change the order of above state variables. 35 | } 36 | -------------------------------------------------------------------------------- /contracts/access/OwnableUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.4; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 7 | 8 | /** 9 | * @dev Contract module which provides a basic access control mechanism, where 10 | * there is an account (an manager) that can be granted exclusive access to 11 | * specific functions. 12 | * 13 | * By default, the manager account will be the one that deploys the contract. This 14 | * can later be changed with {transferOwnership}. 15 | * 16 | * This module is used through inheritance. It will make available the modifier 17 | * `onlyManager`, which can be applied to your functions to restrict their use to 18 | * the manager. 19 | */ 20 | contract OwnableUpgradeable is Initializable, ContextUpgradeable { 21 | address private _manager; 22 | 23 | event OwnershipTransferred(address indexed previousManager, address indexed newManager); 24 | 25 | /** 26 | * @dev Initializes the contract setting the deployer as the initial manager. 27 | */ 28 | function __Ownable_init() internal onlyInitializing { 29 | __Ownable_init_unchained(); 30 | } 31 | 32 | function __Ownable_init_unchained() internal onlyInitializing { 33 | _transferOwnership(_msgSender()); 34 | } 35 | 36 | /** 37 | * @dev Throws if called by any account other than the manager. 38 | */ 39 | modifier onlyManager() { 40 | _checkManager(); 41 | _; 42 | } 43 | 44 | /** 45 | * @dev Returns the address of the current manager. 46 | */ 47 | function manager() public view virtual returns (address) { 48 | return _manager; 49 | } 50 | 51 | /** 52 | * @dev Throws if the sender is not the manager. 53 | */ 54 | function _checkManager() internal view virtual { 55 | require(manager() == _msgSender(), "Ownable: caller is not the manager"); 56 | } 57 | 58 | /** 59 | * @dev Leaves the contract without manager. It will not be possible to call 60 | * `onlyManager` functions anymore. Can only be called by the current manager. 61 | * 62 | * NOTE: Renouncing ownership will leave the contract without a manager, 63 | * thereby removing any functionality that is only available to the manager. 64 | */ 65 | function renounceOwnership() public virtual onlyManager { 66 | _transferOwnership(address(0)); 67 | } 68 | 69 | /** 70 | * @dev Transfers ownership of the contract to a new account (`newManager`). 71 | * Can only be called by the current manager. 72 | */ 73 | function transferOwnership(address newManager) public virtual onlyManager { 74 | require(newManager != address(0), "Ownable: new manager is the zero address"); 75 | _transferOwnership(newManager); 76 | } 77 | 78 | /** 79 | * @dev Transfers ownership of the contract to a new account (`newManager`). 80 | * Internal function without access restriction. 81 | */ 82 | function _transferOwnership(address newManager) internal virtual { 83 | address oldManager = _manager; 84 | _manager = newManager; 85 | emit OwnershipTransferred(oldManager, newManager); 86 | } 87 | 88 | /** 89 | * @dev This empty reserved space is put in place to allow future versions to add new 90 | * variables without shifting down storage in the inheritance chain. 91 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 92 | */ 93 | uint256[49] private __gap; 94 | } 95 | -------------------------------------------------------------------------------- /contracts/errors/FactoryErrors.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | library FactoryErrors { 5 | error ZeroPoolAddress(); 6 | error NoVaultInitDataProvided(); 7 | error MismatchedVaultsAndImplsLength(); 8 | error VaultUpgradeFailed(); 9 | error ImplIsNotAContract(); 10 | error SameTokensAddresses(); 11 | } 12 | -------------------------------------------------------------------------------- /contracts/errors/VaultErrors.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | library VaultErrors { 5 | error MintNotStarted(); 6 | error NotAllowedToUpdateTicks(); 7 | error InvalidManagingFee(); 8 | error InvalidPerformanceFee(); 9 | error OnlyPoolAllowed(); 10 | error InvalidMintAmount(); 11 | error InvalidBurnAmount(); 12 | error MintNotAllowed(); 13 | error ZeroMintAmount(); 14 | error ZeroUnderlyingBalance(); 15 | error TicksOutOfRange(); 16 | error InvalidTicksSpacing(); 17 | error OnlyFactoryAllowed(); 18 | error LiquidityAlreadyAdded(); 19 | error ZeroManagerAddress(); 20 | error SlippageExceedThreshold(); 21 | } 22 | -------------------------------------------------------------------------------- /contracts/interfaces/IRangeProtocolFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | interface IRangeProtocolFactory { 5 | event VaultCreated(address indexed uniPool, address indexed vault); 6 | event VaultImplUpgraded(address indexed uniPool, address indexed vault); 7 | 8 | function createVault( 9 | address tokenA, 10 | address tokenB, 11 | uint24 fee, 12 | address implementation, 13 | bytes memory configData 14 | ) external; 15 | 16 | function upgradeVaults(address[] calldata _vaults, address[] calldata _impls) external; 17 | 18 | function upgradeVault(address _vault, address _impl) external; 19 | } 20 | -------------------------------------------------------------------------------- /contracts/interfaces/IRangeProtocolVault.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | import {IUniswapV3MintCallback} from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol"; 5 | import {IUniswapV3SwapCallback} from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; 6 | 7 | interface IRangeProtocolVault is IUniswapV3MintCallback, IUniswapV3SwapCallback { 8 | event Minted( 9 | address indexed receiver, 10 | uint256 mintAmount, 11 | uint256 amount0In, 12 | uint256 amount1In 13 | ); 14 | event Burned( 15 | address indexed receiver, 16 | uint256 burnAmount, 17 | uint256 amount0Out, 18 | uint256 amount1Out 19 | ); 20 | event LiquidityAdded( 21 | uint256 liquidityMinted, 22 | int24 tickLower, 23 | int24 tickUpper, 24 | uint256 amount0In, 25 | uint256 amount1In 26 | ); 27 | event LiquidityRemoved( 28 | uint256 liquidityRemoved, 29 | int24 tickLower, 30 | int24 tickUpper, 31 | uint256 amount0Out, 32 | uint256 amount1Out 33 | ); 34 | event FeesEarned(uint256 feesEarned0, uint256 feesEarned1); 35 | event FeesUpdated(uint16 managingFee, uint16 performanceFee); 36 | event InThePositionStatusSet(bool inThePosition); 37 | event Swapped(bool zeroForOne, int256 amount0, int256 amount1); 38 | event TicksSet(int24 lowerTick, int24 upperTick); 39 | event MintStarted(); 40 | 41 | function initialize(address _pool, int24 _tickSpacing, bytes memory data) external; 42 | 43 | function updateTicks(int24 _lowerTick, int24 _upperTick) external; 44 | 45 | function mint( 46 | uint256 mintAmount, 47 | uint256[2] calldata maxAmountsIn 48 | ) external returns (uint256 amount0, uint256 amount1); 49 | 50 | function burn( 51 | uint256 burnAmount, 52 | uint256[2] calldata minAmountsOut 53 | ) external returns (uint256 amount0, uint256 amount1); 54 | 55 | function removeLiquidity(uint256[2] calldata minAmountsOut) external; 56 | 57 | function swap( 58 | bool zeroForOne, 59 | int256 swapAmount, 60 | uint160 sqrtPriceLimitX96, 61 | uint256 minAmountOut 62 | ) external returns (int256 amount0, int256 amount1); 63 | 64 | function addLiquidity( 65 | int24 newLowerTick, 66 | int24 newUpperTick, 67 | uint256 amount0, 68 | uint256 amount1, 69 | uint256[2] calldata minAmountsIn, 70 | uint256[2] calldata maxAmountsIn 71 | ) external returns (uint256 remainingAmount0, uint256 remainingAmount1); 72 | 73 | function collectManager() external; 74 | 75 | function updateFees(uint16 newManagingFee, uint16 newPerformanceFee) external; 76 | 77 | function getMintAmounts( 78 | uint256 amount0Max, 79 | uint256 amount1Max 80 | ) external view returns (uint256 amount0, uint256 amount1, uint256 mintAmount); 81 | 82 | function getUnderlyingBalances() 83 | external 84 | view 85 | returns (uint256 amount0Current, uint256 amount1Current); 86 | 87 | function getCurrentFees() external view returns (uint256 fee0, uint256 fee1); 88 | 89 | function getPositionID() external view returns (bytes32 positionID); 90 | 91 | struct UserVaultInfo { 92 | address user; 93 | uint256 token0; 94 | uint256 token1; 95 | } 96 | 97 | function getUserVaults( 98 | uint256 fromIdx, 99 | uint256 toIdx 100 | ) external view returns (UserVaultInfo[] memory); 101 | } 102 | -------------------------------------------------------------------------------- /contracts/mock/FixedPoint96.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.4.0; 3 | 4 | /// @title FixedPoint96 5 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 6 | /// @dev Used in SqrtPriceMath.sol 7 | library FixedPoint96 { 8 | uint8 internal constant RESOLUTION = 96; 9 | uint256 internal constant Q96 = 0x1000000000000000000000000; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/mock/FullMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.0 <0.8.0; 3 | 4 | /// @title Contains 512-bit math functions 5 | /// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision 6 | /// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits 7 | library FullMath { 8 | /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 9 | /// @param a The multiplicand 10 | /// @param b The multiplier 11 | /// @param denominator The divisor 12 | /// @return result The 256-bit result 13 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv 14 | function mulDiv( 15 | uint256 a, 16 | uint256 b, 17 | uint256 denominator 18 | ) internal pure returns (uint256 result) { 19 | // 512-bit multiply [prod1 prod0] = a * b 20 | // Compute the product mod 2**256 and mod 2**256 - 1 21 | // then use the Chinese Remainder Theorem to reconstruct 22 | // the 512 bit result. The result is stored in two 256 23 | // variables such that product = prod1 * 2**256 + prod0 24 | uint256 prod0; // Least significant 256 bits of the product 25 | uint256 prod1; // Most significant 256 bits of the product 26 | assembly { 27 | let mm := mulmod(a, b, not(0)) 28 | prod0 := mul(a, b) 29 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 30 | } 31 | 32 | // Handle non-overflow cases, 256 by 256 division 33 | if (prod1 == 0) { 34 | require(denominator > 0); 35 | assembly { 36 | result := div(prod0, denominator) 37 | } 38 | return result; 39 | } 40 | 41 | // Make sure the result is less than 2**256. 42 | // Also prevents denominator == 0 43 | require(denominator > prod1); 44 | 45 | /////////////////////////////////////////////// 46 | // 512 by 256 division. 47 | /////////////////////////////////////////////// 48 | 49 | // Make division exact by subtracting the remainder from [prod1 prod0] 50 | // Compute remainder using mulmod 51 | uint256 remainder; 52 | assembly { 53 | remainder := mulmod(a, b, denominator) 54 | } 55 | // Subtract 256 bit number from 512 bit number 56 | assembly { 57 | prod1 := sub(prod1, gt(remainder, prod0)) 58 | prod0 := sub(prod0, remainder) 59 | } 60 | 61 | // Factor powers of two out of denominator 62 | // Compute largest power of two divisor of denominator. 63 | // Always >= 1. 64 | uint256 twos = -denominator & denominator; 65 | // Divide denominator by power of two 66 | assembly { 67 | denominator := div(denominator, twos) 68 | } 69 | 70 | // Divide [prod1 prod0] by the factors of two 71 | assembly { 72 | prod0 := div(prod0, twos) 73 | } 74 | // Shift in bits from prod1 into prod0. For this we need 75 | // to flip `twos` such that it is 2**256 / twos. 76 | // If twos is zero, then it becomes one 77 | assembly { 78 | twos := add(div(sub(0, twos), twos), 1) 79 | } 80 | prod0 |= prod1 * twos; 81 | 82 | // Invert denominator mod 2**256 83 | // Now that denominator is an odd number, it has an inverse 84 | // modulo 2**256 such that denominator * inv = 1 mod 2**256. 85 | // Compute the inverse by starting with a seed that is correct 86 | // correct for four bits. That is, denominator * inv = 1 mod 2**4 87 | uint256 inv = (3 * denominator) ^ 2; 88 | // Now use Newton-Raphson iteration to improve the precision. 89 | // Thanks to Hensel's lifting lemma, this also works in modular 90 | // arithmetic, doubling the correct bits in each step. 91 | inv *= 2 - denominator * inv; // inverse mod 2**8 92 | inv *= 2 - denominator * inv; // inverse mod 2**16 93 | inv *= 2 - denominator * inv; // inverse mod 2**32 94 | inv *= 2 - denominator * inv; // inverse mod 2**64 95 | inv *= 2 - denominator * inv; // inverse mod 2**128 96 | inv *= 2 - denominator * inv; // inverse mod 2**256 97 | 98 | // Because the division is now exact we can divide by multiplying 99 | // with the modular inverse of denominator. This will give us the 100 | // correct result modulo 2**256. Since the precoditions guarantee 101 | // that the outcome is less than 2**256, this is the final result. 102 | // We don't need to compute the high bits of the result and prod1 103 | // is no longer required. 104 | result = prod0 * inv; 105 | return result; 106 | } 107 | 108 | /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 109 | /// @param a The multiplicand 110 | /// @param b The multiplier 111 | /// @param denominator The divisor 112 | /// @return result The 256-bit result 113 | function mulDivRoundingUp( 114 | uint256 a, 115 | uint256 b, 116 | uint256 denominator 117 | ) internal pure returns (uint256 result) { 118 | result = mulDiv(a, b, denominator); 119 | if (mulmod(a, b, denominator) > 0) { 120 | require(result < type(uint256).max); 121 | result++; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /contracts/mock/LowGasSafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.0; 3 | 4 | /// @title Optimized overflow and underflow safe math operations 5 | /// @notice Contains methods for doing math operations that revert on overflow or underflow for minimal gas cost 6 | library LowGasSafeMath { 7 | /// @notice Returns x + y, reverts if sum overflows uint256 8 | /// @param x The augend 9 | /// @param y The addend 10 | /// @return z The sum of x and y 11 | function add(uint256 x, uint256 y) internal pure returns (uint256 z) { 12 | require((z = x + y) >= x); 13 | } 14 | 15 | /// @notice Returns x - y, reverts if underflows 16 | /// @param x The minuend 17 | /// @param y The subtrahend 18 | /// @return z The difference of x and y 19 | function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 20 | require((z = x - y) <= x); 21 | } 22 | 23 | /// @notice Returns x * y, reverts if overflows 24 | /// @param x The multiplicand 25 | /// @param y The multiplier 26 | /// @return z The product of x and y 27 | function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { 28 | require(x == 0 || (z = x * y) / x == y); 29 | } 30 | 31 | /// @notice Returns x + y, reverts if overflows or underflows 32 | /// @param x The augend 33 | /// @param y The addend 34 | /// @return z The sum of x and y 35 | function add(int256 x, int256 y) internal pure returns (int256 z) { 36 | require((z = x + y) >= x == (y >= 0)); 37 | } 38 | 39 | /// @notice Returns x - y, reverts if overflows or underflows 40 | /// @param x The minuend 41 | /// @param y The subtrahend 42 | /// @return z The difference of x and y 43 | function sub(int256 x, int256 y) internal pure returns (int256 z) { 44 | require((z = x - y) <= x == (y >= 0)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /contracts/mock/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.4; 3 | 4 | import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 5 | 6 | contract MockERC20 is ERC20Upgradeable { 7 | constructor() initializer { 8 | __ERC20_init("", "TOKEN"); 9 | _mint(msg.sender, 100000e18); 10 | } 11 | 12 | function mint() external { 13 | _mint(msg.sender, 10000e18); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/mock/MockLiquidityAmounts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | import "../uniswap/LiquidityAmounts.sol"; 5 | 6 | contract MockLiquidityAmounts { 7 | function getLiquidityForAmount0( 8 | uint160 sqrtRatioAX96, 9 | uint160 sqrtRatioBX96, 10 | uint256 amount0 11 | ) public pure returns (uint128 liquidity) { 12 | return LiquidityAmounts.getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0); 13 | } 14 | 15 | /// @notice Computes the amount of liquidity received for a given amount of token1 and price range 16 | /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). 17 | /// @param sqrtRatioAX96 A sqrt price 18 | /// @param sqrtRatioBX96 Another sqrt price 19 | /// @param amount1 The amount1 being sent in 20 | /// @return liquidity The amount of returned liquidity 21 | function getLiquidityForAmount1( 22 | uint160 sqrtRatioAX96, 23 | uint160 sqrtRatioBX96, 24 | uint256 amount1 25 | ) public pure returns (uint128 liquidity) { 26 | return LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1); 27 | } 28 | 29 | /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current 30 | /// pool prices and the prices at the tick boundaries 31 | function getLiquidityForAmounts( 32 | uint160 sqrtRatioX96, 33 | uint160 sqrtRatioAX96, 34 | uint160 sqrtRatioBX96, 35 | uint256 amount0, 36 | uint256 amount1 37 | ) public pure returns (uint128 liquidity) { 38 | return 39 | LiquidityAmounts.getLiquidityForAmounts( 40 | sqrtRatioX96, 41 | sqrtRatioAX96, 42 | sqrtRatioBX96, 43 | amount0, 44 | amount1 45 | ); 46 | } 47 | 48 | /// @notice Computes the amount of token0 for a given amount of liquidity and a price range 49 | /// @param sqrtRatioAX96 A sqrt price 50 | /// @param sqrtRatioBX96 Another sqrt price 51 | /// @param liquidity The liquidity being valued 52 | /// @return amount0 The amount0 53 | function getAmount0ForLiquidity( 54 | uint160 sqrtRatioAX96, 55 | uint160 sqrtRatioBX96, 56 | uint128 liquidity 57 | ) public pure returns (uint256 amount0) { 58 | return LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); 59 | } 60 | 61 | /// @notice Computes the amount of token1 for a given amount of liquidity and a price range 62 | /// @param sqrtRatioAX96 A sqrt price 63 | /// @param sqrtRatioBX96 Another sqrt price 64 | /// @param liquidity The liquidity being valued 65 | /// @return amount1 The amount1 66 | function getAmount1ForLiquidity( 67 | uint160 sqrtRatioAX96, 68 | uint160 sqrtRatioBX96, 69 | uint128 liquidity 70 | ) public pure returns (uint256 amount1) { 71 | return LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); 72 | } 73 | 74 | /// @notice Computes the token0 and token1 value for a given amount of liquidity, the current 75 | /// pool prices and the prices at the tick boundaries 76 | function getAmountsForLiquidity( 77 | uint160 sqrtRatioX96, 78 | uint160 sqrtRatioAX96, 79 | uint160 sqrtRatioBX96, 80 | uint128 liquidity 81 | ) public pure returns (uint256 amount0, uint256 amount1) { 82 | return 83 | LiquidityAmounts.getAmountsForLiquidity( 84 | sqrtRatioX96, 85 | sqrtRatioAX96, 86 | sqrtRatioBX96, 87 | liquidity 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/mock/MockSqrtPriceMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity >=0.5.0; 3 | 4 | import "./LowGasSafeMath.sol"; 5 | import "./SafeCast.sol"; 6 | 7 | import "./FullMath.sol"; 8 | import "./UnsafeMath.sol"; 9 | import "./FixedPoint96.sol"; 10 | 11 | /// @title Functions based on Q64.96 sqrt price and liquidity 12 | /// @notice Contains the math that uses square root of price as a Q64.96 and liquidity to compute deltas 13 | contract MockSqrtPriceMath { 14 | using LowGasSafeMath for uint256; 15 | using SafeCast for uint256; 16 | 17 | /// @notice Gets the next sqrt price given a delta of token0 18 | /// @dev Always rounds up, because in the exact output case (increasing price) we need to move the price at least 19 | /// far enough to get the desired output amount, and in the exact input case (decreasing price) we need to move the 20 | /// price less in order to not send too much output. 21 | /// The most precise formula for this is liquidity * sqrtPX96 / (liquidity +- amount * sqrtPX96), 22 | /// if this is impossible because of overflow, we calculate liquidity / (liquidity / sqrtPX96 +- amount). 23 | /// @param sqrtPX96 The starting price, i.e. before accounting for the token0 delta 24 | /// @param liquidity The amount of usable liquidity 25 | /// @param amount How much of token0 to add or remove from virtual reserves 26 | /// @param add Whether to add or remove the amount of token0 27 | /// @return The price after adding or removing amount, depending on add 28 | function getNextSqrtPriceFromAmount0RoundingUp( 29 | uint160 sqrtPX96, 30 | uint128 liquidity, 31 | uint256 amount, 32 | bool add 33 | ) public pure returns (uint160) { 34 | // we short circuit amount == 0 because the result is otherwise not guaranteed to equal the input price 35 | if (amount == 0) return sqrtPX96; 36 | uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION; 37 | 38 | if (add) { 39 | uint256 product; 40 | if ((product = amount * sqrtPX96) / amount == sqrtPX96) { 41 | uint256 denominator = numerator1 + product; 42 | if (denominator >= numerator1) 43 | // always fits in 160 bits 44 | return uint160(FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator)); 45 | } 46 | 47 | return 48 | uint160(UnsafeMath.divRoundingUp(numerator1, (numerator1 / sqrtPX96).add(amount))); 49 | } else { 50 | uint256 product; 51 | // if the product overflows, we know the denominator underflows 52 | // in addition, we must check that the denominator does not underflow 53 | require((product = amount * sqrtPX96) / amount == sqrtPX96 && numerator1 > product); 54 | uint256 denominator = numerator1 - product; 55 | return FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator).toUint160(); 56 | } 57 | } 58 | 59 | /// @notice Gets the next sqrt price given a delta of token1 60 | /// @dev Always rounds down, because in the exact output case (decreasing price) we need to move the price at least 61 | /// far enough to get the desired output amount, and in the exact input case (increasing price) we need to move the 62 | /// price less in order to not send too much output. 63 | /// The formula we compute is within <1 wei of the lossless version: sqrtPX96 +- amount / liquidity 64 | /// @param sqrtPX96 The starting price, i.e., before accounting for the token1 delta 65 | /// @param liquidity The amount of usable liquidity 66 | /// @param amount How much of token1 to add, or remove, from virtual reserves 67 | /// @param add Whether to add, or remove, the amount of token1 68 | /// @return The price after adding or removing `amount` 69 | function getNextSqrtPriceFromAmount1RoundingDown( 70 | uint160 sqrtPX96, 71 | uint128 liquidity, 72 | uint256 amount, 73 | bool add 74 | ) public pure returns (uint160) { 75 | // if we're adding (subtracting), rounding down requires rounding the quotient down (up) 76 | // in both cases, avoid a mulDiv for most inputs 77 | if (add) { 78 | uint256 quotient = ( 79 | amount <= type(uint160).max 80 | ? (amount << FixedPoint96.RESOLUTION) / liquidity 81 | : FullMath.mulDiv(amount, FixedPoint96.Q96, liquidity) 82 | ); 83 | 84 | return uint256(sqrtPX96).add(quotient).toUint160(); 85 | } else { 86 | uint256 quotient = ( 87 | amount <= type(uint160).max 88 | ? UnsafeMath.divRoundingUp(amount << FixedPoint96.RESOLUTION, liquidity) 89 | : FullMath.mulDivRoundingUp(amount, FixedPoint96.Q96, liquidity) 90 | ); 91 | 92 | require(sqrtPX96 > quotient); 93 | // always fits 160 bits 94 | return uint160(sqrtPX96 - quotient); 95 | } 96 | } 97 | 98 | /// @notice Gets the next sqrt price given an input amount of token0 or token1 99 | /// @dev Throws if price or liquidity are 0, or if the next price is out of bounds 100 | /// @param sqrtPX96 The starting price, i.e., before accounting for the input amount 101 | /// @param liquidity The amount of usable liquidity 102 | /// @param amountIn How much of token0, or token1, is being swapped in 103 | /// @param zeroForOne Whether the amount in is token0 or token1 104 | /// @return sqrtQX96 The price after adding the input amount to token0 or token1 105 | function getNextSqrtPriceFromInput( 106 | uint160 sqrtPX96, 107 | uint128 liquidity, 108 | uint256 amountIn, 109 | bool zeroForOne 110 | ) public pure returns (uint160 sqrtQX96) { 111 | require(sqrtPX96 > 0); 112 | require(liquidity > 0); 113 | 114 | // round to make sure that we don't pass the target price 115 | return 116 | zeroForOne 117 | ? getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) 118 | : getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true); 119 | } 120 | 121 | /// @notice Gets the next sqrt price given an output amount of token0 or token1 122 | /// @dev Throws if price or liquidity are 0 or the next price is out of bounds 123 | /// @param sqrtPX96 The starting price before accounting for the output amount 124 | /// @param liquidity The amount of usable liquidity 125 | /// @param amountOut How much of token0, or token1, is being swapped out 126 | /// @param zeroForOne Whether the amount out is token0 or token1 127 | /// @return sqrtQX96 The price after removing the output amount of token0 or token1 128 | function getNextSqrtPriceFromOutput( 129 | uint160 sqrtPX96, 130 | uint128 liquidity, 131 | uint256 amountOut, 132 | bool zeroForOne 133 | ) public pure returns (uint160 sqrtQX96) { 134 | require(sqrtPX96 > 0); 135 | require(liquidity > 0); 136 | 137 | // round to make sure that we pass the target price 138 | return 139 | zeroForOne 140 | ? getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false) 141 | : getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false); 142 | } 143 | 144 | /// @notice Gets the amount0 delta between two prices 145 | /// @dev Calculates liquidity / sqrt(lower) - liquidity / sqrt(upper), 146 | /// i.e. liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower)) 147 | /// @param sqrtRatioAX96 A sqrt price 148 | /// @param sqrtRatioBX96 Another sqrt price 149 | /// @param liquidity The amount of usable liquidity 150 | /// @param roundUp Whether to round the amount up or down 151 | /// @return amount0 Amount of token0 required to cover a position of size liquidity between the two passed prices 152 | function getAmount0Delta( 153 | uint160 sqrtRatioAX96, 154 | uint160 sqrtRatioBX96, 155 | uint128 liquidity, 156 | bool roundUp 157 | ) public pure returns (uint256 amount0) { 158 | if (sqrtRatioAX96 > sqrtRatioBX96) 159 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 160 | 161 | uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION; 162 | uint256 numerator2 = sqrtRatioBX96 - sqrtRatioAX96; 163 | 164 | require(sqrtRatioAX96 > 0); 165 | 166 | return 167 | roundUp 168 | ? UnsafeMath.divRoundingUp( 169 | FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96), 170 | sqrtRatioAX96 171 | ) 172 | : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96; 173 | } 174 | 175 | /// @notice Gets the amount1 delta between two prices 176 | /// @dev Calculates liquidity * (sqrt(upper) - sqrt(lower)) 177 | /// @param sqrtRatioAX96 A sqrt price 178 | /// @param sqrtRatioBX96 Another sqrt price 179 | /// @param liquidity The amount of usable liquidity 180 | /// @param roundUp Whether to round the amount up, or down 181 | /// @return amount1 Amount of token1 required to cover a position of size liquidity between the two passed prices 182 | function getAmount1Delta( 183 | uint160 sqrtRatioAX96, 184 | uint160 sqrtRatioBX96, 185 | uint128 liquidity, 186 | bool roundUp 187 | ) public pure returns (uint256 amount1) { 188 | if (sqrtRatioAX96 > sqrtRatioBX96) 189 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 190 | 191 | return 192 | roundUp 193 | ? FullMath.mulDivRoundingUp( 194 | liquidity, 195 | sqrtRatioBX96 - sqrtRatioAX96, 196 | FixedPoint96.Q96 197 | ) 198 | : FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96); 199 | } 200 | 201 | /// @notice Helper that gets signed token0 delta 202 | /// @param sqrtRatioAX96 A sqrt price 203 | /// @param sqrtRatioBX96 Another sqrt price 204 | /// @param liquidity The change in liquidity for which to compute the amount0 delta 205 | /// @return amount0 Amount of token0 corresponding to the passed liquidityDelta between the two prices 206 | function getAmount0Delta( 207 | uint160 sqrtRatioAX96, 208 | uint160 sqrtRatioBX96, 209 | int128 liquidity 210 | ) public pure returns (int256 amount0) { 211 | return 212 | liquidity < 0 213 | ? -getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), false) 214 | .toInt256() 215 | : getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), true) 216 | .toInt256(); 217 | } 218 | 219 | /// @notice Helper that gets signed token1 delta 220 | /// @param sqrtRatioAX96 A sqrt price 221 | /// @param sqrtRatioBX96 Another sqrt price 222 | /// @param liquidity The change in liquidity for which to compute the amount1 delta 223 | /// @return amount1 Amount of token1 corresponding to the passed liquidityDelta between the two prices 224 | function getAmount1Delta( 225 | uint160 sqrtRatioAX96, 226 | uint160 sqrtRatioBX96, 227 | int128 liquidity 228 | ) public pure returns (int256 amount1) { 229 | return 230 | liquidity < 0 231 | ? -getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), false) 232 | .toInt256() 233 | : getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), true) 234 | .toInt256(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /contracts/mock/SafeCast.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Safe casting methods 5 | /// @notice Contains methods for safely casting between types 6 | library SafeCast { 7 | /// @notice Cast a uint256 to a uint160, revert on overflow 8 | /// @param y The uint256 to be downcasted 9 | /// @return z The downcasted integer, now type uint160 10 | function toUint160(uint256 y) internal pure returns (uint160 z) { 11 | require((z = uint160(y)) == y); 12 | } 13 | 14 | /// @notice Cast a int256 to a int128, revert on overflow or underflow 15 | /// @param y The int256 to be downcasted 16 | /// @return z The downcasted integer, now type int128 17 | function toInt128(int256 y) internal pure returns (int128 z) { 18 | require((z = int128(y)) == y); 19 | } 20 | 21 | /// @notice Cast a uint256 to a int256, revert on overflow 22 | /// @param y The uint256 to be casted 23 | /// @return z The casted integer, now type int256 24 | function toInt256(uint256 y) internal pure returns (int256 z) { 25 | require(y < 2 ** 255); 26 | z = int256(y); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/mock/SwapTest.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 6 | import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; 7 | 8 | contract SwapTest is IUniswapV3SwapCallback { 9 | function swapZeroForOne(address pool, int256 amountSpecified) external { 10 | (uint160 sqrtRatio, , , , , , ) = IUniswapV3Pool(pool).slot0(); 11 | uint160 nextSqrtRatio = sqrtRatio + 12 | uint160(uint160(uint256(amountSpecified) * 2 ** 96) / IUniswapV3Pool(pool).liquidity()); 13 | 14 | IUniswapV3Pool(pool).swap( 15 | address(msg.sender), 16 | false, 17 | amountSpecified, 18 | nextSqrtRatio, 19 | abi.encode(msg.sender) 20 | ); 21 | } 22 | 23 | function washTrade( 24 | address pool, 25 | int256 amountSpecified, 26 | uint256 numTrades, 27 | uint256 ratio 28 | ) external { 29 | for (uint256 i = 0; i < numTrades; i++) { 30 | bool zeroForOne = i % ratio > 0; 31 | (uint160 sqrtRatio, , , , , , ) = IUniswapV3Pool(pool).slot0(); 32 | IUniswapV3Pool(pool).swap( 33 | address(msg.sender), 34 | zeroForOne, 35 | amountSpecified, 36 | zeroForOne ? sqrtRatio - 1000 : sqrtRatio + 1000, 37 | abi.encode(msg.sender) 38 | ); 39 | } 40 | } 41 | 42 | function getSwapResult( 43 | address pool, 44 | bool zeroForOne, 45 | int256 amountSpecified, 46 | uint160 sqrtPriceLimitX96 47 | ) external returns (int256 amount0Delta, int256 amount1Delta, uint160 nextSqrtRatio) { 48 | (amount0Delta, amount1Delta) = IUniswapV3Pool(pool).swap( 49 | address(msg.sender), 50 | zeroForOne, 51 | amountSpecified, 52 | sqrtPriceLimitX96, 53 | abi.encode(msg.sender) 54 | ); 55 | 56 | (nextSqrtRatio, , , , , , ) = IUniswapV3Pool(pool).slot0(); 57 | } 58 | 59 | function uniswapV3SwapCallback( 60 | int256 amount0Delta, 61 | int256 amount1Delta, 62 | bytes calldata data 63 | ) external override { 64 | address sender = abi.decode(data, (address)); 65 | 66 | if (amount0Delta > 0) { 67 | IERC20(IUniswapV3Pool(msg.sender).token0()).transferFrom( 68 | sender, 69 | msg.sender, 70 | uint256(amount0Delta) 71 | ); 72 | } else if (amount1Delta > 0) { 73 | IERC20(IUniswapV3Pool(msg.sender).token1()).transferFrom( 74 | sender, 75 | msg.sender, 76 | uint256(amount1Delta) 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contracts/mock/UnsafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Math functions that do not check inputs or outputs 5 | /// @notice Contains methods that perform common math functions but do not do any overflow or underflow checks 6 | library UnsafeMath { 7 | /// @notice Returns ceil(x / y) 8 | /// @dev division by 0 has unspecified behavior, and must be checked externally 9 | /// @param x The dividend 10 | /// @param y The divisor 11 | /// @return z The quotient, ceil(x / y) 12 | function divRoundingUp(uint256 x, uint256 y) internal pure returns (uint256 z) { 13 | assembly { 14 | z := add(div(x, y), gt(mod(x, y), 0)) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/uniswap/FullMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.4; 3 | 4 | /// @title Contains 512-bit math functions 5 | /// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision 6 | /// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits 7 | library FullMath { 8 | /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 9 | /// @param a The multiplicand 10 | /// @param b The multiplier 11 | /// @param denominator The divisor 12 | /// @return result The 256-bit result 13 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv 14 | function mulDiv( 15 | uint256 a, 16 | uint256 b, 17 | uint256 denominator 18 | ) internal pure returns (uint256 result) { 19 | unchecked { 20 | // 512-bit multiply [prod1 prod0] = a * b 21 | // Compute the product mod 2**256 and mod 2**256 - 1 22 | // then use the Chinese Remainder Theorem to reconstruct 23 | // the 512 bit result. The result is stored in two 256 24 | // variables such that product = prod1 * 2**256 + prod0 25 | uint256 prod0; // Least significant 256 bits of the product 26 | uint256 prod1; // Most significant 256 bits of the product 27 | assembly { 28 | let mm := mulmod(a, b, not(0)) 29 | prod0 := mul(a, b) 30 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 31 | } 32 | 33 | // Handle non-overflow cases, 256 by 256 division 34 | if (prod1 == 0) { 35 | require(denominator > 0); 36 | assembly { 37 | result := div(prod0, denominator) 38 | } 39 | return result; 40 | } 41 | 42 | // Make sure the result is less than 2**256. 43 | // Also prevents denominator == 0 44 | require(denominator > prod1); 45 | 46 | /////////////////////////////////////////////// 47 | // 512 by 256 division. 48 | /////////////////////////////////////////////// 49 | 50 | // Make division exact by subtracting the remainder from [prod1 prod0] 51 | // Compute remainder using mulmod 52 | uint256 remainder; 53 | assembly { 54 | remainder := mulmod(a, b, denominator) 55 | } 56 | // Subtract 256 bit number from 512 bit number 57 | assembly { 58 | prod1 := sub(prod1, gt(remainder, prod0)) 59 | prod0 := sub(prod0, remainder) 60 | } 61 | 62 | // Factor powers of two out of denominator 63 | // Compute largest power of two divisor of denominator. 64 | // Always >= 1. 65 | // EDIT for 0.8 compatibility: 66 | // see: https://ethereum.stackexchange.com/questions/96642/unary-operator-cannot-be-applied-to-type-uint256 67 | uint256 twos = denominator & (~denominator + 1); 68 | 69 | // Divide denominator by power of two 70 | assembly { 71 | denominator := div(denominator, twos) 72 | } 73 | 74 | // Divide [prod1 prod0] by the factors of two 75 | assembly { 76 | prod0 := div(prod0, twos) 77 | } 78 | // Shift in bits from prod1 into prod0. For this we need 79 | // to flip `twos` such that it is 2**256 / twos. 80 | // If twos is zero, then it becomes one 81 | assembly { 82 | twos := add(div(sub(0, twos), twos), 1) 83 | } 84 | prod0 |= prod1 * twos; 85 | 86 | // Invert denominator mod 2**256 87 | // Now that denominator is an odd number, it has an inverse 88 | // modulo 2**256 such that denominator * inv = 1 mod 2**256. 89 | // Compute the inverse by starting with a seed that is correct 90 | // correct for four bits. That is, denominator * inv = 1 mod 2**4 91 | uint256 inv = (3 * denominator) ^ 2; 92 | // Now use Newton-Raphson iteration to improve the precision. 93 | // Thanks to Hensel's lifting lemma, this also works in modular 94 | // arithmetic, doubling the correct bits in each step. 95 | inv *= 2 - denominator * inv; // inverse mod 2**8 96 | inv *= 2 - denominator * inv; // inverse mod 2**16 97 | inv *= 2 - denominator * inv; // inverse mod 2**32 98 | inv *= 2 - denominator * inv; // inverse mod 2**64 99 | inv *= 2 - denominator * inv; // inverse mod 2**128 100 | inv *= 2 - denominator * inv; // inverse mod 2**256 101 | 102 | // Because the division is now exact we can divide by multiplying 103 | // with the modular inverse of denominator. This will give us the 104 | // correct result modulo 2**256. Since the precoditions guarantee 105 | // that the outcome is less than 2**256, this is the final result. 106 | // We don't need to compute the high bits of the result and prod1 107 | // is no longer required. 108 | result = prod0 * inv; 109 | return result; 110 | } 111 | } 112 | 113 | /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 114 | /// @param a The multiplicand 115 | /// @param b The multiplier 116 | /// @param denominator The divisor 117 | /// @return result The 256-bit result 118 | function mulDivRoundingUp( 119 | uint256 a, 120 | uint256 b, 121 | uint256 denominator 122 | ) internal pure returns (uint256 result) { 123 | result = mulDiv(a, b, denominator); 124 | if (mulmod(a, b, denominator) > 0) { 125 | require(result < type(uint256).max); 126 | result++; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /contracts/uniswap/LiquidityAmounts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | import {FullMath} from "./FullMath.sol"; 5 | import "@uniswap/v3-core/contracts/libraries/FixedPoint96.sol"; 6 | 7 | /// @title Liquidity amount functions 8 | /// @notice Provides functions for computing liquidity amounts from token amounts and prices 9 | library LiquidityAmounts { 10 | function toUint128(uint256 x) private pure returns (uint128 y) { 11 | require((y = uint128(x)) == x); 12 | } 13 | 14 | /// @notice Computes the amount of liquidity received for a given amount of token0 and price range 15 | /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)). 16 | /// @param sqrtRatioAX96 A sqrt price 17 | /// @param sqrtRatioBX96 Another sqrt price 18 | /// @param amount0 The amount0 being sent in 19 | /// @return liquidity The amount of returned liquidity 20 | function getLiquidityForAmount0( 21 | uint160 sqrtRatioAX96, 22 | uint160 sqrtRatioBX96, 23 | uint256 amount0 24 | ) internal pure returns (uint128 liquidity) { 25 | if (sqrtRatioAX96 > sqrtRatioBX96) 26 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 27 | uint256 intermediate = 28 | FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96); 29 | return 30 | toUint128( 31 | FullMath.mulDiv( 32 | amount0, 33 | intermediate, 34 | sqrtRatioBX96 - sqrtRatioAX96 35 | ) 36 | ); 37 | } 38 | 39 | /// @notice Computes the amount of liquidity received for a given amount of token1 and price range 40 | /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). 41 | /// @param sqrtRatioAX96 A sqrt price 42 | /// @param sqrtRatioBX96 Another sqrt price 43 | /// @param amount1 The amount1 being sent in 44 | /// @return liquidity The amount of returned liquidity 45 | function getLiquidityForAmount1( 46 | uint160 sqrtRatioAX96, 47 | uint160 sqrtRatioBX96, 48 | uint256 amount1 49 | ) internal pure returns (uint128 liquidity) { 50 | if (sqrtRatioAX96 > sqrtRatioBX96) 51 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 52 | return 53 | toUint128( 54 | FullMath.mulDiv( 55 | amount1, 56 | FixedPoint96.Q96, 57 | sqrtRatioBX96 - sqrtRatioAX96 58 | ) 59 | ); 60 | } 61 | 62 | /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current 63 | /// pool prices and the prices at the tick boundaries 64 | function getLiquidityForAmounts( 65 | uint160 sqrtRatioX96, 66 | uint160 sqrtRatioAX96, 67 | uint160 sqrtRatioBX96, 68 | uint256 amount0, 69 | uint256 amount1 70 | ) internal pure returns (uint128 liquidity) { 71 | if (sqrtRatioAX96 > sqrtRatioBX96) 72 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 73 | 74 | if (sqrtRatioX96 <= sqrtRatioAX96) { 75 | liquidity = getLiquidityForAmount0( 76 | sqrtRatioAX96, 77 | sqrtRatioBX96, 78 | amount0 79 | ); 80 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 81 | uint128 liquidity0 = 82 | getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0); 83 | uint128 liquidity1 = 84 | getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1); 85 | 86 | liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; 87 | } else { 88 | liquidity = getLiquidityForAmount1( 89 | sqrtRatioAX96, 90 | sqrtRatioBX96, 91 | amount1 92 | ); 93 | } 94 | } 95 | 96 | /// @notice Computes the amount of token0 for a given amount of liquidity and a price range 97 | /// @param sqrtRatioAX96 A sqrt price 98 | /// @param sqrtRatioBX96 Another sqrt price 99 | /// @param liquidity The liquidity being valued 100 | /// @return amount0 The amount0 101 | function getAmount0ForLiquidity( 102 | uint160 sqrtRatioAX96, 103 | uint160 sqrtRatioBX96, 104 | uint128 liquidity 105 | ) internal pure returns (uint256 amount0) { 106 | if (sqrtRatioAX96 > sqrtRatioBX96) 107 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 108 | 109 | return 110 | FullMath.mulDiv( 111 | uint256(liquidity) << FixedPoint96.RESOLUTION, 112 | sqrtRatioBX96 - sqrtRatioAX96, 113 | sqrtRatioBX96 114 | ) / sqrtRatioAX96; 115 | } 116 | 117 | /// @notice Computes the amount of token1 for a given amount of liquidity and a price range 118 | /// @param sqrtRatioAX96 A sqrt price 119 | /// @param sqrtRatioBX96 Another sqrt price 120 | /// @param liquidity The liquidity being valued 121 | /// @return amount1 The amount1 122 | function getAmount1ForLiquidity( 123 | uint160 sqrtRatioAX96, 124 | uint160 sqrtRatioBX96, 125 | uint128 liquidity 126 | ) internal pure returns (uint256 amount1) { 127 | if (sqrtRatioAX96 > sqrtRatioBX96) 128 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 129 | 130 | return 131 | FullMath.mulDiv( 132 | liquidity, 133 | sqrtRatioBX96 - sqrtRatioAX96, 134 | FixedPoint96.Q96 135 | ); 136 | } 137 | 138 | /// @notice Computes the token0 and token1 value for a given amount of liquidity, the current 139 | /// pool prices and the prices at the tick boundaries 140 | function getAmountsForLiquidity( 141 | uint160 sqrtRatioX96, 142 | uint160 sqrtRatioAX96, 143 | uint160 sqrtRatioBX96, 144 | uint128 liquidity 145 | ) internal pure returns (uint256 amount0, uint256 amount1) { 146 | if (sqrtRatioAX96 > sqrtRatioBX96) 147 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 148 | 149 | if (sqrtRatioX96 <= sqrtRatioAX96) { 150 | amount0 = getAmount0ForLiquidity( 151 | sqrtRatioAX96, 152 | sqrtRatioBX96, 153 | liquidity 154 | ); 155 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 156 | amount0 = getAmount0ForLiquidity( 157 | sqrtRatioX96, 158 | sqrtRatioBX96, 159 | liquidity 160 | ); 161 | amount1 = getAmount1ForLiquidity( 162 | sqrtRatioAX96, 163 | sqrtRatioX96, 164 | liquidity 165 | ); 166 | } else { 167 | amount1 = getAmount1ForLiquidity( 168 | sqrtRatioAX96, 169 | sqrtRatioBX96, 170 | liquidity 171 | ); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /contracts/uniswap/TickMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.4; 3 | 4 | /// @title Math library for computing sqrt prices from ticks and vice versa 5 | /// @notice Computes sqrt price for ticks of size 1.0001, i.e. sqrt(1.0001^tick) as fixed point Q64.96 numbers. Supports 6 | /// prices between 2**-128 and 2**128 7 | library TickMath { 8 | /// @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128 9 | int24 internal constant MIN_TICK = -887272; 10 | /// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 11 | int24 internal constant MAX_TICK = -MIN_TICK; 12 | 13 | /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) 14 | uint160 internal constant MIN_SQRT_RATIO = 4295128739; 15 | /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) 16 | uint160 internal constant MAX_SQRT_RATIO = 17 | 1461446703485210103287273052203988822378723970342; 18 | 19 | /// @notice Calculates sqrt(1.0001^tick) * 2^96 20 | /// @dev Throws if |tick| > max tick 21 | /// @param tick The input tick for the above formula 22 | /// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0) 23 | /// at the given tick 24 | function getSqrtRatioAtTick(int24 tick) 25 | internal 26 | pure 27 | returns (uint160 sqrtPriceX96) 28 | { 29 | uint256 absTick = 30 | tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); 31 | 32 | // EDIT: 0.8 compatibility 33 | require(absTick <= uint256(int256(MAX_TICK)), "T"); 34 | 35 | uint256 ratio = 36 | absTick & 0x1 != 0 37 | ? 0xfffcb933bd6fad37aa2d162d1a594001 38 | : 0x100000000000000000000000000000000; 39 | if (absTick & 0x2 != 0) 40 | ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; 41 | if (absTick & 0x4 != 0) 42 | ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; 43 | if (absTick & 0x8 != 0) 44 | ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; 45 | if (absTick & 0x10 != 0) 46 | ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; 47 | if (absTick & 0x20 != 0) 48 | ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; 49 | if (absTick & 0x40 != 0) 50 | ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; 51 | if (absTick & 0x80 != 0) 52 | ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; 53 | if (absTick & 0x100 != 0) 54 | ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; 55 | if (absTick & 0x200 != 0) 56 | ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; 57 | if (absTick & 0x400 != 0) 58 | ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; 59 | if (absTick & 0x800 != 0) 60 | ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; 61 | if (absTick & 0x1000 != 0) 62 | ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; 63 | if (absTick & 0x2000 != 0) 64 | ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; 65 | if (absTick & 0x4000 != 0) 66 | ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; 67 | if (absTick & 0x8000 != 0) 68 | ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; 69 | if (absTick & 0x10000 != 0) 70 | ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; 71 | if (absTick & 0x20000 != 0) 72 | ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; 73 | if (absTick & 0x40000 != 0) 74 | ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; 75 | if (absTick & 0x80000 != 0) 76 | ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; 77 | 78 | if (tick > 0) ratio = type(uint256).max / ratio; 79 | 80 | // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. 81 | // we then downcast because we know the result always fits within 160 bits due to our tick input constraint 82 | // we round up in the division so getTickAtSqrtRatio of the output price is always consistent 83 | sqrtPriceX96 = uint160( 84 | (ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1) 85 | ); 86 | } 87 | 88 | /// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio 89 | /// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may 90 | /// ever return. 91 | /// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96 92 | /// @return tick The greatest tick for which the ratio is less than or equal to the input ratio 93 | function getTickAtSqrtRatio(uint160 sqrtPriceX96) 94 | internal 95 | pure 96 | returns (int24 tick) 97 | { 98 | // second inequality must be < because the price can never reach the price at the max tick 99 | require( 100 | sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 101 | "R" 102 | ); 103 | uint256 ratio = uint256(sqrtPriceX96) << 32; 104 | 105 | uint256 r = ratio; 106 | uint256 msb = 0; 107 | 108 | assembly { 109 | let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) 110 | msb := or(msb, f) 111 | r := shr(f, r) 112 | } 113 | assembly { 114 | let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF)) 115 | msb := or(msb, f) 116 | r := shr(f, r) 117 | } 118 | assembly { 119 | let f := shl(5, gt(r, 0xFFFFFFFF)) 120 | msb := or(msb, f) 121 | r := shr(f, r) 122 | } 123 | assembly { 124 | let f := shl(4, gt(r, 0xFFFF)) 125 | msb := or(msb, f) 126 | r := shr(f, r) 127 | } 128 | assembly { 129 | let f := shl(3, gt(r, 0xFF)) 130 | msb := or(msb, f) 131 | r := shr(f, r) 132 | } 133 | assembly { 134 | let f := shl(2, gt(r, 0xF)) 135 | msb := or(msb, f) 136 | r := shr(f, r) 137 | } 138 | assembly { 139 | let f := shl(1, gt(r, 0x3)) 140 | msb := or(msb, f) 141 | r := shr(f, r) 142 | } 143 | assembly { 144 | let f := gt(r, 0x1) 145 | msb := or(msb, f) 146 | } 147 | 148 | if (msb >= 128) r = ratio >> (msb - 127); 149 | else r = ratio << (127 - msb); 150 | 151 | int256 log_2 = (int256(msb) - 128) << 64; 152 | 153 | assembly { 154 | r := shr(127, mul(r, r)) 155 | let f := shr(128, r) 156 | log_2 := or(log_2, shl(63, f)) 157 | r := shr(f, r) 158 | } 159 | assembly { 160 | r := shr(127, mul(r, r)) 161 | let f := shr(128, r) 162 | log_2 := or(log_2, shl(62, f)) 163 | r := shr(f, r) 164 | } 165 | assembly { 166 | r := shr(127, mul(r, r)) 167 | let f := shr(128, r) 168 | log_2 := or(log_2, shl(61, f)) 169 | r := shr(f, r) 170 | } 171 | assembly { 172 | r := shr(127, mul(r, r)) 173 | let f := shr(128, r) 174 | log_2 := or(log_2, shl(60, f)) 175 | r := shr(f, r) 176 | } 177 | assembly { 178 | r := shr(127, mul(r, r)) 179 | let f := shr(128, r) 180 | log_2 := or(log_2, shl(59, f)) 181 | r := shr(f, r) 182 | } 183 | assembly { 184 | r := shr(127, mul(r, r)) 185 | let f := shr(128, r) 186 | log_2 := or(log_2, shl(58, f)) 187 | r := shr(f, r) 188 | } 189 | assembly { 190 | r := shr(127, mul(r, r)) 191 | let f := shr(128, r) 192 | log_2 := or(log_2, shl(57, f)) 193 | r := shr(f, r) 194 | } 195 | assembly { 196 | r := shr(127, mul(r, r)) 197 | let f := shr(128, r) 198 | log_2 := or(log_2, shl(56, f)) 199 | r := shr(f, r) 200 | } 201 | assembly { 202 | r := shr(127, mul(r, r)) 203 | let f := shr(128, r) 204 | log_2 := or(log_2, shl(55, f)) 205 | r := shr(f, r) 206 | } 207 | assembly { 208 | r := shr(127, mul(r, r)) 209 | let f := shr(128, r) 210 | log_2 := or(log_2, shl(54, f)) 211 | r := shr(f, r) 212 | } 213 | assembly { 214 | r := shr(127, mul(r, r)) 215 | let f := shr(128, r) 216 | log_2 := or(log_2, shl(53, f)) 217 | r := shr(f, r) 218 | } 219 | assembly { 220 | r := shr(127, mul(r, r)) 221 | let f := shr(128, r) 222 | log_2 := or(log_2, shl(52, f)) 223 | r := shr(f, r) 224 | } 225 | assembly { 226 | r := shr(127, mul(r, r)) 227 | let f := shr(128, r) 228 | log_2 := or(log_2, shl(51, f)) 229 | r := shr(f, r) 230 | } 231 | assembly { 232 | r := shr(127, mul(r, r)) 233 | let f := shr(128, r) 234 | log_2 := or(log_2, shl(50, f)) 235 | } 236 | 237 | int256 log_sqrt10001 = log_2 * 255738958999603826347141; // 128.128 number 238 | 239 | int24 tickLow = 240 | int24( 241 | (log_sqrt10001 - 3402992956809132418596140100660247210) >> 128 242 | ); 243 | int24 tickHi = 244 | int24( 245 | (log_sqrt10001 + 291339464771989622907027621153398088495) >> 128 246 | ); 247 | 248 | tick = tickLow == tickHi 249 | ? tickLow 250 | : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 251 | ? tickHi 252 | : tickLow; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /deploy/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "implementation": "0xa865Be0D6C37a6E84B1864faeBcFB7aB16540a76", 3 | "token0": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", 4 | "token1": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 5 | "fee": 100, 6 | "name": "Range Uniswap token0/token1 1% LP", 7 | "symbol": "R-UNI", 8 | "manager": "0x278E468e73B661780A5985DB366B3C67d8Af78b4", 9 | "ammFactory": "0x1F98431c8aD98523631AE4a59f267346ea31F984", 10 | "rangeFactory": "0x53A768784B2372CC6C65e7a8d2641EC51f1a3690" 11 | } -------------------------------------------------------------------------------- /deploy/hot-deployment/factory.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | const configPath = path.join(__dirname, "../config.json"); 6 | async function main() { 7 | const configData = JSON.parse(fs.readFileSync(configPath)); 8 | const AMM_FACTORY = configData.ammFactory; 9 | const RangeProtocolFactory = await ethers.getContractFactory( 10 | "RangeProtocolFactory" 11 | ); 12 | const factory = await RangeProtocolFactory.deploy(AMM_FACTORY); 13 | console.log("Factory: ", factory.address); 14 | configData.rangeFactory = factory.address; 15 | fs.writeFileSync(configPath, JSON.stringify(configData)); 16 | console.log("DONE!"); 17 | } 18 | // We recommend this pattern to be able to use async/await everywhere 19 | // and properly handle errors. 20 | main().catch((error) => { 21 | console.error(error); 22 | process.exitCode = 1; 23 | }); 24 | -------------------------------------------------------------------------------- /deploy/hot-deployment/implementation.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | const configPath = path.join(__dirname, "../config.json"); 6 | async function main() { 7 | const RangeProtocolVault = await ethers.getContractFactory("RangeProtocolVault"); 8 | const vaultImpl = await RangeProtocolVault.deploy(); 9 | console.log("Implementation: ", vaultImpl.address); 10 | const configData = JSON.parse(fs.readFileSync(configPath)); 11 | configData.implementation = vaultImpl.address; 12 | fs.writeFileSync(configPath, JSON.stringify(configData)); 13 | console.log("DONE!"); 14 | } 15 | 16 | // We recommend this pattern to be able to use async/await everywhere 17 | // and properly handle errors. 18 | main().catch((error) => { 19 | console.error(error); 20 | process.exitCode = 1; 21 | }); 22 | -------------------------------------------------------------------------------- /deploy/hot-deployment/vault.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { getInitializeData } from "../../test/common"; 5 | 6 | const configPath = path.join(__dirname, "../config.json"); 7 | async function main() { 8 | const configData = JSON.parse(fs.readFileSync(configPath)); 9 | const factory = await ethers.getContractAt( 10 | "RangeProtocolFactory", 11 | configData.rangeFactory 12 | ); 13 | const data = getInitializeData({ 14 | managerAddress: configData.manager, 15 | name: configData.name, 16 | symbol: configData.symbol, 17 | }); 18 | 19 | const tx = await factory.createVault( 20 | configData.token0, 21 | configData.token1, 22 | configData.fee, 23 | configData.implementation, 24 | data 25 | ); 26 | const txReceipt = await tx.wait(); 27 | const [ 28 | { 29 | args: { vault }, 30 | }, 31 | ] = txReceipt.events.filter( 32 | (event: { event: any }) => event.event === "VaultCreated" 33 | ); 34 | console.log("Vault: ", vault); 35 | } 36 | 37 | // We recommend this pattern to be able to use async/await everywhere 38 | // and properly handle errors. 39 | main().catch((error) => { 40 | console.error(error); 41 | process.exitCode = 1; 42 | }); 43 | -------------------------------------------------------------------------------- /deploy/ledger/RangeProtocolFactory.deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { LedgerSigner } from "@anders-t/ethers-ledger"; 3 | async function main() { 4 | const provider = ethers.getDefaultProvider(""); 5 | const ledger = await new LedgerSigner(provider, ""); 6 | const UNI_V3_FACTORY = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; 7 | let RangeProtocolFactory = await ethers.getContractFactory( 8 | "RangeProtocolFactory" 9 | ); 10 | RangeProtocolFactory = await RangeProtocolFactory.connect(ledger); 11 | const factory = await RangeProtocolFactory.deploy(UNI_V3_FACTORY); 12 | console.log("Factory: ", factory.address); 13 | } 14 | // We recommend this pattern to be able to use async/await everywhere 15 | // and properly handle errors. 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); 20 | -------------------------------------------------------------------------------- /deploy/ledger/RangeProtocolVault.implementation.deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { LedgerSigner } from "@anders-t/ethers-ledger"; 3 | import { getInitializeData } from "../test/common"; 4 | 5 | async function main() { 6 | const provider = ethers.getDefaultProvider(""); // To be updated. 7 | const ledger = await new LedgerSigner(provider, ""); // To be updated. 8 | let RangeProtocolVault = await ethers.getContractFactory( 9 | "RangeProtocolVault" 10 | ); 11 | RangeProtocolVault = await RangeProtocolVault.connect(ledger); 12 | const vaultImpl = await RangeProtocolVault.deploy(); 13 | console.log(vaultImpl.address); 14 | } 15 | 16 | // We recommend this pattern to be able to use async/await everywhere 17 | // and properly handle errors. 18 | main().catch((error) => { 19 | console.error(error); 20 | process.exitCode = 1; 21 | }); 22 | -------------------------------------------------------------------------------- /deploy/ledger/RangeProtocolVault.proxy.deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { LedgerSigner } from "@anders-t/ethers-ledger"; 3 | import { getInitializeData } from "../test/common"; 4 | 5 | async function main() { 6 | const provider = ethers.getDefaultProvider(""); // To be updated. 7 | const ledger = await new LedgerSigner(provider, ""); // To be updated. 8 | const managerAddress = "0x84b43ce5fB1FAF013181FEA96ffA4af6179e396a"; // To be updated. 9 | const rangeProtocolFactoryAddress = 10 | "0x4bF9CDcCE12924B559928623a5d23598ca19367B"; // To be updated. 11 | const vaultImplAddress = ""; // to be updated. 12 | const token0 = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; 13 | const token1 = "0xc944E90C64B2c07662A292be6244BDf05Cda44a7"; 14 | const fee = 3000; // To be updated. 15 | const name = ""; // To be updated. 16 | const symbol = ""; // To be updated. 17 | 18 | let factory = await ethers.getContractAt( 19 | "RangeProtocolFactory", 20 | rangeProtocolFactoryAddress 21 | ); 22 | factory = await factory.connect(ledger); 23 | const data = getInitializeData({ 24 | managerAddress, 25 | name, 26 | symbol, 27 | }); 28 | 29 | const tx = await factory.createVault(token0, token1, fee, vaultImplAddress, data); 30 | const txReceipt = await tx.wait(); 31 | const [ 32 | { 33 | args: { vault }, 34 | }, 35 | ] = txReceipt.events.filter( 36 | (event: { event: any }) => event.event === "VaultCreated" 37 | ); 38 | console.log("Vault: ", vault); 39 | } 40 | 41 | // We recommend this pattern to be able to use async/await everywhere 42 | // and properly handle errors. 43 | main().catch((error) => { 44 | console.error(error); 45 | process.exitCode = 1; 46 | }); 47 | -------------------------------------------------------------------------------- /deploy/mock/dummy-token.ts: -------------------------------------------------------------------------------- 1 | import {ethers} from "hardhat"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | const configPath = path.join(__dirname, "../config.json"); 6 | async function main() { 7 | const configData = JSON.parse(fs.readFileSync(configPath)); 8 | const AMM_FACTORY = configData.ammFactory; 9 | const MockERC20 = await ethers.getContractFactory( 10 | "MockERC20" 11 | ); 12 | const token0 = await MockERC20.deploy(); 13 | const token1 = await MockERC20.deploy(); 14 | configData.token0 = token0.address; 15 | configData.token1 = token1.address; 16 | fs.writeFileSync(configPath, JSON.stringify(configData)); 17 | console.log("DONE!"); 18 | } 19 | 20 | // We recommend this pattern to be able to use async/await everywhere 21 | // and properly handle errors. 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | 3 | // PLUGINS 4 | import "@nomiclabs/hardhat-ethers"; 5 | import "@nomiclabs/hardhat-etherscan"; 6 | import "@nomiclabs/hardhat-waffle"; 7 | import "@typechain/hardhat"; 8 | import "hardhat-deploy"; 9 | import "solidity-coverage"; 10 | import "@nomicfoundation/hardhat-chai-matchers"; 11 | import "hardhat-gas-reporter"; 12 | import "hardhat-contract-sizer"; 13 | 14 | // Process Env Variables 15 | import * as dotenv from "dotenv"; 16 | dotenv.config({ path: __dirname + "/.env" }); 17 | const ALCHEMY_ID = process.env.ALCHEMY_ID; 18 | 19 | const config: HardhatUserConfig = { 20 | defaultNetwork: "hardhat", 21 | etherscan: { 22 | apiKey: "", 23 | }, 24 | networks: { 25 | hardhat: { 26 | allowUnlimitedContractSize: true, 27 | forking: { 28 | url: "https://eth-mainnet.g.alchemy.com/v2/_5K15-wfBoWkGwdonG4o77iUgon8ut3N", 29 | }, 30 | }, 31 | mainnet: { 32 | accounts: process.env.PK ? [process.env.PK] : [], 33 | chainId: 1, 34 | url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_ID}`, 35 | }, 36 | tenderly: { 37 | accounts: process.env.PK ? [process.env.PK] : [], 38 | url: "https://rpc.vnet.tenderly.co/devnet/sop-integration/a3cce0b2-7ec7-4ab7-b587-287ae7df97df", 39 | }, 40 | }, 41 | 42 | solidity: { 43 | compilers: [ 44 | { 45 | version: "0.7.3", 46 | settings: { 47 | optimizer: { enabled: true, runs: 100 }, 48 | }, 49 | }, 50 | { 51 | version: "0.8.4", 52 | settings: { 53 | optimizer: { enabled: true, runs: 100 }, 54 | }, 55 | }, 56 | ], 57 | }, 58 | external: { 59 | contracts: [ 60 | { 61 | artifacts: "node_modules/@uniswap/v3-core/artifacts", 62 | }, 63 | { 64 | artifacts: "node_modules/@uniswap/v3-periphery/artifacts", 65 | }, 66 | ], 67 | }, 68 | typechain: { 69 | outDir: "typechain", 70 | target: "ethers-v5", 71 | }, 72 | }; 73 | 74 | export default config; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RangeProtocolVault", 3 | "scripts": { 4 | "build": "yarn compile && npx tsc", 5 | "compile": "npx hardhat compile", 6 | "clean": "rm -rf dist && npx hardhat clean", 7 | "deploy": "npx hardhat deploy ", 8 | "format": "prettier --write .", 9 | "format:check": "prettier --check '*/**/*.{js,sol,json,md,ts}'", 10 | "lint": "eslint --cache . && yarn lint:sol", 11 | "lint:ts": "eslint -c .eslintrc.json --ext \"**/*.ts\" \"**/*.test.ts\"", 12 | "lint:sol": "solhint 'contracts/**/*.sol'", 13 | "postinstall": "yarn husky install", 14 | "test": "npx hardhat test", 15 | "verify": "npx hardhat etherscan-verify", 16 | "prettier": "prettier --write 'contracts/**/**/*.sol'", 17 | "size": "npx hardhat size-contracts", 18 | "deploy-factory": "npx hardhat run deploy/hot-deployment/factory.ts --network chain", 19 | "deploy-implementation": "npx hardhat run deploy/hot-deployment/implementation.ts --network chain", 20 | "deploy-vault": "npx hardhat run deploy/hot-deployment/vault.ts --network chain" 21 | }, 22 | "devDependencies": { 23 | "@anders-t/ethers-ledger": "^1.0.4", 24 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", 25 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", 26 | "@nomiclabs/hardhat-etherscan": "2.1.2", 27 | "@nomiclabs/hardhat-waffle": "2.0.1", 28 | "@openzeppelin/contracts": "4.1.0", 29 | "@openzeppelin/contracts-upgradeable": "4.8.2", 30 | "@tsconfig/recommended": "1.0.1", 31 | "@typechain/ethers-v5": "7.0.0", 32 | "@typechain/hardhat": "2.0.1", 33 | "@types/chai": "4.2.18", 34 | "@types/mocha": "8.2.2", 35 | "@types/node": "15.3.0", 36 | "@typescript-eslint/eslint-plugin": "4.24.0", 37 | "@typescript-eslint/parser": "4.24.0", 38 | "@uniswap/v3-core": "1.0.0", 39 | "@uniswap/v3-periphery": "^1.4.3", 40 | "chai": "4.3.4", 41 | "dotenv": "9.0.2", 42 | "eslint": "7.26.0", 43 | "eslint-config-prettier": "8.3.0", 44 | "eslint-plugin-prettier": "3.4.0", 45 | "ethereum-waffle": "3.3.0", 46 | "ethers": "5.6.2", 47 | "hardhat": "2.12.7", 48 | "hardhat-contract-sizer": "^2.10.0", 49 | "hardhat-deploy": "0.9.14", 50 | "hardhat-gas-reporter": "^1.0.9", 51 | "husky": "6.0.0", 52 | "lint-staged": "11.0.0", 53 | "node-fetch": "2.6.1", 54 | "prettier": "^2.8.4", 55 | "prettier-plugin-solidity": "^1.1.3", 56 | "solhint": "3.3.4", 57 | "solhint-plugin-prettier": "0.0.5", 58 | "solidity-coverage": "0.7.16", 59 | "ts-generator": "0.1.1", 60 | "ts-node": "9.1.1", 61 | "typechain": "5.0.0", 62 | "typescript": "4.2.4" 63 | }, 64 | "lint-staged": { 65 | "*.{ts,js}": "eslint -c .eslintrc.json" 66 | }, 67 | "version": "0.0.0", 68 | "dependencies": { 69 | "decimal.js": "^10.4.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/UpgradeImplementation.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { LedgerSigner } from "@anders-t/ethers-ledger"; 3 | import {RangeProtocolFactory} from "../typechain"; 4 | async function main() { 5 | const VAULT = "0x510982F346cF8083FE935080cD61a78E2E7E8fd1"; 6 | const IMPLEMENTATION = ""; 7 | const provider = ethers.getDefaultProvider(""); 8 | const ledger = await new LedgerSigner(provider, ""); 9 | let factory = await ethers.getContractAt( 10 | "RangeProtocolFactory", 11 | "0x4bF9CDcCE12924B559928623a5d23598ca19367B" 12 | ) as RangeProtocolFactory; 13 | factory = await factory.connect(ledger); 14 | 15 | await factory.upgradeVault(VAULT, IMPLEMENTATION); 16 | } 17 | // We recommend this pattern to be able to use async/await everywhere 18 | // and properly handle errors. 19 | main().catch((error) => { 20 | console.error(error); 21 | process.exitCode = 1; 22 | }); 23 | -------------------------------------------------------------------------------- /scripts/deploy-vault.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { LedgerSigner } from "@anders-t/ethers-ledger"; 3 | import {RangeProtocolFactory} from "../typechain"; 4 | import {getInitializeData} from "../test/common"; 5 | async function main() { 6 | const managerAddress = ""; // to be updated. 7 | const token0 = ""; // to be updated. 8 | const token1 = ""; // to be updated. 9 | const fee = 100; 10 | const name = ""; // To be updated. 11 | const symbol = ""; // To be updated. 12 | const vaultImplAddress = ""; 13 | const data = getInitializeData({ 14 | managerAddress, 15 | name, 16 | symbol, 17 | }); 18 | const createVaultInterface = new ethers.utils.Interface([ 19 | "function createVault(address tokenA, address tokenB, uint24 fee, address implementation, bytes memory data)" 20 | ]); 21 | const txData = createVaultInterface.encodeFunctionData("createVault", [ 22 | token0, 23 | token1, 24 | fee, 25 | vaultImplAddress, 26 | data 27 | ]); 28 | console.log(txData); 29 | } 30 | // We recommend this pattern to be able to use async/await everywhere 31 | // and properly handle errors. 32 | main().catch((error) => { 33 | console.error(error); 34 | process.exitCode = 1; 35 | }); 36 | -------------------------------------------------------------------------------- /scripts/timelock-execute-update-owner.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const newOwner = ""; // to be updated 5 | const factoryAddress = ""; // to be updated 6 | 7 | const transferOwnershipInterface = new ethers.utils.Interface([ 8 | "function transferOwnership(address) external", 9 | ]); 10 | const updateOwnerdata = transferOwnershipInterface.encodeFunctionData( 11 | "transferOwnership", 12 | [newOwner] 13 | ); 14 | 15 | const timeLockInterface = new ethers.utils.Interface([ 16 | "function execute(address,uint256,bytes,bytes32,bytes32) external" 17 | ]); 18 | const data = timeLockInterface.encodeFunctionData("execute", [ 19 | factoryAddress, 20 | 0, 21 | updateOwnerdata, 22 | ethers.utils.zeroPad("0x", 32), 23 | ethers.utils.zeroPad("0x", 32), 24 | ]); 25 | 26 | console.log("data: ", data); 27 | } 28 | 29 | // We recommend this pattern to be able to use async/await everywhere 30 | // and properly handle errors. 31 | main().catch((error) => { 32 | console.error(error); 33 | process.exitCode = 1; 34 | }); -------------------------------------------------------------------------------- /scripts/timelock-schedule-update-owner.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const newOwner = ""; // to be updated 5 | const factoryAddress = ""; // to be updated 6 | 7 | const transferOwnershipInterface = new ethers.utils.Interface([ 8 | "function transferOwnership(address) external", 9 | ]); 10 | const updateOwnerdata = transferOwnershipInterface.encodeFunctionData( 11 | "transferOwnership", 12 | [newOwner] 13 | ); 14 | 15 | const timeLockInterface = new ethers.utils.Interface([ 16 | "function schedule(address,uint256,bytes,bytes32,bytes32,uint256) external", 17 | ]); 18 | const data = timeLockInterface.encodeFunctionData("schedule", [ 19 | factoryAddress, 20 | 0, 21 | updateOwnerdata, 22 | ethers.utils.zeroPad("0x", 32), 23 | ethers.utils.zeroPad("0x", 32), 24 | 300 25 | ]); 26 | 27 | console.log("data: ", data); 28 | } 29 | 30 | // We recommend this pattern to be able to use async/await everywhere 31 | // and properly handle errors. 32 | main().catch((error) => { 33 | console.error(error); 34 | process.exitCode = 1; 35 | }); -------------------------------------------------------------------------------- /scripts/updateOwner.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const newOwner = ""; // to be updated 5 | const transferOwnershipInterface = new ethers.utils.Interface([ 6 | "function transferOwnership(address) external", 7 | ]); 8 | const data = transferOwnershipInterface.encodeFunctionData( 9 | "transferOwnership", 10 | [newOwner] 11 | ); 12 | console.log("data: ", data); 13 | } 14 | 15 | // We recommend this pattern to be able to use async/await everywhere 16 | // and properly handle errors. 17 | main().catch((error) => { 18 | console.error(error); 19 | process.exitCode = 1; 20 | }); 21 | -------------------------------------------------------------------------------- /test/RangeProtocolFactory.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 5 | import { 6 | IERC20, 7 | IUniswapV3Factory, 8 | IUniswapV3Pool, 9 | RangeProtocolVault, 10 | RangeProtocolFactory, 11 | } from "../typechain"; 12 | import { bn, getInitializeData, ZERO_ADDRESS } from "./common"; 13 | import { Contract } from "ethers"; 14 | 15 | let factory: RangeProtocolFactory; 16 | let vaultImpl: RangeProtocolVault; 17 | let uniV3Factory: IUniswapV3Factory; 18 | let univ3Pool: IUniswapV3Pool; 19 | let token0: IERC20; 20 | let token1: IERC20; 21 | let owner: SignerWithAddress; 22 | let nonOwner: SignerWithAddress; 23 | let newOwner: SignerWithAddress; 24 | const poolFee = 10000; 25 | const name = "Test Token"; 26 | const symbol = "TT"; 27 | let initializeData: any; 28 | 29 | describe("RangeProtocolFactory", () => { 30 | before(async function () { 31 | [owner, nonOwner, newOwner] = await ethers.getSigners(); 32 | // eslint-disable-next-line @typescript-eslint/naming-convention 33 | const UniswapV3Factory = await ethers.getContractFactory( 34 | "UniswapV3Factory" 35 | ); 36 | uniV3Factory = (await UniswapV3Factory.deploy()) as IUniswapV3Factory; 37 | 38 | // eslint-disable-next-line @typescript-eslint/naming-convention 39 | const RangeProtocolFactory = await ethers.getContractFactory( 40 | "RangeProtocolFactory" 41 | ); 42 | factory = (await RangeProtocolFactory.deploy( 43 | uniV3Factory.address 44 | )) as RangeProtocolFactory; 45 | 46 | // eslint-disable-next-line @typescript-eslint/naming-convention 47 | const MockERC20 = await ethers.getContractFactory("MockERC20"); 48 | token0 = (await MockERC20.deploy()) as IERC20; 49 | token1 = (await MockERC20.deploy()) as IERC20; 50 | 51 | if (bn(token0.address).gt(token1.address)) { 52 | const tmp = token0; 53 | token0 = token1; 54 | token1 = tmp; 55 | } 56 | 57 | await uniV3Factory.createPool(token0.address, token1.address, poolFee); 58 | univ3Pool = (await ethers.getContractAt( 59 | "IUniswapV3Pool", 60 | await uniV3Factory.getPool(token0.address, token1.address, poolFee) 61 | )) as IUniswapV3Pool; 62 | 63 | // eslint-disable-next-line @typescript-eslint/naming-convention 64 | const RangeProtocolVault = await ethers.getContractFactory( 65 | "RangeProtocolVault" 66 | ); 67 | vaultImpl = (await RangeProtocolVault.deploy()) as RangeProtocolVault; 68 | 69 | initializeData = getInitializeData({ 70 | managerAddress: owner.address, 71 | name, 72 | symbol, 73 | }); 74 | }); 75 | 76 | it("should deploy RangeProtocolFactory", async function () { 77 | expect(await factory.factory()).to.be.equal(uniV3Factory.address); 78 | expect(await factory.owner()).to.be.equal(owner.address); 79 | }); 80 | 81 | it("should not deploy a vault with one of the tokens being zero", async function () { 82 | await expect( 83 | factory.createVault( 84 | ZERO_ADDRESS, 85 | token1.address, 86 | poolFee, 87 | vaultImpl.address, 88 | initializeData 89 | ) 90 | ).to.be.revertedWith("ZeroPoolAddress()"); 91 | }); 92 | 93 | it("should not deploy a vault with both tokens being the same", async function () { 94 | await expect( 95 | factory.createVault( 96 | token0.address, 97 | token0.address, 98 | poolFee, 99 | vaultImpl.address, 100 | initializeData 101 | ) 102 | ).to.be.revertedWith("ZeroPoolAddress()"); 103 | }); 104 | 105 | it("should not deploy vault with zero manager address", async function () { 106 | await expect( 107 | factory.createVault( 108 | token0.address, 109 | token1.address, 110 | poolFee, 111 | vaultImpl.address, 112 | getInitializeData({ 113 | managerAddress: ZERO_ADDRESS, 114 | name, 115 | symbol, 116 | }) 117 | ) 118 | ).to.be.revertedWith("ZeroManagerAddress()"); 119 | }); 120 | 121 | it("non-owner should not be able to deploy vault", async function () { 122 | await expect( 123 | factory 124 | .connect(nonOwner) 125 | .createVault( 126 | token0.address, 127 | token1.address, 128 | poolFee, 129 | vaultImpl.address, 130 | initializeData 131 | ) 132 | ).to.be.revertedWith("Ownable: caller is not the owner"); 133 | }); 134 | 135 | it("owner should be able to deploy vault", async function () { 136 | await expect( 137 | factory.createVault( 138 | token0.address, 139 | token1.address, 140 | poolFee, 141 | vaultImpl.address, 142 | initializeData 143 | ) 144 | ) 145 | .to.emit(factory, "VaultCreated") 146 | .withArgs((univ3Pool as Contract).address, anyValue); 147 | 148 | expect(await factory.vaultCount()).to.be.equal(1); 149 | expect((await factory.getVaultAddresses(0, 0))[0]).to.not.be.equal( 150 | ethers.constants.AddressZero 151 | ); 152 | }); 153 | 154 | it("should allow deploying vault with duplicate pairs", async function () { 155 | await expect( 156 | factory.createVault( 157 | token0.address, 158 | token1.address, 159 | poolFee, 160 | vaultImpl.address, 161 | initializeData 162 | ) 163 | ) 164 | .to.emit(factory, "VaultCreated") 165 | .withArgs((univ3Pool as Contract).address, anyValue); 166 | 167 | expect(await factory.vaultCount()).to.be.equal(2); 168 | const vault0Address = (await factory.getVaultAddresses(0, 0))[0]; 169 | const vault1Address = (await factory.getVaultAddresses(1, 1))[0]; 170 | 171 | expect(vault0Address).to.not.be.equal(ethers.constants.AddressZero); 172 | expect(vault1Address).to.not.be.equal(ethers.constants.AddressZero); 173 | 174 | const dataABI = new ethers.utils.Interface([ 175 | "function token0() returns (address)", 176 | "function token1() returns (address)", 177 | ]); 178 | 179 | expect(vault0Address).to.be.not.equal(vault1Address); 180 | expect( 181 | await ethers.provider.call({ 182 | to: vault0Address, 183 | data: dataABI.encodeFunctionData("token0"), 184 | }) 185 | ).to.be.equal( 186 | await ethers.provider.call({ 187 | to: vault1Address, 188 | data: dataABI.encodeFunctionData("token0"), 189 | }) 190 | ); 191 | 192 | expect( 193 | await ethers.provider.call({ 194 | to: vault0Address, 195 | data: dataABI.encodeFunctionData("token1"), 196 | }) 197 | ).to.be.equal( 198 | await ethers.provider.call({ 199 | to: vault1Address, 200 | data: dataABI.encodeFunctionData("token1"), 201 | }) 202 | ); 203 | }); 204 | 205 | describe("transferOwnership", () => { 206 | it("should not be able to transferOwnership by non owner", async () => { 207 | await expect( 208 | factory.connect(nonOwner).transferOwnership(newOwner.address) 209 | ).to.be.revertedWith("Ownable: caller is not the owner"); 210 | }); 211 | 212 | it("should be able to transferOwnership by owner", async () => { 213 | await expect(factory.transferOwnership(newOwner.address)) 214 | .to.emit(factory, "OwnershipTransferred") 215 | .withArgs(owner.address, newOwner.address); 216 | expect(await factory.owner()).to.be.equal(newOwner.address); 217 | 218 | await factory.connect(newOwner).transferOwnership(owner.address); 219 | expect(await factory.owner()).to.be.equal(owner.address); 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/RangeProtocolVault.exposure.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { Decimal } from "decimal.js"; 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 5 | import { 6 | IERC20, 7 | IUniswapV3Factory, 8 | IUniswapV3Pool, 9 | RangeProtocolVault, 10 | RangeProtocolFactory, 11 | SwapTest, 12 | } from "../typechain"; 13 | import { 14 | bn, 15 | encodePriceSqrt, 16 | getInitializeData, 17 | parseEther, 18 | position, 19 | } from "./common"; 20 | import { beforeEach } from "mocha"; 21 | import { BigNumber } from "ethers"; 22 | 23 | let factory: RangeProtocolFactory; 24 | let vaultImpl: RangeProtocolVault; 25 | let vault: RangeProtocolVault; 26 | let uniV3Factory: IUniswapV3Factory; 27 | let univ3Pool: IUniswapV3Pool; 28 | let nonfungiblePositionManager: INonfungiblePositionManager; 29 | let token0: IERC20; 30 | let token1: IERC20; 31 | let manager: SignerWithAddress; 32 | let trader: SignerWithAddress; 33 | let nonManager: SignerWithAddress; 34 | let newManager: SignerWithAddress; 35 | let user2: SignerWithAddress; 36 | let lpProvider: SignerWithAddress; 37 | const poolFee = 3000; 38 | const name = "Test Token"; 39 | const symbol = "TT"; 40 | const amount0: BigNumber = parseEther("2"); 41 | const amount1: BigNumber = parseEther("3"); 42 | let initializeData: any; 43 | const lowerTick = -887220; 44 | const upperTick = 887220; 45 | 46 | describe("RangeProtocolVault::exposure", () => { 47 | before(async () => { 48 | [manager, nonManager, user2, newManager, trader, lpProvider] = 49 | await ethers.getSigners(); 50 | const UniswapV3Factory = await ethers.getContractFactory( 51 | "UniswapV3Factory" 52 | ); 53 | uniV3Factory = (await UniswapV3Factory.deploy()) as IUniswapV3Factory; 54 | 55 | const NonfungiblePositionManager = await ethers.getContractFactory( 56 | "NonfungiblePositionManager" 57 | ); 58 | nonfungiblePositionManager = (await NonfungiblePositionManager.deploy( 59 | uniV3Factory.address, 60 | trader.address, 61 | trader.address 62 | )) as INonfungiblePositionManager; 63 | 64 | const RangeProtocolFactory = await ethers.getContractFactory( 65 | "RangeProtocolFactory" 66 | ); 67 | factory = (await RangeProtocolFactory.deploy( 68 | uniV3Factory.address 69 | )) as RangeProtocolFactory; 70 | 71 | const MockERC20 = await ethers.getContractFactory("MockERC20"); 72 | token0 = (await MockERC20.deploy()) as IERC20; 73 | token1 = (await MockERC20.deploy()) as IERC20; 74 | 75 | if (bn(token0.address).gt(token1.address)) { 76 | const tmp = token0; 77 | token0 = token1; 78 | token1 = tmp; 79 | } 80 | 81 | await uniV3Factory.createPool(token0.address, token1.address, poolFee); 82 | univ3Pool = (await ethers.getContractAt( 83 | "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol:IUniswapV3Pool", 84 | await uniV3Factory.getPool(token0.address, token1.address, poolFee) 85 | )) as IUniswapV3Pool; 86 | 87 | await univ3Pool.initialize(encodePriceSqrt("1", "1")); 88 | await univ3Pool.increaseObservationCardinalityNext("15"); 89 | 90 | initializeData = getInitializeData({ 91 | managerAddress: manager.address, 92 | name, 93 | symbol, 94 | }); 95 | 96 | // eslint-disable-next-line @typescript-eslint/naming-convention 97 | const RangeProtocolVault = await ethers.getContractFactory( 98 | "RangeProtocolVault" 99 | ); 100 | vaultImpl = (await RangeProtocolVault.deploy()) as RangeProtocolVault; 101 | 102 | await factory.createVault( 103 | token0.address, 104 | token1.address, 105 | poolFee, 106 | vaultImpl.address, 107 | initializeData 108 | ); 109 | 110 | const vaultAddress = await factory.getVaultAddresses(0, 0); 111 | vault = (await ethers.getContractAt( 112 | "RangeProtocolVault", 113 | vaultAddress[0] 114 | )) as RangeProtocolVault; 115 | 116 | await expect(vault.connect(manager).updateTicks(lowerTick, upperTick)); 117 | }); 118 | 119 | beforeEach(async () => { 120 | await token0.approve(vault.address, amount0.mul(bn(2))); 121 | await token1.approve(vault.address, amount1.mul(bn(2))); 122 | }); 123 | 124 | it("should mint with zero totalSupply of vault shares", async () => { 125 | await token0.connect(lpProvider).mint(); 126 | await token1.connect(lpProvider).mint(); 127 | 128 | const { 129 | mintAmount: mintAmountLpProvider, 130 | amount0: amount0LpProvider, 131 | amount1: amount1LpProvider, 132 | } = await vault.getMintAmounts(amount0.mul(10), amount1.mul(10)); 133 | await token0 134 | .connect(lpProvider) 135 | .approve(nonfungiblePositionManager.address, amount0LpProvider); 136 | await token1 137 | .connect(lpProvider) 138 | .approve(nonfungiblePositionManager.address, amount1LpProvider); 139 | 140 | await nonfungiblePositionManager 141 | .connect(lpProvider) 142 | .mint([ 143 | token0.address, 144 | token1.address, 145 | 3000, 146 | lowerTick, 147 | upperTick, 148 | amount0LpProvider, 149 | amount1LpProvider, 150 | 0, 151 | 0, 152 | lpProvider.address, 153 | new Date().getTime() + 10000000, 154 | ]); 155 | 156 | const { 157 | mintAmount: mintAmount1, 158 | // eslint-disable-next-line @typescript-eslint/naming-convention 159 | amount0: amount0Mint1, 160 | // eslint-disable-next-line @typescript-eslint/naming-convention 161 | amount1: amount1Mint1, 162 | } = await vault.getMintAmounts(amount0, amount1); 163 | 164 | await expect( 165 | vault.mint(mintAmount1, [ 166 | amount0Mint1.mul(10100).div(10000), 167 | amount1Mint1.mul(10100).div(10000), 168 | ]) 169 | ) 170 | .to.emit(vault, "Minted") 171 | .withArgs(manager.address, mintAmount1, amount0Mint1, amount1Mint1); 172 | 173 | console.log("Users 1:"); 174 | console.log("mint amount: ", mintAmount1.toString()); 175 | console.log("token0 amount: ", amount0Mint1.toString()); 176 | console.log("token1 amount: ", amount1Mint1.toString()); 177 | console.log("=================================================="); 178 | 179 | await token0.connect(newManager).mint(); 180 | await token1.connect(newManager).mint(); 181 | 182 | const { 183 | mintAmount: mintAmount2, 184 | amount0: amount0Mint2, 185 | amount1: amount1Mint2, 186 | } = await vault.getMintAmounts(amount0, amount1); 187 | await token0.connect(newManager).approve(vault.address, amount0Mint2); 188 | await token1.connect(newManager).approve(vault.address, amount1Mint2); 189 | 190 | await vault 191 | .connect(newManager) 192 | .mint(mintAmount2, [ 193 | amount0Mint2.mul(10100).div(10000), 194 | amount1Mint2.mul(10100).div(10000), 195 | ]); 196 | console.log("Users 2:"); 197 | console.log("mint amount: ", mintAmount1.toString()); 198 | console.log("token0 amount: ", amount0Mint2.toString()); 199 | console.log("token1 amount: ", amount1Mint2.toString()); 200 | console.log("=================================================="); 201 | 202 | const SwapTest = await ethers.getContractFactory("SwapTest"); 203 | const swapTest = (await SwapTest.deploy()) as SwapTest; 204 | 205 | const { amount0Current: amount0Current1, amount1Current: amount1Current1 } = 206 | await vault.getUnderlyingBalances(); 207 | console.log("Vault balance: "); 208 | console.log("token0 amount: ", amount0Current1.toString()); 209 | console.log("token1 amount: ", amount1Current1.toString()); 210 | console.log("=================================================="); 211 | 212 | console.log( 213 | "perform external swap " + amount1.toString(), 214 | " of token1 to token0 to move price" 215 | ); 216 | console.log("=================================================="); 217 | 218 | await token0.connect(trader).mint(); 219 | await token1.connect(trader).mint(); 220 | 221 | await token0.connect(trader).approve(swapTest.address, amount0); 222 | await token1.connect(trader).approve(swapTest.address, amount1); 223 | 224 | await swapTest.connect(trader).swapZeroForOne(univ3Pool.address, amount1); 225 | 226 | const { amount0Current: amount0Current2, amount1Current: amount1Current2 } = 227 | await vault.getUnderlyingBalances(); 228 | console.log("Vault balance after swap: "); 229 | console.log("token0 amount: ", amount0Current2.toString()); 230 | console.log("token1 amount: ", amount1Current2.toString()); 231 | console.log("=================================================="); 232 | 233 | console.log("User2 mints for the second time (after price movement)"); 234 | await token0.connect(newManager).mint(); 235 | await token1.connect(newManager).mint(); 236 | 237 | const { 238 | mintAmount: mintAmount3, 239 | amount0: amount0Mint3, 240 | amount1: amount1Mint3, 241 | } = await vault.getMintAmounts(amount0, amount1); 242 | await token0.connect(newManager).approve(vault.address, amount0Mint3); 243 | await token1.connect(newManager).approve(vault.address, amount1Mint3); 244 | console.log("Users 2:"); 245 | console.log( 246 | "vault shares before: ", 247 | (await vault.balanceOf(newManager.address)).toString() 248 | ); 249 | 250 | await vault 251 | .connect(newManager) 252 | .mint(mintAmount3, [amount0Mint3, amount1Mint3]); 253 | console.log( 254 | "vault shares after: ", 255 | (await vault.balanceOf(newManager.address)).toString() 256 | ); 257 | 258 | console.log("=================================================="); 259 | 260 | console.log("Vault balance after user2 mints for the second time: "); 261 | 262 | const { amount0Current: amount0Current3, amount1Current: amount1Current3 } = 263 | await vault.getUnderlyingBalances(); 264 | console.log("token0 amount: ", amount0Current3.toString()); 265 | console.log("token1 amount: ", amount1Current3.toString()); 266 | console.log("=================================================="); 267 | 268 | console.log("Remove liquidity from uniswap pool"); 269 | await vault.removeLiquidity([0, 0]); 270 | console.log("=================================================="); 271 | 272 | console.log("Total users vault amounts based on their initial deposits"); 273 | const userVaults = await vault.getUserVaults(0, 0); 274 | const { token0VaultTotal, token1VaultTotal } = userVaults.reduce( 275 | (acc, { token0, token1 }) => { 276 | return { 277 | token0VaultTotal: acc.token0VaultTotal.add(token0), 278 | token1VaultTotal: acc.token1VaultTotal.add(token1), 279 | }; 280 | }, 281 | { 282 | token0VaultTotal: bn(0), 283 | token1VaultTotal: bn(0), 284 | } 285 | ); 286 | console.log("token0: ", token0VaultTotal.toString()); 287 | console.log("token1: ", token1VaultTotal.toString()); 288 | console.log("=================================================="); 289 | 290 | console.log("perform vault swap to maintain users' vault exposure"); 291 | let initialAmountBaseToken, 292 | initialAmountQuoteToken, 293 | currentAmountBaseToken, 294 | currentAmountQuoteToken; 295 | initialAmountBaseToken = token0VaultTotal; 296 | initialAmountQuoteToken = token1VaultTotal; 297 | currentAmountBaseToken = amount0Current3; 298 | currentAmountQuoteToken = amount1Current3; 299 | 300 | const swapAmountToken0 = amount0Current3.sub(token0VaultTotal); 301 | const swapAmountToken1 = amount1Current3.sub(token1VaultTotal); 302 | 303 | const MockSqrtPriceMath = await ethers.getContractFactory( 304 | "MockSqrtPriceMath" 305 | ); 306 | const mockSqrtPriceMath = await MockSqrtPriceMath.deploy(); 307 | 308 | let { sqrtPriceX96 } = await univ3Pool.slot0(); 309 | const liquidity = await univ3Pool.liquidity(); 310 | 311 | const nextPrice = currentAmountBaseToken.gt(initialAmountBaseToken) 312 | ? // there is profit in base token that we swap to quote token 313 | await mockSqrtPriceMath.getNextSqrtPriceFromInput( 314 | sqrtPriceX96, 315 | liquidity, 316 | currentAmountBaseToken.sub(initialAmountBaseToken), 317 | true 318 | ) 319 | : // there is loss in base token that is realized in quote token 320 | await mockSqrtPriceMath.getNextSqrtPriceFromInput( 321 | sqrtPriceX96, 322 | liquidity, 323 | initialAmountBaseToken.sub(currentAmountBaseToken), 324 | false 325 | ); 326 | 327 | const ONE = bn(2).pow(bn(96)); 328 | let minAmountIn = ONE.mul(ONE) 329 | .div(nextPrice) 330 | .sub(ONE.mul(ONE).div(sqrtPriceX96)) 331 | .mul(liquidity) 332 | .div(ONE); 333 | minAmountIn = minAmountIn.mul(bn(9_900)).div(bn(10_000)); 334 | const minAmountInSigned = currentAmountBaseToken.gt(initialAmountBaseToken) 335 | ? minAmountIn.toString() 336 | : (-minAmountIn).toString(); 337 | await vault.swap( 338 | currentAmountBaseToken.gt(initialAmountBaseToken), 339 | currentAmountBaseToken.sub(initialAmountBaseToken), 340 | nextPrice, 341 | minAmountInSigned 342 | ); 343 | console.log("=================================================="); 344 | console.log("Vault balance after swap to maintain users' vault exposure: "); 345 | 346 | const { amount0Current: amount0Current4, amount1Current: amount1Current4 } = 347 | await vault.getUnderlyingBalances(); 348 | console.log("token0 amount: ", amount0Current4.toString()); 349 | console.log("token1 amount: ", amount1Current4.toString()); 350 | console.log("=================================================="); 351 | 352 | const MockLiquidityAmounts = await ethers.getContractFactory( 353 | "MockLiquidityAmounts" 354 | ); 355 | const mockLiquidityAmounts = await MockLiquidityAmounts.deploy(); 356 | 357 | ({ sqrtPriceX96 } = await univ3Pool.slot0()); 358 | const sqrtPriceA = new Decimal(1.0001) 359 | .pow(lowerTick) 360 | .sqrt() 361 | .mul(new Decimal(2).pow(96)) 362 | .round() 363 | .toFixed(); 364 | const sqrtPriceB = new Decimal(1.0001) 365 | .pow(upperTick) 366 | .sqrt() 367 | .mul(new Decimal(2).pow(96)) 368 | .round() 369 | .toFixed(); 370 | const liquidityToAdd = await mockLiquidityAmounts.getLiquidityForAmounts( 371 | sqrtPriceX96, 372 | sqrtPriceA, 373 | sqrtPriceB, 374 | await token0.balanceOf(vault.address), 375 | await token1.balanceOf(vault.address) 376 | ); 377 | const { amount0: amount0ToAdd, amount1: amount1ToAdd } = 378 | await mockLiquidityAmounts.getAmountsForLiquidity( 379 | sqrtPriceX96, 380 | sqrtPriceA, 381 | sqrtPriceB, 382 | liquidityToAdd 383 | ); 384 | 385 | console.log("Add liquidity back to the uniswap v3 pool"); 386 | await vault.addLiquidity( 387 | lowerTick, 388 | upperTick, 389 | amount0ToAdd.sub(await vault.managerBalance0()), 390 | amount1ToAdd.sub(await vault.managerBalance1()), 391 | [amount0ToAdd.mul(9900).div(10000), amount1ToAdd.mul(9900).div(10000)], 392 | [amount0ToAdd.mul(10100).div(10000), amount1ToAdd.mul(10100).div(10000)] 393 | ); 394 | 395 | console.log("=================================================="); 396 | console.log( 397 | "Vault balance after providing the liquidity back to the uniswap pool" 398 | ); 399 | const { amount0Current: amount0Current5, amount1Current: amount1Current5 } = 400 | await vault.getUnderlyingBalances(); 401 | console.log("token0 amount: ", amount0Current5.toString()); 402 | console.log("token1 amount: ", amount1Current5.toString()); 403 | console.log("=================================================="); 404 | 405 | console.log("user 1 withdraws liquidity"); 406 | const user1Amount = await vault.balanceOf(manager.address); 407 | let { amount0: amount0Out, amount1: amount1Out } = 408 | await vault.getUnderlyingBalancesByShare(user1Amount); 409 | await vault.burn(user1Amount, [ 410 | amount0Out.mul(9999).div(10000), 411 | amount1Out.mul(9999).div(10000), 412 | ]); 413 | 414 | console.log("=================================================="); 415 | console.log("Vault balance after user1 withdraws liquidity"); 416 | const { amount0Current: amount0Current6, amount1Current: amount1Current6 } = 417 | await vault.getUnderlyingBalances(); 418 | console.log("token0 amount: ", amount0Current6.toString()); 419 | console.log("token1 amount: ", amount1Current6.toString()); 420 | console.log("=================================================="); 421 | 422 | console.log("user 2 withdraws liquidity"); 423 | const user2Amount = await vault.balanceOf(newManager.address); 424 | ({ amount0: amount0Out, amount1: amount1Out } = 425 | await vault.getUnderlyingBalancesByShare(user1Amount)); 426 | await vault.connect(newManager).burn(user2Amount, [amount0Out, amount1Out]); 427 | 428 | console.log("=================================================="); 429 | console.log("Vault balance after user2 withdraws liquidity"); 430 | await vault.collectManager(); 431 | const { amount0Current: amount0Current7, amount1Current: amount1Current7 } = 432 | await vault.getUnderlyingBalances(); 433 | console.log("token0 amount: ", amount0Current7.toString()); 434 | console.log("token1 amount: ", amount1Current7.toString()); 435 | console.log("=================================================="); 436 | console.log((await token0.balanceOf(vault.address)).toString()); 437 | console.log((await token1.balanceOf(vault.address)).toString()); 438 | console.log((await vault.totalSupply()).toString()); 439 | }); 440 | }); 441 | -------------------------------------------------------------------------------- /test/RangeProtocolVault.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 5 | import { 6 | IERC20, 7 | IUniswapV3Factory, 8 | IUniswapV3Pool, 9 | RangeProtocolVault, 10 | RangeProtocolFactory, 11 | } from "../typechain"; 12 | import { 13 | bn, 14 | encodePriceSqrt, 15 | getInitializeData, 16 | parseEther, 17 | position, 18 | } from "./common"; 19 | import { beforeEach } from "mocha"; 20 | import { BigNumber } from "ethers"; 21 | import { Decimal } from "decimal.js"; 22 | 23 | let factory: RangeProtocolFactory; 24 | let vaultImpl: RangeProtocolVault; 25 | let vault: RangeProtocolVault; 26 | let uniV3Factory: IUniswapV3Factory; 27 | let univ3Pool: IUniswapV3Pool; 28 | let token0: IERC20; 29 | let token1: IERC20; 30 | let manager: SignerWithAddress; 31 | let nonManager: SignerWithAddress; 32 | let newManager: SignerWithAddress; 33 | let user2: SignerWithAddress; 34 | const poolFee = 3000; 35 | const name = "Test Token"; 36 | const symbol = "TT"; 37 | const amount0: BigNumber = parseEther("2"); 38 | const amount1: BigNumber = parseEther("3"); 39 | let initializeData: any; 40 | const lowerTick = -887220; 41 | const upperTick = 887220; 42 | 43 | describe("RangeProtocolVault", () => { 44 | before(async () => { 45 | [manager, nonManager, user2, newManager] = await ethers.getSigners(); 46 | const UniswapV3Factory = await ethers.getContractFactory( 47 | "UniswapV3Factory" 48 | ); 49 | uniV3Factory = (await UniswapV3Factory.deploy()) as IUniswapV3Factory; 50 | 51 | const RangeProtocolFactory = await ethers.getContractFactory( 52 | "RangeProtocolFactory" 53 | ); 54 | factory = (await RangeProtocolFactory.deploy( 55 | uniV3Factory.address 56 | )) as RangeProtocolFactory; 57 | 58 | const MockERC20 = await ethers.getContractFactory("MockERC20"); 59 | token0 = (await MockERC20.deploy()) as IERC20; 60 | token1 = (await MockERC20.deploy()) as IERC20; 61 | 62 | if (bn(token0.address).gt(token1.address)) { 63 | const tmp = token0; 64 | token0 = token1; 65 | token1 = tmp; 66 | } 67 | 68 | await uniV3Factory.createPool(token0.address, token1.address, poolFee); 69 | univ3Pool = (await ethers.getContractAt( 70 | "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol:IUniswapV3Pool", 71 | await uniV3Factory.getPool(token0.address, token1.address, poolFee) 72 | )) as IUniswapV3Pool; 73 | 74 | await univ3Pool.initialize(encodePriceSqrt("1", "1")); 75 | await univ3Pool.increaseObservationCardinalityNext("15"); 76 | 77 | initializeData = getInitializeData({ 78 | managerAddress: manager.address, 79 | name, 80 | symbol, 81 | }); 82 | 83 | // eslint-disable-next-line @typescript-eslint/naming-convention 84 | const RangeProtocolVault = await ethers.getContractFactory( 85 | "RangeProtocolVault" 86 | ); 87 | vaultImpl = (await RangeProtocolVault.deploy()) as RangeProtocolVault; 88 | 89 | await factory.createVault( 90 | token0.address, 91 | token1.address, 92 | poolFee, 93 | vaultImpl.address, 94 | initializeData 95 | ); 96 | 97 | const vaultAddress = await factory.getVaultAddresses(0, 0); 98 | vault = (await ethers.getContractAt( 99 | "RangeProtocolVault", 100 | vaultAddress[0] 101 | )) as RangeProtocolVault; 102 | }); 103 | 104 | beforeEach(async () => { 105 | await token0.approve(vault.address, amount0.mul(bn(2))); 106 | await token1.approve(vault.address, amount1.mul(bn(2))); 107 | }); 108 | 109 | it("should not reinitialize the vault", async () => { 110 | await expect( 111 | vault.initialize(uniV3Factory.address, 1, "0x") 112 | ).to.be.revertedWith("Initializable: contract is already initialized"); 113 | }); 114 | 115 | it("should not mint when vault is not initialized", async () => { 116 | await expect( 117 | vault.mint(1111, [amount0, amount1]) 118 | ).to.be.revertedWithCustomError(vault, "MintNotStarted"); 119 | }); 120 | 121 | it("non-manager should not be able to updateTicks", async () => { 122 | expect(await vault.mintStarted()).to.be.equal(false); 123 | await expect( 124 | vault.connect(nonManager).updateTicks(lowerTick, upperTick) 125 | ).to.be.revertedWith("Ownable: caller is not the manager"); 126 | }); 127 | 128 | it("should not updateTicks with out of range ticks", async () => { 129 | await expect( 130 | vault.connect(manager).updateTicks(-887273, 0) 131 | ).to.be.revertedWithCustomError(vault, "TicksOutOfRange"); 132 | 133 | await expect( 134 | vault.connect(manager).updateTicks(0, 887273) 135 | ).to.be.revertedWithCustomError(vault, "TicksOutOfRange"); 136 | }); 137 | 138 | it("should not updateTicks with ticks not following tick spacing", async () => { 139 | await expect( 140 | vault.connect(manager).updateTicks(0, 1) 141 | ).to.be.revertedWithCustomError(vault, "InvalidTicksSpacing"); 142 | 143 | await expect( 144 | vault.connect(manager).updateTicks(1, 0) 145 | ).to.be.revertedWithCustomError(vault, "InvalidTicksSpacing"); 146 | }); 147 | 148 | it("manager should be able to updateTicks", async () => { 149 | expect(await vault.mintStarted()).to.be.equal(false); 150 | await expect(vault.connect(manager).updateTicks(lowerTick, upperTick)) 151 | .to.emit(vault, "MintStarted") 152 | .to.emit(vault, "TicksSet") 153 | .withArgs(lowerTick, upperTick); 154 | 155 | expect(await vault.mintStarted()).to.be.equal(true); 156 | expect(await vault.lowerTick()).to.be.equal(lowerTick); 157 | expect(await vault.upperTick()).to.be.equal(upperTick); 158 | }); 159 | 160 | it("should not allow minting with zero mint amount", async () => { 161 | const mintAmount = 0; 162 | await expect( 163 | vault.mint(mintAmount, [amount0, amount1]) 164 | ).to.be.revertedWithCustomError(vault, "InvalidMintAmount"); 165 | }); 166 | 167 | it("should not mint when contract is paused", async () => { 168 | expect(await vault.paused()).to.be.equal(false); 169 | await expect(vault.pause()) 170 | .to.emit(vault, "Paused") 171 | .withArgs(manager.address); 172 | expect(await vault.paused()).to.be.equal(true); 173 | 174 | const { 175 | amount0: amount0ToAdd, 176 | amount1: amount1ToAdd, 177 | mintAmount, 178 | } = await vault.getMintAmounts(amount0, amount1); 179 | 180 | await expect( 181 | vault.mint(mintAmount, [amount0ToAdd, amount1ToAdd]) 182 | ).to.be.revertedWith("Pausable: paused"); 183 | await expect(vault.unpause()) 184 | .to.emit(vault, "Unpaused") 185 | .withArgs(manager.address); 186 | }); 187 | 188 | it("should mint with zero totalSupply of vault shares", async () => { 189 | const { 190 | mintAmount, 191 | // eslint-disable-next-line @typescript-eslint/naming-convention 192 | amount0: _amount0, 193 | // eslint-disable-next-line @typescript-eslint/naming-convention 194 | amount1: _amount1, 195 | } = await vault.getMintAmounts(amount0, amount1); 196 | // console.log(ethers.utils.formatEther(_amount0), ethers.utils.formatEther(_amount1)) 197 | // 1.999999999999999999 1.999999999999999999 198 | 199 | expect(await vault.totalSupply()).to.be.equal(0); 200 | expect(await token0.balanceOf(univ3Pool.address)).to.be.equal(0); 201 | expect(await token1.balanceOf(univ3Pool.address)).to.be.equal(0); 202 | 203 | await expect(vault.mint(mintAmount, [_amount0, _amount1])) 204 | .to.emit(vault, "Minted") 205 | .withArgs(manager.address, mintAmount, _amount0, _amount1); 206 | 207 | expect(await vault.totalSupply()).to.be.equal(mintAmount); 208 | expect(await token0.balanceOf(univ3Pool.address)).to.be.equal(_amount0); 209 | expect(await token1.balanceOf(univ3Pool.address)).to.be.equal(_amount1); 210 | expect(await vault.users(0)).to.be.equal(manager.address); 211 | expect((await vault.userVaults(manager.address)).exists).to.be.true; 212 | expect((await vault.userVaults(manager.address)).token0).to.be.equal( 213 | _amount0 214 | ); 215 | expect((await vault.userVaults(manager.address)).token1).to.be.equal( 216 | _amount1 217 | ); 218 | 219 | const userVault = (await vault.getUserVaults(0, 0))[0]; 220 | expect(userVault.user).to.be.equal(manager.address); 221 | expect(userVault.token0).to.be.equal(_amount0); 222 | expect(userVault.token1).to.be.equal(_amount1); 223 | expect(await vault.userCount()).to.be.equal(1); 224 | }); 225 | 226 | it("should not mint when min amounts are not satisfied", async () => { 227 | const { 228 | mintAmount, 229 | // eslint-disable-next-line @typescript-eslint/naming-convention 230 | amount0: _amount0, 231 | // eslint-disable-next-line @typescript-eslint/naming-convention 232 | amount1: _amount1, 233 | } = await vault.getMintAmounts(amount0, amount1); 234 | 235 | await expect( 236 | vault.mint(mintAmount, [_amount0.div(2), _amount1.div(2)]) 237 | ).to.be.revertedWithCustomError(vault, "SlippageExceedThreshold"); 238 | }); 239 | 240 | it("should mint with non zero totalSupply", async () => { 241 | const { 242 | mintAmount, 243 | // eslint-disable-next-line @typescript-eslint/naming-convention 244 | amount0: _amount0, 245 | // eslint-disable-next-line @typescript-eslint/naming-convention 246 | amount1: _amount1, 247 | } = await vault.getMintAmounts(amount0, amount1); 248 | // console.log(ethers.utils.formatEther(_amount0), ethers.utils.formatEther(_amount1)) 249 | // 2.0 2.0 250 | 251 | const userVault0Before = (await vault.userVaults(manager.address)).token0; 252 | const userVault1Before = (await vault.userVaults(manager.address)).token1; 253 | 254 | expect(await vault.totalSupply()).to.not.be.equal(0); 255 | await expect(vault.mint(mintAmount, [_amount0, _amount1])) 256 | .to.emit(vault, "Minted") 257 | .withArgs(manager.address, mintAmount, _amount0, _amount1); 258 | 259 | expect(await vault.users(0)).to.be.equal(manager.address); 260 | expect((await vault.userVaults(manager.address)).exists).to.be.true; 261 | expect((await vault.userVaults(manager.address)).token0).to.be.equal( 262 | userVault0Before.add(_amount0) 263 | ); 264 | expect((await vault.userVaults(manager.address)).token1).to.be.equal( 265 | userVault1Before.add(_amount1) 266 | ); 267 | 268 | const userVault = (await vault.getUserVaults(0, 0))[0]; 269 | expect(userVault.user).to.be.equal(manager.address); 270 | expect(userVault.token0).to.be.equal(userVault0Before.add(_amount0)); 271 | expect(userVault.token1).to.be.equal(userVault1Before.add(_amount1)); 272 | expect(await vault.userCount()).to.be.equal(1); 273 | 274 | const { amount0Current, amount1Current } = 275 | await vault.getUnderlyingBalances(); 276 | const shares = await vault.balanceOf(manager.address); 277 | const totalShares = await vault.totalSupply(); 278 | const expectedAmount0 = shares.mul(amount0Current).div(totalShares); 279 | const expectedAmount1 = shares.mul(amount1Current).div(totalShares); 280 | 281 | const { amount0: amount0Got, amount1: amount1Got } = 282 | await vault.getUnderlyingBalancesByShare(shares); 283 | 284 | expect(amount0Got).to.be.equal(expectedAmount0); 285 | expect(amount1Got).to.be.equal(expectedAmount1); 286 | }); 287 | 288 | it("should transfer vault shares to user2", async () => { 289 | const userBalance = await vault.balanceOf(manager.address); 290 | const transferAmount = ethers.utils.parseEther("1"); 291 | const userVault0 = (await vault.userVaults(manager.address)).token0; 292 | const userVault1 = (await vault.userVaults(manager.address)).token1; 293 | 294 | const vault0Moved = userVault0.sub( 295 | userVault0.mul(userBalance.sub(transferAmount)).div(userBalance) 296 | ); 297 | const vault1Moved = userVault1.sub( 298 | userVault1.mul(userBalance.sub(transferAmount)).div(userBalance) 299 | ); 300 | await vault.transfer(user2.address, transferAmount); 301 | 302 | let userVaults = await vault.getUserVaults(0, 2); 303 | expect(userVaults[0].user).to.be.equal(manager.address); 304 | expect(userVaults[0].token0).to.be.equal(userVault0.sub(vault0Moved)); 305 | expect(userVaults[0].token1).to.be.equal(userVault1.sub(vault1Moved)); 306 | expect(await vault.userCount()).to.be.equal(2); 307 | 308 | expect(userVaults[1].user).to.be.equal(user2.address); 309 | expect(userVaults[1].token0).to.be.equal(vault0Moved); 310 | expect(userVaults[1].token1).to.be.equal(vault1Moved); 311 | 312 | const user2Balance = await vault.balanceOf(user2.address); 313 | const user2Vault0 = (await vault.userVaults(user2.address)).token0; 314 | const user2Vault1 = (await vault.userVaults(user2.address)).token1; 315 | await vault.connect(user2).transfer(manager.address, user2Balance); 316 | 317 | userVaults = await vault.getUserVaults(0, 2); 318 | expect(userVaults[0].token0).to.be.equal(userVault0); 319 | expect(userVaults[0].token1).to.be.equal(userVault1); 320 | 321 | expect(userVaults[1].token0).to.be.equal(bn(0)); 322 | expect(userVaults[1].token1).to.be.equal(bn(0)); 323 | }); 324 | 325 | it("should not burn non existing vault shares", async () => { 326 | const burnAmount = parseEther("1"); 327 | await expect( 328 | vault.connect(user2).burn(burnAmount, [0, 0]) 329 | ).to.be.revertedWith("ERC20: burn amount exceeds balance"); 330 | }); 331 | 332 | it("should not burn when min amounts are not satisfied", async () => { 333 | const burnAmount = await vault.balanceOf(manager.address); 334 | const { amount0: minAmount0Out, amount1: minAmount1Out } = 335 | await vault.getUnderlyingBalancesByShare(burnAmount); 336 | await expect( 337 | vault.burn(burnAmount, [minAmount0Out.mul(2), minAmount1Out.mul(2)]) 338 | ).to.be.revertedWithCustomError(vault, "SlippageExceedThreshold"); 339 | }); 340 | 341 | it("should burn vault shares", async () => { 342 | const burnAmount = await vault.balanceOf(manager.address); 343 | const totalSupplyBefore = await vault.totalSupply(); 344 | const [amount0Current, amount1Current] = 345 | await vault.getUnderlyingBalances(); 346 | const userBalance0Before = await token0.balanceOf(manager.address); 347 | const userBalance1Before = await token1.balanceOf(manager.address); 348 | 349 | const userVault0Before = (await vault.userVaults(manager.address)).token0; 350 | const userVault1Before = (await vault.userVaults(manager.address)).token1; 351 | await vault.updateFees(50, 250); 352 | 353 | const managingFee = await vault.managingFee(); 354 | const totalSupply = await vault.totalSupply(); 355 | const vaultShares = await vault.balanceOf(manager.address); 356 | const userBalance0 = amount0Current.mul(vaultShares).div(totalSupply); 357 | const managingFee0 = userBalance0.mul(managingFee).div(10_000); 358 | 359 | const userBalance1 = amount1Current.mul(vaultShares).div(totalSupply); 360 | const managingFee1 = userBalance1.mul(managingFee).div(10_000); 361 | const { fee0, fee1 } = await vault.getCurrentFees(); 362 | 363 | const { amount0: amount0Out, amount1: amount1Out } = 364 | await vault.getUnderlyingBalancesByShare(burnAmount); 365 | await expect(vault.burn(burnAmount, [amount0Out, amount1Out])) 366 | .to.emit(vault, "FeesEarned") 367 | .withArgs(fee0, fee1); 368 | expect(await vault.totalSupply()).to.be.equal( 369 | totalSupplyBefore.sub(burnAmount) 370 | ); 371 | 372 | const amount0Got = amount0Current.mul(burnAmount).div(totalSupplyBefore); 373 | const amount1Got = amount1Current.mul(burnAmount).div(totalSupplyBefore); 374 | 375 | expect(await token0.balanceOf(manager.address)).to.be.equal( 376 | userBalance0Before.add(amount0Got).sub(managingFee0) 377 | ); 378 | expect(await token1.balanceOf(manager.address)).to.be.equal( 379 | userBalance1Before.add(amount1Got).sub(managingFee1) 380 | ); 381 | expect((await vault.userVaults(manager.address)).token0).to.be.equal( 382 | userVault0Before.mul(vaultShares.sub(burnAmount)).div(vaultShares) 383 | ); 384 | expect((await vault.userVaults(manager.address)).token1).to.be.equal( 385 | userVault1Before.mul(vaultShares.sub(burnAmount)).div(vaultShares) 386 | ); 387 | 388 | expect(await vault.managerBalance0()).to.be.equal(managingFee0); 389 | expect(await vault.managerBalance1()).to.be.equal(managingFee1); 390 | // console.log(ethers.utils.formatEther(managingFee0), ethers.utils.formatEther(managingFee1)) 391 | // 0.019999999999999999 0.019999999999999999 392 | }); 393 | 394 | it("should not add liquidity when total supply is zero and vault is out of the pool", async () => { 395 | const { 396 | amount0: amount0ToAdd, 397 | amount1: amount1ToAdd, 398 | mintAmount, 399 | } = await vault.getMintAmounts(amount0, amount1); 400 | await vault.mint(mintAmount, [amount0ToAdd, amount1ToAdd]); 401 | 402 | await vault.removeLiquidity([0, 0]); 403 | const burnAmount = await vault.balanceOf(manager.address); 404 | const { amount0: amount0Out, amount1: amount1Out } = 405 | await vault.getUnderlyingBalancesByShare(burnAmount); 406 | await vault.burn(burnAmount, [amount0Out, amount1Out]); 407 | 408 | await expect(vault.mint(1, [0, 0])).to.be.revertedWithCustomError( 409 | vault, 410 | "MintNotAllowed" 411 | ); 412 | }); 413 | 414 | describe("Manager Fee", () => { 415 | it("should not update managing and performance fee by non manager", async () => { 416 | await expect( 417 | vault.connect(nonManager).updateFees(100, 1000) 418 | ).to.be.revertedWith("Ownable: caller is not the manager"); 419 | }); 420 | 421 | it("should not update managing fee above BPS", async () => { 422 | await expect(vault.updateFees(101, 100)).to.be.revertedWithCustomError( 423 | vault, 424 | "InvalidManagingFee" 425 | ); 426 | }); 427 | 428 | it("should not update performance fee above BPS", async () => { 429 | await expect(vault.updateFees(100, 10001)).to.be.revertedWithCustomError( 430 | vault, 431 | "InvalidPerformanceFee" 432 | ); 433 | }); 434 | 435 | it("should update manager and performance fee by manager", async () => { 436 | await expect(vault.updateFees(100, 300)) 437 | .to.emit(vault, "FeesUpdated") 438 | .withArgs(100, 300); 439 | }); 440 | }); 441 | 442 | describe("Remove Liquidity", () => { 443 | before(async () => { 444 | await vault.updateTicks(lowerTick, upperTick); 445 | }); 446 | 447 | beforeEach(async () => { 448 | await token0.approve(vault.address, amount0.mul(bn(2))); 449 | await token1.approve(vault.address, amount1.mul(bn(2))); 450 | const { 451 | amount0: amount0ToAdd, 452 | amount1: amount1ToAdd, 453 | mintAmount, 454 | } = await vault.getMintAmounts(amount0, amount1); 455 | await vault.mint(mintAmount, [amount0ToAdd, amount1ToAdd]); 456 | }); 457 | 458 | it("should not remove liquidity by non-manager", async () => { 459 | await expect( 460 | vault.connect(nonManager).removeLiquidity([0, 0]) 461 | ).to.be.revertedWith("Ownable: caller is not the manager"); 462 | }); 463 | 464 | it("should remove liquidity by manager", async () => { 465 | expect(await vault.lowerTick()).to.not.be.equal(await vault.upperTick()); 466 | expect(await vault.inThePosition()).to.be.equal(true); 467 | const { _liquidity: liquidityBefore } = await univ3Pool.positions( 468 | position(vault.address, lowerTick, upperTick) 469 | ); 470 | expect(liquidityBefore).not.to.be.equal(0); 471 | 472 | const { fee0, fee1 } = await vault.getCurrentFees(); 473 | await expect(vault.removeLiquidity([0, 0])) 474 | .to.emit(vault, "InThePositionStatusSet") 475 | .withArgs(false) 476 | .to.emit(vault, "FeesEarned") 477 | .withArgs(fee0, fee1); 478 | 479 | expect(await vault.lowerTick()).to.be.equal(await vault.upperTick()); 480 | expect(await vault.inThePosition()).to.be.equal(false); 481 | const { _liquidity: liquidityAfter } = await univ3Pool.positions( 482 | position(vault.address, lowerTick, upperTick) 483 | ); 484 | expect(liquidityAfter).to.be.equal(0); 485 | }); 486 | 487 | it("should burn vault shares when liquidity is removed", async () => { 488 | const { _liquidity: liquidity } = await univ3Pool.positions( 489 | position(vault.address, lowerTick, upperTick) 490 | ); 491 | 492 | expect(liquidity).to.be.equal(0); 493 | await expect(vault.removeLiquidity([0, 0])) 494 | .to.be.emit(vault, "InThePositionStatusSet") 495 | .withArgs(false) 496 | .not.to.emit(vault, "FeesEarned"); 497 | 498 | const userBalance0Before = await token0.balanceOf(manager.address); 499 | const userBalance1Before = await token1.balanceOf(manager.address); 500 | const [amount0Current, amount1Current] = 501 | await vault.getUnderlyingBalances(); 502 | const totalSupply = await vault.totalSupply(); 503 | const vaultShares = await vault.balanceOf(manager.address); 504 | const managerBalance0Before = await vault.managerBalance0(); 505 | const managerBalance1Before = await vault.managerBalance1(); 506 | 507 | const managingFee = await vault.managingFee(); 508 | const userBalance0 = amount0Current.mul(vaultShares).div(totalSupply); 509 | const managingFee0 = userBalance0.mul(managingFee).div(10_000); 510 | 511 | const userBalance1 = amount1Current.mul(vaultShares).div(totalSupply); 512 | const managingFee1 = userBalance1.mul(managingFee).div(10_000); 513 | 514 | const { amount0: amount0Out, amount1: amount1Out } = 515 | await vault.getUnderlyingBalancesByShare(vaultShares); 516 | await expect( 517 | vault.burn(vaultShares, [amount0Out, amount1Out]) 518 | ).not.to.emit(vault, "FeesEarned"); 519 | expect(await token0.balanceOf(manager.address)).to.be.equal( 520 | userBalance0Before.add(userBalance0).sub(managingFee0) 521 | ); 522 | expect(await token1.balanceOf(manager.address)).to.be.equal( 523 | userBalance1Before.add(userBalance1).sub(managingFee1) 524 | ); 525 | expect(await vault.managerBalance0()).to.be.equal( 526 | managerBalance0Before.add(managingFee0) 527 | ); 528 | expect(await vault.managerBalance1()).to.be.equal( 529 | managerBalance1Before.add(managingFee1) 530 | ); 531 | 532 | // console.log(ethers.utils.formatEther(await vault.managerBalance0()), ethers.utils.formatEther(await vault.managerBalance1())) 533 | // 0.089999999999999997 0.089999999999999997 534 | }); 535 | }); 536 | 537 | describe("Add Liquidity", () => { 538 | before(async () => { 539 | await vault.updateTicks(lowerTick, upperTick); 540 | }); 541 | 542 | beforeEach(async () => { 543 | await token0.approve(vault.address, amount0.mul(bn(2))); 544 | await token1.approve(vault.address, amount1.mul(bn(2))); 545 | const { 546 | amount0: amount0ToAdd, 547 | amount1: amount1ToAdd, 548 | mintAmount, 549 | } = await vault.getMintAmounts(amount0, amount1); 550 | await vault.mint(mintAmount, [amount0ToAdd, amount1ToAdd]); 551 | await vault.removeLiquidity([0, 0]); 552 | }); 553 | 554 | it("should not add liquidity by non-manager", async () => { 555 | const amount0 = await token0.balanceOf(vault.address); 556 | const amount1 = await token1.balanceOf(vault.address); 557 | 558 | await expect( 559 | vault 560 | .connect(nonManager) 561 | .addLiquidity( 562 | lowerTick, 563 | upperTick, 564 | amount0, 565 | amount1, 566 | [ 567 | amount0.mul(9900).div(10000), 568 | amount1.mul(9900).div(10000) 569 | ], 570 | [ 571 | amount0.mul(10100).div(10000), 572 | amount1.mul(10100).div(10000) 573 | ] 574 | ) 575 | ).to.be.revertedWith("Ownable: caller is not the manager"); 576 | }); 577 | 578 | it("should not add liquidity when min amounts are not satisfied", async () => { 579 | const { amount0Current, amount1Current } = 580 | await vault.getUnderlyingBalances(); 581 | 582 | await expect( 583 | vault.addLiquidity( 584 | lowerTick, 585 | upperTick, 586 | amount0Current, 587 | amount1Current, 588 | [ 589 | amount0Current.mul(10100).div(10000), 590 | amount1Current.mul(10100).div(10000) 591 | ], 592 | [ 593 | amount0Current.mul(10100).div(10000), 594 | amount1Current.mul(10100).div(10000) 595 | ] 596 | ) 597 | ).to.be.revertedWithCustomError(vault, "SlippageExceedThreshold"); 598 | }); 599 | 600 | it("should not add liquidity when max amounts are not satisfied", async () => { 601 | const { amount0Current, amount1Current } = 602 | await vault.getUnderlyingBalances(); 603 | 604 | await expect( 605 | vault.addLiquidity( 606 | lowerTick, 607 | upperTick, 608 | amount0Current, 609 | amount1Current, 610 | [ 611 | amount0Current.mul(9900).div(10000), 612 | amount1Current.mul(9900).div(10000) 613 | ], 614 | [ 615 | amount0Current.mul(10100).div(10000), 616 | amount1Current.div(2) 617 | ] 618 | ) 619 | ).to.be.revertedWithCustomError(vault, "SlippageExceedThreshold"); 620 | }); 621 | 622 | it("should add liquidity by manager", async () => { 623 | const { amount0Current, amount1Current } = 624 | await vault.getUnderlyingBalances(); 625 | 626 | // eslint-disable-next-line @typescript-eslint/naming-convention 627 | const MockLiquidityAmounts = await ethers.getContractFactory( 628 | "MockLiquidityAmounts" 629 | ); 630 | const mockLiquidityAmounts = await MockLiquidityAmounts.deploy(); 631 | 632 | const { sqrtPriceX96 } = await univ3Pool.slot0(); 633 | const sqrtPriceA = new Decimal(1.0001) 634 | .pow(lowerTick) 635 | .sqrt() 636 | .mul(new Decimal(2).pow(96)) 637 | .round() 638 | .toFixed(); 639 | const sqrtPriceB = new Decimal(1.0001) 640 | .pow(upperTick) 641 | .sqrt() 642 | .mul(new Decimal(2).pow(96)) 643 | .round() 644 | .toFixed(); 645 | const liquidityToAdd = await mockLiquidityAmounts.getLiquidityForAmounts( 646 | sqrtPriceX96, 647 | sqrtPriceA, 648 | sqrtPriceB, 649 | await token0.balanceOf(vault.address), 650 | await token1.balanceOf(vault.address) 651 | ); 652 | const { amount0: amount0ToAdd, amount1: amount1ToAdd } = 653 | await mockLiquidityAmounts.getAmountsForLiquidity( 654 | sqrtPriceX96, 655 | sqrtPriceA, 656 | sqrtPriceB, 657 | liquidityToAdd 658 | ); 659 | await expect( 660 | vault.addLiquidity(lowerTick, upperTick, amount0ToAdd, amount1ToAdd, 661 | [ 662 | amount0ToAdd.mul(9900).div(10000), 663 | amount1ToAdd.mul(9900).div(10000) 664 | ], 665 | [ 666 | amount0ToAdd.mul(10100).div(10000), 667 | amount1ToAdd.mul(10100).div(10000) 668 | ] 669 | )) 670 | .to.emit(vault, "LiquidityAdded") 671 | .withArgs(liquidityToAdd, lowerTick, upperTick, anyValue, anyValue) 672 | .to.emit(vault, "InThePositionStatusSet") 673 | .withArgs(true); 674 | }); 675 | 676 | it("should not add liquidity when in the position", async () => { 677 | const { amount0Current, amount1Current } = 678 | await vault.getUnderlyingBalances(); 679 | 680 | await vault.addLiquidity( 681 | lowerTick, 682 | upperTick, 683 | amount0Current, 684 | amount1Current, 685 | [ 686 | amount0Current.mul(9900).div(10000), 687 | amount1Current.mul(9900).div(10000) 688 | ], 689 | [ 690 | amount0Current.mul(10100).div(10000), 691 | amount1Current.mul(10100).div(10000) 692 | ] 693 | ); 694 | await expect( 695 | vault.addLiquidity( 696 | lowerTick, 697 | upperTick, 698 | amount0Current, 699 | amount1Current, 700 | [ 701 | amount0Current.mul(9900).div(10000), 702 | amount1Current.mul(9900).div(10000) 703 | ], 704 | [ 705 | amount0Current.mul(10100).div(10000), 706 | amount1Current.mul(10100).div(10000) 707 | ] 708 | ) 709 | ).to.be.revertedWithCustomError(vault, "LiquidityAlreadyAdded"); 710 | }); 711 | }); 712 | 713 | describe("Swap", () => { 714 | it("should fail when minAmountIn is not satisfied", async () => { 715 | const { sqrtPriceX96 } = await univ3Pool.slot0(); 716 | const liquidity = await univ3Pool.liquidity(); 717 | await token1.transfer(vault.address, amount1); 718 | const priceDiff = amount1.mul(bn(2).pow(96)).div(liquidity); 719 | const priceNext = sqrtPriceX96.add(priceDiff); 720 | const ONE = bn(1).mul(bn(2).pow(96)); 721 | const minAmountIn = ONE.mul(ONE) 722 | .div(priceNext) 723 | .sub(ONE.mul(ONE).div(sqrtPriceX96)) 724 | .mul(liquidity) 725 | .div(bn(2).pow(96)) 726 | .mul(2); 727 | 728 | await expect( 729 | vault.swap(false, amount1, priceNext, (-minAmountIn).toString()) 730 | ).to.be.revertedWithCustomError(vault, "SlippageExceedThreshold"); 731 | }); 732 | }); 733 | 734 | describe("Fee collection", () => { 735 | it("non-manager should not collect fee", async () => { 736 | const { sqrtPriceX96 } = await univ3Pool.slot0(); 737 | const liquidity = await univ3Pool.liquidity(); 738 | await token1.transfer(vault.address, amount1); 739 | const priceDiff = amount1.mul(bn(2).pow(96)).div(liquidity); 740 | const priceNext = sqrtPriceX96.add(priceDiff); 741 | const ONE = bn(1).mul(bn(2).pow(96)); 742 | let minAmountIn = ONE.mul(ONE) 743 | .div(priceNext) 744 | .sub(ONE.mul(ONE).div(sqrtPriceX96)) 745 | .mul(liquidity) 746 | .div(bn(2).pow(96)); 747 | 748 | minAmountIn = minAmountIn.mul(bn(9_900)).div(bn(10_000)); 749 | await vault.swap(false, amount1, priceNext, (-minAmountIn).toString()); 750 | 751 | const { fee0, fee1 } = await vault.getCurrentFees(); 752 | await expect(vault.pullFeeFromPool()) 753 | .to.emit(vault, "FeesEarned") 754 | .withArgs(fee0, fee1); 755 | 756 | await expect( 757 | vault.connect(nonManager).collectManager() 758 | ).to.be.revertedWith("Ownable: caller is not the manager"); 759 | }); 760 | 761 | it("should manager collect fee", async () => { 762 | const { sqrtPriceX96 } = await univ3Pool.slot0(); 763 | const liquidity = await univ3Pool.liquidity(); 764 | await token1.transfer(vault.address, amount1); 765 | const priceDiff = amount1.mul(bn(2).pow(96)).div(liquidity); 766 | const priceNext = sqrtPriceX96.add(priceDiff); 767 | const ONE = bn(1).mul(bn(2).pow(96)); 768 | let minAmountIn = ONE.mul(ONE) 769 | .div(priceNext) 770 | .sub(ONE.mul(ONE).div(sqrtPriceX96)) 771 | .mul(liquidity) 772 | .div(bn(2).pow(bn(96))); 773 | minAmountIn = minAmountIn.mul(bn(9_900)).div(bn(10_000)); 774 | await vault.swap(false, amount1, priceNext, (-minAmountIn).toString()); 775 | 776 | const { fee0, fee1 } = await vault.getCurrentFees(); 777 | await expect(vault.pullFeeFromPool()) 778 | .to.emit(vault, "FeesEarned") 779 | .withArgs(fee0, fee1); 780 | 781 | const managerBalance0 = await vault.managerBalance0(); 782 | const managerBalance1 = await vault.managerBalance1(); 783 | 784 | const managerBalance0Before = await token0.balanceOf(manager.address); 785 | const managerBalance1Before = await token1.balanceOf(manager.address); 786 | await vault.connect(manager).collectManager(); 787 | 788 | expect(await token0.balanceOf(manager.address)).to.be.equal( 789 | managerBalance0Before.add(managerBalance0) 790 | ); 791 | expect(await token1.balanceOf(manager.address)).to.be.equal( 792 | managerBalance1Before.add(managerBalance1) 793 | ); 794 | 795 | expect(await vault.managerBalance0()).to.be.equal(0); 796 | expect(await vault.managerBalance1()).to.be.equal(0); 797 | }); 798 | 799 | it("pull fee using updateFee function", async () => { 800 | const { sqrtPriceX96 } = await univ3Pool.slot0(); 801 | const liquidity = await univ3Pool.liquidity(); 802 | await token1.transfer(vault.address, amount1); 803 | const priceDiff = amount1.mul(bn(2).pow(96)).div(liquidity); 804 | const priceNext = sqrtPriceX96.add(priceDiff); 805 | const ONE = bn(1).mul(bn(2).pow(96)); 806 | let minAmountIn = ONE.mul(ONE) 807 | .div(priceNext) 808 | .sub(ONE.mul(ONE).div(sqrtPriceX96)) 809 | .mul(liquidity) 810 | .div(bn(2).pow(bn(96))); 811 | 812 | minAmountIn = minAmountIn.mul(bn(9_900)).div(bn(10_000)); 813 | 814 | await vault.swap(false, amount1, priceNext, (-minAmountIn).toString()); 815 | const { fee0, fee1 } = await vault.getCurrentFees(); 816 | await expect(vault.updateFees(0, 0)) 817 | .to.emit(vault, "FeesEarned") 818 | .withArgs(fee0, fee1); 819 | 820 | const managerBalance0 = await vault.managerBalance0(); 821 | const managerBalance1 = await vault.managerBalance1(); 822 | const managerBalance0Before = await token0.balanceOf(manager.address); 823 | const managerBalance1Before = await token1.balanceOf(manager.address); 824 | await vault.connect(manager).collectManager(); 825 | 826 | expect(await token0.balanceOf(manager.address)).to.be.equal( 827 | managerBalance0Before.add(managerBalance0) 828 | ); 829 | expect(await token1.balanceOf(manager.address)).to.be.equal( 830 | managerBalance1Before.add(managerBalance1) 831 | ); 832 | 833 | expect(await vault.managerBalance0()).to.be.equal(0); 834 | expect(await vault.managerBalance1()).to.be.equal(0); 835 | }); 836 | }); 837 | 838 | describe("Test Upgradeability", () => { 839 | it("should not upgrade range vault implementation by non-manager of factory", async () => { 840 | // eslint-disable-next-line @typescript-eslint/naming-convention 841 | const RangeProtocolVault = await ethers.getContractFactory( 842 | "RangeProtocolVault" 843 | ); 844 | const newVaultImpl = 845 | (await RangeProtocolVault.deploy()) as RangeProtocolVault; 846 | 847 | await expect( 848 | factory 849 | .connect(nonManager) 850 | .upgradeVault(vault.address, newVaultImpl.address) 851 | ).to.be.revertedWith("Ownable: caller is not the owner"); 852 | 853 | await expect( 854 | factory 855 | .connect(nonManager) 856 | .upgradeVaults([vault.address], [newVaultImpl.address]) 857 | ).to.be.revertedWith("Ownable: caller is not the owner"); 858 | }); 859 | 860 | it("an EOA address provided as implementation should not upgrade the contract", async () => { 861 | const newVaultImpl = manager.address; 862 | await expect( 863 | factory.upgradeVault(vault.address, newVaultImpl) 864 | ).to.be.revertedWithCustomError(factory, "ImplIsNotAContract"); 865 | }); 866 | 867 | it("should upgrade range vault implementation by factory manager", async () => { 868 | // eslint-disable-next-line @typescript-eslint/naming-convention 869 | const RangeProtocolVault = await ethers.getContractFactory( 870 | "RangeProtocolVault" 871 | ); 872 | const newVaultImpl = 873 | (await RangeProtocolVault.deploy()) as RangeProtocolVault; 874 | 875 | const implSlot = await vaultImpl.proxiableUUID(); 876 | expect( 877 | await ethers.provider.getStorageAt(vault.address, implSlot) 878 | ).to.be.equal( 879 | ethers.utils.hexZeroPad(vaultImpl.address.toLowerCase(), 32) 880 | ); 881 | await expect(factory.upgradeVault(vault.address, newVaultImpl.address)) 882 | .to.emit(factory, "VaultImplUpgraded") 883 | .withArgs(vault.address, newVaultImpl.address); 884 | 885 | expect( 886 | await ethers.provider.getStorageAt(vault.address, implSlot) 887 | ).to.be.equal( 888 | ethers.utils.hexZeroPad(newVaultImpl.address.toLowerCase(), 32) 889 | ); 890 | 891 | const newVaultImpl1 = 892 | (await RangeProtocolVault.deploy()) as RangeProtocolVault; 893 | 894 | expect( 895 | await ethers.provider.getStorageAt(vault.address, implSlot) 896 | ).to.be.equal( 897 | ethers.utils.hexZeroPad(newVaultImpl.address.toLowerCase(), 32) 898 | ); 899 | await expect( 900 | factory.upgradeVaults([vault.address], [newVaultImpl1.address]) 901 | ) 902 | .to.emit(factory, "VaultImplUpgraded") 903 | .withArgs(vault.address, newVaultImpl1.address); 904 | 905 | expect( 906 | await ethers.provider.getStorageAt(vault.address, implSlot) 907 | ).to.be.equal( 908 | ethers.utils.hexZeroPad(newVaultImpl1.address.toLowerCase(), 32) 909 | ); 910 | 911 | vaultImpl = newVaultImpl1; 912 | }); 913 | }); 914 | 915 | describe("transferOwnership", () => { 916 | it("should not be able to transferOwnership by non manager", async () => { 917 | await expect( 918 | vault.connect(nonManager).transferOwnership(newManager.address) 919 | ).to.be.revertedWith("Ownable: caller is not the manager"); 920 | }); 921 | 922 | it("should be able to transferOwnership by manager", async () => { 923 | await expect(vault.transferOwnership(newManager.address)) 924 | .to.emit(vault, "OwnershipTransferred") 925 | .withArgs(manager.address, newManager.address); 926 | expect(await vault.manager()).to.be.equal(newManager.address); 927 | 928 | await vault.connect(newManager).transferOwnership(manager.address); 929 | expect(await vault.manager()).to.be.equal(manager.address); 930 | }); 931 | }); 932 | }); 933 | -------------------------------------------------------------------------------- /test/common.ts: -------------------------------------------------------------------------------- 1 | // returns the sqrt price as a 64x96 2 | import { BigNumber } from "bignumber.js"; 3 | import { ethers } from "hardhat"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/naming-convention 6 | BigNumber.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 }); 7 | 8 | export const encodePriceSqrt = (reserve1: string, reserve0: string) => { 9 | return new BigNumber(reserve1) 10 | .div(reserve0) 11 | .sqrt() 12 | .multipliedBy(new BigNumber(2).pow(96)) 13 | .integerValue(3) 14 | .toString(); 15 | }; 16 | export const position = ( 17 | address: string, 18 | lowerTick: number, 19 | upperTick: number 20 | ) => { 21 | return ethers.utils.solidityKeccak256( 22 | ["address", "int24", "int24"], 23 | [address, lowerTick, upperTick] 24 | ); 25 | }; 26 | 27 | export const getInitializeData = (params: { 28 | managerAddress: string; 29 | name: string; 30 | symbol: string; 31 | }): any => 32 | ethers.utils.defaultAbiCoder.encode( 33 | ["address", "string", "string"], 34 | [ 35 | params.managerAddress, 36 | params.name, 37 | params.symbol, 38 | ] 39 | ); 40 | 41 | export const bn = (value: any) => ethers.BigNumber.from(value); 42 | 43 | export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 44 | export const parseEther = (value: string) => ethers.utils.parseEther(value); 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": true, 5 | "allowSyntheticDefaultImports": true, 6 | "downlevelIteration": true, 7 | "skipLibCheck": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "typeRoots": ["./node_modules/@types"], 11 | "outDir": "dist" 12 | }, 13 | "files": ["hardhat.config.ts"], 14 | "include": ["src", "deploy", "scripts", "test", "typechain"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------