diff --git a/onchain/script/backtesting/AttackRunner.s.sol b/onchain/script/backtesting/AttackRunner.s.sol index a7fced0..4cd0c31 100644 --- a/onchain/script/backtesting/AttackRunner.s.sol +++ b/onchain/script/backtesting/AttackRunner.s.sol @@ -136,7 +136,7 @@ interface IUniswapV3Factory { * 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 + * burn_lp Remove LP via NPM. Fields: positionIndex (1-based index of a prior mint_lp op) * mine Advance block number. Fields: blocks * * Snapshot schema (emitted as JSON line on stdout): @@ -178,6 +178,10 @@ contract AttackRunner is Script { /// @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; + /// @dev NFT token IDs returned by NPM.mint(), in insertion order. + /// Attack files reference LP positions by 1-based index (positionIndex=1 → _mintedLpTokenIds[0]). + /// This makes burn_lp fork-block-independent — the tokenId is resolved at runtime. + uint256[] internal _mintedLpTokenIds; /// @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; @@ -415,7 +419,7 @@ contract AttackRunner is Script { (address t0, address t1) = token0isWeth ? (WETH, krkAddr) : (krkAddr, WETH); vm.startBroadcast(ADV_PK); - INonfungiblePositionManager(npmAddr).mint( + (uint256 tokenId,,,) = INonfungiblePositionManager(npmAddr).mint( INonfungiblePositionManager.MintParams({ token0: t0, token1: t1, @@ -431,15 +435,32 @@ contract AttackRunner is Script { }) ); vm.stopBroadcast(); + _mintedLpTokenIds.push(tokenId); } /// @dev Burn a Uniswap V3 LP position (decreaseLiquidity + collect). + /// positionIndex in the attack file is a 1-based index into _mintedLpTokenIds + /// (the LP positions created by mint_lp ops in this run), not a raw on-chain + /// NFT token ID. This makes burn_lp fork-block-independent — the actual tokenId + /// is resolved at runtime from the mint_lp that created it. function _executeBurnLp(string memory line) internal { - uint256 tokenId = vm.parseJsonUint(line, ".tokenId"); + uint256 positionIndex = vm.parseJsonUint(line, ".positionIndex"); + require(positionIndex >= 1 && positionIndex <= _mintedLpTokenIds.length, + "AttackRunner: burn_lp positionIndex out of range (must be 1-based index of a prior mint_lp op)"); + uint256 tokenId = _mintedLpTokenIds[positionIndex - 1]; // Read current liquidity for this token. (,,,,,,,uint128 liquidity,,,,) = INonfungiblePositionManager(npmAddr).positions(tokenId); - if (liquidity == 0) return; + if (liquidity == 0) { + console.log(string.concat( + "burn_lp: WARNING - tokenId ", + vm.toString(tokenId), + " (positionIndex ", + vm.toString(positionIndex), + ") has zero liquidity, skipping (possible fork-block mismatch)" + )); + return; + } vm.startBroadcast(ADV_PK); INonfungiblePositionManager(npmAddr).decreaseLiquidity(