Solidity Basics — Web3

Solidity Basics Solidity adalah bahasa pemrograman utama untuk menulis smart contract di Ethereum (dan EVM-compatible chains). Syntaxnya mirip JavaScript/C++…

Solidity adalah bahasa pemrograman untuk menulis smart contract di Ethereum dan chain lain yang memakai EVM. Bentuknya mirip JavaScript (kurung kurawal, titik koma, function), tapi tiap variabel punya tipe tetap, dan kodenya dikompilasi jadi bytecode plus ABI sebelum di-deploy. Bayangin SOP kasir Toko Kaos Raka yang ditulis begitu teliti sampai mesin bisa menjalankannya: "kalau yang minta bukan Raka, tolak", "kalau POIN-nya kurang, tolak dan sebutkan kurangnya berapa". Begitu dicetak dan ditempel (di-deploy), SOP itu gak bisa diubah lagi. Di lesson ini kita menulis SOP itu: contract PoinRaka.

🧪 Tebakan cepat: Dinda punya 95 POIN dan memanggil redeem untuk 100 POIN. Kalau baris pengecekan saldo dihapus, sehingga contract langsung mengerjakan balanceOf[msg.sender] -= amount, apa yang terjadi: saldo Dinda jadi -5, jadi angka raksasa, atau transaksinya dibatalkan? Tebak dulu, jawabannya ada di bagian "Angka: uint256, decimals, dan checked arithmetic".

Contract pertama: PoinRaka

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract PoinRaka {
    string public constant name = "Poin Raka";
    string public constant symbol = "POIN";
    uint8 public constant decimals = 18;

    address public owner;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event Earned(address indexed customer, uint256 amount);
    event Redeemed(address indexed customer, uint256 amount);

    error NotEnoughPoin(uint256 balance, uint256 needed);

    constructor() {
        owner = msg.sender;
    }

    function earn(address customer, uint256 rupiah) external {
        require(msg.sender == owner, "Only owner");
        uint256 amount = poinFor(rupiah);
        balanceOf[customer] += amount;
        totalSupply += amount;
        emit Earned(customer, amount);
    }

    function redeem(uint256 amount) external {
        uint256 balance = balanceOf[msg.sender];
        if (balance < amount) revert NotEnoughPoin(balance, amount);
        balanceOf[msg.sender] = balance - amount;
        totalSupply -= amount;
        emit Redeemed(msg.sender, amount);
    }

    function poinFor(uint256 rupiah) public pure returns (uint256) {
        return (rupiah / 1000) * 10 ** decimals;
    }
}

State variable dan mapping

State variable hidup di storage contract dan bertahan di antara transaksi, gak seperti variabel lokal (amount, balance) yang hilang begitu fungsi selesai. Menulisnya mahal: puluhan ribu gas per slot baru.

mapping(address => uint256) balanceOf adalah tabel dari alamat ke angka. Tiap alamat yang belum pernah diisi otomatis bernilai 0, jadi gak ada "key tidak ditemukan". Mapping gak punya panjang dan gak bisa di-loop. Kata public membuat compiler menambahkan fungsi baca otomatis, balanceOf(address).

Function: visibility, view dan pure

Visibility Siapa yang boleh memanggil
public dari luar (transaksi atau contract lain) dan dari dalam contract ini
external cuma dari luar
internal contract ini dan contract turunannya
private cuma contract ini

private bukan berarti rahasia. Semua isi storage bisa dibaca siapa pun lewat node; private cuma melarang contract lain memanggilnya.

Lalu ada penanda state mutability:

Memanggil view atau pure dari luar memakai eth_call: gratis, tanpa transaksi, tanpa gas.

Event dan error

emit Earned(customer, amount) menulis event ke log transaksi. Event gak bisa dibaca contract, tapi frontend dan indexer bisa. Setelah earn(Dinda, 95000), viem membaca log-nya jadi:

