├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------