+1

WTF Solidity 103

image.png

30. ERC-20

ERC-20 là một tiêu chuẩn áp dụng cho token, được đề xuất vào năm 2015. Hiện nay, ERC-20 gần như được áp dụng cho mọi token được phát hành.

IERC20

IERC20 là interface contract của chuẩn ERC-20, nơi chứa định nghĩa về các sự kiện, hàm cơ bản cần phải có.

Event

    /**
     * @dev Khi 1 giao dịch chuyển token hoàn tất, sự kiện sẽ được kích hoạt.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Khi 1 giao dịch ủy quyền token từ 1 người dùng cho người dùng khác hoàn tất, sự kiện được kích hoạt
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

Các hàm

    /**
     * @dev Trả về tổng số lượng token
     */
    function totalSupply() external view returns (uint256);
    
    /**
     * @dev Truy vấn số dư token của 1 địa chỉ bất kỳ
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Chuyển token với 2 tham số đầu vào là địa chỉ người nhận và số lượng
     *
     * Kiểu trả về dạng bool, phụ thuộc vào trạng thái giao dịch
     *
     * Kích hoạt {Transfer} event khi chuyển thành công
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * Trả về số lượng token mà owner ủy quyền cho spender
     *
     * Khi {approve} or {transferFrom} được gọi,`allowance` sẽ thay đổi.
     */
    function allowance(address owner, address spender) external view returns (uint256);

/**
 * @dev Ủy quyền cho địa chỉ spender 1 lượng token của msg.sender
 *
 * Kích hoạt {Approval} event nếu thành công.
 */
		function approve(address spender, uint256 amount) external returns (bool);

/**
 * Chuyển token từ `from` sang `to`, msg.sender cần có allowance(from, msg.sender) >= amount
 *
 * Kích hoạt {Transfer} event.
 */
		function transferFrom(
		    address from,
		    address to,
		    uint256 amount
		) external returns (bool);

Implement ERC-20

Dựa interface IERC20, chúng ta sẽ phải triển khai thêm logic để tạo ra một token ERC-20 hoàn chỉnh.

Biến trạng thái

Chúng ta sẽ cần có những biến, mapping lưu trữ các thông tin về token như name, symbol, số dư, ...

		// mapping lưu trữ số dư của các địa chỉ
    mapping(address => uint256) public override balanceOf;
		
		// mapping lưu trữ số lượng token được ủy quyền
    mapping(address => mapping(address => uint256)) public override allowance;

    uint256 public override totalSupply;   // total supply of the token

    string public name;   // Tên Token
    string public symbol;  // Ký hiệu token
    
    uint8 public decimals = 18; // đơn vị chia nhỏ nhất gồm bao nhiêu số 0 sau dấu phẩy

Hàm

		// Khởi tạo tên và ký hiệu token
    constructor(string memory name_, string memory symbol_){
        name = name_;
        symbol = symbol_;
    }
    
    // Chuyển token và cập nhật lại số dư của các địa chỉ
    function transfer(address recipient, uint amount) external override returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }
    
    // Ủy quyền token, gán giá trị mapping bằng lượng amount truyền vào
    function approve(address spender, uint amount) external override returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }
    
    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external override returns (bool) {
		    // Kiểm tra xem sender có ủy quyền cho msg.sender 1 lượng token đủ không ?
		    require(allowance[sender][msg.sender] >= amount);
				
				// Cập nhật lại allowance và số dư
        allowance[sender][msg.sender] -= amount;
        balanceOf[sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    }
    
    // Đúc thêm token (thường sẽ giới hạn chỉ có thể được gọi bởi admin hay operator)
    function mint(uint amount) external {
        balanceOf[msg.sender] += amount;
        totalSupply += amount;
        emit Transfer(address(0), msg.sender, amount);
    }
    
    // Đốt token (Cũng nên có modifier để giới hạn địa chỉ có thể gọi)
    function burn(uint amount) external {
        balanceOf[msg.sender] -= amount;
        totalSupply -= amount;
        emit Transfer(msg.sender, address(0), amount);
    }

Deploy

Khi deploy, chúng ta cần truyền vào luôn tên và ký hiệu của token.

Deploying the contract

Ban đầu sẽ chưa có bất kỳ token nào, chúng ta sẽ cần đúc mới. Tại đây ta sẽ thấy event Transfer được kích hoạt

Minting tokens

Kiểm tra số dư Check Balance

31. Faucet

Khi phát hành mới token, faucet token là một cách để quảng bá, phổ biến tới cộng đồng. Ở phần trước chúng ta đã cùng tìm hiểu cách viết token theo chuẩn ERC-20, bây giờ hãy cùng triển khai contract cho phép người dùng faucet token ERC-20 đó.

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

import "./IERC20.sol"; //import IERC20

contract Faucet {

    uint256 public amountAllowed = 100; // số lượng token nhận được trong 1 lần faucet
    address public tokenContract;   // địa chỉ của token contract
    mapping(address => bool) public requestedAddress;  // mapping quản lý xem địa chỉ người dùng đã faucet hay chưa, tránh việc 1 tài khoản faucet nhiều lần

    // Event SendToken
    event SendToken(address indexed Receiver, uint256 indexed Amount); 

    // Gán địa chỉ token
    constructor(address _tokenContract) {
        tokenContract = _tokenContract; // set token contract
    }

    // Người dùng gọi đến để faucet token
    function requestTokens() external {
        require(requestedAddress[msg.sender] == false, "Can't Request Multiple Times!")
        IERC20 token = IERC20(tokenContract); // Tạo IERC20 contract object
        require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // Faucet is empty

        token.transfer(msg.sender, amountAllowed); // Gửi token cho người dùng
        requestedAddress[msg.sender] = true; // Gán bằng true khi địa chỉ đã faucet
        
        emit SendToken(msg.sender, amountAllowed); // Kích hoạt SendToken event
    }
}

32. Airdrop

Airdrop cũng là một hình thức giúp quảng bá, phổ biến dự án đến nhiều người dùng. Thông thường, nhà phát hành sẽ có thể yêu cầu người dùng làm một số nhiệm vụ (như like, share, tham gia nhóm chat ...) để có thể đăng ký nhận miễn phí token của dự án.

pragma solidity ^0.8.4;

import "./IERC20.sol"; //import IERC20

/// @notice chuyển ERC20 tokens cho nhiều địa chỉ
contract Airdrop {
		// sum function for arrays
    function getSum(uint256[] calldata _arr) public pure returns(uint sum) {
        for(uint i = 0; i < _arr.length; i++)
            sum = sum + _arr[i];
    }
    
    /// @param _token: address ERC20 token
    /// @param _addresses: Danh sách các địa chỉ nhận airdrop
    /// @param _amounts: Số lượng token từng địa chỉ sẽ nhận được
    function multiTransferToken(
        address _token,
        address[] calldata _addresses,
        uint256[] calldata _amounts
        ) external {
        require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
        IERC20 token = IERC20(_token);
        uint _amountSum = getSum(_amounts); 
        //Kiểm tra: Do sẽ sử dụng hàm transferFrom, ta sẽ phải kiểm tra xem số lượng token mà contract được ủy quyền có đủ ko, để có thể gửi token từ contract => người dùng
        require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token");
        
        // chạy vòng lặp để chuyển token
        for (uint256 i; i < _addresses.length; i++) {
            token.transferFrom(msg.sender, _addresses[i], _amounts[i]);
        }
    }

Một phương pháp triển khai Airdrop chi tiết hơn, sử dụng cây Merkle. Các bạn có thể tham khảo tại bài viết Merkle Airdrop: Giải pháp Airdrop cho các đợt phát hành token

33. ERC-721

EIP và ERC

EIP là viết tắt của Ethereum Improvement Proposals bao gồm tập hợp nhiều đề xuất cải tiến mạng Ethereum, được xếp sắp và phân biệt bởi con số theo sau như EIP165, EIP721 ...

ERC là viết tắt của Ethereum Request For Comment bao gồm các tiêu chuẩn, giao thức của Ethereum ở tầng ứng dụng. Ví dụ như tiêu chuẩn token (ERC20, ERC721), URI paradigms (ERC67) ...

Các đề xuất EIP có thể là bất cứ nội dung nào giúp cải tiến Etherum trong khi đó ERC chỉ gồm các đề xuất ở tầng ứng dụng (application-level). Vậy có thể kết luận rằng EIP sẽ bao gồm ERC .

IERC721

ERC-721 là chuẩn phổ biến dành cho NFT, khác với ERC-20 khi mỗi token có giá trị tương đương và có thể trao đổi cho nhau. Mỗi token theo chuẩn ERC-721 là duy nhất, riêng biệt. Chính tính chất này giúp tạo nên các vật phẩm NFT có tính sưu tầm.

/**
 * @dev ERC721 interface.
 */
interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    // ủy quyền tất cả token của owner cho operator
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    function balanceOf(address owner) external view returns (uint256 balance);

    function ownerOf(uint256 tokenId) external view returns (address owner);

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    function approve(address to, uint256 tokenId) external;

    function setApprovalForAll(address operator, bool _approved) external;

    function getApproved(uint256 tokenId) external view returns (address operator);

    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

IERC721Receiver

Trong trường hợp địa chỉ nhận token không có chức năng của ERC721, token sẽ bị kẹt tại đó và không thể chuyển đi đâu khác.

interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint tokenId,
        bytes calldata data
    ) external returns (bytes4);
}
		// Nếu địa chỉ nhận là contract thì kiểm tra xem có `onERC721Received` không ?
    function _checkOnERC721Received(
        address from,
        address to,
        uint tokenId,
        bytes memory _data
    ) private returns (bool) {
        if (to.isContract()) {
            return
                IERC721Receiver(to).onERC721Received(
                    msg.sender,
                    from,
                    tokenId,
                    _data
                ) == IERC721Receiver.onERC721Received.selector;
        } else {
            return true;
        }
    }

