2026-03-11 02:08:06 +00:00
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
pragma solidity ^0.8.19;
|
|
|
|
|
|
|
|
|
|
|
|
import { FormatLib } from "./FormatLib.sol";
|
2026-03-11 17:14:34 +00:00
|
|
|
|
import { FullMath } from "@aperture/uni-v3-lib/FullMath.sol";
|
2026-03-11 02:08:06 +00:00
|
|
|
|
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
|
|
|
|
|
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
|
|
|
|
|
import { ABDKMath64x64 } from "@abdk/ABDKMath64x64.sol";
|
|
|
|
|
|
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
|
|
|
|
|
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
|
|
|
|
|
import { Script } from "forge-std/Script.sol";
|
|
|
|
|
|
import { console } from "forge-std/console.sol";
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Minimal external interfaces ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
interface IWETH9 {
|
|
|
|
|
|
function deposit() external payable;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ISwapRouter02 {
|
|
|
|
|
|
struct ExactInputSingleParams {
|
|
|
|
|
|
address tokenIn;
|
|
|
|
|
|
address tokenOut;
|
|
|
|
|
|
uint24 fee;
|
|
|
|
|
|
address recipient;
|
|
|
|
|
|
uint256 amountIn;
|
|
|
|
|
|
uint256 amountOutMinimum;
|
|
|
|
|
|
uint160 sqrtPriceLimitX96;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function exactInputSingle(ExactInputSingleParams calldata params) external returns (uint256 amountOut);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ILM {
|
|
|
|
|
|
function getVWAP() external view returns (uint256);
|
|
|
|
|
|
function positions(uint8 stage) external view returns (uint128 liquidity, int24 tickLower, int24 tickUpper);
|
|
|
|
|
|
function recenter() external returns (bool);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface IKraiken is IERC20 {
|
|
|
|
|
|
function outstandingSupply() external view returns (uint256);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface IOptimizer {
|
|
|
|
|
|
function getLiquidityParams()
|
|
|
|
|
|
external
|
|
|
|
|
|
view
|
|
|
|
|
|
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface IStake {
|
2026-03-11 17:14:34 +00:00
|
|
|
|
// taxRate matches the actual Stake.sol parameter name (a raw rate value, not a lookup index)
|
2026-03-12 07:10:13 +00:00
|
|
|
|
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) external returns (uint256 positionId);
|
2026-03-11 02:08:06 +00:00
|
|
|
|
function exitPosition(uint256 positionId) external;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface INonfungiblePositionManager {
|
|
|
|
|
|
struct MintParams {
|
|
|
|
|
|
address token0;
|
|
|
|
|
|
address token1;
|
|
|
|
|
|
uint24 fee;
|
|
|
|
|
|
int24 tickLower;
|
|
|
|
|
|
int24 tickUpper;
|
|
|
|
|
|
uint256 amount0Desired;
|
|
|
|
|
|
uint256 amount1Desired;
|
|
|
|
|
|
uint256 amount0Min;
|
|
|
|
|
|
uint256 amount1Min;
|
|
|
|
|
|
address recipient;
|
|
|
|
|
|
uint256 deadline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct DecreaseLiquidityParams {
|
|
|
|
|
|
uint256 tokenId;
|
|
|
|
|
|
uint128 liquidity;
|
|
|
|
|
|
uint256 amount0Min;
|
|
|
|
|
|
uint256 amount1Min;
|
|
|
|
|
|
uint256 deadline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct CollectParams {
|
|
|
|
|
|
uint256 tokenId;
|
|
|
|
|
|
address recipient;
|
|
|
|
|
|
uint128 amount0Max;
|
|
|
|
|
|
uint128 amount1Max;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
|
|
|
|
|
|
function positions(uint256 tokenId)
|
|
|
|
|
|
external
|
|
|
|
|
|
view
|
|
|
|
|
|
returns (
|
|
|
|
|
|
uint96 nonce,
|
|
|
|
|
|
address operator,
|
|
|
|
|
|
address token0,
|
|
|
|
|
|
address token1,
|
|
|
|
|
|
uint24 fee,
|
|
|
|
|
|
int24 tickLower,
|
|
|
|
|
|
int24 tickUpper,
|
|
|
|
|
|
uint128 liquidity,
|
|
|
|
|
|
uint256 feeGrowthInside0LastX128,
|
|
|
|
|
|
uint256 feeGrowthInside1LastX128,
|
|
|
|
|
|
uint128 tokensOwed0,
|
|
|
|
|
|
uint128 tokensOwed1
|
|
|
|
|
|
);
|
|
|
|
|
|
function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
|
|
|
|
|
|
function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface IUniswapV3Factory {
|
|
|
|
|
|
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @title AttackRunner
|
|
|
|
|
|
* @notice Structured attack executor for replaying adversarial sequences against the LM.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Reads operations from a JSON Lines file (one op per line) specified by the ATTACK_FILE
|
|
|
|
|
|
* environment variable. Executes each operation against the local Anvil deployment and emits
|
|
|
|
|
|
* a state snapshot after every recenter and at the start and end of the sequence.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Usage:
|
|
|
|
|
|
* ATTACK_FILE=script/backtesting/attacks/il-crystallization-15.jsonl \
|
|
|
|
|
|
* forge script script/backtesting/AttackRunner.s.sol \
|
|
|
|
|
|
* --rpc-url http://localhost:8545 --broadcast
|
|
|
|
|
|
*
|
|
|
|
|
|
* Optional overrides:
|
|
|
|
|
|
* DEPLOYMENTS_FILE Path to deployments JSON (default: deployments-local.json)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Supported operations:
|
2026-03-12 13:16:41 +00:00
|
|
|
|
* buy Swap WETH→KRK via SwapRouter. Fields: amount (wei string), token (ignored, WETH assumed)
|
|
|
|
|
|
* sell Swap KRK→WETH via SwapRouter. Fields: amount (wei string or "all"), token (ignored)
|
|
|
|
|
|
* recenter Call LM.recenter() via recenterAccess account. Emits a snapshot.
|
|
|
|
|
|
* buy_recenter_loop Batch N×(buy→recenter) cycles in a single op. Fields: count (uint), amount (wei string).
|
|
|
|
|
|
* Emits a snapshot after each successful recenter. Avoids per-step forge overhead for
|
|
|
|
|
|
* high-cycle attacks that would otherwise time out (e.g. il-crystallization-80).
|
2026-03-11 17:51:18 +00:00
|
|
|
|
* stake Call Stake.snatch(). Fields: amount (wei string), taxRateIndex (raw taxRate value passed to Stake.snatch)
|
2026-03-11 02:08:06 +00:00
|
|
|
|
* unstake Call Stake.exitPosition(). Fields: positionId
|
|
|
|
|
|
* mint_lp Add LP via NPM. Fields: tickLower, tickUpper, amount0 (wei string), amount1 (wei string)
|
|
|
|
|
|
* burn_lp Remove LP via NPM. Fields: tokenId
|
|
|
|
|
|
* mine Advance block number. Fields: blocks
|
|
|
|
|
|
*
|
|
|
|
|
|
* Snapshot schema (emitted as JSON line on stdout):
|
|
|
|
|
|
* seq, tick, lm_eth_free, lm_weth_free, lm_eth_total, positions (floor/anchor/discovery),
|
|
|
|
|
|
* vwap_x96, vwap_tick, outstanding_supply, total_supply, optimizer_output, adversary_eth, adversary_krk
|
|
|
|
|
|
*/
|
|
|
|
|
|
contract AttackRunner is Script {
|
|
|
|
|
|
using FormatLib for uint256;
|
|
|
|
|
|
using FormatLib for int256;
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Protocol constants (local Anvil deployment) ──────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
uint24 internal constant POOL_FEE = 10_000;
|
|
|
|
|
|
address internal constant WETH = 0x4200000000000000000000000000000000000006;
|
|
|
|
|
|
address internal constant SWAP_ROUTER = 0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4;
|
|
|
|
|
|
address internal constant NPM_ADDR = 0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2;
|
|
|
|
|
|
address internal constant V3_FACTORY = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24;
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Anvil test accounts ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Account 8 — adversary (10 000 ETH, 0 KRK)
|
|
|
|
|
|
uint256 internal constant ADV_PK = 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97;
|
|
|
|
|
|
/// @dev Account 2 — recenter caller (granted recenterAccess by bootstrap)
|
|
|
|
|
|
uint256 internal constant RECENTER_PK = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a;
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Runtime state (populated in run()) ──────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
address internal advAddr;
|
|
|
|
|
|
address internal recenterAddr;
|
|
|
|
|
|
address internal lmAddr;
|
|
|
|
|
|
address internal krkAddr;
|
|
|
|
|
|
address internal stakeAddr;
|
|
|
|
|
|
address internal optAddr;
|
|
|
|
|
|
IUniswapV3Pool internal pool;
|
|
|
|
|
|
bool internal token0isWeth;
|
2026-03-12 07:10:13 +00:00
|
|
|
|
/// @dev On-chain position IDs returned by Stake.snatch(), in insertion order.
|
|
|
|
|
|
/// Attack files reference positions by 1-based index (positionId=1 → _stakedPositionIds[0]).
|
|
|
|
|
|
uint256[] internal _stakedPositionIds;
|
2026-03-11 17:14:34 +00:00
|
|
|
|
/// @dev Direction of the most recent recenter: true = price moved up, false = price moved down.
|
|
|
|
|
|
/// Read by _logSnapshot to include in post-recenter snapshots.
|
|
|
|
|
|
bool internal _lastRecenterIsUp;
|
2026-03-11 17:51:18 +00:00
|
|
|
|
/// @dev Set to true after the first recenter call. Allows _logSnapshot to emit
|
|
|
|
|
|
/// recenter_is_up=null on the initial snapshot (before any recenter has occurred)
|
|
|
|
|
|
/// rather than the ambiguous false default.
|
|
|
|
|
|
bool internal _hasRecentered;
|
2026-03-12 13:16:41 +00:00
|
|
|
|
/// @dev Snapshot sequence counter. Shared between run() and batch ops like
|
|
|
|
|
|
/// buy_recenter_loop that emit their own snapshots internally.
|
|
|
|
|
|
uint256 internal _seq;
|
2026-03-11 02:08:06 +00:00
|
|
|
|
|
|
|
|
|
|
// ─── Entry point ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function run() external {
|
|
|
|
|
|
// Load deployment addresses before broadcast.
|
|
|
|
|
|
string memory deploymentsPath = _deploymentsPath();
|
|
|
|
|
|
string memory deployJson = vm.readFile(deploymentsPath);
|
|
|
|
|
|
lmAddr = vm.parseJsonAddress(deployJson, ".contracts.LiquidityManager");
|
|
|
|
|
|
krkAddr = vm.parseJsonAddress(deployJson, ".contracts.Kraiken");
|
|
|
|
|
|
stakeAddr = vm.parseJsonAddress(deployJson, ".contracts.Stake");
|
|
|
|
|
|
optAddr = vm.parseJsonAddress(deployJson, ".contracts.OptimizerProxy");
|
|
|
|
|
|
|
|
|
|
|
|
advAddr = vm.addr(ADV_PK);
|
|
|
|
|
|
recenterAddr = vm.addr(RECENTER_PK);
|
|
|
|
|
|
|
|
|
|
|
|
// Derive pool address from factory.
|
|
|
|
|
|
pool = IUniswapV3Pool(IUniswapV3Factory(V3_FACTORY).getPool(WETH, krkAddr, POOL_FEE));
|
|
|
|
|
|
require(address(pool) != address(0), "AttackRunner: pool not found");
|
|
|
|
|
|
token0isWeth = pool.token0() == WETH;
|
|
|
|
|
|
|
|
|
|
|
|
// Wrap ETH and set approvals for the adversary.
|
|
|
|
|
|
_setup();
|
|
|
|
|
|
|
|
|
|
|
|
// Initial state snapshot (seq=0).
|
|
|
|
|
|
_logSnapshot(0);
|
|
|
|
|
|
|
|
|
|
|
|
// Execute attack operations, snapshotting after each recenter.
|
|
|
|
|
|
string memory attackFile = vm.envString("ATTACK_FILE");
|
2026-03-12 13:16:41 +00:00
|
|
|
|
_seq = 1;
|
2026-03-11 02:08:06 +00:00
|
|
|
|
string memory line = vm.readLine(attackFile);
|
|
|
|
|
|
while (bytes(line).length > 0) {
|
|
|
|
|
|
bool isRecenter = _execute(line);
|
|
|
|
|
|
if (isRecenter) {
|
2026-03-12 13:16:41 +00:00
|
|
|
|
_logSnapshot(_seq++);
|
2026-03-11 02:08:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
line = vm.readLine(attackFile);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Final state snapshot.
|
2026-03-12 13:16:41 +00:00
|
|
|
|
_logSnapshot(_seq);
|
2026-03-11 02:08:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Setup ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// @notice Wrap ETH and pre-approve all tokens for the adversary account.
|
|
|
|
|
|
function _setup() internal {
|
|
|
|
|
|
vm.startBroadcast(ADV_PK);
|
2026-03-11 17:14:34 +00:00
|
|
|
|
// Wrap most of the adversary's ETH (leave 1 ETH for gas).
|
|
|
|
|
|
// The adversary starts with 10 000 ETH; wrapping 9 000 covers the heaviest buy sequences.
|
|
|
|
|
|
IWETH9(WETH).deposit{ value: 9_000 ether }();
|
2026-03-11 02:08:06 +00:00
|
|
|
|
IERC20(WETH).approve(SWAP_ROUTER, type(uint256).max);
|
|
|
|
|
|
IERC20(WETH).approve(NPM_ADDR, type(uint256).max);
|
|
|
|
|
|
IERC20(krkAddr).approve(SWAP_ROUTER, type(uint256).max);
|
|
|
|
|
|
IERC20(krkAddr).approve(stakeAddr, type(uint256).max);
|
|
|
|
|
|
IERC20(krkAddr).approve(NPM_ADDR, type(uint256).max);
|
|
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Operation dispatcher ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Execute a single operation line and return true if it was a recenter.
|
|
|
|
|
|
* @param line A single JSON object from the attack JSONL file.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _execute(string memory line) internal returns (bool isRecenter) {
|
|
|
|
|
|
string memory op = vm.parseJsonString(line, ".op");
|
|
|
|
|
|
|
|
|
|
|
|
if (_eq(op, "buy")) {
|
|
|
|
|
|
_executeBuy(line);
|
|
|
|
|
|
} else if (_eq(op, "sell")) {
|
|
|
|
|
|
_executeSell(line);
|
|
|
|
|
|
} else if (_eq(op, "recenter")) {
|
|
|
|
|
|
vm.startBroadcast(RECENTER_PK);
|
2026-03-12 07:57:31 +00:00
|
|
|
|
// recenter() reverts when amplitude is insufficient (price hasn't moved far enough
|
|
|
|
|
|
// from the anchor center). Use try/catch so attacks with round-trip buy→sell cycles
|
|
|
|
|
|
// — where the net price movement is near zero — don't abort the whole script.
|
|
|
|
|
|
// A caught revert means no state change occurred; skip the snapshot for this step.
|
|
|
|
|
|
try ILM(lmAddr).recenter() returns (bool isUp) {
|
|
|
|
|
|
_lastRecenterIsUp = isUp;
|
|
|
|
|
|
_hasRecentered = true;
|
|
|
|
|
|
isRecenter = true;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log("recenter: skipped (amplitude not reached)");
|
|
|
|
|
|
}
|
2026-03-11 02:08:06 +00:00
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
} else if (_eq(op, "stake")) {
|
|
|
|
|
|
_executeStake(line);
|
|
|
|
|
|
} else if (_eq(op, "unstake")) {
|
|
|
|
|
|
_executeUnstake(line);
|
|
|
|
|
|
} else if (_eq(op, "mint_lp")) {
|
|
|
|
|
|
_executeMintLp(line);
|
|
|
|
|
|
} else if (_eq(op, "burn_lp")) {
|
|
|
|
|
|
_executeBurnLp(line);
|
2026-03-12 13:16:41 +00:00
|
|
|
|
} else if (_eq(op, "buy_recenter_loop")) {
|
|
|
|
|
|
_executeBuyRecenterLoop(line);
|
2026-03-11 02:08:06 +00:00
|
|
|
|
} else if (_eq(op, "mine")) {
|
|
|
|
|
|
uint256 blocks = vm.parseJsonUint(line, ".blocks");
|
|
|
|
|
|
vm.roll(block.number + blocks);
|
2026-03-11 17:14:34 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log(string.concat("AttackRunner: unknown op '", op, "' -- skipping (check attack file for typos)"));
|
2026-03-11 02:08:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Swap WETH→KRK via SwapRouter02.
|
|
|
|
|
|
function _executeBuy(string memory line) internal {
|
|
|
|
|
|
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
|
|
|
|
|
vm.startBroadcast(ADV_PK);
|
|
|
|
|
|
ISwapRouter02(SWAP_ROUTER).exactInputSingle(
|
|
|
|
|
|
ISwapRouter02.ExactInputSingleParams({
|
|
|
|
|
|
tokenIn: WETH,
|
|
|
|
|
|
tokenOut: krkAddr,
|
|
|
|
|
|
fee: POOL_FEE,
|
|
|
|
|
|
recipient: advAddr,
|
|
|
|
|
|
amountIn: amount,
|
|
|
|
|
|
amountOutMinimum: 0,
|
|
|
|
|
|
sqrtPriceLimitX96: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 13:16:41 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @dev Batch buy→recenter loop. Executes `count` cycles of (buy `amount` WETH → recenter),
|
|
|
|
|
|
* emitting a snapshot after each successful recenter. Using a single op instead of
|
|
|
|
|
|
* individual JSONL lines eliminates per-step forge-script dispatch overhead, allowing
|
|
|
|
|
|
* high-cycle attacks (e.g. 80 cycles) to complete within the fitness evaluation budget.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Fields: count (uint), amount (wei string)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _executeBuyRecenterLoop(string memory line) internal {
|
|
|
|
|
|
uint256 count = vm.parseJsonUint(line, ".count");
|
|
|
|
|
|
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
|
|
|
|
|
|
|
|
|
|
|
for (uint256 i = 0; i < count; i++) {
|
|
|
|
|
|
// Buy WETH→KRK.
|
|
|
|
|
|
vm.startBroadcast(ADV_PK);
|
|
|
|
|
|
ISwapRouter02(SWAP_ROUTER).exactInputSingle(
|
|
|
|
|
|
ISwapRouter02.ExactInputSingleParams({
|
|
|
|
|
|
tokenIn: WETH,
|
|
|
|
|
|
tokenOut: krkAddr,
|
|
|
|
|
|
fee: POOL_FEE,
|
|
|
|
|
|
recipient: advAddr,
|
|
|
|
|
|
amountIn: amount,
|
|
|
|
|
|
amountOutMinimum: 0,
|
|
|
|
|
|
sqrtPriceLimitX96: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
|
|
|
|
|
|
// Recenter.
|
|
|
|
|
|
vm.startBroadcast(RECENTER_PK);
|
|
|
|
|
|
try ILM(lmAddr).recenter() returns (bool isUp) {
|
|
|
|
|
|
_lastRecenterIsUp = isUp;
|
|
|
|
|
|
_hasRecentered = true;
|
|
|
|
|
|
_logSnapshot(_seq++);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log("recenter: skipped (amplitude not reached)");
|
|
|
|
|
|
}
|
|
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 02:08:06 +00:00
|
|
|
|
/// @dev Swap KRK→WETH via SwapRouter02. amount="all" uses full adversary KRK balance.
|
|
|
|
|
|
function _executeSell(string memory line) internal {
|
|
|
|
|
|
string memory amtStr = vm.parseJsonString(line, ".amount");
|
|
|
|
|
|
uint256 amount = _eq(amtStr, "all") ? IERC20(krkAddr).balanceOf(advAddr) : vm.parseUint(amtStr);
|
|
|
|
|
|
if (amount == 0) return;
|
|
|
|
|
|
vm.startBroadcast(ADV_PK);
|
|
|
|
|
|
ISwapRouter02(SWAP_ROUTER).exactInputSingle(
|
|
|
|
|
|
ISwapRouter02.ExactInputSingleParams({
|
|
|
|
|
|
tokenIn: krkAddr,
|
|
|
|
|
|
tokenOut: WETH,
|
|
|
|
|
|
fee: POOL_FEE,
|
|
|
|
|
|
recipient: advAddr,
|
|
|
|
|
|
amountIn: amount,
|
|
|
|
|
|
amountOutMinimum: 0,
|
|
|
|
|
|
sqrtPriceLimitX96: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Stake KRK via Stake.snatch() with no snatching.
|
2026-03-11 17:51:18 +00:00
|
|
|
|
/// Attack files use the field key ".taxRateIndex" for backward compatibility;
|
|
|
|
|
|
/// the value is passed directly as a raw taxRate to Stake.snatch().
|
2026-03-12 07:10:13 +00:00
|
|
|
|
/// The returned positionId is appended to _stakedPositionIds so that
|
|
|
|
|
|
/// subsequent unstake ops can reference positions by 1-based index.
|
2026-03-11 02:08:06 +00:00
|
|
|
|
function _executeStake(string memory line) internal {
|
|
|
|
|
|
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
2026-03-11 17:51:18 +00:00
|
|
|
|
uint32 taxRate = uint32(vm.parseJsonUint(line, ".taxRateIndex")); // JSONL key kept for compat
|
2026-03-11 02:08:06 +00:00
|
|
|
|
vm.startBroadcast(ADV_PK);
|
2026-03-12 07:10:13 +00:00
|
|
|
|
uint256 newPositionId = IStake(stakeAddr).snatch(amount, advAddr, taxRate, new uint256[](0));
|
2026-03-11 02:08:06 +00:00
|
|
|
|
vm.stopBroadcast();
|
2026-03-12 07:10:13 +00:00
|
|
|
|
_stakedPositionIds.push(newPositionId);
|
2026-03-11 02:08:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Exit a staking position.
|
2026-03-12 07:10:13 +00:00
|
|
|
|
/// positionId in the attack file is a 1-based index into _stakedPositionIds
|
|
|
|
|
|
/// (the positions created by stake ops in this run), not the raw on-chain ID.
|
|
|
|
|
|
/// Stake.nextPositionId starts at 654_321, so literal IDs like "1" would always
|
|
|
|
|
|
/// revert with PositionNotFound — the index-based lookup resolves the real ID.
|
2026-03-11 02:08:06 +00:00
|
|
|
|
function _executeUnstake(string memory line) internal {
|
2026-03-12 07:10:13 +00:00
|
|
|
|
uint256 positionIndex = vm.parseJsonUint(line, ".positionId");
|
|
|
|
|
|
require(positionIndex >= 1 && positionIndex <= _stakedPositionIds.length,
|
|
|
|
|
|
"AttackRunner: unstake positionId out of range (must be 1-based index of a prior stake op)");
|
|
|
|
|
|
uint256 realPositionId = _stakedPositionIds[positionIndex - 1];
|
2026-03-11 02:08:06 +00:00
|
|
|
|
vm.startBroadcast(ADV_PK);
|
2026-03-12 07:10:13 +00:00
|
|
|
|
IStake(stakeAddr).exitPosition(realPositionId);
|
2026-03-11 02:08:06 +00:00
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Mint a Uniswap V3 LP position via NPM.
|
|
|
|
|
|
function _executeMintLp(string memory line) internal {
|
|
|
|
|
|
int24 tickLower = int24(vm.parseJsonInt(line, ".tickLower"));
|
|
|
|
|
|
int24 tickUpper = int24(vm.parseJsonInt(line, ".tickUpper"));
|
|
|
|
|
|
uint256 amount0 = vm.parseUint(vm.parseJsonString(line, ".amount0"));
|
|
|
|
|
|
uint256 amount1 = vm.parseUint(vm.parseJsonString(line, ".amount1"));
|
|
|
|
|
|
|
|
|
|
|
|
// Determine token order for NPM params (token0 < token1 by address).
|
|
|
|
|
|
(address t0, address t1) = token0isWeth ? (WETH, krkAddr) : (krkAddr, WETH);
|
|
|
|
|
|
|
|
|
|
|
|
vm.startBroadcast(ADV_PK);
|
|
|
|
|
|
INonfungiblePositionManager(NPM_ADDR).mint(
|
|
|
|
|
|
INonfungiblePositionManager.MintParams({
|
|
|
|
|
|
token0: t0,
|
|
|
|
|
|
token1: t1,
|
|
|
|
|
|
fee: POOL_FEE,
|
|
|
|
|
|
tickLower: tickLower,
|
|
|
|
|
|
tickUpper: tickUpper,
|
|
|
|
|
|
amount0Desired: amount0,
|
|
|
|
|
|
amount1Desired: amount1,
|
|
|
|
|
|
amount0Min: 0,
|
|
|
|
|
|
amount1Min: 0,
|
|
|
|
|
|
recipient: advAddr,
|
|
|
|
|
|
deadline: block.timestamp + 3600
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Burn a Uniswap V3 LP position (decreaseLiquidity + collect).
|
|
|
|
|
|
function _executeBurnLp(string memory line) internal {
|
|
|
|
|
|
uint256 tokenId = vm.parseJsonUint(line, ".tokenId");
|
|
|
|
|
|
|
|
|
|
|
|
// Read current liquidity for this token.
|
|
|
|
|
|
(,,,,,,,uint128 liquidity,,,,) = INonfungiblePositionManager(NPM_ADDR).positions(tokenId);
|
|
|
|
|
|
if (liquidity == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
vm.startBroadcast(ADV_PK);
|
|
|
|
|
|
INonfungiblePositionManager(NPM_ADDR).decreaseLiquidity(
|
|
|
|
|
|
INonfungiblePositionManager.DecreaseLiquidityParams({
|
|
|
|
|
|
tokenId: tokenId,
|
|
|
|
|
|
liquidity: liquidity,
|
|
|
|
|
|
amount0Min: 0,
|
|
|
|
|
|
amount1Min: 0,
|
|
|
|
|
|
deadline: block.timestamp + 3600
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
INonfungiblePositionManager(NPM_ADDR).collect(
|
|
|
|
|
|
INonfungiblePositionManager.CollectParams({
|
|
|
|
|
|
tokenId: tokenId,
|
|
|
|
|
|
recipient: advAddr,
|
|
|
|
|
|
amount0Max: type(uint128).max,
|
|
|
|
|
|
amount1Max: type(uint128).max
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
vm.stopBroadcast();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── State snapshot ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Emit a full state snapshot as a JSON line on stdout.
|
|
|
|
|
|
* @param seq Sequence number (0 = initial, N = after Nth recenter, final = last).
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _logSnapshot(uint256 seq) internal view {
|
|
|
|
|
|
(uint160 sqrtPriceX96, int24 tick,,,,,) = pool.slot0();
|
|
|
|
|
|
|
|
|
|
|
|
// LM free balances.
|
|
|
|
|
|
uint256 lmEthFree = lmAddr.balance;
|
|
|
|
|
|
uint256 lmWethFree = IERC20(WETH).balanceOf(lmAddr);
|
|
|
|
|
|
|
|
|
|
|
|
// Position ETH values (using LiquidityAmounts at current sqrtPrice).
|
|
|
|
|
|
(uint128 fLiq, int24 fLo, int24 fHi) = ILM(lmAddr).positions(0); // FLOOR
|
|
|
|
|
|
(uint128 aLiq, int24 aLo, int24 aHi) = ILM(lmAddr).positions(1); // ANCHOR
|
|
|
|
|
|
(uint128 dLiq, int24 dLo, int24 dHi) = ILM(lmAddr).positions(2); // DISCOVERY
|
|
|
|
|
|
|
|
|
|
|
|
uint256 fEthValue = _positionEthValue(sqrtPriceX96, fLo, fHi, fLiq);
|
|
|
|
|
|
uint256 aEthValue = _positionEthValue(sqrtPriceX96, aLo, aHi, aLiq);
|
|
|
|
|
|
uint256 dEthValue = _positionEthValue(sqrtPriceX96, dLo, dHi, dLiq);
|
|
|
|
|
|
|
|
|
|
|
|
uint256 lmEthTotal = lmEthFree + lmWethFree + fEthValue + aEthValue + dEthValue;
|
|
|
|
|
|
|
|
|
|
|
|
// VWAP.
|
|
|
|
|
|
uint256 vwapX96 = ILM(lmAddr).getVWAP();
|
|
|
|
|
|
int24 vwapTick = _computeVwapTick(vwapX96);
|
|
|
|
|
|
|
|
|
|
|
|
// Token supply.
|
|
|
|
|
|
uint256 outstandingSupply = IKraiken(krkAddr).outstandingSupply();
|
|
|
|
|
|
uint256 totalSupply = IERC20(krkAddr).totalSupply();
|
|
|
|
|
|
|
|
|
|
|
|
// Optimizer output (may revert if optimizer doesn't implement getLiquidityParams).
|
|
|
|
|
|
uint256 anchorShare;
|
|
|
|
|
|
uint256 capitalInefficiency;
|
|
|
|
|
|
uint24 anchorWidth;
|
|
|
|
|
|
uint256 discoveryDepth;
|
|
|
|
|
|
try IOptimizer(optAddr).getLiquidityParams() returns (uint256 ci, uint256 as_, uint24 aw, uint256 dd) {
|
|
|
|
|
|
capitalInefficiency = ci;
|
|
|
|
|
|
anchorShare = as_;
|
|
|
|
|
|
anchorWidth = aw;
|
|
|
|
|
|
discoveryDepth = dd;
|
|
|
|
|
|
} catch { }
|
|
|
|
|
|
|
|
|
|
|
|
// Adversary balances.
|
|
|
|
|
|
uint256 advEth = advAddr.balance;
|
|
|
|
|
|
uint256 advKrk = IERC20(krkAddr).balanceOf(advAddr);
|
|
|
|
|
|
|
|
|
|
|
|
// Emit snapshot as a single JSON line.
|
2026-03-11 17:51:18 +00:00
|
|
|
|
// NOTE: pool.slot0() is read as a view call; forge-std finalises broadcast state before
|
|
|
|
|
|
// executing view calls, so the sqrtPriceX96/tick values are always post-broadcast.
|
2026-03-11 02:08:06 +00:00
|
|
|
|
console.log(_buildSnapshotJson(
|
|
|
|
|
|
seq, tick, lmEthFree, lmWethFree, lmEthTotal,
|
|
|
|
|
|
fLiq, fLo, fHi, fEthValue,
|
|
|
|
|
|
aLiq, aLo, aHi, aEthValue,
|
2026-03-11 17:14:34 +00:00
|
|
|
|
dLiq, dLo, dHi, dEthValue,
|
2026-03-11 02:08:06 +00:00
|
|
|
|
vwapX96, vwapTick,
|
|
|
|
|
|
outstandingSupply, totalSupply,
|
|
|
|
|
|
anchorShare, capitalInefficiency, anchorWidth, discoveryDepth,
|
2026-03-11 17:14:34 +00:00
|
|
|
|
advEth, advKrk,
|
2026-03-11 17:51:18 +00:00
|
|
|
|
_lastRecenterIsUp, _hasRecentered
|
2026-03-11 02:08:06 +00:00
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── JSON builder ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Builds the snapshot JSON string. Split into a helper to avoid stack-too-deep.
|
|
|
|
|
|
function _buildSnapshotJson(
|
|
|
|
|
|
uint256 seq,
|
|
|
|
|
|
int24 tick,
|
|
|
|
|
|
uint256 lmEthFree,
|
|
|
|
|
|
uint256 lmWethFree,
|
|
|
|
|
|
uint256 lmEthTotal,
|
|
|
|
|
|
uint128 fLiq,
|
|
|
|
|
|
int24 fLo,
|
|
|
|
|
|
int24 fHi,
|
|
|
|
|
|
uint256 fEthValue,
|
|
|
|
|
|
uint128 aLiq,
|
|
|
|
|
|
int24 aLo,
|
|
|
|
|
|
int24 aHi,
|
|
|
|
|
|
uint256 aEthValue,
|
|
|
|
|
|
uint128 dLiq,
|
|
|
|
|
|
int24 dLo,
|
|
|
|
|
|
int24 dHi,
|
2026-03-11 17:14:34 +00:00
|
|
|
|
uint256 dEthValue,
|
2026-03-11 02:08:06 +00:00
|
|
|
|
uint256 vwapX96,
|
|
|
|
|
|
int24 vwapTick,
|
|
|
|
|
|
uint256 outstandingSupply,
|
|
|
|
|
|
uint256 totalSupply,
|
|
|
|
|
|
uint256 anchorShare,
|
|
|
|
|
|
uint256 capitalInefficiency,
|
|
|
|
|
|
uint24 anchorWidth,
|
|
|
|
|
|
uint256 discoveryDepth,
|
|
|
|
|
|
uint256 advEth,
|
2026-03-11 17:14:34 +00:00
|
|
|
|
uint256 advKrk,
|
2026-03-11 17:51:18 +00:00
|
|
|
|
bool recenterIsUp,
|
|
|
|
|
|
bool hasRecentered
|
2026-03-11 02:08:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
internal
|
|
|
|
|
|
pure
|
|
|
|
|
|
returns (string memory)
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.concat(
|
|
|
|
|
|
_snapshotHeader(seq, tick, lmEthFree, lmWethFree, lmEthTotal),
|
2026-03-11 17:14:34 +00:00
|
|
|
|
_snapshotPositions(fLiq, fLo, fHi, fEthValue, aLiq, aLo, aHi, aEthValue, dLiq, dLo, dHi, dEthValue),
|
2026-03-11 02:08:06 +00:00
|
|
|
|
_snapshotFooter(
|
|
|
|
|
|
vwapX96, vwapTick,
|
|
|
|
|
|
outstandingSupply, totalSupply,
|
|
|
|
|
|
anchorShare, capitalInefficiency, anchorWidth, discoveryDepth,
|
2026-03-11 17:14:34 +00:00
|
|
|
|
advEth, advKrk,
|
2026-03-11 17:51:18 +00:00
|
|
|
|
recenterIsUp, hasRecentered
|
2026-03-11 02:08:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _snapshotHeader(
|
|
|
|
|
|
uint256 seq,
|
|
|
|
|
|
int24 tick,
|
|
|
|
|
|
uint256 lmEthFree,
|
|
|
|
|
|
uint256 lmWethFree,
|
|
|
|
|
|
uint256 lmEthTotal
|
|
|
|
|
|
)
|
|
|
|
|
|
internal
|
|
|
|
|
|
pure
|
|
|
|
|
|
returns (string memory)
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.concat(
|
|
|
|
|
|
'{"seq":',
|
|
|
|
|
|
seq.str(),
|
|
|
|
|
|
',"tick":',
|
|
|
|
|
|
int256(tick).istr(),
|
|
|
|
|
|
',"lm_eth_free":"',
|
|
|
|
|
|
lmEthFree.str(),
|
|
|
|
|
|
'","lm_weth_free":"',
|
|
|
|
|
|
lmWethFree.str(),
|
|
|
|
|
|
'","lm_eth_total":"',
|
|
|
|
|
|
lmEthTotal.str(),
|
|
|
|
|
|
'"'
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _snapshotPositions(
|
|
|
|
|
|
uint128 fLiq,
|
|
|
|
|
|
int24 fLo,
|
|
|
|
|
|
int24 fHi,
|
|
|
|
|
|
uint256 fEthValue,
|
|
|
|
|
|
uint128 aLiq,
|
|
|
|
|
|
int24 aLo,
|
|
|
|
|
|
int24 aHi,
|
|
|
|
|
|
uint256 aEthValue,
|
|
|
|
|
|
uint128 dLiq,
|
|
|
|
|
|
int24 dLo,
|
2026-03-11 17:14:34 +00:00
|
|
|
|
int24 dHi,
|
|
|
|
|
|
uint256 dEthValue
|
2026-03-11 02:08:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
internal
|
|
|
|
|
|
pure
|
|
|
|
|
|
returns (string memory)
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.concat(
|
|
|
|
|
|
',"positions":{"floor":{"liquidity":"',
|
|
|
|
|
|
uint256(fLiq).str(),
|
|
|
|
|
|
'","tickLower":',
|
|
|
|
|
|
int256(fLo).istr(),
|
|
|
|
|
|
',"tickUpper":',
|
|
|
|
|
|
int256(fHi).istr(),
|
|
|
|
|
|
',"ethValue":"',
|
|
|
|
|
|
fEthValue.str(),
|
|
|
|
|
|
'"},"anchor":{"liquidity":"',
|
|
|
|
|
|
uint256(aLiq).str(),
|
|
|
|
|
|
'","tickLower":',
|
|
|
|
|
|
int256(aLo).istr(),
|
|
|
|
|
|
',"tickUpper":',
|
|
|
|
|
|
int256(aHi).istr(),
|
|
|
|
|
|
',"ethValue":"',
|
|
|
|
|
|
aEthValue.str(),
|
|
|
|
|
|
'"},"discovery":{"liquidity":"',
|
|
|
|
|
|
uint256(dLiq).str(),
|
|
|
|
|
|
'","tickLower":',
|
|
|
|
|
|
int256(dLo).istr(),
|
|
|
|
|
|
',"tickUpper":',
|
|
|
|
|
|
int256(dHi).istr(),
|
2026-03-11 17:14:34 +00:00
|
|
|
|
',"ethValue":"',
|
|
|
|
|
|
dEthValue.str(),
|
2026-03-11 17:51:18 +00:00
|
|
|
|
'"}}' // close discovery{} then positions{}; root object is closed by _snapshotFooter
|
2026-03-11 02:08:06 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _snapshotFooter(
|
|
|
|
|
|
uint256 vwapX96,
|
|
|
|
|
|
int24 vwapTick,
|
|
|
|
|
|
uint256 outstandingSupply,
|
|
|
|
|
|
uint256 totalSupply,
|
|
|
|
|
|
uint256 anchorShare,
|
|
|
|
|
|
uint256 capitalInefficiency,
|
|
|
|
|
|
uint24 anchorWidth,
|
|
|
|
|
|
uint256 discoveryDepth,
|
|
|
|
|
|
uint256 advEth,
|
2026-03-11 17:14:34 +00:00
|
|
|
|
uint256 advKrk,
|
2026-03-11 17:51:18 +00:00
|
|
|
|
bool recenterIsUp,
|
|
|
|
|
|
bool hasRecentered
|
2026-03-11 02:08:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
internal
|
|
|
|
|
|
pure
|
|
|
|
|
|
returns (string memory)
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.concat(
|
|
|
|
|
|
',"vwap_x96":"',
|
|
|
|
|
|
vwapX96.str(),
|
|
|
|
|
|
'","vwap_tick":',
|
|
|
|
|
|
int256(vwapTick).istr(),
|
|
|
|
|
|
',"outstanding_supply":"',
|
|
|
|
|
|
outstandingSupply.str(),
|
|
|
|
|
|
'","total_supply":"',
|
|
|
|
|
|
totalSupply.str(),
|
|
|
|
|
|
'","optimizer_output":{"anchorShare":"',
|
|
|
|
|
|
anchorShare.str(),
|
|
|
|
|
|
'","capitalInefficiency":"',
|
|
|
|
|
|
capitalInefficiency.str(),
|
|
|
|
|
|
'","anchorWidth":',
|
|
|
|
|
|
uint256(anchorWidth).str(),
|
|
|
|
|
|
',"discoveryWidth":"',
|
|
|
|
|
|
discoveryDepth.str(),
|
|
|
|
|
|
'"},"adversary_eth":"',
|
|
|
|
|
|
advEth.str(),
|
|
|
|
|
|
'","adversary_krk":"',
|
|
|
|
|
|
advKrk.str(),
|
2026-03-11 17:51:18 +00:00
|
|
|
|
// Emit null before the first recenter so downstream parsers can distinguish
|
|
|
|
|
|
// "no recenter yet" from "last recenter moved price down" (false).
|
2026-03-11 17:14:34 +00:00
|
|
|
|
'","recenter_is_up":',
|
2026-03-11 17:51:18 +00:00
|
|
|
|
hasRecentered ? (recenterIsUp ? "true" : "false") : "null",
|
2026-03-11 17:14:34 +00:00
|
|
|
|
"}"
|
2026-03-11 02:08:06 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Math helpers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-11 17:14:34 +00:00
|
|
|
|
* @notice Compute the total ETH-equivalent value of a liquidity position at the current pool price.
|
2026-03-11 02:08:06 +00:00
|
|
|
|
* @dev Uses LiquidityAmounts.getAmountsForLiquidity which handles all three cases:
|
|
|
|
|
|
* fully below range (all token0), fully above range (all token1), and in-range (split).
|
2026-03-11 17:14:34 +00:00
|
|
|
|
* Both the ETH component and the KRK component (converted to ETH at current sqrtPriceX96)
|
|
|
|
|
|
* are summed so that lm_eth_total accurately reflects TVL regardless of price range.
|
|
|
|
|
|
*
|
|
|
|
|
|
* KRK→ETH conversion:
|
|
|
|
|
|
* If token0=WETH: price = KRK/WETH = sqrtP^2/2^192
|
|
|
|
|
|
* ⟹ krkInEth = krk * 2^192 / sqrtP^2 = mulDiv(mulDiv(krk, 2^96, sqrtP), 2^96, sqrtP)
|
|
|
|
|
|
* If token0=KRK: price = WETH/KRK = sqrtP^2/2^192
|
|
|
|
|
|
* ⟹ krkInEth = krk * sqrtP^2 / 2^192 = mulDiv(mulDiv(krk, sqrtP, 2^96), sqrtP, 2^96)
|
2026-03-11 02:08:06 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function _positionEthValue(
|
|
|
|
|
|
uint160 sqrtPriceX96,
|
|
|
|
|
|
int24 tickLower,
|
|
|
|
|
|
int24 tickUpper,
|
|
|
|
|
|
uint128 liquidity
|
|
|
|
|
|
)
|
|
|
|
|
|
internal
|
|
|
|
|
|
view
|
|
|
|
|
|
returns (uint256)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (liquidity == 0) return 0;
|
|
|
|
|
|
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
|
|
|
|
|
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
|
|
|
|
|
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, liquidity);
|
2026-03-11 17:14:34 +00:00
|
|
|
|
|
|
|
|
|
|
uint256 ethAmount = token0isWeth ? amount0 : amount1;
|
|
|
|
|
|
uint256 krkAmount = token0isWeth ? amount1 : amount0;
|
|
|
|
|
|
|
|
|
|
|
|
uint256 krkInEth = 0;
|
|
|
|
|
|
if (krkAmount > 0 && sqrtPriceX96 > 0) {
|
|
|
|
|
|
if (token0isWeth) {
|
|
|
|
|
|
// token0=WETH, token1=KRK: 1 KRK = 2^192 / sqrtP^2 WETH
|
|
|
|
|
|
krkInEth = FullMath.mulDiv(
|
|
|
|
|
|
FullMath.mulDiv(krkAmount, 1 << 96, sqrtPriceX96),
|
|
|
|
|
|
1 << 96,
|
|
|
|
|
|
sqrtPriceX96
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// token0=KRK, token1=WETH: 1 KRK = sqrtP^2 / 2^192 WETH
|
|
|
|
|
|
krkInEth = FullMath.mulDiv(
|
|
|
|
|
|
FullMath.mulDiv(krkAmount, sqrtPriceX96, 1 << 96),
|
|
|
|
|
|
sqrtPriceX96,
|
|
|
|
|
|
1 << 96
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ethAmount + krkInEth;
|
2026-03-11 02:08:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Convert a VWAP X96 value to a pool tick.
|
|
|
|
|
|
* @dev VWAP is stored as price * 2^96 (Q96 format) by _priceAtTick in UniswapMath.
|
|
|
|
|
|
* To recover the tick: sqrt(vwapX96 / 2^96) = sqrtPrice, then getTickAtSqrtRatio.
|
|
|
|
|
|
* Shift right by 32 converts Q96 → Q64 (ABDK 64x64 format).
|
2026-03-11 17:14:34 +00:00
|
|
|
|
*
|
|
|
|
|
|
* Overflow guard: int128(int256(vwapX96 >> 32)) wraps to a negative value when
|
|
|
|
|
|
* vwapX96 > 2^159 (extremely high price ratios outside realistic KRK/WETH ranges).
|
|
|
|
|
|
* The `priceRatioX64 <= 0` check catches this and returns tick=0 rather than reverting,
|
|
|
|
|
|
* so snapshots remain valid — callers should treat vwap_tick=0 as "VWAP unavailable"
|
|
|
|
|
|
* when vwap_x96 is non-zero. Additionally, ABDKMath64x64.sqrt(priceRatioX64) << 32
|
|
|
|
|
|
* could overflow int128 before the uint160 cast for pathologically large prices, but
|
|
|
|
|
|
* this is unreachable for any token pair with price < 2^32 (covers all practical cases).
|
|
|
|
|
|
*
|
2026-03-11 02:08:06 +00:00
|
|
|
|
* @param vwapX96 The VWAP in Q96 format (as returned by LM.getVWAP()).
|
2026-03-11 17:14:34 +00:00
|
|
|
|
* @return The pool tick corresponding to the VWAP price, or 0 if vwapX96 is out of range.
|
2026-03-11 02:08:06 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function _computeVwapTick(uint256 vwapX96) internal pure returns (int24) {
|
|
|
|
|
|
if (vwapX96 == 0) return 0;
|
|
|
|
|
|
int128 priceRatioX64 = int128(int256(vwapX96 >> 32));
|
2026-03-11 17:14:34 +00:00
|
|
|
|
if (priceRatioX64 <= 0) return 0; // vwapX96 > 2^159: out of representable range, report 0
|
2026-03-11 02:08:06 +00:00
|
|
|
|
uint160 sqrtPriceX96_ = uint160(int160(ABDKMath64x64.sqrt(priceRatioX64) << 32));
|
|
|
|
|
|
return TickMath.getTickAtSqrtRatio(sqrtPriceX96_);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Utility ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function _eq(string memory a, string memory b) internal pure returns (bool) {
|
|
|
|
|
|
return keccak256(bytes(a)) == keccak256(bytes(b));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _deploymentsPath() internal view returns (string memory) {
|
|
|
|
|
|
try vm.envString("DEPLOYMENTS_FILE") returns (string memory path) {
|
|
|
|
|
|
if (bytes(path).length > 0) return path;
|
|
|
|
|
|
} catch { }
|
|
|
|
|
|
return "deployments-local.json";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|