basic shift implementation

This commit is contained in:
JulesCrown 2024-03-28 21:50:22 +01:00
parent c41e7bc8e1
commit ea1b007283
2 changed files with 118 additions and 103 deletions

View file

@ -1,4 +1,5 @@
@openzeppelin/=lib/openzeppelin-contracts/contracts/
@uniswap-v3-core/=lib/uni-v3-lib/node_modules/@uniswap/v3-core/contracts/
@uniswap-v3-periphery=lib/uni-v3-lib/node_modules/@uniswap/v3-periphery/contracts/
@uniswap-v3-periphery/=lib/uni-v3-lib/node_modules/@uniswap/v3-periphery/contracts/
@aperture/uni-v3-lib/=lib/uni-v3-lib/src/
@abdk/=lib/abdk-libraries-solidity/

View file

@ -9,10 +9,11 @@ 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";
import {ABDKMath64x64} from "abdk/ABDKMath64x64.sol";
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
@ -49,7 +50,7 @@ contract BaseLineLP {
uint256 feeGrowthInside1LastX128;
}
mapping(uint256 => TokenPosition) positions;
mapping(Stage => TokenPosition) positions;
modifier checkDeadline(uint256 deadline) {
require(block.timestamp <= deadline, "Transaction too old");
@ -82,13 +83,13 @@ contract BaseLineLP {
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
}
function createPosition(uint256 positionIndex, int24 tickLower, int24 tickUpper, uint256 amount) internal {
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(
sqrtRatioAX96, sqrtRatioBX96, amount
);
(uint256 amount0, uint256 amount1) = pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
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);
@ -116,197 +117,210 @@ contract BaseLineLP {
if (token0isWeth) {
tickLower = startTick;
tickUpper = startTick + 200;
createPosition(tickLower, tickUpper, amount / 10);
createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10);
} else {
tickLower = startTick - 200;
tickUpper = startTick;
createPosition(tickLower, tickUpper, amount / 10);
createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10);
}
// create anchor
if (token0isWeth) {
tickLower += 201;
tickUpper += 601;
createPosition(tickLower, tickUpper, amount / 20);
createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 20);
} else {
tickLower -= 601;
tickUpper -= 201;
createPosition(tickLower, tickUpper, amount / 10);
createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 10);
}
// create discovery
if (token0isWeth) {
tickLower += 601;
tickUpper += 11001;
createPosition(tickLower, tickUpper, harb.balanceOf(address(this)));
createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this)));
} else {
tickLower -= 11001;
tickUpper -= 601;
createPosition(tickLower, tickUpper, harb.balanceOf(address(this)));
createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this)));
}
}
function outstanding() public view returns (uint256 _outstanding) {
harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this));
_outstanding = harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this));
}
function ethInAnchor() public view returns (uint256 _ethInAnchor) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickUpper);
function ethIn(Stage s) public view returns (uint256 _ethInPosition) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(positions[s].tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(positions[s].tickUpper);
if (token0isWeth) {
_ethInAnchor = LiquidityAmounts.getAmount0ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity
_ethInPosition = LiquidityAmounts.getAmount0ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[s].liquidity
);
} else {
_ethInAnchor = LiquidityAmounts.getAmount1ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity
_ethInPosition = LiquidityAmounts.getAmount1ForLiquidity(
sqrtRatioAX96, sqrtRatioBX96, positions[s].liquidity
);
}
}
/// @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))
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)
);
// Proceed as before.
tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
tick_ = tick_ / TICK_SPACING * TICK_SPACING;
tick_ = token0isWeth ? tick_ : -tick_;
}
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
});
}
// 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;
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;
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");
}
// 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 ethAmountAnchor;
for (uint256 i=Stage.FLOOR; i <= Stage.DISCOVERY; i++) {
position = positions[i];
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 == ANCHOR) {
ethAmountAnchor = token0isWeth ? amount0 : amount1;
if (i == uint256(Stage.ANCHOR)) {
ethInAnchor = token0isWeth ? amount0 : amount1;
}
}
// TODO: handle fees
// ## set new positions
// move 10% of new ETH into Floor
ethAmountAnchor -= (ethAmountAnchor - ethInAnchor()) * 10 / LIQUIDITY_RATIO_DIVISOR;
// 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;
// ### set Floor position
uint256 anchorLiquidity;
// ### set Anchor position
uint128 anchorLiquidity;
{
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick - 300);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 300);
int24 tickLower = currentTick - 300;
int24 tickUpper = currentTick + 300;
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor
sqrtRatioAX96, sqrtRatioBX96, ethInAnchor
);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor
sqrtRatioAX96, sqrtRatioBX96, ethInAnchor
);
}
(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
});
_mint(tickLower, tickUpper, anchorLiquidity);
}
// ### set Floor position
{
uint256 ethAmountFloor = address(this).balance;
int24 floorTick = tickAtPrice(outstanding() / ethAmountFloor);
floorTick -= (currentTick - floorTick);
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
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - 300);
uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts(
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(startTick);
uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
token0isWeth ? ethAmountFloor : 0,
token0isWeth ? 0: ethAmountFloor
token0isWeth ? ethInFloor : 0,
token0isWeth ? 0: ethInFloor
);
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
});
// mint
_mint(floorTick, startTick, liquidity);
}
// ##set discovery position
// ## set Discovery position
{
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick + 300);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 11300);
int24 tickLower = token0isWeth ? currentTick + 301 : currentTick - 11301;
int24 tickUpper = token0isWeth ? currentTick + 11301 : currentTick - 301;
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
// discovery with 1.5 times as much liquidity per tick as anchor
uint256 liquidity = anchorLiquidity * 55 / 2;
// A * 3 11000 A * 55
// D = ----- * ----- = ------
// 2 600 2
uint128 liquidity = anchorLiquidity * 55 / 2;
uint256 harbInDiscovery;
if (token0isWeth) {
harbInDiscovery = TickMath.getAmount1ForLiquidity(
harbInDiscovery = LiquidityAmounts.getAmount1ForLiquidity(
sqrtRatioAX96,
sqrtRatioBX96,
liquidity
);
} else {
harbInDiscovery = TickMath.getAmount0ForLiquidity(
harbInDiscovery = LiquidityAmounts.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
});
_mint(tickLower, tickUpper, liquidity);
}
}