我们将选取一些出名的跨链桥协议,分析其关键技术,以及可能面临的安全问题。

欢迎大家来到 Beosin 出品的 “跨链桥安全研究” 系列文章,在上一篇文章里(跨链桥安全研究 (二) | 首次去中心化抢劫 Nomad 跨链桥事件带给我们什么启发?),我们详细对 Nomad 跨链桥协议进行专业的技术分析。

今天,Beosin 研究团队将对多边形战士 Polygon 安全透析,请继续往下看。 

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 链上的一个快照,如下图所示。

图片
 

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 链,主要分为以下几个步骤:

1)首先以太坊上的合约会触发 StateSender.sol 中的 syncState() 函数进行状态同步

2)syncState() 函数将发出一个 event 事件,如下:

图片
3)Heimdall 层的所有验证者都会收到该事件,其中一个验证者会将该交易打包到 heimdall 区块中,并添加到待处理的状态同步列表中;

4)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。

跨链消息传递—PoS BridgePoS 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 保证(公证人)。

跨链消息传递—Plasma BridgePlasma 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 销毁并将对应资产退还给用户:

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 并且不要将其视为不正确的掩码。

图片

免责声明:作为区块链信息平台,本站所发布文章仅代表作者及嘉宾个人观点,与 Web3Caff 立场无关。本文内容仅用于信息分享,均不构成任何投资建议及要约,并请您遵守所在国家或地区的相关法律法规。