ERC404是近期非常紅的提案,主打圖幣結合,也就是一個合約同時擁有ERC20及ERC721的特性,並擁有以下特徵:
- 持有一個 NFT 就代表你也持有一顆 ERC20,持有一顆 ERC20 就代表你也持有一顆NFT。(ps: 被設為白單的地址,不會有 NFT,詳細後續會提到…)
重新鑄造
一個 NFT,發送方會減少一顆 ERC20 及燒毀一個 NFT。(燒毀的 NFT會是狀態變數陣列中最後一個 tokenId)- 交易/轉移一個 NFT,接收方會增加一顆 ERC20 及收到該 NFT,發送方會減少一顆 ERC20 及發送的 NFT。
- 交易/轉移一顆 ERC20,接收方會增加一顆 ERC20 及
重新鑄造
一個 NFT,發送方會減少一顆 ERC20 及燒毀一個 NFT。(燒毀的NFT會是狀態變數陣列中最後一個 tokenId)
上述特性,讓 NFT 碎片化,並可以不斷的交易 ERC20 來重新鑄造新的 NFT,讓 NFT 出現不喜歡就可以刷掉重新鑄造的可能。
- Pandora 則是該提案合約的第一個項目,地址見下方連結:
https://etherscan.io/token/0x9e9fbde7c7a83c43913bddc8779158f1368f0413?a=2656#code
- 這篇來記錄整個 ERC404 的運作邏輯,逐行解釋程式碼運行邏輯,如果大家耐心看完應該會有所收穫,如果有任何其他想法也歡迎指教喔。
導讀開始…
引用
首先是合約的引用部分,他引用了 Ownable.sol 及 ERC721Receiver.sol 合約。
引用 Ownable 合約的用途就是引入管理者
的角色,使得某些 function 得以得到控制,讓一般用戶沒有權限操作。而引用 ERC721Receiver 合約的原因是因為他有實作ERC721 也有的 safeTransferFrom 功能,也就是確保轉移 NFT 目標是合約時,NFT 不會被卡在該合約出不來。
狀態變量&事件&錯誤
接下來先來看ERC404所有的狀態變量、事件、錯誤。
- 首先這個 contract 宣告的前面多了一個
abstract
字樣,說明他是一個抽象合約,其中有些 function 需要被繼承的合約覆寫(override)來實作
。 - 接著宣告一堆
event
,這些 event 只是用來當完成某些行為後拋出,以利前端接收到某行為已完成。宣告的 event 見下方:
- ERC20Transfer: 當完成 ERC20 轉移時,會拋出此 event。
- Approval: 當完成不管是 ERC20 或 ERC721 層級的 approve,都會拋出此event。
- Transfer: 當完成不管是ERC20或ERC721層級的 transfer,都會拋出此 event。
- ERC721Approval: 當完成ERC721 approval時拋出的 event,
痾….但他宣告了根本沒用到…
。 - ApprovalForAll: 當完成 ERC721 層級的 approvalForAll 時拋出的 event。
接下來定義了一些 error
,主要用來在程式發生錯誤的時候識別是那種錯誤。
- NotFound: 當呼叫 ownerOf 時,發現這個 TokenId 沒有人持有時,回報的錯誤。
- AlreadyExists: 當在呼叫 _mint 時,發現這個 tokenId 已經有人持有時,回報的錯誤。
- InvalidRecipient: 當在 transferFrom 或 _mint NFT 時,如果指定接收 NFT 的對象是
零地址
將會拋出此錯誤。 - InvalidSender: 在 transferFrom 時,如果 NFT 發送方不是本人,將拋出此錯誤,或者在 _burn NFT 時,如果要 burn NFT 的地址指派為
零地址
,依然會拋出此錯誤。 - UnsafeRecipient: 呼叫 safeTransferFrom 時,會去檢查接收方次否有實作onERC721Received,並回傳 ERC721Receiver.onERC721Received.selector,以證明接收方是可以收 NFT 的合約地址,如果沒實作或回傳不如預期,則拋出此錯誤。
接下來定義了一堆狀態變數
:
- name: 此資產的 name,既是 ERC20 代幣 name,也是 ERC721 name。
- symbol: 此資產的symbol,既是ERC20代幣symbol,也是ERC721的symbol。
- decimals: 專給
ERC20
用的狀態變量,預設為18,與 ERC20 合約預設一樣。 - totalSupply: 專給
ERC20
用的狀態變量,表示發行總數量。 - minted: 專給
ERC721
用的狀態變量,表示目前 NFT Mint 到幾號。 - balanceOf: 專給
ERC20
用的狀態變量,是一個 mapping 結構,記錄某地址持有的 ERC20 幣量。 - allowance: 專給
ERC20
用的狀態變量,是一個巢狀的 mapping 結構,記錄某地址授權給某地址多少 ERC20 代幣,Ex: A 地址授權給 B 地址動用他 1 顆ERC20代幣(allowance[A][B] = 1 * 10 **18) - getApproved: 專給
ERC721
用的狀態變量,是一個 mapping 結構,記錄某地址授權哪個 tokenId 的 NFT 給哪個地址。 - isApprovedForAll: 專給
ERC721
用的狀態變量,是一個巢狀的 mapping 結構,記錄某地址是否授權給某地址所有 NFT 操作的權限。Ex: A地址授權所有NFT給B地址,isApprovedForAll[A][B] = true。 - _ownerOf: 專給
ERC721
用的狀態變量,是一個 mapping 結構,記錄某 NFT tokenId 的擁有者是那個地址。 - _owned: 專給
ERC721
用的狀態變量,是一個 mapping 結構,記錄某地址持有的所有 tokenId 陣列。 - ownedIndex: 用來輔助 owned mapping 的 mapping 結構,記錄某 tokenId 在_owned 中某地址持有的列表中的 index 資訊。Ex: 地址A擁有tokenIds有0,2,4,_owned[A] = [0, 2, 4],而ownedIndex的記錄會長得像這樣:ownedIndex[0] = 0, ownedIndex[2] = 1, ownedIndex[4] = 2。
- whitelist: 是一個 mapping 結構,基本上 ERC404 在轉移時,可能伴隨著 burn和 mint 的行為,輸入為白名單的地址,如果他是發送方,將不會 burn NFT,如果他是接收方,將不會 mint NFT。
建構子(Constructor)
接下來來說明他的建構子,他的建構子其實就是讓原本 ERC20 或 ERC721 可能需要預先設定的狀態變量全部放上來。直接看程式碼:
輸入參數有五個,底下依序說明:
- _name: 直接指定給 name 狀態變量,作為此 ERC404 資產的 name(ERC20及ERC721的name)
- _symbol: 直接指定給 symbol 狀態變量,作為此 ERC404 資產的 symbol(ERC20 及 ERC721 的 symbol)
- _decimals: 直接指定給 decimals 狀態變量,作為之後 ERC20的最小單位。
- _totalNativeSupply: 直接指定給 totalSupply 狀態變量,表示 ERC20 發行總數。
- _owner: 直接指定給繼承的 Ownable.sol 合約的 owner 狀態變量,代表此ERC404合約的擁有者。
Function
接下來來介紹 ERC404 中的所有 function,並說明 funtion 中的實作細節,每個 function 我會用三個小 topic 介紹,分別為說明、參數及運作,說明的部分會大致解釋function 用途,參數會介紹各參數用意,運作會逐行介紹程式碼行為,到這邊可以泡杯咖啡,然後開始接下來的內容吧。
一、setWhitelist
說明:
這個 function 是用來設定白名單用的,被設為白名單的地址,只涉及 ERC20 的操作,不涉及 NFT 的操作(burn & mint)。
參數:
1. target: 欲設定為白名單的地址。
2. state: 若為 true,代表 target 地址將設中為白名單,反之,則非白名單。
運作:
Step1: 將 whitelist 這個 mapping 結構的狀態變量添增一筆紀錄,讓 target 地址,對應到 state,若 state 為 true 則表示t arget 為白名單,反之,則不為白名單。
二、ownerOf
說明:
這個 function 與 ERC721 的 ownerOf 基本上功能一樣,就是查詢某 NFT tokenId 是那個地址所持有。
參數:
1. id: NFT 的 tokenId。
運作:
Step1: 直接呼叫狀態變量 _ownerOf 這個mapping結構,取得相應結果
Step2: 判斷owner是否為零地址
,如果為零地址,則拋出NoFound Error
。
三、tokenURI
說明:
這個 function 與 ERC721 的 tokebURI 基本上功能一樣,但這邊將其抽象化,因此,繼承 ERC404 合約的子類別需要實作此 function,此 function 的用途為輸出 NFT 的 token metadata 記錄位址(ex: IPFS 位址)。
參數:
1. id: NFT 的 tokenId。
運作:
交由繼承的子類別自行實作。
四、approve
說明:
approve function 大家應該也不陌生,主要就是用來授權 NFT 或 ERC20 代幣給第三方使用的功能。這邊的 approve 結合了 ERC721 及 ERC20 的 approve,也就是同一個approve function,可以用來 approve ERC20,也可以用來 approve NFT。
參數:
1. spender:要授權的第三方地址
2. amountOrId:ERC20 的 amount 或 ERC721 的 tokenId,下面會詳細說明他如何區分帶入的數值是 amount 還是 tokenId 。
運作:
- Step1: 判斷輸入的參數
amountOrId
的值是否小於目前 NFT 的發行數量且大於 0,如果是,則amountOrId 被當作 tokenId
,否則amountOrId 被當作 amount。
這樣的判斷方式可以初步推估原因如下,因為通常 ERC20 的amount 是以最小單位計量,預設是 10 **18,所以 amount 通常不會太小,大多都是天文數字,但如果真的想要授權超小量的 amount,可能會被誤判為tokenId
。 - Step2-1: 若判斷為 tokenId:
Step2–1–1: 取出該 tokenId 的 owner 地址。
Step2–1–2: 判斷若發起交易的人不為 tokenId 的 owner,或沒有被授權可以操作該NFT,則拋出錯誤。
Step2–1–3: 若 Step2 通過,則添增 getApproval 記錄,以授權 spender 可動用該 tokenId 的NFT。
Step2–1–4: 拋出 Approval 事件,以利前端得知完成 Approve 行為。
Step2–2: 若判斷為 amount:
Step2–2–1: 直接添增 allowance 記錄,以授權 spender 可動用 amount 個ERC20 代幣。
Step2–2–2: 拋出 Approval 事件,以利前端得知完成 Approve 行為。
五、setApprovalForAll
說明:
setApprovalForAll function 應該不難理解,就是新增/修改狀態變量 isApprovedForAll,以記錄呼叫者是否授權所有 NFT 操作權給 operator。
參數:
1. operator : 呼叫者要授權所有 NFT 操作權的對象。
2. approved : 是否要授權,true 表示授權,false 則表是不授權
。
運作:
Step1: 直接設定狀態變量 isApprovedForAll,若 approved 為 true ,則授權 operator 動用呼叫者的所有 NFT。
Step2: 拋出 ApprovalForAll 事件,以利前端得知完成 ApprovalForAll 行為。
六、transferFrom
說明:
transferFrom function 不管是 ERC20 或 ERC721 都有此 function,在 ERC404 中,亦將它整合在一起,主要用途即為轉移 NFT 或 ERC20 代幣。
參數:
1. from : 資產 (ERC20/ NFT) 的持有者。
2. to : 資產 (ERC20/ NFT) 的接收者。
3. amountOrId : 資產 (ERC20/ NFT) 的 amount 或 tokenId。
運作:
- Step1: 判斷輸入的參數
amountOrId
的值是否小於目前 NFT 的發行數量且大於0,如果是,則amountOrId 被當作 tokenId
,否則amountOrId 被當作 amount。
這樣的判斷方式可以初步推估原因如下,因為通常 ERC20 的amount 是以最小單位計量,預設是 10 **18,所以 amount 通常不會太小,大多都天文數字,但如果真的想要授權超小量的 amount,可能會被誤判為tokenId
。 - Step2–1: 若判斷為tokenId:
Step2–1–1: 判斷如果 NFT tokenId 的擁有者不是 from 參數帶入的地址,怎拋出InvalidSender Error。
Step2–1–2: 判斷如果 to 參數為零地址,怎拋出 InvalidRecipient Error
Step2–1–3: 判斷如果 from 參數不是呼叫者,且呼叫者必沒有被授權操作此NFT,則拋出 Unauthorized Error。
Step2–1–4: 將 from 的 balance 從 balanceOf 的 mapping 中減少一顆ERC20 代幣,因為一個 NFT 對應一顆 ERC20。
Step2–1–5: 將 to 的 balance 從 balanceOf 的 mapping 中增加一顆 ERC20代幣。
Step2–1–6: 更改 ownerOf mapping 中 tokenId 對應的 owner,將其改為 to地址。
Step2–1–7: 清掉 getApproval mapping 中關於此 NFT tokenId 的記錄,以免讓曾授權過的地址能操作此 NFT。
Step2–1–8: 接下來這步驟主要要來更新記錄用戶 NFT 持有 tokenId 列表的_owned mapping 結構,透過以下步驟完成:
Step2–1–8–1 : 取得 from 地址當前在 _owned mapping 記錄的 tokenId列表中的最後一個 tokenId,存於 updateId
區域變量。
Step2–1–8–2 : 由於 from 地址轉移了 NFT 給 to 地址,所以需要將轉移掉的 tokenId 從 _owned mapping 中的 list 中移除,故先從_ownedIndex中取得轉移的 NFT tokenId 在 owned[from]中的第幾個 index,再將該 index 對應的內容更改為 updateId
。
Step2–1–8–3 : 刪除 _owned[from] List 中最後一個元素,因為已經挪往前蓋掉轉移的 NFT tokenId 了。
Step2–1–8–4 : 更新記錄 _owned tokenId 列表中各 tokenId 對應 index的 mapping 結構- _ownedIndex
,將原本記錄 updateId 對應的 index更改為原轉移 NFT tokenId 所在的 index。
Step2–1–8–5 : 將 _owned[to] 對應的持有 tokenId List 添增收到的 NFT tokenId。
Step2–1–8–6 : 將記錄 _ownedIndex 的 mapping 添增該轉移 tokenId 的 index 為持有 tokenId 列表的長度 -1,也就是持列列表中的最後一個 index。
Step2–1–8–7 : 拋出 Transfer 事件,以利前端得知 NFT 轉移。
Step2–1–8–8: 拋出 ERC20Transfer 事件,以利前端得知 ERC20 代幣轉移。
- Step2–2: 若判斷為 amount:
Step2–2–1: 讀取 allowance mapping 內容,取得 from 地址授權給呼叫者可操作的 ERC20 代幣數量,並記錄在 allowed
區域變量。
Step2–2–2: 判斷 allowed 內容是否不等於 uint256 的上限
, 如果是,怎跳過此步驟,如果否,將 allowed 的數量減上 amountOrId。此判斷說明如果透過 approve 將 allowance 設為 uint256 上限,則可以免去這個減法運算,猜測是用來降低 gas fee 用的,意思即為永遠授權所有代幣的意思。
Step2–2–3: 執行_transfer 這個 internal function,主要用來執行真正的ERC20 轉移行為,後面會詳細說明這邊的轉移邏輯,這邊先直接理解為執行完即代表 ERC20 完成轉移。
七、transfer
說明:
這個 function 就是單純轉移 ERC20 代幣的 function。
參數:
1. to : ERC20轉移對象。
2. amount : ERC20 轉移數量。
運作:
Step1: 直接呼叫_transfer internal function,主要用來執行真正的ERC20轉移行為,後面會詳細說明這邊的轉移邏輯。
八、safeTransferFrom
說明:
這個 function 基本上和 ERC721 的功能大同小異,就是轉移 NFT,但會檢查接收方是否是合約,如果是合約,會進一步檢查他是否實作 onERC721Received ,以證明他是可以接收 NFT 的合約。
參數:
1. from : 資產 (ERC20/ NFT) 的持有者。
2. to : 資產 (ERC20/ NFT) 的接收者。
3. id : NFT 的 tokenId 。
運作:
- Step1: 執行上面介紹的 transferFrom 功能。
- Step2:
- 判斷接收方
- 1)是否為合約?
- 2) 是否有實作 onERC721Received function 且回傳值為ERC721Receiver.onERC721Received.selector ?
- 判斷接收方
若接收方為沒有實作 onERC721Received function 的合約或是 onERC721Received 的回傳值不為 ERC721Receiver.onERC721Received.selector,則拋出 UnsafeRecipient Error。
九、safeTransferFrom
說明:
和上面的 safeTransferFrom 基本上功能一模一樣,但這邊多了一個參數 data ,主要用於驗證接收方的部分,並不直接影響 transferFrom 的功能。
參數:
1. from : 資產 (ERC20/ NFT) 的持有者。
2. to : 資產 (ERC20/ NFT) 的接收者。
3. id : NFT 的 tokenId 。
4. data: 16進制的附加資料。
運作:
- Step1: 執行上面介紹的 transferFrom 功能。
- Step2: 判斷接收方
- 1)是否為合約?
- 2) 是否有實作 onERC721Received function 且回傳值為ERC721Receiver.onERC721Received.selector ?
若接收方為沒有實作 onERC721Received function的 合約或是 onERC721Received 的回傳值不為 ERC721Receiver.onERC721Received.selector,則拋出 UnsafeRecipient Error。
十、transfer
說明:
這個 function 是上述 transferForm、safeTransferFrom 的底層轉移內部 function,執行實際的轉移邏輯,此 function 主要針對 ERC20 代幣的轉移。
參數:
1. from: 資產 (ERC20/ NFT) 的持有者。
2. to : 資產 (ERC20/ NFT) 的接收者。
3. id : NFT 的 tokenId 。
運作:
- Step1: 先取得初始化的 ERC20 基本單位,儲存在區域變數
unit
中。。(Ex: 10 ** 18) - Step2: 取得 from 地址擁有的 ERC20 代幣數量(轉移前),儲存在區域變數
balanceBeforeSender
中。 - Step3: 取得 to 地址擁有的 ERC20 代幣數量(接收前),儲存在區域變數
balanceBeforeReceiver
中。 - Step4: 修改 balanceOf mapping,將 from 地址代幣持有量減少
amount
。 - Step5: 修改 balanceOf mapping,將 to 地址代幣持有量增加
amount
。 - Step6: 判斷 from 地址是否為白名單,如果是白單,則跳過此步驟,如果不是白單,則…
Step 6–1: 算 balanceBeforeSender(取整數)
- balanceOf[from](取整數)
的數量,得出的數字為要燒掉多少 NFT。
Step6–2: 跑一個 for 迴圈一個一個呼叫 _burn 這個 internal function,以燒掉NFT,燒掉的數量即為 Step6–1 算出來的數量,_burn internal function 會在後面詳細說明運作邏輯。
- Step7: 判斷 to 地址是否為白單,如果是白單,則跳過此步驟,如果不是白單,則…
Step7–1: 算 balanceOf[to](取整數)
- balanceBeforeReceiver(取整數)
的數量,得出的數字要重新鑄造的 NFT。
Step7–2: 跑一個 for 迴圈一個一個呼叫 _mint 這個 internal function,以重新鑄造NFT,燒鑄造的數量即為 Step7–1 算出來的數量,_mint internal function 會在後面詳細說明運作邏輯。
十一、_getUnit
說明:
回傳一顆 ERC20 以最小單位計數的 amount。Ex: 一顆 ERC20 = 10 ** 18,那就會回傳 10 **18。
參數:
無
運作:
- Step1 : 直接回完傳一顆 ERC20 以最小單位計數的 amount。
十二、_mint
說明:
此 function 主要是鑄造 NFT 的 function,主要是用來更改此合約的狀態變量,以表示NFT 被鑄造出來,鑄造方式為依序鑄造。
參數:
1. to : 接收 NFT 的地址
運作:
- Step1: 判斷
from
地址是否為零地址,如果是,怎拋出 InvalidSender Error。 - Step2: 將
_owned[from]
中的 tokenId 列表中的最後一個 index 對應的tokenId 取出來,並指派給區域變量id
。 - Step3: 從
_owned[from]
刪除最後一個 tokenId。 - Step4: 刪除
_ownedIndex
mapping 中與id
有關的內容。 - Step5: 刪除
_ownerOf
中與id
有關的內容。 - Step6 : 刪除
getApproved
中與id
有關的內容。 - Step7 : 拋出 Transfer 事件,to 參數為
零地址
。
十三、_burn
說明:
此 function 主要是燒毀 NFT 的 function,主要是用來更改此合約的狀態變量,以表示NFT 被燒毀,燒毀方式為燒毀 owned[ from ] 這個 list 中最後一個 tokenId 的 NFT。
參數:
1. from: 要燒掉 NFT 的持有者地址。
運作:
- Step1: 判斷
from
地址是否為零地址,如果是,怎拋出 InvalidSender Error。 - Step2: 將
_owned[from]
中的 tokenId 列表中的最後一個 index 對應的tokenId 取出來,並指派給區域變量id
。 - Step3: 從
_owned[from]
刪除最後一個 tokenId。 - Step4: 刪除
_ownedIndex
mapping 中與id
有關的內容。 - Step5: 刪除
_ownerOf
中與id
有關的內容。 - Step6 : 刪除
getApproved
中與id
有關的內容。 - Step7 : 拋出 Transfer事件,to 參數為
零地址
。
十四、_setNameSymbol
說明:
用於設定資產 name 與 symbol的function。
參數:
1. _name: 新的資產 name。
2. _symbol: 新的資產 symbol。
運作:
- Step1: 指派參數
_name
值給狀態邊量name
。 - Step2: 指派參數
_symbol
值給狀態邊量symbol
。
以上就是所有程式碼的導讀,如果有說明錯誤的地方歡迎指教,這邊單純分享我的解讀內容,希望對大家有幫助^^。
溫馨提醒: 這個 ERC404 的程式碼並非官方標準通過的程式碼,可能存在漏洞風險。