IERC721Metadata

// Truy vấn thông tin về token như tên, ký hiệu, tokenURI
interface IERC721Metadata is IERC721 {
    function name() external view returns (string memory);

    function symbol() external view returns (string memory);

    function tokenURI(uint256 tokenId) external view returns (string memory);
}

ERC721 contract

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

import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./Address.sol";
import "./String.sol";

contract ERC721 is IERC721, IERC721Metadata{
    using Address for address; // sử dụng thư viện Address để có thể dùng hàm isContract kiểm tra xem 1 địa chỉ có là địa chỉ contract không ?
    using Strings for uint256; // dùng thư viện String

    // Token name
    string public override name;
    // Token symbol
    string public override symbol;
    // Mapping token ID với địa chỉ sở hữu token
    mapping(uint => address) private _owners;
    // Mapping owner address to balance of the token
    mapping(address => uint) private _balances;
    // Mapping quản lý số lượng token của mỗi địa chỉ
    mapping(uint => address) private _tokenApprovals;
    //  Mapping lưu trữ trạng thái approve token
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    /**
     * Gán tên, ký hiệu token khi deploy
     */
    constructor(string memory name_, string memory symbol_) {
        name = name_;
        symbol = symbol_;
    }

    // Implements the supportsInterface of IERC165
    function supportsInterface(bytes4 interfaceId)
        external
        pure
        override
        returns (bool)
    {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC165).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId;
    }

    // Truy vấn số lượng token
    function balanceOf(address owner) external view override returns (uint) {
        require(owner != address(0), "owner = zero address");
        return _balances[owner];
    }

    // Trả về địa chỉ sở hữu tokenId
    function ownerOf(uint tokenId) public view override returns (address owner) {
        owner = _owners[tokenId];
        require(owner != address(0), "token doesn't exist");
    }

    // Kiểm tra approveAll
    function isApprovedForAll(address owner, address operator)
        external
        view
        override
        returns (bool)
    {
        return _operatorApprovals[owner][operator];
    }

    // approveAll
    function setApprovalForAll(address operator, bool approved) external override {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    // Truy vấn địa chỉ được ủy quyền với tokenId
    function getApproved(uint tokenId) external view override returns (address) {
        require(_owners[tokenId] != address(0), "token doesn't exist");
        return _tokenApprovals[tokenId];
    }
     
    function _approve(
        address owner,
        address to,
        uint tokenId
    ) private {
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    function approve(address to, uint tokenId) external override {
        address owner = _owners[tokenId];
        require(
            msg.sender == owner || _operatorApprovals[owner][msg.sender],
            "not owner nor approved for all"
        );
        _approve(owner, to, tokenId);
    }

    function _isApprovedOrOwner(
        address owner,
        address spender,
        uint tokenId
    ) private view returns (bool) {
        return (spender == owner ||
            _tokenApprovals[tokenId] == spender ||
            _operatorApprovals[owner][spender]);
    }
    
    function _transfer(
        address owner,
        address from,
        address to,
        uint tokenId
    ) private {
        require(from == owner, "not owner");
        require(to != address(0), "transfer to the zero address");

        _approve(owner, address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }
    
    function transferFrom(
        address from,
        address to,
        uint tokenId
    ) external override {
        address owner = ownerOf(tokenId);
        require(
            _isApprovedOrOwner(owner, msg.sender, tokenId),
            "not owner nor approved"
        );
        _transfer(owner, from, to, tokenId);
    }
    
    function _safeTransfer(
        address owner,
        address from,
        address to,
        uint tokenId,
        bytes memory _data
    ) private {
        _transfer(owner, from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver");
    }

    function safeTransferFrom(
        address from,
        address to,
        uint tokenId,
        bytes memory _data
    ) public override {
        address owner = ownerOf(tokenId);
        require(
            _isApprovedOrOwner(owner, msg.sender, tokenId),
            "not owner nor approved"
        );
        _safeTransfer(owner, from, to, tokenId, _data);
    }

    function safeTransferFrom(
        address from,
        address to,
        uint tokenId
    ) external override {
        safeTransferFrom(from, to, tokenId, "");
    }

    function _mint(address to, uint tokenId) internal virtual {
        require(to != address(0), "mint to zero address");
        require(_owners[tokenId] == address(0), "token already minted");

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    function _burn(uint tokenId) internal virtual {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner, "not owner of token");

        _approve(owner, address(0), tokenId);

        _balances[owner] -= 1;
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);
    }
  
    function _checkOnERC721Received(
        address from,
        address to,
        uint tokenId,
        bytes memory _data
    ) private returns (bool) {
        if (to.isContract()) {
            return
                IERC721Receiver(to).onERC721Received(
                    msg.sender,
                    from,
                    tokenId,
                    _data
                ) == IERC721Receiver.onERC721Received.selector;
        } else {
            return true;
        }
    }

    /**
     * Đường link chứa metadata của mỗi token (vd như ảnh, mô tả ...)
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_owners[tokenId] != address(0), "Token Not Exist");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
    }

    /**
     * Base URI
     * ví dụ baseURI của bộ sưu tập BAYC's baseURI ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ 
     */
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }
}

Deploy Remix

Chúng ta sẽ cùng deploy thử contract ERC721

How to emphasize NFT information

Đặt tên và ký hiệu NFT là WTF Deploy contract

Đúc tokenId = 0 cho địa chỉ 0x...eddC4 Minting NFTs

Kiểm tra số dư NFT của địa chỉ 0x...eddC4 Querying NFT details Kiểm tra lại xem owner của tokenId = 0 Querying owner details of tokenid

34. Dutch Auction

Dutch auction (tạm dịch là đấu giá kiểu Hà Lan) là một phương thức đấu giá ngược, giá ban đầu được đặt ở mức cao và sau đó giảm dần đến khi có người chấp nhận.

Dutch Auction Trong vũ trụ NFT, nhiều bộ sưu tập được đấu giá theo kiểu này như Azuki hay World of Women

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "../34_ERC721/ERC721.sol";

contract DutchAuction is Ownable, ERC721 {
    uint256 public constant COLLECTOIN_SIZE = 10000; // tổng số NFTs 
    uint256 public constant AUCTION_START_PRICE = 1 ether; // Giá khởi điểm (giá cao nhất)
    uint256 public constant AUCTION_END_PRICE = 0.1 ether; // Giá cuối cùng (giá sàn) 
    uint256 public constant AUCTION_TIME = 10 minutes; // Thời gian đấu giá
    uint256 public constant AUCTION_DROP_INTERVAL = 1 minutes; // Khoảng thời gian mà giá sẽ giảm tiếp khi chưa ai chấp nhận giá hiện tại
    uint256 public constant AUCTION_DROP_PER_STEP =
        (AUCTION_START_PRICE - AUCTION_END_PRICE) /
        (AUCTION_TIME / AUCTION_DROP_INTERVAL); // mức giá giảm mỗi lần

    uint256 public auctionStartTime; // thời gian bắt đầu đấu giá (tính theo timestamp)
    string private _baseTokenURI; // metadata URI
    uint256[] private _allTokens; 

    constructor() ERC721("WTF Dutch Auctoin", "WTF Dutch Auctoin") {
        auctionStartTime = block.timestamp;
    }

    function totalSupply() public view virtual returns (uint256) {
        return _allTokens.length;
    }

    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokens.push(tokenId);
    }
    
	 // Truy vấn giá theo thời gian thực
    function getAuctionPrice()
        public
        view
        returns (uint256)
    {
        if (block.timestamp < auctionStartTime) {
	        return AUCTION_START_PRICE;
        } else if (block.timestamp - auctionStartTime >= AUCTION_TIME) {
	        return AUCTION_END_PRICE;
        } else {
        uint256 steps = (block.timestamp - auctionStartTime) /
            AUCTION_DROP_INTERVAL;
	        return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP);
        }
    }


    /* Khi ai đó thấy giá ok vào chấp nhận mua NFT thì sẽ gọi hàm, gửi kèm 1 lượng ETH
    * quantity: số lượng token muốn mua
    */
    function auctionMint(uint256 quantity) external payable{
        uint256 _saleStartTime = uint256(auctionStartTime)
        
        // Kiểm tra xem đã bắt đầu thời gian đấu giá chưa ?
        require(_saleStartTime != 0 && block.timestamp >= _saleStartTime,"sale has not started yet");
        // Kiểm tra xem số lượng NFT đấu giá có vượt quá số lượng chưa ?
        require(totalSupply() + quantity <= COLLECTOIN_SIZE, "not enough remaining reserved for auction to support desired mint amount"); 

        uint256 totalCost = getAuctionPrice() * quantity; // tính tổng tiền người dùng cần trả
        require(msg.value >= totalCost, "Need to send more ETH."); // kiểm tra số tiền người dùng gửi vào có đủ không ?
        
        // Mint NFT
        for(uint256 i = 0; i < quantity; i++) {
            uint256 mintIndex = totalSupply();
            _mint(msg.sender, mintIndex);
            _addTokenToAllTokensEnumeration(mintIndex);
        }
        // trả lại ETH thừa của người dùng
        if (msg.value > totalCost) {
            payable(msg.sender).transfer(msg.value - totalCost);
        }
    }
    
    function setAuctionStartTime(uint32 timestamp) external onlyOwner {
        auctionStartTime = timestamp;
    }

    // BaseURI
    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }
  
    function setBaseURI(string calldata baseURI) external onlyOwner {
        _baseTokenURI = baseURI;
    }
    // Admin hay bên đấu giá NFT rút tiền về sau khi bán đấu giá NFT
    function withdrawMoney() external onlyOwner {
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success, "Transfer failed.");
    }
}

Deploy

Gán thời gian bắt đầu đấu giá là 14h34 ngày 19/03/2023 UTC, chuyển qua timestamp sẽ là 1679207640 Set auction start time Giá sẽ giảm dần sau mỗi 1 phút, giảm dần về đến giá sàn khi không có ai chấp nhận các mức giá ở trên. Changes in Dutch auction prices

Mua 1 NFT Complete Dutch auction

35. Merkle Tree

Merkle Tree đơn giản là một cấu trúc dữ liệu dạng cây nhị phân, giá trị của các nút, các lá là mã hash của dữ liệu.

Để tạo ra một Merkle Tree, từ dữ liệu chúng ta có, đưa qua hàm băm để tính toán ra giá trị hash tương ứng của dữ liệu, các giá trị này sẽ là nút lá của cây. Tiếp tục hash các giá trị liền kề nhau đến khi còn 1 giá trị hash duy nhất (Gốc của cây Merkle).

Merkle Tree giúp việc xác minh, kiểm tra tính toàn vẹn dữ liệu trong khi chỉ tốn 1 lượng nhỏ không gian lưu trữ (do mã hash có kích thước bé). Trong Blockchain

Merkle Tree được dùng rất phổ biến nhằm xác minh các giao dịch (Dùng trong Bitcoin, Ethereum, v..v)

