From 0c7b67b47513cd480499dbabb2a5b18c750877f7 Mon Sep 17 00:00:00 2001 From: michaelsimeon001 Date: Tue, 28 Jul 2026 14:25:21 +0100 Subject: [PATCH] Add precompile forwarder, ECDSA recover lib, bytecode mutator starter Closes #667, Closes #661, Closes #668 --- contracts/crypto/YulECDSARecover.sol | 39 +++++++++++++++ contracts/precompiles/PrecompileForwarder.sol | 35 +++++++++++++ gasguard-cli/src/fuzzer/bytecode_mutator.rs | 49 +++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 contracts/crypto/YulECDSARecover.sol create mode 100644 contracts/precompiles/PrecompileForwarder.sol create mode 100644 gasguard-cli/src/fuzzer/bytecode_mutator.rs diff --git a/contracts/crypto/YulECDSARecover.sol b/contracts/crypto/YulECDSARecover.sol new file mode 100644 index 0000000..109643c --- /dev/null +++ b/contracts/crypto/YulECDSARecover.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title YulECDSARecover +/// @notice Gas-efficient ECDSA recovery with malleability protection, no intermediate memory copies. +library YulECDSARecover { + // secp256k1n / 2 + uint256 internal constant MALLEABILITY_THRESHOLD = + 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; + + error InvalidSignatureS(); + error RecoverFailed(); + + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal view returns (address signer) { + if (uint256(s) > MALLEABILITY_THRESHOLD) revert InvalidSignatureS(); + + assembly { + let ptr := mload(0x40) + mstore(ptr, hash) + mstore(add(ptr, 0x20), v) + mstore(add(ptr, 0x40), r) + mstore(add(ptr, 0x60), s) + let ok := staticcall(gas(), 0x01, ptr, 0x80, ptr, 0x20) + if iszero(ok) { + revert(0, 0) + } + signer := mload(ptr) + } + if (signer == address(0)) revert RecoverFailed(); + } + + /// @dev Unpacks r, s, v directly from a 65-byte calldata signature slice. + function recoverFromCalldata(bytes32 hash, bytes calldata signature) internal view returns (address) { + bytes32 r = bytes32(signature[0:32]); + bytes32 s = bytes32(signature[32:64]); + uint8 v = uint8(signature[64]); + return recover(hash, v, r, s); + } +} diff --git a/contracts/precompiles/PrecompileForwarder.sol b/contracts/precompiles/PrecompileForwarder.sol new file mode 100644 index 0000000..99d1a94 --- /dev/null +++ b/contracts/precompiles/PrecompileForwarder.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title PrecompileForwarder +/// @notice Minimal Yul proxy for the ecrecover (0x01) and sha256 (0x02) precompiles. +/// @dev Starter implementation; modexp (0x05) and bn256Pairing (0x08) forwarding follow in a later PR. +library PrecompileForwarder { + error PrecompileCallFailed(address precompile); + + function ecrecoverRaw(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal view returns (address signer) { + assembly { + let ptr := mload(0x40) + mstore(ptr, hash) + mstore(add(ptr, 0x20), v) + mstore(add(ptr, 0x40), r) + mstore(add(ptr, 0x60), s) + let ok := staticcall(gas(), 0x01, ptr, 0x80, ptr, 0x20) + if iszero(ok) { + revert(0, 0) + } + signer := mload(ptr) + } + } + + function sha256Raw(bytes memory data) internal view returns (bytes32 digest) { + assembly { + let len := mload(data) + let ok := staticcall(gas(), 0x02, add(data, 0x20), len, 0x00, 0x20) + if iszero(ok) { + revert(0, 0) + } + digest := mload(0x00) + } + } +} diff --git a/gasguard-cli/src/fuzzer/bytecode_mutator.rs b/gasguard-cli/src/fuzzer/bytecode_mutator.rs new file mode 100644 index 0000000..00fc6b6 --- /dev/null +++ b/gasguard-cli/src/fuzzer/bytecode_mutator.rs @@ -0,0 +1,49 @@ +//! Closes #668: bytecode mutation engine (starter) for gas-spike fuzzing. +//! Flips bytes in compiled runtime opcodes to generate mutated variants for +//! gas-runner execution against warm/cold storage state. No external deps. + +/// Deterministic xorshift-style generator, avoids pulling in the `rand` crate +/// for this starter implementation. +struct SimpleRng(u64); + +impl SimpleRng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } +} + +/// Produces `count` mutated copies of `bytecode`, each with a single byte +/// flipped at a pseudo-random offset. +pub fn mutate_bytecode(bytecode: &[u8], count: usize, seed: u64) -> Vec> { + if bytecode.is_empty() { + return Vec::new(); + } + let mut rng = SimpleRng(seed | 1); + let mut variants = Vec::with_capacity(count); + for _ in 0..count { + let mut mutated = bytecode.to_vec(); + let idx = (rng.next() as usize) % mutated.len(); + mutated[idx] ^= 0xFF; + variants.push(mutated); + } + variants +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn produces_requested_variant_count() { + let variants = mutate_bytecode(&[0x60, 0x01, 0x60, 0x02], 5, 42); + assert_eq!(variants.len(), 5); + } + + #[test] + fn empty_input_yields_no_variants() { + assert!(mutate_bytecode(&[], 3, 1).is_empty()); + } +}