Tutoriel·10 min de lecture·Par Solingo

Assembly Yul — Quand et Pourquoi L'Utiliser

L'inline assembly en Solidity fait peur. Voici quand c'est le bon outil et quand c'est prématuré.

# Assembly Yul — Quand et Pourquoi L'Utiliser

L'assembly en Solidity = couteau suisse. Puissant, mais facile de se couper.

Qu'est-ce que Yul ?

Yul = langage intermédiaire entre Solidity et opcodes EVM.

// Solidity high-level

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

return a + b;

}

// Yul (inline assembly)

function addYul(uint a, uint b) public pure returns (uint result) {

assembly {

result := add(a, b) // Opcode ADD direct

}

}

// Équivalent opcodes

PUSH a

PUSH b

ADD

Bases de Yul

Syntaxe

assembly {

let x := 7 // Variable locale

let y := add(x, 3) // y = 10

let z := mul(y, 2) // z = 20

// Stockage

sstore(0, z) // storage[0] = z

let val := sload(0) // val = storage[0]

// Mémoire

mstore(0x80, z) // memory[0x80] = z

let mem := mload(0x80) // mem = memory[0x80]

// Control flow

if lt(x, 10) { // if x < 10

y := mul(y, 2)

}

// Loops

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

// ...

}

}

Opcodes Principaux

Arithmétique : add, sub, mul, div, mod

Comparaison : lt, gt, eq

Bitwise : and, or, xor, not

Crypto : keccak256

Storage : sload, sstore

Memory : mload, mstore

Calldata : calldataload, calldatasize

Layout Mémoire

0x00 - 0x3f : scratch space (réutilisable)

0x40 - 0x5f : free memory pointer

0x60 - 0x7f : zero slot (jamais utilisé)

0x80+ : votre data

Quand Utiliser Assembly

1. Gas-Critical Paths

// Sans assembly : bounds check automatique

function sumArray(uint[] memory arr) public pure returns (uint sum) {

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

sum += arr[i]; // Bounds check à chaque itération

}

}

// Gas : ~3000 + 800*N

// Avec assembly : skip bounds checks

function sumArrayUnchecked(uint[] memory arr) public pure returns (uint sum) {

assembly {

let len := mload(arr) // arr.length

let data := add(arr, 0x20) // Pointer vers arr[0]

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

let offset := mul(i, 0x20)

sum := add(sum, mload(add(data, offset)))

}

}

}

// Gas : ~3000 + 400*N (50% savings)

Gain : 50% gas, mais vous êtes responsable de la safety.

2. Opcodes Non Exposés

// Solidity ne peut pas accéder RETURNDATASIZE/RETURNDATACOPY directement

function forwardReturndata() external {

// Call externe

(bool success, bytes memory data) = target.call(payload);

// Solidity copie TOUTE la returndata en memory (coûteux)

// Même si on ne l'utilise pas !

}

// Assembly : forward sans copier

function forwardReturndataUnchecked() external {

assembly {

let success := call(gas(), target, 0, add(payload, 0x20), mload(payload), 0, 0)

// Forward returndata directement

returndatacopy(0, 0, returndatasize())

switch success

case 0 { revert(0, returndatasize()) }

default { return(0, returndatasize()) }

}

}

3. Précompilés Custom

// ecrecover via Solidity = copie en memory

function recoverSigner(bytes32 hash, bytes memory sig) public view returns (address) {

// Solidity wrapper

(bytes32 r, bytes32 s, uint8 v) = splitSignature(sig);

return ecrecover(hash, v, r, s);

}

// ecrecover via assembly = 0 copy

function recoverSignerUnchecked(bytes32 hash, bytes memory sig) public view returns (address signer) {

assembly {

// sig = 65 bytes : r (32) + s (32) + v (1)

let r := mload(add(sig, 0x20))

let s := mload(add(sig, 0x40))

let v := byte(0, mload(add(sig, 0x60)))

// Prépare input pour ecrecover (address 0x01)

mstore(0x00, hash)

mstore(0x20, v)

mstore(0x40, r)

mstore(0x60, s)

// Call précompilé

let success := staticcall(gas(), 0x01, 0x00, 0x80, 0x00, 0x20)

if success {

signer := mload(0x00)

}

}

}

Quand NE PAS Utiliser Assembly

1. Optimisation Prématurée

// ❌ Mauvais : assembly pour un simple add

function increment(uint x) public pure returns (uint) {

assembly {

x := add(x, 1)

}

return x;

}

