LiqMan WIP

This commit is contained in:
JulesCrown 2024-03-14 17:31:16 +01:00
parent e51f49c612
commit 4e895f57e9
4 changed files with 106 additions and 116 deletions

View file

@ -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();
}

View file

@ -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
// );
// }
// }
}

View file

@ -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);

View file

@ -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