take harb minted for staking into account when setting floor
This commit is contained in:
parent
3eeef258e3
commit
16e65f0f15
2 changed files with 96 additions and 88 deletions
|
|
@ -36,15 +36,12 @@ contract LiquidityManager {
|
|||
// defines the width of the anchor position from the current price to discovery position.
|
||||
int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING;
|
||||
// DISCOVERY_SPACING determines the range above the current price where new tokens are minted and sold.
|
||||
// This spacing is much wider, allowing the contract to place liquidity far from the current market price,
|
||||
// aiming to capture potential price increases and support token issuance within strategic market bounds.
|
||||
// 11000 ticks represent 3x the current price
|
||||
int24 internal constant DISCOVERY_SPACING = 11000;
|
||||
// how much more liquidity per tick discovery is holding over anchor
|
||||
uint128 internal constant DISCOVERY_DEPTH = 450; // 500 // 500%
|
||||
int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that?
|
||||
uint128 internal constant DISCOVERY_DEPTH = 200; // 500 // 500%
|
||||
// only working with UNI V3 1% fee tier pools
|
||||
uint24 internal constant FEE = uint24(10_000);
|
||||
uint160 internal constant MIN_SQRT_RATIO = 4295128739;
|
||||
// ANCHOR_LIQ_SHARE is the mininum share of total ETH in control
|
||||
// that will be left to put into anchor positon.
|
||||
uint256 internal constant MIN_ANCHOR_LIQ_SHARE = 5; // 5 = 5%
|
||||
|
|
@ -55,6 +52,7 @@ contract LiquidityManager {
|
|||
uint256 internal constant MAX_CAPITAL_INEFFICIENCY = 200;
|
||||
// used to double-check price with uni oracle
|
||||
uint32 internal constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes in seconds
|
||||
int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that?
|
||||
|
||||
// the address of the Uniswap V3 factory
|
||||
address private immutable factory;
|
||||
|
|
@ -63,6 +61,7 @@ contract LiquidityManager {
|
|||
IUniswapV3Pool private immutable pool;
|
||||
bool private immutable token0isWeth;
|
||||
PoolKey private poolKey;
|
||||
uint256 private harbPulled; // temporary variable to store amount of harb pulled by Uni
|
||||
|
||||
// the 3 positions this contract is managing
|
||||
enum Stage { FLOOR, ANCHOR, DISCOVERY }
|
||||
|
|
@ -117,7 +116,8 @@ contract LiquidityManager {
|
|||
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
|
||||
CallbackValidation.verifyCallback(factory, poolKey);
|
||||
// take care of harb
|
||||
harb.mint(token0isWeth ? amount1Owed : amount0Owed);
|
||||
harbPulled = token0isWeth ? amount1Owed : amount0Owed;
|
||||
harb.mint(harbPulled);
|
||||
// pack ETH
|
||||
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
|
||||
if (weth.balanceOf(address(this)) < ethOwed) {
|
||||
|
|
@ -227,70 +227,25 @@ contract LiquidityManager {
|
|||
/// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements.
|
||||
function _set(int24 currentTick) internal {
|
||||
|
||||
// set Floor position
|
||||
// estimate the lower tick of the anchor
|
||||
int24 vwapTick;
|
||||
{
|
||||
uint256 outstandingSupply = harb.outstandingSupply();
|
||||
uint256 vwapX96 = 0;
|
||||
uint256 requiredEthForBuyback = 0;
|
||||
if (cumulativeVolume > 0) {
|
||||
vwapX96 = cumulativeVolumeWeightedPriceX96 / cumulativeVolume;
|
||||
requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96));
|
||||
}
|
||||
uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this)));
|
||||
// leave at least ANCHOR_LIQ_SHARE% of supply for anchor
|
||||
uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100;
|
||||
if (floorEthBalance < requiredEthForBuyback) {
|
||||
// not enough ETH, find a lower price
|
||||
requiredEthForBuyback = floorEthBalance;
|
||||
vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback);
|
||||
emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick);
|
||||
} else if (vwapX96 == 0) {
|
||||
requiredEthForBuyback = floorEthBalance;
|
||||
vwapTick = currentTick;
|
||||
} else {
|
||||
// recalculate vwap with capital inefficiency
|
||||
vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth
|
||||
// ETH/HARB tick
|
||||
vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32)));
|
||||
// convert to pool tick
|
||||
vwapTick = token0isWeth ? -vwapTick : vwapTick;
|
||||
emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick);
|
||||
}
|
||||
// never make floor smaller than anchor
|
||||
if (requiredEthForBuyback < ethBalance * 3 / 4) {
|
||||
// use 3/4 instead of 1/2 to also account for liquidity of harb in anchor
|
||||
requiredEthForBuyback = ethBalance * 3 / 4;
|
||||
}
|
||||
// move floor below anchor, if needed
|
||||
if (token0isWeth) {
|
||||
vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick;
|
||||
} else {
|
||||
vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick;
|
||||
}
|
||||
uint256 outstandingSupply = harb.outstandingSupply();
|
||||
uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this)));
|
||||
uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100;
|
||||
if (outstandingSupply > 0) {
|
||||
vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , floorEthBalance);
|
||||
} else {
|
||||
vwapTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING;
|
||||
}
|
||||
|
||||
// normalize tick position for pool
|
||||
vwapTick = vwapTick / TICK_SPACING * TICK_SPACING;
|
||||
// calculate liquidity
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick);
|
||||
int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING;
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick);
|
||||
uint128 liquidity;
|
||||
if (token0isWeth) {
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
||||
sqrtRatioAX96, sqrtRatioBX96, requiredEthForBuyback
|
||||
);
|
||||
} else {
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
||||
sqrtRatioAX96, sqrtRatioBX96, requiredEthForBuyback
|
||||
);
|
||||
}
|
||||
_mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity);
|
||||
// move vwapTick below currentTick, if needed
|
||||
if (token0isWeth) {
|
||||
vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick;
|
||||
} else {
|
||||
vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick;
|
||||
}
|
||||
|
||||
// set Anchor position
|
||||
uint128 anchorLiquidity;
|
||||
uint24 anchorWidth;
|
||||
{
|
||||
int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick;
|
||||
int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING;
|
||||
|
|
@ -300,21 +255,22 @@ contract LiquidityManager {
|
|||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
|
||||
// adding a 2% balance margin, because liquidity calculations are inherently unprecise
|
||||
uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))) * 98 / 100;
|
||||
|
||||
uint256 anchorEthBalance = ethBalance - floorEthBalance;
|
||||
uint128 anchorLiquidity;
|
||||
if (token0isWeth) {
|
||||
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
|
||||
sqrtRatioX96, sqrtRatioBX96, ethBalance
|
||||
sqrtRatioX96, sqrtRatioBX96, anchorEthBalance
|
||||
);
|
||||
} else {
|
||||
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
|
||||
sqrtRatioAX96, sqrtRatioX96, ethBalance
|
||||
sqrtRatioAX96, sqrtRatioX96, anchorEthBalance
|
||||
);
|
||||
}
|
||||
|
||||
anchorWidth = uint24(tickUpper - tickLower);
|
||||
_mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity);
|
||||
}
|
||||
harb.burn(harb.balanceOf(address(this)));
|
||||
currentTick = currentTick / TICK_SPACING * TICK_SPACING;
|
||||
|
||||
// set Discovery position
|
||||
|
|
@ -323,25 +279,77 @@ contract LiquidityManager {
|
|||
int24 tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + DISCOVERY_SPACING + ANCHOR_SPACING;
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
// discovery with x times as much liquidity per tick as anchor
|
||||
uint128 liquidity = anchorLiquidity * uint128(uint24(DISCOVERY_SPACING)) * DISCOVERY_DEPTH / 100 / anchorWidth;
|
||||
uint256 harbInDiscovery;
|
||||
|
||||
uint256 discoveryAmount = harbPulled * uint24(DISCOVERY_SPACING) * uint24(DISCOVERY_DEPTH) / uint24(ANCHOR_SPACING) / 100;
|
||||
uint128 liquidity;
|
||||
if (token0isWeth) {
|
||||
harbInDiscovery = LiquidityAmounts.getAmount0ForLiquidity(
|
||||
sqrtRatioAX96,
|
||||
sqrtRatioBX96,
|
||||
liquidity
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
||||
sqrtRatioAX96, sqrtRatioBX96, discoveryAmount
|
||||
);
|
||||
} else {
|
||||
harbInDiscovery = LiquidityAmounts.getAmount1ForLiquidity(
|
||||
sqrtRatioAX96,
|
||||
sqrtRatioBX96,
|
||||
liquidity
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
||||
sqrtRatioAX96, sqrtRatioBX96, discoveryAmount
|
||||
);
|
||||
}
|
||||
_mint(Stage.DISCOVERY, tickLower, tickUpper, liquidity);
|
||||
harb.burn(harb.balanceOf(address(this)));
|
||||
}
|
||||
|
||||
// set Floor position
|
||||
{
|
||||
outstandingSupply = harb.outstandingSupply();
|
||||
uint256 vwapX96 = 0;
|
||||
uint256 requiredEthForBuyback = 0;
|
||||
if (cumulativeVolume > 0) {
|
||||
vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth
|
||||
requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96));
|
||||
}
|
||||
// make a new calculation of the vwapTick, having updated outstandingSupply
|
||||
if (floorEthBalance < requiredEthForBuyback) {
|
||||
// not enough ETH, find a lower price
|
||||
requiredEthForBuyback = floorEthBalance;
|
||||
vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback);
|
||||
emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick);
|
||||
} else if (vwapX96 == 0) {
|
||||
requiredEthForBuyback = floorEthBalance;
|
||||
vwapTick = currentTick;
|
||||
} else {
|
||||
// ETH/HARB tick
|
||||
vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32)));
|
||||
// convert to pool tick
|
||||
vwapTick = token0isWeth ? -vwapTick : vwapTick;
|
||||
emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick);
|
||||
}
|
||||
// move floor below anchor, if needed
|
||||
if (token0isWeth) {
|
||||
vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick;
|
||||
} else {
|
||||
vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick;
|
||||
}
|
||||
|
||||
// normalize tick position for pool
|
||||
vwapTick = vwapTick / TICK_SPACING * TICK_SPACING;
|
||||
// calculate liquidity
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick);
|
||||
int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING;
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick);
|
||||
|
||||
// adding a 2% balance margin, because liquidity calculations are inherently unprecise
|
||||
floorEthBalance = floorEthBalance * 98 / 100;
|
||||
|
||||
uint128 liquidity;
|
||||
if (token0isWeth) {
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
||||
sqrtRatioAX96, sqrtRatioBX96, floorEthBalance
|
||||
);
|
||||
} else {
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
||||
sqrtRatioAX96, sqrtRatioBX96, floorEthBalance
|
||||
);
|
||||
}
|
||||
_mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function _recordVolumeAndPrice(uint256 currentPriceX96, uint256 fee) internal {
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ contract LiquidityManagerTest is Test {
|
|||
|
||||
assertGt(cumulativeVolumeWeightedPriceX96, type(uint256).max / 2, "Initial cumulativeVolumeWeightedPrice is not near max uint256");
|
||||
|
||||
buy(50 ether);
|
||||
buy(25 ether);
|
||||
|
||||
shift();
|
||||
|
||||
|
|
@ -498,28 +498,28 @@ contract LiquidityManagerTest is Test {
|
|||
|
||||
// function testScenarioB() public {
|
||||
// setUpCustomToken0(false);
|
||||
// vm.deal(account, 201 ether);
|
||||
// vm.deal(account, 501 ether);
|
||||
// vm.prank(account);
|
||||
// weth.deposit{value: 201 ether}();
|
||||
// weth.deposit{value: 501 ether}();
|
||||
|
||||
// uint256 traderBalanceBefore = weth.balanceOf(account);
|
||||
|
||||
// // Setup initial liquidity
|
||||
// slide(false);
|
||||
|
||||
// buy(50 ether);
|
||||
// buy(25 ether);
|
||||
|
||||
// shift();
|
||||
|
||||
// buy(50 ether);
|
||||
// buy(45 ether);
|
||||
|
||||
// shift();
|
||||
|
||||
// buy(50 ether);
|
||||
// buy(80 ether);
|
||||
|
||||
// shift();
|
||||
|
||||
// buy(50 ether);
|
||||
// buy(120 ether);
|
||||
|
||||
// shift();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue