---
# System prepended metadata

title: 區塊鏈全端工程師第一週作業解答

---

第一週作業解答
===

基礎題
---
* 第一題：發行總量100億顆、位數18的代幣！
解答：
```solidity=
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20Example is ERC20 {

    // 定義最大供給量
    uint256 public maxSupply;

    //建構子初始化ERC20必要參數(name與symbol)，並多加設定題目要求的maxSupply
    constructor(
        string memory _name, 
        string memory _symbol,
        uint256 _maxSupply
        ) ERC20(_name, _symbol){
        //執行時填入10000000000(100億) * 1000000000000000000(單位)
        maxSupply = _maxSupply;
    }

    // 實作mint function，主要用來demo確認用
    function mint (uint256 amount) external {
        //判斷這筆交易若完成，是否會超出最大供給量，如果會就回傳錯誤字串"over max supply."
        require(amount + totalSupply() <= maxSupply, "over max supply.");
        _mint(msg.sender, amount);
    }
}
```
* 第二題：發行總量10張的SBT。
```solidity=
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract ERC721Example is ERC721 {

    // 定義最大供給量
    uint256 public maxSupply;
    uint256 public counter = 0;

    modifier avaialbeMint(uint256 amount) {
        //判斷這筆交易若完成，是否會超出最大供給量，如果會就回傳錯誤字串"over max supply."
        require(amount + counter <= maxSupply, "over max supply.");
        _;
    }

    //建構子初始化ERC721必要參數(name與symbol)，並多加設定題目要求的maxSupply
    constructor(
        string memory _name, 
        string memory _symbol, 
        uint256 _maxSupply)
        ERC721(_name, _symbol){
        //執行時填入10
        maxSupply = _maxSupply;
    }

    // 實作mint function，主要用來demo確認用
    function mint (uint256 amount) external avaialbeMint(amount){
        // 迴圈值星批量鑄造NFT
       for(uint256 i=0; i < amount ; i++){
            // 鑄造 NFT, counter為NFT的tokenId
            _mint(msg.sender, counter);
            counter ++ ;
       }
    }
    // 讓transfer無效
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override  {
        revert();
    }
}
```
* 第三題：開發猜數字的合約（一個人設定，大家猜，有人猜中，就結束不讓其他人猜了）
```solidity=
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.20;

contract Game {
    // 定義遊戲參數struct
    struct GameInfo{
        uint256 minValue;
        uint256 maxValue;
        uint256 target;
        address winner;
    }
    // 宣告 struct
    GameInfo gameInfo;

    //定義一個mapping記錄那些地址參與過遊戲，為了不讓同一地址一直投票
    mapping (address => bool) public guessRecord;


    // 透過初始化設定遊戲資訊（包含答案）
    constructor (
        uint256 _min,
        uint256 _max,
        uint256 _target
    ){
        gameInfo = GameInfo(
            {
                minValue: _min,
                maxValue: _max,
                target: _target,
                winner: address(0)
            }
        );
    } 

    //猜數字功能
    function guessNumber(uint256 num) external returns (string memory){
        // 如果有人猜到了，將不再允許猜數字
        require(gameInfo.winner == address(0), "game over");
        //如果你猜過了，將不能再猜
        require(!guessRecord[msg.sender], "already guess.");
        //記錄用戶猜過數字
        guessRecord[msg.sender] = true;

        // 判斷是否猜中
        if (num == gameInfo.target){
            gameInfo.winner = msg.sender;
            return "you win";
        }else{
            return "sorry, you lose.";
        }
    }

    //取得獲勝者地址
    function getWinner () external view returns (address){
        return gameInfo.winner;
    }

}
```

進階題
---
* 第一題：發行有盲盒機制的 NFT

