diff --git a/onchain/script/Deploy.sol b/onchain/script/Deploy.sol index 9ae5482..907dcc5 100644 --- a/onchain/script/Deploy.sol +++ b/onchain/script/Deploy.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.4; import "forge-std/Script.sol"; import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "../src/Harb.sol"; import "../src/Stake.sol"; @@ -21,9 +22,9 @@ contract SepoliaScript is Script { Harb harb = new Harb("Harberger Tax", "HARB", V3_FACTORY, WETH, tc); Stake stake = new Stake(address(harb)); harb.setStakingPool(address(stake)); - factory = IUniswapV3Factory(V3_FACTORY); - IUniswapV3Pool(factory.createPool(WETH, address(harb), FEE)); - liquidityManager = new LiquidityManager(V3_FACTORY, WETH, address(harb)); + IUniswapV3Factory factory = IUniswapV3Factory(V3_FACTORY); + factory.createPool(WETH, address(harb), FEE); + LiquidityManager liquidityManager = new LiquidityManager(V3_FACTORY, WETH, address(harb)); harb.setLiquidityManager(address(liquidityManager)); vm.stopBroadcast(); } diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 5049a87..f63366f 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -11,7 +11,7 @@ import "@aperture/uni-v3-lib/CallbackValidation.sol"; import "@openzeppelin/token/ERC20/IERC20.sol"; /** - * @title LiquidityManager - A contract that supports the $bloodX ecosystem. It + * @title LiquidityManager - A contract that supports the harb ecosystem. It * protects the communities liquidity while allowing a manager role to * take strategic liqudity positions. */ @@ -22,10 +22,14 @@ contract LiquidityManager { // the address of the Uniswap V3 factory address public immutable factory; // the address of WETH9 - address immutable WETH9; + IWETH9 immutable weth; + + Harb immutable harb; IUniswapV3Pool public immutable pool; + bool immutable token0isWeth; + struct AddLiquidityParams { address token0; address token1; @@ -52,14 +56,12 @@ contract LiquidityManager { 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; } - // uint256 = uint128 tokensOwed + uint128 ethOwed - mapping(address => uint256) private _feesOwed; - /// @dev The token ID position data mapping(bytes26 => TokenPosition) private _positions; @@ -79,26 +81,13 @@ contract LiquidityManager { /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity event DecreaseLiquidity(address indexed token, uint128 liquidity, uint256 amount0, uint256 amount1); - constructor(address _factory, address _WETH9, address harb) { + constructor(address _factory, address _WETH9, address _harb) { factory = _factory; - WETH9 = _WETH9; - PoolKey memory poolKey = PoolAddress.getPoolKey(_WETH9, harb, FEE); + weth = IWETH9(_WETH9); + PoolKey memory poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE); pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); - } - - function getToken(address token0, address token1) internal view returns (bool token0isWeth, address token) { - token0isWeth = (token0 == WETH9); - require(token0isWeth || token1 == WETH9, "token error"); - token = (token0isWeth) ? token1 : token0; - } - - function getFeesOwed(address token) public view returns (uint128 tokensOwed, uint128 ethOwed) { - uint256 feesOwed = _feesOwed[token]; - return (uint128(feesOwed >> 128), uint128(feesOwed)); - } - - function getPool() public view returns (address) { - return address(pool); + harb = Harb(_harb); + token0isWeth = _WETH9 < _harb; } function updateFeesOwed(bool token0isEth, address token, uint128 tokensOwed0, uint128 tokensOwed1) internal { @@ -108,20 +97,20 @@ contract LiquidityManager { _feesOwed[token] = uint256(tokensOwed) << 128 + ethOwed; } - function posKey(address token, int24 tickLower, int24 tickUpper) internal pure returns (bytes26 _posKey) { - bytes memory _posKeyBytes = abi.encodePacked(token, tickLower, tickUpper); + function posKey(int24 tickLower, int24 tickUpper) internal pure returns (bytes6 _posKey) { + bytes memory _posKeyBytes = abi.encodePacked(tickLower, tickUpper); assembly { - _posKey := mload(add(_posKeyBytes, 26)) + _posKey := mload(add(_posKeyBytes, 6)) } } - function positions(address token, int24 tickLower, int24 tickUpper) + function positions(int24 tickLower, int24 tickUpper) external view - returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) + returns (uint128 liquidity, uint128 ethOwed, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) { - TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)]; - return (position.liquidity, position.feeGrowthInside0LastX128, position.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 { @@ -133,7 +122,7 @@ contract LiquidityManager { } /// @notice Add liquidity to an initialized pool - function addLiquidity(AddLiquidityParams memory params) external checkDeadline(params.deadline) { + function addLiquidity(AddLiquidityParams memory params) internal checkDeadline(params.deadline) { // compute the liquidity amount uint128 liquidity; @@ -147,7 +136,7 @@ contract LiquidityManager { ); } - (bool token0isWeth, address token) = getToken(params.token0, params.token1); + { PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE); (uint256 amount0, uint256 amount1) = @@ -197,49 +186,40 @@ contract LiquidityManager { } } - function decreaseLiquidity(DecreaseLiquidityParams calldata params) - external - checkDeadline(params.deadline) - returns (uint256 amount0, uint256 amount1) + function liquidatePosition(int24 tickLower, int24 tickUpper, uint256 amount0Min, uint256 amount1Min) + internal + returns (uint256 ethReceived, uint256 liquidity) { - require(params.liquidity > 0); + // load position + TokenPosition storage position = _positions[posKey(tickLower, tickUpper)]; - (bool token0isWeth, address token) = getToken(params.token0, params.token1); - TokenPosition memory position = _positions[posKey(token, params.tickLower, params.tickUpper)]; - uint128 positionLiquidity = position.liquidity; - require(positionLiquidity >= params.liquidity); + // 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"); + harb.burn(token0isWeth ? amount1 : amount0); - (amount0, amount1) = pool.burn(params.tickLower, params.tickUpper, params.liquidity); - - require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check"); - - bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); - // this is now updated to the current transaction + // calculate and transfer fees + bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); - - updateFeesOwed( - token0isWeth, - token, - uint128(amount0) - + uint128( - FullMath.mulDiv( - feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, positionLiquidity, FixedPoint128.Q128 - ) - ), - uint128(amount1) - + uint128( - FullMath.mulDiv( - feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, positionLiquidity, FixedPoint128.Q128 - ) - ) + 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"); - position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; - position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; - // subtraction is safe because we checked positionLiquidity is gte params.liquidity - position.liquidity = positionLiquidity - params.liquidity; - - emit DecreaseLiquidity(token, params.liquidity, amount0, amount1); + // 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) { @@ -272,52 +252,52 @@ contract LiquidityManager { // - withdraw // - burn tokens - // function rebalance(address token, int24 tickLower, int24 tickUpper) external { - // bool ETH_TOKEN_ZERO = WETH9 < token; - // PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE); - // IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); - // // Fetch the current tick from the Uniswap V3 pool - // (, int24 currentTick, , , , , ) = pool.slot0(); + function stretch(int24 tickLower, int24 tickUpper, uint256 deadline, uint256 amount0Min, uint256 amount1Min) external checkDeadline(deadline) { - // // Check if current tick is within the specified range - // require(currentTick >= tickLower && currentTick <= tickUpper, "Current tick out of range"); + // Fetch the current tick from the Uniswap V3 pool + (, int24 currentTick, , , , , ) = pool.slot0(); - // // load position - // TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)]; + // 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"); + } - // // take the position out - // uint256 (amount0, amount1) = pool.burn(tickLower, tickUpper, position.liquidity); - // // TODO: this position might have earned fees, update them here + (uint256 ethReceived, uint256 oldliquidity) = liquidatePosition(tickLower, tickUpper, amount0Min, amount1Min); - // // calculate liquidity - // uint128 liquidity; - // if (ETH_TOKEN_ZERO) { - // // extend/contract the range up - // uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); - // uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + (currentTick - tickLower)); - // liquidity = LiquidityAmounts.getLiquidityForAmount0( - // sqrtRatioAX96, sqrtRatioBX96, amount0 - // ); - // // calculate amount for new liquidity - // uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity( - // sqrtRatioAX96, sqrtRatioBX96, liquidity - // ); - // if (newAmount1 > amount1) { - // IERC20(token).mint(address(this), newAmount1 - amount1 + 1); - // } else { - // IERC20(token).burn(address(this), amount1 - newAmount1 + 1); - // } + uint256 liquidity; + if (token0isWeth) { + // extend the range up + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + (currentTick - tickLower)); + liquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioAX96, sqrtRatioBX96, ethReceived + ); + // calculate amount for new liquidity + uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity( + sqrtRatioAX96, sqrtRatioBX96, liquidity + ); + harb.mint(address(this), newAmount1); + // TODO: addLiquidity(...) + } else { + // 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.getAmount1ForLiquidity( + sqrtRatioAX96, sqrtRatioBX96, liquidity + ); + harb.mint(address(this), newAmount0); + // TODO: addLiquidity(...) + } + } - // } else { - // // extend/contract the range down - // uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper); - // uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick)); - // liquidity = LiquidityAmounts.getLiquidityForAmount1( - // sqrtRatioAX96, sqrtRatioBX96, ethAmountToProvide - // ); - // } - - // } } diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index 8395ba1..96eb908 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -9,6 +9,8 @@ import {Math} from "@openzeppelin/utils/math/Math.sol"; import "./interfaces/IStake.sol"; import "./Harb.sol"; +error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake); + contract Stake is IStake { using Math for uint256; @@ -21,7 +23,6 @@ contract Stake is IStake { * @dev Attempted to deposit more assets than the max amount for `receiver`. */ - error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake); error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId); error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake); error NoPermission(address requester, address owner); diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 21535f1..9b3097a 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -9,7 +9,7 @@ import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "./interfaces/IWETH9.sol"; import "../src/Harb.sol"; -import "../src/Stake.sol"; +import {Stake, ExceededAvailableStake} from "../src/Stake.sol"; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; @@ -32,7 +32,7 @@ contract HarbTest is Test { TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); harb = new Harb("HARB", "HARB", V3_FACTORY, WETH, tc); factory = IUniswapV3Factory(V3_FACTORY); - IUniswapV3Pool(factory.createPool(address(weth), address(harb), FEE)); + factory.createPool(address(weth), address(harb), FEE); stake = new Stake(address(harb)); harb.setStakingPool(address(stake)); liquidityManager = new LiquidityManager(V3_FACTORY, WETH, address(harb)); @@ -95,6 +95,14 @@ contract HarbTest is Test { assertEq(taxRate, 1, "tax rate should match"); } + // test stake when stake full + { + uint256[] memory empty; + vm.prank(account); + vm.expectRevert(ExceededAvailableStake.selector); + stake.snatch(amount, account, 2, empty); + } + // test unstake { // advance the time