// ✅ Bon : Solidity suffit (compilateur optimise déjà)

function increment(uint x) public pure returns (uint) {

return x + 1;

}

Le compilateur Solidity 0.8+ est très bon. N'utilisez assembly que si vous avez profilé et trouvé un hotspot.

2. Logique Complexe

// ❌ Mauvais : business logic en assembly

assembly {

// 100 lignes de calculs compliqués

// Personne ne peut review

// Bug = fonds perdus

}

// ✅ Bon : assembly seulement pour les primitives

function complexLogic(uint a, uint b) public pure returns (uint) {

uint x = efficientHash(a, b); // Assembly ici OK

uint y = businessRules(x); // Solidity ici

return y;

}

Patterns Utiles

1. Hashing Efficient

// Solidity : abi.encodePacked alloue en memory

function hashPair(uint a, uint b) public pure returns (bytes32) {

return keccak256(abi.encodePacked(a, b));

}

// Assembly : hash in-place (pas d'allocation)

function hashPairUnchecked(uint a, uint b) public pure returns (bytes32 hash) {

assembly {

mstore(0x00, a)

mstore(0x20, b)

hash := keccak256(0x00, 0x40) // Hash 64 bytes

}

}

2. Packed Storage Read

// Storage layout :

// slot 0 : | unused (12) | addr (20) |

// slot 1 : | balance (32) |

function getAddress() public view returns (address addr) {

assembly {

let slot0 := sload(0)

addr := and(slot0, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // Mask 20 bytes

}

}

3. Low-Level Delegatecall

// Proxy minimal

fallback() external payable {

address impl = implementation; // Storage var

assembly {

// Copy calldata

calldatacopy(0, 0, calldatasize())

// Delegatecall

let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)

// Copy returndata

returndatacopy(0, 0, returndatasize())

// Return or revert

switch result

case 0 { revert(0, returndatasize()) }

default { return(0, returndatasize()) }

}

}

Pièges à Éviter

1. Memory Safety

// ❌ Dangereux : overwrite free memory pointer

assembly {

mstore(0x40, 0x80) // Reset vers 0x80

// Si Solidity alloue après, collision !

}

// ✅ Safe : toujours lire et update le pointer

assembly {

let freePtr := mload(0x40)

// Use memory at freePtr

mstore(freePtr, data)

// Update pointer

mstore(0x40, add(freePtr, 0x20))

}

2. Stack Too Deep

// EVM stack = max 16 items

assembly {

let a := 1

let b := 2

// ... 15 variables

let p := 16

let q := 17 // ❌ Stack too deep !

}

// Mitigation : use memory

assembly {

mstore(0x80, 17) // Store in memory

let q := mload(0x80)

}

3. Unchecked Arithmetic

// Assembly = pas de overflow checks !

assembly {

let x := 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

x := add(x, 1) // Wrap à 0, pas de revert

}

// Si vous voulez safety, check manually

assembly {

let result := add(a, b)

if lt(result, a) { // Overflow detecté

revert(0, 0)

}

}

Exemple Complet : Efficient Array Sum

contract ArraySum {

// Baseline Solidity

function sumSolidity(uint[] calldata arr) external pure returns (uint sum) {

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

sum += arr[i];

}

}

// Gas : 23,000 + 800*N

// Optimized Yul

function sumYul(uint[] calldata arr) external pure returns (uint sum) {

assembly {

let len := arr.length

// Loop unrolling : process 4 items per iteration

let i := 0

let end := sub(len, mod(len, 4))

for {} lt(i, end) { i := add(i, 4) } {

sum := add(sum, calldataload(add(arr.offset, mul(i, 0x20))))

sum := add(sum, calldataload(add(arr.offset, mul(add(i, 1), 0x20))))

sum := add(sum, calldataload(add(arr.offset, mul(add(i, 2), 0x20))))

sum := add(sum, calldataload(add(arr.offset, mul(add(i, 3), 0x20))))

}

// Handle remaining items

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

sum := add(sum, calldataload(add(arr.offset, mul(i, 0x20))))

}

}

}

// Gas : 23,000 + 300*N (62% savings)

}

Verdict

Utilisez assembly pour :

  • Hot paths profiled (loops, hashing)
  • Opcodes non disponibles (returndatacopy, etc.)
  • Wrappers de précompilés

N'utilisez PAS assembly pour :

  • Logique business
  • Code non-profiled
  • Montrer que vous êtes smart

Et surtout : auditez 3× le code assembly. Un bug = fonds perdus.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement