1. Soạn hợp đồng thông minh ERC-20

1.1 Cấu trúc tổng thể của MyERC20

Dưới đây là mã nguồn đầy đủ của tập tin MyERC20.sol. Trong lần triển khai này, hàm constructor gọi _mint để tạo một số lượng token định trước khi triển khai hợp đồng.

pragma solidity ^0.5.0;

/**
 * @dev Giao diện của chuẩn ERC20 theo định nghĩa trong EIP. Không chứa
 * các hàm tùy chọn; xem `ERC20Detailed` để truy cập.
 */
interface IERC20 {
    function totalSupply() external view returns (uint256);

    function balanceOf(address tài khoản) external view returns (uint256);

    function transfer(address recipient, uint256 amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);

    event Approval(address indexed owner, address indexed spender, uint256 value);
}

library SafeMath {
    /**
     * @dev Trả lại tổng của hai số nguyên không dấu, hoàn ngược nếu có
     * tràn số.
     *
     * Tương đương với toán tử `+` trong Solidity.
     *
     * Yêu cầu:
     * - Phép cộng không được tràn số.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: phép cộng tràn số");

        return c;
    }

    /**
     * @dev Trả về hiệu của hai số nguyên không dấu, hoàn ngược nếu có
     * tràn số (khi kết quả là số âm).
     *
     * Tương đương với toán tử `-` trong Solidity.
     *
     * Yêu cầu:
     * - Phép trừ không được tràn số.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: phép trừ tràn số");
        uint256 c = a - b;

        return c;
    }

    /**
     * @dev Trả về tích của hai số nguyên không dấu, hoàn ngược nếu có
     * tràn số.
     *
     * Tương đương với toán tử `*` trong Solidity.
     *
     * Yêu cầu:
     * - Phép nhân không được tràn số.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Tối ưu hóa phí ga: cách này rẻ hơn yêu cầu 'a' thay vì 0, nhưng
        // lợi ích sẽ mất nếu 'b' cũng kiểm thử.
        // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: phép nhân tràn số");

        return c;
    }

    /**
     * @dev Trả về giá trị nguyên của phép chia hai số nguyên không dấu. Hoàn ngược nếu
     * chia cho 0. Kết quả được làm tròn về 0.
     *
     * Tương đương với toán tử `/` trong Solidity. Lưu ý: hàm này sử dụng một
     * mã vận hành `revert` (giữ lại toàn bộ gas còn lại) trong khi Solidity
     *  sử dụng một mã vận hành không hợp lệ để hoàn ngược (tiêu thụ toàn bộ gas còn lại).
     *
     * Yêu cầu:
     * - Số chia không được bằng 0.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // Solidity chỉ tự động kiểm tra khi chia cho 0
        require(b > 0, "SafeMath: số chia bằng 0");
        uint256 c = a / b;
        // assert(a == b * c + a % b); // Không có trường hợp nào mà điều này không đúng

        return c;
    }

    /**
     * @dev Trả về phần dư của phép chia hai số nguyên không dấu. (unsigned integer modulo),
     * Hoàn ngược khi chia cho 0.
     *
     * Tương đương với toán tử `%` trong Solidity. Hàm này sử dụng một
     * mã vận hành `revert` (giữ lại toàn bộ gas còn lại) trong khi Solidity
     *  sử dụng một mã vận hành không hợp lệ để hoàn ngược (tiêu thụ toàn bộ gas còn lại).
     *
     * Yêu cầu:
     * - Số chia không được bằng 0.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b != 0, "SafeMath: số chia bằng 0");
        return a % b;
    }
}

/**
 * @dev Triển khai giao diện `IERC20`.
 *
 * Lần triển khai này không phụ thuộc vào cách tạo token. Điều này có nghĩa là
 * cơ chế cung cấp phải được thêm vào một hợp đồng phái sinh thông qua `_mint`.
 * Để biết cơ chế tổng quát, xem `ERC20Mintable`.
 *
 * *Để biết thêm chi tiết, hãy xem hướng dẫn của chúng tôi [Cách triển khai
 * cơ chế cung cấp](https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226).*
 *
 * Chúng tôi đã tuân thủ các hướng dẫn tổng quát của OpenZeppelin: các hàm hoàn ngược thay vì
 * trả về `false` khi thất bại. Tuy nhiên, hành vi này khá phổ biến
 * và không xung đột với các kỳ vọng của ứng dụng ERC20.
 *
 * Ngoài ra, sự kiện `Approval` được kích hoạt khi gọi `transferFrom`.
 * Điều này cho phép ứng dụng xây dựng lại ủy quyền cho tất cả các tài khoản
 * chỉ bằng cách nghe các sự kiện này. Các lần triển khai khác của EIP có thể không phát ra
 * các sự kiện này vì không yêu cầu trong tiêu chuẩn kỹ thuật.
 *
 * Cuối cùng, hai hàm không chuẩn `decreaseAllowance` và `increaseAllowance`
 * đã được thêm vào để giảm thiểu các vấn đề phổ biến liên quan đến việc
 * thiết lập ủy quyền. Xem `IERC20.approve`.
 */
contract MyERC20 is IERC20 {
    using SafeMath for uint256;

    mapping (address => uint256) private _balances;

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

    // NOTE Start of https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC20/ERC20Detailed.sol
    string private _name;
    string private _symbol;
    uint8 private _decimals;

    constructor (string memory name, string memory symbol, uint8 decimals) public {
        _name = name;
        _symbol = symbol;
        _decimals = decimals;

        _mint(msg.sender, 100000 * 10 ** uint256(decimals)); // CAUTION!
    }

    /**
     * @dev Trả về tên của token.
     */
    function name() public view returns (string memory) {
        return _name;
    }

    /**
     * @dev Trả về ký hiệu của token, thường là phiên bản ngắn gọn của
     * tên.
     */
    function symbol() public view returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Trả về số chữ số thập phân được sử dụng để hiển thị cho người dùng.
     * Ví dụ, nếu `decimals` là `2`, số dư của token `505` sẽ
     * được hiển thị cho người dùng dưới dạng `5,05` (`505 / 10 ** 2`).
     *
     * Các token thường chọn giá trị 18, tương tự mối quan hệ giữa
     * Ether và Wei.
     *
     * > Lưu ý rằng thông tin này chỉ được sử dụng cho _mục đích_ hiển thị: nó
     * không ảnh hưởng đến bất kỳ phép tính nào của hợp đồng, bao gồm
     * `IERC20.balanceOf` và `IERC20.transfer`.
     */
    function decimals() public view returns (uint8) {
        return _decimals;
    }
    // NOTE End of https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC20/ERC20Detailed.sol

    uint256 private _totalSupply;

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

    /**
     * @dev Xem `IERC20.balanceOf`.
     */
    function balanceOf(address tài khoản) public view returns (uint256) {
        return _balances[tài khoản];
    }

    /**
     * @dev Xem `IERC20.transfer`.
     *
     * Yêu cầu:
     *
     * - `recipient` không được là địa chỉ không hợp lệ.
     * - người gọi phải có số dư tối thiểu là `amount`.
     */
    function transfer(address recipient, uint256 amount) public returns (bool) {
        _transfer(msg.sender, recipient, amount);
        return true;
    }

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

    /**
     * @dev Xem `IERC20.approve`.
     *
     * Yêu cầu:
     *
     * - `spender` không được là địa chỉ không hợp lệ.
     */
    function approve(address spender, uint256 value) public returns (bool) {
        _approve(msg.sender, spender, value);
        return true;
    }

    /**
     * @dev Xem `IERC20.transferFrom`.
     *
     * Kích hoạt sự kiện `Approval` để chỉ thị phân quyền cập nhật. Điều này không
     * bắt buộc theo EIP. Xem phần lưu ý ở đầu mục ERC20`;
     *
     * Yêu cầu:
     * - `sender` và `recipient` không được là địa chỉ không hợp lệ.
     * - `sender` phải có số dư tối thiểu là `value`.
     * - Người gọi phải có phân quyền cho token của `sender` ít nhất là
     * `amount`.
     */
    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        _transfer(sender, recipient, amount);
        _approve(sender, msg.sender, _allowances[sender][msg.sender].sub(amount));
        return true;
    }

    /**
     * @dev Tăng không đáng kể tỷ lệ phân quyền được người gọi cấp cho `spender`.
     *
     * Đây là một phương pháp thay thế cho `approve` có thể được sử dụng để giảm thiểu
     * các vấn đề được mô tả trong `IERC20.approve`.
     *
     * Kích hoạt sự kiện `Approval` để chỉ thị phân quyền cập nhật.
     *
     * Yêu cầu:
     *
     * - `spender` không được là địa chỉ không hợp lệ.
     */
    function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
        _approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue));
        return true;
    }

    /**
     * @dev Tăng tỷ lệ phân quyền được người gọi cấp cho `spender` một cách an toàn.
     *
     * Đây là một phương pháp thay thế cho `approve` có thể được sử dụng để giảm thiểu
     * các vấn đề được mô tả trong `IERC20.approve`.
     *
     * Kích hoạt sự kiện `Approval` để chỉ thị phân quyền cập nhật.
     *
     * Yêu cầu:
     *
     * - `spender` không được là địa chỉ không hợp lệ.
     * - `spender` phải có giá trị phân quyền cho người gọi ít nhất là
     * `subtractedValue`.
     */
    function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
        _approve(msg.sender, spender, _allowances[msg.sender][spender].sub(subtractedValue));
        return true;
    }

    /**
     * @dev Di chuyển `amount` token từ `sender` sang `recipient`.
     *
     * Đây là hàm nội bộ tương đương với `transfer` và có thể được dùng để
     * ví dụ, triển khai phí tự động cho token, các cơ chế xử phạt, v.v.
     *
     * Kích hoạt sự kiện `Transfer`.
     *
     * Yêu cầu:
     *
     * - `sender` không được là địa chỉ không hợp lệ.
     * - `recipient` không được là địa chỉ không hợp lệ.
     * - `sender` phải có số dư tối thiểu là `amount`.
     */
    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: chuyển từ địa chỉ không hợp lệ");
        require(recipient != address(0), "ERC20: chuyển đến địa chỉ không hợp lệ");

        _balances[sender] = _balances[sender].sub(amount);
        _balances[recipient] = _balances[recipient].add(amount);
        emit Transfer(sender, recipient, amount);
    }

    /** @dev Tạo `amount` token và gán chúng cho `tài khoản`, tăng
     * tổng cung.
     *
     * Kích hoạt sự kiện `Transfer` với `from` được đặt thành địa chỉ không hợp lệ.
     *
     * Yêu cầu:
     *
     * - `to` không được là địa chỉ không hợp lệ.
     */
    function _mint(address tài khoản, uint256 amount) internal {
        require(tài khoản != address(0), "ERC20: tạo đến địa chỉ không hợp lệ");

        _totalSupply = _totalSupply.add(amount);
        _balances[tài khoản] = _balances[tài khoản].add(amount);
        emit Transfer(address(0), tài khoản, amount);
    }

     /**
     * @dev Hủy `amount` token từ `tài khoản`, giảm
     * tổng cung.
     *
     * Kích hoạt sự kiện `Transfer` với `to` được thiết lập thành địa chỉ không hợp lệ.
     *
     * Yêu cầu:
     *
     * - `tài khoản` không được là địa chỉ không hợp lệ.
     * - `tài khoản` phải có ít nhất `amount` token.
     */
    function _burn(address tài khoản, uint256 value) internal {
        require(tài khoản != address(0), "ERC20: đốt từ địa chỉ không hợp lệ");

    _balances[tài khoản] = _balances[tài khoản].sub(value);
        _totalSupply = _totalSupply.sub(value);
        emit Transfer(tài khoản, address(0), value);
    }

    /**
     * @dev Thiết lập `amount` làm giá trị phân quyền của `spender` cho token của `owner`.
     *
     * Đây là hàm nội bộ tương đương với `approve` và có thể được dùng để
     * ví dụ, thiết lập cơ chế phân quyền tự động cho các hệ thống phụ nhất định, v.v.
     *
     * Kích hoạt sự kiện `Approval`.
     *
     * Yêu cầu:
     *
     * - `owner` không được là địa chỉ không hợp lệ.
     * - `spender` không được là địa chỉ không hợp lệ.
     */
    function _approve(address owner, address spender, uint256 value) internal {
        require(owner != address(0), "ERC20: duyệt đến địa chỉ không hợp lệ");
        require(spender != address(0), "ERC20: duyệt đến địa chỉ không hợp lệ");

        _allowances[owner][spender] = value;
        emit Approval(owner, spender, value);
    }

    /**
     * @dev Hủy `amount` token từ `tài khoản`.`amount` sau đó được trừ khỏi
     * giá trị phân quyền của người gọi.
     *
     * Xem `_burn` và `_approve`.
     */
    function _burnFrom(address tài khoản, uint256 amount) internal {
        _burn(tài khoản, amount);
        _approve(tài khoản, msg.sender, _allowances[tài khoản][msg.sender].sub(amount));
    }
}

