fix: revm evaluator — UUPS bypass, deployedBytecode, graceful attack ops
- Skip UUPS upgradeTo: etch + vm.store ERC1967 implementation slot directly (OptimizerV3Push3 is standalone, no UUPS inheritance needed for evolution) - Use deployedBytecode (runtime) instead of bytecode (creation) for vm.etch - Inject transpiled body into OptimizerV3.sol (has getLiquidityParams via Optimizer) instead of using standalone OptimizerV3Push3.sol - Wrap buy/sell/stake/unstake in try/catch — attack ops should not abort the batch - Add /tmp read to fs_permissions for batch-eval manifest files - Bootstrap recenter returns bool instead of reverting (soft-fail per candidate)
This commit is contained in:
parent
b34e26eb70
commit
87bb5859e2
3 changed files with 97 additions and 30 deletions
|
|
@ -2,7 +2,7 @@
|
|||
src = "src"
|
||||
out = "out"
|
||||
libs = ["lib"]
|
||||
fs_permissions = [{ access = "read-write", path = "./"}]
|
||||
fs_permissions = [{ access = "read-write", path = "./"}, { access = "read", path = "/tmp"}]
|
||||
gas_limit = 1_000_000_000
|
||||
gas_price = 0
|
||||
optimizer = true
|
||||
|
|
|
|||
|
|
@ -221,24 +221,26 @@ contract FitnessEvaluator is Test {
|
|||
vm.revertTo(baseSnap);
|
||||
baseSnap = vm.snapshot();
|
||||
|
||||
// Etch candidate optimizer bytecode and upgrade proxy.
|
||||
// Wrapped in try/catch: a malformed candidate (compiler bug, bad transpiler output)
|
||||
// would otherwise abort the entire batch. On failure, emit fitness=0 and continue;
|
||||
// vm.revertTo(baseSnap) at the top of the next iteration cleans up state.
|
||||
// Etch candidate optimizer bytecode onto the implementation address
|
||||
// and update the ERC1967 implementation slot directly.
|
||||
// Skips UUPS upgradeTo() check — candidates are standalone contracts
|
||||
// (OptimizerV3Push3) without UUPSUpgradeable inheritance.
|
||||
// This is safe because we only care about calculateParams() output.
|
||||
bytes memory candidateBytecode = vm.parseBytes(bytecodeHex);
|
||||
vm.etch(IMPL_SLOT, candidateBytecode);
|
||||
bool upgradeOk = true;
|
||||
try UUPSUpgradeable(optProxy).upgradeTo(IMPL_SLOT) { }
|
||||
catch {
|
||||
upgradeOk = false;
|
||||
}
|
||||
if (!upgradeOk) {
|
||||
console.log(string.concat('{"candidate_id":"', candidateId, '","fitness":0,"error":"upgrade_failed"}'));
|
||||
if (candidateBytecode.length == 0) {
|
||||
console.log(string.concat('{"candidate_id":"', candidateId, '","fitness":0,"error":"empty_bytecode"}'));
|
||||
continue;
|
||||
}
|
||||
vm.etch(IMPL_SLOT, candidateBytecode);
|
||||
// ERC1967 implementation slot = keccak256("eip1967.proxy.implementation") - 1
|
||||
bytes32 ERC1967_IMPL = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
|
||||
vm.store(optProxy, ERC1967_IMPL, bytes32(uint256(uint160(IMPL_SLOT))));
|
||||
|
||||
// Bootstrap: fund LM, set recenterAccess, initial recenter.
|
||||
_bootstrap();
|
||||
if (!_bootstrap()) {
|
||||
console.log(string.concat('{"candidate_id":"', candidateId, '","fitness":0,"error":"bootstrap_failed"}'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Score: sum lm_eth_total across all attack sequences.
|
||||
uint256 totalFitness = 0;
|
||||
|
|
@ -321,7 +323,7 @@ contract FitnessEvaluator is Test {
|
|||
* d. Wrap 9000 WETH for adversary trades + set approvals.
|
||||
* e. Initial recenter (succeeds immediately: recenterAccess set, no ANCHOR liquidity yet).
|
||||
*/
|
||||
function _bootstrap() internal {
|
||||
function _bootstrap() internal returns (bool) {
|
||||
// a. Grant recenterAccess (feeDestination call, no ETH needed with gas_price=0).
|
||||
vm.prank(FEE_DEST);
|
||||
LiquidityManager(payable(lmAddr)).setRecenterAccess(recenterAddr);
|
||||
|
|
@ -356,9 +358,15 @@ contract FitnessEvaluator is Test {
|
|||
try ILM(lmAddr).recenter() returns (bool) {
|
||||
recentered = true;
|
||||
break;
|
||||
} catch { }
|
||||
} catch (bytes memory reason) {
|
||||
console.log(string.concat("recenter attempt ", vm.toString(_attempt), " failed"));
|
||||
console.logBytes(reason);
|
||||
}
|
||||
require(recentered, "FitnessEvaluator: bootstrap recenter failed after 5 attempts");
|
||||
}
|
||||
if (!recentered) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Attack execution ─────────────────────────────────────────────────────
|
||||
|
|
@ -394,7 +402,7 @@ contract FitnessEvaluator is Test {
|
|||
if (_eq(op, "buy")) {
|
||||
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
||||
vm.prank(advAddr);
|
||||
ISwapRouter02(SWAP_ROUTER).exactInputSingle(
|
||||
try ISwapRouter02(SWAP_ROUTER).exactInputSingle(
|
||||
ISwapRouter02.ExactInputSingleParams({
|
||||
tokenIn: WETH_ADDR,
|
||||
tokenOut: krkAddr,
|
||||
|
|
@ -404,13 +412,13 @@ contract FitnessEvaluator is Test {
|
|||
amountOutMinimum: 0,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
) { } catch { }
|
||||
} else if (_eq(op, "sell")) {
|
||||
string memory amtStr = vm.parseJsonString(line, ".amount");
|
||||
uint256 amount = _eq(amtStr, "all") ? IERC20(krkAddr).balanceOf(advAddr) : vm.parseUint(amtStr);
|
||||
if (amount == 0) return;
|
||||
vm.prank(advAddr);
|
||||
ISwapRouter02(SWAP_ROUTER).exactInputSingle(
|
||||
try ISwapRouter02(SWAP_ROUTER).exactInputSingle(
|
||||
ISwapRouter02.ExactInputSingleParams({
|
||||
tokenIn: krkAddr,
|
||||
tokenOut: WETH_ADDR,
|
||||
|
|
@ -420,7 +428,7 @@ contract FitnessEvaluator is Test {
|
|||
amountOutMinimum: 0,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
) { } catch { }
|
||||
} else if (_eq(op, "recenter")) {
|
||||
vm.prank(recenterAddr);
|
||||
try ILM(lmAddr).recenter() { } catch { }
|
||||
|
|
@ -428,16 +436,14 @@ contract FitnessEvaluator is Test {
|
|||
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
||||
uint32 taxRate = uint32(vm.parseJsonUint(line, ".taxRateIndex"));
|
||||
vm.prank(advAddr);
|
||||
uint256 posId = IStake(stakeAddr).snatch(amount, advAddr, taxRate, new uint256[](0));
|
||||
try IStake(stakeAddr).snatch(amount, advAddr, taxRate, new uint256[](0)) returns (uint256 posId) {
|
||||
_stakedPositionIds.push(posId);
|
||||
} catch { }
|
||||
} else if (_eq(op, "unstake")) {
|
||||
uint256 posIndex = vm.parseJsonUint(line, ".positionId");
|
||||
require(
|
||||
posIndex >= 1 && posIndex <= _stakedPositionIds.length,
|
||||
"FitnessEvaluator: unstake positionId out of range"
|
||||
);
|
||||
if (posIndex < 1 || posIndex > _stakedPositionIds.length) return;
|
||||
vm.prank(advAddr);
|
||||
IStake(stakeAddr).exitPosition(_stakedPositionIds[posIndex - 1]);
|
||||
try IStake(stakeAddr).exitPosition(_stakedPositionIds[posIndex - 1]) { } catch { }
|
||||
} else if (_eq(op, "mine")) {
|
||||
uint256 blocks = vm.parseJsonUint(line, ".blocks");
|
||||
vm.roll(block.number + blocks);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|||
ONCHAIN_DIR="$REPO_ROOT/onchain"
|
||||
TRANSPILER_DIR="$REPO_ROOT/tools/push3-transpiler"
|
||||
TRANSPILER_OUT="$ONCHAIN_DIR/src/OptimizerV3Push3.sol"
|
||||
ARTIFACT_PATH="$ONCHAIN_DIR/out/OptimizerV3Push3.sol/OptimizerV3Push3.json"
|
||||
# Use OptimizerV3 (inherits Optimizer → UUPS compatible, has getLiquidityParams)
|
||||
# instead of standalone OptimizerV3Push3 which lacks UUPS hooks.
|
||||
OPTIMIZERV3_SOL="$ONCHAIN_DIR/src/OptimizerV3.sol"
|
||||
ARTIFACT_PATH="$ONCHAIN_DIR/out/OptimizerV3.sol/OptimizerV3.json"
|
||||
DEFAULT_ATTACKS_DIR="$ONCHAIN_DIR/script/backtesting/attacks"
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -124,6 +127,64 @@ for PUSH3_FILE in "${PUSH3_FILES[@]}"; do
|
|||
continue
|
||||
fi
|
||||
|
||||
# Inject transpiled calculateParams body into OptimizerV3.sol (UUPS-compatible).
|
||||
# Extract function body from OptimizerV3Push3.sol and replace content between
|
||||
# BEGIN/END markers in OptimizerV3.sol.
|
||||
python3 - "$TRANSPILER_OUT" "$OPTIMIZERV3_SOL" <<'PYEOF' || {
|
||||
import sys
|
||||
|
||||
push3_path = sys.argv[1]
|
||||
v3_path = sys.argv[2]
|
||||
|
||||
# Extract function body from OptimizerV3Push3.sol
|
||||
# Find "function calculateParams" then extract everything between the opening { and
|
||||
# the matching closing } at the same indent level (4 spaces / function level)
|
||||
with open(push3_path) as f:
|
||||
push3 = f.read()
|
||||
|
||||
# Find body start: line after "function calculateParams...{"
|
||||
fn_start = push3.find("function calculateParams")
|
||||
if fn_start == -1:
|
||||
sys.exit("calculateParams not found in OptimizerV3Push3")
|
||||
brace_start = push3.find("{", fn_start)
|
||||
body_start = push3.index("\n", brace_start) + 1
|
||||
|
||||
# Find body end: the closing " }" of the function (4-space indent, before contract close)
|
||||
# Walk backwards from end to find the function-level closing brace
|
||||
lines = push3[body_start:].split("\n")
|
||||
body_lines = []
|
||||
for line in lines:
|
||||
if line.strip() == "}" and (line.startswith(" }") or line == "}"):
|
||||
# This is the function-closing brace
|
||||
break
|
||||
body_lines.append(line)
|
||||
|
||||
body = "\n".join(body_lines)
|
||||
|
||||
# Now inject into OptimizerV3.sol between markers
|
||||
with open(v3_path) as f:
|
||||
v3 = f.read()
|
||||
|
||||
begin_marker = "// ── BEGIN TRANSPILER OUTPUT"
|
||||
end_marker = "// ── END TRANSPILER OUTPUT"
|
||||
begin_idx = v3.find(begin_marker)
|
||||
end_idx = v3.find(end_marker)
|
||||
if begin_idx == -1 or end_idx == -1:
|
||||
sys.exit("markers not found in OptimizerV3.sol")
|
||||
|
||||
begin_line_end = v3.index("\n", begin_idx) + 1
|
||||
# Keep the end marker line intact
|
||||
with open(v3_path, "w") as f:
|
||||
f.write(v3[:begin_line_end])
|
||||
f.write(body + "\n")
|
||||
f.write(v3[end_idx:])
|
||||
|
||||
PYEOF
|
||||
log "WARNING: failed to inject calculateParams into OptimizerV3.sol for $CANDIDATE_ID — skipping"
|
||||
FAILED_IDS="$FAILED_IDS $CANDIDATE_ID"
|
||||
continue
|
||||
}
|
||||
|
||||
# Compile (forge's incremental build skips unchanged files quickly)
|
||||
FORGE_EC=0
|
||||
(cd "$ONCHAIN_DIR" && forge build --silent) >/dev/null 2>&1 || FORGE_EC=$?
|
||||
|
|
@ -139,7 +200,7 @@ for PUSH3_FILE in "${PUSH3_FILES[@]}"; do
|
|||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
d = json.load(f)
|
||||
bytecode = d["bytecode"]["object"]
|
||||
bytecode = d["deployedBytecode"]["object"]
|
||||
# Ensure 0x prefix
|
||||
if not bytecode.startswith("0x"):
|
||||
bytecode = "0x" + bytecode
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue