Deep EVM #8: Membangun Token Swap di Yul Murni
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:
- Transfer token input ke pair contract
- Panggil pair.swap() — mengirim token output ke penerima
- 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
| Implementasi | Gas (swap) | Overhead |
|---|---|---|
| Uniswap V2 Router (Solidity) | ~105.000 | Baseline |
| 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
- Tidak ada pemeriksaan overflow — Pastikan input sudah divalidasi
- Tidak ada revert message — Lebih sulit di-debug
- Token non-standard — Perlu penanganan khusus per token
- 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.