Tekan ESC untuk menutup

Pinjaman Kripto: Untung Besar atau Risiko Liquidation?

Ringkasan: Artikel ini membedah mekanisme crypto lending dan likuidasi di protokol Web3 (Aave v3, Morpho Blue). Membahas metrik utama (Health Factor, LTV, Liquidation Threshold), skenario slippage saat oracle Chainlink lag, serta menyajikan skrip Python monitoring untuk likuidasi posisi secara otomatis.

Cara Kerja Crypto Lending dan Risiko Likuidasi

Pinjaman kripto itu beda banget sama bank konvensional, di mana sales bank bakal ngecek slip gaji atau sertifikat rumah nenek lu sebelum nyetujuin KPR. Di Web3, smart contract sama sekali gak peduli nama lu siapa, riwayat BI Checking/SLIK kayak gimana, atau rencana hidup lu apa. Di sini yang berlaku cuma matematika dingin smart contract: lu taruh agunan (collateral), dapet pinjaman berlebih (overcollateralized loan), dan kalau lu lengah pantau Health Factor—bot likuidator bakal melahap aset lu habis-habisan dalam hitungan milidetik.

Ambil contoh skema klasik di Aave v3 atau Morpho Blue. Lu setor $10,000 dalam bentuk WBTC buat minjem $6,000 USDC. Buat apa? Biar gak perlu jual Bitcoin, gak kena pajak capital gains, sekaligus dapet cash dingin buat nge-ape ke shitcoin alternatif atau beli rig baru. Kelihatan kayak holy grail. Di atas kertas.

Tapi terus pasar mendadak dump 18% cuma dalam waktu 15 menit. Likuiditas di order book langsung kering kerontang. Oracle Chainlink ngalamin delay update harga beberapa blok gara-gara jaringan ke-spam, dan posisi lu langsung terjun bebas ke neraka likuidasi.

Anatomi Likuidasi: Kenapa Agunan Lu Bisa Hangus Seketika

Metrik utama yang wajib lu pantau 24/7 adalah Health Factor (HF). Rumusnya simpel banget:

HF = ∑ (Collaterali × LTVi) / Total Borrowed

Kalau HF > 1, posisi lu aman. Tapi kalau HF ≤ 1, posisi lu resmi jadi 'makanan empuk' yang bisa dilikuidasi sama siapa pun di jaringan yang nge-run skrip likuidasi.

ParameterPenjelasanContoh Riil (Pool ETH)
Max LTVPersentase pinjaman maksimal dari total nilai agunan80% (Pinjaman $800 per $1000 ETH)
Liquidation Threshold (LT)Batas aman sebelum posisi lu masuk zona likuidasi82.5%
Liquidation BonusDiskon harga agunan yang didapet sama likuidator5%
Reserve FactorPotongan komisi protokol dari bunga pinjaman15%

Tapi tunggu dulu. Angka 82.5% emang kelihatan ada jeda aman di atas kertas, tapi di lapangan lu harus ngitung slippage gaib sama gas fee mainnet. Begitu oracle tipe push ngirim update harga baru ke contract, bot-bot MEV udah antre manis di bundle Flashbots. Mereka bakal mborong posisi lu pake diskon 5%, langsung melunasi utangnya lewat flash loan di Uniswap v3, terus ngantongin profit bersihnya.

Hasil akhirnya? Lu cuma nyisa USDC yang mungkin udah lu buang entah ke mana, sementara WBTC lu udah amblas. Protokol dapet denda, bot dapet spread. Lu rugi bandar.

Skrip Likuidator: Cara Bot Mengendus Celah Posisi Lu

Gua sendiri setengah tahun lalu nemuin kejadian di Arbitrum di mana gara-gara satu oracle eror, depocit senilai $200k ludes cuma dalam tiga blok. Bikin skrip likuidasi dasar emang gampang, tapi yang nentuin menang kaji itu eksekusi milidetik dan akses langsung ke relay MEV.