MyERC20.sol bao gồm một giao diện IERC20, một thư viện SafeMath và một hợp đồng MyERC20, triển khai giao diện IERC20.

  • Giao diện IERC20 xác định giao diện bắt buộc được mô tả trong tiêu chuẩn kỹ thuật của ERC-20.

  • Thư viện SafeMath xác định các lớp bọc (wrapper) cho các phép toán số học trong Solidity, kèm theo kiểm tra tràn số để đảm bảo tính toàn vẹn của phép tính với kiểu uint256 trong Solidity.

  • MyERC20 triển khai các giao diện IERC20 và cũng xác định ba phương pháp tùy chọn được mô tả trong tiêu chuẩn kỹ thuật của ERC-20.

    • Ngoài ERC20, hàm constructor cũng được xác định và hàm tạo này được sử dụng để đặt tên và ký hiệu cho một token ERC20 mới và để tạo một số lượng token định trước. constructor được gọi một lần trong lần triển khai đầu tiên.

1.2 Tìm hiểu một số phương pháp quan trọng

Hãy tìm hiểu chi tiết một số phương pháp quan trọng.

(1) function balanceOf(address tài khoản) external view returns (uint256);

balanceOf là phương pháp bắt buộc của ERC-20. balanceOf trả về số dư của địa chỉ đã cho.

    function balanceOf(address tài khoản) public view returns (uint256) {
        return _balances[tài khoản];
    }

