Lanjut ke konten utama

Panduan Lengkap Kontrak ERC-20

solidityerc-20
Pemula
Ori Pomerantz
9 Maret 2021
24 bacaan singkat minute read

Pendahuluan

Salah satu kegunaan paling umum dari Ethereum untuk suatu grup adalah membuat token yang dapat dipertukarkan, dalam pengertian sebagai mata uang mereka sendiri. Token ini biasanya mengikuti standar, ERC-20. Standar ini memungkinkan penulisan perangkat, seperti kumpulan likuiditas dan dompet, yang bekerja dengan semua token ERC-20. Dalam artikel ini, kita akan menganalisis penerapan ERC20 Solidity OpenZeppelin(opens in a new tab), maupun definisi antarmuka(opens in a new tab).

Ini adalah kode sumber beranotasi. Jika Anda ingin menerapkan ERC-20, baca tutorial ini(opens in a new tab).

Antarmuka

Tujuan standar seperti ERC-20 adalah membuat banyak penerapan token yang saling berinteraksi di seluruh aplikasi, seperti dompet dan bursa terdesentralisasi. Untuk mencapai hal tersebut, kita membuat antamuka(opens in a new tab). Kode apa pun yang perlu menggunakan kontrak token dapat menggunakan definisi yang sama dalam antarmuka dan dapat menjadi kompatibel dengan semua kontrak token yang menggunakannya, apakah dompet seperti MetaMask, dapp seperti etherscan.io, atau kontrak berbeda seperti pool likuiditas.

Ilustrasi antarmuka ERC-20

Jika Anda adalah pemrogram yang berpengalaman, Anda mungkin pernah melihat konstruk serupa dalam Java(opens in a new tab) atau bahkan dalam file header C(opens in a new tab).

Ini adalah definisi dari Antarmuka ERC-20(opens in a new tab) dari OpenZeppelin. Ini adalah terjemahan dari standar yang dapat dibaca manusia(opens in a new tab) ke kode Solidity. Tentu saja, antarmukanya sendiri tidak menentukan cara melakukan apa pun. Cara ini dijelaskan dalam kode sumber kontrak di bawah.

1// SPDX-License-Identifier: MIT
Salin

File Solidity seharusnya mencakup pengenal lisensi. Anda dapat melihat daftar lisensi di sini(opens in a new tab). Jika Anda perlu lisensi yang berbeda, cukup jelaskan dalam bagian komentar.

1pragma solidity >=0.6.0 <0.8.0;
Salin

Bahasa Solidity masih berevolusi secara cepat, dan versi barunya mungkin tidak kompatibel dengan kode yang lama (lihat di sini(opens in a new tab)). Oleh karena itu, sebaiknya Anda tidak hanya menentukan versi minimum dari bahasa, tetapi juga versi maksimumnya, versi terbaru saat Anda menguji kode.

1/**
2 * @dev Antarmuka standar ERC20 seperti yang didefinisikan dalam EIP.
3 */
Salin

@dev dalam komentar adalah bagian dari format NatSpec(opens in a new tab), yang digunakan untuk membuat dokumentasi dari kode sumber.