Merkle Proof

Merkle Proof là bằng chứng nhằm chứng minh 1 dữ liệu thuộc Merkle Tree mà không tiết lộ về toàn bộ dữ liệu trong đó.

Ví dụ ở hình trên, chúng ta cần chứng minh dữ liệu K thuộc Merkle Tree. Vậy chúng ta sẽ cần Merkle Proof bao gồm các mã hash sau đây:

  • Hash(L)
  • Hash(IJ)
  • Hash(MNOP)

Từ 3 dữ liệu trên trong Merkle Proof và Hash(K), ta hoàn toàn có thể tính được Root. Sau đó thực hiện so sánh với Root ban đầu để xem K có đúng là thuộc Merkle Tree hay không ?

Contract

Ở phần này chúng ta sẽ viết 1 thư viện Merkle Tree, có thể xác minh proof truyền vào là hợp lệ hay không ? Sau đó sẽ xây dựng 1 smart contract ERC-721 sử dụng Merkle Tree để xác minh các địa chỉ whitelist có thể nhận token, sẽ tiết kiệm gas hơn rất nhiều so với việc lưu cả 1 danh sách nhiều địa chỉ ở smart contract.

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

import "./ERC721.sol";


/**
 * Ta có 4 là là 4 địa chỉ:
    [
    "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", 
    "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
    "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
    "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
    ]
 * Merkle proof của địa chỉ đầu tiên:
    [
    "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb",
    "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"
    ]
 * Merkle root: 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097
 */

library MerkleProof {
    /**
     * Trả về true nếu proof hợp lệ
     */
    function verify(
        bytes32[] memory proof,
        bytes32 root,
        bytes32 leaf
    ) internal pure returns (bool) {
        return processProof(proof, leaf) == root;
    }

    /**
	   * Từ giá trị của proof và leaf, tính toán lại Merkle root
     */
    function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = _hashPair(computedHash, proof[i]);
        }
        return computedHash;
    }

    // Sorted Pair Hash
    function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
        return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));
    }
}

contract MerkleTree is ERC721 {
    bytes32 immutable public root; // Root Merkle tree
		
		// mapping lưu trữ để tránh 1 địa chỉ được mint token nhiều lần
    mapping(address => bool) public mintedAddress; 
		
		// Merkle root sẽ được tính toán off-chain sau đó gán giá trị khi deploy contract
    constructor(string memory name, string memory symbol, bytes32 merkleroot) ERC721(name, symbol) {
        root = merkleroot;
    }

    // dựa vào account và proof để xác minh xem account có nằm trong whitelist hay ko ?
    function mint(address account, uint256 tokenId, bytes32[] calldata proof)
    external {
		    // Kiểm tra proof có hợp lệ hay không ?
        require(_verify(_leaf(account), proof), "Invalid merkle proof"); 
				
				// Kiểm tra xem account đã mint chưa ?
        require(!mintedAddress[account], "Already minted!"); // Address has not been minted
        
        mintedAddress[account] = true;
        _mint(account, tokenId); 
    }

    function _leaf(address account)
    internal pure returns (bytes32)
    {
        return keccak256(abi.encodePacked(account));
    }

    // Sử dụng MerkleProof library
    function _verify(bytes32 leaf, bytes32[] memory proof)
    internal view returns (bool)
    {
        return MerkleProof.verify(proof, root, leaf);
    }
}

36. Signature

Digital Signature (Chữ ký số)

Chữ ký số là một dạng chữ ký điện tử sử dụng các ứng dụng của mật mã học. Thuật toán ký số tốt là thuật toán đảm bảo được 3 yếu tố sau:

  1. Tính định danh (Identity authentication): Chứng minh được danh tính của người ký
  2. Chống chối bỏ (Non-repudiation): Người ký không thể phủ nhận được tính xác thực của chữ ký
  3. Tính toàn vẹn (Integrity): Chữ ký không bị sửa đổi trong quá trình truyền tin

metamask signingKý với Metamask

ECDSA Contract

Ethereum sử dụng thuật toán mang tên Elliptic Curve Digital Signature Algorithm (ECDSA) sử dụng các ứng dụng của đường con Elliptic.

ECDSA gồm 2 bước:

  1. Người ký sử dụng private key (bí mật) để tạo chữ ký với 1 thông điệp (message) nào đó.
  2. Người dùng khác sử dụng thông điệp, chữ ký và public key của người ký để xác minh.
Private key: 0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b
Public key: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2
Message: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
Eth signed message: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b
Signature: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c

Tạo chữ ký

1.Mã hóa thông điệp (Packing the message):

Thông điệp với chuẩn ECDSA mã hóa với hàm băm (keccak256), các thông tin được gom lại với abi.encodePacked

