From 631c7abe04383358633f772dd022f487550ac466 Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:57:43 +0000 Subject: [PATCH 1/4] feat(tokens): add zero-allocation Yul ERC20 permit handler --- contracts/token/YulPermitHandler.sol | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 contracts/token/YulPermitHandler.sol diff --git a/contracts/token/YulPermitHandler.sol b/contracts/token/YulPermitHandler.sol new file mode 100644 index 0000000..25e6d49 --- /dev/null +++ b/contracts/token/YulPermitHandler.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title YulPermitHandler +/// @notice Executes ERC-20 EIP-2612 permit calls using zero-allocation Yul assembly. +/// @dev Avoids abi.encodeWithSelector and dynamic memory allocation for improved gas efficiency. + +error PermitCallFailed(); + +interface IERC20Permit { + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} + +contract YulPermitHandler { + /// @dev permit(address,address,uint256,uint256,uint8,bytes32,bytes32) + bytes4 private constant PERMIT_SELECTOR = + bytes4( + keccak256( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)" + ) + ); + + /// @notice Executes an ERC20 permit using inline Yul. + /// @param token Address of the ERC20 token supporting EIP-2612. + /// @param owner Token owner. + /// @param spender Approved spender. + /// @param value Allowance amount. + /// @param deadline Permit deadline. + /// @param v Signature recovery id. + /// @param r Signature parameter. + /// @param s Signature parameter. + function executePermit( + address token, + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bool success; + + assembly { + // ----------------------------------------------------------------- + // Scratch Memory Layout (0x00 - 0xE3) + // + // 0x00 : selector (4 bytes) + // 0x04 : owner + // 0x24 : spender + // 0x44 : value + // 0x64 : deadline + // 0x84 : v + // 0xA4 : r + // 0xC4 : s + // + // Total calldata size = 4 + (7 * 32) = 228 bytes (0xE4) + // ----------------------------------------------------------------- + + let ptr := 0x00 + + // Function selector (left-shifted into the first 4 bytes) + mstore(ptr, shl(224, PERMIT_SELECTOR)) + + // Arguments + mstore(add(ptr, 0x04), owner) + mstore(add(ptr, 0x24), spender) + mstore(add(ptr, 0x44), value) + mstore(add(ptr, 0x64), deadline) + mstore(add(ptr, 0x84), v) + mstore(add(ptr, 0xA4), r) + mstore(add(ptr, 0xC4), s) + + // Execute permit() + success := call( + gas(), + token, + 0, + ptr, + 0xE4, + 0, + 0 + ) + + // Bubble up any revert reason from the token contract + if iszero(success) { + let size := returndatasize() + + if gt(size, 0) { + returndatacopy(0x00, 0x00, size) + revert(0x00, size) + } + } + } + + // Fallback custom error if no revert data exists + if (!success) { + revert PermitCallFailed(); + } + } +} \ No newline at end of file From eade849b5775ae7e6c990d291b219fd253c34377 Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:59:47 +0000 Subject: [PATCH 2/4] feat(data): implement compact packed byte-array storage map --- contracts/data/DenseMap.sol | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 contracts/data/DenseMap.sol diff --git a/contracts/data/DenseMap.sol b/contracts/data/DenseMap.sol new file mode 100644 index 0000000..e42a941 --- /dev/null +++ b/contracts/data/DenseMap.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title DenseMap +/// @notice Stores up to 32 uint8 values inside a single storage slot. +/// @dev Replaces mapping(uint8 => uint8) with bit-packed storage. +contract DenseMap { + /// @dev One storage slot holding 32 packed uint8 values. + bytes32 private packedData; + + error IndexOutOfBounds(); + + /// @notice Stores a uint8 value at the specified index. + /// @param index Position (0-31). + /// @param value Value to store. + function set(uint8 index, uint8 value) external { + if (index >= 32) revert IndexOutOfBounds(); + + uint256 word = uint256(packedData); + + unchecked { + uint256 shift = uint256(index) * 8; + + // Clear existing byte + word &= ~(uint256(0xff) << shift); + + // Insert new byte + word |= uint256(value) << shift; + } + + packedData = bytes32(word); + } + + /// @notice Reads the value stored at an index. + /// @param index Position (0-31). + function get(uint8 index) external view returns (uint8 value) { + if (index >= 32) revert IndexOutOfBounds(); + + unchecked { + uint256 shift = uint256(index) * 8; + + value = uint8( + (uint256(packedData) >> shift) & + 0xff + ); + } + } + + /// @notice Reads the entire packed storage word. + function packedWord() external view returns (bytes32) { + return packedData; + } + + /// @notice Clears all stored values. + function clear() external { + packedData = bytes32(0); + } + + /// @notice Batch writes multiple values. + function setMany( + uint8[] calldata indexes, + uint8[] calldata values + ) external { + require(indexes.length == values.length, "Length mismatch"); + + uint256 word = uint256(packedData); + + for (uint256 i; i < indexes.length; ) { + uint8 index = indexes[i]; + + if (index >= 32) revert IndexOutOfBounds(); + + uint256 shift = uint256(index) * 8; + + word &= ~(uint256(0xff) << shift); + word |= uint256(values[i]) << shift; + + unchecked { + ++i; + } + } + + packedData = bytes32(word); + } + + /// @notice Batch reads multiple values. + function getMany( + uint8[] calldata indexes + ) external view returns (uint8[] memory result) { + result = new uint8[](indexes.length); + + uint256 word = uint256(packedData); + + for (uint256 i; i < indexes.length; ) { + uint8 index = indexes[i]; + + if (index >= 32) revert IndexOutOfBounds(); + + uint256 shift = uint256(index) * 8; + + result[i] = uint8( + (word >> shift) & + 0xff + ); + + unchecked { + ++i; + } + } + } +} \ No newline at end of file From 35c5a53d3e8805c137dd698b9040ba11c81cff5d Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:02:13 +0000 Subject: [PATCH 3/4] feat(crypto): implement scratch-space keccak hasher using Yul --- contracts/crypto/ScratchHasher.sol | 57 ++++++++++++++++++++++++++++++ test/crypto/ScratchHasher.test.ts | 21 +++++++++++ 2 files changed, 78 insertions(+) create mode 100644 contracts/crypto/ScratchHasher.sol create mode 100644 test/crypto/ScratchHasher.test.ts diff --git a/contracts/crypto/ScratchHasher.sol b/contracts/crypto/ScratchHasher.sol new file mode 100644 index 0000000..9542f3d --- /dev/null +++ b/contracts/crypto/ScratchHasher.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title ScratchHasher +/// @notice Computes keccak256 hashes for two 32-byte words using EVM scratch space. +/// @dev Avoids abi.encodePacked() and free memory pointer expansion. +contract ScratchHasher { + /// @notice Hash two 32-byte values. + /// @param a First 32-byte word. + /// @param b Second 32-byte word. + /// @return result keccak256(abi.encodePacked(a, b)) + function hash( + bytes32 a, + bytes32 b + ) external pure returns (bytes32 result) { + assembly { + // -------------------------------------------------------- + // Scratch Space Layout + // + // 0x00 - 0x1F : a + // 0x20 - 0x3F : b + // + // Hash exactly 64 bytes. + // -------------------------------------------------------- + + mstore(0x00, a) + mstore(0x20, b) + + result := keccak256(0x00, 0x40) + } + } + + /// @notice Compare the optimized hash with Solidity's implementation. + function hashSolidity( + bytes32 a, + bytes32 b + ) external pure returns (bytes32) { + return keccak256(abi.encodePacked(a, b)); + } + + /// @notice Returns true if both implementations produce identical hashes. + function verify( + bytes32 a, + bytes32 b + ) external pure returns (bool) { + bytes32 yulHash; + bytes32 solidityHash = keccak256(abi.encodePacked(a, b)); + + assembly { + mstore(0x00, a) + mstore(0x20, b) + yulHash := keccak256(0x00, 0x40) + } + + return yulHash == solidityHash; + } +} \ No newline at end of file diff --git a/test/crypto/ScratchHasher.test.ts b/test/crypto/ScratchHasher.test.ts new file mode 100644 index 0000000..481899a --- /dev/null +++ b/test/crypto/ScratchHasher.test.ts @@ -0,0 +1,21 @@ +describe("ScratchHasher", () => { + it("matches abi.encodePacked hash", async () => { + // Compare hash() vs hashSolidity() + }); + + it("returns identical hashes for random inputs", async () => { + // Multiple random bytes32 pairs + }); + + it("handles zero values", async () => { + // bytes32(0), bytes32(0) + }); + + it("handles max values", async () => { + // 0xffff...ffff + }); + + it("benchmarks gas usage", async () => { + // Compare hash() against hashSolidity() + }); +}); \ No newline at end of file From 872620ed780f595a5c2919520aa9c416e0498487 Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:04:52 +0000 Subject: [PATCH 4/4] feat(gasguard): add G014 rule for uncached storage array length access --- src/rules/g014_storage_array_length.rs | 63 ++++++++++++++++++++++++++ test/fixtures/g014_samples.sol | 52 +++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/rules/g014_storage_array_length.rs create mode 100644 test/fixtures/g014_samples.sol diff --git a/src/rules/g014_storage_array_length.rs b/src/rules/g014_storage_array_length.rs new file mode 100644 index 0000000..86166c2 --- /dev/null +++ b/src/rules/g014_storage_array_length.rs @@ -0,0 +1,63 @@ +use crate::{ + context::RuleContext, + diagnostic::{Diagnostic, Severity}, + rule::{Rule, RuleCategory}, +}; + +pub struct G014StorageArrayLength; + +impl Rule for G014StorageArrayLength { + fn id(&self) -> &'static str { + "G014" + } + + fn name(&self) -> &'static str { + "Storage array length access inside loops" + } + + fn category(&self) -> RuleCategory { + RuleCategory::GasOptimization + } + + fn description(&self) -> &'static str { + "Detect repeated storage array length reads that should be cached." + } + + fn check(&self, ctx: &mut RuleContext) { + for node in ctx.nodes() { + // Find `.length` + if let Some(member) = node.as_member_access() { + if member.member_name() != "length" { + continue; + } + + // Ignore memory/calldata arrays + if !member.base().is_storage_array() { + continue; + } + + // Only warn if inside loop or conditional + if !(ctx.is_inside_loop(node) || ctx.is_inside_conditional(node)) { + continue; + } + + // Skip if already cached + if ctx.is_cached_to_local(member.base()) { + continue; + } + + ctx.report( + Diagnostic::new( + Severity::Warning, + "Storage array length is repeatedly loaded from storage." + ) + .with_rule(self.id()) + .with_location(node.location()) + .with_help( + "Cache storageArray.length in a local variable before the loop." + ), + ); + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/g014_samples.sol b/test/fixtures/g014_samples.sol new file mode 100644 index 0000000..dc65622 --- /dev/null +++ b/test/fixtures/g014_samples.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract G014Samples { + uint256[] public storageArray; + + function badLoop() external { + for (uint256 i; i < storageArray.length; i++) { + storageArray[i]; + } + } + + function badWhile() external { + uint256 i; + + while (i < storageArray.length) { + i++; + } + } + + function badIf() external view returns (bool) { + if (storageArray.length > 0) { + return true; + } + + return false; + } + + function goodLoop() external { + uint256 length = storageArray.length; + + for (uint256 i; i < length; i++) { + storageArray[i]; + } + } + + function memoryArray(uint256[] memory arr) external pure returns (uint256) { + for (uint256 i; i < arr.length; i++) { + return arr[i]; + } + + return 0; + } + + function calldataArray(uint256[] calldata arr) external pure returns (uint256) { + for (uint256 i; i < arr.length; i++) { + return arr[i]; + } + + return 0; + } +} \ No newline at end of file