What inline assembly is good for

Inline assembly lets you access EVM primitives directly inside Solidity. It is most useful when you need one of the following:

  • lower gas costs in tight loops or hot paths
  • direct calldata or memory manipulation
  • custom hashing or encoding logic
  • precise control over return data, revert data, or external calls
  • access to opcodes not exposed cleanly in Solidity

In practice, inline assembly is best reserved for isolated, well-reviewed functions. If a high-level Solidity implementation is already cheap and readable, prefer that first.

Common use cases

Use caseWhy assembly helpsTypical risk
Calldata parsingAvoids ABI decoding overheadMisreading offsets or lengths
Custom hashingReduces memory allocationsIncorrect memory layout
External callsFine-grained control over call, staticcall, delegatecallSilent failure handling
Packed data accessEfficient bit and byte extractionOff-by-one and masking bugs
Return/revert dataCustom error payloads and minimal encodingMalformed returndata

Understanding the assembly block

Solidity’s inline assembly uses the Yul language syntax. Inside an assembly { ... } block, you can read and write memory, inspect calldata, and use EVM instructions such as mload, mstore, sload, sstore, call, and revert.

A simple example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract AssemblyExample {
    function add(uint256 a, uint256 b) external pure returns (uint256 result) {
        assembly {
            result := add(a, b)
        }
    }
}

This example is not faster than a + b in high-level Solidity, but it shows the basic structure: values are assigned with :=, and EVM opcodes are called by name.

Key mental model

  • Memory is temporary and byte-addressed.
  • Storage is persistent and slot-based.
  • Calldata is read-only input data.
  • Stack is where most EVM operations happen implicitly.

If you understand which location your data lives in, assembly becomes much easier to reason about.


Reading calldata directly

One of the most practical uses of inline assembly is parsing calldata without full ABI decoding. This can be useful for routers, meta-transaction relays, and custom dispatchers.

Suppose you want to read the first argument of a function call manually. For external functions, calldata begins with a 4-byte selector, followed by ABI-encoded arguments.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract CalldataReader {
    function firstArg() external pure returns (uint256 value) {
        assembly {
            // calldata layout:
            // 0..3   selector
            // 4..35  first argument
            value := calldataload(4)
        }
    }
}

calldataload(4) reads 32 bytes starting at byte offset 4. For a uint256, that is exactly what you want. For smaller types, you must mask or shift the value appropriately.

Example: extracting a address

An address occupies 20 bytes but is ABI-encoded in a 32-byte word.

function senderFromCalldata() external pure returns (address addr) {
    assembly {
        addr := shr(96, calldataload(4))
    }
}

The shr(96, ...) shifts away the upper 12 bytes, leaving the lower 20 bytes.

Best practices for calldata parsing

  • Validate calldatasize() before reading offsets.
  • Be explicit about ABI layout assumptions.
  • Use helper functions for repeated decoding logic.
  • Prefer high-level decoding when the gas savings are negligible.

Working with memory safely

Memory is commonly used for temporary buffers, hashing inputs, and return values. Solidity maintains a free memory pointer at slot 0x40, which points to the next available memory location.

A common pattern is to allocate memory manually:

function buildBytes32(uint256 x) external pure returns (bytes32 out) {
    assembly {
        let ptr := mload(0x40)
        mstore(ptr, x)
        out := mload(ptr)
        mstore(0x40, add(ptr, 0x20))
    }
}

This example stores a 32-byte word in memory and updates the free memory pointer afterward.

Important memory rules

  • Do not overwrite Solidity’s scratch space unless you know the implications.
  • Always advance the free memory pointer after allocating.
  • Remember that dynamic arrays in memory have a length word at the start.
  • Avoid assuming memory is zeroed unless you explicitly initialize it.

Hashing with a custom memory layout

A frequent assembly optimization is hashing data without creating intermediate Solidity objects.

function hashPair(bytes32 a, bytes32 b) external pure returns (bytes32 h) {
    assembly {
        let ptr := mload(0x40)
        mstore(ptr, a)
        mstore(add(ptr, 0x20), b)
        h := keccak256(ptr, 0x40)
    }
}

This is efficient because it writes exactly 64 bytes and hashes them directly.


Performing low-level calls

Inline assembly gives you full control over external calls. This is useful when you need to forward arbitrary calldata, inspect return data, or implement custom error bubbling.

Example: forwarding a call and bubbling revert data

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract CallForwarder {
    function forward(address target, bytes calldata data) external returns (bytes memory result) {
        assembly {
            let dataOffset := data.offset
            let dataLength := data.length

            let success := call(
                gas(),
                target,
                0,
                dataOffset,
                dataLength,
                0,
                0
            )

            let size := returndatasize()
            let ptr := mload(0x40)
            mstore(0x40, add(ptr, add(size, 0x20)))
            mstore(ptr, size)
            returndatacopy(add(ptr, 0x20), 0, size)

            if iszero(success) {
                revert(add(ptr, 0x20), size)
            }

            result := ptr
        }
    }
}

