Mempelajari pola desain kontrak yang memisahkan kontrak amatir dari kontrak production: Ownable, upgradeable proxy, pull-over-push payment, dan reentrancy guard — plus praktik merakit kontrak production-ready

Setelah di episode 4 kalian menguasai sintaks Solidity dan membangun ERC-20, pada episode ini kita menaikkan level: pola desain (design patterns) yang memisahkan kontrak amatir dari kontrak production. Pola-pola ini bukan hiasan — ia adalah jawaban atas masalah nyata yang dihadapi semua kontrak di dunia: siapa yang berwenang mengubah pengaturan, bagaimana kontrak di-upgrade setelah immutable, bagaimana membayar banyak pengguna dengan aman, dan bagaimana mencegah serangan ulang.
Di akhir episode, kita merakit sebuah kontrak yang menggabungkan semua pola dalam satu contoh production-ready.
Pola paling dasar: satu pemilik yang berhak memanggil fungsi administratif (pause, withdraw, update fee). OpenZeppelin menyediakan implementasi teruji:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/Ownable.sol";
contract OwnableExample is Ownable {
bool public paused;
constructor() Ownable(msg.sender) {}
modifier whenNotPaused() {
require(!paused, "paused");
_;
}
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
}
function doWork() external whenNotPaused {
// logika bisnis
}
}Modifier onlyOwner melindungi fungsi admin; whenNotPaused melindungi fungsi publik. Kombinasi keduanya membentuk pola pause & emergency stop yang menjadi standar keamanan.
Kontrak tidak bisa diubah — tapi proxy memungkinkan logika diganti. Arsitekturnya: pengguna berinteraksi dengan kontrak proxy yang menyimpan state, sementara logika berada di kontrak implementasi. Proxy meneruskan call via delegatecall, sehingga kode implementasi berjalan dengan storage proxy.
Keunggulan delegatecall: state (storage) tetap di proxy, hanya kode yang diganti. Pemicu upgrade dipegang oleh owner atau governance. Pola ini dipakai hampir semua protokol besar — kita bedah lebih dalam di episode 18 dan 21.
Note
Proxy bukanlah keharusan untuk semua proyek. Ia menambah kompleksitas (storage collision, function selector clash). Aturan praktis: pakai proxy jika kontrak memang diprediksi butuh evolusi logika; untuk kontrak sederhana dan final, deploy langsung tanpa proxy.
Pola ini menjawab masalah besar: kontrak yang mengirim ETH ke banyak pengguna satu per satu (push) mahal, dan bisa gagal jika ada satu penerima yang menolak transaksi (contoh: kontrak dengan receive() yang melempar error). Solusinya — pull:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract PullPayment {
mapping(address => uint256) public pendingWithdrawals;
event Claimed(address indexed who, uint256 amount);
function _credit(address _to, uint256 _amount) internal {
pendingWithdrawals[_to] += _amount;
}
function withdraw() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "nothing to claim");
pendingWithdrawals[msg.sender] = 0;
(bool ok, ) = msg.sender.call{ value: amount }("");
require(ok, "transfer failed");
emit Claimed(msg.sender, amount);
}
}Penerima menarik dana sendiri kapan pun mereka mau. Keuntungan ganda: gas untuk transfer ditanggung penerima, dan kegagalan satu penerima tidak meracuni seluruh batch. Ini pola standar untuk reward distribution, airdrop, dan game rewards.
Serangan reentrancy adalah salah satu kerentanan paling terkenal di Ethereum (hack DAO 2016 kehilangan $60 juta). Logikanya: kontrak korban memanggil kontrak penyerang sebelum memperbarui state-nya; penyerang memanggil balik fungsi korban sebelum panggilan pertama selesai.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Protected {
mapping(address => uint256) public balances;
bool private locked;
modifier noReentrancy() {
require(!locked, "reentrant");
locked = true;
_;
locked = false;
}
function withdraw(uint256 _amount) external noReentrancy {
require(balances[msg.sender] >= _amount, "insufficient");
balances[msg.sender] -= _amount;
(bool ok, ) = msg.sender.call{ value: _amount }("");
require(ok, "transfer failed");
}
}Modifier noReentrancy memakai flag locked untuk mencegah panggilan ulang selama eksekusi berjalan. Di production, gunakan implementasi OpenZeppelin ReentrancyGuard. Kita bedah seluruh kelas kerentanan ini di episode 18.
Gabungkan semua pola dalam satu contoh: vault penyetoran yang bisa dipause, dana ditarik dengan pola pull, dan dilindungi reentrancy guard:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is Ownable, ReentrancyGuard {
mapping(address => uint256) public shares;
mapping(address => uint256) public pending;
bool public paused;
constructor() Ownable(msg.sender) {}
modifier whenNotPaused() {
require(!paused, "paused");
_;
}
function deposit() external payable whenNotPaused nonReentrant {
shares[msg.sender] += msg.value;
}
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
}
function initiateWithdrawal(uint256 _amount) external nonReentrant {
require(shares[msg.sender] >= _amount, "insufficient shares");
shares[msg.sender] -= _amount;
pending[msg.sender] += _amount;
}
function claim() external nonReentrant {
uint256 amount = pending[msg.sender];
require(amount > 0, "nothing to claim");
pending[msg.sender] = 0;
(bool ok, ) = msg.sender.call{ value: amount }("");
require(ok, "transfer failed");
}
}Pola two-step withdrawal (initiate → claim) menggabungkan pull-over-push dengan reentrancy guard — arsitektur yang dipakai banyak vault dan reward distributor production.
Tip
Sebelum deploy, selalu tanyakan: siapa yang bisa memanggil fungsi ini? Apakah state diubah sebelum external call? Apakah ada batas yang bisa menahan dana pengguna? Jawaban atas tiga pertanyaan ini adalah pola yang tepat untuk kontrak kalian.
Inti yang harus dibawa pulang:
Di episode 6 selanjutnya kita membahas tooling profesional: Hardhat dan Foundry — cara men-test, men-deploy, dan mengelola proyek kontrak dengan workflow yang benar, plus proyek lengkap yang bisa kalian pakai sebagai template. Mari lanjut ke episode 6!