| name | assembly-patterns |
| description | Yul/inline assembly patterns for advanced Solidity optimization. Use only when gas savings are significant and measured. Covers memory management, efficient hashing, bitwise operations, custom errors, low-level calls, and returndata handling. |
Assembly Patterns
When to Use Assembly
Only use assembly when:
- Gas savings are measured (not assumed) via
forge test --gas-report
- The optimization is in a hot path (called frequently)
- Solidity cannot express the operation (e.g., specific memory layout)
- Savings exceed 500 gas per call for the added complexity
Always add @dev NatSpec explaining why assembly is used and what it does.
Memory Management
EVM memory layout:
0x00-0x3f (64 bytes): Scratch space for hashing
0x40-0x5f (32 bytes): Free memory pointer
0x60-0x7f (32 bytes): Zero slot
/// @dev Efficiently computes keccak256(abi.encodePacked(a, b)) using scratch space.
function efficientHash(bytes32 a, bytes32 b) internal pure returns (bytes32 result) {
assembly {
mstore(0x00, a)
mstore(0x20, b)
result := keccak256(0x00, 0x40)
}
}
Efficient Hashing
Hashing pairs for Merkle trees — avoids memory allocation overhead.
/// @dev Hashes a leaf pair for Merkle tree construction. Sorts to ensure
/// consistent ordering regardless of input order.
function hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32 result) {
assembly {
// Sort to produce canonical ordering
switch lt(a, b)
case 1 {
mstore(0x00, a)
mstore(0x20, b)
}
default {
mstore(0x00, b)
mstore(0x20, a)
}
result := keccak256(0x00, 0x40)
}
}
Bitwise Packing and Unpacking
Pack multiple values into a single uint256 for storage efficiency.
/// @dev Packs owner (160 bits), amount (80 bits), timestamp (16 bits) into uint256.
function pack(address owner, uint80 amount, uint16 ts) internal pure returns (uint256 packed) {
assembly {
packed := or(or(shl(96, owner), shl(16, amount)), ts)
}
}
function unpackOwner(uint256 packed) internal pure returns (address owner) {
assembly {
owner := shr(96, packed)
}
}
function unpackAmount(uint256 packed) internal pure returns (uint80 amount) {
assembly {
amount := and(shr(16, packed), 0xffffffffffffffffffff) // 80-bit mask
}
}
function unpackTimestamp(uint256 packed) internal pure returns (uint16 ts) {
assembly {
ts := and(packed, 0xffff)
}
}
Custom Errors in Assembly
Revert with custom error selectors without Solidity overhead.
error Unauthorized(); // selector: 0x82b42900
error InsufficientBalance(uint256 available, uint256 required);
/// @dev Reverts with Unauthorized() using minimal gas.
function _revertUnauthorized() internal pure {
assembly {
mstore(0x00, 0x82b42900) // Unauthorized() selector
revert(0x1c, 0x04) // offset to align 4-byte selector
}
}
/// @dev Reverts with InsufficientBalance(available, required).
function _revertInsufficientBalance(uint256 available, uint256 required) internal pure {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0xf4d678b8) // InsufficientBalance selector
mstore(add(ptr, 0x04), available)
mstore(add(ptr, 0x24), required)
revert(add(ptr, 0x1c), 0x44) // 4 + 32 + 32 = 68 bytes
}
}
Low-Level Calls
/// @dev Performs a low-level call and bubbles up revert data on failure.
function _call(address target, bytes memory data) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.call(data);
if (!success) {
assembly {
// Bubble up the revert reason
revert(add(returndata, 0x20), mload(returndata))
}
}
return returndata;
}
Return Data Handling
/// @dev Checks if a call returned true (for ERC-20 compatibility).
/// Handles tokens that return nothing (USDT), false, or true.
function _callOptionalReturn(address token, bytes memory data) internal {
(bool success, bytes memory returndata) = token.call(data);
if (!success) {
assembly {
revert(add(returndata, 0x20), mload(returndata))
}
}
// If returndata is present, it must decode to true
if (returndata.length > 0) {
if (abi.decode(returndata, (bool)) == false) {
revert TokenCallFailed();
}
}
}
Efficient Address Checks
/// @dev Checks if an address is a contract (has code). Uses extcodesize.
function isContract(address account) internal view returns (bool result) {
assembly {
result := gt(extcodesize(account), 0)
}
}
/// @dev Reverts if addr is the zero address.
function _requireNonZero(address addr) internal pure {
assembly {
if iszero(addr) {
mstore(0x00, 0xd92e233d) // ZeroAddress() selector
revert(0x1c, 0x04)
}
}
}
Efficient Event Emission
/// @dev Emits Transfer(from, to, amount) without Solidity ABI encoding overhead.
function _emitTransfer(address from, address to, uint256 amount) internal {
assembly {
// Transfer(address,address,uint256) topic
let sig := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
mstore(0x00, amount)
log3(0x00, 0x20, sig, from, to)
}
}
Safety Rules
- Never use assembly for simple operations that Solidity handles well
- Always validate inputs before assembly blocks — assembly skips Solidity's type safety
- Document every assembly block with NatSpec explaining the operation
- Test extensively — assembly bugs are silent and devastating
- Use
returndatasize() instead of hardcoded sizes when reading return data
- Avoid
mstore past the free memory pointer without updating it
- Never use
selfdestruct in assembly (deprecated, behavior changes post-Dencun)
Assembly Checklist