diff --git a/onchain/remappings.txt b/onchain/remappings.txt index 90b1bc0..6af28d9 100644 --- a/onchain/remappings.txt +++ b/onchain/remappings.txt @@ -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/ diff --git a/onchain/src/BaseLineLP.sol b/onchain/src/BaseLineLP.sol index 61c7f36..5c6ead8 100644 --- a/onchain/src/BaseLineLP.sol +++ b/onchain/src/BaseLineLP.sol @@ -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); } }