Created
September 15, 2021 10:00
-
-
Save kassandraoftroy/2f49680df5de3d52be48c449a0d51c63 to your computer and use it in GitHub Desktop.
G-UNI automated strategy in solidity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
pragma solidity 0.8.7; | |
import { | |
IUniswapV3Pool | |
} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; | |
import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | |
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | |
import { | |
ReentrancyGuard | |
} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | |
import {IGUniPool} from "./interfaces/IGUniPool.sol"; | |
import {IGUniStrategy} from "./interfaces/IGUniStrategy.sol"; | |
import {LiquidityAmounts} from "./vendor/uniswap/LiquidityAmounts.sol"; | |
import {TickMath} from "./vendor/uniswap/TickMath.sol"; | |
// solhint-disable-next-line max-states-count | |
contract OneSidedStrategy is IGUniStrategy, ReentrancyGuard, Ownable { | |
using TickMath for int24; | |
IGUniPool public immutable pool; | |
IUniswapV3Pool public immutable uniswapPool; | |
int24 public immutable tickSpacing; | |
uint256 public lastRebalanceBlock; | |
int24 public lastRebalanceTick; | |
uint256 public minBlockDelta; | |
uint32 public rebalanceTwapDuration; | |
uint32 public twapDuration; | |
int24 public minTickDelta; | |
int24 public bufferLength; | |
int24 public maxTwapDelta; | |
uint16 public maxLiquidityDeltaBPS; | |
bool public orientationUpper; | |
constructor( | |
IGUniPool _pool, | |
uint256 _minBlockDelta, | |
uint32 _rebalanceTwapDuration, | |
uint32 _twapDuration, | |
int24 _minTickDelta, | |
int24 _bufferLength, | |
int24 _maxTwapDelta, | |
uint16 _maxLiquidityDeltaBPS, | |
bool _orientationUpper | |
) { | |
require(_minTickDelta > 0, "must be positive"); | |
require(_bufferLength > 0, "must be positive"); | |
require(_maxTwapDelta > 0, "must be positive"); | |
require(_maxLiquidityDeltaBPS <= 10000, "BPS"); | |
require(rebalanceTwapDuration > twapDuration, "TWAPs"); | |
pool = _pool; | |
IUniswapV3Pool _uniswapPool = _pool.pool(); | |
int24 _tickSpacing = _uniswapPool.tickSpacing(); | |
require( | |
_bufferLength % _tickSpacing == 0, | |
"bufferLength must be modulo tickSpacing" | |
); | |
require(_pool.upperTick() - _pool.lowerTick() >= 2 * _bufferLength, ""); | |
uniswapPool = _uniswapPool; | |
tickSpacing = _tickSpacing; | |
(, int24 tick, , , , , ) = _uniswapPool.slot0(); | |
lastRebalanceTick = tick; | |
lastRebalanceBlock = block.number; | |
minBlockDelta = _minBlockDelta; | |
rebalanceTwapDuration = _rebalanceTwapDuration; | |
twapDuration = _twapDuration; | |
minTickDelta = _minTickDelta; | |
bufferLength = _bufferLength; | |
maxTwapDelta = _maxTwapDelta; | |
maxLiquidityDeltaBPS = _maxLiquidityDeltaBPS; | |
orientationUpper = _orientationUpper; | |
} | |
// solhint-disable-next-line function-max-lines | |
function rebalance() external override nonReentrant { | |
// basic checks | |
(uint160 sqrtRatioX96, int24 tick, , , , , ) = uniswapPool.slot0(); | |
int24 twap = _preRebalance(tick); | |
// compute new range | |
int24 lowerTick = pool.lowerTick(); | |
int24 upperTick = pool.upperTick(); | |
(int24 newLowerTick, int24 newUpperTick) = _getNewRange( | |
lowerTick, | |
upperTick, | |
tick, | |
twap | |
); | |
// store amounts invested before rebalance | |
(uint256 amount0Before, uint256 amount1Before) = _getLiquidityAmounts( | |
sqrtRatioX96, | |
lowerTick, | |
upperTick | |
); | |
// update state before external call | |
lastRebalanceTick = tick; | |
lastRebalanceBlock = block.number; | |
// rebalance without swap | |
pool.executiveRebalance(newLowerTick, newUpperTick, 0, 0, false); | |
// check amounts invested after rebalancing | |
//(sqrtRatioX96, , , , , , ) = uniswapPool.slot0(); | |
(uint256 amount0After, uint256 amount1After) = _getLiquidityAmounts( | |
sqrtRatioX96, | |
newLowerTick, | |
newUpperTick | |
); | |
require( | |
(amount0Before * (10000 - maxLiquidityDeltaBPS)) / 10000 <= | |
amount0After, | |
"too much leftover token0" | |
); | |
require( | |
(amount1Before * (10000 - maxLiquidityDeltaBPS)) / 10000 <= | |
amount1After, | |
"too much leftover token1" | |
); | |
} | |
function ejectManager(address newManager) external override onlyOwner { | |
pool.transferOwnership(newManager); | |
} | |
function setMinTickDelta(int24 _minTickDelta) external onlyOwner { | |
require(_minTickDelta > 0, "must be positive"); | |
minTickDelta = _minTickDelta; | |
} | |
function setMaxTwapDelta(int24 _maxTwapDelta) external onlyOwner { | |
require(_maxTwapDelta > 0, "must be positive"); | |
maxTwapDelta = _maxTwapDelta; | |
} | |
function setRebalanceTwapDuration(uint32 _rebalanceTwapDuration) | |
external | |
onlyOwner | |
{ | |
require(_rebalanceTwapDuration > twapDuration, "TWAPs"); | |
rebalanceTwapDuration = _rebalanceTwapDuration; | |
} | |
function setMaxLiquidityDeltaBPS(uint16 _maxLiquidityDeltaBPS) | |
external | |
onlyOwner | |
{ | |
require(_maxLiquidityDeltaBPS <= 10000, "BPS"); | |
maxLiquidityDeltaBPS = _maxLiquidityDeltaBPS; | |
} | |
function setMinBlockDelta(uint256 _minBlockDelta) external onlyOwner { | |
minBlockDelta = _minBlockDelta; | |
} | |
function setTwapDuration(uint32 _twapDuration) external onlyOwner { | |
require(rebalanceTwapDuration > _twapDuration, "TWAPs"); | |
twapDuration = _twapDuration; | |
} | |
function setBufferLength(int24 _bufferLength) external onlyOwner { | |
require(_bufferLength > 0, "must be positive"); | |
require( | |
_bufferLength % tickSpacing == 0, | |
"bufferLength must be modulo tickSpacing" | |
); | |
require(pool.upperTick() - pool.lowerTick() >= 2 * _bufferLength, ""); | |
bufferLength = _bufferLength; | |
} | |
function setOrientation(bool _orientationUpper) external onlyOwner { | |
orientationUpper = _orientationUpper; | |
} | |
function _preRebalance(int24 _tick) internal view returns (int24) { | |
require( | |
block.number - lastRebalanceBlock > minBlockDelta, | |
"rebalanced in recent block" | |
); | |
int24 tickDelta = _tick > lastRebalanceTick | |
? _tick - lastRebalanceTick | |
: lastRebalanceTick - _tick; | |
require(tickDelta > minTickDelta, "rebalanced at nearby tick"); | |
(int24 twapVolatility, int24 twap) = _getTwaps(); | |
int24 twapDelta = _tick > twapVolatility | |
? _tick - twapVolatility | |
: twapVolatility - _tick; | |
require(twapDelta <= maxTwapDelta, "price too volatile"); | |
return twap; | |
} | |
// solhint-disable-next-line function-max-lines, code-complexity | |
function _getNewRange( | |
int24 _lowerTick, | |
int24 _upperTick, | |
int24 _tick, | |
int24 _twap | |
) internal view returns (int24 newLowerTick, int24 newUpperTick) { | |
newLowerTick = _lowerTick; | |
newUpperTick = _upperTick; | |
if (orientationUpper) { | |
// if: in buffer zone | |
if (_twap < _upperTick && _twap > _upperTick - bufferLength) { | |
require( | |
_tick < _upperTick && _tick > _upperTick - bufferLength, | |
"extension tick, twap mismatch" | |
); | |
// then: extend range | |
newLowerTick = _lowerTick - bufferLength; | |
newUpperTick = _upperTick + bufferLength; | |
// if: has extended range, but returned to center | |
} else if ( | |
_twap > _lowerTick + bufferLength && | |
_twap < _upperTick - (2 * bufferLength) | |
) { | |
require( | |
_tick > _lowerTick + bufferLength && | |
_tick < _upperTick - (2 * bufferLength), | |
"contraction tick, twap mismatch" | |
); | |
// then: contract range | |
newLowerTick = _lowerTick + bufferLength; | |
newUpperTick = _upperTick - bufferLength; | |
} | |
} else { | |
// if: in buffer zone | |
if (_twap > _lowerTick && _twap < _lowerTick + bufferLength) { | |
require( | |
_tick > _lowerTick && _tick < _lowerTick + bufferLength, | |
"extension tick, twap mismatch" | |
); | |
// then: extend range | |
newLowerTick = _lowerTick - bufferLength; | |
newUpperTick = _upperTick + bufferLength; | |
// if: has extended range, but returned to center | |
} else if ( | |
_twap < _upperTick - bufferLength && | |
_twap > _lowerTick + (2 * bufferLength) | |
) { | |
require( | |
_tick < _upperTick - bufferLength && | |
_tick > _lowerTick + (2 * bufferLength), | |
"contraction tick, twap mismatch" | |
); | |
// then: contract range | |
newLowerTick = _lowerTick + bufferLength; | |
newUpperTick = _upperTick - bufferLength; | |
} | |
} | |
// if: out of range below | |
if (_twap < _lowerTick) { | |
int24 currentCeilingTick = _floor(_tick) + tickSpacing; | |
require( | |
currentCeilingTick < _lowerTick, | |
"upper range order tick, twap mismatch" | |
); | |
// then: place range order just above current price | |
newLowerTick = currentCeilingTick; | |
newUpperTick = currentCeilingTick + (2 * bufferLength); | |
} | |
// if: out of range above | |
if (_twap > _upperTick) { | |
int24 currentFloorTick = _floor(_tick); | |
require( | |
currentFloorTick > _upperTick, | |
"lower range order tick, twap mismatch" | |
); | |
// then: place range order just above current price | |
newUpperTick = currentFloorTick; | |
newLowerTick = currentFloorTick - (2 * bufferLength); | |
} | |
require( | |
newUpperTick != _upperTick || newLowerTick != _lowerTick, | |
"no need to rebalance" | |
); | |
require( | |
newUpperTick < TickMath.MAX_TICK && | |
newLowerTick > TickMath.MIN_TICK, | |
"new ticks are out of uniswap range" | |
); | |
} | |
function _getLiquidityAmounts( | |
uint160 _sqrtRatioX96, | |
int24 _lowerTick, | |
int24 _upperTick | |
) internal view returns (uint256 amount0, uint256 amount1) { | |
(uint128 liquidity, , , , ) = uniswapPool.positions( | |
pool.getPositionID() | |
); | |
(amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity( | |
_sqrtRatioX96, | |
_lowerTick.getSqrtRatioAtTick(), | |
_upperTick.getSqrtRatioAtTick(), | |
liquidity | |
); | |
} | |
function _floor(int24 _tick) internal view returns (int24 resultTick) { | |
unchecked { | |
int24 compressed = _tick / tickSpacing; | |
if (_tick < 0 && _tick % tickSpacing != 0) compressed--; | |
resultTick = compressed * tickSpacing; | |
} | |
} | |
function _getTwaps() | |
internal | |
view | |
returns (int24 twapShort, int24 twapLong) | |
{ | |
uint32 _rebalanceTwapDuration = rebalanceTwapDuration; | |
uint32 _twapDuration = twapDuration; | |
uint32[] memory secondsAgo = new uint32[](3); | |
secondsAgo[0] = _rebalanceTwapDuration; | |
secondsAgo[1] = _twapDuration; | |
secondsAgo[2] = 0; | |
(int56[] memory tickCumulatives, ) = uniswapPool.observe(secondsAgo); | |
unchecked { | |
twapShort = int24( | |
(tickCumulatives[1] - tickCumulatives[0]) / | |
int56(uint56(_twapDuration)) | |
); | |
twapLong = int24( | |
(tickCumulatives[2] - tickCumulatives[0]) / | |
int56(uint56(_rebalanceTwapDuration)) | |
); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment