harb/onchain/src/BaseLineLP.sol

324 lines
14 KiB
Solidity
Raw Normal View History

// SPDX-License-Identifier: GPL-3.0-or-later
2024-03-28 19:55:01 +01:00
pragma solidity ^0.8.19;
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
import "@uniswap-v3-core/libraries/FixedPoint128.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import "@aperture/uni-v3-lib/PoolAddress.sol";
import "@aperture/uni-v3-lib/CallbackValidation.sol";
import "@openzeppelin/token/ERC20/IERC20.sol";
2024-03-28 19:55:01 +01:00
import {ABDKMath64x64} from "abdk/ABDKMath64x64.sol";
import "./interfaces/IWETH9.sol";
import {Harb} from "./Harb.sol";
/**
* @title LiquidityManager - A contract that supports the harb ecosystem. It
* protects the communities liquidity while allowing a manager role to
* take strategic liqudity positions.
*/
contract BaseLineLP {
2024-03-28 19:55:01 +01:00
int24 constant TICK_SPACING = 200;
// default fee of 1%
uint24 constant FEE = uint24(10_000);
2024-03-28 19:55:01 +01:00
// uint256 constant FLOOR = 0;
// uint256 constant ANCHOR = 1;
// uint256 constant DISCOVERY = 2;
enum Stage { FLOOR, ANCHOR, DISCOVERY }
uint256 constant LIQUIDITY_RATIO_DIVISOR = 100;
// the address of the Uniswap V3 factory
address immutable factory;
IWETH9 immutable weth;
Harb immutable harb;
IUniswapV3Pool immutable pool;
PoolKey private poolKey;
bool immutable token0isWeth;
struct TokenPosition {
// the liquidity of the position
uint128 liquidity;
int24 tickLower;
int24 tickUpper;
// the fee growth of the aggregate position as of the last action on the individual position
uint256 feeGrowthInside0LastX128;
uint256 feeGrowthInside1LastX128;
}
2024-03-28 19:55:01 +01:00
mapping(uint256 => TokenPosition) positions;
modifier checkDeadline(uint256 deadline) {
require(block.timestamp <= deadline, "Transaction too old");
_;
}
/// @notice Emitted when liquidity is increased for a position
/// @param liquidity The amount by which liquidity for the NFT position was increased
/// @param amount0 The amount of token0 that was paid for the increase in liquidity
/// @param amount1 The amount of token1 that was paid for the increase in liquidity
event IncreaseLiquidity(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 amount0, uint256 amount1);
/// @notice Emitted when liquidity is decreased for a position
/// @param liquidity The amount by which liquidity for the NFT position was decreased
/// @param ethReceived The amount of WETH that was accounted for the decrease in liquidity
event PositionLiquidated(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 ethReceived);
constructor(address _factory, address _WETH9, address _harb) {
factory = _factory;
weth = IWETH9(_WETH9);
poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
harb = Harb(_harb);
token0isWeth = _WETH9 < _harb;
}
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
CallbackValidation.verifyCallback(factory, poolKey);
2024-03-28 19:55:01 +01:00
// ## mint harb if needed
if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed);
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
}
2024-03-28 19:55:01 +01:00
function createPosition(uint256 positionIndex, int24 tickLower, int24 tickUpper, uint256 amount) internal {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1(
2024-03-28 19:55:01 +01:00
sqrtRatioAX96, sqrtRatioBX96, amount
);
2024-03-28 19:55:01 +01:00
(uint256 amount0, uint256 amount1) = pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
// TODO: check slippage
// read position and start tracking in storage
bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper);
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
2024-03-28 19:55:01 +01:00
positions[positionIndex] = TokenPosition({
liquidity: liquidity,
tickLower: tickLower,
tickUpper: tickUpper,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128
});
}
// called once at the beginning
function deployLiquidity(int24 startTick, uint256 amount) external {
2024-03-28 19:55:01 +01:00
require(positions[Stage.FLOOR].liquidity == 0, "already set up");
require(positions[Stage.ANCHOR].liquidity == 0, "already set up");
require(positions[Stage.DISCOVERY].liquidity == 0, "already set up");
harb.mint(amount);
int24 tickLower;
int24 tickUpper;
// create floor
if (token0isWeth) {
tickLower = startTick;
tickUpper = startTick + 200;
createPosition(tickLower, tickUpper, amount / 10);
} else {
tickLower = startTick - 200;
tickUpper = startTick;
createPosition(tickLower, tickUpper, amount / 10);
}
// create anchor
if (token0isWeth) {
tickLower += 201;
tickUpper += 601;
createPosition(tickLower, tickUpper, amount / 20);
} else {
tickLower -= 601;
tickUpper -= 201;
createPosition(tickLower, tickUpper, amount / 10);
}
// create discovery
if (token0isWeth) {
tickLower += 601;
tickUpper += 11001;
createPosition(tickLower, tickUpper, harb.balanceOf(address(this)));
} else {
tickLower -= 11001;
tickUpper -= 601;
createPosition(tickLower, tickUpper, harb.balanceOf(address(this)));
}
}
2024-03-28 19:55:01 +01:00
function outstanding() public view returns (uint256 _outstanding) {
harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this));
}
2024-03-28 19:55:01 +01:00
function ethInAnchor() public view returns (uint256 _ethInAnchor) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickUpper);
if (token0isWeth) {
_ethInAnchor = LiquidityAmounts.getAmount0ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity
);
} else {
2024-03-28 19:55:01 +01:00
_ethInAnchor = LiquidityAmounts.getAmount1ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity
);
}
}
2024-03-28 19:55:01 +01:00
/// @dev Calculates cycle tick from cycle price.
function tickAtPrice(uint256 price) internal view returns (int24 tick_) {
tick_ = TickMath.getTickAtSqrtRatio(
uint160(int160(ABDKMath64x64.sqrt(int128(int256(price << 64))) << 32))
);
tick_ = tick_ / TICK_SPACING * TICK_SPACING;
tick_ = token0isWeth ? tick_ : -tick_;
}
// call this function when price has moved up 15%
function shift() external {
// Fetch the current tick from the Uniswap V3 pool
(uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0();
// TODO: check slippage with oracle
// ## check price moved up
{
// Check if current tick is within the specified range
int24 anchorTickLower = anchor.tickLower;
int24 anchorTickUpper = anchor.tickUpper;
// center tick can be calculated positive and negative numbers the same
int24 centerTick = anchorTickLower + ((anchorTickUpper - anchorTickLower) / 2);
int24 amplitudeTick = anchorTickLower + (anchorTickUpper - anchorTickLower) * 3 / 20;
if (token0isWeth) {
require(currentTick > centerTick, "call slide(), not shift()");
// ### check price moved more than 15% of range
require(currentTick > amplitudeTick, "amplitude not reached, come back later");
} else {
// ### check price moved up enough and add hysteresis
require(currentTick < centerTick, "call slide(), not shift()");
// ### check price moved more than 15% of range
require(currentTick < amplitudeTick, "amplitude not reached, come back later");
}
}
// ## scrape positions
uint256 ethAmountAnchor;
for (uint256 i=Stage.FLOOR; i <= Stage.DISCOVERY; i++) {
position = positions[i];
(uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);
if (i == ANCHOR) {
ethAmountAnchor = token0isWeth ? amount0 : amount1;
}
}
// TODO: handle fees
// ## set new positions
// move 10% of new ETH into Floor
ethAmountAnchor -= (ethAmountAnchor - ethInAnchor()) * 10 / LIQUIDITY_RATIO_DIVISOR;
// ### set Floor position
uint256 anchorLiquidity;
{
2024-03-28 19:55:01 +01:00
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick - 300);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 300);
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor
);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor
);
}
(uint256 amount0, uint256 amount1) = pool.mint(address(this), currentTick - 300, currentTick + 300, liquidity, abi.encode(poolKey));
bytes32 positionKey = PositionKey.compute(address(this), currentTick - 300, currentTick + 300);
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
positions[Stage.ANCHOR] = TokenPosition({
liquidity: anchorLiquidity,
tickLower: currentTick - 300,
tickUpper: currentTick + 300,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128
});
}
2024-03-28 19:55:01 +01:00
// ### set Floor position
{
2024-03-28 19:55:01 +01:00
uint256 ethAmountFloor = address(this).balance;
int24 floorTick = tickAtPrice(outstanding() / ethAmountFloor);
floorTick -= (currentTick - floorTick);
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - 300);
uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
token0isWeth ? ethAmountFloor : 0,
token0isWeth ? 0: ethAmountFloor
);
pool.mint(
address(this),
floorTick,
currentTick - 300,
liquidity,
abi.encode(poolKey)
);
bytes32 positionKey = PositionKey.compute(address(this), floorTick, currentTick - 300);
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
positions[Stage.FLOOR] = TokenPosition({
liquidity: liquidity,
tickLower: floorTick,
tickUpper: currentTick - 300,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128
});
}
2024-03-28 19:55:01 +01:00
// ##set discovery position
{
2024-03-28 19:55:01 +01:00
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick + 300);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 11300);
// discovery with 1.5 times as much liquidity per tick as anchor
uint256 liquidity = anchorLiquidity * 55 / 2;
uint256 harbInDiscovery;
if (token0isWeth) {
harbInDiscovery = TickMath.getAmount1ForLiquidity(
sqrtRatioAX96,
sqrtRatioBX96,
liquidity
);
} else {
harbInDiscovery = TickMath.getAmount0ForLiquidity(
sqrtRatioAX96,
sqrtRatioBX96,
liquidity
);
}
pool.mint(
address(this),
currentTick + 300,
currentTick + 11300,
liquidity,
abi.encode(poolKey)
);
bytes32 positionKey = PositionKey.compute(address(this), currentTick + 300, currentTick + 11300);
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
positions[Stage.DISCOVERY] = TokenPosition({
liquidity: liquidity,
tickLower: floorTick,
tickUpper: currentTick - 300,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128
});
}
2024-03-28 19:55:01 +01:00
}
function slide() external {
// check price moved down
// check price moved down enough
// scrape positions
// get outstanding upply and capacity
// set new positions
// burn harb
}
}