diff --git a/onchain/README.md b/onchain/README.md index f51ce18..ec78717 100644 --- a/onchain/README.md +++ b/onchain/README.md @@ -124,3 +124,260 @@ address: 0xeEa613dB62D5F0e914FfE7Ac85a1AdcFEddb8063 - limit discovery position growth to max_issuance / day +## old lp + +``` +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +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"; + +/** + * @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 LiquidityManager { + // default fee of 1% + uint24 constant FEE = uint24(10_000); + + // the address of the Uniswap V3 factory + address immutable factory; + IWETH9 immutable weth; + Harb immutable harb; + IUniswapV3Pool immutable pool; + PoolKey immutable poolKey; + bool immutable token0isWeth; + + + struct TokenPosition { + // the liquidity of the position + uint128 liquidity; + uint128 ethOwed; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + } + + /// @dev The token ID position data + mapping(bytes26 => TokenPosition) private _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 posKey(int24 tickLower, int24 tickUpper) internal pure returns (bytes6 _posKey) { + bytes memory _posKeyBytes = abi.encodePacked(tickLower, tickUpper); + assembly { + _posKey := mload(add(_posKeyBytes, 6)) + } + } + + function positions(int24 tickLower, int24 tickUpper) + external + view + returns (uint128 liquidity, uint128 ethOwed, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) + { + TokenPosition memory position = _positions[posKey(tickLower, tickUpper)]; + return (position.liquidity, position.ethOwed, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128); + } + + function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external { + CallbackValidation.verifyCallback(factory, poolKey); + + if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); + if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); + } + + + /// @notice Add liquidity to an initialized pool + // TODO: use uint256 amount0Min; uint256 amount1Min; if addLiquidity is called directly + function addLiquidity(int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 deadline) internal checkDeadline(deadline) { + + + (uint256 amount0, uint256 amount1) = pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey)); + // If addLiquidity is only called after other pool operations that have checked slippage, this here is not needed + //require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check"); + + + // read position and start tracking in storage + bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper); + (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); + TokenPosition storage position = _positions[posKey(token, tickLower, tickUpper)]; + if (liquidity == 0) { + // create entry + position = TokenPosition({ + liquidity: liquidity, + ethOwed: 0, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128 + }); + } else { + position.ethOwed += FullMath.mulDiv( + (token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, + position.liquidity, + FixedPoint128.Q128 + ); + position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + position.liquidity += liquidity; + } + emit IncreaseLiquidity(tickLower, tickUpper, liquidity, amount0, amount1); + } + + function liquidatePosition(int24 tickLower, int24 tickUpper, uint256 amount0Min, uint256 amount1Min) + internal + returns (uint256 ethReceived, uint256 liquidity) + { + // load position + TokenPosition storage position = _positions[posKey(tickLower, tickUpper)]; + + // burn and check slippage + uint256 liquidity = position.liquidity; + (uint256 amount0, uint256 amount1) = pool.burn(tickLower, tickUpper, liquidity); + require(amount0 >= amount0Min && amount1 >= amount1Min, "Price slippage check"); + // TODO: send harb fees or burn? + //harb.burn(token0isWeth ? amount1 : amount0); + + // calculate and transfer fees + bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper); + (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); + uint256 ethOwed = position.ethOwed; + ethOwed += + FullMath.mulDiv( + (token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, + liquidity, + FixedPoint128.Q128 + ); + weth.withdraw(ethOwed); + (bool sent, ) = feeRecipient.call{value: ethOwed}(""); + require(sent, "Failed to send Ether"); + + // event, cleanup and return + ethReceived = token0isWeth ? amount0 - ethOwed : amount1 - ethOwed; + emit PositionLiquidated(tickLower, tickUpper, liquidity, ethReceived); + delete position.liquidity; + delete position.ethOwed; + delete position.feeGrowthInside0LastX128; + delete position.feeGrowthInside1LastX128; + } + + // function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) { + // // Fetch the current sqrtPriceX96 from the pool + // (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0(); + + // // Convert sqrtPriceX96 to a conventional price format + // // Note: The price is calculated as (sqrtPriceX96^2 / 2^192), simplified here as (price / 2^96) for the sake of example + // uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96); + + // // Calculate the equivalent token amount for the ETH in the position at the current price + // // Assuming price is expressed as the amount of token per ETH + // uint256 equivalentTokenAmountForEth = ethAmountInPosition * price; + + // // Compare to the actual token amount in the position + // hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth; + + // return hasMoreToken; + // } + + //////// + // - check if tick in range, otherwise revert + // - check if the position has more Token or more ETH, at current price + // - if more ETH, + // - calculate the amount of Token needed to be minted to bring the position to 50/50 + // - mint + // - deposit Token into pool + // - if more TOKEN + // - calculate the amount of token needed to be withdrawn from the position, to bring the position to 50/50 + // - withdraw + // - burn tokens + + + + function stretch(int24 tickLower, int24 tickUpper, uint256 deadline, uint256 amount0Min, uint256 amount1Min) external checkDeadline(deadline) { + + // Fetch the current tick from the Uniswap V3 pool + (, int24 currentTick, , , , , ) = pool.slot0(); + + // Check if current tick is within the specified range + int24 centerTick = tickLower + ((tickUpper - tickLower) / 2); + // TODO: add hysteresis + if (token0isWeth) { + require(currentTick > centerTick && currentTick <= tickUpper, "Current tick out of range for stretch"); + } else { + require(currentTick >= tickLower && currentTick < centerTick, "Current tick out of range for stretch"); + } + + (uint256 ethReceived, uint256 oldliquidity) = liquidatePosition(tickLower, tickUpper, amount0Min, amount1Min); + + uint256 liquidity; + int24 newTickLower; + int24 newTickUpper; + if (token0isWeth) { + newTickLower = tickLower; + newTickUpper = currentTick + (currentTick - tickLower); + // extend the range up + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(newTickUpper); + liquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioAX96, sqrtRatioBX96, ethReceived + ); + // calculate amount for new liquidity + uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity( + sqrtRatioAX96, sqrtRatioBX96, liquidity + ); + uint256 currentBal = harb.balanceOf(address(this)); + if (currentBal < newAmount1) { + harb.mint(address(this), newAmount1 - currentBal); + } + + } else { + newTickUpper = tickUpper; + // extend the range down + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick)); + liquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioBX96, ethReceived + ); + // calculate amount for new liquidity + uint256 newAmount0 = LiquidityAmounts.getAmount0ForLiquidity( + sqrtRatioAX96, sqrtRatioBX96, liquidity + ); + uint256 currentBal = harb.balanceOf(address(this)); + if (currentBal < newAmount0) { + harb.mint(address(this), newAmount0 - currentBal); + } + newTickLower = ... + } + addLiquidity(newTickLower, newTickUpper, liquidity, deadline); + } + +} +``` \ No newline at end of file diff --git a/onchain/src/BaseLineLP.sol b/onchain/src/BaseLineLP.sol index 06fbe8d..0d47fcb 100644 --- a/onchain/src/BaseLineLP.sol +++ b/onchain/src/BaseLineLP.sol @@ -21,6 +21,7 @@ import {Harb} from "./Harb.sol"; */ contract BaseLineLP { int24 constant TICK_SPACING = 200; + int24 constant ANCHOR_SPACING = 5 * TICK_SPACING; // default fee of 1% uint24 constant FEE = uint24(10_000); // uint256 constant FLOOR = 0; @@ -88,7 +89,7 @@ contract BaseLineLP { harb.mint(token0isWeth ? amount1Owed : amount0Owed); // pack ETH weth.deposit{value: token0isWeth ? amount0Owed : amount1Owed}(); - // ## mint harb if needed + // do transfers if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } @@ -208,31 +209,33 @@ contract BaseLineLP { return amount; } - event DEBUG(uint256 indexed eth, uint256 indexed outstanding, int24 indexed floorTick, int24 startTick); + event DEBUG(uint256 indexed eth, uint256 indexed outstanding, int24 indexed floorTick, int24 startTick, bool ethIs0); function _set(uint160 sqrtPriceX96, int24 currentTick, uint256 ethInNewAnchor) internal { // ### set Anchor position uint128 anchorLiquidity; { - int24 tickLower = currentTick - 400; - int24 tickUpper = currentTick + 400; + int24 tickLower = currentTick - ANCHOR_SPACING; + int24 tickUpper = currentTick + ANCHOR_SPACING; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); if (token0isWeth) { - anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( - sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor - ); - } else { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor ); + } else { + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor + ); } - _mint(tickLower, tickUpper, anchorLiquidity); + // TODO: calculate liquidity correctly + // or make sure that we don't have to pay more than we have + _mint(tickLower, tickUpper, anchorLiquidity * 2); } // ### set Floor position { - int24 startTick = token0isWeth ? currentTick - 400 : currentTick + 400; + int24 startTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING; // all remaining eth will be put into this position uint256 ethInFloor = address(this).balance; @@ -241,10 +244,12 @@ contract BaseLineLP { uint256 _outstanding = outstanding(); if (_outstanding > 0) { floorTick = tickAtPrice(_outstanding, ethInFloor); + emit DEBUG(ethInFloor,0,floorTick,startTick,token0isWeth); + // put a position symetrically around the price, startTick being edge on one side floorTick = token0isWeth ? startTick + (floorTick - startTick) : floorTick - (startTick - floorTick); } else { - floorTick = startTick + ((token0isWeth ? int24(1) : int24(-1)) * 200); + floorTick = startTick + ((token0isWeth ? int24(1) : int24(-1)) * 400); } // calculate liquidity uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick); @@ -253,17 +258,18 @@ contract BaseLineLP { sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, - token0isWeth ? 0 : ethInFloor, - token0isWeth ? ethInFloor : 0 + token0isWeth ? ethInFloor : 0, + token0isWeth ? 0 : ethInFloor ); + emit DEBUG(ethInFloor,uint256(liquidity),floorTick,startTick,token0isWeth); // mint - _mint(floorTick, startTick, liquidity); + _mint(startTick, floorTick, liquidity); } // ## set Discovery position { - int24 tickLower = token0isWeth ? currentTick + 400 : currentTick - 11400; - int24 tickUpper = token0isWeth ? currentTick + 11400 : currentTick - 400; + int24 tickLower = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - 11400; + int24 tickUpper = token0isWeth ? currentTick + 11400 : currentTick - ANCHOR_SPACING; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); // discovery with 1.5 times as much liquidity per tick as anchor @@ -294,19 +300,17 @@ contract BaseLineLP { harb.mint(amount); mintedToday += amount; amount = harb.balanceOf(address(this)); - emit DEBUG(amount, harbInDiscovery, 0, 0); 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)) + 400; + int24 tickWidth = int24(int256(11000 * amount / harbInDiscovery)) + ANCHOR_SPACING; tickWidth = tickWidth / TICK_SPACING * TICK_SPACING; - tickWidth = (tickWidth <= 400) ? tickWidth + TICK_SPACING : tickWidth; - tickLower = token0isWeth ? currentTick - tickWidth : currentTick + 400; - tickUpper = token0isWeth ? currentTick - 400 : currentTick + tickWidth; + tickWidth = (tickWidth <= ANCHOR_SPACING) ? tickWidth + TICK_SPACING : tickWidth; + tickLower = token0isWeth ? currentTick - tickWidth : currentTick + ANCHOR_SPACING; + tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + tickWidth; sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - emit DEBUG(uint256(sqrtRatioAX96), uint256(sqrtRatioBX96), tickLower, tickWidth); liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, @@ -417,6 +421,7 @@ contract BaseLineLP { // but cap anchor size at 10 % of total ETH ethInNewAnchor = (ethInNewAnchor > ethBalance / 10) ? ethBalance / 10 : ethInNewAnchor; + emit DEBUG(0, 0, currentTick, currentTick / TICK_SPACING * TICK_SPACING, token0isWeth); currentTick = currentTick / TICK_SPACING * TICK_SPACING; _set(sqrtPriceX96, currentTick, ethInNewAnchor); } diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index c3c4a05..430c2f4 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -178,6 +178,9 @@ contract Harb is ERC20, ERC20Permit { } function ubiDue(address _account, uint256 lastTaxClaimed, uint256 _sumTaxCollected) internal view returns (uint256) { + if (lastTaxClaimed == 0 || block.timestamp - lastTaxClaimed < 60) { + return 0; + } uint256 accountTwab = twabController.getTwabBetween(address(this), _account, lastTaxClaimed, block.timestamp); uint256 stakeTwab = twabController.getTwabBetween(address(this), stakingPool, lastTaxClaimed, block.timestamp); diff --git a/onchain/test/BaseLineLP.t.sol b/onchain/test/BaseLineLP.t.sol index 3cd97b8..ab0b968 100644 --- a/onchain/test/BaseLineLP.t.sol +++ b/onchain/test/BaseLineLP.t.sol @@ -43,14 +43,14 @@ contract BaseLineLPTest is Test { uint256 price; if (isEthToken0) { // ETH as token0, so we are setting the price of 1 ETH in terms of token1 (USD cent) - price = 3700 * 10**20; // 1 ETH = 3700 USD, scaled by 10^18 for precision + price = 3000 * 10**20; // 1 ETH = 3700 USD, scaled by 10^18 for precision } else { // Token (valued at 1 USD cent) as token0, ETH as token1 // We invert the logic to represent the price of 1 token in terms of ETH price = uint256(10**16) / 3700; // Adjust for 18 decimal places } - uint160 sqrtPriceX96 = uint160(sqrt(price) * 2**96 / 10**18); // Adjust sqrt value to 96-bit precision + uint160 sqrtPriceX96 = uint160(sqrt(price) * 2**96 / 10**9); // Adjust sqrt value to 96-bit precision // Initialize pool with the calculated sqrtPriceX96 IUniswapV3Pool(pool).initialize(sqrtPriceX96); diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 7a820dc..27436dd 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -59,6 +59,7 @@ contract HarbTest is Test { vm.assume(account != address(2)); // tax pool address vm.assume(account != address(harb)); vm.assume(account != address(stake)); + address alice = makeAddr("alice"); // test mint uint256 totalSupplyBefore = harb.totalSupply(); @@ -75,7 +76,6 @@ contract HarbTest is Test { // prepare UBI title vm.prank(account); harb.mint(amount * 4); - address alice = makeAddr("alice"); vm.prank(account); harb.transfer(alice, amount); vm.prank(alice); @@ -116,11 +116,12 @@ contract HarbTest is Test { stake.snatch(amount, account, 2, empty); } + // test unstake { // advance the time uint256 timeBefore = block.timestamp; - vm.warp(timeBefore + 60 * 60 * 24 * 4); + vm.warp(timeBefore + (60 * 60 * 24 * 4)); uint256 taxDue = stake.taxDue(0, 60 * 60 * 24 * 3); uint256 sumTaxCollectedBefore = harb.sumTaxCollected(); @@ -137,8 +138,17 @@ contract HarbTest is Test { uint256 ubiDue = harb.getUbiDue(account); vm.prank(account); harb.claimUbi(account); - assertFalse(ubiDue == 0, "Not UBI paid"); + assertFalse(ubiDue == 0, "No UBI paid"); assertEq(balanceBefore + ubiDue, harb.balanceOf(account), "ubi should match"); } + + + // test UBIdue + { + uint256 timeBefore = block.timestamp; + vm.warp(timeBefore + (60 * 60 * 24 * 7)); + harb.getUbiDue(account); + harb.getUbiDue(alice); + } } }