Nih prototipe siap pakai dalam Python via web3.py yang nge-monitoring peminjam di LendingPool dan langsung nyiapin transaksi begitu HF jebol di bawah satu.

import os
import time
from web3 import Web3
from web3.exceptions import ContractLogicError, TransactionNotFound

# ------------------------------------------------------------------------------
# 1. Konfigurasi & Koneksi
# ------------------------------------------------------------------------------
RPC_URL = os.getenv("RPC_URL", "https://arb-mainnet.g.alchemy.com/v2/YOUR_API_KEY")
PRIVATE_KEY = os.getenv("PRIVATE_KEY", "0x_YOUR_PRIVATE_KEY")

w3 = Web3(Web3.HTTPProvider(RPC_URL))
if not w3.is_connected():
    raise RuntimeError("Gagal konek ke RPC. Coba cek RPC_URL lu.")

ACCOUNT = w3.eth.account.from_key(PRIVATE_KEY)

# Kontrak Aave v3 di Arbitrum
POOL_ADDRESS = Web3.to_checksum_address("0x794a61358D6845594F94dc1DB02A252b5b4814aD")
DATA_PROVIDER_ADDRESS = Web3.to_checksum_address("0x69FA0fee221AD11012B2f52097e8F6B7EAcF57E7")

TARGET_USER = Web3.to_checksum_address("0x0000000000000000000000000000000000000000")

COLLATERAL_ASSET = Web3.to_checksum_address("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f")  # WBTC
DEBT_ASSET = Web3.to_checksum_address("0xaf88d065e77c8cC2239327C5EDb3A432268e5831")        # USDC

# ------------------------------------------------------------------------------
# 2. ABI
# ------------------------------------------------------------------------------
POOL_ABI = [
    {
        "inputs": [{"internalType": "address", "name": "user", "type": "address"}],
        "name": "getUserAccountData",
        "outputs": [
            {"internalType": "uint256", "name": "totalCollateralBase", "type": "uint256"},
            {"internalType": "uint256", "name": "totalDebtBase", "type": "uint256"},
            {"internalType": "uint256", "name": "availableBorrowsBase", "type": "uint256"},
            {"internalType": "uint256", "name": "currentLiquidationThreshold", "type": "uint256"},
            {"internalType": "uint256", "name": "ltv", "type": "uint256"},
            {"internalType": "uint256", "name": "healthFactor", "type": "uint256"}
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {"internalType": "address", "name": "collateralAsset", "type": "address"},
            {"internalType": "address", "name": "debtAsset", "type": "address"},
            {"internalType": "address", "name": "user", "type": "address"},
            {"internalType": "uint256", "name": "debtToCover", "type": "uint256"},
            {"internalType": "bool", "name": "receiveAToken", "type": "bool"}
        ],
        "name": "liquidationCall",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

DATA_PROVIDER_ABI = [
    {
        "inputs": [
            {"internalType": "address", "name": "asset", "type": "address"},
            {"internalType": "address", "name": "user", "type": "address"}
        ],
        "name": "getUserReserveData",
        "outputs": [
            {"internalType": "uint256", "name": "currentATokenBalance", "type": "uint256"},
            {"internalType": "uint256", "name": "currentStableDebt", "type": "uint256"},
            {"internalType": "uint256", "name": "currentVariableDebt", "type": "uint256"},
            {"internalType": "uint256", "name": "principalStableDebt", "type": "uint256"},
            {"internalType": "uint256", "name": "scaledVariableDebt", "type": "uint256"},
            {"internalType": "uint256", "name": "stableBorrowRate", "type": "uint256"},
            {"internalType": "uint256", "name": "liquidityRate", "type": "uint256"},
            {"internalType": "uint40", "name": "stableRateLastUpdated", "type": "uint40"},
            {"internalType": "bool", "name": "usageAsCollateralEnabled", "type": "bool"}
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

ERC20_ABI = [
    {
        "inputs": [
            {"internalType": "address", "name": "owner", "type": "address"},
            {"internalType": "address", "name": "spender", "type": "address"}
        ],
        "name": "allowance",
        "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {"internalType": "address", "name": "spender", "type": "address"},
            {"internalType": "uint256", "name": "amount", "type": "uint256"}
        ],
        "name": "approve",
        "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], # FIX: Tipe balik dibetulin jadi bool
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [{"internalType": "address", "name": "account", "type": "address"}],
        "name": "balanceOf",
        "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
        "stateMutability": "view",
        "type": "function"
    }
]

pool = w3.eth.contract(address=POOL_ADDRESS, abi=POOL_ABI)
data_provider = w3.eth.contract(address=DATA_PROVIDER_ADDRESS, abi=DATA_PROVIDER_ABI)
debt_token = w3.eth.contract(address=DEBT_ASSET, abi=ERC20_ABI)

tx_in_flight = False

# ------------------------------------------------------------------------------
# 3. Helper Gas & Nonce yang Aman
# ------------------------------------------------------------------------------
def get_fee_parameters():
    """Narik parameter EIP-1559, plus ada fallback buat network yang kagak ada baseFeePerGas."""
    latest_block = w3.eth.get_block('latest')
    base_fee = latest_block.get('baseFeePerGas')
    
    if base_fee is not None:
        max_priority_fee = w3.to_wei(0.1, 'gwei')
        max_fee = base_fee * 2 + max_priority_fee
        return {
            'maxFeePerGas': max_fee,
            'maxPriorityFeePerGas': max_priority_fee
        }
    else:
        # Fallback buat network Legacy / custom L2
        return {'gasPrice': w3.eth.gas_price}

def ensure_allowance(required_amount: int) -> bool:
    """Cek dan execute approve, lengkap ama verifikasi status receipt-nya."""
    current_allowance = debt_token.functions.allowance(ACCOUNT.address, POOL_ADDRESS).call()
    if current_allowance >= required_amount:
        return True

    print("[*] Allowance kurang. Lagi ngirim tx approve...")
    nonce = w3.eth.get_transaction_count(ACCOUNT.address, 'pending')
    
    tx_params = {
        'chainId': w3.eth.chain_id,
        'from': ACCOUNT.address,
        'nonce': nonce,
        **get_fee_parameters()
    }
    
    tx = debt_token.functions.approve(POOL_ADDRESS, 2**256 - 1).build_transaction(tx_params)
    signed_tx = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)
    tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
    
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=30)
    if receipt['status'] == 1:
        print(f"[+] Approve sukses terkonfirmasi di blok {receipt['blockNumber']}")
        return True
    else:
        print(f"[!] ERROR: Transaksi approve di-revert!")
        return False

# ------------------------------------------------------------------------------
# 4. Logika Utama Eksekusi Likuidasi
# ------------------------------------------------------------------------------
def execute_liquidation(user_address: str):
    global tx_in_flight
    if tx_in_flight:
        print("[!] Process lokal kekunci (transaksi masih running/in flight).")
        return

    # FIX #9: Double-check HF secara kilat sebelum ngeproscess kalkulasi berat
    account_data = pool.functions.getUserAccountData(user_address).call()
    health_factor = account_data[5] / 10**18
    if health_factor >= 1.0:
        print(f"[-] Batal: HF posisi udah naik lagi ke {health_factor:.6f} (>= 1.0)")
        return

    # FIX #11: Hitung utang riil dalam unit token USDC
    reserve_data = data_provider.functions.getUserReserveData(DEBT_ASSET, user_address).call()
    actual_debt_tokens = reserve_data[1] + reserve_data[2]  # stable + variable
    
    if actual_debt_tokens == 0:
        print("[!] Kagak ada utang aktif buat token ini.")
        return

    debt_to_cover = actual_debt_tokens // 2

    # Cek saldo USDC di wallet sendiri
    our_balance = debt_token.functions.balanceOf(ACCOUNT.address).call()
    if our_balance < debt_to_cover:
        print(f"[!] Saldo USDC kurang. Butuh: {debt_to_cover}, Ada: {our_balance}")
        debt_to_cover = our_balance
        if debt_to_cover == 0:
            return

    # FIX #8: Pastikan proses approve berhasil dulu
    if not ensure_allowance(debt_to_cover):
        return

    # Pake nonce 'pending' biar nggak bentrok internal account
    nonce = w3.eth.get_transaction_count(ACCOUNT.address, 'pending')
    tx_params = {
        'chainId': w3.eth.chain_id,
        'from': ACCOUNT.address,
        'nonce': nonce,
        **get_fee_parameters()
    }

    # FIX #5: Simulasi transaksi persis sebelum dipack/dibuild
    try:
        estimated_gas = pool.functions.liquidationCall(
            COLLATERAL_ASSET,
            DEBT_ASSET,
            user_address,
            debt_to_cover,
            False
        ).estimate_gas(tx_params)
        
        tx_params['gas'] = int(estimated_gas * 1.2)
    except ContractLogicError as e:
        print(f"[!] Smart contract nolak simulasi liquidationCall: {e}")
        return
    except Exception as e:
        print(f"[!] Error pas estimasi gas: {e}")
        return

    # Build & broadcast
    try:
        tx_in_flight = True
        tx_data = pool.functions.liquidationCall(
            COLLATERAL_ASSET,
            DEBT_ASSET,
            user_address,
            debt_to_cover,
            False
        ).build_transaction(tx_params)

        signed_tx = w3.eth.account.sign_transaction(tx_data, PRIVATE_KEY)
        tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
        print(f"[+] Transaksi meluncur! Tx Hash: {tx_hash.hex()}")
        
        # FIX #10: Cek status receipt buat mastiin hasil likuidasi
        receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=30)
        if receipt['status'] == 1:
            print(f"[SUKSES] Posisi berhasil dilikuidasi di blok {receipt['blockNumber']}!")
        else:
            print(f"[GAGAL] Tx masuk blok tapi kena REVERT (Status 0). Gas hangus!")

    except Exception as e:
        print(f"[!] Error kritikal pas broadcast: {e}")
    finally:
        tx_in_flight = False

# ------------------------------------------------------------------------------
# 5. Loop Monitoring
# ------------------------------------------------------------------------------
def monitor_user(user_address: str):
    account_data = pool.functions.getUserAccountData(user_address).call()
    health_factor = account_data[5] / 10**18
    
    print(f"Target: {user_address} | Health Factor: {health_factor:.6f}")

    if health_factor < 1.0:
        print("[!] TARGET LIKUIDASI KETANGKAP!")
        execute_liquidation(user_address)

if __name__ == "__main__":
    while True:
        try:
            monitor_user(TARGET_USER)
            time.sleep(1)
        except KeyboardInterrupt:
            print("\nBot dihentikan oleh user.")
            break
        except Exception as e:
            print(f"ERROR: {e}")
            time.sleep(3)

Di L2 itu gak ada yang namanya mempool. Codel. Semuanya pake sistem FCFS (First-Come, First-Served). Siapa cepat, dia yang dapet.

Dan selagi skrip lu masih nge-lag gara-gara time.sleep(1), bot-bot HFT spesialis udah nempel via socket di rak server yang sama tempat validator jalan. Peluang lu buat balapan ngalahin margin call secara manual itu NOL BESAR.

Di Mana Cuan Sebenarnya (Dan Kenapa Orang Masih Pada Minjem)

Terus ngapain juga njeblokin diri ke risiko gede kayak gini?

  • Arbitrase Suku Bunga (Interest Rate Arbitrage). Lu pinjem USDT dengan APY 4% di Platform A, terus lu taruh di Vault ber-APY 12% di Platform B. Selisih 8% murni masuk kantong lu. Tapi ingat, ada risiko smart contract: kalau Platform B ngalamin depeg atau kena hack reentrancy, lu tetep utang duit riil ke Platform A.
  • Delegasi Likuiditas & Tokenisasi Agunan. Coba lirik protokol kayak Pendle atau Morpho. Lu jaminin stETH, dapet stablecoin, terus beliin PT (Principal Tokens) yang punya yield tetap. Ribet? Banget. Cuan? Parah—sampai timbul masalah pertama di oracle token LST.
  • Optimasi Pajak. Take profit = wajib bayar pajak. Minjem duit pake agunan aset yang harganya naik = gak dihitung sebagai transaksi penjualan. Gak ada penjualan = gak kena pajak. Pragmatis? Jelas.

Tapi orang-orang sering lupa sama efek likuidasi berantai (cascade liquidations). Kejadian Mei 2021 sama runtuhnya FTX ngebuktikan banyak protokol ngalamin kredit macet (bad debt) gara-gara harga agunan anjlok lebih cepet dibanding kemampuan bot menutup utang. Ujung-ujungnya, lender awam yang cuma berniat membungakan stablecoin mereka yang paling merana.

Isolated Margin Pool vs Cross-Margin: Di Mana Letak Jebakannya?

Banyak pemula keburu rungkad cuma gara-gara kagak paham arsitektur manajemen risiko. Di dunia DeFi, ada dua pendekatan yang beda banget: Cross-Margin (satu kuali bersama) dan Isolated Markets (pasar terisolasi ala Morpho atau Euler v2).

Di mode Cross-Margin (mode bawaan di Aave), seluruh portofolio jaminan lu berfungsi sebagai satu tameng bersama. Lu deposit WBTC, ETH, sama DAI — terus ngutang USDC. Kalau cuma harga WBTC yang anjlok, bantalan DAI bakal nahan posisi lu. Enak kan? Memang. Tapi kalau salah satu aset jaminan lu terjun bebas ke jurang (ingat kasus depeg stETH tahun 2022 atau rug pull token hasil bridge), dia bakal nyeret SELURUH posisi lu. Rungkad total sampai bersih!

Nah, di Isolated Pool, lu membatasi risiko cuma di pair tertentu aja — contohnya vault khusus wstETH / USDC.

[Depo Lu] ---> [Isolated Pool A (ETH/USDC)]  ---> Risiko cuma sebatas Pool A
          ---> [Isolated Pool B (PEPE/USDC)] ---> PEPE rug pull = yang rugi cuma Pool B

Kalau shitcoin di Pool B mendadak longsor 99% — lu cuma kehilangan aset yang ada di Pool B. ETH utama lu yang anteng di Pool A bakal tetep aman 100%.

Memang sih, ngurusin tiga posisi terpisah itu lumayan ribet dan boros gas fee. Tapi pas pasar lagi volatil-volatilnya, ini satu-satunya strategi yang paling masuk akal biar kagak bonyok.

Oracle: Cara Lu Kena Rungkad Tanpa Smart Contract Dihack

Smart contract di platform lending itu aslinya buta dan tuli. Dia kagak tahu berapa harga ETH detik ini — dia cuma percaya bulat-bulat sama oracle. Di sinilah keseruan (dan petaka) dimulai.

Ini dia beberapa penyakit utama oracle yang sering bikin deposit user ludes:

  • Stale Prices (Harga Kadaluarsa). Oracle kagak bakal nge-push harga tiap detik (bisa tekor bayar gas fee), tapi cuma pas ada perubahan harga, misalnya 0.5%, atau tiap 24 jam sekali (heartbeat). Kalau pasar mendadak nge-dump 5% dalam 3 detik terus langsung mantul naik lagi, oracle bisa aja baru update harga TEPAT DI TITIK TERENDAH. Kontrakt mambaca HF lu "0.99", langsung ngasih lampu hijau ke likuidator, dan posisi lu dilikuidasi saat itu juga. Pasarnya sih mantul naik, tapi lu udah keburu gigit jari.
  • Illiquid DEX Feeds (Manipulasi Pasar Spot). Kalau protokol pakai oracle TWAP dari Uniswap v3 buat token yang likuiditasnya tipis, whale tinggal jualan gede-gedean buat banting harga ke dasar bumi, memicu kaskade likuidasi di protokol lending, terus serok balik asetnya di harga murah. Jurus klasik!

Tabel di bawah ini nampilin hitung-hitungan riil kerugian pas terkena likuidasi di berbagai tingkat penurunan harga jaminan (dengan LTV awal 75% dan Bonus Likuidasi 5%):

Penurunan Harga JaminanHealth FactorStatus PosisiKerugian Peminjam dari Deposit Awal
-5%1.26Aman0% (cuma unrealized loss pada nilai jaminan)
-15%1.13Area Waspada0% (perlu top-up jaminan)
-25%0.99LIKUIDASI~15-20% (penalti likuidator + spread)
-40% (Flash Crash)< 0.80Rungkad Total100% jaminan ludes (sisa utang pinjaman doang)

Checklist Bertahan Hidup: Cara Ngutang Tanpa Kena Eksekusi

Kalau lu emang nekat mau ngutang di DeFi, pegang teguh aturan higienis ini. Daging semua ini, hasil pengalaman pahit gue sendiri yang pernah beberapa kali kena revert dan likuidasi paksa.

  • Jaga HF Selalu di Atas 1.5 - 1.8. Jangan sekali-kali main api di HF 1.05. Ada jarum merah dikit di chart, lu langsung rata.
  • Otomatisasi Top-Up (Self-Kicker). Manfaatkan bot automation kaya Gelato Automation atau Chainlink Automation. Bikin bot simpel yang bakal otomatis mentransfer stablecoin dari wallet lu ke contract buat bayar sebagian utang kalau HF lu drop di bawah 1.2.
  • Pantau Jenis Oracle-nya. Lu harus paham oracle apa yang dipakai di pool tempat lu nambang/ngutang. Kalau pakai Chainlink tipe push — pantau heartbeat-nya. Kalau pakai Pyth — cek confidence interval-nya.
  • Hedge Pakai Perps. Ngutang pakai jaminan ETH? Buka posisi short ETH di platform derivatif pakai leverage 1x sesuai nilai pinjaman. Langkah ini bakal mengunci risiko lu dalam bentuk nilai Dolar (delta-neutral).

Pinjaman kripto itu alat yang super powerful buat melipatgandakan modal, tapi di tangan orang yang asal-asalan, dia bisa berubah jadi gilotina dengan bom waktu. Smart contract kagak bakal peduli sama analisa teknikal atau firasat lu tentang arah pasar!

Rangkum postingan blog ini dengan:

FAQ

Likuidasi Aave v3 terjadi secara otomatis saat Health Factor peminjam turun di bawah 1,0 akibat penurunan nilai agunan atau kenaikan nilai pinjaman. Likuidator eksternal memanggil fungsi liquidationCall pada kontrak Pool untuk melunasi hingga 50% utang pengguna dengan imbalan penyerahan ekivalen aset jaminan ditambah bonus likuidasi sebesar 1% hingga 15%.

Health Factor dihitung dengan mengalikan total nilai agunan dalam ETH dengan rata-rata terbobot Liquidation Threshold, kemudian membaginya dengan total nilai utang dalam ETH. Pengguna mencegah likuidasi dengan memantau posisi secara aktif, menambah aset agunan yang didukung, atau melunasi sebagian utang melalui fungsi repay pada kontrak Pool sebelum transaksi likuidasi dieksekusi.

Profitabilitas likuidasi ditentukan oleh selisih antara nilai bonus agunan yang diperoleh dengan total biaya transaksi berdasarkan estimasi gas EIP-1559 dan slippage penukaran pada DEX. Pengembang bot menggunakan simulasi statis melalui estimateGas untuk memverifikasi eksekusi sebelum mengirimkan transaksi guna memastikan nilai maxFeePerGas dan maxPriorityFeePerGas tidak melebihi keuntungan likuidasi.
Piter Wacker

I am a trading specialist with expertise in market analysis, risk management, and investment strategies. I focus on identifying opportunities, executing trades with discipline, and delivering consistent results.

...

Sampaikan pemikiran Anda

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *