Chuyển đến nội dung chính

Hướng dẫn chi tiết hợp đồng ERC-20

Solidity
erc-20
Người mới bắt đầu
Ori Pomerantz
9 tháng 3, 2021
34 phút đọc

Giới thiệu

Một trong những ứng dụng phổ biến nhất của Ethereum là để một nhóm tạo ra một token có thể giao dịch, theo một nghĩa nào đó là tiền tệ của riêng họ. Các token này thường tuân theo một tiêu chuẩn, ERC-20. Tiêu chuẩn này giúp cho việc viết các công cụ, chẳng hạn như pool thanh khoản và ví, có thể hoạt động với tất cả các token ERC-20. Trong bài viết này, chúng ta sẽ phân tích bản triển khai ERC20 bằng Solidity của OpenZeppelin (opens in a new tab), cũng như định nghĩa giao diện (opens in a new tab).

Đây là mã nguồn có chú thích. Nếu bạn muốn triển khai ERC-20, hãy đọc hướng dẫn này (opens in a new tab).

Giao diện

Mục đích của một tiêu chuẩn như ERC-20 là cho phép nhiều bản triển khai token có khả năng tương tác trên các ứng dụng, như ví và các sàn giao dịch phi tập trung. Để đạt được điều đó, chúng ta tạo ra một giao diện (opens in a new tab). Bất kỳ mã nào cần sử dụng hợp đồng token đều có thể sử dụng các định nghĩa tương tự trong giao diện và tương thích với tất cả các hợp đồng token sử dụng nó, cho dù đó là ví như MetaMask, một ứng dụng phi tập trung (dapp) như etherscan.io, hay một hợp đồng khác như pool thanh khoản.

Illustration of the ERC-20 interface

Nếu bạn là một lập trình viên có kinh nghiệm, bạn có thể nhớ đã thấy các cấu trúc tương tự trong Java (opens in a new tab) hoặc thậm chí trong các tệp tiêu đề C (opens in a new tab).

Đây là định nghĩa của Giao diện ERC-20 (opens in a new tab) từ OpenZeppelin. Nó là bản dịch của tiêu chuẩn con người có thể đọc được (opens in a new tab) sang mã Solidity. Tất nhiên, bản thân giao diện không định nghĩa cách để làm bất cứ điều gì. Điều đó được giải thích trong mã nguồn hợp đồng bên dưới.

 

// SPDX-License-Identifier: MIT

Các tệp Solidity được cho là bao gồm một mã định danh giấy phép. Bạn có thể xem danh sách các giấy phép tại đây (opens in a new tab). Nếu bạn cần một giấy phép khác, chỉ cần giải thích nó trong các bình luận.

 

pragma solidity >=0.6.0 <0.8.0;

Ngôn ngữ Solidity vẫn đang phát triển nhanh chóng và các phiên bản mới có thể không tương thích với mã cũ (xem tại đây (opens in a new tab)). Do đó, bạn nên chỉ định không chỉ phiên bản tối thiểu của ngôn ngữ mà còn cả phiên bản tối đa, phiên bản mới nhất mà bạn đã kiểm tra mã.

 

/**
 * @dev Giao diện của tiêu chuẩn ERC-20 như được định nghĩa trong EIP.
 */

@dev trong bình luận là một phần của định dạng NatSpec (opens in a new tab), được sử dụng để tạo tài liệu từ mã nguồn.

 

interface IERC20 {

Theo quy ước, tên giao diện bắt đầu bằng I.

 

    /**
     * @dev Trả về số lượng token đang tồn tại.
     */
    function totalSupply() external view returns (uint256);

Hàm này là external, nghĩa là nó chỉ có thể được gọi từ bên ngoài hợp đồng (opens in a new tab). Nó trả về tổng nguồn cung token trong hợp đồng. Giá trị này được trả về bằng cách sử dụng kiểu phổ biến nhất trong Ethereum, số nguyên không dấu 256 bit (256 bit là kích thước từ gốc của EVM). Hàm này cũng là một view, có nghĩa là nó không thay đổi trạng thái, vì vậy nó có thể được thực thi trên một nút duy nhất thay vì yêu cầu mọi nút trong chuỗi khối chạy nó. Loại hàm này không tạo ra giao dịch và không tốn gas.

Lưu ý: Về lý thuyết, có vẻ như người tạo hợp đồng có thể gian lận bằng cách trả về tổng nguồn cung nhỏ hơn giá trị thực, làm cho mỗi token có vẻ giá trị hơn thực tế. Tuy nhiên, nỗi sợ hãi đó bỏ qua bản chất thực sự của chuỗi khối. Mọi thứ xảy ra trên chuỗi khối đều có thể được xác minh bởi mọi nút. Để đạt được điều này, mã ngôn ngữ máy và bộ nhớ của mọi hợp đồng đều có sẵn trên mọi nút. Mặc dù bạn không bắt buộc phải xuất bản mã Solidity cho hợp đồng của mình, nhưng sẽ không ai coi trọng bạn trừ khi bạn xuất bản mã nguồn và phiên bản Solidity mà nó được biên dịch, để nó có thể được xác minh dựa trên mã ngôn ngữ máy mà bạn đã cung cấp. Ví dụ, xem hợp đồng này (opens in a new tab).

 

    /**
     * @dev Trả về số lượng token thuộc sở hữu của `account`.
     */
    function balanceOf(address account) external view returns (uint256);

Đúng như tên gọi, balanceOf trả về số dư của một tài khoản. Các tài khoản Ethereum được xác định trong Solidity bằng cách sử dụng kiểu address, chứa 160 bit. Nó cũng là externalview.

 

    /**
     * @dev Chuyển `amount` token từ Tài khoản của người gọi đến `recipient`.
     *
     * Trả về một giá trị boolean chỉ định liệu hoạt động có thành công hay không.
     *
     * Phát ra một sự kiện {Transfer}.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

Hàm transfer chuyển token từ người gọi đến một địa chỉ khác. Điều này liên quan đến sự thay đổi trạng thái, vì vậy nó không phải là view. Khi người dùng gọi hàm này, nó tạo ra một giao dịch và tốn gas. Nó cũng phát ra một sự kiện, Transfer, để thông báo cho mọi người trên chuỗi khối về sự kiện này.

Hàm có hai loại đầu ra cho hai loại người gọi khác nhau:

  • Người dùng gọi hàm trực tiếp từ giao diện người dùng. Thông thường, người dùng gửi một giao dịch và không đợi phản hồi, điều này có thể mất một khoảng thời gian không xác định. Người dùng có thể xem những gì đã xảy ra bằng cách tìm kiếm biên lai giao dịch (được xác định bởi mã băm giao dịch) hoặc bằng cách tìm kiếm sự kiện Transfer.
  • Các hợp đồng khác, gọi hàm như một phần của giao dịch tổng thể. Các hợp đồng đó nhận được kết quả ngay lập tức, vì chúng chạy trong cùng một giao dịch, do đó chúng có thể sử dụng giá trị trả về của hàm.

Cùng một loại đầu ra được tạo bởi các hàm khác làm thay đổi trạng thái của hợp đồng.

 

Hạn mức cho phép một tài khoản chi tiêu một số token thuộc về một chủ sở hữu khác. Điều này rất hữu ích, ví dụ, đối với các hợp đồng đóng vai trò là người bán. Các hợp đồng không thể theo dõi các sự kiện, vì vậy nếu người mua chuyển token trực tiếp đến hợp đồng người bán hợp đồng đó sẽ không biết nó đã được thanh toán. Thay vào đó, người mua cho phép hợp đồng người bán chi tiêu một số lượng nhất định và người bán chuyển số lượng đó. Điều này được thực hiện thông qua một hàm mà hợp đồng người bán gọi, để hợp đồng người bán có thể biết liệu nó có thành công hay không.

    /**
     * @dev Trả về số lượng token còn lại mà `spender` sẽ được
     * phép chi tiêu thay mặt cho `owner` thông qua {transferFrom}. Giá trị này mặc định
     * là không (zero).
     *
     * Giá trị này thay đổi khi {approve} hoặc {transferFrom} được gọi.
     */
    function allowance(address owner, address spender) external view returns (uint256);

Hàm allowance cho phép bất kỳ ai truy vấn để xem hạn mức mà một địa chỉ (owner) cho phép một địa chỉ khác (spender) chi tiêu là bao nhiêu.

 

Hàm approve tạo ra một hạn mức. Hãy chắc chắn đọc thông điệp về cách nó có thể bị lạm dụng. Trong Ethereum, bạn kiểm soát thứ tự các giao dịch của riêng mình, nhưng bạn không thể kiểm soát thứ tự mà các giao dịch của người khác sẽ được thực thi, trừ khi bạn không gửi giao dịch của riêng mình cho đến khi bạn thấy giao dịch của bên kia đã xảy ra.

 

Cuối cùng, transferFrom được sử dụng bởi người chi tiêu để thực sự chi tiêu hạn mức.

 

Các sự kiện này được phát ra khi trạng thái của hợp đồng ERC-20 thay đổi.

Hợp đồng thực tế

Đây là hợp đồng thực tế triển khai tiêu chuẩn ERC-20, được lấy từ đây (opens in a new tab). Nó không nhằm mục đích được sử dụng nguyên bản, nhưng bạn có thể kế thừa (opens in a new tab) từ nó để mở rộng thành một thứ gì đó có thể sử dụng được.

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

 

Các câu lệnh Import

Ngoài các định nghĩa giao diện ở trên, định nghĩa hợp đồng nhập hai tệp khác:


import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
  • GSN/Context.sol là các định nghĩa cần thiết để sử dụng OpenGSN (opens in a new tab), một hệ thống cho phép người dùng không có ether sử dụng chuỗi khối. Lưu ý rằng đây là phiên bản cũ, nếu bạn muốn tích hợp với OpenGSN hãy sử dụng hướng dẫn này (opens in a new tab).
  • Thư viện SafeMath (opens in a new tab), giúp ngăn chặn tràn số/tràn số dưới số học cho các phiên bản Solidity <0.8.0. Trong Solidity ≥0.8.0, các phép toán số học tự động hoàn nguyên khi tràn số/tràn số dưới, làm cho SafeMath trở nên không cần thiết. Hợp đồng này sử dụng SafeMath để tương thích ngược với các phiên bản trình biên dịch cũ hơn.

 

Bình luận này giải thích mục đích của hợp đồng.

Định nghĩa hợp đồng

contract ERC20 is Context, IERC20 {

Dòng này chỉ định sự kế thừa, trong trường hợp này là từ IERC20 ở trên và Context, cho OpenGSN.

 


    using SafeMath for uint256;

Dòng này gắn thư viện SafeMath vào kiểu uint256. Bạn có thể tìm thấy thư viện này tại đây (opens in a new tab).

Định nghĩa biến

Các định nghĩa này chỉ định các biến trạng thái của hợp đồng. Các biến này được khai báo là private, nhưng điều đó chỉ có nghĩa là các hợp đồng khác trên chuỗi khối không thể đọc chúng. Không có bí mật nào trên chuỗi khối, phần mềm trên mọi nút đều có trạng thái của mọi hợp đồng tại mọi khối. Theo quy ước, các biến trạng thái được đặt tên là _<something>.

Hai biến đầu tiên là các ánh xạ (mappings) (opens in a new tab), nghĩa là chúng hoạt động gần giống như mảng kết hợp (opens in a new tab), ngoại trừ việc các khóa là các giá trị số. Bộ nhớ chỉ được phân bổ cho các mục nhập có giá trị khác với mặc định (không).

    mapping (address => uint256) private _balances;

Ánh xạ đầu tiên, _balances, là các địa chỉ và số dư tương ứng của chúng đối với token này. Để truy cập số dư, hãy sử dụng cú pháp này: _balances[<address>].

 

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

Biến này, _allowances, lưu trữ các hạn mức đã giải thích trước đó. Chỉ số đầu tiên là chủ sở hữu của các token và chỉ số thứ hai là hợp đồng có hạn mức. Để truy cập số lượng địa chỉ A có thể chi tiêu từ tài khoản của địa chỉ B, hãy sử dụng _allowances[B][A].

 

    uint256 private _totalSupply;

Đúng như tên gọi, biến này theo dõi tổng nguồn cung token.

 

    string private _name;
    string private _symbol;
    uint8 private _decimals;

Ba biến này được sử dụng để cải thiện khả năng đọc. Hai biến đầu tiên tự giải thích, nhưng _decimals thì không.

Một mặt, Ethereum không có biến dấu phẩy động hoặc phân số. Mặt khác, con người thích có thể chia nhỏ token. Một lý do khiến mọi người chọn vàng làm tiền tệ là vì rất khó để trả lại tiền thừa khi ai đó muốn mua một con bò bằng giá trị của một con vịt.

Giải pháp là theo dõi các số nguyên, nhưng thay vì đếm token thực, hãy đếm một token phân số gần như vô giá trị. Trong trường hợp của ether, token phân số được gọi là Wei và 10^18 Wei bằng một ETH. Tại thời điểm viết bài, 10.000.000.000.000 Wei xấp xỉ một xu Mỹ hoặc Euro.

Các ứng dụng cần biết cách hiển thị số dư token. Nếu người dùng có 3.141.000.000.000.000.000 Wei, đó là 3,14 ETH? 31,41 ETH? 3.141 ETH? Trong trường hợp của ether, nó được định nghĩa là 10^18 Wei cho mỗi ETH, nhưng đối với token của bạn, bạn có thể chọn một giá trị khác. Nếu việc chia nhỏ token không có ý nghĩa, bạn có thể sử dụng giá trị _decimals bằng không. Nếu bạn muốn sử dụng cùng tiêu chuẩn với ETH, hãy sử dụng giá trị 18.

Hàm khởi tạo

Hàm khởi tạo được gọi khi hợp đồng được tạo lần đầu tiên. Theo quy ước, các tham số hàm được đặt tên là <something>_.

Các hàm giao diện người dùng

Các hàm này, name, symboldecimals giúp giao diện người dùng biết về hợp đồng của bạn để chúng có thể hiển thị nó một cách chính xác.

Kiểu trả về là string memory, nghĩa là trả về một chuỗi được lưu trữ trong bộ nhớ. Các biến, chẳng hạn như chuỗi, có thể được lưu trữ ở ba vị trí:

Vòng đờiQuyền truy cập hợp đồngChi phí gas
Bộ nhớLệnh gọi hàmĐọc/GhiHàng chục hoặc hàng trăm (cao hơn đối với các vị trí cao hơn)
Dữ liệu lệnh gọiLệnh gọi hàmChỉ đọcKhông thể được sử dụng làm kiểu trả về, chỉ là kiểu tham số hàm
Lưu trữCho đến khi thay đổiĐọc/GhiCao (800 cho đọc, 20k cho ghi)

Trong trường hợp này, memory là lựa chọn tốt nhất.

Đọc thông tin token

Đây là các hàm cung cấp thông tin về token, có thể là tổng nguồn cung hoặc số dư của một tài khoản.

    /**
     * @dev Xem {IERC20-totalSupply}.
     */
    function totalSupply() public view override returns (uint256) {
        return _totalSupply;
    }

Hàm totalSupply trả về tổng nguồn cung token.

 

    /**
     * @dev Xem {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view override returns (uint256) {
        return _balances[account];
    }

Đọc số dư của một tài khoản. Lưu ý rằng bất kỳ ai cũng được phép lấy số dư tài khoản của bất kỳ ai khác. Không có ích gì khi cố gắng che giấu thông tin này, vì dù sao nó cũng có sẵn trên mọi nút. Không có bí mật nào trên chuỗi khối.

Chuyển token

Hàm transfer được gọi để chuyển token từ tài khoản của người gửi sang một tài khoản khác. Lưu ý rằng mặc dù nó trả về một giá trị boolean, giá trị đó luôn là true. Nếu việc chuyển thất bại, hợp đồng sẽ hoàn nguyên lệnh gọi.

 

        _transfer(_msgSender(), recipient, amount);
        return true;
    }

Hàm _transfer thực hiện công việc thực tế. Nó là một hàm riêng tư chỉ có thể được gọi bởi các hàm hợp đồng khác. Theo quy ước, các hàm riêng tư được đặt tên là _<something>, giống như các biến trạng thái.

Thông thường trong Solidity, chúng ta sử dụng msg.sender cho người gửi thông điệp. Tuy nhiên, điều đó làm hỏng OpenGSN (opens in a new tab). Nếu chúng ta muốn cho phép các giao dịch không cần ether với token của mình, chúng ta cần sử dụng _msgSender(). Nó trả về msg.sender cho các giao dịch bình thường, nhưng đối với các giao dịch không cần ether nó trả về người ký ban đầu chứ không phải hợp đồng đã chuyển tiếp thông điệp.

Các hàm hạn mức

Đây là các hàm triển khai chức năng hạn mức: allowance, approve, transferFrom_approve. Ngoài ra, bản triển khai OpenZeppelin vượt xa tiêu chuẩn cơ bản để bao gồm một số tính năng cải thiện bảo mật: increaseAllowancedecreaseAllowance.

Hàm allowance

    /**
     * @dev Xem {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

Hàm allowance cho phép mọi người kiểm tra bất kỳ hạn mức nào.

Hàm approve

    /**
     * @dev Xem {IERC20-approve}.
     *
     * Yêu cầu:
     *
     * - `spender` không thể là địa chỉ zero.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {

Hàm này được gọi để tạo một hạn mức. Nó tương tự như hàm transfer ở trên:

  • Hàm chỉ gọi một hàm nội bộ (trong trường hợp này là _approve) thực hiện công việc thực sự.
  • Hàm trả về true (nếu thành công) hoặc hoàn nguyên (nếu không).

 

        _approve(_msgSender(), spender, amount);
        return true;
    }

Chúng ta sử dụng các hàm nội bộ để giảm thiểu số lượng vị trí xảy ra thay đổi trạng thái. Bất kỳ hàm nào làm thay đổi trạng thái đều là một rủi ro bảo mật tiềm ẩn cần được kiểm toán bảo mật. Bằng cách này, chúng ta có ít cơ hội làm sai hơn.

Hàm transferFrom

Đây là hàm mà người chi tiêu gọi để chi tiêu một hạn mức. Điều này yêu cầu hai thao tác: chuyển số lượng đang được chi tiêu và giảm hạn mức đi số lượng đó.

 

Lệnh gọi hàm a.sub(b, "message") thực hiện hai việc. Đầu tiên, nó tính toán a-b, đây là hạn mức mới. Thứ hai, nó kiểm tra xem kết quả này có âm hay không. Nếu nó âm, lệnh gọi sẽ hoàn nguyên với thông điệp được cung cấp. Lưu ý rằng khi một lệnh gọi hoàn nguyên, bất kỳ quá trình xử lý nào được thực hiện trước đó trong lệnh gọi đó đều bị bỏ qua, vì vậy chúng ta không cần phải hoàn tác _transfer.

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

Các bổ sung an toàn của OpenZeppelin

Thật nguy hiểm khi đặt một hạn mức khác không thành một giá trị khác không khác, bởi vì bạn chỉ kiểm soát thứ tự các giao dịch của riêng mình, không phải của bất kỳ ai khác. Hãy tưởng tượng bạn có hai người dùng, Alice ngây thơ và Bill không trung thực. Alice muốn một số dịch vụ từ Bill, mà cô ấy nghĩ là có giá năm token - vì vậy cô ấy cung cấp cho Bill một hạn mức là năm token.

Sau đó, một cái gì đó thay đổi và giá của Bill tăng lên mười token. Alice, người vẫn muốn dịch vụ, gửi một giao dịch đặt hạn mức của Bill thành mười. Ngay khi Bill nhìn thấy giao dịch mới này trong bể giao dịch, anh ta gửi một giao dịch chi tiêu năm token của Alice và có giá gas cao hơn nhiều để nó sẽ được khai thác nhanh hơn. Bằng cách đó, Bill có thể chi tiêu năm token đầu tiên và sau đó, khi hạn mức mới của Alice được khai thác, chi tiêu thêm mười token nữa với tổng giá là mười lăm token, nhiều hơn mức Alice định ủy quyền. Kỹ thuật này được gọi là chạy trước (opens in a new tab)

Giao dịch của AliceNonce của AliceGiao dịch của BillNonce của BillHạn mức của BillTổng thu nhập của Bill từ Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Để tránh vấn đề này, hai hàm này (increaseAllowancedecreaseAllowance) cho phép bạn sửa đổi hạn mức theo một số lượng cụ thể. Vì vậy, nếu Bill đã chi tiêu năm token, anh ta sẽ chỉ có thể chi tiêu thêm năm token nữa. Tùy thuộc vào thời gian, có hai cách điều này có thể hoạt động, cả hai đều kết thúc với việc Bill chỉ nhận được mười token:

A:

Giao dịch của AliceNonce của AliceGiao dịch của BillNonce của BillHạn mức của BillTổng thu nhập của Bill từ Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Giao dịch của AliceNonce của AliceGiao dịch của BillNonce của BillHạn mức của BillTổng thu nhập của Bill từ Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010

Hàm a.add(b) là một phép cộng an toàn. Trong trường hợp khó xảy ra là a+b>=2^256, nó không bị vòng lại theo cách mà phép cộng bình thường làm.

Các hàm sửa đổi thông tin token

Đây là bốn hàm thực hiện công việc thực tế: _transfer, _mint, _burn_approve.

Hàm _transfer

Hàm này, _transfer, chuyển token từ tài khoản này sang tài khoản khác. Nó được gọi bởi cả transfer (đối với các khoản chuyển từ tài khoản của chính người gửi) và transferFrom (để sử dụng hạn mức để chuyển từ tài khoản của người khác).

 

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

Không ai thực sự sở hữu địa chỉ zero trong Ethereum (nghĩa là không ai biết khóa riêng tư có khóa công khai khớp được chuyển đổi thành địa chỉ zero). Khi mọi người sử dụng địa chỉ đó, nó thường là một lỗi phần mềm - vì vậy chúng ta sẽ thất bại nếu địa chỉ zero được sử dụng làm người gửi hoặc người nhận.

 

        _beforeTokenTransfer(sender, recipient, amount);

Có hai cách để sử dụng hợp đồng này:

  1. Sử dụng nó làm mẫu cho mã của riêng bạn
  2. Kế thừa từ nó (opens in a new tab) và chỉ ghi đè những hàm mà bạn cần sửa đổi

Phương pháp thứ hai tốt hơn nhiều vì mã ERC-20 của OpenZeppelin đã được kiểm toán và cho thấy là an toàn. Khi bạn sử dụng tính kế thừa rõ ràng là bạn sửa đổi những hàm nào và để tin tưởng hợp đồng của bạn, mọi người chỉ cần kiểm toán những hàm cụ thể đó.

Thường rất hữu ích khi thực hiện một hàm mỗi khi token đổi chủ. Tuy nhiên, _transfer là một hàm rất quan trọng và có thể viết nó một cách không an toàn (xem bên dưới), vì vậy tốt nhất là không nên ghi đè nó. Giải pháp là _beforeTokenTransfer, một hàm hook (opens in a new tab). Bạn có thể ghi đè hàm này và nó sẽ được gọi trên mỗi lần chuyển.

 

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

Đây là những dòng thực sự thực hiện việc chuyển. Lưu ý rằng không có gì ở giữa chúng và chúng ta trừ số lượng đã chuyển khỏi người gửi trước khi thêm nó vào người nhận. Điều này rất quan trọng vì nếu có một lệnh gọi đến một hợp đồng khác ở giữa, nó có thể đã được sử dụng để gian lận hợp đồng này. Bằng cách này, việc chuyển là nguyên tử, không có gì có thể xảy ra ở giữa nó.

 

        emit Transfer(sender, recipient, amount);
    }

Cuối cùng, phát ra một sự kiện Transfer. Các hợp đồng thông minh không thể truy cập các sự kiện, nhưng mã chạy bên ngoài chuỗi khối có thể lắng nghe các sự kiện và phản ứng với chúng. Ví dụ, một ví có thể theo dõi khi chủ sở hữu nhận được nhiều token hơn.

Các hàm _mint và _burn

Hai hàm này (_mint_burn) sửa đổi tổng nguồn cung token. Chúng là nội bộ và không có hàm nào gọi chúng trong hợp đồng này, vì vậy chúng chỉ hữu ích nếu bạn kế thừa từ hợp đồng và thêm logic của riêng bạn để quyết định trong những điều kiện nào sẽ đúc token mới hoặc đốt những token hiện có.

LƯU Ý: Mỗi token ERC-20 có logic nghiệp vụ riêng quy định việc quản lý token. Ví dụ, một hợp đồng nguồn cung cố định có thể chỉ gọi _mint trong hàm khởi tạo và không bao giờ gọi _burn. Một hợp đồng bán token sẽ gọi _mint khi nó được thanh toán và có lẽ sẽ gọi _burn vào một thời điểm nào đó để tránh lạm phát phi mã.

Đảm bảo cập nhật _totalSupply khi tổng số token thay đổi.

 

Hàm _burn gần như giống hệt với _mint, ngoại trừ việc nó đi theo hướng ngược lại.

Hàm _approve

Đây là hàm thực sự chỉ định các hạn mức. Lưu ý rằng nó cho phép chủ sở hữu chỉ định một hạn mức cao hơn số dư hiện tại của chủ sở hữu. Điều này là ổn vì số dư được kiểm tra tại thời điểm chuyển, khi nó có thể khác với số dư khi hạn mức được tạo.

 

Phát ra một sự kiện Approval. Tùy thuộc vào cách ứng dụng được viết, hợp đồng người chi tiêu có thể được thông báo về sự chấp thuận bởi chủ sở hữu hoặc bởi một máy chủ lắng nghe các sự kiện này.

        emit Approval(owner, spender, amount);
    }

Sửa đổi biến Decimals

Hàm này sửa đổi biến _decimals được sử dụng để cho giao diện người dùng biết cách diễn giải số lượng. Bạn nên gọi nó từ hàm khởi tạo. Sẽ là không trung thực nếu gọi nó ở bất kỳ thời điểm nào sau đó và các ứng dụng không được thiết kế để xử lý nó.

Hooks

Đây là hàm hook được gọi trong quá trình chuyển. Nó trống ở đây, nhưng nếu bạn cần nó làm điều gì đó, bạn chỉ cần ghi đè nó.

Kết luận

Để ôn lại, đây là một số ý tưởng quan trọng nhất trong hợp đồng này (theo ý kiến của tôi, ý kiến của bạn có thể khác):

  • Không có bí mật nào trên chuỗi khối. Bất kỳ thông tin nào mà một hợp đồng thông minh có thể truy cập đều có sẵn cho toàn thế giới.
  • Bạn có thể kiểm soát thứ tự các giao dịch của riêng mình, nhưng không phải khi giao dịch của người khác xảy ra. Đây là lý do tại sao việc thay đổi một hạn mức có thể nguy hiểm, bởi vì nó cho phép người chi tiêu chi tiêu tổng của cả hai hạn mức.
  • Các giá trị của kiểu uint256 bị vòng lại. Nói cách khác, 0-1=2^256-1. Nếu đó không phải là hành vi mong muốn, bạn phải kiểm tra nó (hoặc sử dụng thư viện SafeMath để làm điều đó cho bạn). Lưu ý rằng điều này đã thay đổi trong Solidity 0.8.0 (opens in a new tab).
  • Thực hiện tất cả các thay đổi trạng thái của một loại cụ thể ở một nơi cụ thể, vì nó giúp việc kiểm toán dễ dàng hơn. Đây là lý do mà chúng ta có, ví dụ, _approve, được gọi bởi approve, transferFrom, increaseAllowancedecreaseAllowance
  • Các thay đổi trạng thái phải là nguyên tử, không có bất kỳ hành động nào khác ở giữa chúng (như bạn có thể thấy trong _transfer). Điều này là do trong quá trình thay đổi trạng thái, bạn có một trạng thái không nhất quán. Ví dụ, giữa thời điểm bạn trừ vào số dư của người gửi và thời điểm bạn cộng vào số dư của người nhận, có ít token tồn tại hơn mức bình thường. Điều này có khả năng bị lạm dụng nếu có các hoạt động giữa chúng, đặc biệt là các lệnh gọi đến một hợp đồng khác.

Bây giờ bạn đã thấy cách hợp đồng ERC-20 của OpenZeppelin được viết và đặc biệt là cách nó được làm cho an toàn hơn, hãy đi và viết các hợp đồng và ứng dụng an toàn của riêng bạn.

Xem tại đây để biết thêm về công việc của tôi (opens in a new tab).