igneum/tools/exec-attacks/contracts/PgasBomb.sol
igneum-labs 1037d06276 exec-attacks: execution-layer attack suite (tools + bench log)
Adversarial robustness and conformance tests of the execution layer against a
throwaway 3-node simnet on ports 27600+. Six scenarios, each a runnable command
with a design-derived pass criterion and a measured result: malformed/boundary
txs, nonce games across parallel blocks, RPC fuzz, pgas exhaustion, reorgs under
execution, and developer-registry abuse. 98 checks, 0 failures, 0 node panics.

Two findings filed in the bench-log entry: the mempool admits txs with gas_limit
above B_e (low), and an over-pgas-budget tx is executed natively in full before
being skipped for no fee (medium, griefing).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 22:57:19 +00:00

46 lines
1.8 KiB
Solidity

// SPDX-License-Identifier: ISC
pragma solidity ^0.8.20;
// A contract that is cheap in execution gas but heavy in proving gas (pgas): it hammers the modexp precompile
// (0x05) and the KECCAK256 opcode in a loop. Under Igneum's pgas table (igneum/exec/src/pgas.rs) modexp is
// ~1000 + 10*input_len pgas for ~200 execution gas, so each call carries a large pgas/gas ratio and a loop can
// drive a transaction's pgas toward the per-block proving budget B_p with only a few million execution gas.
contract PgasBomb {
event Done(uint256 iterations, uint256 acc);
// Call the 1024-bit modexp precompile `n` times. Each call: baseLen=1, expLen=1, modLen=128 (input_len 226).
function modexpLoop(uint256 n) external returns (uint256 acc) {
bytes memory input = new bytes(96 + 1 + 1 + 128);
// baseLen = 1
input[31] = 0x01;
// expLen = 1
input[63] = 0x01;
// modLen = 128
input[95] = 0x80;
// base byte = 2
input[96] = 0x02;
// exp byte = 1
input[97] = 0x01;
// modulus: all 0xff (a large odd number)
for (uint256 i = 0; i < 128; i++) input[98 + i] = 0xff;
bytes memory out = new bytes(128);
for (uint256 i = 0; i < n; i++) {
bool ok;
assembly {
ok := staticcall(gas(), 0x05, add(input, 32), mload(input), add(out, 32), 128)
}
require(ok, "modexp failed");
acc += uint8(out[127]);
}
emit Done(n, acc);
}
// Hash a 32-byte word `n` times, chaining the result. Pure KECCAK256 opcode load.
function keccakLoop(uint256 n) external returns (bytes32 h) {
h = keccak256(abi.encodePacked(block.number));
for (uint256 i = 0; i < n; i++) {
h = keccak256(abi.encodePacked(h));
}
}
}