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