BRC20 真的懂 Web3.0 ?Yuga
智能合约安全审计入门篇 —— 抢跑
背景概述
在上篇文章中我们了解了合约中隐藏的恶意代码,本次我们来了解一个非常常见的攻击手法 —— 抢跑。
前置知识
提到抢跑,大家第一时间想到的一定是田径比赛,在田径运动中各个选手的体能素质几乎相同,起步越早的人得到第一名的概率越大。那么在以太坊中是如何抢跑的呢?
想了解抢跑攻击必须先了解以太坊的交易流程,我们通过下面这个发送交易的流程图来了解以太坊上一笔交易发出后经历的流程:
可以看到图中一笔交易从签名到被打包一共会经历 7 个阶段:
1. 使用私钥对交易内容签名;
2. 选择 Gas Price;
3. 发送签名后的交易;
4. 交易在各个节点之间广播;
5. 交易进入交易池;
6. 矿工取出 Gas Price 高的交易;
7. 矿工打包交易并出块。
交易送出之后会被丢进交易池里,等待被矿工打包。矿工从交易池中取出交易进行打包与出块。根据 Eherscan 的数据,目前区块的 Gas 限制在 3000 万左右这是一个动态调整的值。若以一笔基础交易 21,000 Gas 来计算,则目前一个以太坊区块可以容纳约 1428 笔交易。因此当交易池里的交易量大时,会有许多交易没办法即时被打包而滞留在池子中等待。这里就衍生出了一个问题,交易池中有那么多笔交易,矿工先打包谁的交易呢?
矿工节点可以自行设置参数,不过大多数矿工都是按照手续费的多少排序。手续费高的会被优先打包出块,手续费低的则需要等前面手续费高的交易全部被打包完才能被打包。当然进入交易池中的交易是源源不断的,不管交易进入交易池时间的先后,手续费高的永远会被优先打包,手续费过低的可能永远都不会被打包。
那么手续费是怎么来的呢?
我们先看以太坊手续费计算公式:
Tx Fee(手续费)= Gas Used(燃料用量)* Gas Price(单位燃料价格)
其中 Gas Used 是由系统计算得出的,Gas Price 是可以自定义的,所以最终手续费的多少取决于 Gas Price 设置的多少。
举个例子:
例如 Gas Price 设置为 10 GWEI,Gas Used 为 21,000(WEI 是以太坊上最小的单位 1 WEI = 10^-18 个 Ether,GWEI 则是 1G 的 WEI,1 GWEI = 10^-9 个 Ether)。因此,根据手续费计算公式可以算出手续费为:
10 GWEI(单位燃料价格)* 21,000(燃料用量)= 0.00021 Ether(手续费)
在合约中我们常见到 Call 函数会设置 Gas Limit,下面我们来看看它是什么东西:
Gas Limit 可以从字面意思理解,就是 Gas 限制的意思,设置它是为了表示你愿意花多少数量的 Gas 在这笔交易上。当交易涉及复杂的合约交互时,不太确定实际的 Gas Used,可以设置 Gas Limit,被打包时只会收取实际 Gas Used 作为手续费,多给的 Gas 会退返回来,当然如果实际操作中 Gas Used > Gas Limit 就会发生 Out of gas,造成交易回滚。
当然,在实际交易中选择一个合适的 Gas Price 也是有讲究的,我们可以在 ETH GAS STATION 上看到实时的 Gas Price 对应的打包速度:
由上图可见,当前最快的打包速度对应的 Gas Price 为 2,我们只需要在发送交易时将 Gas Price 设置为 >= 2 的值就可以被尽快打包。
好了,到这里相信大家已经可以大致猜出抢跑的攻击方式了,就是在发送交易时将 Gas Price 调高从而被矿工优先打包。下面我们还是通过一个合约代码来带大家了解抢跑是如何完成攻击的。
合约示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract FindThisHash {
bytes32 public constant hash =
0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2;
constructor() payable {}
function solve(string memory solution) public {
require(hash == keccak256(abi.encodePacked(solution)), "Incorrect answer");
(bool sent, ) = msg.sender.call{value: 10 ether}("");
require(sent, "Failed to send Ether");
}
}
攻击分析