balanceOf chỉ trả về giá trị khóa tài khoản được lưu trong _balances là kiểu mapping (address => uint256) như dưới đây.

    mapping (address => uint256) private _balances;

Nếu không có khóa tài khoản trong _balances thì giá trị trả về chỉ là 0.

(2) function transfer(address recipient, uint256 amount) external returns (bool);

transfer là phương pháp bắt buộc của ERC-20. transfer chuyển amount token cho recipient và hàm MUST kích hoạt sự kiện Transfer. Hàm SHOULD thông báo lỗi ngoại lệ nếu số dư tài khoản của người gọi thông báo không có đủ token để chi tiêu.

transfer chỉ gọi phương pháp nội bộ _transfer thực hiện việc chuyển giao thực tế và tạo sự kiện như sau.

    function transfer(address recipient, uint256 amount) public returns (bool) {
        _transfer(msg.sender, recipient, amount);
        return true;
    }

_transfer triển khai hành vi thực tế của phương pháp transfer trong tiêu chuẩn ERC-20.

Ngoài ra, hàm này ngăn chặn việc gửi token từ hoặc đến địa chỉ không hợp lệ bằng cách sử dụng lệnh require như sau.

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

        _balances[sender] = _balances[sender].sub(amount);
        _balances[recipient] = _balances[recipient].add(amount);
        emit Transfer(sender, recipient, amount);
    }

