深度解析Polygon 網絡結構、互操作性與跨鏈消息傳遞方式,再談雙花漏洞安全事件。
1 Polygon 是誰?
Polygon 是以太坊的layer2 擴容方案,其願景是建造以太坊的區塊鏈互聯網。 Polygon 提供了一個通用框架,允許開發人員利用以太坊的安全性創建定制的,專注於應用程序的鏈,並提供一個互操作性網絡,結合了各種不同的擴展方案,如:zk-rollup、PoS 等。其中,Polygon PoS 是目前Polygon 上最成熟和廣為人知的擴容方案。它利用側鏈進行交易處理,實現提升交易速度並節省Gas 消耗的目的,網絡結構主要包含以下三層:
Ethereum 層:
以太坊主網上的一系列合約,主要包括:Staking、Checkpoint、Reward 合約,負責PoS 權益相關的質押管理功能,包括:提供MATIC 原生代幣的質押功能,使得任何質押該代幣的人可以作為驗證者加入系統;驗證Polygon 網絡的轉態轉換獲得質押獎勵;懲罰驗證者的雙重簽名、驗證者停機等不合法行為;保存checkpoint。
Heimdall 層:
權益證明驗證節點層,包括一組PoS Heimdall 節點,負責將Polygon 網絡的檢查點提交給以太坊主網,同時監聽部署在以太坊上的一組質押合約。主要流程為:首先選擇驗證者池中的一部分活躍的驗證者作為塊生產者,它們將負責在Bor 層創建區塊並廣播;接著根據Bor 提交的檢查點,驗證Merkle 根哈希並附加簽名;最後,提議者將負責收集指定檢查點的所有驗證者簽名,如果簽名數量達到2/3 以上,則在以太坊上提交該檢查點。
Bor 層:
出塊節點層,包括一組由Heimdall 層上的驗證者委員會定期選取的區塊生產者,它們是一個驗證者子集,負責將Polygon 側鏈上的交易聚合併生成區塊。該層會定期向Heimdall 層發布檢查點(checkpoint),其中檢查點代表Bor 鏈上的一個快照,如下圖所示。
2 Polygon 互操作性
2.1 檢查點(checkpoint)
檢查點機制是一種將Bor 層的數據同步到以太坊上的機制,其中同步的數據是檢查點,即在一個檢查點間隔的時間段內包含的Bor 層區塊數據快照,源碼如下:
-
Proposer:提議者,它也是由驗證者選取的,區塊生產者和提議者都是驗證者的子集,且他們的責任取決於其在整個池子中的股權比例。
-
RootHash:是由StartBlock 到EndBlock 之間的Bor 塊生成的Merkle Hash。
以下是編號1 到n 的Bor 塊生成RootHash 值的偽碼:
綜上,該值是Bor 區塊頭中的區塊號number、區塊時間戳time、交易樹根Hash 值tx hash、收據樹根Hash 值receipt hash 計算得到的keccak256 哈希值構成的Merkel tree 的根哈希值。
AccountRootHash:需要將每個檢查點發送到以太坊上的驗證者相關賬戶信息的Merkle Hash,單個賬戶信息的哈希值計算方式如下:
由賬戶Merkle tree 根哈希值生成AccountRootHash 的方式與RootHash 值相同。
2.2 StateSync
狀態同步機制(StateSync)是指將以太坊數據同步到Polygon Matic 鏈,主要分為以下幾個步驟:
-
首先以太坊上的合約會觸發StateSender.sol 中的syncState() 函數進行狀態同步
-
syncState() 函數將發出一個event 事件,如下:
-
Heimdall 層的所有驗證者都會收到該事件,其中一個驗證者會將該交易打包到heimdall 區塊中,並添加到待處理的狀態同步列表中;
-
bor 層節點會通過API 獲取到上述待同步列表,交給bor 層的合約進行進一步的業務邏輯處理。
2.3 Polygon Bridge
Polygon Bridge 實現了Polygon 和Ethereum 之間的雙向跨鏈通道,使得用戶可以在兩個不同鏈平台之間更為方便地轉移代幣而不會產生第三方威脅和市場流動性限制。 Polygon Bridge 有PoS 和Plasma 兩種類型,二者在Polygon 和Ethereum 之間的資產轉移都有以下相同之處:
1)首先需要將Ethereum 上的代幣映射到Polygon,如下圖所示:
2)同樣採用雙向錨定技術(Two-way Peg),即
-
a:從以太坊上轉移的代幣資產都會先在Ethereum 上被鎖定,且相同數量的映射代幣會在Polygon 上被鑄造;
-
b:為了將代幣資產提取到Ethereum,首先需要將這些映射代幣在Polygon 上burn 掉,之後再解鎖鎖定在Ethereum 上的資產;
下圖為PoS Bridge 和Plasma Bridge 的對比:
由上圖可知,安全性方面,PoS Bridge 依賴於外部驗證者集合的安全性,而Plasma 依賴於Ethereum 主鏈的安全性。同時在用戶進行跨鏈資產轉移時(如將代幣從Polygon 轉移到Ethereum),PoS 僅需要一個檢查點的間隔時間,大約20 分鐘到3 小時;而Plasma 則需要一個7 天的爭議挑戰期。同時PoS 支持更多的標準代幣,而Plasma 僅支持三種類型,包括:ETH、ERC20、ERC721。
3 跨鏈消息傳遞—PoS Bridge
PoS Bridge 主要包含兩個功能:Deposit 和Withdrawals,其中Deposit 指的是將用戶在以太坊上的資產轉移到Polygon,Withdrawals 則指的是將資產從Polygon 提取到以太坊上。
Deposit
下面以用戶Alice 使用PoS Bridge 將其以太坊賬戶上的代幣資產發送到其在Polygon 賬戶中為例進行介紹:
1、如果用戶想轉移的代幣資產為ERC20、ERC721、ERC1155 類型,則首先需要用戶將要轉移的代幣通過approve 函數授權。如下所示:通過調用以太坊上token 合約中的approve 方法將對應數量的token 授權給erc20Predicate 合約。
其中approve 函數有兩個參數:
-
spender:用戶授權允許花費代幣的目標地址
-
amount:可以被花費的代幣數量
2、上述授權交易被確認後,用戶接著通過調用RootChainManager 合約的depositFor() 方法將代幣鎖定到以太坊上的erc20Predicate 合約中。此處,如果轉移的資產類型是ETH,則調用depositEtherFor()。具體如下:
其中depositFor 函數有三個參數:
-
user:接收Polygon 上deposit 代幣的用戶地址
-
rootToken:以太坊主鏈上的token 地址
-
depositData:ABI 編碼後的代幣數量
以下是RootChainManager 合約中depositFor 函數的具體代碼:
分析源碼可知,該函數首先獲取到token 對應的predicate 合約地址,接著調用其lockTokens() 函數將token 鎖定在該合約中。最後_stateSender 將調用syncState() 進行狀態同步,該函數只有admin 設置的狀態發送者(state sender)才能調用。
3、StateSender.sol 中的syncState() 函數將提交事件StateSynced,具體為:
其中第一個參數為該log 的序號索引,第二個參數用於校驗調用者是否是已註冊的合法合約地址,第三個是需要進行狀態同步的數據。該交易會被添加到Heimdall 塊中,並被添加到掛起的狀態同步列表中。
4、接著Polygon Matic 鏈上的bor 節點通過API 獲取到狀態同步列表中的StateSynced 事件後,該鏈上的ChildChainManager 合約會調用onStateReceive() 函數,該函數用於接收從以太坊上傳過來的同步數據,根據狀態同步的業務邏輯類型進行下一步處理:
data:包括bytes32 類型的syncType、bytes 類型的syncData。其中,syncType 代表業務類型,包括deposit 和mapping 代幣映射;當syncType 為mapping 時,syncData 為編碼後的rootToken 地址、childToken 地址和bytes32 類型的tokenType;當syncType 為deposit 時,syncData 為編碼後的user 地址、rootToken 地址和bytes 類型的depositData。 depositData 在REC20 中是數量,ERC721 中指的是tokenId。
5、由於此處進行的是Deposit 業務,所以接著會調用_syncDeposit() 函數。該函數會首先將syncData 按照對應格式解碼,得到對應的rootToken、user 地址、depositData。接著校驗rootToken 在polygon 上是否有對應的映射代幣childToken,如果有則調用childToken 的deposit() 函數。
6、此處我們以ERC20 的代幣合約為例,介紹映射代幣合約如何deposit。該函數將mint 對應數量的代幣到用戶賬戶中。
該函數有兩個參數:
-
user:正在進行存款的用戶地址
-
depositData:用ABI 編碼的amount
Withdrawals
下面以用戶Alice 使用PoS Bridge 將其在Polygon 賬戶中存放的資金提取到以太坊賬戶為例進行介紹:
1、當用戶withdraw 時,需要首先在Polygon 鏈上通過調用映射token 合約的withdraw() 函數,burn 掉對應數量的映射代幣。
withdraw 僅包含一個參數:將要被burn 掉的token 數量。對應的token 合約中的withdraw() 函數如下:
2、上述交易將經過大約20 分鐘到3 小時將被包含到checkpoint 中,被驗證者提交到以太坊。
3、一旦交易被添加到檢查點中並提交到了以太坊,將調用以太坊上的RootChainManager 合約的exit() 函數,該函數將通過驗證提交的檢查點內容確認在Polygon 上withdraw 交易的有效性,並觸發對應的Predicate 合約解鎖用戶deposit 的代幣。
其中,傳入該函數的Proof 證明inputData 包括以下數據:
-
headerNumber:包含了withdraw 交易的檢查點區塊header
-
blockProof:證明子鏈中的區塊頭是提交的merkle root 的葉子節點
-
blockNumber:子鏈上包含withdraw 交易的區塊號
-
blockTime:withdraw 交易的區塊時間戳
-
txRoot:區塊交易樹的root 值
-
receiptRoot:區塊收據樹的root 值
-
receipt:withdraw 交易的收據
-
receiptProof:withdraw 交易收據的默爾克證明
-
branchMask:收據樹中32 位表示的收據路徑
-
receiptLogIndex:從收據樹中讀取的日誌索引
下面是該函數的核心邏輯,主要包括三部分:第一部分是校驗withdraw 交易收據的有效性,第二部分是校驗檢查點是否包含了交易區塊,第三部分是調用predicate 合約中的exitTokens() 函數將鎖定的代幣發送給用戶。
4、以ERC20Predicate 合約為例,即從log 中解碼出接收者、發送者、發送代幣數量後,將給定數量的代幣發送給用戶。
由PoS Bridge 跨鏈消息傳遞過程源碼分析可知,整個過程的函數調用都只有驗證者指定的角色才能調用,所以跨鏈的安全性僅由PoS 保證(公證人)。
4 跨鏈消息傳遞—Plasma Bridge
Plasma Bridge 同樣包含兩個功能:Deposit 和Withdrawals,具體流程如下圖所示:
Polygon Plasma 與我們跨鏈橋系列第一篇文章介紹的比特幣Plasma MVP 實現略有差別,主要採用基於賬戶模型的Plasma MoreVP。該算法與Plasma 相比,主要在withdraw 部分做了部分改進。
由於ERC20、ERC721 的代幣傳輸,是通過類似比特幣UTXO 的event 日誌實現的,所以我們首先介紹一下該事件:
-
input1:轉賬前發送者的賬戶餘額
-
input2:轉賬前接收者的賬戶餘額
-
output1:轉賬後發送者的賬戶餘額
-
output2:轉賬後接收者的賬戶餘額
其次,原先的Plasma MVP,由於區塊是由單個(Operator)或者少數的區塊生產者生成,因此在Polygon 上存在以下兩種攻擊場景:
Operator 作惡:
上一篇文章(《跨鏈橋安全回顧:Nomad 去中心化搶劫事件帶給我們什麼啟發?》)提到,當用戶的交易被Operator 打包為Plasma 區塊後,存在鏈下數據的不可用性問題。因此,用戶在進行exit 交易時,如果從較舊的交易開始退出,Operator 可以使用其最近的一筆交易對其發起挑戰,則會挑戰成功。同時,由於Plasma 中採用了PoS 的檢查點機制,Operator 如果勾結驗證者作惡,甚至可以偽造一些狀態轉換並提交到以太坊。
用戶作惡:
用戶在發起exit 交易後,繼續在Polygon 上花費代幣,類似於跨鏈的雙花。
綜上,Polygon 的Plasma MoreVp 算法採用了另一種計算退出優先級的算法,即從最近的交易開始退出。該方式由於使用了類似UTXO 的LogTransfer 事件,只要用戶的合法交易使用了正確的input1、input2,即使Operator 一些惡意交易打包在用戶交易之前,由於用戶交易僅來自有效的input,所以也能被正確處理。相關偽代碼如下:
Deposit
下面以用戶Alice 使用Plasma Bridge 將其以太坊賬戶上的代幣資產發送到其在Polygon 賬戶中為例進行介紹:
1、首先用戶同樣需要將其需要轉移的代幣資產通過approve 函數授權給主鏈(Ethereum)上的Polygon 合約depositManager。
2、同樣等到授權交易被確認後,用戶調用erc20token.deposit() 函數,觸發depositManager 合約的depositERC20ForUser() 函數,存入用戶的ERC20 代幣資產。
3、當以太坊主網確認了該deposit 交易,接下來會創建一個僅包含這筆交易的區塊,並將其採用狀態同步機制發送到Polygon 網絡上的childChain 合約中,mint 相同數量的映射幣並存入用戶在Polygon 上的賬戶。
注:由childChain 合約源碼分析可知,Plasma 僅支持三種類型,包括:ETH、ERC20、ERC721。
Withdraw
當用戶想使用Plasma bridge 從Polygon 上提取資產到以太坊上,會經歷以下幾個步驟:
1、用戶通過調用Polygon 上映射幣的withdraw() 函數,burn 掉Polygon 鏈上的映射代幣資產:
也可以調用Polygon 上的Plasma Client 的withdrawStart() 接口實現。
2、用戶可以調用ERC20Predicate 合約中startExitWithBurntTokens() 函數,該函數首先會調用WithdrawManager.verifyInclusion() 校驗checkpoint 是否包含withdraw 交易和對應的收據,代碼如下:
驗證通過後,將調用WithdrawManager.addExitToQueue() 將其按照優先級排序插入到消息隊列中:
最後,addExitToQueue() 調用_addExitToQueue() 鑄造一個NFT 作為退款憑證:
3、用戶等待7 天的挑戰期
4、挑戰期完成,可以調用WithdrawManager.processExits() 函數將代幣發送給用戶。
該函數主要分為兩個步驟:首先確認消息隊列中的withdraw 交易是否已經過了7 天挑戰期,如果已經超過挑戰期則將其該交易移除隊列:
接著,判斷退款憑證NFT 是否在挑戰期內被刪除,未被刪除則將該NFT 銷毀並將對應資產退還給用戶:
5 Polygon Plasma Bridge 雙花漏洞
2021 年10 月5 日,白帽子Gerhard Wagner 提交了一個Polygon 漏洞,該漏洞可能導致雙花攻擊,涉及到的金額為8.5 億美元,白帽子因此獲得了Polygon 官方的2,000,000 美元漏洞賞金。
在前文Plasma Bridge 的介紹中我們知道,完整的一次Withdraw 交易過程為:
-
用戶在Polygon 上發起Withdraw 交易,該交易會burn 掉用戶在Polygon 的代幣;
-
經過一個檢查點間隔(大約30 分鐘),等待該withdraw 交易被包含到檢查點中;
-
超過2/3 的驗證者簽名後將其提交到以太坊,此時用戶調用ERC20PredicateBurnOnly 合約中的startExitWithBurntTokens() 校驗checkpoint 是否包含burn 交易;
-
校驗通過,則鑄造一個NFT 退款憑證發給用戶
-
用戶等待7 天挑戰期
-
調用WithdrawManager.processExits() 銷毀NFT,並退款給用戶
注意:Polygon 為了防止交易重放(雙花攻擊),使用NFT 作為退款憑證,來唯一標識一筆Withdraw 交易。但是,由於NFT 的ID 生成缺陷,造成了攻擊者可以構造參數利用同一筆有效的Withdraw 交易,生成多個不同ID 的NFT,再利用這些NFT 進行退款交易,從而實現「雙花攻擊」。
下面將對如何如何生成NFT 進行詳細介紹:
1、由上文中的源碼解析可知,addExitToQueue() 會調用_addExitToQueue() 鑄造一個NFT:
由傳參分析可知,exitid = priority,則NFT 的ID 即為Plasma Bridge 中的age 優先級左移一位生成。
2、上文的源碼解析可知,age 是WithdrawManager.verifyInclusion() 函數的返回值,該函數會首先校驗withdraw 交易的有效性,校驗通過則生成對應的age。其中,校驗的邏輯中使用了可控參數data 解碼出的值branchMaskBytes:
同時生成age 時也使用了該值:
3、跟踪交易驗證邏輯中的調用的MerklePatriciaProof.verify() 函數,發現該函數調用_getNibbleArray() 對branchMaskBytes 進行了轉碼操作:
4、繼續跟踪該解碼函數,該函數對branchMaskBytes 轉碼時存在丟棄部分值的情況,這種數值丟失的方式會造成不同的值轉碼後獲得同樣的解碼值。具體為:如果傳入的hp 編碼後的值b 的第一個十六進制位(半個字節)是1 或3,就解析第二個十六進制位。否則,就直接忽略第一個字節。
那麼如果攻擊者構造一個branchMaskBytes 參數,使得其第一個十六進制位不等於1 和3,則共有14*16 = 224 種方式,能夠獲得相同的轉碼後的值。
具體的攻擊流程為:
-
通過Polygon Plasma 向Polygon 存入大量ETH/ 代幣
-
在Polygon 上發起Withdraw 交易,等待7 天的挑戰期
-
修改withdraw 交易中branchMaskBytes 參數的第一個字節(同一有效交易最多可以重新提交223 次),重複發起Withdraw 交易
綜上,該漏洞主要是由於生成防止重放的退款憑證NFT 的ID 算法設計存在問題,導致相同的退款交易可以生成不同的NFT,造成雙花攻擊。事實證明,編碼分支掩碼的第一個字節應該始終是0x00. 修復方法是檢查編碼的分支掩碼的第一個字節是否是0x00 並且不要將其視為不正確的掩碼。
原文標題:《跨鏈橋安全研究( 三) | 多邊形戰士Polygon 安全透析,如何預防「潘多拉魔盒」的開啟? 》
撰文:成都鏈安
來源:ForesightNews