harb/onchain/src/abstracts/ThreePositionStrategy.sol
johba 61ea25517c fix: Fix discovery position KRAIKEN amount calculation
The discovery position was incorrectly calculating ETH amount instead
of KRAIKEN amount when determining how much to subtract from outstanding
supply. This caused the floor position to be placed at extreme ticks
(141k+) instead of bordering the anchor position.

When token0isWeth=true:
- Before: discoveryAmount = getAmount0 (ETH amount)
- After: discoveryAmount = getAmount1 (KRAIKEN amount)

This ensures the outstanding supply calculation properly excludes all
KRAIKEN tokens locked in liquidity positions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-18 17:05:32 +02:00

266 lines
No EOL
13 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import {Math} from "@openzeppelin/utils/math/Math.sol";
import "../libraries/UniswapMath.sol";
import "../VWAPTracker.sol";
/**
* @title ThreePositionStrategy
* @notice Abstract contract implementing the three-position liquidity strategy (Floor, Anchor, Discovery)
* @dev Provides the core logic for anti-arbitrage asymmetric slippage profile
*
* Three-Position Strategy:
* - ANCHOR: Near current price, fast price discovery (1-100% width)
* - DISCOVERY: Borders anchor, captures fees (11000 tick spacing)
* - FLOOR: Deep liquidity at VWAP-adjusted prices
*
* The asymmetric slippage profile prevents profitable arbitrage by making
* buys progressively more expensive while sells remain liquid
*/
abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
using Math for uint256;
/// @notice Tick spacing for the pool (base spacing)
int24 internal constant TICK_SPACING = 200;
/// @notice Discovery spacing (3x current price in ticks - 11000 ticks = ~3x price)
int24 internal constant DISCOVERY_SPACING = 11000;
/// @notice Minimum discovery depth multiplier
uint128 internal constant MIN_DISCOVERY_DEPTH = 200;
/// @notice The three liquidity position types
enum Stage {
FLOOR,
ANCHOR,
DISCOVERY
}
/// @notice Structure representing a liquidity position
struct TokenPosition {
uint128 liquidity;
int24 tickLower;
int24 tickUpper;
}
/// @notice Parameters for position strategy
struct PositionParams {
uint256 capitalInefficiency;
uint256 anchorShare;
uint24 anchorWidth;
uint256 discoveryDepth;
}
/// @notice Storage for the three positions
mapping(Stage => TokenPosition) public positions;
/// @notice Events for tracking ETH abundance/scarcity scenarios
event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick);
event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick);
/// @notice Abstract functions that must be implemented by inheriting contracts
function _getHarbToken() internal view virtual returns (address);
function _getWethToken() internal view virtual returns (address);
function _isToken0Weth() internal view virtual returns (bool);
function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal virtual;
function _getEthBalance() internal view virtual returns (uint256);
function _getOutstandingSupply() internal view virtual returns (uint256);
/// @notice Sets all three positions according to the asymmetric slippage strategy
/// @param currentTick The current market tick
/// @param params Position parameters from optimizer
function _setPositions(int24 currentTick, PositionParams memory params) internal {
uint256 ethBalance = _getEthBalance();
// Calculate floor ETH allocation (75% to 95% of total)
uint256 floorEthBalance = (19 * ethBalance / 20) - (2 * params.anchorShare * ethBalance / 10 ** 19);
// Step 1: Set ANCHOR position (shallow liquidity for fast price movement)
(uint256 pulledHarb, uint128 anchorLiquidity) = _setAnchorPosition(currentTick, ethBalance - floorEthBalance, params);
// Step 2: Set DISCOVERY position (depends on anchor's liquidity)
uint256 discoveryAmount = _setDiscoveryPosition(currentTick, anchorLiquidity, params);
// Step 3: Set FLOOR position (deep liquidity, uses VWAP for historical memory)
_setFloorPosition(currentTick, floorEthBalance, pulledHarb, discoveryAmount, params);
}
/// @notice Sets the anchor position around current price (shallow liquidity)
/// @param currentTick Current market tick
/// @param anchorEthBalance ETH allocated to anchor position
/// @param params Position parameters
/// @return pulledHarb Amount of HARB pulled for this position
/// @return anchorLiquidity The liquidity amount for the anchor position
function _setAnchorPosition(
int24 currentTick,
uint256 anchorEthBalance,
PositionParams memory params
) internal returns (uint256 pulledHarb, uint128 anchorLiquidity) {
// Enforce anchor range of 1% to 100% of the price
int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100);
int24 tickLower = _clampToTickSpacing(currentTick - anchorSpacing, TICK_SPACING);
int24 tickUpper = _clampToTickSpacing(currentTick + anchorSpacing, TICK_SPACING);
uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick);
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
bool token0isWeth = _isToken0Weth();
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, anchorEthBalance);
pulledHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, anchorLiquidity);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, anchorEthBalance);
pulledHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, anchorLiquidity);
}
_mintPosition(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity);
}
/// @notice Sets the discovery position (deep edge liquidity)
/// @param currentTick Current market tick (normalized to tick spacing)
/// @param anchorLiquidity Liquidity amount from anchor position
/// @param params Position parameters
/// @return discoveryAmount Amount of HARB used for discovery
function _setDiscoveryPosition(
int24 currentTick,
uint128 anchorLiquidity,
PositionParams memory params
) internal returns (uint256 discoveryAmount) {
currentTick = currentTick / TICK_SPACING * TICK_SPACING;
bool token0isWeth = _isToken0Weth();
// Calculate anchor spacing (same as in anchor position)
int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100);
int24 tickLower = _clampToTickSpacing(
token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing,
TICK_SPACING
);
int24 tickUpper = _clampToTickSpacing(
token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing,
TICK_SPACING
);
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
// Calculate discovery liquidity to ensure X times more liquidity per tick than anchor
// Discovery should have 2x to 10x more liquidity per tick (not just total liquidity)
uint256 discoveryMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18);
// Calculate anchor width in ticks
int24 anchorWidth = 2 * anchorSpacing;
// Adjust for width difference: discovery liquidity = anchor liquidity * multiplier * (discovery width / anchor width)
uint128 liquidity = uint128(
uint256(anchorLiquidity) * discoveryMultiplier * uint256(int256(DISCOVERY_SPACING))
/ (100 * uint256(int256(anchorWidth)))
);
// Calculate discoveryAmount for floor position calculation
if (token0isWeth) {
discoveryAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity);
} else {
discoveryAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity);
}
_mintPosition(Stage.DISCOVERY, tickLower, tickUpper, liquidity);
}
/// @notice Sets the floor position using VWAP for historical price memory (deep edge liquidity)
/// @dev Floor position placement depends on ETH scarcity vs abundance:
/// - Scarcity: Floor moves to extreme ticks (140k+) where KRAIKEN is very cheap
/// - Abundance: Floor placed near VWAP-adjusted price
/// Extreme floor positions are CORRECT behavior protecting protocol solvency
/// @param currentTick Current market tick
/// @param floorEthBalance ETH allocated to floor position (75% of total)
/// @param pulledHarb HARB amount from anchor position
/// @param discoveryAmount HARB amount from discovery position
/// @param params Position parameters including capital inefficiency
function _setFloorPosition(
int24 currentTick,
uint256 floorEthBalance,
uint256 pulledHarb,
uint256 discoveryAmount,
PositionParams memory params
) internal {
bool token0isWeth = _isToken0Weth();
// Calculate outstanding supply after position minting
uint256 outstandingSupply = _getOutstandingSupply();
outstandingSupply -= pulledHarb;
outstandingSupply -= (outstandingSupply >= discoveryAmount) ? discoveryAmount : outstandingSupply;
// Use VWAP for floor position (historical price memory for dormant whale protection)
uint256 vwapX96 = getAdjustedVWAP(params.capitalInefficiency);
uint256 ethBalance = _getEthBalance();
int24 vwapTick;
if (vwapX96 > 0) {
// vwapX96 is price² in X96 format, need to convert to regular price
// price = sqrt(price²) = sqrt(vwapX96) * 2^48 / 2^96 = sqrt(vwapX96) / 2^48
uint256 sqrtVwapX96 = Math.sqrt(vwapX96) << 48; // sqrt(price²) in X96 format
uint256 requiredEthForBuyback = outstandingSupply.mulDiv(sqrtVwapX96, (1 << 96));
if (floorEthBalance < requiredEthForBuyback) {
// ETH scarcity: not enough ETH to buy back at VWAP price
uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * params.capitalInefficiency / 10 ** 18);
vwapTick = _tickAtPrice(token0isWeth, balancedCapital, floorEthBalance);
emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick);
} else {
// ETH abundance: sufficient ETH reserves
// vwapX96 is price² in X96 format, need to convert to regular price in X64 format
// price = sqrt(price²), then convert from X96 to X64 by >> 32
uint256 sqrtVwapX96Abundance = Math.sqrt(vwapX96) << 48; // sqrt(price²) in X96 format
vwapTick = _tickAtPriceRatio(int128(int256(sqrtVwapX96Abundance >> 32)));
vwapTick = token0isWeth ? -vwapTick : vwapTick;
emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick);
}
} else {
// No VWAP data available, use current tick
vwapTick = currentTick;
}
// Ensure floor doesn't overlap with anchor position
int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100);
if (token0isWeth) {
vwapTick = (vwapTick < currentTick + anchorSpacing) ? currentTick + anchorSpacing : vwapTick;
} else {
vwapTick = (vwapTick > currentTick - anchorSpacing) ? currentTick - anchorSpacing : vwapTick;
}
// Normalize and create floor position
vwapTick = _clampToTickSpacing(vwapTick, TICK_SPACING);
int24 floorTick = _clampToTickSpacing(
token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING,
TICK_SPACING
);
// Use planned floor ETH balance, but fallback to remaining if insufficient
uint256 remainingEthBalance = _getEthBalance();
uint256 actualFloorEthBalance = (remainingEthBalance >= floorEthBalance) ? floorEthBalance : remainingEthBalance;
uint128 liquidity;
if (token0isWeth) {
liquidity = LiquidityAmounts.getLiquidityForAmount1(
TickMath.getSqrtRatioAtTick(vwapTick),
TickMath.getSqrtRatioAtTick(floorTick),
actualFloorEthBalance
);
} else {
liquidity = LiquidityAmounts.getLiquidityForAmount0(
TickMath.getSqrtRatioAtTick(vwapTick),
TickMath.getSqrtRatioAtTick(floorTick),
actualFloorEthBalance
);
}
_mintPosition(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity);
}
}