```solidity=
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";


contract MyNFT is ERC721{

    using Strings for uint256;

    address owner;
    uint256 public maxSupply = 10; // 最大發行量
    bool private isOpened = false;//盲盒是否打開

    uint256 public counter = 0;

    modifier onlyOwner{
        require(msg.sender == owner);
        _;
    }

    constructor (string memory _name, string memory _symbol) ERC721(_name, _symbol){
        owner = msg.sender;
    }
    
    //開盲盒
    function openBlindBox() external onlyOwner{
        isOpened = true;
    }
    
    
    //設定NFT的baseURI(盲盒)
    function _baseURI() internal pure override returns (string memory) {
        return "ipfs://QmXxZBg4RnGxC2dDxfUSAmxgGooHsoncPQgCLiNw8kj3Ls/";
    }
    
    //查看NFT Metadata網址
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        if (!isOpened){
            return _baseURI();
        }
        return string(abi.encodePacked("ipfs://QmWQcaFFCm9ofyVN2ZwGbTGopLEbQ6QSc2Xn1C7ekKAYDF/", tokenId.toString(), ".json"));
        
    }

    // 實作mint function，主要用來demo確認用
    function mint (uint256 amount) external{
        require(amount + counter <= maxSupply, "over max supply.");
        // 迴圈批量鑄造NFT
        for(uint256 i=0; i < amount ; i++){
                // 鑄造 NFT, counter為NFT的tokenId
                _mint(msg.sender, counter);
                counter ++ ;
        }
    }

}
```
* 第二題：
#### 理解 ERC721 與 ERC721A 的差異，並實作 ERC721A 合約實際比較：

ERC721A為Azuki項目提出來的ERC721改良版，目的是用來節省Gas Fee，先來聊聊ERC721現況，ERC721會記錄每個NFT之Token ID對應哪個地址，就像下方這樣：

<table>
    <tr><th>Token ID</th><th>Address</th></tr>
    <tr><td>1</td><td>Address1</td></tr>
    <tr><td>2</td><td>Address1</td></tr>
    <tr><td>3</td><td>Address2</td></tr>
    <tr><td>4</td><td>Address2</td></tr>
    <tr><td>5</td><td>Address2</td></tr>
    <tr><td>6</td><td>Address4</td></tr>
    <tr><td>7</td><td>Address5</td></tr>
    <tr><td>8</td><td>Address6</td></tr>
</table>

所以如果同一個地址鑄造多個NFTs，就會透過for迴圈依序鑄造並記錄，每一筆鑄造紀錄就都要收取手續費，如此，當你鑄造越多，就會花越多手續費，如同下方所示（Mint越多，for-loop跑越多次，手續費越高）：
    

