Langsung ke konten utama
BlockchainMar 28, 2026

Deep EVM #4: Primitif Keamanan — msg.sender, Kontrol Akses, dan Reentrancy

OS
Open Soft Team

Engineering Team

Keamanan Dimulai dari Opcode

Keamanan smart contract bukan hanya tentang pola kode tingkat tinggi — ia berakar pada perilaku opcode EVM. Memahami bagaimana CALLER, ORIGIN, dan pola panggilan bekerja di tingkat bytecode adalah fondasi untuk menulis kontrak yang aman.

msg.sender vs tx.origin

Dua opcode EVM menyediakan informasi tentang siapa yang memulai eksekusi:

  • CALLER (msg.sender) — Alamat yang langsung memanggil kontrak saat ini. Dalam rantai panggilan A → B → C, msg.sender di C adalah B.
  • ORIGIN (tx.origin) — Alamat yang menandatangani transaksi asli. Dalam rantai A → B → C, tx.origin di C tetap A.
// JANGAN PERNAH gunakan tx.origin untuk autentikasi!
function transfer(address to, uint256 amount) external {
    // BURUK: rentan terhadap phishing
    require(tx.origin == owner);
    
    // BAIK: hanya pemanggil langsung
    require(msg.sender == owner);
}

Serangan tx.origin: penyerang membuat kontrak berbahaya yang memanggil kontrak korban. Karena tx.origin adalah pengguna asli (yang berinteraksi dengan kontrak penyerang), pemeriksaan tx.origin berhasil dan penyerang mendapatkan akses.

Pola Kontrol Akses

Ownable Sederhana

address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

Di tingkat EVM, modifier ini mengkompilasi menjadi: CALLER → SLOAD(owner_slot) → EQ → JUMPI ke revert atau lanjutkan.

Role-Based Access Control (RBAC)

mapping(bytes32 => mapping(address => bool)) private _roles;

bytes32 public constant ADMIN_ROLE = keccak256("ADMIN");
bytes32 public constant MINTER_ROLE = keccak256("MINTER");

function hasRole(bytes32 role, address account) public view returns (bool) {
    return _roles[role][account];
}

RBAC menggunakan mapping bersarang — di EVM, ini berarti double keccak256 hash untuk menghitung slot storage: keccak256(account . keccak256(role . slot_dasar)).

Serangan Reentrancy

Reentrancy adalah kerentanan paling terkenal dalam smart contract. Ia terjadi ketika kontrak memanggil kontrak eksternal sebelum memperbarui state internalnya.

Contoh Klasik

contract Vault {
    mapping(address => uint256) public balances;
    
    function withdraw() external {
        uint256 bal = balances[msg.sender];
        // BAHAYA: panggilan eksternal sebelum update state
        (bool ok,) = msg.sender.call{value: bal}("");
        require(ok);
        balances[msg.sender] = 0;  // Terlambat!
    }
}

Penyerang bisa membuat kontrak dengan fungsi receive() yang memanggil withdraw() lagi. Karena balances[attacker] belum di-nolkan, pemeriksaan berhasil dan penyerang menarik dana berulang kali.

Di Tingkat EVM

Inilah yang terjadi secara sekuensial:

  1. SLOAD balances[attacker] → mengembalikan 1 ETH
  2. CALL dengan value=1 ETH ke attacker
  3. Kontrak attacker menerima 1 ETH, memanggil withdraw() lagi
  4. SLOAD balances[attacker] → MASIH 1 ETH (belum diupdate!)
  5. CALL lagi… dan seterusnya sampai vault kosong

Pola Checks-Effects-Interactions (CEI)

Pola pertahanan utama terhadap reentrancy:

function withdraw() external {
    // CHECKS: validasi
    uint256 bal = balances[msg.sender];
    require(bal > 0, "No balance");
    
    // EFFECTS: update state SEBELUM panggilan eksternal
    balances[msg.sender] = 0;
    
    // INTERACTIONS: panggilan eksternal terakhir
    (bool ok,) = msg.sender.call{value: bal}("");
    require(ok);
}

Sekarang meskipun attacker masuk kembali, balances[attacker] sudah 0 dan withdraw() akan revert.

Mutex / Reentrancy Guard

uint256 private _locked = 1;

modifier nonReentrant() {
    require(_locked == 1, "Reentrancy");
    _locked = 2;
    _;
    _locked = 1;
}

Biaya gas: 2 × SSTORE (warm) = ~5.800 gas overhead. Dengan EIP-1153 (transient storage), ini turun menjadi 2 × TSTORE = 200 gas.

// Dengan transient storage (EIP-1153):
modifier nonReentrant() {
    assembly {
        if tload(0) { revert(0, 0) }
        tstore(0, 1)
    }
    _;
    assembly {
        tstore(0, 0)
    }
}

Kerentanan Integer Overflow

Sebelum Solidity 0.8.0, operasi aritmatika bisa overflow tanpa peringatan:

// Solidity 0.7.x:
uint8 x = 255;
x += 1; // x = 0 (overflow!)

// Solidity 0.8.0+:
uint8 x = 255;
x += 1; // REVERT: overflow

Di tingkat EVM, Solidity 0.8+ menyisipkan pemeriksaan setelah setiap operasi aritmatika: DUP hasil → perbandingan → JUMPI ke revert. Ini menambah ~30 gas per operasi tetapi mencegah kerentanan kritis.

Flash Loan dan Manipulasi Harga

Flash loan memungkinkan peminjaman tanpa jaminan dalam satu transaksi. Ini bisa digunakan untuk memanipulasi harga oracle:

  1. Pinjam 1 juta token via flash loan
  2. Jual di DEX untuk memindahkan harga
  3. Gunakan harga yang dimanipulasi untuk likuidasi/arbitrase
  4. Kembalikan pinjaman + biaya

Pertahanan: gunakan Time-Weighted Average Price (TWAP) daripada harga spot, atau oracle off-chain seperti Chainlink.

DELEGATECALL dan Risiko Keamanan

DELEGATECALL mengeksekusi kode kontrak lain dalam konteks storage pemanggil. Jika kontrak yang dipanggil berbahaya, ia bisa memodifikasi semua storage pemanggil.

// Proxy pattern: storage di proxy, logika di implementation
contract Proxy {
    address public implementation;
    
    fallback() external payable {
        // DELEGATECALL ke implementation
        (bool ok,) = implementation.delegatecall(msg.data);
        require(ok);
    }
}

Risiko: jika implementation contract memiliki selfdestruct atau mengubah slot storage yang sama dengan proxy, hasilnya bisa katastrofis.

Praktik Terbaik Keamanan

  1. Gunakan CEI pattern — Selalu update state sebelum panggilan eksternal
  2. Tambahkan reentrancy guard — Untuk fungsi yang berinteraksi dengan kontrak lain
  3. Jangan gunakan tx.origin — Selalu gunakan msg.sender untuk autentikasi
  4. Gunakan Solidity 0.8+ — Untuk overflow protection bawaan
  5. Audit profesional — Dapatkan audit sebelum deployment mainnet
  6. Verifikasi formal — Untuk kontrak yang menangani dana besar
  7. Gunakan OpenZeppelin — Library yang telah diaudit untuk pola umum

Kesimpulan

Keamanan smart contract dimulai dari pemahaman mendalam tentang bagaimana EVM mengeksekusi kode. msg.sender, pola kontrol akses, pertahanan reentrancy, dan kesadaran akan vektor serangan — semuanya berakar pada perilaku opcode. Kontrak yang aman ditulis oleh pengembang yang berpikir di tingkat bytecode.