/*
 * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
 * _tokenId: 0
 * message msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
 */
function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
    return keccak256(abi.encodePacked(_account, _tokenId));
}

2. Tính toán chữ ký Ethereum

    /**
     * `hash`: Thông điệp đã được mã hóa bằng hàm băm
     * Đọc kỹ hơn về chuẩn ký của Ethereum: https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
     * and `EIP191`:https://eips.ethereum.org/EIPS/eip-191`
     * Thêm chuỗi "\x19Ethereum Signed Message:\n32" để ngăn chặn việc giả mạo chữ ký (bắt buộc trong chữ ký Ethereum phải có)
     */
    function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
	// Ethereum signed message: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b

Xác minh chữ ký

   // Đảo ngược để biết địa chỉ ký từ _msgHash và _signature
    function recoverSigner(bytes32 _msgHash, bytes memory _signature) internal pure returns (address) {
        // Checks the length of the signature. 65 is the length of a standard r,s,v signature.
        // Kiểm tra độ dài của chữ ký, 65 là độ dài tiêu chuẩn của chữ ký dạng r,s,v
        require(_signature.length == 65, "invalid signature length");
        bytes32 r;
        bytes32 s;
        uint8 v;
       
        assembly {
            /*
            Tách r, s, v từ _signature
            */
            // Reads the next 32 bytes after the length data
            r := mload(add(_signature, 0x20))
            // Reads the next 32 bytes after r
            s := mload(add(_signature, 0x40))
            // Reads the last byte
            v := byte(0, mload(add(_signature, 0x60)))
        }
        // trả về địa chỉ ký
        return ecrecover(_msgHash, v, r, s);
    }

/**
* @dev So sánh địa chỉ ký được recover và địa chỉ ký truyền vào
* _msgHash giá trị băm của thông điệp (message).
* _signature là chữ ký.
* _signer là địa chỉ ký.
*/
function verify(bytes32 _msgHash, bytes memory _signature, address _signer) internal pure returns (bool) {
    return recoverSigner(_msgHash, _signature) == _signer;
}
_msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b
_signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c

Public key recovery by signature and message

Ví dụ

Chúng ta cùng ứng dụng chữ ký ECDSA với contract ERC-721. Hàm mint sẽ xác minh chữ ký, bất kỳ chữ ký nào không phải do signer đã được thiết lập trước ký sẽ không hợp lệ.

// SPDX-License-Identifier: MIT

pragma  solidity  ^0.8.20;

import  "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import  "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import  "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

contract SignatureNFT is ERC721 {
	// Địa chỉ signer duy nhất, không đổi. Hàm mint sẽ chỉ chấp nhận chữ ký từ địa chỉ signer
	address  immutable  public signer;

	// mapping lưu trữ trạng thái đã mint hay chưa của các địa chỉ ?
	mapping(address => bool)  public mintedAddress;

	constructor(string  memory _name,  string  memory _symbol,  address _signer) ERC721(_name, _symbol) {
		signer = _signer;
	}

// Khi chữ ký ECDSA hợp lệ (được ký bởi signer) thì NFT mới có thể được mint
	function mint(address _account,  uint256 _tokenId,  bytes  memory _signature)
external {
		bytes32 _msgHash = getMessageHash(_account, _tokenId);  // Tạo message hash
		bytes32 _ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(_msgHash);  // ký

		require(verify(_ethSignedMessageHash, _signature),  "Invalid signature");  // Xác minh chữ ký

		require(!mintedAddress[_account],  "Already minted!");  // Chắc chắn rằng địa chỉ _account chưa được mint NFT

		mintedAddress[_account]  =  true;
		_mint(_account, _tokenId);
}

  
	function getMessageHash(address _account,  uint256 _tokenId)  public  pure  returns (bytes32) {
		return  keccak256(abi.encodePacked(_account, _tokenId));

	}
  
	function verify(bytes32 _msgHash,  bytes  memory _signature) public  view  returns  (bool) {
			return SignatureChecker.isValidSignatureNow(signer, _msgHash, _signature);
	}
}

Deploy với các tham số sau:

_name: WTF Signature
_symbol: WTF
_signer: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2

Gọi hàm mint

_account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
_tokenId: 0
_signature: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c

Deploying SignatureNFT Contract

Gọi ownerOf để kiểm tra, token NFT đã được mint 😄

The owner of tokenId 0 has been changed, indicating that the contract has been executed successfully!

37. NFT Swap

Chúng ta sẽ cùng thiết kế 1 hợp đồng thông minh giúp người dùng mua bán, trao đổi NFT.

Các đối tượng

  • Người bán NFT
  • Người mua NFT
  • Order (Đơn hàng): Lưu trữ thông tin về các NFT đang đăng bán, người bán có thể tạo và hủy đơn hàng.

Hợp đồng

Sự kiện

		// Đăng bán NFT
    event List(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 price);
    
		// Mua NFT
    event Purchase(address indexed buyer, address indexed nftAddr, uint256 indexed tokenId, uint256 price);

		// Hủy đơn hàng
    event Revoke(address indexed seller, address indexed nftAddr, uint256 indexed tokenId);    
    
    // Cập nhật giá NFT đăng bán
		event Update(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 newPrice);

Order

    // order structure
    struct Order {
        address owner;
        uint256 price; 
    }
    // NFT Order mapping: Mỗi order ứng 1 id (uint256). Mỗi địa chỉ có thể có nhiều order (Đăng bán nhiều NFT cùng 1 lúc)
    mapping(address => mapping(uint256 => Order)) public nftList;

