Tutoriel·10 min de lecture·Par Solingo

Yul Assembly — When and Why You Should Use It

Inline assembly in Solidity scares developers. Here's when it's the right tool and when it's premature.

# Yul Assembly — When and Why You Should Use It

Inline assembly gives you access to EVM opcodes that Solidity does not expose. It is powerful, gas-efficient, and dangerous.

Here is when to use it and when to avoid it.

Yul Basics

Yul is an intermediate language compiled to EVM bytecode. You write it inside assembly { ... } blocks.

function getCodeSize(address addr) public view returns (uint256 size) {

assembly {

size := extcodesize(addr)

}

}

This is not possible in pure Solidity. You need assembly.

When to Use Assembly

1. Gas-Critical Paths

If gas savings matter more than readability, assembly wins.

// Solidity: ~500 gas

function sumArray(uint256[] calldata arr) public pure returns (uint256 total) {

for (uint i = 0; i < arr.length; i++) {

total += arr[i];

}

}

// Assembly: ~300 gas

function sumArrayAsm(uint256[] calldata arr) public pure returns (uint256 total) {

assembly {

let len := arr.length

let dataPtr := arr.offset

for { let i := 0 } lt(i, len) { i := add(i, 1) } {

total := add(total, calldataload(add(dataPtr, mul(i, 32))))

}

}

}

The assembly version avoids bounds checks and memory allocation.

2. Opcodes Solidity Does Not Expose

Some opcodes have no Solidity equivalent:

  • returndatasize — get size of return data
  • returndatacopy — copy return data to memory
  • extcodehash — get contract code hash
function getReturnDataSize() internal pure returns (uint256 size) {

assembly {

size := returndatasize()

}

}

3. Precompile Calls

Calling precompiles (like ecrecover, sha256, or bn256 pairing) requires assembly for optimal gas usage.

function ecrecoverRaw(bytes32 hash, uint8 v, bytes32 r, bytes32 s)

public view returns (address signer) {

assembly {

let ptr := mload(0x40)

mstore(ptr, hash)

mstore(add(ptr, 32), v)

mstore(add(ptr, 64), r)

mstore(add(ptr, 96), s)

if iszero(staticcall(gas(), 1, ptr, 128, ptr, 32)) {

revert(0, 0)

}

signer := mload(ptr)

}

}

This saves gas compared to ecrecover() wrapper.

When NOT to Use Assembly

1. Normal Business Logic

If Solidity can express it clearly, do not use assembly.

// BAD: premature optimization

function add(uint256 a, uint256 b) public pure returns (uint256 c) {

assembly {

c := add(a, b)

}

}

// GOOD: readable Solidity

function add(uint256 a, uint256 b) public pure returns (uint256) {

return a + b;

}

2. When Readability Matters More Than Gas

Most contracts are not gas-bottlenecked. Clarity beats a 100 gas savings.

Common Patterns

Efficient Hashing

function hashPacked(address a, uint256 b) public pure returns (bytes32 hash) {

assembly {

let ptr := mload(0x40)

mstore(ptr, a)

mstore(add(ptr, 32), b)

hash := keccak256(ptr, 64)

}

}

Returndata Forwarding

function forwardCall(address target, bytes calldata data) external payable {

assembly {

let result := call(gas(), target, callvalue(), add(data.offset, 32), mload(data.offset), 0, 0)

let size := returndatasize()

returndatacopy(0, 0, size)

switch result

case 0 { revert(0, size) }

default { return(0, size) }

}

}

This is how proxies forward calls without decoding return data.

Packed Storage

struct PackedData {

uint128 a;

uint128 b;

}

function readPacked(PackedData storage data) internal view returns (uint128 a, uint128 b) {

assembly {

let packed := sload(data.slot)

a := and(packed, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)

b := shr(128, packed)

}

}

This avoids double SLOAD operations.

Common Pitfalls

1. Memory Safety

Solidity assumes memory beyond 0x40 is free. If you write to arbitrary memory, you can corrupt other data.

Always use mload(0x40) to get the free memory pointer.

2. Stack Too Deep

EVM has a 16-item stack limit. Assembly does not help — you still hit stack-too-deep errors.

// BAD: too many variables

function complexCalc(uint a, uint b, uint c, uint d, uint e, uint f, uint g)

public pure returns (uint) {

assembly {

let x := add(a, b)

let y := add(c, d)

let z := add(e, f)

let w := add(g, x)

// Stack too deep

}

}

Solution: use memory or storage.

3. No Type Safety

Yul has no type checking. Mistakes are silent.

assembly {

let x := 0x1234

mstore(x, 100) // Overwrites memory at 0x1234, probably wrong

}

Always validate offsets and pointers.

Low-Level Delegatecall

Here is a full delegatecall implementation:

function delegatecallWithRevert(address target, bytes calldata data)

external payable returns (bytes memory) {

assembly {

let ptr := mload(0x40)

calldatacopy(ptr, data.offset, data.length)

let result := delegatecall(gas(), target, ptr, data.length, 0, 0)

let size := returndatasize()

returndatacopy(ptr, 0, size)

switch result

case 0 { revert(ptr, size) }

default { return(ptr, size) }

}

}

This is how minimal proxies work.

Summary

Use assembly when:

  • Gas savings justify the complexity
  • You need opcodes Solidity does not expose
  • You are optimizing a hot path

Do not use assembly when:

  • Readability matters more than gas
  • You are writing normal business logic
  • You do not fully understand EVM memory layout

Assembly is a scalpel, not a hammer. Use it carefully.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement