Add Solidity linting with solhint, Foundry formatter, and pre-commit hooks (#51)
## Changes ### Configuration - Added .solhint.json with recommended rules + custom config - 160 char line length (warn) - Double quotes enforcement (error) - Explicit visibility required (error) - Console statements allowed (scripts/tests need them) - Gas optimization warnings enabled - Ignores test/helpers/, lib/, out/, cache/, broadcast/ - Added foundry.toml [fmt] section - 160 char line length - 4-space tabs - Double quotes - Thousands separators for numbers - Sort imports enabled - Added .lintstagedrc.json for pre-commit auto-fix - Runs solhint --fix on .sol files - Runs forge fmt on .sol files - Added husky pre-commit hook via lint-staged ### NPM Scripts - lint:sol - run solhint - lint:sol:fix - auto-fix solhint issues - format:sol - format with forge fmt - format:sol:check - check formatting - lint / lint:fix - combined commands ### Code Changes - Added explicit visibility modifiers (internal) to constants in scripts and tests - Fixed quote style in DeployLocal.sol - All Solidity files formatted with forge fmt ## Verification - ✅ forge fmt --check passes - ✅ No solhint errors (warnings only) - ✅ forge build succeeds - ✅ forge test passes (107/107) resolves #44 Co-authored-by: johba <johba@harb.eth> Reviewed-on: https://codeberg.org/johba/harb/pulls/51
This commit is contained in:
parent
f8927b426e
commit
d7c2184ccf
45 changed files with 2853 additions and 1225 deletions
|
|
@ -1,23 +1,23 @@
|
|||
// 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";
|
||||
import "../libraries/UniswapMath.sol";
|
||||
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
import "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
||||
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.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
|
||||
*/
|
||||
|
|
@ -27,7 +27,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
/// @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;
|
||||
int24 internal constant DISCOVERY_SPACING = 11_000;
|
||||
/// @notice Minimum discovery depth multiplier
|
||||
uint128 internal constant MIN_DISCOVERY_DEPTH = 200;
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
/// @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);
|
||||
|
||||
|
|
@ -97,19 +97,22 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
int24 currentTick,
|
||||
uint256 anchorEthBalance,
|
||||
PositionParams memory params
|
||||
) internal returns (uint256 pulledKraiken, uint128 anchorLiquidity) {
|
||||
)
|
||||
internal
|
||||
returns (uint256 pulledKraiken, 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);
|
||||
pulledKraiken = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, anchorLiquidity);
|
||||
|
|
@ -117,7 +120,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, anchorEthBalance);
|
||||
pulledKraiken = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, anchorLiquidity);
|
||||
}
|
||||
|
||||
|
||||
_mintPosition(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity);
|
||||
}
|
||||
|
||||
|
|
@ -126,49 +129,36 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
/// @param anchorLiquidity Liquidity amount from anchor position
|
||||
/// @param params Position parameters
|
||||
/// @return discoveryAmount Amount of KRAIKEN used for discovery
|
||||
function _setDiscoveryPosition(
|
||||
int24 currentTick,
|
||||
uint128 anchorLiquidity,
|
||||
PositionParams memory params
|
||||
) internal returns (uint256 discoveryAmount) {
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
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)))
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -188,26 +178,27 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
uint256 pulledKraiken,
|
||||
uint256 discoveryAmount,
|
||||
PositionParams memory params
|
||||
) internal {
|
||||
)
|
||||
internal
|
||||
{
|
||||
bool token0isWeth = _isToken0Weth();
|
||||
|
||||
|
||||
// Calculate outstanding supply after position minting
|
||||
uint256 outstandingSupply = _getOutstandingSupply();
|
||||
outstandingSupply -= pulledKraiken;
|
||||
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);
|
||||
|
|
@ -237,31 +228,22 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
|
||||
// Normalize and create floor position
|
||||
vwapTick = _clampToTickSpacing(vwapTick, TICK_SPACING);
|
||||
int24 floorTick = _clampToTickSpacing(
|
||||
token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING,
|
||||
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) {
|
||||
// floor leg sits entirely above current tick when WETH is token0, so budget is token0
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
||||
TickMath.getSqrtRatioAtTick(vwapTick),
|
||||
TickMath.getSqrtRatioAtTick(floorTick),
|
||||
actualFloorEthBalance
|
||||
);
|
||||
liquidity =
|
||||
LiquidityAmounts.getLiquidityForAmount0(TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance);
|
||||
} else {
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
||||
TickMath.getSqrtRatioAtTick(vwapTick),
|
||||
TickMath.getSqrtRatioAtTick(floorTick),
|
||||
actualFloorEthBalance
|
||||
);
|
||||
liquidity =
|
||||
LiquidityAmounts.getLiquidityForAmount1(TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance);
|
||||
}
|
||||
|
||||
|
||||
_mintPosition(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue