Mengatasi sifat immutable kontrak dengan pola proxy: mekanisme delegatecall, EIP-1967 untuk storage proxy, perbandingan pola Transparent dan UUPS, plugin OpenZeppelin Upgrades, serta governance dengan timelock dan multi-sig Safe

Setelah di episode 13 kalian menghubungkan kontrak ke dunia luar dengan oracles, pada episode ini kita menghadapi salah satu keputusan arsitektur terbesar: bagaimana memperbaiki kontrak yang sudah di-deploy.
Seperti yang sering kita tekankan, kontrak yang di-deploy bersifat immutable. Namun hampir semua protokol produksi butuh perbaikan bug, penambahan fitur, atau upgrade parameter. Solusinya adalah pola proxy: kontrak tetap bisa "diganti logikanya" tanpa mengubah alamat yang dipakai pengguna.
Mengapa episode ini penting? Karena proxy adalah arsitektur di balik mayoritas protokol besar — USDC, Uniswap, dan hampir semua protokol upgradeable memakainya. Tetapi proxy juga menambah kompleksitas keamanan yang besar: satu kesalahan konfigurasi bisa membuat dana terkunci atau kontrak "disandera".
Tanpa proxy, setiap perbaikan berarti migrasi pengguna ke kontrak baru dengan alamat baru — mahal, membingungkan, dan berisiko. Dengan proxy:
Jantung proxy adalah delegatecall: memanggil fungsi kontrak lain dengan storage milik sendiri. Implementasi dieksekusi, tetapi baca/tulis storage mengarah ke kontrak pemanggil.
Semua state variable tinggal di proxy; implementation hanya berisi logika. Saat upgrade, proxy diarahkan ke implementation baru — data pengguna tidak berubah.
Karena proxy harus kompatibel dengan storage layout, posisi alamat implementation tidak boleh bertabrakan dengan state variable kontrak. Solusinya: simpan di slot khusus yang tidak mungkin dipakai kontrak biasa.
bytes32 internal constant IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
function implementation() public view returns (address impl) {
assembly {
impl := sload(IMPLEMENTATION_SLOT)
}
}Slot tersebut dihitung dari rumus keccak256("eip1967.proxy.implementation") - 1. EIP-1967 menstandarkan slot untuk implementation, admin, dan beacon sehingga semua tooling (explorer, plugin) bisa mengenali proxy dengan benar.
Dua pola utama upgradeable proxy:
| Aspek | Transparent Proxy | UUPS |
|---|---|---|
| Siapa yang meng-upgrade | Admin terpisah, semua fungsi dialihkan | Fungsi upgrade ada di implementation itu sendiri |
| Storage footprint | Lebih besar (admin storage) | Lebih kecil |
| Gas tiap call | Sedikit lebih mahal (ada pengecekan admin) | Lebih hemat |
| Upgradeability | Bisa dilepas (set impl ke 0) | Dipegang kontrak sendiri |
| Umum dipakai | Kontrak lama (OpenZeppelin 3.x) | Standar modern |
Pada UUPS, upgrade dikelola lewat fungsi upgradeToAndCall yang hidup di implementation. Karena logika upgrade ada di kode, kehilangan kendali terhadap implementation = kehilangan kemampuan upgrade. Pada Transparent, admin proxy memegang kendali, sehingga impl bisa diganti bahkan jika kode impl bermasalah — tetapi tiap transaksi user membayar biaya pengecekan admin.
Tip
Untuk proyek baru, UUPS adalah pilihan yang disarankan — lebih hemat gas dan lebih sederhana. Transparent tetap relevan jika kalian ingin memisahkan kontrol upgrade sepenuhnya dari logika kontrak.
Mengerjakan proxy manual rentan error. OpenZeppelin Upgrades Plugin mengotomatisasi deploy, validasi, dan verifikasi:
npm install --save-dev @openzeppelin/hardhat-upgradesconst { ethers, upgrades } = require("hardhat");
async function main() {
const VaultV1 = await ethers.getContractFactory("VaultV1");
const proxy = await upgrades.deployProxy(VaultV1, [], { initializer: "initialize" });
await proxy.waitForDeployment();
const VaultV2 = await ethers.getContractFactory("VaultV2");
const upgraded = await upgrades.upgradeProxy(await proxy.getAddress(), VaultV2);
await upgraded.waitForDeployment();
}Plugin memvalidasi storage layout — ia menolak upgrade yang menggeser slot variabel yang sudah ada. Ingat: kontrak upgradeable tidak memakai constructor, melainkan initialize (pola initializer) karena constructor berjalan di implementation yang storage-nya terpisah.
Mengendalikan upgrade dari satu owner (EOA) adalah risiko tunggal. Standar produksi menggabungkan:
Alur umum: komunitas memilih → proposal dieksekusi via timelock → timelock dieksekusi oleh multi-sig → multi-sig memanggil upgradeToAndCall di proxy. Tidak ada satu entitas pun yang bisa meng-upgrade secara diam-diam.
constructor tidak tersimpan di proxy; wajib pakai initialize.fallback + delegatecall.Inti yang harus dibawa pulang:
delegatecall — logika di implementation, storage di proxy.initialize, dan kendalikan upgrade via timelock + multi-sig Safe.Di episode 15 selanjutnya kita akan membahas security: reentrancy & common vulnerabilities — serangan reentrancy dan pola checks-effects-interactions, overflow bawaan 0.8.x, flash loans, oracle manipulation, front-running, serta pitfall tx.origin dan selfdestruct. Sampai jumpa di episode 15!