Merge pull request 'fix: revm evaluator — UUPS bypass, deployedBytecode, graceful attack ops' (#629) from fix/revm-evaluator into master

Reviewed-on: https://codeberg.org/johba/harb/pulls/629
Reviewed-by: review_bot <review_bot@noreply.codeberg.org>
This commit is contained in:
johba 2026-03-12 21:43:53 +01:00
commit c3c262a719
3 changed files with 97 additions and 30 deletions

View file

@ -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

View file

@ -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));
_stakedPositionIds.push(posId);
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);