1interface IERC20 {
Salin

Secara konvensi, nama antarmuka dimulai dengan I.

1 /**
2 * @dev Mengembalikan jumlah token yang ada.
3 */
4 function totalSupply() external view returns (uint256);
Salin

Fungsi ini bersifat external, artinya hanya dapat dipanggil dari luar kontrak(opens in a new tab). Fungsi ini mengembalikan total persediaan dari token dalam kontrak. Nilai ini dikembalikan menggunakan jenis yang paling umum di Ethereum, 256 bit yang tak bertandatangan (256 bit adalah ukuran kata asli untuk EVM). Fungsi ini juga adalah view, artinya tidak mengubah status, sehingga dapat dilaksanakan pada simpul tunggal alih-alih membuat semua simpul dalam rantai blok menjalankannya. Jenis fungsi tidaktidak menghasilkan transaksi dan tidak memerlukan biaya gas.

Catatan: Dalam teori, mungkin tampak bahwa pembuat kontrak dapat melakukan kecurangan dengan mengembalikan total persediaan yang lebih kecil dari nilai aslinya, sehingga membuat setiap token tampak lebih berharga dari nilai sebenarnya. Namun, ketakutan itu mengabaikan sifat sebenarnya dari rantai blok. Semua hal yang terjadi di rantai blok dapat diverifikasi oleh setiap simpul. Untuk mencapai hal tersebut, setiap kode bahasa mesin dan penyimpanan kontrak tersedia di setiap simpul. Meskipun Anda tidak diharuskan menerbitkan kode Solidity untuk kontrak Anda, tidak seorang pun akan menghargai Anda kecuali jika Anda menerbitkan kode sumber dan versi Solidity yang dengannya kontrak dikumpulkan, sehingga dapat diverifikasi terhadap kode bahasa mesin yang Anda sediakan. Contohnya, lihat kontrak ini(opens in a new tab).

1 /**
2 * @dev Mengembalikan jumlah token yang dimiliki oleh `akun`.
3 */
4 function balanceOf(address account) external view returns (uint256);
Salin

Seperti namanya, balanceOf mengembalikan saldo akun. Akun Ethereum dikenali dalam Solidity menggunakan jenis alamat, yang menampung 160 bit. Selain itu, merupakan external dan view.

1 /**
2 * @dev Memindahkan `jumlah` token dari akun pemanggil ke `penerima`.
3 *
4 * Mengembalikan nilai boolean yang menunjukkan apakah operasi berhasil.
5 *
6 * Memancarkan peristiwa {Transfer}.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);
Salin

Fungsi transfer mentransfer token dari pemanggil ke alamat berbeda. Fungsi ini melibatkan perubahan status, sehingga bukan view. Ketika pengguna memanggil fungsi ini, pihaknya membuat transaksi dan memerlukan gas. Selain itu, memancarkan peristiwa, Transfer, untuk memberitahu semua orang di rantai blok tentang peristiwa tersebut.

Fungsi tersebut memiliki dua jenis output untuk dua jenis pemanggil yang berbeda:

  • Pengguna yang memanggil fungsi secara langsung dari antarmuka pengguna. Pada umumnya, pengguna mengirimkan transaksi dan tidak menunggu respon, yang dapat memerlukan waktu yang tak dapat diperkirakan. Pengguna dapat melihat hal yang terjadi dengan mencari tanda terima transaksi (yang dikenali melalui hash transaksi) atau mencari peristiwa Transfer.
  • Kontrak lainnya, yang memanggil fungsi sebagai bagian dari transaksi secara keseluruhan. Kontrak-kontrak ini mendapatkan hasil segera, karena beroperasi dalam transaksi yang sama, sehingga dapat menggunakan nilai pengembalian fungsi.

Jenis output yang sama dibuat dengan fungsi lainnya yang mengubah status kontrak.

Tunjangan membuat akun mengeluarkan beberapa token yang dimiliki oleh pengguna yang berbeda. Hal ini berguna, contohnya, untuk kontrak yang bertindak sebagai penjual. Kontrak tidak dapat memantau aksi, sehingga jika pembeli mentransfer token ke kontrak penjual secara langsung, maka kontrak tersebut tidak akan diketahui telah dibayar. Sebagai gantinya, pembeli mengizinkan kontrak penjual mengeluarkan jumlah tertentu, dan penjual mentransfer jumlah tersebut. Hal ini dilakukan melalui fungsi yang dipanggil kontrak penjual, sehingga kontrak penjual dapat mengetahui jika transfer berhasil.

1 /**
2 * @dev Mengembalikan sisa jumlah token yang akan
3 * diizinkan untuk dikeluarkan oleh `pengguna` atas nama `pemilik` melalui {transferFrom}. Nilai ini merupakan
4 * nol secara default.
5 *
6 * Nilai ini berubah ketika {approve} atau {transferFrom} dipanggil.
7 */
8 function allowance(address owner, address spender) external view returns (uint256);
Salin

Fungsi tunjangan membiarkan setiap orang membuat kueri untuk melihat tunjangan yang diizinkan satu alamat (owner) untuk digunakan oleh alamat (spender).

1 /**
2 * @dev Menetapkan `jumlah` sebagai uang tunjangan `pengguna` melalui token pemanggil.
3 *
4 * Mengembalikan nilai boolean yang menunjukkan apakah operasi berhasil.
5 *
6 * PENTING: Ingatlah bahwa mengubah uang tunjangan dengan cara ini membawa resiko
7 * seseorang mungkin menggunakan baik uang tunjangan lama maupun baru dengan
8 * pengurutan transaksi yang tidak diharapkan. Satu solusi yang mungkin untuk mengatasi kompetisi ini
9 * adalah pertama-tama mengurangi uang tunjangan pengguna ke 0 dan menetapkan
10 * nilai yang diinginkan setelahnya:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Memancarkan aksi {Approval}.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Tampilkan semua
Salin

Fungsi persetujuan membuat tunjangan. Pastikan membaca pesan tentang cara tunjangan dapat disalahgunakan. Di Ethereum, Anda mengendalikan urutan transaksi Anda sendiri, tetapi Anda tidak dapat mengendalikan urutan di mana transaksi orang lain akan dilaksanakan, kecuali jika Anda tidak mengirimkan transaksi Anda sendiri sampai Anda melihat transaksi pihak lainnya telah terjadi.

1 /**
2 * @dev Memindahkan `jumlah` token dari `pengirim` ke `penerima` menggunakan
3 * mekanisme tunjangan. `jumlah` kemudian dikurangi dari tunjangan
4 * pemanggil.
5 *
6 * Mengembalikan nilai boolean yang menunjukkan apakah operasi berhasil.
7 *
8 * Memancarkan peristiwa {Transfer}.
9 */
10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Tampilkan semua
Salin

Akhirnya, transferFrom digunakan oleh pembelanja untuk benar-benar menggunakan tunjangan.

1
2 /**
3 * @dev Dipancarkan ketika token `nilai` dipindahkan dari satu akun (`dari`) ke
4 * lainnya (`ke`).
5 *
6 * Perhatikan bahwa `nilai` dapat menjadi nol.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by
12 * a call to {approve}. `nilai` adalah tunjangan baru.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Tampilkan semua
Salin

Aksi ini dipancarkan ketika status kontrak ERC-20 berubah.

Kontrak Sebenarnya

Ini adalah kontrak sebenarnya yang menerapkan standar ERC-20, yang diambil dari sini(opens in a new tab). Kontrak ini tidak dimaksudkan untuk digunakan seperti namanya, tetapi Anda dapat mewarisi(opens in a new tab) untuk memperluasnya menjadi sesuatu yang berguna.

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.6.0 <0.8.0;
Salin

Impor Pernyataan

Selain definisi antarmuka di atas, definisi kontrak mengimpor dua file lainnya:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
Salin

Komentar ini menjelaskan tujuan dari kontrak.

1/**
2 * @dev Penerapan dari antarmuka {IERC20}.
3 *
4 * Penerapan ini bersifat agnostik terhadap cara token dibuat. Artinya,
5 * mekanisme persediaan harus ditambahkan dalam kontrak yang dihasilkan menggunakan {_mint}.
6 * Untuk mekanisme generik, lihat {ERC20PresetMinterPauser}.
7 *
8 * TIP: Untuk tulisan terperinci, lihat panduan kami
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Cara
10 * menerapkan mekanisme persediaan].
11 *
12 * Kita telah mengikuti pedoman OpenZeppelin: fungsi membalikkan alih-alih
13 * mengembalikan `salah` saat kegagalan terjadi. Namun, perilaku ini bersifat konvensional
14 * dan tidak bertentangan dengan ekspektasi aplikasi ERC20.
15 *
16 * Selain itu, aksi {Approval} dipancarkan saat melakukan panggilan ke {transferFrom}.
17 * Aksi ini membuat aplikasi merekonstruksi kembali tunjangan untuk semua akun hanya
18 * dengan mendengarkan aksi yang dikatakan. Penerapan lain dari EIP tidak dapat memancarkan
19 * aksi ini, karena tidak diharuskan oleh spesifikasi.
20 *
21 * Akhirnya, fungsi {decreaseAllowance} dan {increaseAllowance}
22 * non-standar telah ditambahkan untuk mengurangi masalah yang diketahui seputar pengaturan
23 * tunjangan. Lihat {IERC20-approve}.
24 */
25
Tampilkan semua
Salin

Definisi Kontrak

1contract ERC20 is Context, IERC20 {
Salin

This line specifies the inheritance, in this case from IERC20 from above and Context, for OpenGSN.

1
2 using SafeMath for uint256;
3
Salin

Baris ini melekatkan pustaka SafeMath ke jenis uint256. Anda dapat menemukan pustaka ini di sini(opens in a new tab).

Definisi Variabel

Definisi-definisi ini menentukan variabel status kontrak. Variabel-variabel ini dinyatakan bersifat pribadi, tetapi hanya berarti bahwa kontrak lain di rantai blok tidak dapat membaca variabel-variabel tersebut. Tidak ada rahasia di rantai blok, perangkat lunak di setiap simpul memiliki status dari setiap kontrak dalam setiap blok. Dengan konvensi, variabel status diberi nama _<something>.

Kedua variabel pertama adalah pemetaan(opens in a new tab), artinya berperilaku kurang lebih sama dengan, larik asosiatif(opens in a new tab), dengan pengecualian bahwa kuncinya adalah nilai angka. Penyimpanan hanya dialokasikan untuk entri yang memiliki nilai berbeda dari default (nol).

1 mapping (address => uint256) private _balances;
Salin

Pemetaan pertama, _balances, adalah alamat dan saldo masing-masing token ini. Untuk mengakses saldo, gunakan sintaksis ini: _balances[<address>].

1 mapping (address => mapping (address => uint256)) private _allowances;
Salin

Variabel ini, _allowances, menyimpan tunjangan yang dijelaskan sebelumnya. Indeks pertama adalah pemiliki token, dan indeks kedua adalah kontrak dengan tunjangan. Untuk mengakses jumlah yang dapat dibelanjakan alamat A dari akun alamat B, gunakan _allowances[B][A].

1 uint256 private _totalSupply;
Salin

Seperti usulan namanya, variabel ini menelusuri total persediaan token.

1 string private _name;
2 string private _symbol;
3 uint8 private _decimals;
Salin

Ketiga variabel ini digunakan untuk meningkatkan keterbacaan. Kedua variabel pertama cukup jelas, tetapi _decimals tidak jelas.

Di satu sisi, Ethereum tidak memiliki titik mengambang atau variabel pecahan. Di sisi lain, manusia suka bisa membagi token. Satu alasan orang-orang puas dengan emas sebagai mata uang adalah bahwa emas sulit diubah ketika seseorang ingin membeli nilai yang sedikit dari sesuatu yang besar.

Solusinya adalah menelusuri bilangan bulat, tetapi sebagai gantinya menghitung token aslinya sebagai token pecahan yang hampir tidak berharga. Dalam kasus ether, token pecahan disebut wei, dan 10^18 wei sama dengan satu ETH. Dalam tulisan, 10.000.000.000.000 wei kira-kira sama dengan satu sen AS atau Euro.

Aplikasi perlu mengetahui cara menampilkan saldo token. Jika pengguna memiliki 3.141.000.000.000.000.000 wei, apakah itu sama dengan 3,14 ETH? 31,41 ETH? 3.141 ETH? Dalam kasus ether, nilai ini didefiniskan sebagai 10^18 wei untuk ETH, tetapi untuk token Anda, Anda dapat memilih nilai yang berbeda. Jika membagi token tidak masuk akal, Anda dapat menggunakan nilai nol _decimals. Jika Anda ingin menggunakan standar yang sama seperti ETH, gunakan nilai 18.

Konstruktor

1 /**
2 * @dev Menetapkan nilai untuk {name} dan {symbol}, menginisialisasi {decimals} dengan
3 * nilai default 18.
4 *
5 * Untuk memilih nilai berbeda untuk {decimals}, gunakan {_setupDecimals}.
6 *
7 * Semua ketiga nilai ini tak dapat diubah: mereka hanya dapat ditetapkan sekali setelah
8 * konstruksi.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 _name = name_;
12 _symbol = symbol_;
13 _decimals = 18;
14 }
Tampilkan semua
Salin

Konstruktor dipanggil ketika kontrak terlebih dahulu dibuat. Dengan konvensi, parameter fungsi diberi nama <something>_.

Fungsi Antarmuka Pengguna

1 /**
2 * @dev Mengembalikan nama token.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Returns the symbol of the token, usually a shorter version of the
10 * name.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Returns the number of decimals used to get its user representation.
18 * Sebagai contoh, jika `desimal` sama dengan `2`, saldo token `505` token harus
19 * ditampilkan untuk pengguna sebagai `5.05` (`505 / 10 ** 2`).
20 *
21 * Token biasanya memilih nilai 18, yang meniru hubungan antara
22 * ether dan wei. Ini nilai yang digunakan {ERC20}, kecuali {_setupDecimals} di
23 * panggil.
24 *
25 * CATATAN: Informasi ini hanya digunakan untuk tujuan _display_: itu tidak
26 * sama sekali berdampak terhadap aritmatika mana pun dari kontrak, termasuk
27 * {IERC20-balanceOf} dan {IERC20-transfer}.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
Tampilkan semua
Salin

Fungsi-fungsi ini, nama, simbol, dan desimal menolong antarmuka pengguna mengetahui tentang kontrak Anda sehingga mereka dapat menampilkannya dengan benar.

Jenis pengembaliannya adalah memori string, yang berarti kembalikan string yang tersimpan dalam memori. Variabel-variabel, seperti string, dapat tersimpan dalam tiga lokasi:

Seumur hidupAkses KontrakBiaya Gas
MemoriPemanggilan fungsiBaca/TulisPuluhan atau ratusan (lebih tinggi untuk lokasi yang lebih tinggi)
CalldataPemanggilan fungsiBaca SajaTidak dapat digunakan sebagai jenis pengembalian, hanya sebagai jenis parameter fungsi
PenyimpananSampai diubahBaca/TulisTinggi (800 untuk baca, 20.000 untuk tulis)

Dalam kasus ini, memori adalah pilihan terbaik.

Informasi Token Baca

Fungsi-fungsi ini menyediakan informasi tentang token, baik total persediaan atau saldo akun.

1 /**
2 * @dev See {IERC20-totalSupply}.
3 */
4 function totalSupply() public view override returns (uint256) {
5 return _totalSupply;
6 }
Salin

Fungsi totalSupply mengembalikan total persediaan token.

1 /**
2 * @dev See {IERC20-balanceOf}.
3 */
4 function balanceOf(address account) public view override returns (uint256) {
5 return _balances[account];
6 }
Salin

Baca saldo akun. Perhatikan bahwa siapa pun boleh mendapatkan saldo akun orang lain. Bagaimanapun juga, tidak ada gunanya mencoba menyembunyikan informasi ini, karena informasi tersebut tersedia di setiap simpul. Tidak ada rahasia di rantai blok.

Token Transfer

1 /**
2 * @dev See {IERC20-transfer}.
3 *
4 * Persyaratan:
5 *
6 * - `penerima` tidak dapat berupa alamat kosong.
7 * - pemanggil harus memiliki saldo `jumlah` minimum.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Tampilkan semua
Salin

Fungsi transfer dipanggil untuk mentransfer token dari akun pengirim ke akun berbeda. Perhatikan bahwa meskipun fungsi tersebut mengembalikan nilai boolean, nilai tesebut selalu benar. Jika transfer gagal, kontrak membalikkan panggilannya.

1 _transfer(_msgSender(), recipient, amount);
2 return true;
3 }
Salin

Fungsi _transfer melakukan pekerjaan sebenarnya. Ini adalah fungsi pribadi yang hanya dapat dipanggil melalui fungsi kontrak lainnya. Dengan konvensi, fungsi pribadi diberi nama _<something>, sama seperti variabel status.

Umumnya dalam Solidity, kita menggunakan msg.sender untuk pengirim pesan. Namun, itu memecah OpenGSN(opens in a new tab). Jika kita ingin mengizinkan transaksi tanpa ether dengan token kita, kita perlu menggunakan _msgSender(). Fungsi tersebut mengembalikan msg.sender untuk transaksi normal, tetapi untuk transaksi tanpa ether mengembalikan penandatangan asli dan bukan kontrak yang menyampaikan pesan.

Fungsi Tunjangan

Ini adalah fungsi yang menerapkan fungsionalitas tunjangan: tunjangan, persetujuan, transferFrom, dan _approve. Selain itu, penerapan OpenZeppelin melebihi standar dasar yang memasukkan beberapa fitur sehingga meningkatkan keamanan: increaseAllowance, dan decreaseAllowance.

Fungsi tunjangan

1 /**
2 * @dev See {IERC20-allowance}.
3 */
4 function allowance(address owner, address spender) public view virtual override returns (uint256) {
5 return _allowances[owner][spender];
6 }
Salin

Fungsi tunjangan membuat semua orang memeriksa tunjangan mana pun.

Fungsi persetujuan

1 /**
2 * @dev See {IERC20-approve}.
3 *
4 * Persyaratan:
5 *
6 * - `pembelanja` tidak dapat berupa alamat kosong.
7 */
8 function approve(address spender, uint256 amount) public virtual override returns (bool) {
Salin

Fungsi ini dipanggil untuk membuat tunjangan. Fungsi ini serupa dengan fungsi transfer di atas:

  • Fungsi hanya memanggil fungsi internal (dalam kasus ini, _approve) yang melakukan proses sebenarnya.
  • Fungsi baik mengembalikan yang benar (jika berhasil) maupun membalikkan (jika tidak).
1 _approve(_msgSender(), spender, amount);
2 return true;
3 }
Salin

Kita menggunakan fungsi internal untuk meminimalkan jumlah tempat di mana perubahan status terjadi. Fungsi mana pun yang mengubah status merupakan resiko keamanan berpotensi yang perlu diaudit demi keamanan. Dengan cara ini, kita mengurangi peluang untuk kesalahan.

Fungsi transferFrom

Fungsi ini dipanggil pembelanja untuk membelanjakan tunjangan. Fungsi ini memerlukan dua operasi: transfer jumlah yang akan dibelanjakan dan kurangi tunjangan sebesar jumlah tersebut.

1 /**
2 * @dev See {IERC20-transferFrom}.
3 *
4 * Memancarkan aksi {Approval} menunjukkan tunjangan yang diperbarui. Ini tidak
5 * diharuskan oleh EIP. Lihat catatan awal {ERC20}.
6 *
7 * Persyaratan:
8 *
9 * - `pengirim` dan `penerima` tidak dapat merupakan alamat nol.
10 * - `pengirim` harus memiliki saldo `jumlah` minimum.
11 * - pemanggil harus memiliki saldo untuk token``pengirim`` sebesar
12 * `jumlah` minimum.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
Tampilkan semua
Salin

Panggilan fungsi a.sub(b, "message") melakukan dua hal. Pertama, fungsi tersebut menghitung a-b, yang adalah tunjangan baru. Kedua, fungsi tersebut memeriksa apakah hasil ini tidak negatif. Jika negatif, panggilan membalikkan dengan pesan yang disediakan. Perhatikan bahwa ketika panggilan membalikkan, pemrosesan mana pun yang dilakukan sebelumnya selama panggilan tersebut diabaikan, sehingga kita tidak perlu membatalkan _transfer.

1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
2 "ERC20: transfer amount exceeds allowance"));
3 return true;
4 }
Salin

Penambahan keamanan OpenZeppelin

Menetapkan tunjangan tidak nol ke nilai tidak nol lainnya berbahaya, karena Anda hanya mengendalikan urutan transaksi Anda sendiri, bukan milik orang lain. Bayangkan Anda mempunyai dua pengguna, Alice yang naif dan Bill yang tidak jujur. Alice menginginkan beberapa layanan dari Bill, yang dipikirnya membutuhkan lima token - sehingga ia memberikan tunjangan sebesar lima token kepada Bill.

Lalu, sesuatu berubah dan harga Bill naik menjadi sepuluh token. Alice, yang masih memerlukan layanan, mengirim transaksi yang menetapkan tunjangan Bill menjadi sepuluh token. Saat Bill melihat transaksi baru ini dalam pool transaksi, ia mengirim transaksi yang membelanjakan lima token Alice dan memiliki harga gas yang jauh lebih tinggi, sehingga transaksi akan ditambang lebih cepat. Dengan cara itu, Bill dapat membelanjakan kelima token pertama dan kemudian, setelah tunjangan baru Alice ditambang, membelanjakan sepuluh token lagi untuk total harga lima belas token, melebihi jumlah yang dizinkan oleh Alice. Teknik ini disebut front-running(opens in a new tab)

Transaksi AliceNonce AliceTransaksi BillNonce BillTunjangan BillTagihkan Total Pendapatan dari Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10.12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10.124015

Untuk menghindari masalah ini, kedua fungsi ini (increaseAllowance dan decreaseAllowance) memmbuat Anda dapat memodifikasi tunjangan sebesar jumlah tertentu. Jadi, jika Bill telah membelanjakan lima token, ia hanya akan dapat membelanjakan lima token lagi. Depending on the timing, there are two ways this can work, both of which end with Bill only getting ten tokens:

A:

Transaksi AliceNonce AliceTransaksi BillNonce BillTunjangan BillTagihkan Total Pendapatan dari Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10.12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10.124010

B:

Transaksi AliceNonce AliceTransaksi BillNonce BillTunjangan BillTagihkan Total Pendapatan dari Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10.124010
1 /**
2 * @dev Secara atomik meningkatkan tunjangan yang diberikan ke `pembelanja` oleh pemanggil.
3 *
4 * Alternatif ini untuk {approve} yang dapat digunakan sebagai solusi untuk masalah yang
5 * dideskripsikan dalam {IERC20-approve}.
6 *
7 * Memancarkan aksi {Approval} menunjukkan tunjangan yang diperbarui.
8 *
9 * Persyaratan:
10 *
11 * - `pembelanja` tidak dapat berupa alamat kosong.
12 */
13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));
15 return true;
16 }
Tampilkan semua
Salin

