owned this note
owned this note
Published
Linked with GitHub
# Garrick信仲 | C0051202 Cryptocamp第5期Solidity工程師實戰營作業
## 第一週課程作業
<details>
### 1.請簡單描述錢包助記詞,私鑰,公鑰之間的關係
#### 錢包、助記詞、公鑰、私鑰
錢包就是一個身份認證或是一把鑰匙,而錢包助記詞的標準是BIP-39,助記詞先產生私鑰,再透過私鑰產生公鑰,公鑰產生公鑰雜湊,再取20Byte為錢包地址(例如Metamask上看到的位址), 公鑰雜湊不可以反推出公鑰,公鑰也沒辦法反推出私鑰。
### 2.用 Remix 部署智能合約(Local London)

## 基本題
### 3.Solidity TodoList 延伸
* 將寫好的合約部署到 Görli / Goerli 測試鏈上,並 verify 開源程式碼

* 請貼上合約地址
```
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畫面

---
#### 合約程式
實作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`

---
RT from Ryan, tGD from 企鵝

---
### 2.引用 OpenZeppelin ERC721 部署一個 ERC721 NFT 合約,同時擁有付費 mint 功能
* 開源合約並提交合約地址
* 使用 OpenSea Metadata 標準,並提交 OpenSea頁面
* 將檔案上傳至 ipfs
* 擁有白名單機制並且將組員加入白名單
* 設定總量上限
* 加入至少一種自定義功能,例如:荷蘭拍、盲盒、返佣、融合…
#### 合約地址
`0x7eb9Fa0cCf33eaa6f5a5818799c6851DBf497c2e`

---
#### 合約程式
程式說明:
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

將

總共傳了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`

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`

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;
}
}
```