Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions contracts/crypto/ScratchHasher.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
111 changes: 111 additions & 0 deletions contracts/data/DenseMap.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
109 changes: 109 additions & 0 deletions contracts/token/YulPermitHandler.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
63 changes: 63 additions & 0 deletions src/rules/g014_storage_array_length.rs
Original file line number Diff line number Diff line change
@@ -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."
),
);
}
}
}
}
21 changes: 21 additions & 0 deletions test/crypto/ScratchHasher.test.ts
Original file line number Diff line number Diff line change
@@ -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()
});
});
Loading
Loading