harb/onchain/src/BaseLineLP.sol

435 lines
18 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 21:50:22 +01:00
import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol";
import "./interfaces/IWETH9.sol";
import {Harb} from "./Harb.sol";
2024-03-28 21:50:22 +01:00
/**
* @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-04-06 07:32:55 +02:00
// for minting limits
uint256 private lastDay;
uint256 private mintedToday;
2024-03-28 21:50:22 +01:00
mapping(Stage => 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 21:50:22 +01:00
function createPosition(Stage 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 21:50:22 +01:00
pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
2024-03-28 19:55:01 +01:00
// 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;
2024-03-28 21:50:22 +01:00
createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10);
} else {
tickLower = startTick - 200;
tickUpper = startTick;
2024-03-28 21:50:22 +01:00
createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10);
}
// create anchor
if (token0isWeth) {
tickLower += 201;
tickUpper += 601;
2024-03-28 21:50:22 +01:00
createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 20);
} else {
tickLower -= 601;
tickUpper -= 201;
2024-03-28 21:50:22 +01:00
createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 10);
}
// create discovery
if (token0isWeth) {
tickLower += 601;
tickUpper += 11001;
2024-03-28 21:50:22 +01:00
createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this)));
} else {
tickLower -= 11001;
tickUpper -= 601;
2024-03-28 21:50:22 +01:00
createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this)));
}
}
2024-03-28 19:55:01 +01:00
function outstanding() public view returns (uint256 _outstanding) {
2024-03-28 21:50:22 +01:00
_outstanding = harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this));
2024-03-28 19:55:01 +01:00
}
2024-04-06 07:32:55 +02:00
function spendingLimit() public view returns (uint256, uint256) {
return (lastDay, mintedToday);
}
2024-03-28 21:50:22 +01:00
function ethIn(Stage s) public view returns (uint256 _ethInPosition) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(positions[s].tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(positions[s].tickUpper);
2024-03-28 19:55:01 +01:00
if (token0isWeth) {
2024-03-28 21:50:22 +01:00
_ethInPosition = LiquidityAmounts.getAmount0ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[s].liquidity
2024-03-28 19:55:01 +01:00
);
} else {
2024-03-28 21:50:22 +01:00
_ethInPosition = LiquidityAmounts.getAmount1ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[s].liquidity
2024-03-28 19:55:01 +01:00
);
}
}
2024-03-28 21:50:22 +01:00
function tickAtPrice(uint256 tokenAmount, uint256 ethAmount) internal view returns (int24 tick_) {
require(ethAmount > 0, "ETH amount cannot be zero");
// Use a fixed-point library or more precise arithmetic for the division here.
// For example, using ABDKMath64x64 for a more precise division and square root calculation.
int128 priceRatio = ABDKMath64x64.div(
int128(int256(tokenAmount)),
int128(int256(ethAmount))
);
// Convert the price ratio into a sqrt price in the format expected by Uniswap's TickMath.
uint160 sqrtPriceX96 = uint160(
int160(ABDKMath64x64.sqrt(priceRatio) << 32)
2024-03-28 19:55:01 +01:00
);
2024-03-28 21:50:22 +01:00
// Proceed as before.
tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
2024-03-28 19:55:01 +01:00
tick_ = tick_ / TICK_SPACING * TICK_SPACING;
tick_ = token0isWeth ? tick_ : -tick_;
}
2024-03-28 21:50:22 +01:00
function _mint(int24 tickLower, int24 tickUpper, uint128 liquidity) internal {
// create position
pool.mint(
address(this),
tickLower,
tickUpper,
liquidity,
abi.encode(poolKey)
);
// get fee data
bytes32 positionKey = PositionKey.compute(
address(this),
tickLower,
tickUpper
);
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
// put into storage
positions[Stage.ANCHOR] = TokenPosition({
liquidity: liquidity,
tickLower: tickLower,
tickUpper: tickUpper,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128
});
}
2024-04-06 07:32:55 +02:00
/// @dev Returns if amount is within daily limit and resets spentToday after one day.
/// @param amount Amount to withdraw.
/// @return Returns if amount is under daily limit.
function availableMint(uint256 amount) internal returns (uint256) {
if (block.timestamp > lastDay + 24 hours) {
lastDay = block.timestamp;
mintedToday = 0;
}
uint256 mintLimit = harb.totalSupply() * 3 / 20;
if (mintedToday + amount > mintLimit) {
return mintLimit - mintedToday;
}
return amount;
}
2024-04-03 21:43:12 +02:00
function _set(uint160 sqrtPriceX96, int24 currentTick, uint256 ethInNewAnchor) internal {
2024-03-28 21:50:22 +01:00
// ### set Anchor position
uint128 anchorLiquidity;
{
2024-03-28 21:50:22 +01:00
int24 tickLower = currentTick - 300;
int24 tickUpper = currentTick + 300;
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
2024-03-28 19:55:01 +01:00
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
2024-04-03 21:43:12 +02:00
sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor
2024-03-28 19:55:01 +01:00
);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
2024-04-03 21:43:12 +02:00
sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor
2024-03-28 19:55:01 +01:00
);
}
2024-03-28 21:50:22 +01:00
_mint(tickLower, tickUpper, anchorLiquidity);
}
2024-03-28 19:55:01 +01:00
// ### set Floor position
{
2024-03-28 21:50:22 +01:00
int24 startTick = token0isWeth ? currentTick - 301 : currentTick + 301;
// all remaining eth will be put into this position
uint256 ethInFloor = address(this).balance;
// calculate price at which all HARB can be bought back
int24 floorTick = tickAtPrice(outstanding(), ethInFloor);
// put a position symetrically around the price, startTick being edge on one side
floorTick = token0isWeth ? floorTick - (startTick - floorTick) : startTick + (floorTick - startTick);
// calculate liquidity
2024-03-28 19:55:01 +01:00
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick);
2024-03-28 21:50:22 +01:00
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(startTick);
uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
2024-03-28 19:55:01 +01:00
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
2024-03-28 21:50:22 +01:00
token0isWeth ? ethInFloor : 0,
token0isWeth ? 0: ethInFloor
2024-03-28 19:55:01 +01:00
);
2024-03-28 21:50:22 +01:00
// mint
_mint(floorTick, startTick, liquidity);
}
2024-03-28 21:50:22 +01:00
// ## set Discovery position
{
2024-03-28 21:50:22 +01:00
int24 tickLower = token0isWeth ? currentTick + 301 : currentTick - 11301;
int24 tickUpper = token0isWeth ? currentTick + 11301 : currentTick - 301;
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
2024-03-28 19:55:01 +01:00
// discovery with 1.5 times as much liquidity per tick as anchor
2024-03-28 21:50:22 +01:00
// A * 3 11000 A * 55
// D = ----- * ----- = ------
// 2 600 2
uint128 liquidity = anchorLiquidity * 55 / 2;
2024-03-28 19:55:01 +01:00
uint256 harbInDiscovery;
if (token0isWeth) {
2024-03-28 21:50:22 +01:00
harbInDiscovery = LiquidityAmounts.getAmount1ForLiquidity(
2024-03-28 19:55:01 +01:00
sqrtRatioAX96,
sqrtRatioBX96,
liquidity
);
} else {
2024-03-28 21:50:22 +01:00
harbInDiscovery = LiquidityAmounts.getAmount0ForLiquidity(
2024-03-28 19:55:01 +01:00
sqrtRatioAX96,
sqrtRatioBX96,
liquidity
);
}
2024-04-06 07:32:55 +02:00
// manage minting limits of harb here
if (harbInDiscovery <= harb.balanceOf(address(this))) {
_mint(tickLower, tickUpper, liquidity);
harb.burn(harb.balanceOf(address(this)));
} else {
uint256 amount = availableMint(harbInDiscovery - harb.balanceOf(address(this)));
harb.mint(amount);
mintedToday += amount;
amount = harb.balanceOf(address(this));
if(amount < harbInDiscovery) {
// calculate new ticks so that discovery liquidity is still
// deeper than anchor, but less wide
int24 tickWidth = int24(int256(11000 * amount / harbInDiscovery)) + 301;
tickLower = token0isWeth ? currentTick + 301 : currentTick - tickWidth;
tickUpper = token0isWeth ? currentTick + tickWidth : currentTick - 301;
sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
token0isWeth ? 0 : amount,
token0isWeth ? amount: 0
);
}
_mint(tickLower, tickUpper, liquidity);
}
}
2024-04-03 21:43:12 +02:00
}
// call this function when price has moved up 15%
function shift() external {
require(positions[Stage.ANCHOR].liquidity > 0, "Not initialized");
// 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 = positions[Stage.ANCHOR].tickLower;
int24 anchorTickUpper = positions[Stage.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;
// Determine the correct comparison direction based on token0isWeth
bool isUp = token0isWeth ? currentTick > centerTick : currentTick < centerTick;
bool isEnough = token0isWeth ? currentTick > amplitudeTick : currentTick < amplitudeTick;
// Check Conditions
require(isUp, "call slide(), not shift()");
require(isEnough, "amplitude not reached, come back later!");
}
// ## scrape positions
uint256 ethInAnchor;
for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
TokenPosition storage position = positions[Stage(i)];
(uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);
if (i == uint256(Stage.ANCHOR)) {
ethInAnchor = token0isWeth ? amount0 : amount1;
}
}
// TODO: handle fees
2024-03-28 19:55:01 +01:00
2024-04-03 21:43:12 +02:00
// ## set new positions
// reduce Anchor by 10% of new ETH. It will be moved into Floor
uint256 initialEthInAnchor = ethIn(Stage.ANCHOR);
ethInAnchor -= (ethInAnchor - initialEthInAnchor) * 10 / LIQUIDITY_RATIO_DIVISOR;
// cap anchor size at 10 % of total ETH
uint256 ethBalance = address(this).balance;
ethInAnchor = (ethInAnchor > ethBalance / 10) ? ethBalance / 10 : ethInAnchor;
_set(sqrtPriceX96, currentTick, ethInAnchor);
2024-03-28 19:55:01 +01:00
}
function slide() external {
2024-04-03 21:43:12 +02:00
// Fetch the current tick from the Uniswap V3 pool
(uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0();
// TODO: check slippage with oracle
// ## check price moved down
if (positions[Stage.ANCHOR].liquidity > 0) {
// Check if current tick is within the specified range
int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
int24 anchorTickUpper = positions[Stage.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;
// Determine the correct comparison direction based on token0isWeth
bool isDown = token0isWeth ? currentTick < centerTick : currentTick > centerTick;
bool isEnough = token0isWeth ? currentTick < amplitudeTick : currentTick > amplitudeTick;
// Check Conditions
require(isDown, "call shift(), not slide()");
require(isEnough, "amplitude not reached, diamond hands!");
}
// ## scrape positions
for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
TokenPosition storage position = positions[Stage(i)];
if (position.liquidity > 0) {
pool.burn(position.tickLower, position.tickUpper, position.liquidity);
// TODO: handle fees
}
}
uint256 ethBalance = address(this).balance;
if (ethBalance == 0) {
// TODO: set only discovery
return;
}
uint256 ethInAnchor = ethIn(Stage.ANCHOR);
uint256 ethInFloor = ethIn(Stage.FLOOR);
// use previous ration of Floor to Anchor
uint256 ethInNewAnchor = ethBalance * ethInAnchor / (ethInAnchor + ethInFloor);
// but cap anchor size at 10 % of total ETH
ethInNewAnchor = (ethInNewAnchor > ethBalance / 10) ? ethBalance / 10 : ethInNewAnchor;
_set(sqrtPriceX96, currentTick, ethInNewAnchor);
}
}