Merge pull request 'fix: Backtesting: replay red-team attack sequences against optimizer candidates (#536)' (#565) from fix/issue-536 into master
This commit is contained in:
commit
514a55a1ac
8 changed files with 1335 additions and 2 deletions
717
onchain/script/backtesting/AttackRunner.s.sol
Normal file
717
onchain/script/backtesting/AttackRunner.s.sol
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { FormatLib } from "./FormatLib.sol";
|
||||
import { FullMath } from "@aperture/uni-v3-lib/FullMath.sol";
|
||||
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 {
|
||||
// taxRate matches the actual Stake.sol parameter name (a raw rate value, not a lookup index)
|
||||
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) external;
|
||||
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:
|
||||
* 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.
|
||||
* stake Call Stake.snatch(). Fields: amount (wei string), taxRateIndex (raw taxRate value passed to Stake.snatch)
|
||||
* 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;
|
||||
/// @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;
|
||||
/// @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;
|
||||
|
||||
// ─── 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");
|
||||
uint256 seq = 1;
|
||||
string memory line = vm.readLine(attackFile);
|
||||
while (bytes(line).length > 0) {
|
||||
bool isRecenter = _execute(line);
|
||||
if (isRecenter) {
|
||||
_logSnapshot(seq++);
|
||||
}
|
||||
line = vm.readLine(attackFile);
|
||||
}
|
||||
|
||||
// Final state snapshot.
|
||||
_logSnapshot(seq);
|
||||
}
|
||||
|
||||
// ─── Setup ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// @notice Wrap ETH and pre-approve all tokens for the adversary account.
|
||||
function _setup() internal {
|
||||
vm.startBroadcast(ADV_PK);
|
||||
// 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 }();
|
||||
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);
|
||||
// Capture direction: true = price moved up, false = price moved down.
|
||||
// recenter() reverts (not returns false) when amplitude is insufficient,
|
||||
// so a successful call is always a real recenter regardless of direction.
|
||||
_lastRecenterIsUp = ILM(lmAddr).recenter();
|
||||
_hasRecentered = true;
|
||||
vm.stopBroadcast();
|
||||
isRecenter = true;
|
||||
} 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);
|
||||
} else if (_eq(op, "mine")) {
|
||||
uint256 blocks = vm.parseJsonUint(line, ".blocks");
|
||||
vm.roll(block.number + blocks);
|
||||
} else {
|
||||
console.log(string.concat("AttackRunner: unknown op '", op, "' -- skipping (check attack file for typos)"));
|
||||
}
|
||||
}
|
||||
|
||||
/// @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();
|
||||
}
|
||||
|
||||
/// @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.
|
||||
/// Attack files use the field key ".taxRateIndex" for backward compatibility;
|
||||
/// the value is passed directly as a raw taxRate to Stake.snatch().
|
||||
function _executeStake(string memory line) internal {
|
||||
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
||||
uint32 taxRate = uint32(vm.parseJsonUint(line, ".taxRateIndex")); // JSONL key kept for compat
|
||||
vm.startBroadcast(ADV_PK);
|
||||
IStake(stakeAddr).snatch(amount, advAddr, taxRate, new uint256[](0));
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
|
||||
/// @dev Exit a staking position.
|
||||
function _executeUnstake(string memory line) internal {
|
||||
uint256 positionId = vm.parseJsonUint(line, ".positionId");
|
||||
vm.startBroadcast(ADV_PK);
|
||||
IStake(stakeAddr).exitPosition(positionId);
|
||||
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.
|
||||
// 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.
|
||||
console.log(_buildSnapshotJson(
|
||||
seq, tick, lmEthFree, lmWethFree, lmEthTotal,
|
||||
fLiq, fLo, fHi, fEthValue,
|
||||
aLiq, aLo, aHi, aEthValue,
|
||||
dLiq, dLo, dHi, dEthValue,
|
||||
vwapX96, vwapTick,
|
||||
outstandingSupply, totalSupply,
|
||||
anchorShare, capitalInefficiency, anchorWidth, discoveryDepth,
|
||||
advEth, advKrk,
|
||||
_lastRecenterIsUp, _hasRecentered
|
||||
));
|
||||
}
|
||||
|
||||
// ─── 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,
|
||||
uint256 dEthValue,
|
||||
uint256 vwapX96,
|
||||
int24 vwapTick,
|
||||
uint256 outstandingSupply,
|
||||
uint256 totalSupply,
|
||||
uint256 anchorShare,
|
||||
uint256 capitalInefficiency,
|
||||
uint24 anchorWidth,
|
||||
uint256 discoveryDepth,
|
||||
uint256 advEth,
|
||||
uint256 advKrk,
|
||||
bool recenterIsUp,
|
||||
bool hasRecentered
|
||||
)
|
||||
internal
|
||||
pure
|
||||
returns (string memory)
|
||||
{
|
||||
return string.concat(
|
||||
_snapshotHeader(seq, tick, lmEthFree, lmWethFree, lmEthTotal),
|
||||
_snapshotPositions(fLiq, fLo, fHi, fEthValue, aLiq, aLo, aHi, aEthValue, dLiq, dLo, dHi, dEthValue),
|
||||
_snapshotFooter(
|
||||
vwapX96, vwapTick,
|
||||
outstandingSupply, totalSupply,
|
||||
anchorShare, capitalInefficiency, anchorWidth, discoveryDepth,
|
||||
advEth, advKrk,
|
||||
recenterIsUp, hasRecentered
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
int24 dHi,
|
||||
uint256 dEthValue
|
||||
)
|
||||
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(),
|
||||
',"ethValue":"',
|
||||
dEthValue.str(),
|
||||
'"}}' // close discovery{} then positions{}; root object is closed by _snapshotFooter
|
||||
);
|
||||
}
|
||||
|
||||
function _snapshotFooter(
|
||||
uint256 vwapX96,
|
||||
int24 vwapTick,
|
||||
uint256 outstandingSupply,
|
||||
uint256 totalSupply,
|
||||
uint256 anchorShare,
|
||||
uint256 capitalInefficiency,
|
||||
uint24 anchorWidth,
|
||||
uint256 discoveryDepth,
|
||||
uint256 advEth,
|
||||
uint256 advKrk,
|
||||
bool recenterIsUp,
|
||||
bool hasRecentered
|
||||
)
|
||||
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(),
|
||||
// Emit null before the first recenter so downstream parsers can distinguish
|
||||
// "no recenter yet" from "last recenter moved price down" (false).
|
||||
'","recenter_is_up":',
|
||||
hasRecentered ? (recenterIsUp ? "true" : "false") : "null",
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Math helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @notice Compute the total ETH-equivalent value of a liquidity position at the current pool price.
|
||||
* @dev Uses LiquidityAmounts.getAmountsForLiquidity which handles all three cases:
|
||||
* fully below range (all token0), fully above range (all token1), and in-range (split).
|
||||
* 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)
|
||||
*/
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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).
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* @param vwapX96 The VWAP in Q96 format (as returned by LM.getVWAP()).
|
||||
* @return The pool tick corresponding to the VWAP price, or 0 if vwapX96 is out of range.
|
||||
*/
|
||||
function _computeVwapTick(uint256 vwapX96) internal pure returns (int24) {
|
||||
if (vwapX96 == 0) return 0;
|
||||
int128 priceRatioX64 = int128(int256(vwapX96 >> 32));
|
||||
if (priceRatioX64 <= 0) return 0; // vwapX96 > 2^159: out of representable range, report 0
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
153
onchain/script/backtesting/attacks/il-crystallization-80.jsonl
Normal file
153
onchain/script/backtesting/attacks/il-crystallization-80.jsonl
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
60
onchain/script/backtesting/attacks/round-trip-safe.jsonl
Normal file
60
onchain/script/backtesting/attacks/round-trip-safe.jsonl
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
14
onchain/script/backtesting/attacks/staking-safe.jsonl
Normal file
14
onchain/script/backtesting/attacks/staking-safe.jsonl
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{"op":"buy","amount":"50000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"50000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":5}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"50000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"unstake","positionId":1}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue