Langsung ke konten utama
BlockchainMar 28, 2026

Deep EVM #8: Membangun Token Swap di Yul Murni

OS
Open Soft Team

Engineering Team

Mengapa Token Swap di Yul?

Token swap adalah operasi paling umum di DeFi — dan salah satu yang paling sensitif terhadap gas. Bot MEV, aggregator DEX, dan protokol DeFi semuanya melakukan ribuan swap per hari. Setiap gas yang dihemat per swap bertambah menjadi keuntungan yang signifikan.

Dalam artikel ini, kita membangun token swap lengkap di Yul murni yang berinteraksi dengan Uniswap V2 pair — tanpa Solidity interface, tanpa SafeERC20, murni opcode.

Arsitektur Uniswap V2 Swap

Uniswap V2 swap melibatkan beberapa langkah:

  1. Transfer token input ke pair contract
  2. Panggil pair.swap() — mengirim token output ke penerima
  3. Pair memverifikasi invariant K (x * y = k)
// Urutan panggilan:
token.transfer(pair, amountIn)
pair.swap(amount0Out, amount1Out, to, data)

Langkah 1: Transfer ERC20 di Yul

Untuk memanggil transfer(address,uint256) pada kontrak ERC20:

assembly {
    // Encode calldata
    let ptr := mload(0x40)
    
    // Function selector: transfer(address,uint256)
    // keccak256("transfer(address,uint256)") = 0xa9059cbb...
    mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
    mstore(add(ptr, 0x04), recipient)  // address to
    mstore(add(ptr, 0x24), amount)     // uint256 amount
    
    // Call
    let success := call(
        gas(),      // forward semua gas
        token,      // alamat kontrak ERC20
        0,          // tanpa ETH
        ptr,        // input offset
        0x44,       // input size (4 + 32 + 32 = 68 byte)
        0x00,       // output offset
        0x20        // output size (32 byte untuk bool)
    )
    
    // Verifikasi sukses
    // Beberapa token tidak mengembalikan bool (USDT!)
    let returnOk := or(
        and(success, or(
            iszero(returndatasize()),           // Tidak ada return data (USDT)
            and(gt(returndatasize(), 31), eq(mload(0x00), 1)) // true
        )),
        0  // fallback: gagal
    )
    
    if iszero(returnOk) {
        revert(0, 0)
    }
}

Menangani Token Non-Standard

Beberapa token ERC20 tidak mengikuti standar:

  • USDT — Tidak mengembalikan bool pada transfer
  • BNB — Mengembalikan lebih dari 32 byte
  • Token deflasi — Membakar persentase pada transfer

Kode di atas menangani kasus USDT dengan memeriksa iszero(returndatasize()).

Langkah 2: Panggil Uniswap V2 Pair.swap()

assembly {
    let ptr := mload(0x40)
    
    // swap(uint256,uint256,address,bytes)
    // selector: 0x022c0d9f
    mstore(ptr, 0x022c0d9f00000000000000000000000000000000000000000000000000000000)
    mstore(add(ptr, 0x04), amount0Out)  // uint256
    mstore(add(ptr, 0x24), amount1Out)  // uint256
    mstore(add(ptr, 0x44), to)          // address
    mstore(add(ptr, 0x64), 0x80)        // offset ke bytes data
    mstore(add(ptr, 0x84), 0)           // bytes length = 0
    
    let success := call(
        gas(),
        pair,
        0,
        ptr,
        0xa4,       // 4 + 32*4 + 32 = 164 byte
        0x00,
        0x00        // Tidak mengharapkan return data
    )
    
    if iszero(success) {
        // Bubble up revert reason
        returndatacopy(0, 0, returndatasize())
        revert(0, returndatasize())
    }
}

Menghitung Amount Out

Formula Uniswap V2:

amountOut = (amountIn * 997 * reserveOut) / (reserveIn * 1000 + amountIn * 997)

Di Yul:

assembly {
    function getAmountOut(amountIn, reserveIn, reserveOut) -> amountOut {
        let amountInWithFee := mul(amountIn, 997)
        let numerator := mul(amountInWithFee, reserveOut)
        let denominator := add(mul(reserveIn, 1000), amountInWithFee)
        amountOut := div(numerator, denominator)
    }
}

Perhatian: untuk jumlah besar, mul(amountIn, 997) dan operasi selanjutnya bisa overflow uint256. Dalam praktik, Anda perlu menggunakan aritmatika mulmod atau membagi komputasi.

Mendapatkan Reserves

Panggil getReserves() pada pair contract:

assembly {
    let ptr := mload(0x40)
    
    // getReserves() selector: 0x0902f1ac
    mstore(ptr, 0x0902f1ac00000000000000000000000000000000000000000000000000000000)
    
    let success := staticcall(
        gas(),
        pair,
        ptr,
        0x04,       // Hanya selector, tanpa parameter
        ptr,        // Tulis output di tempat yang sama
        0x60        // 3 * 32 byte (reserve0, reserve1, blockTimestampLast)
    )
    
    if iszero(success) { revert(0, 0) }
    
    let reserve0 := mload(ptr)
    let reserve1 := mload(add(ptr, 0x20))
    // blockTimestampLast di add(ptr, 0x40) — biasanya tidak diperlukan
}

Swap Lengkap: Menggabungkan Semuanya

function swapExactTokensForTokens(
    address tokenIn,
    address tokenOut,
    address pair,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient
) external {
    assembly {
        // 1. Dapatkan reserves
        let ptr := mload(0x40)
        mstore(ptr, 0x0902f1ac00000000000000000000000000000000000000000000000000000000)
        pop(staticcall(gas(), pair, ptr, 0x04, ptr, 0x60))
        
        let reserve0 := mload(ptr)
        let reserve1 := mload(add(ptr, 0x20))
        
        // 2. Tentukan arah swap
        let zeroForOne := lt(tokenIn, tokenOut)
        let reserveIn := reserve0
        let reserveOut := reserve1
        if iszero(zeroForOne) {
            reserveIn := reserve1
            reserveOut := reserve0
        }
        
        // 3. Hitung amount out
        let amountInWithFee := mul(amountIn, 997)
        let num := mul(amountInWithFee, reserveOut)
        let den := add(mul(reserveIn, 1000), amountInWithFee)
        let amountOut := div(num, den)
        
        // 4. Slippage check
        if lt(amountOut, minAmountOut) { revert(0, 0) }
        
        // 5. Transfer token ke pair
        mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
        mstore(add(ptr, 0x04), pair)
        mstore(add(ptr, 0x24), amountIn)
        pop(call(gas(), tokenIn, 0, ptr, 0x44, 0x00, 0x20))
        
        // 6. Execute swap
        mstore(ptr, 0x022c0d9f00000000000000000000000000000000000000000000000000000000)
        switch zeroForOne
        case 1 {
            mstore(add(ptr, 0x04), 0)
            mstore(add(ptr, 0x24), amountOut)
        }
        default {
            mstore(add(ptr, 0x04), amountOut)
            mstore(add(ptr, 0x24), 0)
        }
        mstore(add(ptr, 0x44), recipient)
        mstore(add(ptr, 0x64), 0x80)
        mstore(add(ptr, 0x84), 0)
        
        if iszero(call(gas(), pair, 0, ptr, 0xa4, 0, 0)) {
            returndatacopy(0, 0, returndatasize())
            revert(0, returndatasize())
        }
    }
}

Perbandingan Gas

ImplementasiGas (swap)Overhead
Uniswap V2 Router (Solidity)~105.000Baseline
Custom Solidity (tanpa router)~85.000-19%
Yul murni (di atas)~72.000-31%
Huff (artikel berikutnya)~65.000-38%

Penghematan 31% datang dari: tanpa pemeriksaan overflow, tanpa SafeERC20 wrapper, encoding calldata manual, dan tanpa penanganan error verbose.

Risiko dan Pertimbangan

  1. Tidak ada pemeriksaan overflow — Pastikan input sudah divalidasi
  2. Tidak ada revert message — Lebih sulit di-debug
  3. Token non-standard — Perlu penanganan khusus per token
  4. Auditability — Kode Yul jauh lebih sulit di-audit

Kesimpulan

Membangun token swap di Yul murni mendemonstrasikan kekuatan kontrol tingkat rendah atas EVM. Anda menghemat ~31% gas dibanding implementasi Solidity dengan router, tetapi dengan biaya readability dan maintainability. Untuk bot MEV dan kontrak frekuensi tinggi, trade-off ini layak dilakukan. Dalam artikel berikutnya, kita akan mendorong lebih jauh lagi dengan Huff — menulis swap di assembly murni.