This pattern copies returndata into memory and reverts with the exact returned payload if the call fails.

Why this matters

High-level Solidity often wraps external calls in a way that hides revert details or adds overhead. Assembly lets you preserve the original revert reason, which is especially valuable in proxy-like routers and integration layers.

Call types at a glance

OpcodeState accessTypical use
callCan modify stateRegular external calls
staticcallRead-onlyOracle reads, view helpers
delegatecallUses caller’s storageLibrary-style execution
callcodeLegacyAvoid in modern contracts

Use delegatecall only when you fully control the callee and understand the storage implications.


Handling storage in assembly

Storage access is where inline assembly can become especially dangerous. Solidity’s storage layout rules are deterministic, but assembly does not protect you from writing to the wrong slot.

A simple storage read/write example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Counter {
    uint256 public count;

    function increment() external {
        assembly {
            let current := sload(count.slot)
            sstore(count.slot, add(current, 1))
        }
    }
}

Here count.slot is a Solidity-generated constant representing the storage slot of count.

Best practices for storage access

  • Use .slot and .offset where possible.
  • Keep storage assembly localized and documented.
  • Avoid hardcoding slot numbers unless the contract is intentionally using a fixed layout.
  • Never mix manual storage writes with assumptions about packed variables unless you have verified the layout.

Packed storage caution

If multiple variables share a slot, assembly must preserve unrelated bits. A blind sstore can corrupt neighboring values. In those cases, read-modify-write with masks and shifts is required.


Writing safer assembly

Inline assembly is not inherently unsafe, but it removes many guardrails. The safest approach is to minimize the assembly surface area and make invariants obvious.

Practical guidelines

  1. Isolate assembly in small functions.
  2. Keep the logic short and easy to audit.

  1. Document assumptions.
  2. Note expected calldata layout, memory usage, and storage slots.

  1. Prefer named variables.
  2. Avoid opaque stack juggling when a clear variable improves readability.

  1. Validate inputs before assembly.
  2. Check lengths and bounds in Solidity when possible.

  1. Test edge cases aggressively.
  2. Include zero values, maximum values, empty calldata, and malformed inputs.

  1. Compare against a high-level reference implementation.
  2. This is one of the best ways to catch subtle bugs.

A useful pattern: high-level wrapper, low-level core

A strong design is to keep public-facing validation in Solidity and reserve assembly for the inner operation.

function safeHash(bytes calldata data) external pure returns (bytes32) {
    require(data.length <= 1024, "too large");
    assembly {
        let h := keccak256(data.offset, data.length)
        mstore(0x00, h)
        return(0x00, 0x20)
    }
}

This pattern keeps the dangerous part small and the preconditions explicit.


When not to use inline assembly

Assembly is not the right tool for every optimization. It can increase audit cost, reduce portability, and make future refactors harder.

Avoid it when:

  • the gas savings are minor
  • the logic is already clear in Solidity
  • the code will be maintained by a broad team
  • the operation depends on complex ABI structures that are easy to misread
  • the same result can be achieved with a well-optimized library

A good rule: if you cannot explain the memory or stack behavior in one or two sentences, the assembly may be too complex for production.


Testing and auditing assembly-heavy code

Assembly requires more than standard unit tests. You should verify both functional correctness and low-level assumptions.

Recommended testing approach

  • Write tests for normal and boundary inputs.
  • Fuzz calldata lengths, offsets, and numeric extremes.
  • Compare assembly output against a reference Solidity implementation.
  • Test revert behavior, including exact revert data when relevant.
  • Use static analysis and manual review for storage and call opcodes.

Audit checklist

AreaQuestions to ask
MemoryIs the free memory pointer updated correctly?
CalldataAre offsets and lengths validated?
StorageAre slots and packed fields handled safely?
CallsAre failures and returndata handled correctly?
RevertsIs revert data preserved or intentionally transformed?

A disciplined review process matters more than clever opcode usage.


Conclusion

Inline assembly is one of Solidity’s most powerful advanced tools. It gives you direct access to the EVM, enabling precise calldata parsing, custom memory layouts, low-level calls, and gas-conscious storage operations. Used carefully, it can make critical contract paths leaner and more expressive.

The tradeoff is responsibility: every optimization must be justified, documented, and tested. In production systems, the best assembly code is usually the smallest amount necessary to solve a specific problem well.

Learn more with useful resources