# 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.