{ eventName: 'Earned', args: { customer: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', amount: 95000000000000000000n } }

indexed membuat customer bisa dipakai untuk menyaring: "tampilkan semua Earned milik Dinda".

Untuk menolak, ada dua cara. require(kondisi, "pesan") membatalkan transaksi dengan teks. Custom error (error NotEnoughPoin(...) lalu revert NotEnoughPoin(balance, amount)) lebih hemat gas dan membawa data. Ini pesan asli viem 2.57.4 untuk keduanya:

The contract function "earn" reverted with the following reason:
Only owner

The contract function "redeem" reverted.

Error: NotEnoughPoin(uint256 balance, uint256 needed)
                    (95000000000000000000, 100000000000000000000)

Angka: uint256, decimals, dan checked arithmetic

Solidity gak punya angka desimal. uint256 adalah bilangan bulat dari 0 sampai 2^256 - 1. Supaya POIN bisa dipecah seperti ETH, kita pakai decimals = 18: 1 POIN disimpan sebagai 10^18 satuan terkecil. Belanja Kaos Sablon Kopi Rp 95.000 menghasilkan poinFor(95000) = 95000000000000000000. Pembagiannya juga bulat: poinFor(999) = 0, karena 999 / 1000 dibulatkan ke bawah jadi 0.

Sejak Solidity 0.8.0, setiap +, - dan * dicek. Kalau hasilnya keluar dari rentang tipenya, transaksi dibatalkan dengan Panic 0x11. Jadi kalau pengecekan saldo di redeem dihapus, 95 POIN dikurangi 100 POIN gak jadi -5 dan gak jadi angka raksasa: transaksinya revert. Itu jawaban tebakannya. Pesan aslinya:

The contract function "redeem" reverted with the following reason:
Arithmetic operation resulted in underflow or overflow.

dan node Hardhat menulis reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block). Hal yang sama terjadi pada uint8 (0 sampai 255): 255 + 1 revert. Di dalam blok unchecked { ... } pengecekan itu dimatikan, dan 255 + 1 jadi 0. Sebelum 0.8, inilah perilaku default-nya, dan banyak contract dibobol karenanya. Pengecekan NotEnoughPoin tetap kita tulis, karena pesannya jauh lebih jelas dari Panic.

Compile sungguhan dengan solc-js

solc di npm adalah compiler Solidity versi JavaScript. Di folder kosong:

npm install [email protected]
npx solcjs --abi --bin PoinRaka.sol -o build

Kalau berhasil, gak ada output sama sekali, dan muncul dua file: build/PoinRaka_sol_PoinRaka.bin (bytecode yang di-deploy) dan build/PoinRaka_sol_PoinRaka.abi. ABI (Application Binary Interface) adalah daftar fungsi, event dan error contract dalam JSON. Dalam bentuk yang lebih mudah dibaca (Interface.format() dari ethers):

constructor()
error NotEnoughPoin(uint256 balance, uint256 needed)
event Earned(address indexed customer, uint256 amount)
event Redeemed(address indexed customer, uint256 amount)
function balanceOf(address) view returns (uint256)
function decimals() view returns (uint8)
function earn(address customer, uint256 rupiah)
function name() view returns (string)
function owner() view returns (address)
function poinFor(uint256 rupiah) pure returns (uint256)
function redeem(uint256 amount)
function symbol() view returns (string)
function totalSupply() view returns (uint256)

Kalau ada yang salah, solcjs berhenti dengan error. Lupa titik koma setelah totalSupply += amount:

ParserError: Expected ';' but got 'emit'
  --> PoinRaka.sol:27:9:
   |
27 |         emit Earned(customer, amount);
   |         ^^^^

Menandai earn sebagai view padahal ia menulis storage:

TypeError: Function cannot be declared as view because this expression (potentially) modifies the state.
  --> PoinRaka.sol:25:9:
   |
25 |         balanceOf[customer] += amount;
   |         ^^^^^^^^^^^^^^^^^^^

⚠️ Jebakan yang sering ketemu

TL;DR: Solidity adalah bahasa untuk menulis smart contract di Ethereum: diawali SPDX dan pragma, isinya contract dengan state variable (permanen di storage), mapping (default 0, gak bisa di-loop), constructor (sekali saat deploy), function, event dan error. Visibility: public, external, internal, private (bukan rahasia). view membaca state tanpa mengubahnya, pure gak membaca maupun mengubah, dan keduanya gratis dipanggil dari luar. require menolak dengan teks, custom error menolak dengan data. Gak ada desimal: POIN pakai 18 decimals, dan pembagian selalu bulat. Sejak 0.8, overflow dan underflow revert dengan Panic 0x11. npx solcjs --abi --bin menghasilkan bytecode dan ABI; error-nya menyebut file, baris dan kolom.

Yang akan kamu pelajari