第一週作業解答
===
基礎題
---
* 第一題:發行總量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://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://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);
}
}
```