├── .env.example ├── .gitignore ├── 96x96 logo.png ├── Gamma Security Review (Jan 2024).pdf ├── LICENSE ├── README.md ├── audits ├── AE_Gamma_audit_09_03_22.pdf ├── ConsenSys-Diligence-Audit-28-03-22.pdf └── REP-Hypervisor-2021-07-07.pdf ├── contracts ├── ClearingV2.sol ├── Hypervisor.sol ├── HypervisorFactory.sol ├── RebalanceProxy.sol ├── UniProxy.sol ├── adapters │ └── tokemak │ │ ├── BaseController.sol │ │ ├── GammaController.sol │ │ ├── TokeHypervisor.sol │ │ ├── TokeHypervisorFactory.sol │ │ └── interfaces │ │ ├── IHypervisorFactory.sol │ │ └── ITokeHypervisor.sol ├── interfaces │ ├── IHypervisor.sol │ ├── IUniProxy.sol │ ├── IUniversalVault.sol │ └── IVault.sol ├── mocks │ ├── MockUniswapV3Pool.sol │ ├── MockUniswapV3PoolDeployer.sol │ └── TestERC20.sol ├── proxy │ ├── AutoRebal.sol │ └── admin.sol └── test │ ├── MockToken.sol │ └── TestRouter.sol ├── funding.json ├── gamma logo.svg ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── scripts ├── copy-uniswap-v3-artifacts.ts ├── flatten.sh └── test.sh ├── tasks ├── hypervisor.ts ├── shared │ ├── tick.ts │ └── utilities.ts ├── swap.ts └── utils.ts ├── test ├── deposit_withdraw.test.ts ├── shared │ ├── ethUtils.ts │ ├── fixtures.ts │ └── utilities.ts └── tokemak.test.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | ALCHEMY_API_KEY='' 2 | MAINNET_PRIVATE_KEY='' 3 | ETHERSCAN_APIKEY='' 4 | GRAPH_KEY='' 5 | CMC_APIKEY='' 6 | ETHEREUM_ARCHIVE_URL='' 7 | REPORT_GAS=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.* 2 | node_modules 3 | 4 | # include only following dirs/files 5 | !/audits/** 6 | !/contracts/** 7 | !/patches/** 8 | !/scripts/** 9 | !/tasks/** 10 | !/test/** 11 | !/.gitignore 12 | !/.env.example 13 | !/hardhat.config.ts 14 | !/LICENSE 15 | !/package.json 16 | !/yarn.lock 17 | !/README.md 18 | !/tsconfig.json 19 | -------------------------------------------------------------------------------- /96x96 logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaStrategies/hypervisor/fd04069625988e714313b6e3c0b8ae3a02c5e403/96x96 logo.png -------------------------------------------------------------------------------- /Gamma Security Review (Jan 2024).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaStrategies/hypervisor/fd04069625988e714313b6e3c0b8ae3a02c5e403/Gamma Security Review (Jan 2024).pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | --- 7 | 8 | Parameters 9 | 10 | Licensor: Gamma 11 | 12 | Licensed Work: Hypervisor 13 | 14 | Additional Use Grant: Any uses listed and defined at this [LICENSE](https://github.com/GammaStrategies/hypervisor/blob/master/LICENSE) 15 | 16 | Change Date: 1 January 2024 17 | 18 | Change License: GNU General Public License v2.0 or later 19 | 20 | --- 21 | 22 | Terms 23 | 24 | The Licensor hereby grants you the right to copy, modify, create derivative 25 | works, redistribute, and make non-production use of the Licensed Work. The 26 | Licensor may make an Additional Use Grant, above, permitting limited 27 | production use. 28 | 29 | Effective on the Change Date, or the fourth anniversary of the first publicly 30 | available distribution of a specific version of the Licensed Work under this 31 | License, whichever comes first, the Licensor hereby grants you rights under 32 | the terms of the Change License, and the rights granted in the paragraph 33 | above terminate. 34 | 35 | If your use of the Licensed Work does not comply with the requirements 36 | currently in effect as described in this License, you must purchase a 37 | commercial license from the Licensor, its affiliated entities, or authorized 38 | resellers, or you must refrain from using the Licensed Work. 39 | 40 | All copies of the original and modified Licensed Work, and derivative works 41 | of the Licensed Work, are subject to this License. This License applies 42 | separately for each version of the Licensed Work and the Change Date may vary 43 | for each version of the Licensed Work released by Licensor. 44 | 45 | You must conspicuously display this License on each original or modified copy 46 | of the Licensed Work. If you receive the Licensed Work in original or 47 | modified form from a third party, the terms and conditions set forth in this 48 | License apply to your use of that work. 49 | 50 | Any use of the Licensed Work in violation of this License will automatically 51 | terminate your rights under this License for the current and all other 52 | versions of the Licensed Work. 53 | 54 | This License does not grant you any right in any trademark or logo of 55 | Licensor or its affiliates (provided that you may use a trademark or logo of 56 | Licensor as expressly required by this License). 57 | 58 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 59 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 60 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 61 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 62 | TITLE. 63 | 64 | MariaDB hereby grants you permission to use this License’s text to license 65 | your works, and to refer to it using the trademark "Business Source License", 66 | as long as you comply with the Covenants of Licensor below. 67 | 68 | --- 69 | 70 | Covenants of Licensor 71 | 72 | In consideration of the right to use this License’s text and the "Business 73 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 74 | other recipients of the licensed work to be provided by Licensor: 75 | 76 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 77 | or a license that is compatible with GPL Version 2.0 or a later version, 78 | where "compatible" means that software provided under the Change License can 79 | be included in a program with software provided under GPL Version 2.0 or a 80 | later version. Licensor may specify additional Change Licenses without 81 | limitation. 82 | 83 | 2. To either: (a) specify an additional grant of rights to use that does not 84 | impose any additional restriction on the right granted in this License, as 85 | the Additional Use Grant; or (b) insert the text "None". 86 | 87 | 3. To specify a Change Date. 88 | 89 | 4. Not to modify this License in any other way. 90 | 91 | --- 92 | 93 | Notice 94 | 95 | The Business Source License (this document, or the "License") is not an Open 96 | Source license. However, the Licensed Work will eventually be made available 97 | under an Open Source License, as stated in this License. 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Hypervisor 2 | 3 | ### 4 | A Uniswap V2-like interface with fungible liquidity to Uniswap V3 5 | which allows for arbitrary liquidity provision: one-sided, lop-sided, and 6 | balanced 7 | 8 | Consult tests/deposit_withdraw.test.ts for deposit, withdrawal, rebalance examples 9 | 10 | ### Tasks 11 | 12 | Deploys hypervisor 13 | 14 | `npx hardhat deploy-hypervisor-orphan --pool UNIV3-POOL-ADDRESS --name ERC20-NAME --symbol ERC20-SYMBOL --network NETWORK` 15 | 16 | Initialize hypervisor 17 | 18 | `npx hardhat initialize-hypervisor --hypervisor HYPERVISOR-ADDRESS --amount0 TOKEN0-AMOUNT --amount1 TOKEN1-AMOUNT --uniProxy UNIPROXY-ADDRESS --adminAddress ADMIN-ADDRESS --network NETWORK` 19 | 20 | ### Testing 21 | 22 | `npx hardhat test` 23 | -------------------------------------------------------------------------------- /audits/AE_Gamma_audit_09_03_22.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaStrategies/hypervisor/fd04069625988e714313b6e3c0b8ae3a02c5e403/audits/AE_Gamma_audit_09_03_22.pdf -------------------------------------------------------------------------------- /audits/ConsenSys-Diligence-Audit-28-03-22.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaStrategies/hypervisor/fd04069625988e714313b6e3c0b8ae3a02c5e403/audits/ConsenSys-Diligence-Audit-28-03-22.pdf -------------------------------------------------------------------------------- /audits/REP-Hypervisor-2021-07-07.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaStrategies/hypervisor/fd04069625988e714313b6e3c0b8ae3a02c5e403/audits/REP-Hypervisor-2021-07-07.pdf -------------------------------------------------------------------------------- /contracts/ClearingV2.sol: -------------------------------------------------------------------------------- 1 | /// SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "./interfaces/IHypervisor.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 8 | import "@uniswap/v3-core/contracts/libraries/FullMath.sol"; 9 | import "@openzeppelin/contracts/math/SafeMath.sol"; 10 | import "@openzeppelin/contracts/math/SignedSafeMath.sol"; 11 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 12 | import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; 13 | 14 | /// @title Clearing v2 15 | /// @notice Proxy contract for hypervisor positions management 16 | contract ClearingV2 is ReentrancyGuard { 17 | using SafeERC20 for IERC20; 18 | using SafeMath for uint256; 19 | using SignedSafeMath for int256; 20 | 21 | string constant VERSION = '2.0.0'; 22 | address public owner; 23 | bool public paused; 24 | mapping(address => Position) public positions; 25 | 26 | bool public twapCheck = true; 27 | uint32 public twapInterval = 3600; 28 | uint256 public depositDelta = 10_010; 29 | uint256 public deltaScale = 10_000; /// must be a power of 10 30 | uint256 public priceThreshold = 10_000; 31 | uint256 constant MAX_UINT = 2**256 - 1; 32 | uint256 public constant PRECISION = 1e36; 33 | mapping (address => mapping (address => bool)) public freeDepositList; 34 | 35 | struct Position { 36 | bool customRatio; 37 | bool customTwap; 38 | bool ratioRemoved; 39 | bool depositOverride; // force custom deposit constraints 40 | bool twapOverride; // force twap check for hypervisor instance 41 | uint8 version; 42 | uint32 twapInterval; // override global twap 43 | uint256 priceThreshold; // custom price threshold 44 | uint256 deposit0Max; 45 | uint256 deposit1Max; 46 | uint256 maxTotalSupply; 47 | uint256 fauxTotal0; 48 | uint256 fauxTotal1; 49 | uint256 customDepositDelta; 50 | // mapping(address=>bool) list; // whitelist certain accounts for freedeposit 51 | } 52 | 53 | event PositionAdded(address, uint8); 54 | event CustomDeposit(address, uint256, uint256, uint256); 55 | event PriceThresholdSet(uint256 _priceThreshold); 56 | event DepositDeltaSet(uint256 _depositDelta); 57 | event DeltaScaleSet(uint256 _deltaScale); 58 | event TwapIntervalSet(uint32 _twapInterval); 59 | event TwapOverrideSet(address pos, bool twapOverride, uint32 _twapInterval, uint256 _priceThreshold); 60 | event PriceThresholdPosSet(address pos, uint256 _priceThreshold); 61 | event DepositOverrideSet(address pos, bool depositOverride); 62 | event TwapCheckSet(bool twapCheck); 63 | event ListAppended(address pos, address[] listed); 64 | event ListRemoved(address pos, address listed); 65 | event CustomRatio(address pos, uint256 fauxTotal0, uint256 fauxTotal1); 66 | event RatioRemoved(address pos); 67 | 68 | constructor() { 69 | owner = msg.sender; 70 | } 71 | 72 | modifier onlyAddedPosition(address pos) { 73 | Position storage p = positions[pos]; 74 | require(p.version != 0, "not added"); 75 | _; 76 | } 77 | 78 | /// @notice Add the hypervisor position 79 | /// @param pos Address of the hypervisor 80 | /// @param version Type of hypervisor 81 | function addPosition(address pos, uint8 version) external onlyOwner { 82 | Position storage p = positions[pos]; 83 | require(p.version == 0, 'already added'); 84 | require(version > 0, 'version < 1'); 85 | p.version = version; 86 | IHypervisor(pos).token0().safeApprove(pos, MAX_UINT); 87 | IHypervisor(pos).token1().safeApprove(pos, MAX_UINT); 88 | emit PositionAdded(pos, version); 89 | } 90 | 91 | /// @notice apply configuration constraints to shares minted 92 | /// @param pos Address of the hypervisor 93 | /// @param shares Amount of shares minted (included for upgrades) 94 | /// @return cleared whether shares are cleared 95 | function clearShares( 96 | address pos, 97 | uint256 shares 98 | ) public view onlyAddedPosition(pos) returns (bool cleared) { 99 | if(positions[pos].maxTotalSupply != 0) { 100 | require(IHypervisor(pos).totalSupply() <= positions[pos].maxTotalSupply, "exceeds max supply"); 101 | } 102 | return true; 103 | } 104 | 105 | /// @notice apply configuration constraints to deposit 106 | /// @param pos Address of the hypervisor 107 | /// @param deposit0 Amount of token0 to deposit 108 | /// @param deposit1 Amount of token1 to deposit 109 | /// @param to Address to receive liquidity tokens 110 | /// @param pos Hypervisor Address 111 | /// @param minIn min assets to expect in position during a direct deposit 112 | /// @return cleared whether deposit is cleared 113 | function clearDeposit( 114 | uint256 deposit0, 115 | uint256 deposit1, 116 | address from, 117 | address to, 118 | address pos, 119 | uint256[4] memory minIn 120 | ) public view onlyAddedPosition(pos) returns (bool cleared) { 121 | require(!paused, "paused"); 122 | require(to != address(0), "to should be non-zero"); 123 | require( 124 | IHypervisor(pos).currentTick() >= IHypervisor(pos).baseLower() && 125 | IHypervisor(pos).currentTick() < IHypervisor(pos).baseUpper(), 126 | "price out of base range" 127 | ); 128 | Position storage p = positions[pos]; 129 | if (twapCheck || p.twapOverride) { 130 | /// check twap 131 | checkPriceChange( 132 | pos, 133 | (p.twapOverride ? p.twapInterval : twapInterval), 134 | (p.twapOverride ? p.priceThreshold : priceThreshold) 135 | ); 136 | } 137 | 138 | if (!freeDepositList[pos][from]) { 139 | require(deposit0 > 0 && deposit1 > 0, "must deposit to both sides"); 140 | (uint256 test1Min, uint256 test1Max) = getDepositAmount(pos, address(IHypervisor(pos).token0()), deposit0); 141 | require(deposit1 >= test1Min && deposit1 <= test1Max, "Improper ratio"); 142 | 143 | (uint256 test0Min, uint256 test0Max) = getDepositAmount(pos, address(IHypervisor(pos).token1()), deposit1); 144 | require(deposit0 >= test0Min && deposit0 <= test0Max, "Improper ratio"); 145 | 146 | if (p.depositOverride) { 147 | require(deposit0 <= p.deposit0Max, "token0 exceeds"); 148 | require(deposit1 <= p.deposit1Max, "token1 exceeds"); 149 | } 150 | } 151 | 152 | return true; 153 | } 154 | 155 | /// @notice Get the amount of token to deposit for the given amount of pair token 156 | /// @param pos Hypervisor Address 157 | /// @param token Address of token to deposit 158 | /// @param _deposit Amount of token to deposit 159 | /// @return amountStart Minimum amounts of the pair token to deposit 160 | /// @return amountEnd Maximum amounts of the pair token to deposit 161 | function getDepositAmount( 162 | address pos, 163 | address token, 164 | uint256 _deposit 165 | ) public view returns (uint256 amountStart, uint256 amountEnd) { 166 | require(token == address(IHypervisor(pos).token0()) || token == address(IHypervisor(pos).token1()), "token mistmatch"); 167 | require(_deposit > 0, "deposits can't be zero"); 168 | (uint256 total0, uint256 total1) = IHypervisor(pos).getTotalAmounts(); 169 | if (IHypervisor(pos).totalSupply() == 0) { 170 | amountStart = 0; 171 | if (positions[pos].depositOverride) { 172 | amountEnd = (token == address(IHypervisor(pos).token0())) ? 173 | positions[pos].deposit1Max : 174 | positions[pos].deposit0Max; 175 | } else { 176 | amountEnd = (token == address(IHypervisor(pos).token0())) ? 177 | IHypervisor(pos).deposit1Max() : 178 | IHypervisor(pos).deposit0Max(); 179 | } 180 | } else if (total0 == 0 || total1 == 0) { 181 | amountStart = 0; 182 | amountEnd = 0; 183 | } else { 184 | (uint256 ratioStart, uint256 ratioEnd) = positions[pos].customRatio ? 185 | applyRatio(pos, token, positions[pos].fauxTotal0, positions[pos].fauxTotal1) : 186 | applyRatio(pos, token, total0, total1); 187 | amountStart = FullMath.mulDiv(_deposit, PRECISION, ratioStart); 188 | amountEnd = FullMath.mulDiv(_deposit, PRECISION, ratioEnd); 189 | } 190 | } 191 | 192 | /// @notice Get range for deposit based on provided amounts 193 | /// @param pos Hypervisor Address 194 | /// @param token Address of token to deposit 195 | /// @param total0 Amount of token0 in hype 196 | /// @param total1 Amount of token1 in hype 197 | /// @return ratioStart Minimum amounts of the pair token to deposit 198 | /// @return ratioEnd Maximum amounts of the pair token to deposit 199 | function applyRatio( 200 | address pos, 201 | address token, 202 | uint256 total0, 203 | uint256 total1 204 | ) public view returns (uint256 ratioStart, uint256 ratioEnd) { 205 | require(token == address(IHypervisor(pos).token0()) || token == address(IHypervisor(pos).token1()), "token mistmatch"); 206 | uint256 _depositDelta = positions[pos].depositOverride ? 207 | positions[pos].customDepositDelta : 208 | depositDelta; 209 | if (token == address(IHypervisor(pos).token0())) { 210 | ratioStart = FullMath.mulDiv(total0.mul(_depositDelta), PRECISION, total1.mul(deltaScale)); 211 | ratioEnd = FullMath.mulDiv(total0.mul(deltaScale), PRECISION, total1.mul(_depositDelta)); 212 | } else { 213 | ratioStart = FullMath.mulDiv(total1.mul(_depositDelta), PRECISION, total0.mul(deltaScale)); 214 | ratioEnd = FullMath.mulDiv(total1.mul(deltaScale), PRECISION, total0.mul(_depositDelta)); 215 | } 216 | } 217 | 218 | /// @notice Check if the price change overflows or not based on given twap and threshold in the hypervisor 219 | /// @param pos Hypervisor Address 220 | /// @param _twapInterval Time intervals 221 | /// @param _priceThreshold Price Threshold 222 | /// @return price Current price 223 | function checkPriceChange( 224 | address pos, 225 | uint32 _twapInterval, 226 | uint256 _priceThreshold 227 | ) public view returns (uint256 price) { 228 | 229 | (uint160 sqrtPrice, , , , , , ) = IHypervisor(pos).pool().slot0(); 230 | price = FullMath.mulDiv(uint256(sqrtPrice).mul(uint256(sqrtPrice)), PRECISION, 2**(96 * 2)); 231 | 232 | uint160 sqrtPriceBefore = getSqrtTwapX96(pos, _twapInterval); 233 | uint256 priceBefore = FullMath.mulDiv(uint256(sqrtPriceBefore).mul(uint256(sqrtPriceBefore)), PRECISION, 2**(96 * 2)); 234 | if (price.mul(10_000).div(priceBefore) > _priceThreshold || priceBefore.mul(10_000).div(price) > _priceThreshold) 235 | revert("Price change Overflow"); 236 | } 237 | 238 | /// @notice Get the sqrt price before the given interval 239 | /// @param pos Hypervisor Address 240 | /// @param _twapInterval Time intervals 241 | /// @return sqrtPriceX96 Sqrt price before interval 242 | function getSqrtTwapX96(address pos, uint32 _twapInterval) public view returns (uint160 sqrtPriceX96) { 243 | if (_twapInterval == 0) { 244 | /// return the current price if _twapInterval == 0 245 | (sqrtPriceX96, , , , , , ) = IHypervisor(pos).pool().slot0(); 246 | } 247 | else { 248 | uint32[] memory secondsAgos = new uint32[](2); 249 | secondsAgos[0] = _twapInterval; /// from (before) 250 | secondsAgos[1] = 0; /// to (now) 251 | 252 | (int56[] memory tickCumulatives, ) = IHypervisor(pos).pool().observe(secondsAgos); 253 | 254 | /// tick(imprecise as it's an integer) to price 255 | sqrtPriceX96 = TickMath.getSqrtRatioAtTick( 256 | int24((tickCumulatives[1] - tickCumulatives[0]) / _twapInterval) 257 | ); 258 | } 259 | } 260 | 261 | /// @param _priceThreshold Price Threshold 262 | function setPriceThreshold(uint256 _priceThreshold) external onlyOwner { 263 | priceThreshold = _priceThreshold; 264 | emit PriceThresholdSet(_priceThreshold); 265 | } 266 | 267 | /// @param _depositDelta Number to calculate deposit ratio 268 | function setDepositDelta(uint256 _depositDelta) external onlyOwner { 269 | depositDelta = _depositDelta; 270 | emit DepositDeltaSet(_depositDelta); 271 | } 272 | 273 | /// @param _deltaScale Number to calculate deposit ratio 274 | function setDeltaScale(uint256 _deltaScale) external onlyOwner { 275 | deltaScale = _deltaScale; 276 | emit DeltaScaleSet(_deltaScale); 277 | } 278 | 279 | /// @param pos Hypervisor address 280 | /// @param deposit0Max Amount of maximum deposit amounts of token0 281 | /// @param deposit1Max Amount of maximum deposit amounts of token1 282 | /// @param maxTotalSupply Maximum total suppoy of hypervisor 283 | /// @param customDepositDelta custom deposit delta 284 | function customDeposit( 285 | address pos, 286 | uint256 deposit0Max, 287 | uint256 deposit1Max, 288 | uint256 maxTotalSupply, 289 | uint256 customDepositDelta 290 | ) external onlyOwner onlyAddedPosition(pos) { 291 | Position storage p = positions[pos]; 292 | p.deposit0Max = deposit0Max; 293 | p.deposit1Max = deposit1Max; 294 | p.maxTotalSupply = maxTotalSupply; 295 | p.customDepositDelta = customDepositDelta; 296 | emit CustomDeposit(pos, deposit0Max, deposit1Max, maxTotalSupply); 297 | } 298 | 299 | /// @param pos Hypervisor address 300 | /// @param _customRatio whether to use custom ratio 301 | /// @param fauxTotal0 override total0 302 | /// @param fauxTotal1 override total1 303 | function customRatio( 304 | address pos, 305 | bool _customRatio, 306 | uint256 fauxTotal0, 307 | uint256 fauxTotal1 308 | ) external onlyOwner onlyAddedPosition(pos) { 309 | require(!positions[pos].ratioRemoved, "custom ratio is no longer available"); 310 | Position storage p = positions[pos]; 311 | p.customRatio = _customRatio; 312 | p.fauxTotal0 = fauxTotal0; 313 | p.fauxTotal1 = fauxTotal1; 314 | emit CustomRatio(pos, fauxTotal0, fauxTotal1); 315 | } 316 | 317 | // @note permantently remove ability to apply custom ratio to hype 318 | function removeRatio(address pos) external onlyOwner onlyAddedPosition(pos) { 319 | Position storage p = positions[pos]; 320 | p.ratioRemoved = true; 321 | emit RatioRemoved(pos); 322 | } 323 | 324 | /// @notice set deposit override 325 | /// @param pos Hypervisor Address 326 | function setDepositOverride(address pos, bool _depositOverride) external onlyOwner onlyAddedPosition(pos) { 327 | Position storage p = positions[pos]; 328 | p.depositOverride = _depositOverride; 329 | emit DepositOverrideSet(pos, _depositOverride); 330 | } 331 | 332 | /// @param _twapInterval Time intervals 333 | function setTwapInterval(uint32 _twapInterval) external onlyOwner { 334 | twapInterval = _twapInterval; 335 | emit TwapIntervalSet(_twapInterval); 336 | } 337 | 338 | /// @param pos Hypervisor Address 339 | /// @param twapOverride Twap Override 340 | /// @param _twapInterval Time Intervals 341 | /// @param _priceThreshold Price Threshold 342 | function setTwapOverride(address pos, bool twapOverride, uint32 _twapInterval, uint256 _priceThreshold) external onlyOwner onlyAddedPosition(pos) { 343 | Position storage p = positions[pos]; 344 | p.twapOverride = twapOverride; 345 | p.twapInterval = _twapInterval; 346 | p.priceThreshold = _priceThreshold; 347 | emit TwapOverrideSet(pos, twapOverride, _twapInterval, _priceThreshold); 348 | } 349 | 350 | /// @notice Set Twap 351 | function setTwapCheck(bool _twapCheck) external onlyOwner { 352 | twapCheck = _twapCheck; 353 | emit TwapCheckSet(_twapCheck); 354 | } 355 | 356 | // @notice check if an address is whitelisted for hype 357 | function getListed(address pos, address i) public view returns(bool) { 358 | return freeDepositList[pos][i]; 359 | } 360 | 361 | function getPositionInfo(address pos) public view returns (Position memory) { 362 | return positions[pos]; 363 | } 364 | 365 | /// @notice Append whitelist to hypervisor 366 | /// @param pos Hypervisor Address 367 | /// @param listed Address array to add in whitelist 368 | function appendList(address pos, address[] memory listed) external onlyOwner onlyAddedPosition(pos) { 369 | for (uint8 i; i < listed.length; i++) { 370 | freeDepositList[pos][listed[i]] = true; 371 | } 372 | emit ListAppended(pos, listed); 373 | } 374 | 375 | /// @notice Remove address from whitelist 376 | /// @param pos Hypervisor Address 377 | /// @param listed Address to remove from whitelist 378 | function removeListed(address pos, address listed) external onlyOwner onlyAddedPosition(pos) { 379 | freeDepositList[pos][listed] = false; 380 | emit ListRemoved(pos, listed); 381 | } 382 | 383 | function pause(bool _paused) external onlyOwner { 384 | paused = _paused; 385 | } 386 | 387 | function transferOwnership(address newOwner) external onlyOwner { 388 | require(newOwner != address(0), "newOwner should be non-zero"); 389 | owner = newOwner; 390 | } 391 | 392 | modifier onlyOwner { 393 | require(msg.sender == owner, "only owner"); 394 | _; 395 | } 396 | } -------------------------------------------------------------------------------- /contracts/Hypervisor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | 3 | pragma solidity 0.7.6; 4 | 5 | import "@openzeppelin/contracts/math/Math.sol"; 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | import "@openzeppelin/contracts/math/SignedSafeMath.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 10 | import "@openzeppelin/contracts/drafts/ERC20Permit.sol"; 11 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 12 | 13 | import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol"; 14 | import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 15 | import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; 16 | import "@uniswap/v3-core/contracts/libraries/FullMath.sol"; 17 | import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; 18 | 19 | /// @title Hypervisor v1.3 20 | /// @notice A Uniswap V2-like interface with fungible liquidity to Uniswap V3 21 | /// which allows for arbitrary liquidity provision: one-sided, lop-sided, and balanced 22 | contract Hypervisor is IUniswapV3MintCallback, ERC20Permit, ReentrancyGuard { 23 | using SafeERC20 for IERC20; 24 | using SafeMath for uint256; 25 | using SignedSafeMath for int256; 26 | 27 | IUniswapV3Pool public pool; 28 | IERC20 public token0; 29 | IERC20 public token1; 30 | uint8 public fee = 5; 31 | int24 public tickSpacing; 32 | 33 | int24 public baseLower; 34 | int24 public baseUpper; 35 | int24 public limitLower; 36 | int24 public limitUpper; 37 | 38 | address public owner; 39 | uint256 public deposit0Max; 40 | uint256 public deposit1Max; 41 | uint256 public maxTotalSupply; 42 | address public whitelistedAddress; 43 | address public feeRecipient; 44 | bool public directDeposit; /// enter uni on deposit (avoid if client uses public rpc) 45 | 46 | uint256 public constant PRECISION = 1e36; 47 | 48 | bool mintCalled; 49 | 50 | event Deposit( 51 | address indexed sender, 52 | address indexed to, 53 | uint256 shares, 54 | uint256 amount0, 55 | uint256 amount1 56 | ); 57 | 58 | event Withdraw( 59 | address indexed sender, 60 | address indexed to, 61 | uint256 shares, 62 | uint256 amount0, 63 | uint256 amount1 64 | ); 65 | 66 | event Rebalance( 67 | int24 tick, 68 | uint256 totalAmount0, 69 | uint256 totalAmount1, 70 | uint256 feeAmount0, 71 | uint256 feeAmount1, 72 | uint256 totalSupply 73 | ); 74 | 75 | event ZeroBurn(uint8 fee, uint256 fees0, uint256 fees1); 76 | event SetFee(uint8 newFee); 77 | 78 | 79 | /// @param _pool Uniswap V3 pool for which liquidity is managed 80 | /// @param _owner Owner of the Hypervisor 81 | constructor( 82 | address _pool, 83 | address _owner, 84 | string memory name, 85 | string memory symbol 86 | ) ERC20Permit(name) ERC20(name, symbol) { 87 | require(_pool != address(0)); 88 | require(_owner != address(0)); 89 | pool = IUniswapV3Pool(_pool); 90 | token0 = IERC20(pool.token0()); 91 | token1 = IERC20(pool.token1()); 92 | require(address(token0) != address(0)); 93 | require(address(token1) != address(0)); 94 | tickSpacing = pool.tickSpacing(); 95 | 96 | owner = _owner; 97 | 98 | maxTotalSupply = 0; /// no cap 99 | deposit0Max = uint256(-1); 100 | deposit1Max = uint256(-1); 101 | } 102 | 103 | /// @notice Deposit tokens 104 | /// @param deposit0 Amount of token0 transfered from sender to Hypervisor 105 | /// @param deposit1 Amount of token1 transfered from sender to Hypervisor 106 | /// @param to Address to which liquidity tokens are minted 107 | /// @param from Address from which asset tokens are transferred 108 | /// @param inMin min spend for directDeposit is true 109 | /// @return shares Quantity of liquidity tokens minted as a result of deposit 110 | function deposit( 111 | uint256 deposit0, 112 | uint256 deposit1, 113 | address to, 114 | address from, 115 | uint256[4] memory inMin 116 | ) nonReentrant external returns (uint256 shares) { 117 | require(deposit0 > 0 || deposit1 > 0); 118 | require(deposit0 <= deposit0Max && deposit1 <= deposit1Max); 119 | require(to != address(0) && to != address(this), "to"); 120 | require(msg.sender == whitelistedAddress, "WHE"); 121 | 122 | /// update fees 123 | zeroBurn(); 124 | 125 | (uint160 sqrtPrice, , , , , , ) = pool.slot0(); 126 | uint256 price = FullMath.mulDiv(uint256(sqrtPrice).mul(uint256(sqrtPrice)), PRECISION, 2**(96 * 2)); 127 | 128 | (uint256 pool0, uint256 pool1) = getTotalAmounts(); 129 | 130 | shares = deposit1.add(deposit0.mul(price).div(PRECISION)); 131 | 132 | if (deposit0 > 0) { 133 | token0.safeTransferFrom(from, address(this), deposit0); 134 | } 135 | if (deposit1 > 0) { 136 | token1.safeTransferFrom(from, address(this), deposit1); 137 | } 138 | 139 | uint256 total = totalSupply(); 140 | if (total != 0) { 141 | uint256 pool0PricedInToken1 = pool0.mul(price).div(PRECISION); 142 | shares = shares.mul(total).div(pool0PricedInToken1.add(pool1)); 143 | if (directDeposit) { 144 | uint128 liquidity = _liquidityForAmounts( 145 | baseLower, 146 | baseUpper, 147 | token0.balanceOf(address(this)), 148 | token1.balanceOf(address(this)) 149 | ); 150 | _mintLiquidity(baseLower, baseUpper, liquidity, address(this), inMin[0], inMin[1]); 151 | liquidity = _liquidityForAmounts( 152 | limitLower, 153 | limitUpper, 154 | token0.balanceOf(address(this)), 155 | token1.balanceOf(address(this)) 156 | ); 157 | _mintLiquidity(limitLower, limitUpper, liquidity, address(this), inMin[2], inMin[3]); 158 | } 159 | } 160 | _mint(to, shares); 161 | emit Deposit(from, to, shares, deposit0, deposit1); 162 | /// Check total supply cap not exceeded. A value of 0 means no limit. 163 | require(maxTotalSupply == 0 || total <= maxTotalSupply, "max"); 164 | } 165 | 166 | function _zeroBurn(int24 tickLower, int24 tickUpper) internal returns(uint128 liquidity) { 167 | /// update fees for inclusion 168 | (liquidity, ,) = _position(tickLower, tickUpper); 169 | if(liquidity > 0) { 170 | pool.burn(tickLower, tickUpper, 0); 171 | (uint256 owed0, uint256 owed1) = pool.collect(address(this), tickLower, tickUpper, type(uint128).max, type(uint128).max); 172 | emit ZeroBurn(fee, owed0, owed1); 173 | if (owed0.div(fee) > 0 && token0.balanceOf(address(this)) > 0) token0.safeTransfer(feeRecipient, owed0.div(fee)); 174 | if (owed1.div(fee) > 0 && token1.balanceOf(address(this)) > 0) token1.safeTransfer(feeRecipient, owed1.div(fee)); 175 | } 176 | } 177 | 178 | /// @notice Update fees of the positions 179 | /// @return baseLiquidity Fee of base position 180 | /// @return limitLiquidity Fee of limit position 181 | function zeroBurn() internal returns(uint128 baseLiquidity, uint128 limitLiquidity) { 182 | baseLiquidity = _zeroBurn(baseLower, baseUpper); 183 | limitLiquidity = _zeroBurn(limitLower, limitUpper); 184 | } 185 | 186 | /// @notice Pull liquidity tokens from liquidity and receive the tokens 187 | /// @param shares Number of liquidity tokens to pull from liquidity 188 | /// @param tickLower lower tick 189 | /// @param tickUpper upper tick 190 | /// @param amountMin min outs 191 | /// @return amount0 amount of token0 received from base position 192 | /// @return amount1 amount of token1 received from base position 193 | function pullLiquidity( 194 | int24 tickLower, 195 | int24 tickUpper, 196 | uint128 shares, 197 | uint256[2] memory amountMin 198 | ) external onlyOwner returns (uint256 amount0, uint256 amount1) { 199 | _zeroBurn(tickLower, tickUpper); 200 | (amount0, amount1) = _burnLiquidity( 201 | tickLower, 202 | tickUpper, 203 | _liquidityForShares(tickLower, tickUpper, shares), 204 | address(this), 205 | false, 206 | amountMin[0], 207 | amountMin[1] 208 | ); 209 | } 210 | 211 | /// @param shares Number of liquidity tokens to redeem as pool assets 212 | /// @param to Address to which redeemed pool assets are sent 213 | /// @param from Address from which liquidity tokens are sent 214 | /// @param minAmounts min amount0,1 returned for shares of liq 215 | /// @return amount0 Amount of token0 redeemed by the submitted liquidity tokens 216 | /// @return amount1 Amount of token1 redeemed by the submitted liquidity tokens 217 | function withdraw( 218 | uint256 shares, 219 | address to, 220 | address from, 221 | uint256[4] memory minAmounts 222 | ) nonReentrant external returns (uint256 amount0, uint256 amount1) { 223 | require(shares > 0, "shares"); 224 | require(to != address(0), "to"); 225 | 226 | /// update fees 227 | zeroBurn(); 228 | 229 | /// Withdraw liquidity from Uniswap pool 230 | (uint256 base0, uint256 base1) = _burnLiquidity( 231 | baseLower, 232 | baseUpper, 233 | _liquidityForShares(baseLower, baseUpper, shares), 234 | to, 235 | false, 236 | minAmounts[0], 237 | minAmounts[1] 238 | ); 239 | (uint256 limit0, uint256 limit1) = _burnLiquidity( 240 | limitLower, 241 | limitUpper, 242 | _liquidityForShares(limitLower, limitUpper, shares), 243 | to, 244 | false, 245 | minAmounts[2], 246 | minAmounts[3] 247 | ); 248 | 249 | // Push tokens proportional to unused balances 250 | uint256 unusedAmount0 = token0.balanceOf(address(this)).mul(shares).div(totalSupply()); 251 | uint256 unusedAmount1 = token1.balanceOf(address(this)).mul(shares).div(totalSupply()); 252 | if (unusedAmount0 > 0) token0.safeTransfer(to, unusedAmount0); 253 | if (unusedAmount1 > 0) token1.safeTransfer(to, unusedAmount1); 254 | 255 | amount0 = base0.add(limit0).add(unusedAmount0); 256 | amount1 = base1.add(limit1).add(unusedAmount1); 257 | 258 | require( from == msg.sender, "own"); 259 | _burn(from, shares); 260 | 261 | emit Withdraw(from, to, shares, amount0, amount1); 262 | } 263 | 264 | /// @param _baseLower The lower tick of the base position 265 | /// @param _baseUpper The upper tick of the base position 266 | /// @param _limitLower The lower tick of the limit position 267 | /// @param _limitUpper The upper tick of the limit position 268 | /// @param inMin min spend 269 | /// @param outMin min amount0,1 returned for shares of liq 270 | /// @param _feeRecipient Address of recipient of 10% of earned fees since last rebalance 271 | function rebalance( 272 | int24 _baseLower, 273 | int24 _baseUpper, 274 | int24 _limitLower, 275 | int24 _limitUpper, 276 | address _feeRecipient, 277 | uint256[4] memory inMin, 278 | uint256[4] memory outMin 279 | ) nonReentrant external onlyOwner { 280 | require( 281 | _baseLower < _baseUpper && 282 | _baseLower % tickSpacing == 0 && 283 | _baseUpper % tickSpacing == 0 284 | ); 285 | require( 286 | _limitLower < _limitUpper && 287 | _limitLower % tickSpacing == 0 && 288 | _limitUpper % tickSpacing == 0 289 | ); 290 | require( 291 | _limitUpper != _baseUpper || 292 | _limitLower != _baseLower 293 | ); 294 | require(_feeRecipient != address(0)); 295 | feeRecipient = _feeRecipient; 296 | 297 | /// update fees 298 | zeroBurn(); 299 | 300 | /// Withdraw all liquidity and collect all fees from Uniswap pool 301 | (uint128 baseLiquidity, uint256 feesLimit0, uint256 feesLimit1) = _position(baseLower, baseUpper); 302 | (uint128 limitLiquidity, uint256 feesBase0, uint256 feesBase1) = _position(limitLower, limitUpper); 303 | 304 | _burnLiquidity(baseLower, baseUpper, baseLiquidity, address(this), true, outMin[0], outMin[1]); 305 | _burnLiquidity(limitLower, limitUpper, limitLiquidity, address(this), true, outMin[2], outMin[3]); 306 | 307 | emit Rebalance( 308 | currentTick(), 309 | token0.balanceOf(address(this)), 310 | token1.balanceOf(address(this)), 311 | feesBase0.add(feesLimit0), 312 | feesBase1.add(feesLimit1), 313 | totalSupply() 314 | ); 315 | 316 | baseLower = _baseLower; 317 | baseUpper = _baseUpper; 318 | baseLiquidity = _liquidityForAmounts( 319 | baseLower, 320 | baseUpper, 321 | token0.balanceOf(address(this)), 322 | token1.balanceOf(address(this)) 323 | ); 324 | _mintLiquidity(baseLower, baseUpper, baseLiquidity, address(this), inMin[0], inMin[1]); 325 | 326 | limitLower = _limitLower; 327 | limitUpper = _limitUpper; 328 | limitLiquidity = _liquidityForAmounts( 329 | limitLower, 330 | limitUpper, 331 | token0.balanceOf(address(this)), 332 | token1.balanceOf(address(this)) 333 | ); 334 | _mintLiquidity(limitLower, limitUpper, limitLiquidity, address(this), inMin[2], inMin[3]); 335 | } 336 | 337 | /// @notice Compound pending fees 338 | /// @param inMin min spend 339 | /// @return baseToken0Owed Pending fees of base token0 340 | /// @return baseToken1Owed Pending fees of base token1 341 | /// @return limitToken0Owed Pending fees of limit token0 342 | /// @return limitToken1Owed Pending fees of limit token1 343 | function compound(uint256[4] memory inMin) external onlyOwner returns ( 344 | uint128 baseToken0Owed, 345 | uint128 baseToken1Owed, 346 | uint128 limitToken0Owed, 347 | uint128 limitToken1Owed 348 | ) { 349 | // update fees for compounding 350 | zeroBurn(); 351 | 352 | uint128 liquidity = _liquidityForAmounts( 353 | baseLower, 354 | baseUpper, 355 | token0.balanceOf(address(this)), 356 | token1.balanceOf(address(this)) 357 | ); 358 | _mintLiquidity(baseLower, baseUpper, liquidity, address(this), inMin[0], inMin[1]); 359 | 360 | liquidity = _liquidityForAmounts( 361 | limitLower, 362 | limitUpper, 363 | token0.balanceOf(address(this)), 364 | token1.balanceOf(address(this)) 365 | ); 366 | _mintLiquidity(limitLower, limitUpper, liquidity, address(this), inMin[2], inMin[3]); 367 | } 368 | 369 | /// @notice Add Liquidity 370 | function addLiquidity( 371 | int24 tickLower, 372 | int24 tickUpper, 373 | uint256 amount0, 374 | uint256 amount1, 375 | uint256[2] memory inMin 376 | ) public onlyOwner { 377 | _zeroBurn(tickLower, tickUpper); 378 | uint128 liquidity = _liquidityForAmounts(tickLower, tickUpper, amount0, amount1); 379 | _mintLiquidity(tickLower, tickUpper, liquidity, address(this), inMin[0], inMin[1]); 380 | } 381 | 382 | /// @notice Adds the liquidity for the given position 383 | /// @param tickLower The lower tick of the position in which to add liquidity 384 | /// @param tickUpper The upper tick of the position in which to add liquidity 385 | /// @param liquidity The amount of liquidity to mint 386 | /// @param payer Payer Data 387 | /// @param amount0Min Minimum amount of token0 that should be paid 388 | /// @param amount1Min Minimum amount of token1 that should be paid 389 | function _mintLiquidity( 390 | int24 tickLower, 391 | int24 tickUpper, 392 | uint128 liquidity, 393 | address payer, 394 | uint256 amount0Min, 395 | uint256 amount1Min 396 | ) internal { 397 | if (liquidity > 0) { 398 | mintCalled = true; 399 | (uint256 amount0, uint256 amount1) = pool.mint( 400 | address(this), 401 | tickLower, 402 | tickUpper, 403 | liquidity, 404 | abi.encode(payer) 405 | ); 406 | require(amount0 >= amount0Min && amount1 >= amount1Min, 'PSC'); 407 | } 408 | } 409 | 410 | /// @notice Burn liquidity from the sender and collect tokens owed for the liquidity 411 | /// @param tickLower The lower tick of the position for which to burn liquidity 412 | /// @param tickUpper The upper tick of the position for which to burn liquidity 413 | /// @param liquidity The amount of liquidity to burn 414 | /// @param to The address which should receive the fees collected 415 | /// @param collectAll If true, collect all tokens owed in the pool, else collect the owed tokens of the burn 416 | /// @return amount0 The amount of fees collected in token0 417 | /// @return amount1 The amount of fees collected in token1 418 | function _burnLiquidity( 419 | int24 tickLower, 420 | int24 tickUpper, 421 | uint128 liquidity, 422 | address to, 423 | bool collectAll, 424 | uint256 amount0Min, 425 | uint256 amount1Min 426 | ) internal returns (uint256 amount0, uint256 amount1) { 427 | if (liquidity > 0) { 428 | /// Burn liquidity 429 | (uint256 owed0, uint256 owed1) = pool.burn(tickLower, tickUpper, liquidity); 430 | require(owed0 >= amount0Min && owed1 >= amount1Min, "PSC"); 431 | 432 | // Collect amount owed 433 | uint128 collect0 = collectAll ? type(uint128).max : _uint128Safe(owed0); 434 | uint128 collect1 = collectAll ? type(uint128).max : _uint128Safe(owed1); 435 | if (collect0 > 0 || collect1 > 0) { 436 | (amount0, amount1) = pool.collect(to, tickLower, tickUpper, collect0, collect1); 437 | } 438 | } 439 | } 440 | 441 | /// @notice Get the liquidity amount for given liquidity tokens 442 | /// @param tickLower The lower tick of the position 443 | /// @param tickUpper The upper tick of the position 444 | /// @param shares Shares of position 445 | /// @return The amount of liquidity toekn for shares 446 | function _liquidityForShares( 447 | int24 tickLower, 448 | int24 tickUpper, 449 | uint256 shares 450 | ) internal view returns (uint128) { 451 | (uint128 position, , ) = _position(tickLower, tickUpper); 452 | return _uint128Safe(uint256(position).mul(shares).div(totalSupply())); 453 | } 454 | 455 | /// @notice Get the info of the given position 456 | /// @param tickLower The lower tick of the position 457 | /// @param tickUpper The upper tick of the position 458 | /// @return liquidity The amount of liquidity of the position 459 | /// @return tokensOwed0 Amount of token0 owed 460 | /// @return tokensOwed1 Amount of token1 owed 461 | function _position(int24 tickLower, int24 tickUpper) 462 | internal 463 | view 464 | returns ( 465 | uint128 liquidity, 466 | uint128 tokensOwed0, 467 | uint128 tokensOwed1 468 | ) 469 | { 470 | bytes32 positionKey = keccak256(abi.encodePacked(address(this), tickLower, tickUpper)); 471 | (liquidity, , , tokensOwed0, tokensOwed1) = pool.positions(positionKey); 472 | } 473 | 474 | /// @notice Callback function of uniswapV3Pool mint 475 | function uniswapV3MintCallback( 476 | uint256 amount0, 477 | uint256 amount1, 478 | bytes calldata data 479 | ) external override { 480 | require(msg.sender == address(pool)); 481 | require(mintCalled == true); 482 | mintCalled = false; 483 | 484 | if (amount0 > 0) token0.safeTransfer(msg.sender, amount0); 485 | if (amount1 > 0) token1.safeTransfer(msg.sender, amount1); 486 | } 487 | 488 | /// @return total0 Quantity of token0 in both positions and unused in the Hypervisor 489 | /// @return total1 Quantity of token1 in both positions and unused in the Hypervisor 490 | function getTotalAmounts() public view returns (uint256 total0, uint256 total1) { 491 | (, uint256 base0, uint256 base1) = getBasePosition(); 492 | (, uint256 limit0, uint256 limit1) = getLimitPosition(); 493 | total0 = token0.balanceOf(address(this)).add(base0).add(limit0); 494 | total1 = token1.balanceOf(address(this)).add(base1).add(limit1); 495 | } 496 | 497 | /// @return liquidity Amount of total liquidity in the base position 498 | /// @return amount0 Estimated amount of token0 that could be collected by 499 | /// burning the base position 500 | /// @return amount1 Estimated amount of token1 that could be collected by 501 | /// burning the base position 502 | function getBasePosition() 503 | public 504 | view 505 | returns ( 506 | uint128 liquidity, 507 | uint256 amount0, 508 | uint256 amount1 509 | ) 510 | { 511 | (uint128 positionLiquidity, uint128 tokensOwed0, uint128 tokensOwed1) = _position( 512 | baseLower, 513 | baseUpper 514 | ); 515 | (amount0, amount1) = _amountsForLiquidity(baseLower, baseUpper, positionLiquidity); 516 | amount0 = amount0.add(uint256(tokensOwed0)); 517 | amount1 = amount1.add(uint256(tokensOwed1)); 518 | liquidity = positionLiquidity; 519 | } 520 | 521 | /// @return liquidity Amount of total liquidity in the limit position 522 | /// @return amount0 Estimated amount of token0 that could be collected by 523 | /// burning the limit position 524 | /// @return amount1 Estimated amount of token1 that could be collected by 525 | /// burning the limit position 526 | function getLimitPosition() 527 | public 528 | view 529 | returns ( 530 | uint128 liquidity, 531 | uint256 amount0, 532 | uint256 amount1 533 | ) 534 | { 535 | (uint128 positionLiquidity, uint128 tokensOwed0, uint128 tokensOwed1) = _position( 536 | limitLower, 537 | limitUpper 538 | ); 539 | (amount0, amount1) = _amountsForLiquidity(limitLower, limitUpper, positionLiquidity); 540 | amount0 = amount0.add(uint256(tokensOwed0)); 541 | amount1 = amount1.add(uint256(tokensOwed1)); 542 | liquidity = positionLiquidity; 543 | } 544 | 545 | /// @notice Get the amounts of the given numbers of liquidity tokens 546 | /// @param tickLower The lower tick of the position 547 | /// @param tickUpper The upper tick of the position 548 | /// @param liquidity The amount of liquidity tokens 549 | /// @return Amount of token0 and token1 550 | function _amountsForLiquidity( 551 | int24 tickLower, 552 | int24 tickUpper, 553 | uint128 liquidity 554 | ) internal view returns (uint256, uint256) { 555 | (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); 556 | return 557 | LiquidityAmounts.getAmountsForLiquidity( 558 | sqrtRatioX96, 559 | TickMath.getSqrtRatioAtTick(tickLower), 560 | TickMath.getSqrtRatioAtTick(tickUpper), 561 | liquidity 562 | ); 563 | } 564 | 565 | /// @notice Get the liquidity amount of the given numbers of token0 and token1 566 | /// @param tickLower The lower tick of the position 567 | /// @param tickUpper The upper tick of the position 568 | /// @param amount0 The amount of token0 569 | /// @param amount0 The amount of token1 570 | /// @return Amount of liquidity tokens 571 | function _liquidityForAmounts( 572 | int24 tickLower, 573 | int24 tickUpper, 574 | uint256 amount0, 575 | uint256 amount1 576 | ) internal view returns (uint128) { 577 | (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); 578 | return 579 | LiquidityAmounts.getLiquidityForAmounts( 580 | sqrtRatioX96, 581 | TickMath.getSqrtRatioAtTick(tickLower), 582 | TickMath.getSqrtRatioAtTick(tickUpper), 583 | amount0, 584 | amount1 585 | ); 586 | } 587 | 588 | /// @return tick Uniswap pool's current price tick 589 | function currentTick() public view returns (int24 tick) { 590 | (, tick, , , , , ) = pool.slot0(); 591 | } 592 | 593 | function _uint128Safe(uint256 x) internal pure returns (uint128) { 594 | assert(x <= type(uint128).max); 595 | return uint128(x); 596 | } 597 | 598 | /// @param _address Array of addresses to be appended 599 | function setWhitelist(address _address) external onlyOwner { 600 | whitelistedAddress = _address; 601 | } 602 | 603 | /// @notice Remove Whitelisted 604 | function removeWhitelisted() external onlyOwner { 605 | whitelistedAddress = address(0); 606 | } 607 | 608 | /// @notice set fee 609 | function setFee(uint8 newFee) external onlyOwner { 610 | fee = newFee; 611 | emit SetFee(fee); 612 | } 613 | 614 | /// @notice Toggle Direct Deposit 615 | function toggleDirectDeposit() external onlyOwner { 616 | directDeposit = !directDeposit; 617 | } 618 | 619 | function transferOwnership(address newOwner) external onlyOwner { 620 | require(newOwner != address(0)); 621 | owner = newOwner; 622 | } 623 | 624 | modifier onlyOwner { 625 | require(msg.sender == owner, "only owner"); 626 | _; 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /contracts/HypervisorFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import {IUniswapV3Factory} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol'; 5 | 6 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 7 | 8 | import {Hypervisor} from './Hypervisor.sol'; 9 | 10 | /// @title HypervisorFactory 11 | 12 | contract HypervisorFactory is Ownable { 13 | IUniswapV3Factory public uniswapV3Factory; 14 | mapping(address => mapping(address => mapping(uint24 => address))) public getHypervisor; // toke0, token1, fee -> hypervisor address 15 | address[] public allHypervisors; 16 | 17 | event HypervisorCreated(address token0, address token1, uint24 fee, address hypervisor, uint256); 18 | 19 | constructor(address _uniswapV3Factory) { 20 | require(_uniswapV3Factory != address(0), "uniswapV3Factory should be non-zero"); 21 | uniswapV3Factory = IUniswapV3Factory(_uniswapV3Factory); 22 | } 23 | 24 | /// @notice Get the number of hypervisors created 25 | /// @return Number of hypervisors created 26 | function allHypervisorsLength() external view returns (uint256) { 27 | return allHypervisors.length; 28 | } 29 | 30 | /// @notice Create a Hypervisor 31 | /// @param tokenA Address of token0 32 | /// @param tokenB Address of toekn1 33 | /// @param fee The desired fee for the hypervisor 34 | /// @param name Name of the hyervisor 35 | /// @param symbol Symbole of the hypervisor 36 | /// @return hypervisor Address of hypervisor created 37 | function createHypervisor( 38 | address tokenA, 39 | address tokenB, 40 | uint24 fee, 41 | string memory name, 42 | string memory symbol 43 | ) external onlyOwner returns (address hypervisor) { 44 | require(tokenA != tokenB, 'SF: IDENTICAL_ADDRESSES'); // TODO: using PoolAddress library (uniswap-v3-periphery) 45 | (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 46 | require(token0 != address(0), 'SF: ZERO_ADDRESS'); 47 | require(getHypervisor[token0][token1][fee] == address(0), 'SF: HYPERVISOR_EXISTS'); 48 | int24 tickSpacing = uniswapV3Factory.feeAmountTickSpacing(fee); 49 | require(tickSpacing != 0, 'SF: INCORRECT_FEE'); 50 | address pool = uniswapV3Factory.getPool(token0, token1, fee); 51 | if (pool == address(0)) { 52 | pool = uniswapV3Factory.createPool(token0, token1, fee); 53 | } 54 | hypervisor = address( 55 | new Hypervisor{salt: keccak256(abi.encodePacked(token0, token1, fee, tickSpacing))}(pool, owner(), name, symbol) 56 | ); 57 | 58 | getHypervisor[token0][token1][fee] = hypervisor; 59 | getHypervisor[token1][token0][fee] = hypervisor; // populate mapping in the reverse direction 60 | allHypervisors.push(hypervisor); 61 | emit HypervisorCreated(token0, token1, fee, hypervisor, allHypervisors.length); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /contracts/RebalanceProxy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.7.6; 2 | 3 | import "@openzeppelin/contracts/math/SafeMath.sol"; 4 | import "@openzeppelin/contracts/math/SignedSafeMath.sol"; 5 | import "./interfaces/IHypervisor.sol"; 6 | import "./proxy/admin.sol"; 7 | import "hardhat/console.sol"; 8 | 9 | contract RebalanceProxy { 10 | using SignedSafeMath for int256; 11 | using SafeMath for uint256; 12 | 13 | address public owner; 14 | 15 | uint256 public maxTranslation = 300; 16 | uint256 public maxWidth = 300; 17 | uint256 public minInterval = 0; 18 | 19 | mapping(address => bool) public exempted; 20 | mapping(address => uint256) public customDiff; 21 | mapping(address => uint256) public customWidth; 22 | mapping(address => uint256) public customInterval; 23 | mapping(address => uint256) public lastRebalance; 24 | 25 | mapping(address => address) public rebalancers; 26 | mapping(address => address) public admins; 27 | 28 | modifier onlyOwner() { 29 | require(msg.sender == owner, "only owner"); 30 | _; 31 | } 32 | 33 | modifier onlyRebalancer(address hypervisor) { 34 | require(msg.sender == rebalancers[hypervisor], "only rebalancer"); 35 | _; 36 | } 37 | 38 | constructor(address _owner) { 39 | owner = _owner; 40 | } 41 | 42 | function isWithinRange( 43 | uint256 maxTranslation, 44 | int24 tickSpacing, 45 | int24 lastLowerTick, 46 | int24 lastUpperTick, 47 | int24 newLowerTick, 48 | int24 newUpperTick 49 | ) public view returns (bool) { 50 | 51 | int256 lastMidPoint; 52 | int256 newMidPoint; 53 | 54 | // Calculate the mid point for the last range 55 | if(lastLowerTick < 0 && lastUpperTick > 0) { 56 | lastMidPoint = int256(lastLowerTick).add((int256(abs(int256(lastLowerTick))).add(int256(lastUpperTick))).div(2)); 57 | } 58 | else { 59 | lastMidPoint = int256(lastLowerTick).add((int256(lastUpperTick).sub(int256(lastLowerTick))).div(2)); 60 | } 61 | // Calculate the mid point for the new range 62 | if(newLowerTick < 0 && newUpperTick > 0) { 63 | newMidPoint = int256(newLowerTick).add((int256(abs(int256(newLowerTick))).add(int256(newUpperTick))).div(2)); 64 | } 65 | else { 66 | newMidPoint = int256(newLowerTick).add((int256(newUpperTick).sub(int256(newLowerTick))).div(2)); 67 | } 68 | 69 | // Calculate the difference between the new and last mid points 70 | int256 diff = newMidPoint > lastMidPoint ? newMidPoint.sub(lastMidPoint) : lastMidPoint.sub(newMidPoint); 71 | 72 | // Check if the difference is within the allowed translation range 73 | return diff <= int256(maxTranslation); 74 | } 75 | 76 | 77 | function isWidthChangeWithinRange( 78 | uint256 maxWidth, 79 | int24 lastLowerTick, 80 | int24 lastUpperTick, 81 | int24 newLowerTick, 82 | int24 newUpperTick 83 | ) public view returns (bool) { 84 | int256 oldWidth = int256(lastUpperTick).sub(int256(lastLowerTick)); 85 | int256 newWidth = int256(newUpperTick).sub(int256(newLowerTick)); 86 | int256 allowedWidthDiff = int256(maxWidth); 87 | int256 lowerWidthBound = oldWidth.sub(allowedWidthDiff); 88 | int256 upperWidthBound = oldWidth.add(allowedWidthDiff); 89 | 90 | return (newWidth >= lowerWidthBound && newWidth <= upperWidthBound); 91 | } 92 | 93 | 94 | function rebalance( 95 | address hypervisor, 96 | int24 _baseLower, 97 | int24 _baseUpper, 98 | int24 _limitLower, 99 | int24 _limitUpper, 100 | address _feeRecipient, 101 | uint256[4] memory inMin, 102 | uint256[4] memory outMin 103 | ) external onlyRebalancer(hypervisor) { 104 | 105 | // Check if the rebalance request is for a full-range position 106 | bool isFullRange = _baseLower <= -886800 && _baseUpper >= 886800; 107 | 108 | // Proceed with the rebalance operation only if the minimum interval has passed or it's a full-range adjustment 109 | uint256 _minInterval = customInterval[hypervisor] == 0 ? minInterval : customInterval[hypervisor]; 110 | require( 111 | lastRebalance[hypervisor] == 0 || block.timestamp >= lastRebalance[hypervisor] + _minInterval || isFullRange, 112 | "too soon" 113 | ); 114 | 115 | // If not exempted and not a full-range position, perform the standard range and width checks 116 | if(!exempted[hypervisor] && !isFullRange) { 117 | uint256 _maxTranslation = customDiff[hypervisor] == 0 ? maxTranslation : customDiff[hypervisor]; 118 | require( 119 | isWithinRange( 120 | _maxTranslation, 121 | IHypervisor(hypervisor).tickSpacing(), 122 | IHypervisor(hypervisor).baseLower(), 123 | IHypervisor(hypervisor).baseUpper(), 124 | _baseLower, 125 | _baseUpper 126 | ), "Exceeds range delta"); 127 | 128 | uint256 _maxWidth = customWidth[hypervisor] == 0 ? maxWidth : customWidth[hypervisor]; 129 | require( 130 | isWidthChangeWithinRange( 131 | _maxWidth, 132 | IHypervisor(hypervisor).baseLower(), 133 | IHypervisor(hypervisor).baseUpper(), 134 | _baseLower, 135 | _baseUpper 136 | ), "Exceeds width delta"); 137 | } 138 | 139 | // Execute the rebalance operation 140 | Admin(admins[hypervisor]).rebalance(hypervisor, _baseLower, _baseUpper, _limitLower, _limitUpper, _feeRecipient, inMin, outMin); 141 | lastRebalance[hypervisor] = block.timestamp; 142 | } 143 | 144 | function updateMaxTranslation(uint256 newMaxTranslation) external onlyOwner { 145 | require(maxTranslation != 0, "should be non-zero"); 146 | maxTranslation = newMaxTranslation; 147 | } 148 | 149 | function updateMaxWidth(uint256 newMaxWidth) external onlyOwner { 150 | require(maxWidth != 0, "should be non-zero"); 151 | maxWidth = newMaxWidth; 152 | } 153 | 154 | function exemptHypervisor(address hypervisor) external onlyOwner { 155 | require(hypervisor != address(0), "hypervisor should be non-zero"); 156 | exempted[hypervisor] = true; 157 | } 158 | 159 | function removeExemption(address hypervisor) external onlyOwner { 160 | require(hypervisor != address(0), "hypervisor should be non-zero"); 161 | exempted[hypervisor] = false; 162 | } 163 | 164 | function setRebalancer(address hypervisor, address newRebalancer) external onlyOwner { 165 | require(newRebalancer != address(0), "newRebalancer should be non-zero"); 166 | rebalancers[hypervisor] = newRebalancer; 167 | } 168 | 169 | function setAdmin(address hypervisor, address newAdmin) external onlyOwner { 170 | require(newAdmin != address(0), "newAdmin should be non-zero"); 171 | admins[hypervisor] = newAdmin; 172 | } 173 | 174 | function setCustomDiff(address hypervisor, uint256 diff) external onlyOwner { 175 | customDiff[hypervisor] = diff; 176 | } 177 | 178 | function setCustomDiffWidth(address hypervisor, uint256 diffWidth) external onlyOwner { 179 | customWidth[hypervisor] = diffWidth; 180 | } 181 | 182 | function setMinInterval(uint256 interval) external onlyOwner { 183 | minInterval = interval; 184 | } 185 | 186 | function setCustomInterval(address hypervisor, uint256 interval) external onlyOwner { 187 | customInterval[hypervisor] = interval; 188 | } 189 | 190 | function transferOwner(address newOwner) external onlyOwner { 191 | require(newOwner != address(0), "newOwner should be non-zero"); 192 | owner = newOwner; 193 | } 194 | 195 | function abs(int x) private pure returns (uint) { 196 | return x >= 0 ? uint(x) : uint(-x); 197 | } 198 | } -------------------------------------------------------------------------------- /contracts/UniProxy.sol: -------------------------------------------------------------------------------- 1 | /// SPDX-License-Identifier: BUSL-1.1 2 | 3 | pragma solidity 0.7.6; 4 | pragma abicoder v2; 5 | 6 | import "./interfaces/IHypervisor.sol"; 7 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 8 | 9 | interface IClearing { 10 | 11 | function clearDeposit( 12 | uint256 deposit0, 13 | uint256 deposit1, 14 | address from, 15 | address to, 16 | address pos, 17 | uint256[4] memory minIn 18 | ) external view returns (bool cleared); 19 | 20 | function clearShares( 21 | address pos, 22 | uint256 shares 23 | ) external view returns (bool cleared); 24 | 25 | function getDepositAmount( 26 | address pos, 27 | address token, 28 | uint256 _deposit 29 | ) external view returns (uint256 amountStart, uint256 amountEnd); 30 | } 31 | 32 | /// @title UniProxy v1.2.3 33 | /// @notice Proxy contract for hypervisor positions management 34 | contract UniProxy is ReentrancyGuard { 35 | 36 | IClearing public clearance; 37 | address public owner; 38 | 39 | constructor(address _clearance) { 40 | owner = msg.sender; 41 | clearance = IClearing(_clearance); 42 | } 43 | 44 | /// @notice Deposit into the given position 45 | /// @param deposit0 Amount of token0 to deposit 46 | /// @param deposit1 Amount of token1 to deposit 47 | /// @param to Address to receive liquidity tokens 48 | /// @param pos Hypervisor Address 49 | /// @param minIn min assets to expect in position during a direct deposit 50 | /// @return shares Amount of liquidity tokens received 51 | function deposit( 52 | uint256 deposit0, 53 | uint256 deposit1, 54 | address to, 55 | address pos, 56 | uint256[4] memory minIn 57 | ) nonReentrant external returns (uint256 shares) { 58 | require(to != address(0), "to should be non-zero"); 59 | require(clearance.clearDeposit(deposit0, deposit1, msg.sender, to, pos, minIn), "deposit not cleared"); 60 | 61 | /// transfer assets from msg.sender and mint lp tokens to provided address 62 | shares = IHypervisor(pos).deposit(deposit0, deposit1, to, msg.sender, minIn); 63 | require(clearance.clearShares(pos, shares), "shares not cleared"); 64 | } 65 | 66 | /// @notice Get the amount of token to deposit for the given amount of pair token 67 | /// @param pos Hypervisor Address 68 | /// @param token Address of token to deposit 69 | /// @param _deposit Amount of token to deposit 70 | /// @return amountStart Minimum amounts of the pair token to deposit 71 | /// @return amountEnd Maximum amounts of the pair token to deposit 72 | function getDepositAmount( 73 | address pos, 74 | address token, 75 | uint256 _deposit 76 | ) public view returns (uint256 amountStart, uint256 amountEnd) { 77 | return clearance.getDepositAmount(pos, token, _deposit); 78 | } 79 | 80 | function transferClearance(address newClearance) external onlyOwner { 81 | require(newClearance != address(0), "newClearance should be non-zero"); 82 | clearance = IClearing(newClearance); 83 | } 84 | 85 | function transferOwnership(address newOwner) external onlyOwner { 86 | require(newOwner != address(0), "newOwner should be non-zero"); 87 | owner = newOwner; 88 | } 89 | 90 | modifier onlyOwner { 91 | require(msg.sender == owner, "only owner"); 92 | _; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /contracts/adapters/tokemak/BaseController.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity >=0.6.11 <=0.6.12; 3 | 4 | interface IAddressRegistry { } 5 | 6 | contract BaseController { 7 | 8 | address public immutable manager; 9 | IAddressRegistry public immutable addressRegistry; 10 | 11 | constructor(address _manager, address _addressRegistry) public { 12 | require(_manager != address(0), "INVALID_ADDRESS"); 13 | require(_addressRegistry != address(0), "INVALID_ADDRESS"); 14 | 15 | manager = _manager; 16 | addressRegistry = IAddressRegistry(_addressRegistry); 17 | } 18 | 19 | modifier onlyManager() { 20 | require(msg.sender == manager, "NOT_MANAGER_ADDRESS"); 21 | _; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/adapters/tokemak/GammaController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.11; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/math/SafeMath.sol"; 8 | import "@openzeppelin/contracts/utils/Address.sol"; 9 | import "./interfaces/ITokeHypervisor.sol"; 10 | import "./interfaces/IHypervisorFactory.sol"; 11 | import "./BaseController.sol"; 12 | 13 | contract GammaController is BaseController { 14 | using SafeERC20 for IERC20; 15 | using Address for address; 16 | using SafeMath for uint256; 17 | uint256 public constant N_COINS = 2; 18 | IHypervisorFactory hypeFactory; 19 | constructor( 20 | address manager, 21 | address addressRegistry, 22 | address _hypeFactory 23 | ) public BaseController(manager, addressRegistry) { 24 | hypeFactory = IHypervisorFactory(_hypeFactory); 25 | } 26 | 27 | /// @notice Deploy liquidity to a Gamma Hypervisor ( controller owns assets, manager receives LP tokens ) 28 | /// @dev Calls to external contract 29 | /// @dev We trust sender to send a true gamma lpTokenAddress 30 | /// @param amount0 quantity of token0 of Hypervisor 31 | /// @param amount1 quantity of token1 of Hypervisor 32 | /// @param token0 address of pool token0 33 | /// @param token1 address of pool token1 34 | /// @param fee fee of pool 35 | /// @param minMintAmount min amount of LP tokens to accept 36 | function deploy( 37 | uint256 amount0, 38 | uint256 amount1, 39 | address token0, 40 | address token1, 41 | uint24 fee, 42 | uint256 minMintAmount, 43 | uint256[4] memory inMin 44 | ) external onlyManager { 45 | 46 | address lpTokenAddress = hypeFactory.getHypervisor(token0, token1, fee); 47 | uint256 balance0 = IERC20(token0).balanceOf(manager); 48 | uint256 balance1 = IERC20(token0).balanceOf(manager); 49 | 50 | require(balance0 >= amount0 && balance1 >= amount1, "INSUFFICIENT_BALANCE"); 51 | 52 | // approve Hypervisor to spend amount0,1 amounts of Hypervisor.token0,1 53 | _approve(IERC20(token0), lpTokenAddress, amount0); 54 | _approve(IERC20(token1), lpTokenAddress, amount1); 55 | 56 | uint256 lpTokenBalanceBefore = IERC20(lpTokenAddress).balanceOf(manager); 57 | // deposit amount0, amount1 and mint LP tokens to the manager 58 | uint256 lpTokenReceived = ITokeHypervisor(lpTokenAddress).deposit(amount0, amount1, manager, manager, inMin); 59 | 60 | uint256 lpTokenBalanceAfter = IERC20(lpTokenAddress).balanceOf(manager); 61 | require(lpTokenBalanceBefore + lpTokenReceived == lpTokenBalanceAfter, "LP_TOKEN_MISMATCH"); 62 | require(lpTokenReceived >= minMintAmount, "INSUFFICIENT_MINT"); 63 | } 64 | 65 | /// @notice Withdraw liquidity from TokeHypervisor ( TokeHypervisor's msg.sender owns LP tokens, manager receives assets ) 66 | /// @dev Calls to external contract 67 | /// @param token0 address of pool token0 68 | /// @param token1 address of pool token1 69 | /// @param fee fee of pool 70 | /// @param amount Quantity of LP tokens to burn in the withdrawal 71 | /// @param minAmounts min amount of token0, token1 to receive after LP burn 72 | function withdraw( 73 | address token0, 74 | address token1, 75 | uint24 fee, 76 | uint256 amount, 77 | uint256[4] memory minAmounts 78 | ) external onlyManager { 79 | 80 | address lpTokenAddress = hypeFactory.getHypervisor(token0, token1, fee); 81 | uint256 lpTokenBalanceBefore = IERC20(lpTokenAddress).balanceOf(manager); 82 | uint256[N_COINS] memory coinsBalancesBefore = _getCoinsBalances(lpTokenAddress); 83 | 84 | ITokeHypervisor(lpTokenAddress).withdraw(amount, manager, manager, minAmounts); 85 | 86 | uint256 lpTokenBalanceAfter = IERC20(lpTokenAddress).balanceOf(manager); 87 | uint256[N_COINS] memory coinsBalancesAfter = _getCoinsBalances(lpTokenAddress); 88 | 89 | _compareCoinsBalances(coinsBalancesBefore, coinsBalancesAfter, [minAmounts[0].add(minAmounts[2]), minAmounts[1].add(minAmounts[3])]); 90 | 91 | require(lpTokenBalanceBefore - amount == lpTokenBalanceAfter, "LP_TOKEN_MISMATCH"); 92 | } 93 | 94 | function _getCoinsBalances(address lpTokenAddress) internal view returns (uint256[N_COINS] memory coinsBalances) { 95 | coinsBalances[0] = ITokeHypervisor(lpTokenAddress).token0().balanceOf(manager); 96 | coinsBalances[1] = ITokeHypervisor(lpTokenAddress).token1().balanceOf(manager); 97 | return coinsBalances; 98 | } 99 | 100 | function _compareCoinsBalances(uint256[N_COINS] memory balancesBefore, uint256[N_COINS] memory balancesAfter, uint256[N_COINS] memory amounts) internal pure { 101 | for (uint256 i = 0; i < N_COINS; i++) { 102 | if (amounts[i] > 0) { 103 | require(balancesBefore[i] < balancesAfter[i], "BALANCE_MUST_INCREASE"); 104 | require(amounts[i] <= balancesAfter[i] - balancesBefore[i], "BALANCE_LT_MIN"); 105 | } 106 | } 107 | } 108 | 109 | function _approve( 110 | IERC20 token, 111 | address spender, 112 | uint256 amount 113 | ) internal { 114 | uint256 currentAllowance = token.allowance(address(this), spender); 115 | if (currentAllowance > 0) { 116 | token.safeDecreaseAllowance(spender, currentAllowance); 117 | } 118 | token.safeIncreaseAllowance(spender, amount); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /contracts/adapters/tokemak/TokeHypervisor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.7.6; 3 | 4 | import "@openzeppelin/contracts/math/Math.sol"; 5 | import "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/math/SignedSafeMath.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 9 | import "@openzeppelin/contracts/drafts/ERC20Permit.sol"; 10 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 11 | 12 | import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol"; 13 | import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 14 | import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; 15 | import "@uniswap/v3-core/contracts/libraries/FullMath.sol"; 16 | import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; 17 | 18 | /// @title TokeHypervisor 19 | /// @notice A Uniswap V2-like interface with fungible liquidity to Uniswap V3 20 | /// which allows for arbitrary liquidity provision: one-sided, lop-sided, and balanced 21 | contract TokeHypervisor is IUniswapV3MintCallback, ERC20Permit, ReentrancyGuard { 22 | using SafeERC20 for IERC20; 23 | using SafeMath for uint256; 24 | using SignedSafeMath for int256; 25 | 26 | IUniswapV3Pool public pool; 27 | IERC20 public token0; 28 | IERC20 public token1; 29 | uint24 public fee; 30 | int24 public tickSpacing; 31 | 32 | int24 public baseLower; 33 | int24 public baseUpper; 34 | int24 public limitLower; 35 | int24 public limitUpper; 36 | 37 | address public owner; 38 | uint256 public deposit0Max; 39 | uint256 public deposit1Max; 40 | uint256 public maxTotalSupply; 41 | address public whitelistedAddress; 42 | bool public directDeposit; /// enter uni on deposit (avoid if client uses public rpc) 43 | 44 | uint256 public constant PRECISION = 1e36; 45 | 46 | bool mintCalled; 47 | 48 | event Deposit( 49 | address indexed sender, 50 | address indexed to, 51 | uint256 shares, 52 | uint256 amount0, 53 | uint256 amount1 54 | ); 55 | 56 | event Withdraw( 57 | address indexed sender, 58 | address indexed to, 59 | uint256 shares, 60 | uint256 amount0, 61 | uint256 amount1 62 | ); 63 | 64 | event Rebalance( 65 | int24 tick, 66 | uint256 totalAmount0, 67 | uint256 totalAmount1, 68 | uint256 feeAmount0, 69 | uint256 feeAmount1, 70 | uint256 totalSupply 71 | ); 72 | 73 | /// @param _pool Uniswap V3 pool for which liquidity is managed 74 | /// @param _owner Owner of the Hypervisor 75 | constructor( 76 | address _pool, 77 | address _owner, 78 | string memory name, 79 | string memory symbol 80 | ) ERC20Permit(name) ERC20(name, symbol) { 81 | require(_pool != address(0)); 82 | require(_owner != address(0)); 83 | pool = IUniswapV3Pool(_pool); 84 | token0 = IERC20(pool.token0()); 85 | token1 = IERC20(pool.token1()); 86 | require(address(token0) != address(0)); 87 | require(address(token1) != address(0)); 88 | fee = pool.fee(); 89 | tickSpacing = pool.tickSpacing(); 90 | 91 | owner = _owner; 92 | 93 | maxTotalSupply = 0; /// no cap 94 | deposit0Max = uint256(-1); 95 | deposit1Max = uint256(-1); 96 | } 97 | 98 | /// @notice Deposit tokens 99 | /// @param deposit0 Amount of token0 transfered from sender to Hypervisor 100 | /// @param deposit1 Amount of token1 transfered from sender to Hypervisor 101 | /// @param to Address to which liquidity tokens are minted 102 | /// @param from Address from which asset tokens are transferred 103 | /// @return shares Quantity of liquidity tokens minted as a result of deposit 104 | function deposit( 105 | uint256 deposit0, 106 | uint256 deposit1, 107 | address to, 108 | address from, 109 | uint256[4] memory inMin 110 | ) nonReentrant external returns (uint256 shares) { 111 | require(deposit0 > 0 || deposit1 > 0); 112 | require(deposit0 <= deposit0Max && deposit1 <= deposit1Max); 113 | require(to != address(0) && to != address(this), "to"); 114 | require(msg.sender == whitelistedAddress, "WHE"); 115 | 116 | /// update fees 117 | zeroBurn(); 118 | 119 | uint160 sqrtPrice = TickMath.getSqrtRatioAtTick(currentTick()); 120 | uint256 price = FullMath.mulDiv(uint256(sqrtPrice).mul(uint256(sqrtPrice)), PRECISION, 2**(96 * 2)); 121 | 122 | (uint256 pool0, uint256 pool1) = getTotalAmounts(); 123 | 124 | shares = deposit1.add(deposit0.mul(price).div(PRECISION)); 125 | 126 | if (deposit0 > 0) { 127 | token0.safeTransferFrom(from, address(this), deposit0); 128 | } 129 | if (deposit1 > 0) { 130 | token1.safeTransferFrom(from, address(this), deposit1); 131 | } 132 | 133 | uint256 total = totalSupply(); 134 | if (total != 0) { 135 | uint256 pool0PricedInToken1 = pool0.mul(price).div(PRECISION); 136 | shares = shares.mul(total).div(pool0PricedInToken1.add(pool1)); 137 | if (directDeposit) { 138 | addLiquidity( 139 | baseLower, 140 | baseUpper, 141 | address(this), 142 | token0.balanceOf(address(this)), 143 | token1.balanceOf(address(this)), 144 | [inMin[0], inMin[1]] 145 | ); 146 | addLiquidity( 147 | limitLower, 148 | limitUpper, 149 | address(this), 150 | token0.balanceOf(address(this)), 151 | token1.balanceOf(address(this)), 152 | [inMin[2],inMin[3]] 153 | ); 154 | } 155 | } 156 | _mint(to, shares); 157 | emit Deposit(from, to, shares, deposit0, deposit1); 158 | /// Check total supply cap not exceeded. A value of 0 means no limit. 159 | require(maxTotalSupply == 0 || total <= maxTotalSupply, "max"); 160 | } 161 | 162 | /// @notice Update fees of the positions 163 | /// @return baseLiquidity Fee of base position 164 | /// @return limitLiquidity Fee of limit position 165 | function zeroBurn() internal returns(uint128 baseLiquidity, uint128 limitLiquidity) { 166 | /// update fees for inclusion 167 | (baseLiquidity, , ) = _position(baseLower, baseUpper); 168 | if (baseLiquidity > 0) { 169 | pool.burn(baseLower, baseUpper, 0); 170 | } 171 | (limitLiquidity, , ) = _position(limitLower, limitUpper); 172 | if (limitLiquidity > 0) { 173 | pool.burn(limitLower, limitUpper, 0); 174 | } 175 | } 176 | 177 | /// @notice Pull liquidity tokens from liquidity and receive the tokens 178 | /// @param shares Number of liquidity tokens to pull from liquidity 179 | /// @return base0 amount of token0 received from base position 180 | /// @return base1 amount of token1 received from base position 181 | /// @return limit0 amount of token0 received from limit position 182 | /// @return limit1 amount of token1 received from limit position 183 | function pullLiquidity( 184 | uint256 shares, 185 | uint256[4] memory minAmounts 186 | ) external onlyOwner returns( 187 | uint256 base0, 188 | uint256 base1, 189 | uint256 limit0, 190 | uint256 limit1 191 | ) { 192 | zeroBurn(); 193 | (base0, base1) = _burnLiquidity( 194 | baseLower, 195 | baseUpper, 196 | _liquidityForShares(baseLower, baseUpper, shares), 197 | address(this), 198 | false, 199 | minAmounts[0], 200 | minAmounts[1] 201 | ); 202 | (limit0, limit1) = _burnLiquidity( 203 | limitLower, 204 | limitUpper, 205 | _liquidityForShares(limitLower, limitUpper, shares), 206 | address(this), 207 | false, 208 | minAmounts[2], 209 | minAmounts[3] 210 | ); 211 | } 212 | 213 | function _baseLiquidityForShares(uint256 shares) internal view returns (uint128) { 214 | return _liquidityForShares(baseLower, baseUpper, shares); 215 | } 216 | 217 | function _limitLiquidityForShares(uint256 shares) internal view returns (uint128) { 218 | return _liquidityForShares(limitLower, limitUpper, shares); 219 | } 220 | 221 | /// @param shares Number of liquidity tokens to redeem as pool assets 222 | /// @param to Address to which redeemed pool assets are sent 223 | /// @param from Address from which liquidity tokens are sent 224 | /// @param minAmounts min amount0,1 returned for shares of liq 225 | /// @return amount0 Amount of token0 redeemed by the submitted liquidity tokens 226 | /// @return amount1 Amount of token1 redeemed by the submitted liquidity tokens 227 | function withdraw( 228 | uint256 shares, 229 | address to, 230 | address from, 231 | uint256[4] memory minAmounts 232 | ) nonReentrant external returns (uint256 amount0, uint256 amount1) { 233 | require(shares > 0, "shares"); 234 | require(to != address(0), "to"); 235 | require(msg.sender == whitelistedAddress, "WHE"); 236 | 237 | /// update fees 238 | zeroBurn(); 239 | 240 | /// Withdraw liquidity from Uniswap pool 241 | (uint256 base0, uint256 base1) = _burnLiquidity( 242 | baseLower, 243 | baseUpper, 244 | _baseLiquidityForShares(shares), 245 | to, 246 | false, 247 | minAmounts[0], 248 | minAmounts[1] 249 | ); 250 | (uint256 limit0, uint256 limit1) = _burnLiquidity( 251 | limitLower, 252 | limitUpper, 253 | _limitLiquidityForShares(shares), 254 | to, 255 | false, 256 | minAmounts[2], 257 | minAmounts[3] 258 | ); 259 | 260 | // Push tokens proportional to unused balances 261 | uint256 unusedAmount0 = token0.balanceOf(address(this)).mul(shares).div(totalSupply()); 262 | uint256 unusedAmount1 = token1.balanceOf(address(this)).mul(shares).div(totalSupply()); 263 | if (unusedAmount0 > 0) token0.safeTransfer(to, unusedAmount0); 264 | if (unusedAmount1 > 0) token1.safeTransfer(to, unusedAmount1); 265 | 266 | amount0 = base0.add(limit0).add(unusedAmount0); 267 | amount1 = base1.add(limit1).add(unusedAmount1); 268 | 269 | _burn(from, shares); 270 | 271 | emit Withdraw(from, to, shares, amount0, amount1); 272 | } 273 | 274 | /// @param _baseLower The lower tick of the base position 275 | /// @param _baseUpper The upper tick of the base position 276 | /// @param _limitLower The lower tick of the limit position 277 | /// @param _limitUpper The upper tick of the limit position 278 | /// @param inMin min spend 279 | /// @param outMin min amount0,1 returned for shares of liq 280 | /// @param feeRecipient Address of recipient of 10% of earned fees since last rebalance 281 | function rebalance( 282 | int24 _baseLower, 283 | int24 _baseUpper, 284 | int24 _limitLower, 285 | int24 _limitUpper, 286 | address feeRecipient, 287 | uint256[4] memory inMin, 288 | uint256[4] memory outMin 289 | ) nonReentrant external onlyOwner { 290 | require( 291 | _baseLower < _baseUpper && 292 | _baseLower % tickSpacing == 0 && 293 | _baseUpper % tickSpacing == 0 294 | ); 295 | require( 296 | _limitLower < _limitUpper && 297 | _limitLower % tickSpacing == 0 && 298 | _limitUpper % tickSpacing == 0 299 | ); 300 | require( 301 | _limitUpper != _baseUpper || 302 | _limitLower != _baseLower 303 | ); 304 | require(feeRecipient != address(0)); 305 | 306 | /// update fees 307 | (uint128 baseLiquidity, uint128 limitLiquidity) = zeroBurn(); 308 | 309 | /// Withdraw all liquidity and collect all fees from Uniswap pool 310 | (, uint256 feesLimit0, uint256 feesLimit1) = _position(baseLower, baseUpper); 311 | (, uint256 feesBase0, uint256 feesBase1) = _position(limitLower, limitUpper); 312 | 313 | uint256 fees0 = feesBase0.add(feesLimit0); 314 | uint256 fees1 = feesBase1.add(feesLimit1); 315 | (baseLiquidity, , ) = _position(baseLower, baseUpper); 316 | (limitLiquidity, , ) = _position(limitLower, limitUpper); 317 | 318 | _burnLiquidity(baseLower, baseUpper, baseLiquidity, address(this), true, outMin[0], outMin[1]); 319 | _burnLiquidity(limitLower, limitUpper, limitLiquidity, address(this), true, outMin[2], outMin[3]); 320 | 321 | /// transfer 10% of fees for VISR buybacks 322 | if (fees0 > 0) token0.safeTransfer(feeRecipient, fees0.div(10)); 323 | if (fees1 > 0) token1.safeTransfer(feeRecipient, fees1.div(10)); 324 | 325 | emit Rebalance( 326 | currentTick(), 327 | token0.balanceOf(address(this)), 328 | token1.balanceOf(address(this)), 329 | fees0, 330 | fees1, 331 | totalSupply() 332 | ); 333 | 334 | uint256[2] memory addMins = [inMin[0],inMin[1]]; 335 | baseLower = _baseLower; 336 | baseUpper = _baseUpper; 337 | addLiquidity( 338 | baseLower, 339 | baseUpper, 340 | address(this), 341 | token0.balanceOf(address(this)), 342 | token1.balanceOf(address(this)), 343 | addMins 344 | ); 345 | 346 | addMins = [inMin[2],inMin[3]]; 347 | limitLower = _limitLower; 348 | limitUpper = _limitUpper; 349 | addLiquidity( 350 | limitLower, 351 | limitUpper, 352 | address(this), 353 | token0.balanceOf(address(this)), 354 | token1.balanceOf(address(this)), 355 | addMins 356 | ); 357 | } 358 | 359 | /// @notice Compound pending fees 360 | /// @param inMin min spend 361 | /// @return baseToken0Owed Pending fees of base token0 362 | /// @return baseToken1Owed Pending fees of base token1 363 | /// @return limitToken0Owed Pending fees of limit token0 364 | /// @return limitToken1Owed Pending fees of limit token1 365 | function compound(uint256[4] memory inMin) external onlyOwner returns ( 366 | uint128 baseToken0Owed, 367 | uint128 baseToken1Owed, 368 | uint128 limitToken0Owed, 369 | uint128 limitToken1Owed 370 | ) { 371 | // update fees for compounding 372 | zeroBurn(); 373 | (, baseToken0Owed,baseToken1Owed) = _position(baseLower, baseUpper); 374 | (, limitToken0Owed,limitToken1Owed) = _position(limitLower, limitUpper); 375 | 376 | // collect fees 377 | pool.collect(address(this), baseLower, baseLower, baseToken0Owed, baseToken1Owed); 378 | pool.collect(address(this), limitLower, limitUpper, limitToken0Owed, limitToken1Owed); 379 | 380 | addLiquidity( 381 | baseLower, 382 | baseUpper, 383 | address(this), 384 | token0.balanceOf(address(this)), 385 | token1.balanceOf(address(this)), 386 | [inMin[0],inMin[1]] 387 | ); 388 | addLiquidity( 389 | limitLower, 390 | limitUpper, 391 | address(this), 392 | token0.balanceOf(address(this)), 393 | token1.balanceOf(address(this)), 394 | [inMin[2],inMin[3]] 395 | ); 396 | } 397 | 398 | /// @notice Add tokens to base liquidity 399 | /// @param amount0 Amount of token0 to add 400 | /// @param amount1 Amount of token1 to add 401 | function addBaseLiquidity(uint256 amount0, uint256 amount1, uint256[2] memory inMin) external onlyOwner { 402 | addLiquidity( 403 | baseLower, 404 | baseUpper, 405 | address(this), 406 | amount0 == 0 && amount1 == 0 ? token0.balanceOf(address(this)) : amount0, 407 | amount0 == 0 && amount1 == 0 ? token1.balanceOf(address(this)) : amount1, 408 | inMin 409 | ); 410 | } 411 | 412 | /// @notice Add tokens to limit liquidity 413 | /// @param amount0 Amount of token0 to add 414 | /// @param amount1 Amount of token1 to add 415 | function addLimitLiquidity(uint256 amount0, uint256 amount1, uint256[2] memory inMin) external onlyOwner { 416 | addLiquidity( 417 | limitLower, 418 | limitUpper, 419 | address(this), 420 | amount0 == 0 && amount1 == 0 ? token0.balanceOf(address(this)) : amount0, 421 | amount0 == 0 && amount1 == 0 ? token1.balanceOf(address(this)) : amount1, 422 | inMin 423 | ); 424 | } 425 | 426 | /// @notice Add Liquidity 427 | function addLiquidity( 428 | int24 tickLower, 429 | int24 tickUpper, 430 | address payer, 431 | uint256 amount0, 432 | uint256 amount1, 433 | uint256[2] memory inMin 434 | ) internal { 435 | uint128 liquidity = _liquidityForAmounts(tickLower, tickUpper, amount0, amount1); 436 | _mintLiquidity(tickLower, tickUpper, liquidity, payer, inMin[0], inMin[1]); 437 | } 438 | 439 | /// @notice Adds the liquidity for the given position 440 | /// @param tickLower The lower tick of the position in which to add liquidity 441 | /// @param tickUpper The upper tick of the position in which to add liquidity 442 | /// @param liquidity The amount of liquidity to mint 443 | /// @param payer Payer Data 444 | /// @param amount0Min Minimum amount of token0 that should be paid 445 | /// @param amount1Min Minimum amount of token1 that should be paid 446 | function _mintLiquidity( 447 | int24 tickLower, 448 | int24 tickUpper, 449 | uint128 liquidity, 450 | address payer, 451 | uint256 amount0Min, 452 | uint256 amount1Min 453 | ) internal { 454 | if (liquidity > 0) { 455 | mintCalled = true; 456 | (uint256 amount0, uint256 amount1) = pool.mint( 457 | address(this), 458 | tickLower, 459 | tickUpper, 460 | liquidity, 461 | abi.encode(payer) 462 | ); 463 | require(amount0 >= amount0Min && amount1 >= amount1Min, 'PSC'); 464 | } 465 | } 466 | 467 | /// @notice Burn liquidity from the sender and collect tokens owed for the liquidity 468 | /// @param tickLower The lower tick of the position for which to burn liquidity 469 | /// @param tickUpper The upper tick of the position for which to burn liquidity 470 | /// @param liquidity The amount of liquidity to burn 471 | /// @param to The address which should receive the fees collected 472 | /// @param collectAll If true, collect all tokens owed in the pool, else collect the owed tokens of the burn 473 | /// @return amount0 The amount of fees collected in token0 474 | /// @return amount1 The amount of fees collected in token1 475 | function _burnLiquidity( 476 | int24 tickLower, 477 | int24 tickUpper, 478 | uint128 liquidity, 479 | address to, 480 | bool collectAll, 481 | uint256 amount0Min, 482 | uint256 amount1Min 483 | ) internal returns (uint256 amount0, uint256 amount1) { 484 | if (liquidity > 0) { 485 | /// Burn liquidity 486 | (uint256 owed0, uint256 owed1) = pool.burn(tickLower, tickUpper, liquidity); 487 | require(owed0 >= amount0Min && owed1 >= amount1Min, "PSC"); 488 | 489 | // Collect amount owed 490 | uint128 collect0 = collectAll ? type(uint128).max : _uint128Safe(owed0); 491 | uint128 collect1 = collectAll ? type(uint128).max : _uint128Safe(owed1); 492 | if (collect0 > 0 || collect1 > 0) { 493 | (amount0, amount1) = pool.collect(to, tickLower, tickUpper, collect0, collect1); 494 | } 495 | } 496 | } 497 | 498 | /// @notice Get the liquidity amount for given liquidity tokens 499 | /// @param tickLower The lower tick of the position 500 | /// @param tickUpper The upper tick of the position 501 | /// @param shares Shares of position 502 | /// @return The amount of liquidity toekn for shares 503 | function _liquidityForShares( 504 | int24 tickLower, 505 | int24 tickUpper, 506 | uint256 shares 507 | ) internal view returns (uint128) { 508 | (uint128 position, , ) = _position(tickLower, tickUpper); 509 | return _uint128Safe(uint256(position).mul(shares).div(totalSupply())); 510 | } 511 | 512 | /// @notice Get the info of the given position 513 | /// @param tickLower The lower tick of the position 514 | /// @param tickUpper The upper tick of the position 515 | /// @return liquidity The amount of liquidity of the position 516 | /// @return tokensOwed0 Amount of token0 owed 517 | /// @return tokensOwed1 Amount of token1 owed 518 | function _position(int24 tickLower, int24 tickUpper) 519 | internal 520 | view 521 | returns ( 522 | uint128 liquidity, 523 | uint128 tokensOwed0, 524 | uint128 tokensOwed1 525 | ) 526 | { 527 | bytes32 positionKey = keccak256(abi.encodePacked(address(this), tickLower, tickUpper)); 528 | (liquidity, , , tokensOwed0, tokensOwed1) = pool.positions(positionKey); 529 | } 530 | 531 | /// @notice Callback function of uniswapV3Pool mint 532 | function uniswapV3MintCallback( 533 | uint256 amount0, 534 | uint256 amount1, 535 | bytes calldata data 536 | ) external override { 537 | require(msg.sender == address(pool)); 538 | require(mintCalled == true); 539 | mintCalled = false; 540 | 541 | if (amount0 > 0) token0.safeTransfer(msg.sender, amount0); 542 | if (amount1 > 0) token1.safeTransfer(msg.sender, amount1); 543 | } 544 | 545 | /// @return total0 Quantity of token0 in both positions and unused in the Hypervisor 546 | /// @return total1 Quantity of token1 in both positions and unused in the Hypervisor 547 | function getTotalAmounts() public view returns (uint256 total0, uint256 total1) { 548 | (, uint256 base0, uint256 base1) = getBasePosition(); 549 | (, uint256 limit0, uint256 limit1) = getLimitPosition(); 550 | total0 = token0.balanceOf(address(this)).add(base0).add(limit0); 551 | total1 = token1.balanceOf(address(this)).add(base1).add(limit1); 552 | } 553 | 554 | /// @return liquidity Amount of total liquidity in the base position 555 | /// @return amount0 Estimated amount of token0 that could be collected by 556 | /// burning the base position 557 | /// @return amount1 Estimated amount of token1 that could be collected by 558 | /// burning the base position 559 | function getBasePosition() 560 | public 561 | view 562 | returns ( 563 | uint128 liquidity, 564 | uint256 amount0, 565 | uint256 amount1 566 | ) 567 | { 568 | (uint128 positionLiquidity, uint128 tokensOwed0, uint128 tokensOwed1) = _position( 569 | baseLower, 570 | baseUpper 571 | ); 572 | (amount0, amount1) = _amountsForLiquidity(baseLower, baseUpper, positionLiquidity); 573 | amount0 = amount0.add(uint256(tokensOwed0)); 574 | amount1 = amount1.add(uint256(tokensOwed1)); 575 | liquidity = positionLiquidity; 576 | } 577 | 578 | /// @return liquidity Amount of total liquidity in the limit position 579 | /// @return amount0 Estimated amount of token0 that could be collected by 580 | /// burning the limit position 581 | /// @return amount1 Estimated amount of token1 that could be collected by 582 | /// burning the limit position 583 | function getLimitPosition() 584 | public 585 | view 586 | returns ( 587 | uint128 liquidity, 588 | uint256 amount0, 589 | uint256 amount1 590 | ) 591 | { 592 | (uint128 positionLiquidity, uint128 tokensOwed0, uint128 tokensOwed1) = _position( 593 | limitLower, 594 | limitUpper 595 | ); 596 | (amount0, amount1) = _amountsForLiquidity(limitLower, limitUpper, positionLiquidity); 597 | amount0 = amount0.add(uint256(tokensOwed0)); 598 | amount1 = amount1.add(uint256(tokensOwed1)); 599 | liquidity = positionLiquidity; 600 | } 601 | 602 | /// @notice Get the amounts of the given numbers of liquidity tokens 603 | /// @param tickLower The lower tick of the position 604 | /// @param tickUpper The upper tick of the position 605 | /// @param liquidity The amount of liquidity tokens 606 | /// @return Amount of token0 and token1 607 | function _amountsForLiquidity( 608 | int24 tickLower, 609 | int24 tickUpper, 610 | uint128 liquidity 611 | ) internal view returns (uint256, uint256) { 612 | (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); 613 | return 614 | LiquidityAmounts.getAmountsForLiquidity( 615 | sqrtRatioX96, 616 | TickMath.getSqrtRatioAtTick(tickLower), 617 | TickMath.getSqrtRatioAtTick(tickUpper), 618 | liquidity 619 | ); 620 | } 621 | 622 | /// @notice Get the liquidity amount of the given numbers of token0 and token1 623 | /// @param tickLower The lower tick of the position 624 | /// @param tickUpper The upper tick of the position 625 | /// @param amount0 The amount of token0 626 | /// @param amount0 The amount of token1 627 | /// @return Amount of liquidity tokens 628 | function _liquidityForAmounts( 629 | int24 tickLower, 630 | int24 tickUpper, 631 | uint256 amount0, 632 | uint256 amount1 633 | ) internal view returns (uint128) { 634 | (uint160 sqrtRatioX96, , , , , , ) = pool.slot0(); 635 | return 636 | LiquidityAmounts.getLiquidityForAmounts( 637 | sqrtRatioX96, 638 | TickMath.getSqrtRatioAtTick(tickLower), 639 | TickMath.getSqrtRatioAtTick(tickUpper), 640 | amount0, 641 | amount1 642 | ); 643 | } 644 | 645 | /// @return tick Uniswap pool's current price tick 646 | function currentTick() public view returns (int24 tick) { 647 | (, tick, , , , , ) = pool.slot0(); 648 | } 649 | 650 | function _uint128Safe(uint256 x) internal pure returns (uint128) { 651 | assert(x <= type(uint128).max); 652 | return uint128(x); 653 | } 654 | 655 | /// @param _address Array of addresses to be appended 656 | function setWhitelist(address _address) external onlyOwner { 657 | whitelistedAddress = _address; 658 | } 659 | 660 | /// @notice Remove Whitelisted 661 | function removeWhitelisted() external onlyOwner { 662 | whitelistedAddress = address(0); 663 | } 664 | 665 | /// @notice Toggle Direct Deposit 666 | function toggleDirectDeposit() external onlyOwner { 667 | directDeposit = !directDeposit; 668 | } 669 | 670 | function transferOwnership(address newOwner) external onlyOwner { 671 | require(newOwner != address(0)); 672 | owner = newOwner; 673 | } 674 | 675 | modifier onlyOwner { 676 | require(msg.sender == owner, "only owner"); 677 | _; 678 | } 679 | } 680 | -------------------------------------------------------------------------------- /contracts/adapters/tokemak/TokeHypervisorFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import {IUniswapV3Factory} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol'; 5 | 6 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 7 | 8 | import {TokeHypervisor} from './TokeHypervisor.sol'; 9 | 10 | /// @title TokeHypervisorFactory 11 | 12 | contract TokeHypervisorFactory is Ownable { 13 | IUniswapV3Factory public uniswapV3Factory; 14 | mapping(address => mapping(address => mapping(uint24 => address))) public getHypervisor; // toke0, token1, fee -> hypervisor address 15 | address[] public allHypervisors; 16 | 17 | event HypervisorCreated(address token0, address token1, uint24 fee, address hypervisor, uint256); 18 | 19 | constructor(address _uniswapV3Factory) { 20 | require(_uniswapV3Factory != address(0), "uniswapV3Factory should be non-zero"); 21 | uniswapV3Factory = IUniswapV3Factory(_uniswapV3Factory); 22 | } 23 | 24 | /// @notice Get the number of hypervisors created 25 | /// @return Number of hypervisors created 26 | function allHypervisorsLength() external view returns (uint256) { 27 | return allHypervisors.length; 28 | } 29 | 30 | /// @notice Create a Hypervisor 31 | /// @param tokenA Address of token0 32 | /// @param tokenB Address of toekn1 33 | /// @param fee The desired fee for the hypervisor 34 | /// @param name Name of the hyervisor 35 | /// @param symbol Symbole of the hypervisor 36 | /// @return hypervisor Address of hypervisor created 37 | function createHypervisor( 38 | address tokenA, 39 | address tokenB, 40 | uint24 fee, 41 | string memory name, 42 | string memory symbol 43 | ) external onlyOwner returns (address hypervisor) { 44 | require(tokenA != tokenB, 'SF: IDENTICAL_ADDRESSES'); // TODO: using PoolAddress library (uniswap-v3-periphery) 45 | (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 46 | require(token0 != address(0), 'SF: ZERO_ADDRESS'); 47 | require(getHypervisor[token0][token1][fee] == address(0), 'SF: HYPERVISOR_EXISTS'); 48 | int24 tickSpacing = uniswapV3Factory.feeAmountTickSpacing(fee); 49 | require(tickSpacing != 0, 'SF: INCORRECT_FEE'); 50 | address pool = uniswapV3Factory.getPool(token0, token1, fee); 51 | if (pool == address(0)) { 52 | pool = uniswapV3Factory.createPool(token0, token1, fee); 53 | } 54 | hypervisor = address( 55 | new TokeHypervisor{salt: keccak256(abi.encodePacked(token0, token1, fee, tickSpacing))}(pool, owner(), name, symbol) 56 | ); 57 | 58 | getHypervisor[token0][token1][fee] = hypervisor; 59 | getHypervisor[token1][token0][fee] = hypervisor; // populate mapping in the reverse direction 60 | allHypervisors.push(hypervisor); 61 | emit HypervisorCreated(token0, token1, fee, hypervisor, allHypervisors.length); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /contracts/adapters/tokemak/interfaces/IHypervisorFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.6.11; 3 | 4 | interface IHypervisorFactory { 5 | function getHypervisor( 6 | address token0, 7 | address token1, 8 | uint24 fee 9 | ) external returns (address hypervisor); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/adapters/tokemak/interfaces/ITokeHypervisor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity =0.6.11; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 7 | 8 | interface ITokeHypervisor { 9 | function deposit( 10 | uint256, 11 | uint256, 12 | address, 13 | address, 14 | uint256[4] memory minIn 15 | ) external returns (uint256); 16 | 17 | function withdraw( 18 | uint256, 19 | address, 20 | address, 21 | uint256[4] memory 22 | ) external returns (uint256, uint256); 23 | 24 | function rebalance( 25 | int24 _baseLower, 26 | int24 _baseUpper, 27 | int24 _limitLower, 28 | int24 _limitUpper, 29 | address _feeRecipient, 30 | uint256[4] memory minIn, 31 | uint256[4] memory outMin 32 | ) external; 33 | 34 | function addBaseLiquidity( 35 | uint256 amount0, 36 | uint256 amount1, 37 | uint256[2] memory minIn 38 | ) external; 39 | 40 | function addLimitLiquidity( 41 | uint256 amount0, 42 | uint256 amount1, 43 | uint256[2] memory minIn 44 | ) external; 45 | 46 | function pullLiquidity( 47 | uint256 shares, 48 | uint256[4] memory minAmounts 49 | ) external returns ( 50 | uint256 base0, 51 | uint256 base1, 52 | uint256 limit0, 53 | uint256 limit1 54 | ); 55 | 56 | function compound() external returns ( 57 | uint128 baseToken0Owed, 58 | uint128 baseToken1Owed, 59 | uint128 limitToken0Owed, 60 | uint128 limitToken1Owed 61 | ); 62 | 63 | function pool() external view returns (IUniswapV3Pool); 64 | 65 | function currentTick() external view returns (int24 tick); 66 | 67 | function token0() external view returns (IERC20); 68 | 69 | function token1() external view returns (IERC20); 70 | 71 | function deposit0Max() external view returns (uint256); 72 | 73 | function deposit1Max() external view returns (uint256); 74 | 75 | function balanceOf(address) external view returns (uint256); 76 | 77 | function approve(address, uint256) external returns (bool); 78 | 79 | function transferFrom(address, address, uint256) external returns (bool); 80 | 81 | function transfer(address, uint256) external returns (bool); 82 | 83 | function getTotalAmounts() external view returns (uint256 total0, uint256 total1); 84 | 85 | function totalSupply() external view returns (uint256 ); 86 | 87 | function setWhitelist(address _address) external; 88 | 89 | function removeWhitelisted() external; 90 | 91 | function transferOwnership(address newOwner) external; 92 | 93 | } 94 | -------------------------------------------------------------------------------- /contracts/interfaces/IHypervisor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 7 | 8 | interface IHypervisor { 9 | 10 | function deposit( 11 | uint256, 12 | uint256, 13 | address, 14 | address, 15 | uint256[4] memory minIn 16 | ) external returns (uint256); 17 | 18 | function withdraw( 19 | uint256, 20 | address, 21 | address, 22 | uint256[4] memory 23 | ) external returns (uint256, uint256); 24 | 25 | function compound() external returns ( 26 | 27 | uint128 baseToken0Owed, 28 | uint128 baseToken1Owed, 29 | uint128 limitToken0Owed, 30 | uint128 limitToken1Owed 31 | ); 32 | 33 | function compound(uint256[4] memory inMin) external returns ( 34 | 35 | uint128 baseToken0Owed, 36 | uint128 baseToken1Owed, 37 | uint128 limitToken0Owed, 38 | uint128 limitToken1Owed 39 | ); 40 | 41 | 42 | function rebalance( 43 | int24 _baseLower, 44 | int24 _baseUpper, 45 | int24 _limitLower, 46 | int24 _limitUpper, 47 | address _feeRecipient, 48 | uint256[4] memory minIn, 49 | uint256[4] memory outMin 50 | ) external; 51 | 52 | function addBaseLiquidity( 53 | uint256 amount0, 54 | uint256 amount1, 55 | uint256[2] memory minIn 56 | ) external; 57 | 58 | function addLimitLiquidity( 59 | uint256 amount0, 60 | uint256 amount1, 61 | uint256[2] memory minIn 62 | ) external; 63 | 64 | function pullLiquidity( 65 | int24 tickLower, 66 | int24 tickUpper, 67 | uint128 shares, 68 | uint256[2] memory amountMin 69 | ) external returns ( 70 | uint256 base0, 71 | uint256 base1 72 | ); 73 | 74 | function pullLiquidity( 75 | uint256 shares, 76 | uint256[4] memory minAmounts 77 | ) external returns( 78 | uint256 base0, 79 | uint256 base1, 80 | uint256 limit0, 81 | uint256 limit1 82 | ); 83 | 84 | function addLiquidity( 85 | int24 tickLower, 86 | int24 tickUpper, 87 | uint256 amount0, 88 | uint256 amount1, 89 | uint256[2] memory inMin 90 | ) external; 91 | 92 | 93 | function pool() external view returns (IUniswapV3Pool); 94 | 95 | function currentTick() external view returns (int24 tick); 96 | 97 | function tickSpacing() external view returns (int24 spacing); 98 | 99 | function baseLower() external view returns (int24 tick); 100 | 101 | function baseUpper() external view returns (int24 tick); 102 | 103 | function limitLower() external view returns (int24 tick); 104 | 105 | function limitUpper() external view returns (int24 tick); 106 | 107 | function token0() external view returns (IERC20); 108 | 109 | function token1() external view returns (IERC20); 110 | 111 | function deposit0Max() external view returns (uint256); 112 | 113 | function deposit1Max() external view returns (uint256); 114 | 115 | function balanceOf(address) external view returns (uint256); 116 | 117 | function approve(address, uint256) external returns (bool); 118 | 119 | function transferFrom(address, address, uint256) external returns (bool); 120 | 121 | function transfer(address, uint256) external returns (bool); 122 | 123 | function getTotalAmounts() external view returns (uint256 total0, uint256 total1); 124 | 125 | function getBasePosition() external view returns (uint256 liquidity, uint256 total0, uint256 total1); 126 | 127 | function totalSupply() external view returns (uint256 ); 128 | 129 | function setWhitelist(address _address) external; 130 | 131 | function setFee(uint8 newFee) external; 132 | 133 | function removeWhitelisted() external; 134 | 135 | function transferOwnership(address newOwner) external; 136 | 137 | } 138 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | 3 | pragma solidity =0.7.6; 4 | pragma abicoder v2; 5 | 6 | interface IUniProxy { 7 | 8 | function deposit( 9 | uint256 deposit0, 10 | uint256 deposit1, 11 | address to, 12 | address from, 13 | address pos 14 | ) external returns (uint256 shares); 15 | 16 | function getDepositAmount( 17 | address pos, 18 | address token, 19 | uint256 _deposit 20 | ) external view returns ( 21 | uint256 amountStart, 22 | uint256 amountEnd 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniversalVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | interface IUniversalVault { 6 | /* user events */ 7 | 8 | event Locked(address delegate, address token, uint256 amount); 9 | event Unlocked(address delegate, address token, uint256 amount); 10 | event RageQuit(address delegate, address token, bool notified, string reason); 11 | 12 | /* data types */ 13 | 14 | struct LockData { 15 | address delegate; 16 | address token; 17 | uint256 balance; 18 | } 19 | 20 | /* initialize function */ 21 | 22 | function initialize() external; 23 | 24 | /* user functions */ 25 | 26 | function lock( 27 | address token, 28 | uint256 amount, 29 | bytes calldata permission 30 | ) external; 31 | 32 | function unlock( 33 | address token, 34 | uint256 amount, 35 | bytes calldata permission 36 | ) external; 37 | 38 | function rageQuit(address delegate, address token) 39 | external 40 | returns (bool notified, string memory error); 41 | 42 | function transferERC20( 43 | address token, 44 | address to, 45 | uint256 amount 46 | ) external; 47 | 48 | function transferETH(address to, uint256 amount) external payable; 49 | 50 | /* pure functions */ 51 | 52 | function calculateLockID(address delegate, address token) 53 | external 54 | pure 55 | returns (bytes32 lockID); 56 | 57 | /* getter functions */ 58 | 59 | function getPermissionHash( 60 | bytes32 eip712TypeHash, 61 | address delegate, 62 | address token, 63 | uint256 amount, 64 | uint256 nonce 65 | ) external view returns (bytes32 permissionHash); 66 | 67 | function getNonce() external view returns (uint256 nonce); 68 | 69 | function owner() external view returns (address ownerAddress); 70 | 71 | function getLockSetCount() external view returns (uint256 count); 72 | 73 | function getLockAt(uint256 index) external view returns (LockData memory lockData); 74 | 75 | function getBalanceDelegated(address token, address delegate) 76 | external 77 | view 78 | returns (uint256 balance); 79 | 80 | function getBalanceLocked(address token) external view returns (uint256 balance); 81 | 82 | function checkBalances() external view returns (bool validity); 83 | } 84 | -------------------------------------------------------------------------------- /contracts/interfaces/IVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity 0.7.6; 4 | 5 | interface IVault { 6 | function deposit( 7 | uint256, 8 | uint256, 9 | address, 10 | address 11 | ) external returns (uint256); 12 | 13 | function withdraw( 14 | uint256, 15 | address, 16 | address, 17 | uint256, 18 | uint256 19 | ) external returns (uint256, uint256); 20 | 21 | function rebalance( 22 | int24 _baseLower, 23 | int24 _baseUpper, 24 | int24 _limitLower, 25 | int24 _limitUpper, 26 | address feeRecipient, 27 | uint256 _amount0Min, 28 | uint256 _amount1Min 29 | ) external; 30 | 31 | function getTotalAmounts() external view returns (uint256, uint256); 32 | 33 | event Deposit( 34 | address indexed sender, 35 | address indexed to, 36 | uint256 shares, 37 | uint256 amount0, 38 | uint256 amount1 39 | ); 40 | 41 | event Withdraw( 42 | address indexed sender, 43 | address indexed to, 44 | uint256 shares, 45 | uint256 amount0, 46 | uint256 amount1 47 | ); 48 | 49 | event Rebalance( 50 | int24 tick, 51 | uint256 totalAmount0, 52 | uint256 totalAmount1, 53 | uint256 feeAmount0, 54 | uint256 feeAmount1, 55 | uint256 totalSupply 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /contracts/mocks/MockUniswapV3Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import {IUniswapV3Pool} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 5 | import {IUniswapV3Factory} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol'; 6 | import {IUniswapV3MintCallback} from '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol'; 7 | import {IUniswapV3SwapCallback} from '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; 8 | import {IERC20Minimal} from '@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol'; 9 | import {IUniswapV3PoolDeployer} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3PoolDeployer.sol'; 10 | 11 | import {TickMath} from '@uniswap/v3-core/contracts/libraries/TickMath.sol'; 12 | import {LowGasSafeMath} from '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol'; 13 | import {TransferHelper} from '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; 14 | import {LiquidityAmounts} from '@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol'; 15 | 16 | contract MockUniswapV3Pool is IUniswapV3MintCallback, IUniswapV3SwapCallback, IERC20Minimal { 17 | using LowGasSafeMath for uint256; 18 | 19 | address public immutable token0; 20 | address public immutable token1; 21 | 22 | uint24 public fee; 23 | int24 public tickSpacing; 24 | 25 | IUniswapV3Pool public currentPool; 26 | IUniswapV3Factory public immutable uniswapFactory; 27 | 28 | int24 private constant MIN_TICK = -887220; 29 | int24 private constant MAX_TICK = 887220; 30 | 31 | mapping(address => uint256) private _balances; 32 | mapping(address => mapping(address => uint256)) public override allowance; 33 | 34 | constructor() { 35 | (address _uniswapFactory, address _token0, address _token1, uint24 _fee, int24 _tickSpacing) = 36 | IUniswapV3PoolDeployer(msg.sender).parameters(); 37 | token0 = _token0; 38 | token1 = _token1; 39 | uniswapFactory = IUniswapV3Factory(_uniswapFactory); 40 | 41 | fee = _fee; 42 | tickSpacing = _tickSpacing; 43 | 44 | address uniswapPool = IUniswapV3Factory(_uniswapFactory).getPool(_token0, _token1, _fee); 45 | require(uniswapPool != address(0)); 46 | currentPool = IUniswapV3Pool(uniswapPool); 47 | } 48 | 49 | function balanceOf(address account) external view override returns (uint256) { 50 | return _balances[account]; 51 | } 52 | 53 | function deposit( 54 | int24 lowerTick, 55 | int24 upperTick, 56 | uint256 amount0, 57 | uint256 amount1 58 | ) external returns (uint256 rest0, uint256 rest1) { 59 | (uint160 sqrtRatioX96, , , , , , ) = currentPool.slot0(); 60 | 61 | // First, deposit as much as we can 62 | uint128 baseLiquidity = 63 | LiquidityAmounts.getLiquidityForAmounts( 64 | sqrtRatioX96, 65 | TickMath.getSqrtRatioAtTick(lowerTick), 66 | TickMath.getSqrtRatioAtTick(upperTick), 67 | amount0, 68 | amount1 69 | ); 70 | (uint256 amountDeposited0, uint256 amountDeposited1) = 71 | currentPool.mint(msg.sender, lowerTick, upperTick, baseLiquidity, abi.encode(msg.sender)); 72 | rest0 = amount0 - amountDeposited0; 73 | rest1 = amount1 - amountDeposited1; 74 | } 75 | 76 | function swap(bool zeroForOne, int256 amountSpecified) external { 77 | (uint160 sqrtRatio, , , , , , ) = currentPool.slot0(); 78 | currentPool.swap( 79 | address(this), 80 | zeroForOne, 81 | amountSpecified, 82 | zeroForOne ? sqrtRatio - 1 : sqrtRatio + 1, 83 | abi.encode(msg.sender) 84 | ); 85 | } 86 | 87 | function uniswapV3MintCallback( 88 | uint256 amount0Owed, 89 | uint256 amount1Owed, 90 | bytes calldata data 91 | ) external override { 92 | require(msg.sender == address(currentPool)); 93 | 94 | address sender = abi.decode(data, (address)); 95 | 96 | if (sender == address(this)) { 97 | if (amount0Owed > 0) { 98 | TransferHelper.safeTransfer(token0, msg.sender, amount0Owed); 99 | } 100 | if (amount1Owed > 0) { 101 | TransferHelper.safeTransfer(token1, msg.sender, amount1Owed); 102 | } 103 | } else { 104 | if (amount0Owed > 0) { 105 | TransferHelper.safeTransferFrom(token0, sender, msg.sender, amount0Owed); 106 | } 107 | if (amount1Owed > 0) { 108 | TransferHelper.safeTransferFrom(token1, sender, msg.sender, amount1Owed); 109 | } 110 | } 111 | } 112 | 113 | function uniswapV3SwapCallback( 114 | int256 amount0Delta, 115 | int256 amount1Delta, 116 | bytes calldata data 117 | ) external override { 118 | require(msg.sender == address(currentPool)); 119 | 120 | address sender = abi.decode(data, (address)); 121 | 122 | if (amount0Delta > 0) { 123 | TransferHelper.safeTransferFrom(token0, sender, msg.sender, uint256(amount0Delta)); 124 | } else if (amount1Delta > 0) { 125 | TransferHelper.safeTransferFrom(token1, sender, msg.sender, uint256(amount1Delta)); 126 | } 127 | } 128 | 129 | function transfer(address recipient, uint256 amount) external override returns (bool) { 130 | uint256 balanceBefore = _balances[msg.sender]; 131 | require(balanceBefore >= amount, 'insufficient balance'); 132 | _balances[msg.sender] = balanceBefore - amount; 133 | 134 | uint256 balanceRecipient = _balances[recipient]; 135 | require(balanceRecipient + amount >= balanceRecipient, 'recipient balance overflow'); 136 | _balances[recipient] = balanceRecipient + amount; 137 | 138 | emit Transfer(msg.sender, recipient, amount); 139 | return true; 140 | } 141 | 142 | function approve(address spender, uint256 amount) external override returns (bool) { 143 | allowance[msg.sender][spender] = amount; 144 | emit Approval(msg.sender, spender, amount); 145 | return true; 146 | } 147 | 148 | function transferFrom( 149 | address sender, 150 | address recipient, 151 | uint256 amount 152 | ) external override returns (bool) { 153 | uint256 allowanceBefore = allowance[sender][msg.sender]; 154 | require(allowanceBefore >= amount, 'allowance insufficient'); 155 | 156 | allowance[sender][msg.sender] = allowanceBefore - amount; 157 | 158 | uint256 balanceRecipient = _balances[recipient]; 159 | require(balanceRecipient + amount >= balanceRecipient, 'overflow balance recipient'); 160 | _balances[recipient] = balanceRecipient + amount; 161 | uint256 balanceSender = _balances[sender]; 162 | require(balanceSender >= amount, 'underflow balance sender'); 163 | _balances[sender] = balanceSender - amount; 164 | 165 | emit Transfer(sender, recipient, amount); 166 | return true; 167 | } 168 | 169 | function _mint(address to, uint256 amount) internal { 170 | uint256 balanceNext = _balances[to] + amount; 171 | require(balanceNext >= amount, 'overflow balance'); 172 | _balances[to] = balanceNext; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /contracts/mocks/MockUniswapV3PoolDeployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import {IUniswapV3PoolDeployer} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3PoolDeployer.sol'; 5 | import {MockUniswapV3Pool} from './MockUniswapV3Pool.sol'; 6 | 7 | contract MockUniswapV3PoolDeployer is IUniswapV3PoolDeployer { 8 | struct Parameters { 9 | address factory; 10 | address token0; 11 | address token1; 12 | uint24 fee; 13 | int24 tickSpacing; 14 | } 15 | 16 | Parameters public override parameters; 17 | 18 | event PoolDeployed(address pool); 19 | 20 | function deploy( 21 | address factory, 22 | address token0, 23 | address token1, 24 | uint24 fee, 25 | int24 tickSpacing 26 | ) external returns (address pool) { 27 | parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing}); 28 | pool = address(new MockUniswapV3Pool{salt: keccak256(abi.encodePacked(token0, token1, fee, tickSpacing))}()); 29 | emit PoolDeployed(pool); 30 | delete parameters; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/mocks/TestERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import {IERC20Minimal} from '@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol'; 5 | 6 | contract TestERC20 is IERC20Minimal { 7 | mapping(address => uint256) public override balanceOf; 8 | mapping(address => mapping(address => uint256)) public override allowance; 9 | 10 | constructor(uint256 amountToMint) { 11 | mint(msg.sender, amountToMint); 12 | } 13 | 14 | function mint(address to, uint256 amount) public { 15 | uint256 balanceNext = balanceOf[to] + amount; 16 | require(balanceNext >= amount, 'overflow balance'); 17 | balanceOf[to] = balanceNext; 18 | } 19 | 20 | function transfer(address recipient, uint256 amount) external override returns (bool) { 21 | uint256 balanceBefore = balanceOf[msg.sender]; 22 | require(balanceBefore >= amount, 'insufficient balance'); 23 | balanceOf[msg.sender] = balanceBefore - amount; 24 | 25 | uint256 balanceRecipient = balanceOf[recipient]; 26 | require(balanceRecipient + amount >= balanceRecipient, 'recipient balance overflow'); 27 | balanceOf[recipient] = balanceRecipient + amount; 28 | 29 | emit Transfer(msg.sender, recipient, amount); 30 | return true; 31 | } 32 | 33 | function approve(address spender, uint256 amount) external override returns (bool) { 34 | allowance[msg.sender][spender] = amount; 35 | emit Approval(msg.sender, spender, amount); 36 | return true; 37 | } 38 | 39 | function transferFrom( 40 | address sender, 41 | address recipient, 42 | uint256 amount 43 | ) external override returns (bool) { 44 | uint256 allowanceBefore = allowance[sender][msg.sender]; 45 | require(allowanceBefore >= amount, 'allowance insufficient'); 46 | 47 | allowance[sender][msg.sender] = allowanceBefore - amount; 48 | 49 | uint256 balanceRecipient = balanceOf[recipient]; 50 | require(balanceRecipient + amount >= balanceRecipient, 'overflow balance recipient'); 51 | balanceOf[recipient] = balanceRecipient + amount; 52 | uint256 balanceSender = balanceOf[sender]; 53 | require(balanceSender >= amount, 'underflow balance sender'); 54 | balanceOf[sender] = balanceSender - amount; 55 | 56 | emit Transfer(sender, recipient, amount); 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contracts/proxy/AutoRebal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | 3 | pragma solidity 0.7.6; 4 | 5 | import "../interfaces/IHypervisor.sol"; 6 | import "@openzeppelin/contracts/math/SignedSafeMath.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 9 | import "@uniswap/v3-core/contracts/libraries/FullMath.sol"; 10 | import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 11 | import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; 12 | import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; 13 | 14 | contract AutoRebal { 15 | using SafeMath for uint256; 16 | 17 | address public admin; 18 | address public advisor; 19 | address public feeRecipient; 20 | IUniswapV3Pool public pool; 21 | IHypervisor public hypervisor; 22 | int24 public limitWidth = 1; 23 | 24 | modifier onlyAdvisor { 25 | require(msg.sender == advisor, "only advisor"); 26 | _; 27 | } 28 | 29 | modifier onlyAdmin { 30 | require(msg.sender == admin, "only admin"); 31 | _; 32 | } 33 | 34 | constructor(address _admin, address _advisor, address _hypervisor) { 35 | require(_admin != address(0), "_admin should be non-zero"); 36 | require(_advisor != address(0), "_advisor should be non-zero"); 37 | require(_hypervisor != address(0), "_hypervisor should be non-zero"); 38 | admin = _admin; 39 | advisor = _advisor; 40 | hypervisor = IHypervisor(_hypervisor); 41 | } 42 | 43 | function liquidityOptions() public view returns(bool, int24 currentTick) { 44 | 45 | (uint256 total0, uint256 total1) = hypervisor.getTotalAmounts(); 46 | 47 | uint160 sqrtRatioX96; 48 | (sqrtRatioX96, currentTick, , , , , ) = hypervisor.pool().slot0(); 49 | 50 | uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( 51 | sqrtRatioX96, 52 | TickMath.getSqrtRatioAtTick(hypervisor.baseLower()), 53 | TickMath.getSqrtRatioAtTick(hypervisor.baseUpper()), 54 | total0, 55 | total1 56 | ); 57 | 58 | (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( 59 | sqrtRatioX96, 60 | TickMath.getSqrtRatioAtTick(hypervisor.baseLower()), 61 | TickMath.getSqrtRatioAtTick(hypervisor.baseUpper()), 62 | liquidity 63 | ); 64 | 65 | uint256 price = FullMath.mulDiv(uint256(sqrtRatioX96), (uint256(sqrtRatioX96)), 2**(96 * 2)); 66 | return ((total0-amount0) * price > (total1-amount1), currentTick); 67 | 68 | } 69 | 70 | /// @param outMin min amount0,1 returned for shares of liq 71 | function autoRebalance( 72 | uint256[4] memory outMin 73 | ) external onlyAdvisor returns(int24 limitLower, int24 limitUpper) { 74 | 75 | (bool token0Limit, int24 currentTick) = liquidityOptions(); 76 | 77 | if(!token0Limit) { 78 | // extra token1 in limit position = limit below 79 | limitUpper = (currentTick / hypervisor.tickSpacing()) * hypervisor.tickSpacing() - hypervisor.tickSpacing(); 80 | if(limitUpper == currentTick) limitUpper = limitUpper - hypervisor.tickSpacing(); 81 | 82 | limitLower = limitUpper - hypervisor.tickSpacing() * limitWidth; 83 | } 84 | else { 85 | // extra token0 in limit position = limit above 86 | limitLower = (currentTick / hypervisor.tickSpacing()) * hypervisor.tickSpacing() + hypervisor.tickSpacing(); 87 | if(limitLower == currentTick) limitLower = limitLower + hypervisor.tickSpacing(); 88 | 89 | limitUpper = limitLower + hypervisor.tickSpacing() * limitWidth; 90 | } 91 | 92 | uint256[4] memory inMin; 93 | hypervisor.rebalance( 94 | hypervisor.baseLower(), 95 | hypervisor.baseUpper(), 96 | limitLower, 97 | limitUpper, 98 | feeRecipient, 99 | inMin, 100 | outMin 101 | ); 102 | } 103 | 104 | /// @notice compound pending fees 105 | function compound() external onlyAdvisor returns( 106 | uint128 baseToken0Owed, 107 | uint128 baseToken1Owed, 108 | uint128 limitToken0Owed, 109 | uint128 limitToken1Owed, 110 | uint256[4] memory inMin 111 | ) { 112 | hypervisor.compound(); 113 | } 114 | 115 | /// @param newAdmin New Admin Address 116 | function transferAdmin(address newAdmin) external onlyAdmin { 117 | require(newAdmin != address(0), "newAdmin should be non-zero"); 118 | admin = newAdmin; 119 | } 120 | 121 | /// @notice Transfer tokens to recipient from the contract 122 | /// @param token Address of token 123 | /// @param recipient Recipient Address 124 | function rescueERC20(IERC20 token, address recipient) external onlyAdmin { 125 | require(recipient != address(0), "recipient should be non-zero"); 126 | require(token.transfer(recipient, token.balanceOf(address(this)))); 127 | } 128 | 129 | /// @param _recipient fee recipient 130 | function setRecipient(address _recipient) external onlyAdmin { 131 | require(feeRecipient == address(0), "fee recipient already set"); 132 | feeRecipient = _recipient; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /contracts/proxy/admin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | 3 | pragma solidity 0.7.6; 4 | 5 | import "../interfaces/IHypervisor.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | /// @title Admin 9 | 10 | contract Admin { 11 | 12 | address public admin; 13 | bool public ownerFixed = false; 14 | mapping(address => address) public rebalancers; 15 | mapping(address => address) public advisors; 16 | 17 | modifier onlyAdmin { 18 | require(msg.sender == admin, "only admin"); 19 | _; 20 | } 21 | 22 | modifier onlyAdvisor(address hypervisor) { 23 | require(msg.sender == advisors[hypervisor], "only advisor"); 24 | _; 25 | } 26 | 27 | modifier onlyRebalancer(address hypervisor) { 28 | require(msg.sender == rebalancers[hypervisor], "only rebalancer"); 29 | _; 30 | } 31 | 32 | constructor(address _admin) { 33 | admin = _admin; 34 | } 35 | 36 | /// @param _hypervisor Hypervisor Address 37 | /// @param _baseLower The lower tick of the base position 38 | /// @param _baseUpper The upper tick of the base position 39 | /// @param _limitLower The lower tick of the limit position 40 | /// @param _limitUpper The upper tick of the limit position 41 | /// @param _feeRecipient Address of recipient of 10% of earned fees since last rebalance 42 | function rebalance( 43 | address _hypervisor, 44 | int24 _baseLower, 45 | int24 _baseUpper, 46 | int24 _limitLower, 47 | int24 _limitUpper, 48 | address _feeRecipient, 49 | uint256[4] memory inMin, 50 | uint256[4] memory outMin 51 | ) external onlyRebalancer(_hypervisor) { 52 | IHypervisor(_hypervisor).rebalance(_baseLower, _baseUpper, _limitLower, _limitUpper, _feeRecipient, inMin, outMin); 53 | } 54 | 55 | /// @notice Pull liquidity tokens from liquidity and receive the tokens 56 | /// @param _hypervisor Hypervisor Address 57 | /// @param tickLower lower tick 58 | /// @param tickUpper upper tick 59 | /// @param shares Number of liquidity tokens to pull from liquidity 60 | /// @return base0 amount of token0 received from base position 61 | /// @return base1 amount of token1 received from base position 62 | function pullLiquidity( 63 | address _hypervisor, 64 | int24 tickLower, 65 | int24 tickUpper, 66 | uint128 shares, 67 | uint256[2] memory minAmounts 68 | ) external onlyRebalancer(_hypervisor) returns( 69 | uint256 base0, 70 | uint256 base1 71 | ) { 72 | (base0, base1) = IHypervisor(_hypervisor).pullLiquidity(tickLower, tickUpper, shares, minAmounts); 73 | } 74 | 75 | function pullLiquidity( 76 | address _hypervisor, 77 | uint256 shares, 78 | uint256[4] memory minAmounts 79 | ) external onlyRebalancer(_hypervisor) returns( 80 | uint256 base0, 81 | uint256 base1, 82 | uint256 limit0, 83 | uint256 limit1 84 | ) { 85 | (base0, base1, limit0, limit1) = IHypervisor(_hypervisor).pullLiquidity(shares, minAmounts); 86 | } 87 | 88 | function addLiquidity( 89 | address _hypervisor, 90 | int24 tickLower, 91 | int24 tickUpper, 92 | uint256 amount0, 93 | uint256 amount1, 94 | uint256[2] memory inMin 95 | ) external onlyRebalancer(_hypervisor) { 96 | IHypervisor(_hypervisor).addLiquidity(tickLower, tickUpper, amount0, amount1, inMin); 97 | } 98 | 99 | /// @notice Add tokens to base liquidity 100 | /// @param _hypervisor Hypervisor Address 101 | /// @param amount0 Amount of token0 to add 102 | /// @param amount1 Amount of token1 to add 103 | function addBaseLiquidity(address _hypervisor, uint256 amount0, uint256 amount1, uint256[2] memory inMin) external onlyRebalancer(_hypervisor) { 104 | IHypervisor(_hypervisor).addBaseLiquidity(amount0, amount1, inMin); 105 | } 106 | 107 | /// @notice Add tokens to limit liquidity 108 | /// @param _hypervisor Hypervisor Address 109 | /// @param amount0 Amount of token0 to add 110 | /// @param amount1 Amount of token1 to add 111 | function addLimitLiquidity(address _hypervisor, uint256 amount0, uint256 amount1, uint256[2] memory inMin) external onlyRebalancer(_hypervisor) { 112 | IHypervisor(_hypervisor).addLimitLiquidity(amount0, amount1, inMin); 113 | } 114 | 115 | /// @notice compound pending fees 116 | /// @param _hypervisor Hypervisor Address 117 | function compound( address _hypervisor) external onlyAdvisor(_hypervisor) returns( 118 | uint128 baseToken0Owed, 119 | uint128 baseToken1Owed, 120 | uint128 limitToken0Owed, 121 | uint128 limitToken1Owed, 122 | uint256[4] memory inMin 123 | ) { 124 | IHypervisor(_hypervisor).compound(); 125 | } 126 | 127 | function compound( address _hypervisor, uint256[4] memory inMin) 128 | external onlyAdvisor(_hypervisor) returns( 129 | uint128 baseToken0Owed, 130 | uint128 baseToken1Owed, 131 | uint128 limitToken0Owed, 132 | uint128 limitToken1Owed 133 | ) { 134 | IHypervisor(_hypervisor).compound(inMin); 135 | } 136 | /// @param _hypervisor Hypervisor Address 137 | function setWhitelist(address _hypervisor, address newWhitelist) external onlyAdmin { 138 | IHypervisor(_hypervisor).setWhitelist(newWhitelist); 139 | } 140 | 141 | /// @param _hypervisor Hypervisor Address 142 | function removeWhitelisted(address _hypervisor) external onlyAdmin { 143 | IHypervisor(_hypervisor).removeWhitelisted(); 144 | } 145 | 146 | /// @param newAdmin New Admin Address 147 | function transferAdmin(address newAdmin) external onlyAdmin { 148 | require(newAdmin != address(0), "newAdmin should be non-zero"); 149 | admin = newAdmin; 150 | } 151 | 152 | /// @param _hypervisor Hypervisor Address 153 | /// @param newOwner New Owner Address 154 | function transferHypervisorOwner(address _hypervisor, address newOwner) external onlyAdmin { 155 | require(!ownerFixed, "permanent owner in place"); 156 | IHypervisor(_hypervisor).transferOwnership(newOwner); 157 | } 158 | 159 | // @dev permanently disable hypervisor ownership transfer 160 | function fixOwnership() external onlyAdmin { 161 | ownerFixed = false; 162 | } 163 | 164 | /// @param newAdvisor New Advisor Address 165 | function setAdvisor(address _hypervisor, address newAdvisor) external onlyAdmin { 166 | require(newAdvisor != address(0), "newAdvisor should be non-zero"); 167 | advisors[_hypervisor] = newAdvisor; 168 | } 169 | 170 | /// @param newRebalancer New Rebalancer Address 171 | function setRebalancer(address _hypervisor, address newRebalancer) external onlyAdmin { 172 | require(newRebalancer != address(0), "newRebalancer should be non-zero"); 173 | rebalancers[_hypervisor] = newRebalancer; 174 | } 175 | 176 | /// @notice Transfer tokens to the recipient from the contract 177 | /// @param token Address of token 178 | /// @param recipient Recipient Address 179 | function rescueERC20(IERC20 token, address recipient) external onlyAdmin { 180 | require(recipient != address(0), "recipient should be non-zero"); 181 | require(token.transfer(recipient, token.balanceOf(address(this)))); 182 | } 183 | 184 | /// @param _hypervisor Hypervisor Address 185 | /// @param newFee fee amount 186 | function setFee(address _hypervisor, uint8 newFee) external onlyAdmin { 187 | IHypervisor(_hypervisor).setFee(newFee); 188 | } 189 | /// @notice Toggle Direct Deposit 190 | function toggleDirectDeposit(address _hypervisor) external onlyAdmin { 191 | IHypervisor(_hypervisor).toggleDirectDeposit(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /contracts/test/MockToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity 0.7.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockToken is ERC20 { 8 | constructor( 9 | string memory name, 10 | string memory symbol, 11 | uint8 decimals 12 | ) ERC20(name, symbol) { 13 | _setupDecimals(decimals); 14 | } 15 | 16 | function mint(address account, uint256 amount) external { 17 | _mint(account, amount); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/test/TestRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity 0.7.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 6 | 7 | import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 8 | import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol"; 9 | import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; 10 | import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; 11 | 12 | /** 13 | * @title TestRouter 14 | * @dev DO NOT USE IN PRODUCTION. This is only intended to be used for 15 | * tests and lacks slippage and callback caller checks. 16 | */ 17 | contract TestRouter is IUniswapV3MintCallback, IUniswapV3SwapCallback { 18 | using SafeERC20 for IERC20; 19 | 20 | function mint( 21 | IUniswapV3Pool pool, 22 | int24 tickLower, 23 | int24 tickUpper, 24 | uint128 amount 25 | ) external returns (uint256, uint256) { 26 | int24 tickSpacing = pool.tickSpacing(); 27 | require(tickLower % tickSpacing == 0, "tickLower must be a multiple of tickSpacing"); 28 | require(tickUpper % tickSpacing == 0, "tickUpper must be a multiple of tickSpacing"); 29 | return pool.mint(msg.sender, tickLower, tickUpper, amount, abi.encode(msg.sender)); 30 | } 31 | 32 | function swap( 33 | IUniswapV3Pool pool, 34 | bool zeroForOne, 35 | int256 amountSpecified 36 | ) external returns (int256, int256) { 37 | return 38 | pool.swap( 39 | msg.sender, 40 | zeroForOne, 41 | amountSpecified, 42 | zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, 43 | abi.encode(msg.sender) 44 | ); 45 | } 46 | 47 | function uniswapV3MintCallback( 48 | uint256 amount0Owed, 49 | uint256 amount1Owed, 50 | bytes calldata data 51 | ) external override { 52 | _callback(amount0Owed, amount1Owed, data); 53 | } 54 | 55 | function uniswapV3SwapCallback( 56 | int256 amount0Delta, 57 | int256 amount1Delta, 58 | bytes calldata data 59 | ) external override { 60 | uint256 amount0 = amount0Delta > 0 ? uint256(amount0Delta) : 0; 61 | uint256 amount1 = amount1Delta > 0 ? uint256(amount1Delta) : 0; 62 | _callback(amount0, amount1, data); 63 | } 64 | 65 | function _callback( 66 | uint256 amount0, 67 | uint256 amount1, 68 | bytes calldata data 69 | ) internal { 70 | IUniswapV3Pool pool = IUniswapV3Pool(msg.sender); 71 | address payer = abi.decode(data, (address)); 72 | 73 | IERC20(pool.token0()).safeTransferFrom(payer, msg.sender, amount0); 74 | IERC20(pool.token1()).safeTransferFrom(payer, msg.sender, amount1); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x07ebc28f47416a421775e01f1c5dadeb882249509ed5081b29d7f3d75ed46db3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /gamma logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-ethers' 2 | import '@nomiclabs/hardhat-etherscan' 3 | import '@nomiclabs/hardhat-waffle' 4 | import '@typechain/hardhat' 5 | import "hardhat-watcher" 6 | import './scripts/copy-uniswap-v3-artifacts.ts' 7 | import './tasks/hypervisor' 8 | import './tasks/swap' 9 | import { parseUnits } from 'ethers/lib/utils' 10 | import { HardhatUserConfig } from 'hardhat/types' 11 | require('dotenv').config() 12 | const mnemonic = process.env.DEV_MNEMONIC || '' 13 | 14 | const config: HardhatUserConfig = { 15 | networks: { 16 | hardhat: { 17 | allowUnlimitedContractSize: false, 18 | }, 19 | celo: { 20 | url: "https://forno.celo.org", 21 | accounts: [process.env.MAINNET_PRIVATE_KEY as string], 22 | chainId: 42220 23 | }, 24 | polygon: { 25 | url: 'https://polygon-mainnet.g.alchemy.com/v2/' + process.env.ALCHEMY_POLYGON, 26 | accounts: [process.env.MAINNET_PRIVATE_KEY as string], 27 | gasPrice: parseUnits('300', 'gwei').toNumber(), 28 | }, 29 | mainnet: { 30 | url: 'https://eth-mainnet.alchemyapi.io/v2/' + process.env.ALCHEMY_MAINNET, 31 | accounts: [process.env.MAINNET_PRIVATE_KEY as string], 32 | gasPrice: parseUnits('40', 'gwei').toNumber(), 33 | }, 34 | optimism: { 35 | url: 'https://opt-mainnet.g.alchemy.com/v2/' + process.env.ALCHEMY_OPTIMISM, 36 | accounts: [process.env.MAINNET_PRIVATE_KEY as string], 37 | gasPrice: parseUnits('100', 'gwei').toNumber(), 38 | }, 39 | arbitrum: { 40 | url: 'https://arb-mainnet.g.alchemy.com/v2/' + process.env.ALCHEMY_ARBITRUM, 41 | accounts: [process.env.MAINNET_PRIVATE_KEY as string], 42 | gasPrice: parseUnits('10', 'gwei').toNumber(), 43 | }, 44 | 45 | }, 46 | watcher: { 47 | compilation: { 48 | tasks: ["compile"], 49 | } 50 | }, 51 | solidity: { 52 | compilers: [ 53 | { 54 | version: '0.7.6', 55 | settings: { 56 | optimizer: { 57 | enabled: true, 58 | runs: 800, 59 | }, 60 | metadata: { 61 | bytecodeHash: 'none', 62 | }, 63 | }, 64 | }, 65 | { version: '0.6.11' }, 66 | { version: '0.6.0' }, 67 | { version: '0.6.2' }, 68 | { version: '0.6.12' }, 69 | ], 70 | }, 71 | etherscan: { 72 | apiKey: process.env.CELO_APIKEY, 73 | // apiKey: process.env.ETHERSCAN_APIKEY, 74 | // apiKey: process.env.OPTIMISM_APIKEY, 75 | // apiKey: process.env.ARBISCAN_APIKEY, 76 | // apiKey: process.env.POLYGONSCAN_APIKEY, 77 | }, 78 | mocha: { 79 | timeout: 2000000 80 | } 81 | } 82 | export default config; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypervisor", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "compile": "hardhat compile", 7 | "test": "bash ./scripts/test.sh", 8 | "flatten": "bash ./scripts/flatten.sh", 9 | "prettier": "prettier --write contracts/*.sol", 10 | "lint": "solhint contracts/*.sol" 11 | }, 12 | "dependencies": { 13 | "@chainlink/contracts": "^0.1.9", 14 | "@ethersproject/abstract-signer": "^5.3.0", 15 | "@openzeppelin/contracts": "^3.4.1-solc-0.7-2", 16 | "@uniswap/v3-core": "*", 17 | "@uniswap/v3-periphery": "*", 18 | "bignumber.js": "^9.0.1", 19 | "prettier": "^2.2.1", 20 | "prettier-plugin-solidity": "^1.0.0-beta.10", 21 | "solhint-plugin-prettier": "0.0.5" 22 | }, 23 | "devDependencies": { 24 | "@nomiclabs/hardhat-ethers": "^2.0.2", 25 | "@nomiclabs/hardhat-etherscan": "^3.0.1", 26 | "@nomiclabs/hardhat-waffle": "^2.0.1", 27 | "@openzeppelin/test-helpers": "^0.5.10", 28 | "@typechain/ethers-v5": "^7.0.0", 29 | "@typechain/hardhat": "^2.1.0", 30 | "@types/chai": "^4.2.16", 31 | "@types/fs-extra": "^9.0.11", 32 | "@types/mocha": "^8.2.2", 33 | "chai": "^4.3.4", 34 | "decimal.js": "^10.2.1", 35 | "dotenv": "^8.2.0", 36 | "ethereum-waffle": "^3.3.0", 37 | "ethers": "^5.1.3", 38 | "fs-extra": "^7.0.1", 39 | "hardhat": "^2.2.0", 40 | "hardhat-gas-reporter": "^1.0.8", 41 | "hardhat-typechain": "^0.3.5", 42 | "hardhat-watcher": "^2.1.1", 43 | "mocha": "^7.2.0", 44 | "prettier": "^2.2.1", 45 | "prettier-plugin-solidity": "*", 46 | "solc-0.7": "npm:solc@^0.7.6", 47 | "solhint": "^3.3.4", 48 | "solhint-plugin-prettier": "^0.0.5", 49 | "ts-generator": "^0.1.1", 50 | "ts-node": "^8.10.2", 51 | "typechain": "^5.0.0", 52 | "typescript": "^4.2.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/copy-uniswap-v3-artifacts.ts: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra' 2 | const { TASK_COMPILE_GET_COMPILATION_TASKS } = require("hardhat/builtin-tasks/task-names") 3 | import { subtask } from "hardhat/config" 4 | import * as path from "path"; 5 | 6 | let TASK_CREATE_UNISWAPV3_ARTIFACT = '' 7 | 8 | subtask( 9 | TASK_COMPILE_GET_COMPILATION_TASKS, 10 | async (_, __, runSuper) => { 11 | const otherTasks = await runSuper() 12 | return [...otherTasks, TASK_CREATE_UNISWAPV3_ARTIFACT] 13 | } 14 | ); 15 | 16 | subtask(TASK_CREATE_UNISWAPV3_ARTIFACT, async (_, { artifacts }) => { 17 | fse.copySync( 18 | path.join(__dirname, '../node_modules/@uniswap/v3-core/artifacts/contracts'), 19 | path.join((artifacts as any)['_artifactsPath'], '@uniswap/v3-core/contracts'), 20 | { overwrite: false } 21 | ) 22 | fse.copySync( 23 | path.join(__dirname, '../node_modules/@uniswap/v3-periphery/artifacts/contracts'), 24 | path.join((artifacts as any)['_artifactsPath'], '@uniswap/v3-periphery/contracts'), 25 | { overwrite: false } 26 | ) 27 | }); 28 | -------------------------------------------------------------------------------- /scripts/flatten.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z $1 ]; then 4 | truffle-flattener contracts/ForFlattened.sol | awk '/SPDX-License-Identifier/&&c++>0 {next} 1' | awk '/pragma experimental ABIEncoderV2;/&&c++>0 {next} 1' > contracts/Flattened.sol 5 | else 6 | truffle-flattener $1 | awk '/SPDX-License-Identifier/&&c++>0 {next} 1' | awk '/pragma experimental ABIEncoderV2;/&&c++>0 {next} 1' > contracts/Flattened.sol 7 | fi 8 | 9 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d "$(pwd)/artifacts" ]; then 4 | hardhat compile 5 | fi 6 | 7 | hardhat test --network hardhat test/mainnet_fork.test.ts 8 | -------------------------------------------------------------------------------- /tasks/hypervisor.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { constants, Wallet } from 'ethers' 3 | import { formatEther, parseEther, formatUnits, parseUnits } from 'ethers/lib/utils' 4 | import { task } from 'hardhat/config' 5 | import { deployContract, signPermission } from './utils' 6 | import { 7 | FeeAmount, 8 | TICK_SPACINGS, 9 | encodePriceSqrt, 10 | getPositionKey, 11 | getMinTick, 12 | getMaxTick, 13 | MaxUint256 14 | } from './shared/utilities' 15 | import { 16 | baseTicksFromCurrentTick, 17 | limitTicksFromCurrentTick 18 | } from './shared/tick' 19 | 20 | task('deploy-router', 'Deploy Hypervisor contract') 21 | .addParam('token0', 'token address') 22 | .addParam('token1', 'token address') 23 | .addParam('pos', 'token address') 24 | .setAction(async (cliArgs, { ethers, run, network }) => { 25 | 26 | const args = { 27 | token0: cliArgs.token0, 28 | token1: cliArgs.token1, 29 | pos: cliArgs.pos 30 | }; 31 | console.log('Network') 32 | console.log(' ', network.name) 33 | console.log('Task Args') 34 | console.log(args) 35 | 36 | // compile 37 | 38 | await run('compile') 39 | 40 | // get signer 41 | 42 | const signer = (await ethers.getSigners())[0] 43 | console.log('Signer') 44 | console.log(' at', signer.address) 45 | console.log(' ETH', formatEther(await signer.getBalance())) 46 | 47 | // deploy contracts 48 | const router = await deployContract( 49 | 'Router', 50 | await ethers.getContractFactory('Router'), 51 | signer, 52 | [args.token0, args.token1, args.pos] 53 | ) 54 | 55 | await router.deployTransaction.wait(5) 56 | await run('verify:verify', { 57 | address: router.address, 58 | constructorArguments: [args.token0, args.token1, args.pos] 59 | }) 60 | }) 61 | 62 | task('deploy-timelock', 'Deploy timelock contract') 63 | .addParam('chef', 'chef') 64 | .addParam('mindelay', 'min delay') 65 | .addParam('proposer', 'proposer address') 66 | .addParam('executor', 'exec address') 67 | .setAction(async (args, { ethers, run, network }) => { 68 | 69 | console.log('Network') 70 | console.log(' ', network.name) 71 | console.log('Task Args') 72 | console.log(args) 73 | 74 | // compile 75 | 76 | await run('compile') 77 | 78 | // get signer 79 | 80 | const signer = (await ethers.getSigners())[0] 81 | console.log('Signer') 82 | console.log(' at', signer.address) 83 | console.log(' ETH', formatEther(await signer.getBalance())) 84 | 85 | // deploy contracts 86 | const timelock = await deployContract( 87 | 'TimeLock', 88 | await ethers.getContractFactory('Timelock'), 89 | signer, 90 | [args.chef, args.mindelay, [args.proposer], [args.executor]] 91 | ) 92 | await timelock.deployTransaction.wait(5) 93 | await run('verify:verify', { 94 | address: timelock.address, 95 | constructorArguments: [args.chef, args.mindelay, [args.proposer], [args.executor]] 96 | }) 97 | }) 98 | 99 | 100 | task('add-chef-pool', 'Deploy admin contract') 101 | .addParam('chef', 'token address') 102 | .addParam('rewardPerBlock', 'token address') 103 | .addParam('lpToken', 'token address') 104 | .addParam('withUpdate', 'token address') 105 | .setAction(async (args, { ethers, run, network }) => { 106 | 107 | console.log('Network') 108 | console.log(' ', network.name) 109 | console.log('Task Args') 110 | console.log(args) 111 | 112 | // compile 113 | 114 | await run('compile') 115 | 116 | // get signer 117 | 118 | const signer = (await ethers.getSigners())[0] 119 | console.log('Signer') 120 | console.log(' at', signer.address) 121 | console.log(' ETH', formatEther(await signer.getBalance())) 122 | 123 | 124 | const chef = await ethers.getContractAt( 125 | 'MasterChef', 126 | args.chef, 127 | signer, 128 | ) 129 | 130 | await chef.add(args.rewardPerBlock, args.lpToken, args.withUpdate); 131 | }); 132 | 133 | task('deploy-masterchef', 'Deploy admin contract') 134 | .addParam('rewardToken', 'reward rate') 135 | .addParam('rewardPerBlock', 'reward rate') 136 | .addParam('startBlock', 'start block') 137 | .addParam('endBlock', 'end block') 138 | .setAction(async (args, { ethers, run, network }) => { 139 | console.log('Network') 140 | console.log(' ', network.name) 141 | console.log('Task Args') 142 | console.log(args) 143 | 144 | // compile 145 | 146 | await run('compile') 147 | 148 | // get signer 149 | 150 | const signer = (await ethers.getSigners())[0] 151 | console.log('Signer') 152 | console.log(' at', signer.address) 153 | console.log(' ETH', formatEther(await signer.getBalance())) 154 | 155 | // deploy contracts 156 | 157 | const chefFactory = await ethers.getContractFactory('MasterChef') 158 | 159 | const chef = await deployContract( 160 | 'MasterChef', 161 | await ethers.getContractFactory('MasterChef'), 162 | signer, 163 | [args.rewardToken, args.rewardPerBlock, args.startBlock, args.endBlock] 164 | ) 165 | 166 | await chef.deployTransaction.wait(5) 167 | await run('verify:verify', { 168 | address: chef.address, 169 | constructorArguments: [args.rewardToken, args.rewardPerBlock, args.startBlock, args.endBlock] 170 | 171 | }) 172 | 173 | }); 174 | 175 | 176 | task('deploy-token', 'Deploy admin contract') 177 | .addParam('name', 'admin account') 178 | .addParam('symbol', 'advisor account') 179 | .addParam('decimals', 'advisor account') 180 | .setAction(async (args, { ethers, run, network }) => { 181 | console.log('Network') 182 | console.log(' ', network.name) 183 | console.log('Task Args') 184 | console.log(args) 185 | 186 | // compile 187 | 188 | await run('compile') 189 | 190 | // get signer 191 | 192 | const signer = (await ethers.getSigners())[0] 193 | console.log('Signer') 194 | console.log(' at', signer.address) 195 | console.log(' ETH', formatEther(await signer.getBalance())) 196 | 197 | // deploy contracts 198 | 199 | const adminFactory = await ethers.getContractFactory('MockToken') 200 | 201 | const admin = await deployContract( 202 | 'MockToken', 203 | await ethers.getContractFactory('MockToken'), 204 | signer, 205 | [args.name, args.symbol, args.decimals] 206 | ) 207 | 208 | await admin.deployTransaction.wait(5) 209 | await run('verify:verify', { 210 | address: admin.address, 211 | constructorArguments: [args.name, args.symbol, args.decimals] 212 | }) 213 | 214 | }); 215 | 216 | 217 | task('deploy-admin', 'Deploy admin contract') 218 | .addParam('admin', 'admin account') 219 | .addParam('advisor', 'advisor account') 220 | .setAction(async (args, { ethers, run, network }) => { 221 | console.log('Network') 222 | console.log(' ', network.name) 223 | console.log('Task Args') 224 | console.log(args) 225 | 226 | // compile 227 | 228 | await run('compile') 229 | 230 | // get signer 231 | 232 | const signer = (await ethers.getSigners())[0] 233 | console.log('Signer') 234 | console.log(' at', signer.address) 235 | console.log(' ETH', formatEther(await signer.getBalance())) 236 | 237 | // deploy contracts 238 | 239 | const adminFactory = await ethers.getContractFactory('Admin') 240 | 241 | const admin = await deployContract( 242 | 'Admin', 243 | await ethers.getContractFactory('Admin'), 244 | signer, 245 | [args.admin, args.advisor] 246 | ) 247 | 248 | await admin.deployTransaction.wait(5) 249 | await run('verify:verify', { 250 | address: admin.address, 251 | constructorArguments: [args.admin, args.advisor] 252 | }) 253 | 254 | }); 255 | 256 | task('deploy-hypervisor-factory', 'Deploy Hypervisor contract') 257 | .setAction(async (cliArgs, { ethers, run, network }) => { 258 | 259 | const args = { 260 | uniswapFactory: "0x1f98431c8ad98523631ae4a59f267346ea31f984", 261 | }; 262 | 263 | console.log('Network') 264 | console.log(' ', network.name) 265 | console.log('Task Args') 266 | console.log(args) 267 | 268 | // compile 269 | 270 | await run('compile') 271 | 272 | // get signer 273 | 274 | const signer = (await ethers.getSigners())[0] 275 | console.log('Signer') 276 | console.log(' at', signer.address) 277 | console.log(' ETH', formatEther(await signer.getBalance())) 278 | 279 | // deploy contracts 280 | 281 | const hypervisorFactoryFactory = await ethers.getContractFactory('HypervisorFactory') 282 | 283 | const hypervisorFactory = await deployContract( 284 | 'HypervisorFactory', 285 | await ethers.getContractFactory('HypervisorFactory'), 286 | signer, 287 | [args.uniswapFactory] 288 | ) 289 | 290 | await hypervisorFactory.deployTransaction.wait(5) 291 | await run('verify:verify', { 292 | address: hypervisorFactory.address, 293 | constructorArguments: [args.uniswapFactory], 294 | }) 295 | }) 296 | 297 | task('deploy-hypervisor-orphan', 'Deploy Hypervisor contract without factory') 298 | .addParam('pool', 'the uniswap pool address') 299 | .addParam('name', 'erc20 name') 300 | .addParam('symbol', 'erc2 symbol') 301 | .setAction(async (cliArgs, { ethers, run, network }) => { 302 | 303 | // compile 304 | 305 | await run('compile') 306 | 307 | // get signer 308 | 309 | const signer = (await ethers.getSigners())[0] 310 | console.log('Signer') 311 | console.log(' at', signer.address) 312 | console.log(' ETH', formatEther(await signer.getBalance())) 313 | 314 | const args = { 315 | pool: cliArgs.pool, 316 | owner: signer.address, 317 | name: cliArgs.name, 318 | symbol: cliArgs.symbol 319 | } 320 | 321 | console.log('Network') 322 | console.log(' ', network.name) 323 | console.log('Task Args') 324 | console.log(args) 325 | 326 | const hypervisor = await deployContract( 327 | 'Hypervisor', 328 | await ethers.getContractFactory('Hypervisor'), 329 | signer, 330 | [args.pool, args.owner, args.name, args.symbol] 331 | ) 332 | 333 | await hypervisor.deployTransaction.wait(5) 334 | await run('verify:verify', { 335 | address: hypervisor.address, 336 | constructorArguments: [args.pool, args.owner, args.name, args.symbol], 337 | }) 338 | 339 | }); 340 | 341 | task('deploy-hypervisor', 'Deploy Hypervisor contract via the factory') 342 | .addParam('factory', 'address of hypervisor factory') 343 | .addParam('token0', 'token0 of pair') 344 | .addParam('token1', 'token1 of pair') 345 | .addParam('fee', 'LOW, MEDIUM, or HIGH') 346 | .addParam('name', 'erc20 name') 347 | .addParam('symbol', 'erc2 symbol') 348 | .setAction(async (cliArgs, { ethers, run, network }) => { 349 | 350 | await run('compile') 351 | 352 | // get signer 353 | 354 | const signer = (await ethers.getSigners())[0] 355 | console.log('Signer') 356 | console.log(' at', signer.address) 357 | console.log(' ETH', formatEther(await signer.getBalance())) 358 | 359 | const args = { 360 | factory: cliArgs.factory, 361 | token0: cliArgs.token0, 362 | token1: cliArgs.token1, 363 | fee: FeeAmount[cliArgs.fee], 364 | name: cliArgs.name, 365 | symbol: cliArgs.symbol 366 | }; 367 | 368 | console.log('Network') 369 | console.log(' ', network.name) 370 | console.log('Task Args') 371 | console.log(args) 372 | 373 | 374 | const hypervisorFactory = await ethers.getContractAt( 375 | 'HypervisorFactory', 376 | args.factory, 377 | signer, 378 | ) 379 | 380 | const hypervisor = await hypervisorFactory.createHypervisor( 381 | args.token0, args.token1, args.fee, args.name, args.symbol) 382 | 383 | }) 384 | 385 | task('verify-hypervisor', 'Verify Hypervisor contract') 386 | .addParam('hypervisor', 'the hypervisor to verify') 387 | .addParam('pool', 'the uniswap pool address') 388 | .addParam('name', 'erc20 name') 389 | .addParam('symbol', 'erc2 symbol') 390 | .setAction(async (cliArgs, { ethers, run, network }) => { 391 | 392 | console.log('Network') 393 | console.log(' ', network.name) 394 | 395 | await run('compile') 396 | 397 | // get signer 398 | 399 | const signer = (await ethers.getSigners())[0] 400 | console.log('Signer') 401 | console.log(' at', signer.address) 402 | console.log(' ETH', formatEther(await signer.getBalance())) 403 | 404 | const args = { 405 | pool: cliArgs.pool, 406 | owner: signer.address, 407 | name: cliArgs.name, 408 | symbol: cliArgs.symbol 409 | } 410 | 411 | console.log('Task Args') 412 | console.log(args) 413 | 414 | const hypervisor = await ethers.getContractAt( 415 | 'Hypervisor', 416 | cliArgs.hypervisor, 417 | signer, 418 | ) 419 | await run('verify:verify', { 420 | address: hypervisor.address, 421 | constructorArguments: Object.values(args), 422 | }) 423 | 424 | }); 425 | 426 | task('deploy-uniproxy', 'Deploy UniProxy contract') 427 | .setAction(async (cliArgs, { ethers, run, network }) => { 428 | 429 | await run('compile') 430 | 431 | // get signer 432 | 433 | const signer = (await ethers.getSigners())[0] 434 | console.log('Signer') 435 | console.log(' at', signer.address) 436 | console.log(' ETH', formatEther(await signer.getBalance())) 437 | 438 | console.log('Network') 439 | console.log(' ', network.name) 440 | 441 | const uniProxyFactory = await ethers.getContractFactory('UniProxy') 442 | 443 | const uniProxy = await deployContract( 444 | 'UniProxy', 445 | uniProxyFactory, 446 | signer 447 | ) 448 | 449 | await uniProxy.deployTransaction.wait(5) 450 | await run('verify:verify', { 451 | address: uniProxy.address 452 | }) 453 | }) 454 | 455 | task('verify-uniproxy', 'Verify UniProxy contract') 456 | .addParam('uniproxy', 'the UniProxy to verify') 457 | .setAction(async (cliArgs, { ethers, run, network }) => { 458 | 459 | await run('compile') 460 | 461 | // get signer 462 | 463 | const signer = (await ethers.getSigners())[0] 464 | console.log('Signer') 465 | console.log(' at', signer.address) 466 | console.log(' ETH', formatEther(await signer.getBalance())) 467 | 468 | console.log('Network') 469 | console.log(' ', network.name) 470 | 471 | const uniProxy = await ethers.getContractAt( 472 | 'UniProxy', 473 | cliArgs.uniproxy, 474 | signer, 475 | ) 476 | 477 | await run('verify:verify', { 478 | address: uniProxy.address 479 | }) 480 | }) 481 | 482 | task('initialize-hypervisor', 'Initialize Hypervisor contract') 483 | .addParam('hypervisor', 'the hypervisor') 484 | .addParam('amount0', 'the amount of token0') 485 | .addParam('amount1', 'the amount of token1') 486 | .addParam('uniproxy', 'the uniproxy') 487 | .addParam('admin', 'the admin address') 488 | .setAction(async (cliArgs, { ethers, run, network }) => { 489 | 490 | console.log('Network') 491 | console.log(' ', network.name) 492 | 493 | await run('compile') 494 | 495 | // get signer 496 | 497 | const signer = (await ethers.getSigners())[0] 498 | console.log('Signer') 499 | console.log(' at', signer.address) 500 | console.log(' ETH', formatEther(await signer.getBalance())) 501 | 502 | const args = { 503 | hypervisor: cliArgs.hypervisor, 504 | owner: signer.address, 505 | amount0: cliArgs.amount0, 506 | amount1: cliArgs.amount1, 507 | uniproxy: cliArgs.uniproxy, 508 | admin: cliArgs.admin 509 | } 510 | 511 | console.log('Task Args') 512 | console.log(args) 513 | 514 | const hypervisor = await ethers.getContractAt( 515 | 'Hypervisor', 516 | cliArgs.hypervisor, 517 | signer, 518 | ) 519 | 520 | const uniproxy = await ethers.getContractAt( 521 | 'UniProxy', 522 | cliArgs.uniproxy, 523 | signer, 524 | ) 525 | 526 | const token0 = await ethers.getContractAt( 527 | 'ERC20', 528 | await hypervisor.token0(), 529 | signer 530 | ) 531 | 532 | const token1 = await ethers.getContractAt( 533 | 'ERC20', 534 | await hypervisor.token1(), 535 | signer 536 | ) 537 | 538 | console.log('Signer') 539 | console.log(' at', signer.address) 540 | console.log(' ', (await token0.symbol()), ' ', formatUnits(await token0.balanceOf(signer.address), await token0.decimals())) 541 | console.log(' ', (await token1.symbol()), ' ', formatUnits(await token1.balanceOf(signer.address), await token1.decimals())) 542 | 543 | // Token Approval 544 | console.log('Token Approving...') 545 | await token0.approve(hypervisor.address, MaxUint256) 546 | await token1.approve(hypervisor.address, MaxUint256) 547 | console.log('Approval Success') 548 | 549 | // Set Whitelist 550 | console.log('Whitelist Signer...') 551 | await hypervisor.setWhitelist(signer.address) 552 | console.log('Success') 553 | 554 | // Make First Deposit 555 | console.log('First Depositing...') 556 | console.log( parseUnits(cliArgs.amount0, (await token0.decimals())), 557 | parseUnits(cliArgs.amount1, (await token1.decimals())), 558 | signer.address, 559 | signer.address) 560 | 561 | await hypervisor.deposit( 562 | parseUnits(cliArgs.amount0, (await token0.decimals())), 563 | parseUnits(cliArgs.amount1, (await token1.decimals())), 564 | signer.address, 565 | signer.address, 566 | [0, 0, 0, 0] 567 | ) 568 | console.log('Success') 569 | 570 | // Rebalance 571 | console.log('Rebalancing') 572 | const pool = await ethers.getContractAt( 573 | 'UniswapV3Pool', 574 | await hypervisor.pool(), 575 | signer 576 | ) 577 | const tickSpacing = 100 578 | const percent = 8 579 | let currentTick: number 580 | [, currentTick] = await pool.slot0() 581 | let [baseLower, baseUpper] = baseTicksFromCurrentTick( 582 | currentTick, 583 | await token0.decimals(), 584 | await token1.decimals(), 585 | tickSpacing, 586 | percent 587 | ) 588 | let [limitLower, limitUpper] = limitTicksFromCurrentTick( 589 | currentTick, 590 | await token0.decimals(), 591 | await token1.decimals(), 592 | tickSpacing, 593 | percent, 594 | true 595 | ) 596 | 597 | console.log(baseLower) 598 | console.log(baseUpper) 599 | console.log(limitLower) 600 | console.log(limitUpper) 601 | 602 | // await hypervisor.rebalance( 603 | // -6000, 604 | // 6000, 605 | // -600, 606 | // 600, 607 | // signer.address, 608 | // [0, 0, 0, 0], 609 | // [0, 0, 0, 0] 610 | // ) 611 | 612 | await hypervisor.rebalance( 613 | baseLower, 614 | baseUpper, 615 | limitLower, 616 | limitUpper, 617 | signer.address, 618 | [0, 0, 0, 0], 619 | [0, 0, 0, 0] 620 | ) 621 | console.log('Success') 622 | 623 | // Whitelist uniproxy 624 | console.log('Whitelist uniproxy') 625 | await hypervisor.setWhitelist(cliArgs.uniproxy) 626 | console.log('Success') 627 | 628 | // TransferOnwership 629 | console.log('Transferring Ownership') 630 | await hypervisor.transferOwnership(cliArgs.admin) 631 | console.log('Success') 632 | 633 | console.log('Add to uniproxy'); 634 | await uniproxy.addPosition(hypervisor.address,4); 635 | console.log('Success') 636 | 637 | }); 638 | -------------------------------------------------------------------------------- /tasks/shared/tick.ts: -------------------------------------------------------------------------------- 1 | export function generateTick(price: number, decimals0: number, decimals1: number) { 2 | // note the change of logarithmic base 3 | return Math.round(Math.log(price * Math.pow(10, decimals1 - decimals0)) / Math.log(1.0001)); 4 | } 5 | 6 | export function tickToPrice(tick: number, decimals0: number, decimals1: number) { 7 | return tickToRawPrice(tick) / Math.pow(10, decimals1 - decimals0); 8 | } 9 | 10 | export function tickToRawPrice(tick: number) { 11 | return Math.pow(1.0001, tick); 12 | } 13 | 14 | export function priceBands(price: number, percent: number) { 15 | return [price * ((100 - percent)/100), price * ((100 + percent)/100)] 16 | } 17 | 18 | export function tickBands(tick: number, percent: number) { 19 | if (tick > 0) { 20 | return [tick * ((100 - percent)/100), tick * ((100 + percent)/100)] 21 | } else { 22 | return [tick * ((100 + percent)/100), tick * ((100 - percent)/100)] 23 | } 24 | } 25 | 26 | export function baseTicksFromCurrentTick(tick: number, decimals0: number, decimals1: number, tickSpacing: number, percent: number) { 27 | let lowerTick: number; 28 | let upperTick: number; 29 | [lowerTick, upperTick] = tickBands(tick, percent); 30 | return [roundTick(lowerTick, tickSpacing), roundTick(upperTick, tickSpacing)]; 31 | } 32 | 33 | export function limitTicksFromCurrentTick(tick: number, decimals0: number, decimals1: number, tickSpacing: number, percent: number, above: boolean) { 34 | let price = tickToPrice(tick, decimals0, decimals1); 35 | let priceTick = generateTick(price, decimals0, decimals1); 36 | let lowerTick: number; 37 | let upperTick: number; 38 | [lowerTick, upperTick] = tickBands(tick, percent); 39 | 40 | let modulus = tick % tickSpacing; 41 | modulus = modulus < 0 ? modulus + tickSpacing : modulus; 42 | 43 | if (above) { 44 | return [tick + tickSpacing - modulus, roundTick(upperTick, tickSpacing)]; 45 | } else { 46 | return [roundTick(lowerTick, tickSpacing), tick - modulus]; 47 | } 48 | } 49 | 50 | export function positionTicksFromCurrentTick(tick: number, decimals0: number, decimals1: number, tickSpacing: number, percent: number, above: boolean) { 51 | let [baseLower, baseUpper] = baseTicksFromCurrentTick(tick, decimals0, decimals1, tickSpacing, percent); 52 | let [limitLower, limitUpper] = limitTicksFromCurrentTick(tick, decimals0, decimals1, tickSpacing, percent, above); 53 | return [baseLower, baseUpper, limitLower, limitUpper]; 54 | } 55 | 56 | export function roundTick(tick: number, tickSpacing: number) { 57 | let modulus = tick % tickSpacing; 58 | modulus = modulus < 0 ? modulus + tickSpacing : modulus; 59 | return (modulus > tickSpacing/2) ? 60 | tick - modulus : 61 | tick + tickSpacing - modulus; 62 | } 63 | -------------------------------------------------------------------------------- /tasks/shared/utilities.ts: -------------------------------------------------------------------------------- 1 | import bn from 'bignumber.js' 2 | import {BigNumber, BigNumberish, constants, Contract, ContractTransaction, utils, Wallet} from 'ethers' 3 | 4 | export const MaxUint128 = BigNumber.from(2).pow(128).sub(1) 5 | export const MaxUint256 = BigNumber.from(2).pow(256).sub(1) 6 | 7 | export const getMinTick = (tickSpacing: number) => Math.ceil(-887272 / tickSpacing) * tickSpacing 8 | export const getMaxTick = (tickSpacing: number) => Math.floor(887272 / tickSpacing) * tickSpacing 9 | 10 | bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 }) 11 | 12 | export const getMaxLiquidityPerTick = (tickSpacing: number) => 13 | BigNumber.from(2) 14 | .pow(128) 15 | .sub(1) 16 | .div((getMaxTick(tickSpacing) - getMinTick(tickSpacing)) / tickSpacing + 1) 17 | 18 | export const MIN_SQRT_RATIO = BigNumber.from('4295128739') 19 | export const MAX_SQRT_RATIO = BigNumber.from('1461446703485210103287273052203988822378723970342') 20 | 21 | export enum FeeAmount { 22 | LOW = 500, 23 | MEDIUM = 3000, 24 | HIGH = 10000, 25 | } 26 | 27 | export const TICK_SPACINGS: { [amount in FeeAmount]: number } = { 28 | [FeeAmount.LOW]: 10, 29 | [FeeAmount.MEDIUM]: 60, 30 | [FeeAmount.HIGH]: 200, 31 | } 32 | 33 | export function encodePriceSqrt(reserve1: BigNumberish, reserve0: BigNumberish): BigNumber { 34 | return BigNumber.from( 35 | new bn(reserve1.toString()) 36 | .div(reserve0.toString()) 37 | .sqrt() 38 | .multipliedBy(new bn(2).pow(96)) 39 | .integerValue(3) 40 | .toString() 41 | ) 42 | } 43 | 44 | export function getPositionKey(address: string, lowerTick: number, upperTick: number): string { 45 | return utils.keccak256(utils.solidityPack(['address', 'int24', 'int24'], [address, lowerTick, upperTick])) 46 | } 47 | -------------------------------------------------------------------------------- /tasks/swap.ts: -------------------------------------------------------------------------------- 1 | import { formatEther} from 'ethers/lib/utils' 2 | import { task } from 'hardhat/config' 3 | import { deployContract } from './utils' 4 | 5 | task('deploy-swap', 'Deploy Swap contract') 6 | .addParam('owner', "your address") 7 | .addParam('token', "visr address") 8 | .setAction(async (cliArgs, { ethers, run, network }) => { 9 | // compile 10 | 11 | await run('compile') 12 | 13 | // get signer 14 | 15 | const signer = (await ethers.getSigners())[0] 16 | console.log('Signer') 17 | console.log(' at', signer.address) 18 | console.log(' ETH', formatEther(await signer.getBalance())) 19 | 20 | const _owner = ethers.utils.getAddress(cliArgs.owner); 21 | const _VISR = ethers.utils.getAddress(cliArgs.token); 22 | 23 | // TODO cli args 24 | // goerli 25 | const args = { 26 | _owner, 27 | _router: "0xE592427A0AEce92De3Edee1F18E0157C05861564", 28 | _VISR, 29 | }; 30 | 31 | console.log('Network') 32 | console.log(' ', network.name) 33 | console.log('Task Args') 34 | console.log(args) 35 | 36 | const swap = await deployContract( 37 | 'Swap', 38 | await ethers.getContractFactory('Swap'), 39 | signer, 40 | Object.values(args) 41 | ) 42 | 43 | await swap.deployTransaction.wait(5) 44 | await run('verify:verify', { 45 | address: swap.address, 46 | constructorArguments: Object.values(args), 47 | }) 48 | 49 | }); 50 | 51 | task('run-swap', 'Run Swap contract swap function') 52 | .addParam('token', 'token which to swap for VISR') 53 | .addParam('path', 'path to use') 54 | .addOptionalParam('send', 'flag for sending recipient or not') 55 | .setAction(async (cliArgs, { ethers, run, network }) => { 56 | // compile 57 | 58 | const signer = (await ethers.getSigners())[0] 59 | const swapAddress = "0x92f8964e7e261f872Cf4AAE01C7d845333aeB4C7"; 60 | 61 | const swap = await ethers.getContractAt( 62 | 'Swap', 63 | swapAddress, 64 | signer, 65 | ) 66 | 67 | const _token = ethers.utils.getAddress(cliArgs.token); 68 | const _path = cliArgs.path; 69 | const _send = cliArgs.send == 'false' ? false : true; 70 | 71 | await swap.swap(_token, _path, _send); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /tasks/utils.ts: -------------------------------------------------------------------------------- 1 | import { TypedDataField } from '@ethersproject/abstract-signer' 2 | import { BigNumberish, Contract, ContractFactory, Signer, Wallet } from 'ethers' 3 | import { splitSignature } from 'ethers/lib/utils' 4 | 5 | export async function deployContract( 6 | name: string, 7 | factory: ContractFactory, 8 | signer: Signer, 9 | args: Array = [], 10 | ): Promise { 11 | const contract = await factory.connect(signer).deploy(...args) 12 | console.log('Deploying', name) 13 | console.log(' to', contract.address) 14 | console.log(' in', contract.deployTransaction.hash) 15 | return contract.deployed() 16 | } 17 | 18 | export const signPermission = async ( 19 | method: string, 20 | vault: Contract, 21 | owner: Wallet, 22 | delegateAddress: string, 23 | tokenAddress: string, 24 | amount: BigNumberish, 25 | vaultNonce: BigNumberish, 26 | chainId?: BigNumberish, 27 | ) => { 28 | // get chainId 29 | chainId = chainId || (await vault.provider.getNetwork()).chainId 30 | // craft permission 31 | const domain = { 32 | name: 'UniversalVault', 33 | version: '1.0.0', 34 | chainId: chainId, 35 | verifyingContract: vault.address, 36 | } 37 | const types = {} as Record 38 | types[method] = [ 39 | { name: 'delegate', type: 'address' }, 40 | { name: 'token', type: 'address' }, 41 | { name: 'amount', type: 'uint256' }, 42 | { name: 'nonce', type: 'uint256' }, 43 | ] 44 | const value = { 45 | delegate: delegateAddress, 46 | token: tokenAddress, 47 | amount: amount, 48 | nonce: vaultNonce, 49 | } 50 | // sign permission 51 | // todo: add fallback if wallet does not support eip 712 rpc 52 | const signedPermission = await owner._signTypedData(domain, types, value) 53 | // return 54 | return signedPermission 55 | } 56 | 57 | export const signPermitEIP2612 = async ( 58 | owner: Wallet, 59 | token: Contract, 60 | spenderAddress: string, 61 | value: BigNumberish, 62 | deadline: BigNumberish, 63 | nonce?: BigNumberish, 64 | ) => { 65 | // get nonce 66 | nonce = nonce || (await token.nonces(owner.address)) 67 | // get chainId 68 | const chainId = (await token.provider.getNetwork()).chainId 69 | // get domain 70 | const domain = { 71 | name: 'Uniswap V2', 72 | version: '1', 73 | chainId: chainId, 74 | verifyingContract: token.address, 75 | } 76 | // get types 77 | const types = {} as Record 78 | types['Permit'] = [ 79 | { name: 'owner', type: 'address' }, 80 | { name: 'spender', type: 'address' }, 81 | { name: 'value', type: 'uint256' }, 82 | { name: 'nonce', type: 'uint256' }, 83 | { name: 'deadline', type: 'uint256' }, 84 | ] 85 | // get values 86 | const values = { 87 | owner: owner.address, 88 | spender: spenderAddress, 89 | value: value, 90 | nonce: nonce, 91 | deadline: deadline, 92 | } 93 | // sign permission 94 | // todo: add fallback if wallet does not support eip 712 rpc 95 | const signedPermission = await owner._signTypedData(domain, types, values) 96 | // split signature 97 | const sig = splitSignature(signedPermission) 98 | // return 99 | return [ 100 | values.owner, 101 | values.spender, 102 | values.value, 103 | values.deadline, 104 | sig.v, 105 | sig.r, 106 | sig.s, 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /test/shared/ethUtils.ts: -------------------------------------------------------------------------------- 1 | import * as hre from "hardhat"; 2 | 3 | const mineBlockNumber = async (blockNumber: number) => { 4 | return rpc({ method: "evm_mineBlockNumber", params: [blockNumber] }); 5 | }; 6 | 7 | const mineBlock = async () => { 8 | return rpc({ method: "evm_mine" }); 9 | }; 10 | 11 | const increaseTime: (seconds: number) => Promise = async (seconds) => { 12 | await rpc({ method: "evm_increaseTime", params: [seconds] }); 13 | return rpc({ method: "evm_mine" }); 14 | }; 15 | 16 | // doesn't work with hardhat 17 | const setTime = async (seconds: number) => { 18 | await rpc({ method: "evm_setTime", params: [new Date(seconds * 1000)] }); 19 | }; 20 | 21 | // doesn't work with hardhat 22 | const freezeTime = async (seconds: number) => { 23 | await rpc({ method: "evm_freezeTime", params: [seconds] }); 24 | return rpc({ method: "evm_mine" }); 25 | }; 26 | 27 | // adapted for both truffle and hardhat 28 | const advanceBlocks = async (blocks: number) => { 29 | let currentBlockNumber = await blockNumber(); 30 | for (let i = currentBlockNumber; i < blocks; i++) { 31 | await mineBlock(); 32 | } 33 | }; 34 | 35 | const setNextBlockTimestamp = async (timestamp: number) => { 36 | await rpc({ method: "evm_setNextBlockTimestamp", params: [timestamp] }); 37 | }; 38 | 39 | const blockNumber = async () => { 40 | let { result: num }: any = await rpc({ method: "eth_blockNumber" }); 41 | if (num === undefined) num = await rpc({ method: "eth_blockNumber" }); 42 | return parseInt(num); 43 | }; 44 | 45 | const lastBlock = async () => { 46 | return await rpc({ 47 | method: "eth_getBlockByNumber", 48 | params: ["latest", true], 49 | }); 50 | }; 51 | 52 | // doesn't work with hardhat 53 | const minerStart = async () => { 54 | return rpc({ method: "miner_start" }); 55 | }; 56 | 57 | // doesn't work with hardhat 58 | const minerStop = async () => { 59 | return rpc({ method: "miner_stop" }); 60 | }; 61 | 62 | // adapted to work in both truffle and hardhat 63 | const rpc = async (request: any) => { 64 | try { 65 | return await hre.network.provider.request(request); 66 | } catch (e) { 67 | if (typeof hre.network != "undefined") console.error(e); 68 | } 69 | }; 70 | 71 | export { 72 | advanceBlocks, 73 | blockNumber, 74 | lastBlock, 75 | freezeTime, 76 | increaseTime, 77 | mineBlock, 78 | mineBlockNumber, 79 | minerStart, 80 | minerStop, 81 | rpc, 82 | setTime, 83 | setNextBlockTimestamp, 84 | }; 85 | -------------------------------------------------------------------------------- /test/shared/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import { ethers } from 'hardhat' 3 | 4 | import { 5 | TestERC20, 6 | IUniswapV3Factory, 7 | ISwapRouter, 8 | HypervisorFactory, 9 | MockUniswapV3PoolDeployer, 10 | TokeHypervisorFactory 11 | } from "../../typechain"; 12 | 13 | import { Fixture } from 'ethereum-waffle' 14 | 15 | interface UniswapV3Fixture { 16 | factory: IUniswapV3Factory 17 | router: ISwapRouter 18 | } 19 | 20 | async function uniswapV3Fixture(): Promise { 21 | const factoryFactory = await ethers.getContractFactory('UniswapV3Factory') 22 | const factory = (await factoryFactory.deploy()) as IUniswapV3Factory 23 | 24 | const tokenFactory = await ethers.getContractFactory('TestERC20') 25 | const WETH = (await tokenFactory.deploy(BigNumber.from(2).pow(255))) as TestERC20 // TODO: change to real WETH 26 | 27 | const routerFactory = await ethers.getContractFactory('SwapRouter') 28 | const router = (await routerFactory.deploy(factory.address, WETH.address)) as ISwapRouter 29 | return { factory, router } 30 | } 31 | 32 | 33 | interface TokensFixture { 34 | token0: TestERC20 35 | token1: TestERC20 36 | token2: TestERC20 37 | } 38 | 39 | async function tokensFixture(): Promise { 40 | const tokenFactory = await ethers.getContractFactory('TestERC20') 41 | const tokenA = (await tokenFactory.deploy(BigNumber.from(2).pow(255))) as TestERC20 42 | const tokenB = (await tokenFactory.deploy(BigNumber.from(2).pow(255))) as TestERC20 43 | const tokenC = (await tokenFactory.deploy(BigNumber.from(2).pow(255))) as TestERC20 44 | 45 | const [token0, token1, token2] = [tokenA, tokenB, tokenC].sort((tokenA, tokenB) => 46 | tokenA.address.toLowerCase() < tokenB.address.toLowerCase() ? -1 : 1 47 | ) 48 | 49 | return { token0, token1, token2 } 50 | } 51 | 52 | interface HypervisorFactoryFixture { 53 | hypervisorFactory: HypervisorFactory 54 | } 55 | 56 | async function hypervisorFactoryFixture(factory: IUniswapV3Factory): Promise { 57 | const hypervisorFactoryFactory = await ethers.getContractFactory('HypervisorFactory') 58 | const hypervisorFactory = (await hypervisorFactoryFactory.deploy(factory.address)) as HypervisorFactory 59 | return { hypervisorFactory } 60 | } 61 | interface TokeHypervisorFactoryFixture { 62 | tokeHypervisorFactory: TokeHypervisorFactory 63 | } 64 | 65 | async function tokeHypervisorFactoryFixture(factory: IUniswapV3Factory): Promise { 66 | const tokeHypervisorFactoryFactory = await ethers.getContractFactory('TokeHypervisorFactory') 67 | const tokeHypervisorFactory = (await tokeHypervisorFactoryFactory.deploy(factory.address)) as TokeHypervisorFactory 68 | return { tokeHypervisorFactory } 69 | } 70 | 71 | 72 | interface OurFactoryFixture { 73 | ourFactory: MockUniswapV3PoolDeployer 74 | } 75 | 76 | async function ourFactoryFixture(): Promise { 77 | const ourFactoryFactory = await ethers.getContractFactory('MockUniswapV3PoolDeployer') 78 | const ourFactory = (await ourFactoryFactory.deploy()) as MockUniswapV3PoolDeployer 79 | return { ourFactory } 80 | } 81 | 82 | type allContractsFixture = UniswapV3Fixture & TokensFixture & OurFactoryFixture 83 | 84 | export const fixture: Fixture = async function (): Promise { 85 | const { factory, router } = await uniswapV3Fixture() 86 | const { token0, token1, token2 } = await tokensFixture() 87 | const { ourFactory } = await ourFactoryFixture() 88 | 89 | return { 90 | token0, 91 | token1, 92 | token2, 93 | factory, 94 | router, 95 | ourFactory, 96 | } 97 | } 98 | 99 | type HypervisorTestFixture = UniswapV3Fixture & TokensFixture & HypervisorFactoryFixture 100 | 101 | export const hypervisorTestFixture: Fixture = async function (): Promise { 102 | const { factory, router } = await uniswapV3Fixture() 103 | const { token0, token1, token2 } = await tokensFixture() 104 | const { hypervisorFactory } = await hypervisorFactoryFixture(factory) 105 | 106 | return { 107 | token0, 108 | token1, 109 | token2, 110 | factory, 111 | router, 112 | hypervisorFactory, 113 | } 114 | } 115 | 116 | type TokeHypervisorTestFixture = UniswapV3Fixture & TokensFixture & TokeHypervisorFactoryFixture 117 | 118 | export const tokeHypervisorTestFixture: Fixture = async function (): Promise { 119 | const { factory, router } = await uniswapV3Fixture() 120 | const { token0, token1, token2 } = await tokensFixture() 121 | const { tokeHypervisorFactory } = await tokeHypervisorFactoryFixture(factory) 122 | 123 | return { 124 | token0, 125 | token1, 126 | token2, 127 | factory, 128 | router, 129 | tokeHypervisorFactory, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/shared/utilities.ts: -------------------------------------------------------------------------------- 1 | import bn from 'bignumber.js' 2 | import {BigNumber, BigNumberish, constants, Contract, ContractTransaction, utils, Wallet} from 'ethers' 3 | 4 | export const MaxUint128 = BigNumber.from(2).pow(128).sub(1) 5 | 6 | export const getMinTick = (tickSpacing: number) => Math.ceil(-887272 / tickSpacing) * tickSpacing 7 | export const getMaxTick = (tickSpacing: number) => Math.floor(887272 / tickSpacing) * tickSpacing 8 | 9 | bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 }) 10 | 11 | export const getMaxLiquidityPerTick = (tickSpacing: number) => 12 | BigNumber.from(2) 13 | .pow(128) 14 | .sub(1) 15 | .div((getMaxTick(tickSpacing) - getMinTick(tickSpacing)) / tickSpacing + 1) 16 | 17 | export const MIN_SQRT_RATIO = BigNumber.from('4295128739') 18 | export const MAX_SQRT_RATIO = BigNumber.from('1461446703485210103287273052203988822378723970342') 19 | 20 | export enum FeeAmount { 21 | LOW = 500, 22 | MEDIUM = 3000, 23 | HIGH = 10000, 24 | } 25 | 26 | export const TICK_SPACINGS: { [amount in FeeAmount]: number } = { 27 | [FeeAmount.LOW]: 10, 28 | [FeeAmount.MEDIUM]: 60, 29 | [FeeAmount.HIGH]: 200, 30 | } 31 | 32 | export function encodePriceSqrt(reserve1: BigNumberish, reserve0: BigNumberish): BigNumber { 33 | return BigNumber.from( 34 | new bn(reserve1.toString()) 35 | .div(reserve0.toString()) 36 | .sqrt() 37 | .multipliedBy(new bn(2).pow(96)) 38 | .integerValue(3) 39 | .toString() 40 | ) 41 | } 42 | 43 | export function getPositionKey(address: string, lowerTick: number, upperTick: number): string { 44 | return utils.keccak256(utils.solidityPack(['address', 'int24', 'int24'], [address, lowerTick, upperTick])) 45 | } 46 | -------------------------------------------------------------------------------- /test/tokemak.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, waffle } from 'hardhat' 2 | import { BigNumber, BigNumberish, constants } from 'ethers' 3 | import chai from 'chai' 4 | import { expect } from 'chai' 5 | import { fixture, tokeHypervisorTestFixture } from "./shared/fixtures" 6 | import { solidity } from "ethereum-waffle" 7 | 8 | chai.use(solidity) 9 | 10 | import { 11 | FeeAmount, 12 | TICK_SPACINGS, 13 | encodePriceSqrt, 14 | getPositionKey, 15 | getMinTick, 16 | getMaxTick 17 | } from './shared/utilities' 18 | 19 | import { 20 | ISwapRouter, 21 | IUniswapV3Factory, 22 | IUniswapV3Pool, 23 | TokeHypervisorFactory, 24 | TokeHypervisor, 25 | TestERC20 26 | } from "../typechain" 27 | 28 | const createFixtureLoader = waffle.createFixtureLoader 29 | 30 | describe('Tokemak', () => { 31 | const [wallet, alice, manager, carol, other, 32 | user0, user1, user2, user3, user4] = waffle.provider.getWallets() 33 | 34 | const minSqrtPrice = 4295128740; 35 | const maxSqrtPrice = 1461446703485210103287273052203988822378723970341; 36 | 37 | let factory: IUniswapV3Factory 38 | let router: ISwapRouter 39 | let token0: TestERC20 40 | let token1: TestERC20 41 | let token2: TestERC20 42 | let uniswapPool: IUniswapV3Pool 43 | let tokeHypervisorFactory: TokeHypervisorFactory 44 | let tokeHypervisor: TokeHypervisor 45 | 46 | let loadFixture: ReturnType 47 | before('create fixture loader', async () => { 48 | loadFixture = createFixtureLoader([wallet, other]) 49 | }) 50 | 51 | beforeEach('deploy contracts', async () => { 52 | ({ token0, token1, token2, factory, router, tokeHypervisorFactory } = await loadFixture(tokeHypervisorTestFixture)) 53 | await tokeHypervisorFactory.createHypervisor(token0.address, token1.address, FeeAmount.MEDIUM,"Test Visor", "TVR"); 54 | const tokeHypervisorAddress = await tokeHypervisorFactory.getHypervisor(token0.address, token1.address, FeeAmount.MEDIUM) 55 | tokeHypervisor = (await ethers.getContractAt('TokeHypervisor', tokeHypervisorAddress)) as TokeHypervisor 56 | const poolAddress = await factory.getPool(token0.address, token1.address, FeeAmount.MEDIUM) 57 | uniswapPool = (await ethers.getContractAt('IUniswapV3Pool', poolAddress)) as IUniswapV3Pool 58 | await uniswapPool.initialize(encodePriceSqrt('1', '1')) 59 | }) 60 | 61 | it('tokemak deposit & withdraw', async () => { 62 | 63 | // deploy GammaController 64 | let gammaControllerFactory = await ethers.getContractFactory('GammaController') 65 | let gammaController = await (gammaControllerFactory.connect(manager).deploy( 66 | manager.address, manager.address, tokeHypervisorFactory.address 67 | )) 68 | 69 | await tokeHypervisor.setWhitelist(gammaController.address); 70 | await token0.mint(manager.address, ethers.utils.parseEther('1000000')) 71 | await token1.mint(manager.address, ethers.utils.parseEther('1000000')) 72 | 73 | await token0.connect(manager).approve(tokeHypervisor.address, ethers.utils.parseEther('1000000')) 74 | await token1.connect(manager).approve(tokeHypervisor.address, ethers.utils.parseEther('1000000')) 75 | 76 | let liqBalance = await tokeHypervisor.balanceOf(manager.address) 77 | let amount0 = await token0.balanceOf(manager.address) 78 | let amount1 = await token0.balanceOf(manager.address) 79 | expect(liqBalance).to.equal(0) 80 | console.log("Before Deposit: " + ethers.utils.formatEther(liqBalance)) 81 | console.log("Amount 0: " + ethers.utils.formatEther(amount0)) 82 | console.log("Amount 1: " + ethers.utils.formatEther(amount1)) 83 | 84 | // alice may deposit from manager to recieve LP tokens 85 | await expect(tokeHypervisor.connect(alice).deposit(ethers.utils.parseEther('1000'), ethers.utils.parseEther('1000'), alice.address, manager.address, [0, 0, 0, 0])).to.be.revertedWith("WHE") 86 | 87 | // deposit 88 | await gammaController.connect(manager).deploy( 89 | ethers.utils.parseEther('1000'), 90 | ethers.utils.parseEther('1000'), 91 | token0.address, 92 | token1.address, 93 | FeeAmount.MEDIUM, 94 | 0, 95 | [0, 0, 0, 0] 96 | ) 97 | 98 | liqBalance = await tokeHypervisor.balanceOf(manager.address) 99 | amount0 = await token0.balanceOf(manager.address) 100 | amount1 = await token0.balanceOf(manager.address) 101 | expect(liqBalance).to.equal(ethers.utils.parseEther('2000')) 102 | console.log("After Deposit: " + ethers.utils.formatEther(liqBalance)) 103 | console.log("Amount 0: " + ethers.utils.formatEther(amount0)) 104 | console.log("Amount 1: " + ethers.utils.formatEther(amount1)) 105 | 106 | // alice may not withdraw from manager to recieve token0, token1 107 | await expect(tokeHypervisor.connect(alice).withdraw(liqBalance, alice.address, manager.address, [0, 0, 0, 0])).to.be.revertedWith("WHE"); 108 | 109 | // withdraw 110 | await gammaController.connect(manager).withdraw( 111 | token0.address, 112 | token1.address, 113 | FeeAmount.MEDIUM, 114 | liqBalance, 115 | [0, 0, 0, 0], 116 | ) 117 | 118 | liqBalance = await tokeHypervisor.balanceOf(manager.address) 119 | amount0 = await token0.balanceOf(manager.address) 120 | amount1 = await token0.balanceOf(manager.address) 121 | expect(liqBalance).to.equal(0) 122 | console.log("After Withdraw: " + ethers.utils.formatEther(liqBalance)) 123 | console.log("Amount 0: " + ethers.utils.formatEther(amount0)) 124 | console.log("Amount 1: " + ethers.utils.formatEther(amount1)) 125 | }); 126 | }) 127 | 128 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "typeRoots": ["./typechain", "./node_modules/@types"], 9 | "types": ["@nomiclabs/hardhat-ethers", "@nomiclabs/hardhat-waffle"] 10 | }, 11 | "include": ["./scripts", "./test"], 12 | "files": ["./hardhat.config.ts"] 13 | } 14 | --------------------------------------------------------------------------------