Trading

    // Hàm được gọi khi người bán muốn đăng bán NFT của mình
    function list(address _nftAddr, uint256 _tokenId, uint256 _price) public {
        IERC721 _nft = IERC721(_nftAddr); // tạo instance với địa chỉ contract NFT cần bán
        
        // contract NFTSwap cần được người bán approve với tokenId trước
				require(_nft.getApproved(_tokenId) == address(this), "Need Approval");
				
				// price tính bằng wei
        require(_price > 0);
				
				// lưu thông tin về order
        Order storage _order = nftList[_nftAddr][_tokenId];
        _order.owner = msg.sender;
        _order.price = _price;
        
        // Chuyển NFT vào contract
        _nft.safeTransferFrom(msg.sender, address(this), _tokenId);

        emit List(msg.sender, _nftAddr, _tokenId, _price);
    }
		
		// Hủy bán NFT
		function revoke(address _nftAddr, uint256 _tokenId) public {
	    Order storage _order = nftList[_nftAddr][_tokenId];
		
			// Địa chỉ gọi phải là người bán
	    require(_order.owner == msg.sender, "Not Owner");
	    
	    // tạo instance với địa chỉ contract NFT cần bán
	    IERC721 _nft = IERC721(_nftAddr);

			// Kiểm tra xem NFT đã được gửi vào contract chưa ?
	    require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order");
    
	    // chuyển lại NFT cho người bán
	    _nft.safeTransferFrom(address(this), msg.sender, _tokenId);
	    delete nftList[_nftAddr][_tokenId]; // xóa thông tin về order

	    emit Revoke(msg.sender, _nftAddr, _tokenId);
		}
		
    // Người bán cập nhật lại giá NFT
    function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public {
        require(_newPrice > 0, "Invalid Price");
	       
	      // Lấy thông tin về order 
        Order storage _order = nftList[_nftAddr][_tokenId];
				
				// msg.sender phải là người đăng bán
        require(_order.owner == msg.sender, "Not Owner");
        
        IERC721 _nft = IERC721(_nftAddr);
        // Kiểm tra xem NFT đã được gửi vào contract chưa ?
        require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order");
        
        // Thay đổi thông tin về giá
        _order.price = _newPrice;
      
        // Release Update event
        emit Update(msg.sender, _nftAddr, _tokenId, _newPrice);
    }
		
		// Người mua gọi đến hàm purchase và gửi ETH vào để mua NFT
    function purchase(address _nftAddr, uint256 _tokenId) payable public {
        Order storage _order = nftList[_nftAddr][_tokenId];
        require(_order.price > 0, "Invalid Price");
				
				// ETH người mua gửi vào phải lớn hơn hoặc bằng giá của NFT
        require(msg.value >= _order.price, "Increase price");
        
        IERC721 _nft = IERC721(_nftAddr);
        require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order");

        // Chuyển NFT cho người mua
        _nft.safeTransferFrom(address(this), msg.sender, _tokenId);
        
        // Chuyển ETH cho người bán
        payable(_order.owner).transfer(_order.price);
				
				// Trả lại tiền thừa (nếu có) cho người mua
        payable(msg.sender).transfer(msg.value - _order.price);

        delete nftList[_nftAddr][_tokenId]; // Xóa thông tin về order
        
        emit Purchase(msg.sender, _nftAddr, _tokenId, msg.value);
    }

38. Random

Nhiều ứng dụng Ethereum cần sử dụng đến các số ngẫu nhiên, chẳng hạn như lựa chọn tokenId ngẫu nhiên NFT hay ứng dụng trong GameFi (quay thưởng, gambling ..). Tuy nhiên, vì tất cả dữ liệu trên Ethereum đều công khai, xác định nên nó không thể cung cấp cho các nhà phát triển phương pháp tạo số ngẫu nhiên đủ tốt. Chúng ta cùng tìm hiểu hai phương pháp tạo số ngẫu nhiên trên chuỗi (hàm băm) và ngoài chuỗi (Chainlink oracle) và sử dụng chúng để tạo NFT với tokenId ngẫu nhiên.

Tạo ngẫu nhiên số trên chuỗi (On-chain Random Number Generation)

function getRandomOnchain() public view returns(uint256){
     bytes32 randomBytes = keccak256(abi.encodePacked(block.timestamp, msg.sender, blockhash(block.number -1 )));
        
     return uint256(randomBytes);
}

Phương thức này không an toàn, do:

  • Các biến như block.timestamp , msg.sender, block.number là công khai, có thể tra cứu được. Người dùng hoàn toàn có thể đoán trước được kết quả tạo số ngẫu nhiên.
  • blockhash hay block.timestamp cũng có thể bị kiểm soát bởi validator.

Nhiều dự án đã bị tấn công và khai thác lỗ hổng khi áp dụng cách sinh số ngẫu nhiên như trên.

Tạo ngẫu nhiên số ngoài chuỗi (Off-chain random number generation)

Một hướng tiếp cận khác khi dùng các bộ sinh số ngẫu nhiên bên ngoài, gọi là oracles. Chúng ta sẽ cùng tìm hiểu về Chainlink (1 dịch vụ oracles rất phổ biến). Chainlink cung cấp dịch vụ VRF để các nhà phát triển có thể thanh toán bằng token LINK để nhận được số ngẫu nhiên.

Chainlnk VRF

Để có thể sử dụng dịch vụ của Chainlink, chúng ta cần phải kế thừa hợp đồng VRFConsumerBase.

Trước, tiên, cần có ETH và Faucets token LINK Sepolia testnet: https://faucets.chain.link/sepolia

image.png

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

import  "@chainlink/contracts/src/v0.8/VRFv2Consumer.sol";

contract RandomNumberConsumer is VRFv2Consumer {
    bytes32 internal keyHash; // VRF unique identifier
    uint256 internal fee; // phí trả cho oracles (bằng token LINK)

    uint256 public randomResult;

    /**
     * Các tham số là khác nhau với các mạng khác nhau: https://vrf.chain.link/
     * Mạng: Rinkeby test network
     * Chainlink VRF Coordinator address: 0x8103b0a8a00be2ddc778e6e7eaa21791cd364625
     * LINK token address: 0x779877a7b0d9e8603169ddbd7836e478b4624789
     * Key Hash: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c
     */
    constructor()
        VRFConsumerBase(
            0x8103b0a8a00be2ddc778e6e7eaa21791cd364625, // VRF Coordinator
            0x779877a7b0d9e8603169ddbd7836e478b4624789 // LINK Token
        )
    {
        keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
        fee = 0.1 * 10 ** 18; // 0.1 LINK
    }
		
		// Yêu cầu dịch vụ ChainLink tạo số ngẫu nhiên
    function getRandomNumber() public returns (bytes32 requestId) {
        // Yêu cầu hợp đồng phải có đủ token LINK để trả phí
        require(
            LINK.balanceOf(address(this)) >= fee,
            "Not enough LINK - fill contract with faucet"
        );
        return requestRandomness(keyHash, fee);
    }

     // Khi ChainLink tạo số ngẫu nhiên xong, hàm sẽ tự động được gọi và gán kết quả với biến randomResult
    function fulfillRandomness(
        bytes32 requestId,
        uint256 randomness
    ) internal override {
        randomResult = randomness;
    }
}

