π Unit 1 β Solidity Dasar
Di akhir unit ini kamu akan:
- Bisa membaca dan menulis contract Solidity sederhana dari nol
- Paham tipe data, variabel state, dan function
- Paham visibility (
public,private,external,internal) dan kapan memakainya - Paham mapping, event, require, dan payable
- Sudah men-deploy dan menguji sendiri setiap konsep di browser, tanpa install apa pun
- β Phase 0 Unit 4 β wallet sudah siap
- π‘ Pernah ngoding bahasa apa pun akan membantu, tapi tidak wajib
Unit ini tidak meminta kamu membaca teori panjang lalu berharap paham. Formatnya:
satu konsep β satu contract kecil β deploy β klik tombolnya β lihat hasilnya
Setiap bagian punya blok π§ͺ Coba di Remix berisi langkah klik yang konkret beserta hasil yang seharusnya kamu lihat. Kalau hasilmu berbeda, kamu langsung tahu ada yang salah β bukan tiga jam kemudian.
Jangan cuma dibaca. Contract-contract di sini sengaja dibuat sangat kecil supaya kamu benar-benar mengetiknya dan menjalankannya. Lima belas menit mengklik di Remix mengajarkan lebih banyak daripada satu jam membaca.
π€ Apa itu Smart Contract?β
Smart contract adalah program yang berjalan di blockchain.
Bedanya dengan program biasa:
| Program biasa | Smart contract | |
|---|---|---|
| Berjalan di | Komputer/server tertentu | Semua node blockchain |
| Bisa diubah setelah rilis? | Ya, tinggal deploy ulang | β Tidak (secara default) |
| Menyimpan data di | Database | State blockchain |
| Biaya menjalankan | Biaya server | Gas, dibayar pemanggil |
| Siapa yang bisa memanggil | Sesuai autentikasi aplikasi | Siapa pun, kecuali kamu batasi |
1. Kode yang sudah di-deploy tidak bisa diubah. Tidak ada hotfix. Bug yang lolos ke mainnet akan tetap ada selamanya, dan bisa dieksploitasi siapa pun.
2. Siapa pun bisa memanggil function-mu. Bukan hanya frontend buatanmu β siapa saja, dengan input apa saja, dalam urutan apa saja.
Karena itu di Solidity kamu menulis dengan pola pikir yang berbeda: anggap setiap pemanggil adalah penyerang sampai kamu buktikan sebaliknya.
π οΈ Setup β 5 Menit, Tanpa Installβ
Step 1 β Buka Remixβ
Buka remix.ethereum.org. Tidak perlu daftar, tidak perlu install.
Kenali empat bagiannya:
| Bagian | Isinya |
|---|---|
| Kiri β File Explorer | Daftar file .sol milikmu |
| Tengah β Editor | Tempat menulis kode |
| Bawah β Terminal | Hasil, log, dan pesan error |
| Ikon kiri jauh | Tab Solidity Compiler dan Deploy & Run Transactions |
Step 2 β Pakai Remix VM dulu (bukan testnet)β
Buka tab Deploy & Run Transactions. Di dropdown Environment, pilih Remix VM (namanya bisa Remix VM (Cancun) atau nama hard fork lain β tidak masalah).
Remix VM adalah blockchain simulasi di dalam browser-mu. Artinya:
- β Gratis dan instan β tidak butuh gas sungguhan
- β Tidak butuh wallet, tidak butuh faucet
- β Kamu langsung dapat beberapa akun uji dengan saldo penuh
- β Salah pun tidak ada konsekuensi apa pun
Baru di bagian terakhir kita deploy satu contract ke Injective testnet sungguhan.
Alasannya sederhana: di Remix VM tidak ada faucet yang habis, tidak ada transaksi yang lambat, dan tidak ada popup wallet setiap kali kamu mengklik tombol. Kamu bisa fokus belajar Solidity-nya dulu.
Step 3 β Kenali warna tombol di Remixβ
Ini kecil tapi sangat membantu. Setelah contract di-deploy, tombol function-nya diberi warna berbeda:
| Warna tombol | Artinya | Gas |
|---|---|---|
| π΅ Biru | Hanya membaca (view / pure) | π Gratis |
| π Oranye | Mengubah state | π° Bayar gas |
| π΄ Merah | Mengubah state dan bisa menerima token (payable) | π° Bayar gas |
Aturannya tetap sama dan bisa kamu cek sendiri: function baca hasilnya muncul seketika, sedangkan function tulis menghasilkan transaksi di Terminal (dan popup wallet kalau kamu tidak sedang di Remix VM).
Step 4 β Nanti saja: sambungkan ke Injective EVM Testnetβ
Kamu belum butuh ini sekarang, tapi begini caranya nanti di bagian akhir unit:
- Pastikan MetaMask sudah punya jaringan Injective EVM Testnet (Chain ID
1439) β lihat Phase 0 Unit 4, atau tambahkan lewat Chainlist - Pastikan ada testnet INJ dari faucet
- Di Remix, ganti Environment menjadi Injected Provider - MetaMask
- Pastikan MetaMask sedang memilih jaringan Injective EVM Testnet
Saat Environment-nya Injected Provider, Remix men-deploy ke jaringan mana pun yang sedang aktif di MetaMask. Kalau MetaMask sedang di Ethereum Mainnet, kamu akan mencoba deploy dengan uang sungguhan.
Sebelum menekan Deploy, lihat dua tempat: dropdown jaringan di MetaMask, dan alamat akun di panel Remix.
π Solidity 101 β Tipe Data, Satu per Satuβ
Kita pakai konteks yang dekat denganmu: data peserta camp.
Setiap contract di bawah ini berdiri sendiri. Buat file baru untuk masing-masing, deploy, lalu klik tombolnya.
1. String β Teksβ
Apa itu: menyimpan teks, seperti "Budi Santoso" atau "Injective".
Kenapa penting: untuk nama, judul, deskripsi, dan semua data berbentuk tulisan.
Buat file BelajarString.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarString {
// Variabel state β tersimpan permanen di blockchain
string public namaPeserta;
// Constructor jalan SEKALI saja, saat contract di-deploy
constructor() {
namaPeserta = "Budi Santoso";
}
// Function untuk mengubah nama
function ubahNama(string memory _namaBaru) public {
namaPeserta = _namaBaru;
}
}
Penjelasan:
// SPDX-License-Identifier: MITβ penanda lisensi. Tanpa ini compiler memberi peringatan. Selalu tulis.pragma solidity ^0.8.20;β versi compiler.^berarti "0.8.20 ke atas, tapi di bawah 0.9.0".contract BelajarString { ... }β deklarasi contract, miripclassdi bahasa lain.string public namaPeserta;β variabel state. Katapublicotomatis membuatkan getter, itulah kenapa nanti ada tombolnya di Remix.constructor()β dijalankan sekali saat deploy, untuk mengisi nilai awal.string memory _namaBaruβ parameter.memoryberarti data sementara, tidak disimpan permanen.
Solidity berubah cukup sering, dan perubahannya bisa memengaruhi keamanan. Contoh nyata: sejak versi 0.8.0, penjumlahan yang melebihi batas tipe otomatis gagal. Sebelumnya angka akan "berputar" ke nilai terkecil tanpa peringatan β sumber banyak eksploitasi mahal di masa lalu.
Pakai 0.8.x ke atas. Selalu. Seluruh dokumentasi camp ini memakai ^0.8.20.
π§ͺ Coba di Remixβ
-
Tab Solidity Compiler β pilih versi
0.8.20atau lebih baru β klik Compile -
Tab Deploy & Run β Environment Remix VM β klik Deploy
-
Buka Deployed Contracts di bawah, klik tombol biru
namaPesertaβ Hasil:
Budi Santoso -
Ketik
"Ani Wijaya"di kolom sebelahubahNamaβ klikubahNama(tombol oranye) -
Klik
namaPesertalagiβ Hasil:
Ani Wijayaβ
namaPeserta (biru) langsung menampilkan hasil. ubahNama (oranye) menghasilkan transaksi β lihat Terminal di bawah, ada entri baru di sana.
Itulah beda membaca dan menulis di blockchain, dan kamu baru saja melihatnya sendiri.
2. uint256 β Angkaβ
Apa itu: bilangan bulat positif, dari 0 sampai angka yang sangat besar.
Kenapa penting: untuk ID, jumlah, skor, saldo, timestamp β hampir semua data numerik di Web3.
Buat BelajarAngka.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarAngka {
uint256 public nomorPeserta;
uint256 public poin;
constructor() {
nomorPeserta = 11001;
poin = 0;
}
function ubahNomor(uint256 _nomorBaru) public {
nomorPeserta = _nomorBaru;
}
function tambahPoin() public {
poin = poin + 10;
// Bisa juga ditulis singkat: poin += 10;
}
}
Penjelasan:
uint256β unsigned integer 256-bit. "Unsigned" artinya tidak bisa negatif.uintsaja sama denganuint256.poin += 10β cara singkat daripoin = poin + 10.
| Tipe | Isinya | Catatan |
|---|---|---|
uint256 | 0 sampai angka sangat besar | Paling umum dipakai |
int256 | Bilangan bulat, bisa negatif | Lebih jarang |
bool | true / false | |
address | Alamat 20-byte | Punya .balance |
string | Teks | Lebih mahal dari bytes32 |
bytes32 | 32 byte tetap | Lebih murah untuk data ukuran tetap |
Solidity tidak punya tipe float atau double. Ini mengejutkan hampir semua pendatang baru.
Cara mengatasinya: simpan nilai dalam satuan terkecil. INJ punya 18 desimal, jadi 1 INJ direpresentasikan sebagai 1000000000000000000 (1 diikuti 18 nol).
uint256 satuINJ = 1 ether; // = 1000000000000000000
Kata kunci ether di sini hanyalah pengali 10^18 β tidak ada hubungannya dengan Ethereum. Di Injective ia bekerja sama saja karena INJ juga 18 desimal.
Konsekuensi praktis: pembagian membuang sisa. 7 / 2 menghasilkan 3, bukan 3.5. Kalau menghitung persentase, kalikan dulu baru bagi.
π§ͺ Coba di Remixβ
-
Compile β Deploy
-
Klik
poinβ Hasil:0 -
Klik
tambahPoinempat kali -
Klik
poinlagi β Hasil:40β -
Klik
nomorPesertaβ Hasil:11001 -
Ketik
-5diubahNomorlalu klikβ Hasil: gagal. Remix menolaknya karena
uint256tidak menerima angka negatif. Ini contoh compiler/encoder menyelamatkanmu dari bug.
3. bool β Benar atau Salahβ
Apa itu: hanya dua nilai, true atau false.
Kenapa penting: untuk status aktif/nonaktif, lulus/belum, sudah dibayar/belum β semua kondisi ya-tidak.
Buat BelajarBoolean.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarBoolean {
bool public sedangAktif;
bool public sudahLulus;
constructor() {
sedangAktif = true;
sudahLulus = false;
}
function ubahStatus(bool _status) public {
sedangAktif = _status;
}
function luluskan() public {
sudahLulus = true;
}
}
Penjelasan:
boolhanya menerimatrueataufalseβ tidak ada1,0, atau"ya".- Nilai default sebuah
boolyang belum diisi adalahfalse.
π§ͺ Coba di Remixβ
- Compile β Deploy
- Klik
sudahLulusβ Hasil:false - Klik
luluskan - Klik
sudahLuluslagi β Hasil:trueβ - Ketik
falsediubahStatusβ klik β ceksedangAktifβ Hasil:false
4. address β Alamat Walletβ
Apa itu: menyimpan alamat wallet atau contract, bentuknya 0x diikuti 40 karakter heksadesimal.
Kenapa penting: ini dasar dari kepemilikan dan kontrol akses β siapa boleh melakukan apa.
Buat BelajarAddress.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarAddress {
address public admin;
address public peserta;
constructor() {
admin = msg.sender; // msg.sender = alamat yang men-deploy contract ini
}
function setPeserta(address _peserta) public {
peserta = _peserta;
}
function siapaYangMemanggil() public view returns (address) {
return msg.sender;
}
}
Penjelasan:
msg.senderβ alamat yang sedang memanggil function ini. Salah satu variabel terpenting di Solidity.- Di dalam
constructor,msg.senderberarti alamat yang men-deploy contract. - Di dalam function biasa,
msg.senderberarti siapa pun yang mengklik tombol itu.
Variabel global lain yang sering dipakai:
| Variabel global | Isinya |
|---|---|
msg.sender | Alamat pemanggil |
msg.value | Jumlah token yang dikirim bersama panggilan |
block.timestamp | Waktu blok saat ini (detik, Unix timestamp) |
block.number | Nomor blok saat ini |
block.chainid | Chain ID jaringan (Injective EVM Testnet = 1439) |
π§ͺ Coba di Remixβ
-
Compile β Deploy
-
Klik
adminβ Hasil: alamatmu sendiri β cocokkan dengan dropdown ACCOUNT di panel Remix. Sama, kan?
-
Sekarang bagian menariknya. Buka dropdown ACCOUNT di Remix, ganti ke akun kedua
-
Klik
siapaYangMemanggilβ Hasil: alamat akun kedua, bukan
admin -
Klik
adminsekali lagiβ Hasil: tetap alamat akun pertama β
admin tidak berubah karena disimpan sekali saat deploy. msg.sender berubah mengikuti siapa yang memanggil.
Membandingkan keduanya β msg.sender == admin β adalah pola di balik hampir semua izin di smart contract. Kita pakai betulan di bagian require nanti.
Remix VM memberimu beberapa akun uji persis supaya kamu bisa mencoba hal seperti ini. Manfaatkan.
π§ Function, view, dan pureβ
Apa itu: blok kode yang bisa dipanggil, dengan atau tanpa mengubah data.
Kenapa penting: menentukan apakah pemakai aplikasimu membayar gas atau tidak.
Buat BelajarFunction.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarFunction {
uint256 public hitungan;
// Mengubah state β butuh gas
function tambah() public {
hitungan = hitungan + 1;
}
function tambahSebanyak(uint256 _jumlah) public {
hitungan = hitungan + _jumlah;
}
// Hanya MEMBACA state β view
function lihatHitungan() public view returns (uint256) {
return hitungan;
}
// Tidak baca, tidak tulis state β pure
function jumlahkan(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
}
Anatomi sebuah functionβ
function tambahSebanyak (uint256 _jumlah) public view returns (uint256)
β β β β β β
kata kunci nama parameter visibility sifat nilai balik
Beda view dan pureβ
| Kata kunci | Artinya | Biaya gas kalau dipanggil dari luar |
|---|---|---|
| (tidak ada) | Bisa mengubah state | π° Bayar gas |
view | Hanya membaca state | π Gratis |
pure | Tidak baca, tidak tulis state | π Gratis |
Function view dan pure gratis dipanggil dari luar chain (misalnya dari frontend) karena tidak mengubah apa pun, jadi tidak perlu masuk ke dalam blok.
Biasakan menandai function dengan benar. Selain menghemat gas pengguna aplikasimu, compiler juga akan menangkap kesalahanmu β kalau kamu menandai sesuatu view lalu tidak sengaja mengubah state, kodenya tidak akan ter-compile.
π§ͺ Coba di Remixβ
- Compile β Deploy
- Perhatikan warna tombolnya:
tambahdantambahSebanyakoranye,lihatHitungandanjumlahkanbiru - Klik
tambahβ lihat Terminal: muncul entri transaksi baru - Klik
lihatHitunganβ hasil1muncul seketika, dan tidak ada transaksi baru di Terminal β - Isi
a = 7,b = 5dijumlahkanβ klik β Hasil:12 - Klik
hitunganβ Hasil: tetap1βjumlahkanmemang tidak menyentuh state apa pun
Coba ubah lihatHitungan menjadi seperti ini, lalu compile:
function lihatHitungan() public view returns (uint256) {
hitungan = hitungan + 1; // menulis state di dalam view
return hitungan;
}
Compiler akan menolaknya. Bacalah pesan errornya sampai habis β itu contoh bagus bahwa view bukan sekadar hiasan, tapi janji yang ditegakkan compiler.
Kembalikan lagi setelah mencoba.
ποΈ Visibility β Siapa yang Boleh Memanggilβ
Apa itu: aturan tentang dari mana sebuah function atau variabel bisa diakses.
Kenapa penting: membatasi permukaan serang contract-mu. Semakin sedikit yang terbuka, semakin sedikit yang bisa disalahgunakan.
| Visibility | Bisa dipanggil dari |
|---|---|
public | Mana saja β luar chain, contract lain, dalam contract |
external | Hanya dari luar contract |
internal | Dalam contract ini dan turunannya |
private | Hanya dalam contract ini |
Buat BelajarVisibility.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarVisibility {
uint256 public terlihat = 1; // otomatis punya getter
uint256 private rahasia = 2; // TIDAK ada getter otomatis
function siapaPunBisa() public pure returns (string memory) {
return "dipanggil dari mana saja";
}
function hanyaDariLuar() external pure returns (string memory) {
return "hanya dari luar contract";
}
// Karena `rahasia` private, kita butuh function sendiri untuk membacanya
function intipRahasia() public view returns (uint256) {
return rahasia;
}
}
π§ͺ Coba di Remixβ
- Compile β Deploy
- Lihat daftar tombolnya: ada
terlihat, tidak adarahasiaβ - Klik
terlihatβ Hasil:1 - Klik
intipRahasiaβ Hasil:2β datanya ada, hanya tidak punya getter otomatis
private TIDAK berarti rahasiaIni kesalahpahaman paling berbahaya bagi pemula Solidity, dan latihan di atas bisa menyesatkan kalau berhenti di situ.
private hanya membatasi akses dari kode lain. Tapi semua data di blockchain bisa dibaca siapa saja β cukup baca langsung slot penyimpanan contract-nya lewat RPC. Tidak butuh alat khusus, tidak butuh izin, dan tidak meninggalkan jejak.
Nilai 2 di atas terlihat jelas oleh siapa pun yang mau melihat, dengan atau tanpa intipRahasia.
Jangan pernah menyimpan apa pun yang benar-benar rahasia di dalam smart contract. Bukan password, bukan kunci API, bukan data pribadi. Tidak ada satu pun cara membuatnya rahasia.
πΊοΈ Mapping β Menyimpan Data Banyak Orangβ
Apa itu: struktur kunci-nilai, mirip dictionary atau HashMap.
Kenapa penting: ini cara standar menyimpan data per alamat β saldo, kepemilikan, izin. Hampir semua contract nyata memakainya.
Buat BelajarMapping.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarMapping {
// Setiap alamat punya poinnya sendiri
mapping(address => uint256) public poin;
// Setiap nomor peserta punya namanya sendiri
mapping(uint256 => string) public namaPeserta;
function tambahPoinSaya(uint256 _jumlah) public {
poin[msg.sender] = poin[msg.sender] + _jumlah;
}
function daftarkan(uint256 _nomor, string memory _nama) public {
namaPeserta[_nomor] = _nama;
}
function lihatPoin(address _siapa) public view returns (uint256) {
return poin[_siapa];
}
}
Penjelasan:
mapping(address => uint256)β dari alamat, ke angka.poin[msg.sender]β poin milik si pemanggil, bukan milik orang lain. Pola ini muncul di mana-mana.publicpada mapping otomatis membuat getter yang menerima kunci sebagai input.
- Semua kunci "sudah ada" dan bernilai nol. Tidak ada konsep "key tidak ditemukan" β
poin[alamatAsing]mengembalikan0, bukan error. Kalau kamu perlu membedakan "nol" dari "belum pernah ada", simpan penandaboolterpisah. - Mapping tidak bisa di-loop. Tidak ada cara mengambil daftar semua kunci. Kalau butuh itu, simpan array kunci secara terpisah β kita bahas array di Unit 2.
π§ͺ Coba di Remixβ
-
Compile β Deploy
-
Ketik
50ditambahPoinSayaβ klik -
Salin alamat aktifmu dari dropdown ACCOUNT, tempel ke
lihatPoinβ klikβ Hasil:
50 -
Ganti ke akun kedua di dropdown ACCOUNT
-
Klik
lihatPoindengan alamat akun keduaβ Hasil:
0β poin akun pertama tidak ikut terbawa β -
Dari akun kedua, ketik
7ditambahPoinSayaβ klik β cek lagiβ Hasil:
7, sementara akun pertama tetap50 -
Sekarang tempel alamat acak (misalnya
0x0000000000000000000000000000000000000001) kelihatPoinβ Hasil:
0, bukan error β persis seperti peringatan di atas -
Untuk mapping kedua: ketik
11001dan"Budi"didaftarkanβ klik β ketik11001dinamaPesertaβ Hasil:Budi
π‘οΈ require β Penjaga Contract-muβ
Apa itu: pemeriksaan kondisi. Kalau kondisinya false, seluruh transaksi dibatalkan.
Kenapa penting: ini alat validasi utama di Solidity. Tanpa require, siapa pun bisa melakukan apa pun terhadap contract-mu.
Buat BelajarRequire.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarRequire {
address public admin;
mapping(address => uint256) public poin;
constructor() {
admin = msg.sender;
}
// Hanya admin yang boleh memberi poin
function beriPoin(address _kepada, uint256 _jumlah) public {
require(msg.sender == admin, "Hanya admin yang boleh");
require(_jumlah > 0, "Jumlah harus lebih dari nol");
poin[_kepada] = poin[_kepada] + _jumlah;
}
}
Penjelasan:
require(kondisi, "pesan")β kalaukondisibenar, kode lanjut. Kalau salah, transaksi dibatalkan seluruhnya dan pesan dikembalikan.- "Dibatalkan seluruhnya" itu penting: perubahan state yang sudah terjadi sebelum
requiregagal pun ikut dibatalkan. Tidak ada kondisi setengah jadi. msg.sender == adminβ pola kontrol akses yang kita bangun di bagianaddresstadi, sekarang dipakai betulan.
π§ͺ Coba di Remixβ
-
Compile β Deploy dari akun pertama (akun ini jadi
admin) -
Salin alamat akun pertama, tempel ke
_kepada, isi_jumlah=10β klikberiPoinβ Hasil: berhasil. Cek
poindengan alamat itu β10 -
Coba
_jumlah=0β klikberiPoinβ Hasil: GAGAL dengan pesan
"Jumlah harus lebih dari nol"β -
Sekarang ganti ke akun kedua di dropdown ACCOUNT
-
Coba
beriPoinlagi dengan jumlah berapa punβ Hasil: GAGAL dengan pesan
"Hanya admin yang boleh"β -
Buka Terminal di bawah dan baca detail transaksi yang gagal β di sana tertulis alasan
revert-nya
Kebanyakan orang hanya menguji jalur yang berhasil. Padahal di Web3, jalur yang gagal justru yang melindungi uang orang.
Biasakan bertanya untuk setiap function yang kamu tulis: "siapa yang seharusnya TIDAK boleh memanggil ini, dan apa yang terjadi kalau mereka mencobanya?"
π’ Event β Cara Contract Berbicara ke Dunia Luarβ
Apa itu: log yang dipancarkan contract dan bisa didengarkan aplikasi di luar chain.
Kenapa penting: jauh lebih murah daripada menyimpan data di state, dan ini cara frontend tahu bahwa sesuatu telah terjadi.
Buat BelajarEvent.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarEvent {
mapping(address => uint256) public poin;
// Deklarasi event
event PoinDitambah(address indexed kepada, uint256 jumlah, uint256 totalSekarang);
function tambahPoin(uint256 _jumlah) public {
require(_jumlah > 0, "Jumlah harus lebih dari nol");
poin[msg.sender] = poin[msg.sender] + _jumlah;
// Pancarkan event
emit PoinDitambah(msg.sender, _jumlah, poin[msg.sender]);
}
}
Penjelasan:
event PoinDitambah(...)β mendeklarasikan bentuk log-nya.emit PoinDitambah(...)β benar-benar memancarkannya.indexedβ membuat parameter itu bisa difilter, misalnya "tampilkan semua penambahan poin untuk alamat ini". Maksimal 3 parameterindexedper event.
Kenapa event penting:
- Jauh lebih murah daripada menyimpan data di state
- Frontend bisa mendengarkan dan memperbarui tampilan secara realtime
- Menjadi riwayat aktivitas contract yang bisa dibaca explorer seperti Blockscout
Event ditujukan untuk dunia luar. Smart contract lain tidak bisa membaca event. Kalau datanya perlu dibaca contract lain, simpan di state.
π§ͺ Coba di Remixβ
-
Compile β Deploy
-
Ketik
25ditambahPoinβ klik -
Di Terminal, klik baris transaksi tadi untuk membukanya
-
Cari bagian
logsdan bukaβ Hasil: ada entri
PoinDitambahberisi alamatmu,25, dan total25β -
Klik
tambahPoinlagi dengan25, lalu lihat log-nyaβ Hasil:
jumlah=25tapitotalSekarang=50
Di Phase 3 kamu akan menulis aplikasi yang mendengarkan event seperti ini untuk memperbarui tampilan tanpa perlu menanyai chain terus-menerus.
Membuka logs di Remix sekarang membuat bagian itu jauh lebih masuk akal nanti.
π° payable β Menerima INJβ
Apa itu: kata kunci yang membuat function boleh menerima token.
Kenapa penting: tanpa payable, setiap upaya mengirim INJ ke function-mu akan gagal.
Buat BelajarPayable.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BelajarPayable {
mapping(address => uint256) public setoran;
// payable = function ini boleh menerima INJ
function setor() public payable {
require(msg.value > 0, "Kirim INJ lebih dari nol");
setoran[msg.sender] = setoran[msg.sender] + msg.value;
}
// Saldo yang dipegang contract ini
function saldoContract() public view returns (uint256) {
return address(this).balance;
}
}
Penjelasan:
payableβ tanpa kata ini, mengirim token ke function akan ditolak.msg.valueβ jumlah token yang ikut terkirim, dalam satuan terkecil (wei-nya INJ).address(this).balanceβ saldo yang dipegang contract ini sendiri.
Di kolom VALUE, satuan yang ditawarkan Remix bernama Wei / Gwei / Finney / Ether. Nama-nama itu warisan dari Ethereum dan tidak berubah meski kamu berada di jaringan lain.
Di Injective, yang benar-benar berpindah adalah INJ. Karena INJ juga punya 18 desimal, 1 Ether di kolom Remix berarti 1 INJ. Anggap saja "Ether" di Remix sebagai label satuan 10^18.
π§ͺ Coba di Remixβ
-
Compile β Deploy
-
Cari kolom VALUE di atas tombol Deploy. Isi
1dan pilih satuan Ether -
Klik
setor(tombolnya merah, karena payable) -
Klik
saldoContractβ Hasil:
1000000000000000000β yaitu 1 INJ dalam satuan terkecil β -
Klik
setorandengan alamatmu β Hasil: angka yang sama -
Sekarang kembalikan VALUE ke
0, lalu kliksetorlagiβ Hasil: GAGAL dengan pesan
"Kirim INJ lebih dari nol"β
Kalau kamu lupa mengisi kolom VALUE, msg.value bernilai 0 β walaupun function-nya sudah payable.
Menandai function payable hanya berarti "boleh menerima". Yang benar-benar mengirim token adalah kolom VALUE, terpisah dari argumen function.
π§© Tantangan β Tulis Sendiri DataPeserta.solβ
Sekarang giliranmu. Jangan copy-paste β ini bagian yang benar-benar memindahkan pemahaman dari "mengerti saat dibaca" ke "bisa menulis sendiri".
Spesifikasiβ
Variabel state yang dibutuhkan:
| Nama | Tipe | Isi |
|---|---|---|
nama | string public | Nama peserta |
nomorPeserta | uint256 public | Nomor peserta |
aktif | bool public | Status keaktifan |
wallet | address public | Alamat yang men-deploy |
waktuDaftar | uint256 public | Waktu pendaftaran |
poin | uint256 public | Poin, mulai dari 0 |
Constructor harus mengisi: nama = "Budi Santoso", nomorPeserta = 11001, aktif = true, wallet = alamat pen-deploy, waktuDaftar = waktu sekarang.
Function yang dibutuhkan:
| Function | Sifat | Yang dilakukan |
|---|---|---|
tambahPoin(uint256 _jumlah) | public | Tambah poin, tolak kalau _jumlah nol |
lamaTerdaftar() | public view returns (uint256) | Berapa detik sejak mendaftar |
nonaktifkan() | public | Set aktif jadi false, hanya boleh dipanggil wallet |
Kerangka awalβ
Buat file DataPeserta.sol dan mulai dari sini:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DataPeserta {
// TODO 1: Deklarasikan 6 variabel state sesuai tabel di atas
// Petunjuk: string public nama;
// Petunjuk: uint256 public nomorPeserta;
// ... tulis 4 sisanya
// TODO 2: Buat constructor yang mengisi nilai awal
constructor() {
// Petunjuk: nama = "Budi Santoso";
// Petunjuk: wallet = msg.sender;
// Petunjuk: waktuDaftar = block.timestamp;
}
// TODO 3: Buat tambahPoin(uint256 _jumlah)
// Petunjuk: pakai require(_jumlah > 0, "...") lebih dulu
// TODO 4: Buat lamaTerdaftar()
// Petunjuk: function lamaTerdaftar() public view returns (uint256) { ... }
// Petunjuk: return block.timestamp - waktuDaftar;
// TODO 5: Buat nonaktifkan()
// Petunjuk: require(msg.sender == wallet, "...") lalu ubah `aktif`
}
β Checklist verifikasiβ
- Ada 6 variabel state, semuanya
public - Constructor mengisi kelima nilai awal
-
tambahPoinmenolak jumlah nol -
lamaTerdaftarbersifatviewdan mengembalikanuint256 -
nonaktifkanhanya bisa dipanggil oleh alamat pen-deploy - Contract ter-compile tanpa error
π§ͺ Cara menguji hasilmuβ
-
Deploy, lalu klik satu per satu:
nama,nomorPeserta,aktif,wallet,waktuDaftarβ nilainya harus sesuai constructor, dan
wallet= alamat akun aktifmu -
Klik
lamaTerdaftarβ Hasil: angka kecil (baru saja deploy) -
Tunggu sekitar satu menit, klik lagi β Hasil: bertambah sekitar 60 β
-
Ketik
10ditambahPoinβ klik β cekpoinβ Hasil:10 -
Ketik
0ditambahPoinβ Hasil: GAGAL β -
Ganti ke akun kedua β klik
nonaktifkanβ Hasil: GAGAL β -
Kembali ke akun pertama β klik
nonaktifkanβ cekaktifβ Hasil:falseβ
lamaTerdaftar bisa bertambah di Remix VMblock.timestamp mengikuti waktu blok. Di Remix VM, blok baru dibuat setiap kali ada transaksi, dan waktunya mengikuti jam komputermu β jadi angkanya memang bertambah seiring waktu nyata.
π οΈ Error yang paling sering munculβ
| Pesan error | Artinya |
|---|---|
ParserError: Expected ';' but got '}' | Lupa titik koma di akhir baris |
DeclarationError: Undeclared identifier | Variabel belum dideklarasikan, atau salah ketik namanya |
TypeError: Member "sender" not found | Salah tulis msg.sender β semuanya huruf kecil |
TypeError: Different number of arguments | Jumlah parameter tidak cocok dengan deklarasinya |
Function declared as view but this expression modifies the state | Ada penulisan state di dalam function view |
- Buka lagi contract-contract kecil di atas β semua pola yang kamu butuhkan sudah pernah muncul di sana
- Kerjakan satu TODO dalam satu waktu, compile setiap selesai satu bagian. Jangan tulis semuanya lalu compile sekali di akhir
- Baca pesan error sampai habis, bukan hanya baris pertama β Solidity biasanya menyebutkan nomor barisnya
- Masih macet? Tanyakan di grup Telegram, sertakan kode dan pesan errornya
π§ͺ Menyatukan Semuanya β Celengan Bersamaβ
Sekarang gabungkan semua konsep tadi jadi satu contract utuh yang benar-benar memegang INJ.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title Celengan β contoh contract CLC11 Injective
/// @notice Setiap orang bisa menabung dan menarik tabungannya sendiri
contract Celengan {
// ---- State ----
mapping(address => uint256) private tabungan;
uint256 public totalTersimpan;
address public pemilik;
// ---- Event ----
event Menabung(address indexed penabung, uint256 jumlah);
event Menarik(address indexed penabung, uint256 jumlah);
// ---- Constructor: jalan sekali saat deploy ----
constructor() {
pemilik = msg.sender;
}
// ---- Menabung: payable = bisa menerima INJ ----
function menabung() public payable {
require(msg.value > 0, "Jumlah harus lebih dari nol");
tabungan[msg.sender] = tabungan[msg.sender] + msg.value;
totalTersimpan = totalTersimpan + msg.value;
emit Menabung(msg.sender, msg.value);
}
// ---- Menarik tabungan sendiri ----
function menarik(uint256 jumlah) public {
require(tabungan[msg.sender] >= jumlah, "Tabungan tidak cukup");
// Kurangi state DULU, baru kirim (lihat catatan di bawah)
tabungan[msg.sender] = tabungan[msg.sender] - jumlah;
totalTersimpan = totalTersimpan - jumlah;
emit Menarik(msg.sender, jumlah);
(bool sukses, ) = msg.sender.call{value: jumlah}("");
require(sukses, "Pengiriman gagal");
}
// ---- Lihat tabungan sendiri ----
function tabunganSaya() public view returns (uint256) {
return tabungan[msg.sender];
}
}
Yang baru di contoh iniβ
Semua yang lain sudah kamu pakai. Yang benar-benar baru hanya satu baris:
(bool sukses, ) = msg.sender.call{value: jumlah}("");
require(sukses, "Pengiriman gagal");
Ini cara mengirim INJ dari contract ke sebuah alamat. Nilai baliknya adalah penanda berhasil/gagal, dan kamu wajib memeriksanya dengan require β kalau tidak, kegagalan pengiriman akan lolos tanpa ketahuan.
menarikKita mengurangi saldo sebelum mengirim token. Ini disengaja, dan namanya pola checks-effects-interactions:
- Checks β validasi dengan
require - Effects β ubah state
- Interactions β baru kirim token atau panggil contract lain
Kalau urutannya dibalik (kirim dulu, kurangi belakangan), contract-mu rentan terhadap reentrancy attack: contract penerima bisa memanggil menarik lagi sebelum saldonya sempat dikurangi, dan menguras seluruh isi contract.
Serangan ini pernah menyebabkan salah satu kerugian terbesar dalam sejarah Ethereum. Selalu ikuti urutan ini. Kita bahas lebih dalam di Unit 2.
π§ͺ Coba di Remix β skenario lengkapβ
Kali ini kita uji seperti pengguna sungguhan, dengan dua akun.
-
Compile β Deploy dari akun pertama
-
Isi VALUE =
2Ether β klikmenabung -
Klik
tabunganSayaβ Hasil:2000000000000000000(2 INJ) -
Klik
totalTersimpanβ Hasil: sama,2000000000000000000 -
Ganti ke akun kedua. Isi VALUE =
1Ether β klikmenabung -
Klik
tabunganSaya(masih sebagai akun kedua) β Hasil:1000000000000000000 -
Klik
totalTersimpanβ Hasil:3000000000000000000β total gabungan β -
Masih sebagai akun kedua, coba tarik lebih dari tabunganmu: ketik
2000000000000000000dimenarikβ Hasil: GAGAL dengan
"Tabungan tidak cukup"β β Akun kedua tidak bisa menyentuh tabungan akun pertama. Itulah gunanyamappingdenganmsg.sender -
Tarik jumlah yang wajar: ketik
500000000000000000(0,5 INJ) β klikmenarik -
Cek
tabunganSayaβ Hasil:500000000000000000, dan saldo akun kedua di panel Remix bertambah -
Buka transaksi terakhir di Terminal β
logsβ Hasil: eventMenarikβ
Setelah semuanya berhasil, coba hal-hal ini β masing-masing mengajarkan sesuatu:
- Hapus
payabledarimenabung, compile, dan lihat apa yang terjadi - Ubah
require(msg.value > 0, ...)menjadirequire(msg.value > 1 ether, ...), lalu coba setor 0,5 INJ - Hapus
require(sukses, ...)β masih ter-compile, dan itulah masalahnya. Renungkan kenapa itu berbahaya
Remix VM adalah tempat paling aman untuk salah. Manfaatkan sekarang, sebelum kodenya memegang uang sungguhan.
π Naik Kelas β Deploy ke Injective EVM Testnetβ
Semua di atas berjalan di simulasi browser. Sekarang jalankan satu contract di jaringan Injective sungguhan, supaya kamu merasakan bedanya.
- Jaringan Injective EVM Testnet (Chain ID
1439) sudah ada di MetaMask - Ada testnet INJ dari faucet
Belum siap? Kembali ke Phase 0 Unit 4 dulu.
-
Buka MetaMask, pilih jaringan Injective EVM Testnet
-
Di Remix, tab Deploy & Run β Environment β Injected Provider - MetaMask
-
Setujui permintaan koneksi dari MetaMask
-
Periksa dulu: alamat di panel Remix harus sama dengan alamat MetaMask-mu, dan saldonya harus menunjukkan testnet INJ-mu
-
Pilih contract
Celenganβ klik Deploy β konfirmasi di MetaMask -
Tunggu beberapa detik sampai transaksinya masuk blok
-
Salin alamat contract dari Remix, lalu cari di Blockscout testnet
β Hasil: contract-mu ada di explorer publik, dengan riwayat transaksinya β
-
Coba
menabungdengan VALUE kecil (misalnya0.01Ether = 0,01 INJ) β konfirmasi di MetaMask -
Buka lagi di Blockscout β Hasil: transaksinya muncul, beserta event
Menabung
Apa yang berubah dibanding Remix VM?β
| Remix VM | Injective EVM Testnet | |
|---|---|---|
| Kecepatan | Instan | Beberapa detik per transaksi |
| Biaya | Gratis | Gas dibayar pakai testnet INJ |
| Konfirmasi | Tidak ada | Popup MetaMask tiap transaksi |
| Siapa yang bisa melihat | Hanya kamu | Siapa pun, lewat explorer |
| Kalau salah | Hilang saat refresh | Permanen di chain |
Catat alamat contract yang barusan kamu deploy. Kamu akan butuh alamat seperti ini di Phase 3 saat menghubungkan frontend, dan di guided project sebagai bukti pekerjaanmu.
π― Rangkumanβ
- Smart contract tidak bisa diubah setelah deploy, dan siapa pun bisa memanggilnya
- Selalu pakai
pragma solidity ^0.8.xβ versi 0.8+ punya proteksi overflow bawaan - Tidak ada desimal di Solidity; pakai satuan terkecil (18 desimal untuk INJ)
viewdanpuregratis dipanggil dari luar β di Remix tombolnya biru, dan tidak menghasilkan transaksiprivatetidak berarti rahasia β semua data on-chain bisa dibaca siapa sajamappingtidak bisa di-loop, dan semua kuncinya bernilai nol secara defaultmsg.senderadalah dasar dari hampir semua kontrol akses;requireadalah alat penegaknyapayablehanya berarti "boleh menerima" β yang mengirim token adalah kolom VALUE- Checks β Effects β Interactions. Ubah state sebelum mengirim token
- Uji jalur yang gagal, bukan hanya jalur yang berhasil
β Quick Checkβ
- Apa beda
viewdanpure? - Kenapa
1 ethersama dengan1000000000000000000? - Kamu menyimpan data dengan
private. Apakah orang lain bisa membacanya? Jelaskan. - Function-mu sudah
payabletapimsg.valueselalu0. Apa yang paling mungkin kamu lupa? - Apa yang terjadi kalau kamu mengirim token sebelum mengurangi saldo di state?
- Apa hasil dari
7 / 2di Solidity?
#4 β Kolom VALUE di Remix belum diisi. payable hanya mengizinkan penerimaan; jumlah yang dikirim ditentukan terpisah dari argumen function.
#6 β 3. Solidity membuang sisa pembagian. Kalau kamu butuh presisi, kalikan dulu dengan faktor skala baru bagi.
Lanjut: Unit 2 β Solidity Lanjutan π