Fungsi a.add(b) aman untuk ditambahkan. Dalam kasus yang jarang terjadi bahwa a+b>=2^256, fungsi tersebut tidak membungkus seperti dalam cara yang dilakukan penambahan normal.

1
2 /**
3 * @dev Secara atomik mengurangi tunjangan yang diberikan ke `pembelanja` oleh pemanggil.
4 *
5 * Alternatif ini untuk {approve} yang dapat digunakan sebagai solusi untuk masalah yang
6 * dideskripsikan dalam {IERC20-approve}.
7 *
8 * Memancarkan aksi {Approval} menunjukkan tunjangan yang diperbarui.
9 *
10 * Persyaratan:
11 *
12 * - `pembelanja` tidak dapat berupa alamat kosong.
13 * - `pembelanja` harus memiliki tunjangan untuk pemanggil minimum
14 * `subtractedValue`.
15 */
16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,
18 "ERC20: decreased allowance below zero"));
19 return true;
20 }
Tampilkan semua
Salin

Fungsi yang Memodifikasi Informasi Token

Keempat fungsi ini melakukan pekerjaan sebenarnya: _transfer, _mint, _burn, dan _approve.

Fungsi _transfer {#_transfer}

1 /**
2 * @dev Memindahkan `jumlah` token dari `pengirim` ke `penerima`.
3 *
4 * Fungsi internal ini setara dengan {transfer}, dan dapat digunakan untuk
5 * misalnya, menerapkan biaya token otomatis, mekanisme pemotongan, dll.
6 *
7 * Memancarkan peristiwa {Transfer}.
8 *
9 * Persyaratan:
10 *
11 * - `pengirim` tidak dapat berupa alamat kosong.
12 * - `penerima` tidak dapat berupa alamat kosong.
13 * - `pengirim` harus memiliki saldo `jumlah` minimum.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Tampilkan semua
Salin

Fungsi ini, _transfer, mentransfer token dari satu akun ke akun lainnya. Fungsi ini dipanggil melalui kedua transfer (untuk mentransfer dari akun pengirim sendiri) dan transferFrom (untuk menggunakan tunjangan guna mentransfer dari akun orang lain).

1 require(sender != address(0), "ERC20: transfer from the zero address");
2 require(recipient != address(0), "ERC20: transfer to the zero address");
Salin

Tidak seorang pun yang benar-benar memiliki alamat kosong di Ethereum (yakni, tidak seorang pun yang tahu kunci pribadi di mana kunci publik yang berkesesuaian diubah menjadi alamat kosong). Ketika manusia menggunakan alamat itu, biasanya alamat itu merupakan bug perangkat lunak - sehingga transaksi kita gagal jika alamat kosong digunakan sebagai pengirim atau penerima.

1 _beforeTokenTransfer(sender, recipient, amount);
2
Salin

Ada dua cara untuk menggunakan kontrak ini:

  1. Gunakan kontrak tersebut sebagai templat untuk kode Anda sendiri
  2. Mewarisi dari kontrak(opens in a new tab), dan hanya menimpa fungsi-fungsi yang perlu Anda modifikasi

Metode kedua jauh lebih baik karena kode ERC-20 OpenZeppelin telah diaudit dan dinyatakan aman. Ketika Anda menggunakan warisan, jelas fungsi yang Anda modifikasi, dan untuk mempercayai kontrak Anda, manusia hanya perlu mengaudit fungsi khusus tersebut.

Sering kali kita perlu menjalankan fungsi setiap kali token berpindah tangan. Namun, _transfer merupakan fungsi yang sangat penting dan mungkin untuk menulis dengan tidak aman (lihat di bawah), sehingga menjadi langkah terbaik jika tidak menimpanya. Solusinya adalah _beforeTokenTransfer, fungsi kaitan(opens in a new tab). Anda dapat menimpa fungsi ini, dan fungsi tersebut akan dipanggil pada setiap transfer.

1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
2 _balances[recipient] = _balances[recipient].add(amount);
Salin

Baris-baris ini benar-benar melakukan transfer. Perhatikan bahwa tidak terjadi masalah di antara baris-baris tersebut, dan bahwa kita mengurangi jumlah yang ditransfer dari pengirim sebelum menambahkannya ke penerima. Ini penting karena jika ada panggilan ke kontrak berbeda di pertengahan, panggilan tersebut dapat digunakan untuk mencurangi kontrak ini. Dengan cara ini, transfer bersifat atomik, tidak ada yang dapat terjadi di pertengahan proses.

1 emit Transfer(sender, recipient, amount);
2 }
Salin

Akhirnya, pancarkan aksi Transfer. Aksi tidak dapat diakses oleh kontrak pintar, tetapi kode yang beroperasi di luar rantai blok dapat mendengarkan aksi dan bereaksi terhadapnya. Contohnya, dompet dapat menelusuri waktu pemilik mendapatkan lebih banyak token.

Fungsi _mint dan _burn {#_mint-and-_burn}

Kedua fungsi (_mint dan _burn) ini memodifikasi total persediaan token. Kedua fungsi tersebut bersifat internal dan tidak memiliki fungsi yang memanggil kedua fungsi tersebut dalam kontrak ini, sehingga kedua fungsi tersebut hanya berguna jika Anda mewariskannya dari kontrak dan menambahkan logika Anda sendiri untuk menentukan dalam kondisi apa untuk mencetak token baru atau membakar token yang sudah ada.

CATATAN: Setiap token ERC-20 memiliki logika bisnisnya sendiri yang mengatur manajemen token. Contohnya, kontrak persediaan yang tetap mungkin hanya memanggil _mint dalam konstruktor dan tidak pernah memanggil _burn. Kontrak yang menjual token akan memanggil _mint ketika ia dibayarkan, dan agaknya memanggil _burn pada titik tertentu untuk menghindari inflasi yang cepat.

1 /** @dev Membuat token `jumlah` dan menetapkan token ke `akun`, yang meningkatkan
2 * total persediaan token.
3 *
4 * Memancarkan aksi {Transfer} dengan `dari` yang ditetapkan ke alamat kosong.
5 *
6 * Persyaratan:
7 *
8 * - `ke` tidak dapat berupa alamat kosong.
9 */
10 function _mint(address account, uint256 amount) internal virtual {
11 require(account != address(0), "ERC20: mint to the zero address");
12 _beforeTokenTransfer(address(0), account, amount);
13 _totalSupply = _totalSupply.add(amount);
14 _balances[account] = _balances[account].add(amount);
15 emit Transfer(address(0), account, amount);
16 }
Tampilkan semua
Salin

Pastikan memperbarui _totalSupply ketika total jumlah token berubah.

1 /**
2 * @dev Menghancurkan token `jumlah` dari `akun`, yang mengurangi
3 * total persediaan.
4 *
5 * Memancarkan aksi {Transfer} dengan `ke` yang ditetapkan ke alamat kosong.
6 *
7 * Persyaratan:
8 *
9 * - `akun` tidak dapat berupa alamat kosong.
10 * - `akun` harus memiliki token `jumlah` minimum.
11 */
12 function _burn(address account, uint256 amount) internal virtual {
13 require(account != address(0), "ERC20: burn from the zero address");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");
18 _totalSupply = _totalSupply.sub(amount);
19 emit Transfer(account, address(0), amount);
20 }
Tampilkan semua

Fungsi _burn hampir sama dengan _mint, kecuali bergerak ke arah yang lain.

Fungsi _approve {#_approve}

Fungsi ini benar-benar menentukan tunjangan. Perhatikan bahwa fungsi tersebut membuat pemilik menentukan tunjangan yang lebih tinggi dari saldo pemilik saat ini. Ini OKE karena saldo diperiksa pada waktu transfer terjadi, ketika saldonya dapat berbeda dari saldo saat tunjangan dibuat.

1 /**
2 * @dev Menetapkan `jumlah` sebagai tunjangan `pengguna` melalui token `pemilik`.
3 *
4 * Fungsi internal ini sama dengan `persetujuan`, dan dapat digunakan untuk
5 * misalnya, menetapkan tunjangan otomatis untuk subsistem tertentu, dll.
6 *
7 * Memancarkan aksi {Approval}.
8 *
9 * Persyaratan:
10 *
11 * - `pemilik` tidak dapat berupa alamat kosong.
12 * - `pembelanja` tidak dapat berupa alamat kosong.
13 */
14 function _approve(address owner, address spender, uint256 amount) internal virtual {
15 require(owner != address(0), "ERC20: approve from the zero address");
16 require(spender != address(0), "ERC20: approve to the zero address");
17
18 _allowances[owner][spender] = amount;
Tampilkan semua
Salin

Pancarkan aksi Persetujuan. Bergantung pada cara aplikasi ditulis, kontrak pembelanja dapat diberitahu tentang persetujuan baik oleh pemilik maupun server yang mendengarkan aksi ini.

1 emit Approval(owner, spender, amount);
2 }
3
Salin

Modifikasi Variabel Desimal

1
2
3 /**
4 * @dev Menetapkan {decimals} ke nilai selain nilai default 18.
5 *
6 * PERINGATAN: Fungsi ini hanya boleh dipanggil dari konstruktor. Kebanyakan
7 * aplikasi yang berinteraksi dengan kontrak token tidak akan mengharapkan
8 * {decimals} berubah, dan dapat berfungsi dengan tidak benar jika itu terjadi.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Tampilkan semua
Salin

Fungsi ini memodifikasi variabel _decimals yang digunakan untuk memberitahu antarmuka pengguna tentang cara menafsirkan jumlahnya. Anda seharusnya memanggilnya dari konstruktor. Tidak jujur untuk memanggilnya pada titik berikut mana pun, dan aplikasi tidak dirancang untuk menangani ketidakjujuran tersebut.

Kaitan

1
2 /**
3 * @dev Kaitan yang dipanggil sebelum transfer token mana pun. Termasuk
4 * mencetak dan membakar.
5 *
6 * Syarat panggilan:
7 *
8 * - ketika `dari` dan `ke` keduanya berupa bukan nol, `jumlah` dari token ``dari``
9 * akan ditransfer ke `ke`.
10 * - ketika `dari` adalah nol, token `jumlah` akan dicetak untuk `ke`.
11 * - ketika `ke` adalah nol, `jumlah` dari token ``dari`` akan dibakar.
12 * - `dari` dan `ke` keduanya tidak pernah berupa nol.
13 *
14 * Untuk mempelajari selengkapnya tentang kaitan, beralih ke xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
Tampilkan semua
Salin

Fungsi kaitan ini yang akan dipanggil selama transfer. It is empty here, but if you need it to do something you just override it.

Kesimpulan

Sebagai tinjauan ulang, berikut adalah beberapa pemikiran paling penting dalam kontrak ini (menurut pendapat saya, kepunyaan Anda mungkin bisa saja berbeda):

  • Tidak ada rahasia di rantai blok. Informasi apa pun yang dapat diakses oleh kontrak pintar tersedia untuk seluruh dunia.
  • Anda dapat mengendalikan urutan transaksi Anda sendiri, tetapi tidak dapat mengendalikan saat transaksi orang lain terjadi. Alasan ini mengubah tunjangan dapat menjadi berbahaya, karena membiarkan pembelanja membelanjakan jumlah dari kedua tunjangan.
  • Nilai dari jenis uint256 diperbesar. Dengan kata lain, 0-1=2^256-1. Jika merupakan perilaku yang tidak diharapkan, Anda perlu memeriksanya (atau gunakan pustaka SafeMath yang melakukannya untuk Anda). Perhatikan bahwa perilaku ini berubah dalam Solidity 0.8.0(opens in a new tab).
  • Lakukan semua perubahan status dari jenis khusus dalam tempat khusus, karena membuat proses audit lebih mudah dilakukan. Alasan ini membuat kita memiliki, contohnya, _approve, yang dipanggil melalui persetujuan, transferFrom, increaseAllowance, dan decreaseAllowance
  • Perubahan status harus bersifat atomik, tanpa adanya tindakan lain di pertengahan (seperti yang dapat Anda lihat dalam _transfer). Perubahan ini terjadi selama perubahan status di mana Anda memiliki status yang tidak konsisten. Contohnya, antara waktu Anda mengurangi saldo pengirim dan waktu Anda menambahkan saldo penerima, lebih sedikit token yang ada dari jumlah sebenarnya. Dapat berpotensi disalahgunakan jika ada operasi di antara kedua waktu tersebut, khususnya panggilan ke kontrak yang berbeda.

Sekarang, karena Anda telah melihat cara kontrak OpenZeppelin ERC-20 ditulis, dan khususnya caranya dibuat lebih aman, buka dan tulis kontrak dan aplikasi Anda sendiri yang aman.

Terakhir diedit: @nhsz(opens in a new tab), 18 Februari 2024

Apakah tutorial ini membantu?