![](https://i.imgur.com/jvVUp5p.png =300x300)
(圖源：https://github.com/chiru-labs/ERC721A)

而ERC721A為此進行的改良，用下方這種方式紀錄Token ID與Address的關係：

<table>
    <tr><th>Token ID</th><th>Address</th></tr>
    <tr><td>1</td><td>Address1</td></tr>
    <tr><td>2</td><td><></td></tr>
    <tr><td>3</td><td>Address2</td></tr>
    <tr><td>4</td><td><></td></tr>
    <tr><td>5</td><td><></td></tr>
    <tr><td>6</td><td>Adress4</td></tr>
    <tr><td>7</td><td>Address5</td></tr>
    <tr><td>8</td><td>Address6</td></tr>
</table>

當同時鑄造多個NFTs，只有Token ID最小的會對應到地址，後續連號的Token ID會記錄為空，即代表都屬於此地址所有之NFT。

當要查找某Token ID之NFT的持有地址時，若查詢到的地址為空，僅需往前找到第一個對應到實際地址的記錄，該地址極為持有者。

以上例來說，假設我要查Token ID為5的NFT的持有者是誰，查到為空，因為就往前找Token ID為4，也對應空地址，於是再往前找到Token ID為3的NFT，發現對應到Address2，因此就可以知道，Token ID為4, 5的NFT都是Address2所持有。

如此便可有有效節省Gas，因為只需要知道一次Mint的數量，就可以自動將後面填上空值，不用依序每個Token ID都寫入地址。

Ex: Address8一次鑄造8個NFTs，那麼只需要在Token ID為1的位置記錄Address8，後面Token ID 2~8僅需記錄空值即可，下一個鑄造NFT的地址就從Token ID為9的開始鑄造，如下圖示：

<table>
    <tr><th>Token ID</th><th>Address</th></tr>
    <tr><td>1</td><td>Address8</td></tr>
    <tr><td>2</td><td><></td></tr>
    <tr><td>3</td><td><></td></tr>
    <tr><td>4</td><td><></td></tr>
    <tr><td>5</td><td><></td></tr>
    <tr><td>6</td><td><></td></tr>
    <tr><td>7</td><td><></td></tr>
    <tr><td>8</td><td><></td></tr>
    <tr><td>9</td><td>Address9</td></tr>
    <tr><td>10</td><td>Address10</td></tr>
</table>


如此，當你一次鑄造多個NFT時，會比原本的ERC721省下許多Gas Fee，見下圖所示：

![](https://i.imgur.com/kx5timg.png =400x300)
(圖源：https://github.com/chiru-labs/ERC721A)

但ERC721A有個小缺點，當NFT被交易換持有人時，如果Token ID為連號的中間值時，那更改持有人需要更改兩個值...，假設原本為下方的記錄：
<table>
    <tr><th>Token ID</th><th>Address</th></tr>
    <tr><td>1</td><td>Address8</td></tr>
    <tr><td>2</td><td><></td></tr>
    <tr><td>3</td><td><></td></tr>
    <tr><td>4</td><td><></td></tr>
    <tr><td>5</td><td><></td></tr>
    <tr><td>6</td><td><></td></tr>
    <tr><td>7</td><td><></td></tr>
    <tr><td>8</td><td><></td></tr>
    <tr><td>9</td><td>Address9</td></tr>
    <tr><td>10</td><td>Address10</td></tr>
</table>
當Token ID為4的NFT轉手給Address11時，將會如此變動(更改兩個值)：
<table>
    <tr><th>Token ID</th><th>Address</th></tr>
    <tr><td>1</td><td>Address8</td></tr>
    <tr><td>2</td><td><></td></tr>
    <tr><td>3</td><td><></td></tr>
    <tr><td>4</td><td><font color = 'red'>Address11</font></td></tr>
    <tr><td>5</td><td><font color = 'red'>Address8</font></td></tr>
    <tr><td>6</td><td><></td></tr>
    <tr><td>7</td><td><></td></tr>
    <tr><td>8</td><td><></td></tr>
    <tr><td>9</td><td>Address9</td></tr>
    <tr><td>10</td><td>Address10</td></tr>
</table>

反倒ERC721僅需更改該Token ID對應之Address即可。

PS: 如果
的持有者是轉移其持有之Token ID最後面的NFT，那就可以更改一個值就完成轉移行為。

Ex: Address8轉移Token ID為8的NFT：

<table>
    <tr><th>Token ID</th><th>Address</th></tr>
    <tr><td>1</td><td>Address8</td></tr>
    <tr><td>2</td><td><></td></tr>
    <tr><td>3</td><td><></td></tr>
    <tr><td>4</td><td><></td></tr>
    <tr><td>5</td><td><></td></tr>
    <tr><td>6</td><td><></td></tr>
    <tr><td>7</td><td><></td></tr>
    <tr><td>8</td><td><font color = 'red'>Address11</font></td></tr>
    <tr><td>9</td><td>Address9</td></tr>
    <tr><td>10</td><td>Address10</td></tr>
</table>

```solidity=
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.20;

import "erc721a/contracts/ERC721A.sol";

contract ERC721ADemo is ERC721A{

    constructor(string memory _name, string memory _symbol) ERC721A(_name, _symbol){}

    function mint(uint256 _quantity) external payable {
        // quantity是數量，不是token ID
        _mint(msg.sender, _quantity);
    }
}
```
