ERC404 合約程式碼導讀

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 見下方:
  1. ERC20Transfer: 當完成 ERC20 轉移時,會拋出此 event。
  2. Approval: 當完成不管是 ERC20 或 ERC721 層級的 approve,都會拋出此event。
  3. Transfer: 當完成不管是ERC20或ERC721層級的 transfer,都會拋出此 event。
  4. ERC721Approval: 當完成ERC721 approval時拋出的 event,痾….但他宣告了根本沒用到…
  5. ApprovalForAll: 當完成 ERC721 層級的 approvalForAll 時拋出的 event。

接下來定義了一些 error,主要用來在程式發生錯誤的時候識別是那種錯誤。

  1. NotFound: 當呼叫 ownerOf 時,發現這個 TokenId 沒有人持有時,回報的錯誤。
  2. AlreadyExists: 當在呼叫 _mint 時,發現這個 tokenId 已經有人持有時,回報的錯誤。
  3. InvalidRecipient: 當在 transferFrom 或 _mint NFT 時,如果指定接收 NFT 的對象是零地址 將會拋出此錯誤。
  4. InvalidSender: 在 transferFrom 時,如果 NFT 發送方不是本人,將拋出此錯誤,或者在 _burn NFT 時,如果要 burn NFT 的地址指派為零地址 ,依然會拋出此錯誤。
  5. UnsafeRecipient: 呼叫 safeTransferFrom 時,會去檢查接收方次否有實作onERC721Received,並回傳 ERC721Receiver.onERC721Received.selector,以證明接收方是可以收 NFT 的合約地址,如果沒實作或回傳不如預期,則拋出此錯誤。

接下來定義了一堆狀態變數 :

  1. name: 此資產的 name,既是 ERC20 代幣 name,也是 ERC721 name。
  2. symbol: 此資產的symbol,既是ERC20代幣symbol,也是ERC721的symbol。
  3. decimals: 專給ERC20 用的狀態變量,預設為18,與 ERC20 合約預設一樣。
  4. totalSupply: 專給ERC20 用的狀態變量,表示發行總數量。
  5. minted: 專給 ERC721 用的狀態變量,表示目前 NFT Mint 到幾號。
  6. balanceOf: 專給 ERC20 用的狀態變量,是一個 mapping 結構,記錄某地址持有的 ERC20 幣量。
  7. allowance: 專給 ERC20 用的狀態變量,是一個巢狀的 mapping 結構,記錄某地址授權給某地址多少 ERC20 代幣,Ex: A 地址授權給 B 地址動用他 1 顆ERC20代幣(allowance[A][B] = 1 * 10 **18)
  8. getApproved: 專給 ERC721 用的狀態變量,是一個 mapping 結構,記錄某地址授權哪個 tokenId 的 NFT 給哪個地址。
  9. isApprovedForAll: 專給 ERC721 用的狀態變量,是一個巢狀的 mapping 結構,記錄某地址是否授權給某地址所有 NFT 操作的權限。Ex: A地址授權所有NFT給B地址,isApprovedForAll[A][B] = true。
  10. _ownerOf: 專給 ERC721 用的狀態變量,是一個 mapping 結構,記錄某 NFT tokenId 的擁有者是那個地址。
  11. _owned: 專給 ERC721 用的狀態變量,是一個 mapping 結構,記錄某地址持有的所有 tokenId 陣列。
  12. 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。
  13. whitelist: 是一個 mapping 結構,基本上 ERC404 在轉移時,可能伴隨著 burn和 mint 的行為,輸入為白名單的地址,如果他是發送方,將不會 burn NFT,如果他是接收方,將不會 mint NFT。

建構子(Constructor)

接下來來說明他的建構子,他的建構子其實就是讓原本 ERC20 或 ERC721 可能需要預先設定的狀態變量全部放上來。直接看程式碼:


 

輸入參數有五個,底下依序說明:

  1. _name: 直接指定給 name 狀態變量,作為此 ERC404 資產的 name(ERC20及ERC721的name)
  2. _symbol: 直接指定給 symbol 狀態變量,作為此 ERC404 資產的 symbol(ERC20 及 ERC721 的 symbol)
  3. _decimals: 直接指定給 decimals 狀態變量,作為之後 ERC20的最小單位。
  4. _totalNativeSupply: 直接指定給 totalSupply 狀態變量,表示 ERC20 發行總數。
  5. _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: 刪除 _ownedIndexmapping 中與 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: 刪除 _ownedIndexmapping 中與 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 的程式碼並非官方標準通過的程式碼,可能存在漏洞風險。

完整程式碼