Deep EVM #5: Pengantar Yul — Bahasa Assembly Rahasia Solidity
Engineering Team
Apa Itu Yul?
Yul adalah bahasa assembly intermediate yang dirancang untuk EVM. Ia berada di antara Solidity tingkat tinggi dan opcode EVM mentah. Anda bisa menggunakan Yul dalam dua cara:
- Inline assembly — Blok
assembly { }di dalam Solidity - Standalone Yul — File
.yulyang dikompilasi langsung ke bytecode
Yul memberikan kontrol langsung atas opcode EVM sambil mempertahankan sintaks yang dapat dibaca. Tidak seperti Huff yang benar-benar mentah, Yul memiliki variabel, fungsi, dan kontrol alur terstruktur.
Sintaks Dasar Yul
assembly {
// Variabel
let x := 42
let y := add(x, 1) // y = 43
// Memory
mstore(0x00, x) // Simpan x di memory[0x00]
let z := mload(0x00) // Baca dari memory[0x00]
// Storage
sstore(0, x) // Simpan x di storage slot 0
let w := sload(0) // Baca dari storage slot 0
// Kontrol alur
if gt(x, 0) {
// x > 0
}
switch x
case 0 { /* x == 0 */ }
case 1 { /* x == 1 */ }
default { /* lainnya */ }
// Loop
for { let i := 0 } lt(i, 10) { i := add(i, 1) } {
// badan loop
}
}
Variabel dan Tipe
Yul hanya memiliki satu tipe: u256 (unsigned 256-bit integer). Semua nilai adalah 32-byte word. Tidak ada string, array, struct, atau tipe kompleks lainnya.
assembly {
let a := 1 // u256
let b := 0xff // u256
let c := true // 1 (u256)
let d := false // 0 (u256)
// let s := "hello" // TIDAK VALID di Yul EVM
}
Boolean direpresentasikan sebagai 0 (false) dan non-zero (true). Alamat adalah u256 dengan 12 byte atas nol.
Mengakses Data Solidity dari Yul
Ketika menggunakan inline assembly di Solidity, Anda bisa mengakses variabel Solidity:
function example(uint256 input) external pure returns (uint256) {
uint256 result;
assembly {
// Akses parameter fungsi
result := add(input, 42)
}
return result;
}
Untuk variabel state (storage), Anda mengakses slot-nya:
uint256 public myValue; // slot 0
function readValue() external view returns (uint256) {
uint256 val;
assembly {
val := sload(0) // Baca dari slot 0
}
return val;
}
Untuk mapping, Anda harus menghitung slot secara manual:
mapping(address => uint256) public balances; // slot 0
function getBalance(address user) external view returns (uint256) {
uint256 bal;
assembly {
// slot = keccak256(user . 0)
mstore(0x00, user)
mstore(0x20, 0) // slot dasar mapping
let slot := keccak256(0x00, 0x40)
bal := sload(slot)
}
return bal;
}
Fungsi Yul
assembly {
function safeAdd(a, b) -> result {
result := add(a, b)
if lt(result, a) {
revert(0, 0) // overflow
}
}
function max(a, b) -> m {
m := a
if gt(b, a) {
m := b
}
}
let sum := safeAdd(100, 200)
let bigger := max(sum, 500)
}
Fungsi Yul dikompilasi menjadi JUMP/JUMPDEST di bytecode — mirip dengan fungsi di Huff. Tidak ada overhead pemanggilan fungsi yang signifikan (8 gas untuk JUMP + 1 untuk JUMPDEST).
Kapan Menggunakan Inline Assembly
Inline assembly berguna ketika:
- Optimasi gas kritis — Hot path yang dipanggil jutaan kali
- Operasi yang tidak tersedia di Solidity —
RETURNDATASIZE,CHAINID,COINBASE - Packing/unpacking data — Manipulasi bit yang lebih efisien
- Komputasi hash kustom — Keccak256 dengan layout memory manual
- Interaksi tingkat rendah — DELEGATECALL, CREATE2 dengan parameter spesifik
Contoh: Penghematan Gas Nyata
// Solidity murni: ~800 gas
function transferSolidity(address to, uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
}
// Dengan assembly: ~600 gas
function transferAssembly(address to, uint256 amount) external {
assembly {
// Hitung slot pengirim
mstore(0x00, caller())
mstore(0x20, 0) // slot dasar balances
let senderSlot := keccak256(0x00, 0x40)
let senderBal := sload(senderSlot)
// Check
if lt(senderBal, amount) { revert(0, 0) }
// Update pengirim
sstore(senderSlot, sub(senderBal, amount))
// Hitung slot penerima
mstore(0x00, to)
let recipientSlot := keccak256(0x00, 0x40)
let recipientBal := sload(recipientSlot)
// Update penerima
sstore(recipientSlot, add(recipientBal, amount))
}
}
Keselamatan dan Risiko Assembly
Inline assembly melewati banyak pemeriksaan keselamatan Solidity:
- Tidak ada pemeriksaan overflow — Anda harus menambahkan sendiri
- Tidak ada pemeriksaan tipe — Semua 32-byte word
- Akses memory langsung — Bisa menimpa free memory pointer
- Tidak ada reentrancy guard otomatis — Harus ditangani manual
Aturan emas: gunakan assembly hanya ketika penghematan gas terbukti signifikan melalui benchmarking.
Debugging Yul
Foundry mendukung debugging assembly:
# Trace eksekusi dengan opcode detail:
forge test -vvvv --match-test testTransferAssembly
# Gas snapshot:
forge snapshot
Anda juga bisa menggunakan forge debug untuk stepping melalui opcode satu per satu.
Kesimpulan
Yul adalah jembatan antara Solidity dan opcode EVM mentah. Ia memberikan kontrol yang cukup untuk optimasi gas serius sambil mempertahankan readability yang lebih baik dari Huff. Untuk sebagian besar kasus penggunaan optimasi, inline assembly Yul adalah pilihan yang tepat — cukup dekat dengan metal untuk menghemat gas, cukup tinggi levelnya untuk tetap maintainable.