(3) function approve(address spender, uint256 amount) external returns (bool);

approve là phương pháp bắt buộc của ERC-20. Hàm approve cho phép spender được rút tiền nhiều lần từ tài khoản của bạn, với giá trị lên đến amount. Nếu hàm này được gọi nhiều lần, hàm sẽ đơn giản là đặt lại giới hạn ủy quyền với giá trị amount.

approve gọi phương pháp nội bộ _approve để thực hiện hành vi approve thực tế. msg.sender được truyền dưới dạng tài khoản owner.

    function approve(address spender, uint256 value) public returns (bool) {
        _approve(msg.sender, spender, value);
        return true;
    }

    function _approve(address owner, address spender, uint256 value) internal {
        require(owner != address(0), "ERC20: duyệt từ địa chỉ không hợp lệ");
        require(spender != address(0), "ERC20: duyệt đến địa chỉ không hợp lệ");

        _allowances[owner][spender] = value;
        emit Approval(owner, spender, value);
    }

Hàm _approve cập nhật _allowances, đây là một từ điển 2 chiều giữ value được ủy quyền cho spender từ address cụ thể.

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

(4) function _mint(address tài khoản, uint256 amount) internal

_mint không phải là một phần của tiêu chuẩn ERC-20. Tuy nhiên, chúng tôi cần một cách để tạo ra các token ERC-20 mới và đã áp dụng _mint để tạo ra các token mới trong lần triển khai này như sau.

    function _mint(address tài khoản, uint256 amount) internal {
        require(tài khoản != address(0), "ERC20: mint to the zero address");

        _totalSupply = _totalSupply.add(amount);
        _balances[tài khoản] = _balances[tài khoản].add(amount);
        emit Transfer(address(0), tài khoản, amount);
    }

_mint là một phương pháp nội bộ và có thể được gọi bên trong hợp đồng này.

Trong tiêu chuẩn MyERC20.sol, _mint được gọi chỉ một lần từ constructor khi triển khai hợp đồng thông minh để tạo một số lượng token định trước.

Nếu bạn muốn phát hành thêm token sau khi triển khai hợp đồng thông minh, bạn phải giới thiệu một phương pháp công khai mới như mint. Cần CẨN TRỌNG khi triển khai phương pháp này vì chỉ những người dùng được ủy quyền mới có thể tạo token.

Vui lòng xem ví dụ ERC20Mintable.sol của OpenZeppelin để biết thêm chi tiết.

Last updated