Để tìm hiểu chi tiết hơn về kiến trúc và các thần phần của oracles ChainLink, các bạn có thể tham khảo tại đây

Tạo tokenId ngẫu nhiên khi mint NFT

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

import  "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import  "@chainlink/contracts/src/v0.8/VRFv2Consumer.sol";

// Faucets token LINK và ETH trên testnet: https://faucets.chain.link/

contract RandomNumber is ERC721, VRFv2Consumer {
    // Số lượng NFT tối đa
    uint256 public totalSupply = 100; 
    
    // Lưu trữ các tokenId
    uint256[100] public ids
    uint256 public mintCount; 
    
    // tham số chainlink VRF
    bytes32 internal keyHash;
    uint256 internal fee;

    // mapping lưu trữ requestId (gọi đến ChainLink) của mỗi địa chỉ
    mapping(bytes32 => address) public requestToSender;

    /**
     * Mạng: Sepolia testnet
     * Chainlink VRF Coordinator address: 0x8103b0a8a00be2ddc778e6e7eaa21791cd364625
     * LINK token address: 0x779877a7b0d9e8603169ddbd7836e478b4624789
     * Key Hash: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c
     */
    constructor()
        VRFConsumerBase(
            0x8103b0a8a00be2ddc778e6e7eaa21791cd364625, // VRF Coordinator
            0x779877a7b0d9e8603169ddbd7836e478b4624789 // LINK Token
        )
        ERC721("WTF Random", "WTF")
    {
        keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
        fee = 0.1 * 10 ** 18; // 0.1 LINK
    }

    /**
     * Input a uint256 number and return a tokenId that can be mint
     */
    function pickRandomUniqueId(
        uint256 random
    ) private returns (uint256 tokenId) {
        // Kiểm tra xem số NFT đã đạt tối đa chưa ?
        uint256 len = totalSupply - mintCount++;
        require(len > 0, "mint close");
        
        uint256 randomIndex = random % len;

        // Tính toán tokenId
        tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; 
        ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; 
        ids[len - 1] = 0; 
    }

    // Tạo số ngẫu nhiên on-chain (ko an toàn)
    function getRandomOnchain() public view returns (uint256) {
        bytes32 randomBytes = keccak256(
            abi.encodePacked(
                blockhash(block.number - 1),
                msg.sender,
                block.timestamp
            )
        );
        return uint256(randomBytes);
    }

    // mint NFT từ kết quả random onchain
    function mintRandomOnchain() public {
        uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); // Use the random number on the chain to generate tokenId
        _mint(msg.sender, _tokenId);
    }

    /**
     * Gọi VRF để tạo số ngẫu nhiên và mint
     */
    function mintRandomVRF() public returns (bytes32 requestId) {
        // Kiểm tra số dư LINK
        require(
            LINK.balanceOf(address(this)) >= fee,
            "Not enough LINK - fill contract with faucet"
        );
        
        // Gọi để yêu cầu ChainLink tạo số ngẫu nhiên
        requestId = requestRandomness(keyHash, fee);

				// mỗi requestId ứng với địa chỉ gọi hàm
        requestToSender[requestId] = msg.sender;
        return requestId;
    }

    /**
     * VRF callback function, được gọi bởi VRF Coordinator khi có kết quả từ bộ tạo số ngẫu nhiên 
     */
    function fulfillRandomness(
        bytes32 requestId,
        uint256 randomness
    ) internal override {
        address sender = requestToSender[requestId]; // Lấy địa chỉ của requestId
        uint256 _tokenId = pickRandomUniqueId(randomness);
        _mint(sender, _tokenId);
    }
}

39. ERC-1155

ERC-721 là chuẩn token dạng single token contract. Có nghĩa là với một loại vật phẩm hay bộ sưu tập NFT khác nhau, các nhà phát triển sẽ phải triển khai các hợp đồng thông minh khác nhau. Với những ứng dụng có rất nhiều loại vật phẩm như Game NFT, việc phải triển khai và quản lý một số lượng lớn các hợp đồng NFT là một trở ngại không nhỏ.

ERC-1155 được đề xuất dưới dạng một chuẩn multi-token. Nơi các nhà phát triển có thể quản lý tất cả các loại token, vật phẩm chỉ với một hợp đồng thông minh. Mỗi tokenId dạng uint256 đại diện cho một loại vật phẩm.

Chuẩn ERC-1155 không hẳn là một chuẩn non-fungible token như ERC-721, nó có sự giao thoa cả của fungible lẫn non-fungible. Với các tokenId có số lượng bằng 1, chúng chính là loại NFT như ERC-721 và ngược lại, các tokenId có số lượng lớn hơn 1 sẽ là dạng fungible như ERC-20.

IERC1155 interface

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

import  "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface IERC1155 is IERC165 {
	  
	  // Sự kiện chuyển 1 token
    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 value
    );

   // Sự kiện chuyển token theo lô
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );

   // Sự kiện ủy quyền tất cả loại token
    event ApprovalForAll(
        address indexed account,
        address indexed operator,
        bool approved
    );

    // Sự kiện khi URI thay đổi
    event URI(string value, uint256 indexed id);

    // Số dư token
    function balanceOf(
        address account,
        uint256 id
    ) external view returns (uint256);

	  // Truy vấn số dư theo lô
    function balanceOfBatch(
        address[] calldata accounts,
        uint256[] calldata ids
    ) external view returns (uint256[] memory);

    // Ủy quyền tất cả loại token
    function setApprovalForAll(address operator, bool approved) external;

   // Kiểm tra trạng thái approve all
    function isApprovedForAll(
        address account,
        address operator
    ) external view returns (bool);

   // Chuyển token 
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external;

   // Chuyển token theo lô
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external;
}

ERC1155 Receive

Để tránh việc gửi token vào các hợp ko áp dụng chuẩn (sẽ nằm vĩnh viễn trong hợp đồng mà ko chuyển đi được), hợp đồng nhận cần kế thừa ERC1155Receiver.

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

import  "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface IERC1155Receiver is IERC165 {
    /**
     * @dev accept ERC1155 safe transfer `safeTransferFrom`
     * Need to return 0xf23a6e61 or `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
     */
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    ) external returns (bytes4);

    /**
     * @dev accept ERC1155 batch safe transfer `safeBatchTransferFrom`
     * Need to return 0xbc197c81 or `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
     */
    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    ) external returns (bytes4);
}

