# Garrick信仲 | C0051202 Cryptocamp第5期Solidity工程師實戰營作業 ## 第一週課程作業 <details> ### 1.請簡單描述錢包助記詞,私鑰,公鑰之間的關係 #### 錢包、助記詞、公鑰、私鑰 錢包就是一個身份認證或是一把鑰匙,而錢包助記詞的標準是BIP-39,助記詞先產生私鑰,再透過私鑰產生公鑰,公鑰產生公鑰雜湊,再取20Byte為錢包地址(例如Metamask上看到的位址), 公鑰雜湊不可以反推出公鑰,公鑰也沒辦法反推出私鑰。 ### 2.用 Remix 部署智能合約(Local London) ![](https://i.imgur.com/CeOrzGT.png) ## 基本題 ### 3.Solidity TodoList 延伸 * 將寫好的合約部署到 Görli / Goerli 測試鏈上,並 verify 開源程式碼 ![](https://i.imgur.com/U3CYiDO.png) * 請貼上合約地址 ``` 0xc0eb0907C53E30AEAf7363e3f85CbA922fA26d40 ``` * #### Todo List 增加 Pending 功能 請參考以下程式碼區塊 * 提示:多新增一個 Pending 狀態,string[] public pending; ## 進階題 * 撰寫此功能測試 * 增加清空 Completed 功能 * 增加 Pending 最多滯留 n 秒(當 TODO 搬移到 PENDING 後,要記錄時間,當時間超過 n 秒,就不可以再搬回 TODO) - Array - Mapping - Struct (懷恩💜熱情推薦) 下方有TodoList和TestTodoList合約,可以使用TestTodoList來和TodoList互動,先佈署TodoList後複製地址再佈署TestTodoList時貼上地址。 測試案例: 1. 先在addTodo依序輸入工作項目,例如:w1,w2,w3,w4,w5,w6,w7等工作項目,現有功能需分7次輸入尚未能一次貼上。 2. 可使用getAllTodo列出所有工作項目。 3. 執行setCompleted給值 1,並執行getAllCompleted可看到完成的工作項目增加 w2,再執行getAllTodo可發現工作項目 w2已不在列表中。 4. 本項執行前請問注意被轉到pending的工作項目10秒後就不能再resume,執行setPending給值 3,並執行getAllPending可看到完成的工作項目增加 w5,再執行getAllTodo可發現工作項目 w5已不在列表中。 5. 可執行emptyCompleted清掉所有完成的工作項目。 6. deleteTodo功能執行後在array裡元素還是存在但是值變成空,這個元素還是可以被轉到completed或是pending,可以將該元素移至array最後方再用pop移除(此功能暫時未實現)。 ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract TodoList { struct TodoItem { string todoName; uint256 updateTimestamp; } TodoItem[] public todos; TodoItem[] public todoCompleted; TodoItem[] public todoPending; uint256 public startTime; constructor() { startTime = block.timestamp; } // 永久儲存是storage,短暫使用就用memory,calldata傳入後只能參考不能改變的data type //--------------Todo------------------------------------------------------------------------------------------ function addTodo(string memory _todoName) external { pushTodoItem(_todoName); } function pushTodoItem(string memory _todoName) internal { todos.push(TodoItem(_todoName, block.timestamp)); } function getTodoItem(uint256 index) internal view returns (string memory) { return todos[index].todoName; } function getTodo(uint256 _index) external view returns (string memory) { return getTodoItem(_index); } function deleteTodo(uint256 index) external { delete todos[index]; } function getAllTodo() external view returns (string memory) { string memory todosName; todosName = "The Todo List: "; for (uint256 i = 0; i <= todos.length - 1; i++){ todosName = string(abi.encodePacked(todosName, " ", todos[i].todoName)); } return todosName; } //--------------Todo end------------------------------------------------------------------------------------------ //--------------Complete------------------------------------------------------------------------------------------ function setCompleted(uint256 index) external { TodoItem memory compeltedTodo = todos[index]; for (uint256 i = index; i < todos.length - 1; i++){ todos[i] = todos[i + 1]; } delete todos[todos.length - 1]; todos.pop(); compeltedTodo.updateTimestamp = block.timestamp; todoCompleted.push(compeltedTodo); } function setUncompleted(uint256 index) external { TodoItem memory uncompeltedTodo = todoCompleted[index]; require(block.timestamp<(uncompeltedTodo.updateTimestamp+86400), "The time is waiting too long to restore the Todo Item."); for (uint256 i = index; i < todoCompleted.length - 1; i++){ todoCompleted[i] = todoCompleted[i + 1]; } delete todoCompleted[todoCompleted.length - 1]; todoCompleted.pop(); todos.push(uncompeltedTodo); } function getCompletedTodoItem(uint256 index) internal view returns (string memory) { return todoCompleted[index].todoName; } function getCompleted(uint256 _index) external view returns (string memory) { return getCompletedTodoItem(_index); } function getAllCompleted() external view returns (string memory) { string memory completedTodosName; completedTodosName = "The Completed Todo List: "; for (uint256 i = 0; i <= todoCompleted.length - 1; i++){ completedTodosName = string(abi.encodePacked(completedTodosName, " ", todoCompleted[i].todoName)); } return completedTodosName; } function emptyCompleted() external returns (uint256) { delete todoCompleted; return todoCompleted.length; } //--------------Complete end------------------------------------------------------------------------------------------ //--------------Pending------------------------------------------------------------------------------------------ function setPending(uint256 index) external { TodoItem memory pendingTodo = todos[index]; for (uint256 i = index; i < todos.length - 1; i++){ todos[i] = todos[i + 1]; } delete todos[todos.length - 1]; todos.pop(); pendingTodo.updateTimestamp = block.timestamp; todoPending.push(pendingTodo); } function setResume(uint256 index) external { TodoItem memory resumeTodo = todoPending[index]; require(block.timestamp<(resumeTodo.updateTimestamp+10), "The time is waiting too long to restore the Todo Item."); for (uint256 i = index; i < todoPending.length - 1; i++){ todoPending[i] = todoPending[i + 1]; } delete todoPending[todoPending.length - 1]; todoPending.pop(); todos.push(resumeTodo); } function getPendingTodoItem(uint256 index) internal view returns (string memory) { return todoPending[index].todoName; } function getPending(uint256 _index) external view returns (string memory) { return getPendingTodoItem(_index); } function getAllPending() external view returns (string memory) { string memory pendingTodosName; pendingTodosName = "The Pending Todo List: "; for (uint256 i = 0; i <= todoPending.length - 1; i++){ pendingTodosName = string(abi.encodePacked(pendingTodosName, " ", todoPending[i].todoName)); } return pendingTodosName; } //--------------Pending end------------------------------------------------------------------------------------------ function getTSDisplay() external view returns (uint256) { return startTime; } } contract TestTodoList { TodoList public cryptoTodoList; constructor(address _todoListAddr) { cryptoTodoList = TodoList(_todoListAddr); } function addTodo(string memory _todoName) external { cryptoTodoList.addTodo(_todoName); } function getTodo(uint256 _index) external view returns (string memory) { return cryptoTodoList.getTodo(_index); } function deleteTodo(uint256 index) external { cryptoTodoList.deleteTodo(index); } function getAllTodo() external view returns (string memory) { return cryptoTodoList.getAllTodo(); } function setCompleted(uint256 index) external { cryptoTodoList.setCompleted(index); } function setUncompleted(uint256 index) external { cryptoTodoList.setUncompleted(index); } function getCompleted(uint256 _index) external view returns (string memory) { return cryptoTodoList.getCompleted(_index); } function getAllCompleted() external view returns (string memory) { return cryptoTodoList.getAllCompleted(); } function emptyCompleted() external returns (uint256){ return cryptoTodoList.emptyCompleted(); } function setPending(uint256 _index) external { cryptoTodoList.setPending(_index); } function setResume(uint256 _index) external { cryptoTodoList.setResume(_index); } function getPending(uint256 _index) external view returns (string memory) { return cryptoTodoList.getPending(_index); } function getAllPending() external view returns (string memory) { return cryptoTodoList.getAllPending(); } function getTSDisplay() external view returns (uint256) { return cryptoTodoList.getTSDisplay(); } } ``` </details> ## 第二週課程作業 ## 基本題 提醒,作業標題:請填寫你的學號 (Discord KryptoCamp 的名稱),例:Hazel | C0031601 用 HackMD 繳交作業。如不做進階題可留空白,繳交作業時,請提供 HackMD網址 ### 1.引用 OpenZeppelin ERC20 部署一個自定義的 ERC20 Token 在 Goerli 鏈上 * 開源合約並提交合約地址 * 實作 mint 和 burn 功能 * 轉移 100 個 Token 到以下地址 0x6e24f0fF0337edf4af9c67bFf22C402302fc94D3 * 轉移 Token 給所有組員 #### 合約地址 測試鏈地址 : `0x66081dF82eb1fcC32831B902fA733f92fd67770F` Symbol : GHC Decimals : 6 合約verify畫面 ![](https://i.imgur.com/fwMnrzV.png) --- #### 合約程式 實作mint和burn功能 ``` // SPDX-License-Identifier: MIT pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract GCERC20Test is ERC20 { uint8 private _decimals; constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol){ _decimals = decimals_; } function decimals() public view override returns (uint8) { return _decimals; } function mint(address to, uint256 amount) external { _mint(to, amount); } function burn(address from, uint256 amount) external { _burn(from, amount); } function transfer(address to, uint256 amount) public override returns (bool) { address owner = _msgSender(); _burn(owner, 1); _transfer(owner, to, amount); return true; } } ``` --- #### 轉移結果 轉給`0x6e24f0fF0337edf4af9c67bFf22C402302fc94D3` ![](https://i.imgur.com/ueH7KVi.png) --- RT from Ryan, tGD from 企鵝 ![](https://i.imgur.com/88nkT8Z.png) --- ### 2.引用 OpenZeppelin ERC721 部署一個 ERC721 NFT 合約,同時擁有付費 mint 功能 * 開源合約並提交合約地址 * 使用 OpenSea Metadata 標準,並提交 OpenSea頁面 * 將檔案上傳至 ipfs * 擁有白名單機制並且將組員加入白名單 * 設定總量上限 * 加入至少一種自定義功能,例如:荷蘭拍、盲盒、返佣、融合… #### 合約地址 `0x7eb9Fa0cCf33eaa6f5a5818799c6851DBf497c2e` ![](https://i.imgur.com/QkzcWNN.png) --- #### 合約程式 程式說明: 1. 使用batchMint可以一次鑄造多個NFT,給的數量不能超過5但未上線總數限制,需要輸入proof使用Merkle Tree白名單(可見以下測試結果)。 2. 使用setURIState方法輸入1後,可以讓替換baseURI讓切換圖片,類似盲盒功能。 ``` // SPDX-License-Identifier: MIT pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract ERC721FeedAnimal is ERC721 { uint256 public tokenId = 1000; address public owner; bytes32 public root; uint256 public uriState; using Strings for uint256; constructor(string memory _name, string memory _symbol, bytes32 _root) ERC721(_name, _symbol) { owner = msg.sender; setRoot(_root); } function mint(uint256 _tokenId, bytes32[] calldata _proof) external verifyProof(_proof){ require(_tokenId<=5, "The maxium mint number is 5."); // tokenId++; _safeMint(msg.sender, _tokenId); } function batchMint(uint256 _quantity, bytes32[] calldata _proof) external verifyProof(_proof){ // unit256 _tokenId = _quantity + 1001; require(_quantity<=5, "The maxium mint amount and number is 5."); for(uint256 i=1; i <= _quantity; i++) { tokenId++; _safeMint(msg.sender, tokenId); } } function _baseURI() internal pure override returns (string memory) { return "https://gateway.pinata.cloud/ipfs/QmYgfUPBRgH1KWGAKuirRgCodM78Bpu8XmR9D8LL7a2JPr/"; // return "https://gateway.pinata.cloud/ipfs/QmRRSVSkKHA93qspxbX9q2vJ8Utmk2B1dY4Nmi8D3gmzQK/"; } function setURIState(uint256 _URIState) external { uriState = _URIState; } function tokenURI(uint256 _tokenId) public view override returns (string memory) { _requireMinted(_tokenId); string memory baseURI = (uriState == 1? "https://gateway.pinata.cloud/ipfs/QmRRSVSkKHA93qspxbX9q2vJ8Utmk2B1dY4Nmi8D3gmzQK/": _baseURI()); return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, _tokenId.toString(), ".json")) : ""; } modifier verifyProof(bytes32[] memory proof) { require(MerkleProof.verify(proof, root, keccak256(abi.encodePacked(msg.sender))), "Invalid proof"); _; } // function whitelistMint(bytes32[] calldata _proof) external verifyProof(_proof) { // tokenId++; // _safeMint(msg.sender, tokenId); // } function verify(bytes32[] memory proof) internal view returns (bool) { return MerkleProof.verify(proof, root, keccak256(abi.encodePacked(msg.sender))); } function setRoot(bytes32 _root) internal { require(msg.sender == owner, "Only owner can set root"); root = _root; } } ``` --- #### 測試結果 在Opensea testnet上可以看到以下的NFT ![](https://i.imgur.com/BjH0Dsd.png) 將 ![](https://i.imgur.com/v9LS37d.png) 總共傳了5個圖在Pinata供這個合約使用,現在已經mint了2個NFT,還剩下3個NFT的圖可以使用,測試資料如下: ``` root 0x7ae5b565b6bd62cb8f307d1daf2b64657446d3ca29b31febcee1bc87dfcd4c37 address 0x6e24f0fF0337edf4af9c67bFf22C402302fc94D3 proof ["0xafe7c546eb582218cf94b848c36f3b058e2518876240ae6100c4ef23d38f3e07","0xe2f68cf91ee6803cd4e7305b6918ca7e0cd67a68c79bdaa252022ebf5052f166","0xd0587cdd201c1673cf4f382ae7c88a83f944a0de445ce8fc14518f80a1596ce7"] ``` --- ## 進階題 (以下佈在Sepolia測試鏈,只是測試mint不會顯示圖片到opensea) ## 1.研究 ERC721A 合約 * 寫下 ERC721A 及 ERC721 差異 * 實際部署 ERC721A 合約比較所花費的 gas fee #### ERC721在用戶可以一次mint多個NFT的時候,不僅要使用For loop執行_safeMint方法而且都要執行totalSupply()方法取得發行的NFT數量後再加1傳入_safeMint當tokenId,這樣造成許多的gas fee的費用。 ``` function _safeMint(address to, uint256 tokenId) internal virtual { _safeMint(to, tokenId, ""); } ``` #### ERC721A的_safeMint方法是改成傳入quantity給_mint,就不需要在迴圈中一直對mint總數進行累加計算,在_mint的do...while迴圈中的tokenId不等於end變數的時候持續mint,直到得到輸入的數量,以節省gas fee。 ``` function _safeMint( address to, uint256 quantity, bytes memory _data ) internal virtual { _mint(to, quantity); unchecked { if (to.code.length != 0) { uint256 end = _currentIndex; uint256 index = end - quantity; do { if (!_checkContractOnERC721Received(address(0), to, index++, _data)) { _revert(TransferToNonERC721ReceiverImplementer.selector); } } while (index < end); // Reentrancy protection. if (_currentIndex != end) _revert(bytes4(0)); } } } ``` --- #### ERC721 mint 100個token花費如下: Contract `0xD8D0E88bcADB7d7B07CEF12a0E3B6b50c2b99485` Transaction Hash `0xfff9436efc48519881db810fd67c0cb690fd8029d41971164b93502411e40972` ![](https://i.imgur.com/XzUrz4t.png) ERC721合約 ``` // SPDX-License-Identifier: MIT pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract MyERC721NFT is ERC721 { uint256 public tokenId = 1000; address public owner; using Strings for uint256; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) { owner = msg.sender; } function mint(uint256 _tokenId) external { // tokenId++; _safeMint(msg.sender, _tokenId); } function batchMint(uint256 _quantity) external { // unit256 _tokenId = _quantity + 1001; for(uint256 i=1; i <= _quantity; i++) { tokenId++; _safeMint(msg.sender, tokenId); } } function _baseURI() internal pure override returns (string memory) { return "https://gateway.pinata.cloud/ipfs/QmXzSFXTxCDW12JYQnDXj5N8QUffmycHw4i8cpMYmrBX13/"; } function tokenURI(uint256 _tokenId) public view override returns (string memory) { _requireMinted(_tokenId); string memory baseURI = _baseURI(); return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, _tokenId.toString(), ".json")) : ""; } } ``` --- #### ERC721A mint 100個token花費如下: Contract `0xc0eb0907C53E30AEAf7363e3f85CbA922fA26d40` Transaction Hash `0x242e9d8e2293b4017de22bd8bacdc514567ea94a0eef871e404397e50435db20` ![](https://i.imgur.com/RL3q7Q7.png) ERC721A合約 ``` pragma solidity 0.8.17; import "erc721a/contracts/ERC721A.sol"; contract MyERC721ANFT is ERC721A { // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). // ============================================================= // CONSTANTS // ============================================================= // Mask of an entry in packed address data. uint256 private constant _BITMASK_ADDRESS_DATA_ENTRY = (1 << 64) - 1; // The bit position of `numberMinted` in packed address data. uint256 private constant _BITPOS_NUMBER_MINTED = 64; // The bit position of `numberBurned` in packed address data. uint256 private constant _BITPOS_NUMBER_BURNED = 128; // The bit position of `aux` in packed address data. uint256 private constant _BITPOS_AUX = 192; // Mask of all 256 bits in packed address data except the 64 bits for `aux`. uint256 private constant _BITMASK_AUX_COMPLEMENT = (1 << 192) - 1; // The bit position of `startTimestamp` in packed ownership. uint256 private constant _BITPOS_START_TIMESTAMP = 160; // The bit mask of the `burned` bit in packed ownership. uint256 private constant _BITMASK_BURNED = 1 << 224; // The bit position of the `nextInitialized` bit in packed ownership. uint256 private constant _BITPOS_NEXT_INITIALIZED = 225; // The bit mask of the `nextInitialized` bit in packed ownership. uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225; // The bit position of `extraData` in packed ownership. uint256 private constant _BITPOS_EXTRA_DATA = 232; // Mask of all 256 bits in a packed ownership except the 24 bits for `extraData`. uint256 private constant _BITMASK_EXTRA_DATA_COMPLEMENT = (1 << 232) - 1; // The mask of the lower 160 bits for addresses. uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1; // The maximum `quantity` that can be minted with {_mintERC2309}. // This limit is to prevent overflows on the address data entries. // For a limit of 5000, a total of 3.689e15 calls to {_mintERC2309} // is required to cause an overflow, which is unrealistic. uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000; // The `Transfer` event signature is given by: // `keccak256(bytes("Transfer(address,address,uint256)"))`. bytes32 private constant _TRANSFER_EVENT_SIGNATURE = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; // ============================================================= // STORAGE // ============================================================= // The next token ID to be minted. uint256 private _currentIndex; // The number of tokens burned. uint256 private _burnCounter; // Token name string private _name; // Token symbol string private _symbol; // Mapping from token ID to ownership details // An empty struct value does not necessarily mean the token is unowned. // See {_packedOwnershipOf} implementation for details. // // Bits Layout: // - [0..159] `addr` // - [160..223] `startTimestamp` // - [224] `burned` // - [225] `nextInitialized` // - [232..255] `extraData` mapping(uint256 => uint256) private _packedOwnerships; // Mapping owner address to address data. // // Bits Layout: // - [0..63] `balance` // - [64..127] `numberMinted` // - [128..191] `numberBurned` // - [192..255] `aux` mapping(address => uint256) private _packedAddressData; // Mapping from token ID to approved address. mapping(uint256 => TokenApprovalRef) private _tokenApprovals; // Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private _operatorApprovals; // constructor(string memory _name, string memory _symbol) ERC721A(_name, _symbol) { // owner = msg.sender; // } // ============================================================= // CONSTRUCTOR // ============================================================= constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) { _name = name_; _symbol = symbol_; _currentIndex = _startTokenId(); } function mint(uint256 quantity) external { _safeMint(msg.sender, quantity); } function _baseURI() internal pure override returns (string memory) { return "https://gateway.pinata.cloud/ipfs/QmVJYFp12YVLtgwtnnD7KmAGL2gEFfTWBhzy2foGeeqcf1/"; } // function tokenURI(uint256 _tokenId) public view override returns (string memory) { // // _requireMinted(_tokenId); // string memory baseURI = _baseURI(); // return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, _tokenId.toString(), ".json")) : ""; // } function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { if (!_exists(tokenId)) _revert(URIQueryForNonexistentToken.selector); string memory baseURI = _baseURI(); return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), ".json")) : ''; } /** * @dev For more efficient reverts. */ function _revert(bytes4 errorSelector) internal pure { assembly { mstore(0x00, errorSelector) revert(0x00, 0x04) } } } ``` ## 2.Contract Factory 使用合約來部署多個 ERC20 或 ERC721 合約 * 開源合約並提交合約地址 * 每個產出的子合約可以設定不同的參數,name, symbol... 以下import一個合約後new新的合約出來使用: ``` // SPDX-License-Identifier: MIT pragma solidity 0.8.17; import "./ERC721-test.sol"; contract ContractFactory { MyERC721NFT[] public erc721TestArray; function createNewNFTSeries(string memory name_, string memory symbol_) external payable{ MyERC721NFT erc721Test = new MyERC721NFT({ _name : name_, _symbol : symbol_ }); erc721TestArray.push(erc721Test); } function getErc721TestArray() external view returns(uint256) { return erc721TestArray.length; } } ```