fix: fix: Debug failing staking-safe attack in evolution fitness (#596)

Stake.nextPositionId starts at 654_321, so attack files cannot use literal
on-chain IDs (e.g. positionId=1 always reverts with PositionNotFound).

Fix AttackRunner to treat the JSONL positionId field as a 1-based index into
the list of positions created by stake ops during the current run:
- Add IStake.snatch returns (uint256) to the interface so the returned ID is
  captured.
- Track returned IDs in _stakedPositionIds[] (inserted in creation order).
- _executeUnstake resolves positionId to _stakedPositionIds[positionId-1]
  before calling exitPosition, matching the natural "unstake position 1"
  semantics in the attack DSL.

KRK approval for Stake was already present in _setup(); no other changes needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-12 07:10:13 +00:00
parent 6f3601711b
commit 78246ed399

View file

@ -50,7 +50,7 @@ interface IOptimizer {
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 snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) external returns (uint256 positionId);
function exitPosition(uint256 positionId) external;
}
@ -169,6 +169,9 @@ contract AttackRunner is Script {
address internal optAddr;
IUniswapV3Pool internal pool;
bool internal token0isWeth;
/// @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 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;
@ -313,19 +316,29 @@ contract AttackRunner is Script {
/// @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().
/// The returned positionId is appended to _stakedPositionIds so that
/// subsequent unstake ops can reference positions by 1-based index.
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));
uint256 newPositionId = IStake(stakeAddr).snatch(amount, advAddr, taxRate, new uint256[](0));
vm.stopBroadcast();
_stakedPositionIds.push(newPositionId);
}
/// @dev Exit a staking position.
/// 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.
function _executeUnstake(string memory line) internal {
uint256 positionId = vm.parseJsonUint(line, ".positionId");
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];
vm.startBroadcast(ADV_PK);
IStake(stakeAddr).exitPosition(positionId);
IStake(stakeAddr).exitPosition(realPositionId);
vm.stopBroadcast();
}