ERC-1155

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

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "./IERC1155MetadataURI.sol";
import "../34_ERC721_en/Address.sol";
import "../34_ERC721_en/String.sol";
import "../34_ERC721_en/IERC165.sol";

/**
 * @dev ERC1155 multi-token standard
 * See https://eips.ethereum.org/EIPS/eip-1155
 */
contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI {
		// Sử dụng thư viện Address với hàm isContract để kiểm tra 1 địa chỉ là hợp đồng hay ko ? 
    using Address for address;
    
    // Sử dụng thư viện String
    using Strings for uint256;
    // Token name
    string public name;
    // Token code name
    string public symbol;
    
    // Mapping quản lý số dư của từng địa chỉ với mỗi tokenID
    mapping(uint256 => mapping(address => uint256)) private _balances;
    // Mapping quản lý việc ủy quyền
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    constructor(string memory name_, string memory symbol_) {
        name = name_;
        symbol = symbol_;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC1155).interfaceId ||
            interfaceId == type(IERC1155MetadataURI).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }

   
    function balanceOf(
        address account,
        uint256 id
    ) public view virtual override returns (uint256) {
        require(
            account != address(0),
            "ERC1155: address zero is not a valid owner"
        );
        return _balances[id][account];
    }

  
    function balanceOfBatch(
        address[] memory accounts,
        uint256[] memory ids
    ) public view virtual override returns (uint256[] memory) {
        require(
            accounts.length == ids.length,
            "ERC1155: accounts and ids length mismatch"
        );
        uint256[] memory batchBalances = new uint256[](accounts.length);
        for (uint256 i = 0; i < accounts.length; ++i) {
            batchBalances[i] = balanceOf(accounts[i], ids[i]);
        }
        return batchBalances;
    }

     function setApprovalForAll(
        address operator,
        bool approved
    ) public virtual override {
        require(
            msg.sender != operator,
            "ERC1155: setting approval status for self"
        );
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

   
    function isApprovedForAll(
        address account,
        address operator
    ) public view virtual override returns (bool) {
        return _operatorApprovals[account][operator];
    }

  
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) public virtual override {
        address operator = msg.sender;
        // Người chuyển là chủ token hoặc đc ủy quyền
        require(
            from == operator || isApprovedForAll(from, operator),
            "ERC1155: caller is not token owner nor approved"
        );
        require(to != address(0), "ERC1155: transfer to the zero address");
        uint256 fromBalance = _balances[id][from];

				// Kiểm tra số dư
        require(
            fromBalance >= amount,
            "ERC1155: insufficient balance for transfer"
        );
        
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }
        _balances[id][to] += amount;
       
        emit TransferSingle(operator, from, to, id, amount);
        // Security check
        _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
    }

   
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override {
        address operator = msg.sender;
        
        require(
            from == operator || isApprovedForAll(from, operator),
            "ERC1155: caller is not token owner nor approved"
        );
        require(
            ids.length == amounts.length,
            "ERC1155: ids and amounts length mismatch"
        );
        require(to != address(0), "ERC1155: transfer to the zero address");

        // Cập nhật số dư với vòng lặp 
        for (uint256 i = 0; i < ids.length; ++i) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(
                fromBalance >= amount,
                "ERC1155: insufficient balance for transfer"
            );
            unchecked {
                _balances[id][from] = fromBalance - amount;
            }
            _balances[id][to] += amount;
        }

        emit TransferBatch(operator, from, to, ids, amounts);
        // Security check
        _doSafeBatchTransferAcceptanceCheck(
            operator,
            from,
            to,
            ids,
            amounts,
            data
        );
    }

  
    function _mint(
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");

        address operator = msg.sender;

        _balances[id][to] += amount;
        emit TransferSingle(operator, address(0), to, id, amount);

        _doSafeTransferAcceptanceCheck(
            operator,
            address(0),
            to,
            id,
            amount,
            data
        );
    }

   
    function _mintBatch(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");
        require(
            ids.length == amounts.length,
            "ERC1155: ids and amounts length mismatch"
        );

        address operator = msg.sender;

        for (uint256 i = 0; i < ids.length; i++) {
            _balances[ids[i]][to] += amounts[i];
        }

        emit TransferBatch(operator, address(0), to, ids, amounts);

        _doSafeBatchTransferAcceptanceCheck(
            operator,
            address(0),
            to,
            ids,
            amounts,
            data
        );
    }

       function _burn(address from, uint256 id, uint256 amount) internal virtual {
        require(from != address(0), "ERC1155: burn from the zero address");

        address operator = msg.sender;

        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }

        emit TransferSingle(operator, from, address(0), id, amount);
    }

   
    function _burnBatch(
        address from,
        uint256[] memory ids,
        uint256[] memory amounts
    ) internal virtual {
        require(from != address(0), "ERC1155: burn from the zero address");
        require(
            ids.length == amounts.length,
            "ERC1155: ids and amounts length mismatch"
        );

        address operator = msg.sender;

        for (uint256 i = 0; i < ids.length; i++) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(
                fromBalance >= amount,
                "ERC1155: burn amount exceeds balance"
            );
            unchecked {
                _balances[id][from] = fromBalance - amount;
            }
        }

        emit TransferBatch(operator, from, address(0), ids, amounts);
    }

    // @dev Kiểm tra xem địa chỉ nhận có áp dụng chuẩn ERC1155 hay ko (security check)
    function _doSafeTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try
                IERC1155Receiver(to).onERC1155Received(
                    operator,
                    from,
                    id,
                    amount,
                    data
                )
            returns (bytes4 response) {
                if (response != IERC1155Receiver.onERC1155Received.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155 Receiver implementer");
            }
        }
    }

    
    function _doSafeBatchTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try
                IERC1155Receiver(to).onERC1155BatchReceived(
                    operator,
                    from,
                    ids,
                    amounts,
                    data
                )
            returns (bytes4 response) {
                if (
                    response != IERC1155Receiver.onERC1155BatchReceived.selector
                ) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155 Receiver implementer");
            }
        }
    }

   
    function uri(
        uint256 id
    ) public view virtual override returns (string memory) {
        string memory baseURI = _baseURI();
        return
            bytes(baseURI).length > 0
                ? string(abi.encodePacked(baseURI, id.toString()))
                : "";
    }

    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }
}

Nguồn, tài liệu tham khảo

https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en

https://www.wtf.academy/en/solidity-start/


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.