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
116 changes: 116 additions & 0 deletions contracts/crypto/YulMerkleVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title YulMerkleVerifier
/// @notice Zero-allocation Merkle proof verification written entirely in Yul assembly.
/// @dev Processes proof path validation without any dynamic memory allocation.
/// All intermediate leaf hashes are computed strictly in scratch memory (0x00–0x40).
/// Sibling nodes are ordered using low-level bitwise comparison and hashed via
/// keccak256(0x00, 0x40). This avoids the repeated mstore/memory-pointer updates
/// that OpenZeppelin's MerkleProof library performs for every leaf hashing step.
library YulMerkleVerifier {
error InvalidProof();
error InvalidLeaf();
error InvalidProofLength();

/// @notice Verifies a Merkle proof against a root, leaf, and proof elements.
/// @dev Uses only scratch memory (0x00–0x40) for intermediate hashes.
/// No dynamic memory allocation occurs during proof processing.
/// Sibling nodes are ordered using low-level bitwise comparison
/// so the smaller hash is always on the left (canonical ordering).
/// @param root The expected Merkle root.
/// @param leaf The leaf value to verify.
/// @param proof The array of sibling proof elements in order from leaf to root.
/// @return True if the proof is valid, false otherwise.
function verifyProof(
bytes32 root,
bytes32 leaf,
bytes32[] calldata proof
) external pure returns (bool) {
if (proof.length == 0) {
return leaf == root;
}

if (proof.length > 256) {
revert InvalidProofLength();
}

bytes32 computedHash = leaf;

for (uint256 i = 0; i < proof.length; i++) {
bytes32 sibling = proof[i];

assembly {
// Load current computed hash and sibling into scratch memory.
// [computedHash, sibling] at 0x00–0x40
mstore(0x00, computedHash)
mstore(0x20, sibling)

// Canonical ordering via bitwise comparison.
// gt returns 1 if computedHash > sibling (unsigned comparison).
let swap := gt(computedHash, sibling)

// Compute hash with computedHash on the left (normal order).
mstore(0x00, computedHash)
mstore(0x20, sibling)
let hashNormal := keccak256(0x00, 0x40)

// Compute hash with sibling on the left (swapped order).
mstore(0x00, sibling)
mstore(0x20, computedHash)
let hashSwapped := keccak256(0x00, 0x40)

// Select based on swap flag:
// When swap == 1, use hashSwapped (sibling on left).
// When swap == 0, use hashNormal (computedHash on left).
computedHash := add(
mul(swap, hashSwapped),
mul(sub(1, swap), hashNormal)
)

// Store result back to scratch memory for next iteration.
mstore(0x00, computedHash)
}
}

return computedHash == root;
}

/// @notice Verifies a Merkle proof with explicit left/right ordering.
/// @dev Uses only scratch memory (0x00–0x40) for intermediate hashes.
/// No dynamic memory allocation occurs. The caller must provide
/// proof elements in the correct left-to-right order.
/// @param root The expected Merkle root.
/// @param leaf The leaf value to verify.
/// @param proof The array of sibling proof elements in order from leaf to root.
/// @return True if the proof is valid, false otherwise.
function verifyProofOrdered(
bytes32 root,
bytes32 leaf,
bytes32[] calldata proof
) external pure returns (bool) {
if (proof.length == 0) {
return leaf == root;
}

if (proof.length > 256) {
revert InvalidProofLength();
}

bytes32 computedHash = leaf;

for (uint256 i = 0; i < proof.length; i++) {
bytes32 sibling = proof[i];

assembly {
// [computedHash, sibling] -> [hash]
// Always place computedHash on the left, sibling on the right.
mstore(0x00, computedHash)
mstore(0x20, sibling)
computedHash := keccak256(0x00, 0x40)
}
}

return computedHash == root;
}
}
3 changes: 3 additions & 0 deletions contracts/utils/CodeCheckLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ library CodeCheckLib {
/// @notice Returns true if `target` has deployed code.
/// @dev Skips the stack management overhead of high-level `.code.length`.
function isContract(address target) internal view returns (bool result) {
// [target] -> [result]
// Safety: reads scratch 0x00-0x40 only; reads storage (extcodesize); no external calls.
// Gas: extcodesize is a cold access (~2600 gas) if not previously accessed.
assembly {
result := gt(extcodesize(target), 0)
}
Expand Down
26 changes: 21 additions & 5 deletions contracts/utils/MappingResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ library MappingResolver {
/// @param key The mapping key.
/// @return result The computed storage slot.
function computeSlot(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) {
// [key, slot] -> [result]
// Safety: reads scratch 0x00-0x40 only; no storage reads/writes; no external calls.
// Gas: keccak256(0x00, 0x40) costs 30 gas + word count * 6 gas.
assembly {
// Write key to scratch space at 0x00
mstore(0x00, key)
// Write slot to scratch space at 0x20
mstore(0x20, slot)
// Compute keccak256 of the 64-byte range
result := keccak256(0x00, 0x40)
}
}
Expand All @@ -31,13 +31,14 @@ library MappingResolver {
bytes32 key1,
bytes32 key2
) internal pure returns (bytes32 result) {
// [key1, slot, key2, innerSlot] -> [result]
// Safety: reads scratch 0x00-0x40 only; no storage reads/writes; no external calls.
// Gas: two keccak256(0x00, 0x40) calls; each costs 30 gas + word count * 6 gas.
assembly {
// Compute inner slot: keccak256(abi.encode(key1, slot))
mstore(0x00, key1)
mstore(0x20, slot)
let innerSlot := keccak256(0x00, 0x40)

// Compute outer slot: keccak256(abi.encode(key2, innerSlot))
mstore(0x00, key2)
mstore(0x20, innerSlot)
result := keccak256(0x00, 0x40)
Expand All @@ -49,6 +50,9 @@ library MappingResolver {
/// @param key The address key.
/// @return result The computed storage slot.
function computeAddrSlot(bytes32 slot, address key) internal pure returns (bytes32 result) {
// [key, slot] -> [result]
// Safety: reads scratch 0x00-0x40 only; no storage reads/writes; no external calls.
// Gas: keccak256(0x00, 0x40) costs 30 gas + word count * 6 gas.
assembly {
mstore(0x00, key)
mstore(0x20, slot)
Expand All @@ -61,6 +65,9 @@ library MappingResolver {
/// @param key The uint256 key.
/// @return result The computed storage slot.
function computeUintSlot(bytes32 slot, uint256 key) internal pure returns (bytes32 result) {
// [key, slot] -> [result]
// Safety: reads scratch 0x00-0x40 only; no storage reads/writes; no external calls.
// Gas: keccak256(0x00, 0x40) costs 30 gas + word count * 6 gas.
assembly {
mstore(0x00, key)
mstore(0x20, slot)
Expand Down Expand Up @@ -88,6 +95,9 @@ contract MappingResolverConsumer {
/// @return value The balance value.
function readBalanceAssembly(address user) external view returns (uint256 value) {
bytes32 slot = MappingResolver.computeAddrSlot(bytes32(0), user);
// [slot] -> [value]
// Safety: reads scratch 0x00-0x40 only; reads storage (sload); no external calls.
// Gas: SLOAD (2100 warm / 100 cold).
assembly {
value := sload(slot)
}
Expand All @@ -106,6 +116,9 @@ contract MappingResolverConsumer {
bytes32(uint256(uint160(owner))),
bytes32(uint256(uint160(spender)))
);
// [slot] -> [value]
// Safety: reads scratch 0x00-0x40 only; reads storage (sload); no external calls.
// Gas: SLOAD (2100 warm / 100 cold).
assembly {
value := sload(slot)
}
Expand All @@ -121,6 +134,9 @@ contract MappingResolverConsumer {
bytes32(uint256(uint160(user))),
bytes32(key)
);
// [slot] -> [value]
// Safety: reads scratch 0x00-0x40 only; reads storage (sload); no external calls.
// Gas: SLOAD (2100 warm / 100 cold).
assembly {
value := sload(slot)
}
Expand Down
3 changes: 3 additions & 0 deletions contracts/utils/YulNativeTransfer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ library YulNativeTransfer {
/// @notice Sends `amount` wei to `recipient` using a zero-memory-footprint call.
function safeTransferETH(address recipient, uint256 amount) internal {
bool success;
// [gas, recipient, amount, 0, 0, 0, 0] -> [success]
// Safety: reads scratch 0x00-0x40 only; no storage writes; makes external call (reentrancy risk).
// Gas: call(gas(), ...) is a low-level call with gas stipend; success indicates transfer completion.
assembly {
success := call(gas(), recipient, amount, 0, 0, 0, 0)
}
Expand Down
147 changes: 147 additions & 0 deletions docs/ASSEMBLY_STYLE_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Assembly Style Guide

## Overview

This guide establishes a consistent inline documentation standard for all Yul assembly blocks across the GasGuard repository. The goal is to maintain gas efficiency while ensuring that assembly code remains readable, auditable, and maintainable.

## Stack State Annotations

Every assembly block must include a stack state annotation above it using the format:

```
// [arg1, arg2] -> [result]
```

This annotation describes the stack effect of the assembly block:
- **Left side** (`[arg1, arg2]`): The values on the stack before the block executes, listed in bottom-to-top order.
- **Right side** (`[result]`): The value(s) left on the stack after the block executes, listed in bottom-to-top order.

### Examples

```solidity
// [value] -> [hash]
assembly {
hash := keccak256(0x00, 0x20)
}
```

```solidity
// [a, b] -> [result]
assembly {
mstore(0x00, a)
mstore(0x20, b)
result := keccak256(0x00, 0x40)
}
```

```solidity
// [slot] -> [value]
assembly {
value := sload(slot)
}
```

## Memory Layout Conventions

### Scratch Memory (0x00–0x40)

Scratch memory is reserved for temporary computation within assembly blocks. It is the only memory region that may be used for intermediate hash computations without affecting the free memory pointer.

- **0x00–0x1F**: First 32-byte word (scratch slot A)
- **0x20–0x3F**: Second 32-byte word (scratch slot B)

All scratch memory usage must be explicitly commented above the assembly block.

### Free Memory Pointer (0x40)

The free memory pointer at `0x40` must never be modified inside assembly blocks unless the block's purpose is explicitly memory allocation. All assembly blocks that use scratch memory must preserve the free memory pointer.

```solidity
// Uses scratch memory 0x00-0x40 only; does not modify 0x40 (free memory pointer).
// [a, b] -> [hash]
assembly {
mstore(0x00, a)
mstore(0x20, b)
hash := keccak256(0x00, 0x40)
}
```

## Safety Invariants

Every assembly block must declare its safety invariants in a comment directly above the block. Required invariants:

1. **Memory safety**: State whether the block modifies memory beyond scratch (0x00–0x40).
2. **Storage safety**: State whether the block reads or writes storage.
3. **Reentrancy safety**: State whether the block makes external calls.
4. **Gas safety**: Note any operations with variable gas costs (e.g., `SLOAD`, `SSTORE`, `KECCAK256`).

### Example

```solidity
// Safety invariants:
// - Memory: reads scratch 0x00-0x40 only; does not write beyond scratch.
// - Storage: reads slot; does not write storage.
// - Reentrancy: no external calls.
// - Gas: SLOAD (2100 warm / 100 cold), KECCAK256 (quadratic word cost).
// [slot] -> [value]
assembly {
value := sload(slot)
}
```

## Required Comment Format

Each assembly block must have exactly two comment lines directly above it:

1. **Stack state annotation** (required): `// [args] -> [results]`
2. **Safety invariants** (required): One or more lines starting with `// Safety:`

### Correct Example

```solidity
// [key, slot] -> [result]
// Safety: reads scratch 0x00-0x40 only; no storage writes; no external calls.
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
```

### Incorrect Example

```solidity
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
```

The incorrect example is missing both the stack state annotation and the safety invariants.

## Variable Naming in Assembly

- Use `camelCase` for Yul local variables.
- Use descriptive names that reflect the value's purpose (e.g., `baseSlot`, `innerSlot`, `leafHash`).
- Avoid single-letter variable names except for loop counters (`i`, `j`).

## Loop Patterns

Loops in assembly must include an invariant comment and a termination guarantee:

```solidity
// [i, accumulator] -> [result]
// Safety: reads scratch 0x00-0x40 only; no storage writes; no external calls.
// Invariant: accumulator holds the running hash of all processed elements.
// Terminates when i == proofLength.
assembly {
for { let i := 0 } lt(i, proofLength) { i := add(i, 1) } {
// loop body
}
}
```

## Applying This Guide

All Yul assembly blocks in `contracts/utils/` and other directories must be annotated according to this standard. When adding new assembly blocks, follow the format above. When reviewing code, verify that every assembly block has both a stack state annotation and safety invariants.
6 changes: 6 additions & 0 deletions gasguard-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "gasguard-cli"
version = "0.1.0"
edition = "2021"

[dependencies]
1 change: 1 addition & 0 deletions gasguard-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod optimize_storage;
Loading
Loading