Skip to content

Instantly share code, notes, and snippets.

@kassandraoftroy
Created September 15, 2021 10:00
Show Gist options
  • Save kassandraoftroy/2f49680df5de3d52be48c449a0d51c63 to your computer and use it in GitHub Desktop.
Save kassandraoftroy/2f49680df5de3d52be48c449a0d51c63 to your computer and use it in GitHub Desktop.
G-UNI automated strategy in solidity
// 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