WTF Solidity 极简入门:Solidity 控制流与插入排序实现,以及 uint 下溢(underflow)陷阱全解析
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
本篇文章对应 WTF-Solidity 教程第 10 讲,围绕Solidity的控制流结构与如何在链上合约中用Solidity实现经典排序算法——插入排序(Insertion Sort)展开。读完你将掌握if-else、for、while、do-while、三元运算符等控制流的正确写法,理解为什么直接照搬 Python 版插入排序到Solidity会触发underflow报错,并拿到一份可直接在 Remix 中编译运行的正确版本。同时,本讲是理解 Solidity 安全缺陷(如数值溢出)的重要入门案例,对后续阅读合约审计相关内容很有帮助。
Solidity 控制流:与其他语言相似的六大结构
Solidity的控制流与C、JavaScript等主流语言高度相似,主要包含以下结构。所有示例代码均来自仓库 10_InsertionSort/InsertionSort.sol 中的InsertionSort合约,你可以在 Remix 中直接粘贴编译运行。
1. if-else 条件分支
function ifElseTest(uint256 _number) public pure returns(bool){ if(_number == 0){ return(true); }else{ return(false); } }if-else用于根据条件执行不同分支。上例中,入参_number为0时返回true,否则返回false。注意Solidity中布尔条件两侧的括号是必须的,与 JavaScript 风格一致。
2. for 循环
function forLoopTest() public pure returns(uint256){ uint sum = 0; for(uint i = 0; i < 10; i++){ sum += i; } return(sum); }for循环由初始化语句(uint i = 0)、循环条件(i < 10)和迭代语句(i++)三部分组成,上例计算0+1+...+9 = 45。由于该函数不读取也不修改链上状态,声明为pure可以节省 gas。
3. while 循环
function whileTest() public pure returns(uint256){ uint sum = 0; uint i = 0; while(i < 10){ sum += i; i++; } return(sum); }while在进入循环体之前先判断条件,条件为假时循环体一次都不会执行,因此又叫"前测试循环"。
4. do-while 循环
function doWhileTest() public pure returns(uint256){ uint sum = 0; uint i = 0; do{ sum += i; i++; }while(i < 10); return(sum); }do-while先执行一次循环体再判断条件,因此循环体至少执行一次。这是它与while的核心区别,在需要"先操作后判断"的场景中非常有用。
5. 三元(条件)运算符
三元运算符是Solidity中唯一接受三个操作数的运算符,规则为条件 ? 条件为真时的表达式 : 条件为假时的表达式,经常作为if语句的快捷写法:
// 三元运算符 ternary/conditional operator function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){ // return the max of x and y return x >= y ? x: y; }上例一行代码即返回x与y中的较大值,等价于完整的if-else分支。
6. continue 与 break
除上述结构外,Solidity还支持循环控制关键字:
continue:立即跳过本次循环的剩余语句,进入下一次迭代;break:立即跳出当前整个循环。
这两个关键字与其它语言语义一致,常用于在循环中过滤数据或提前终止搜索。
插入排序:最简单也最容易写错的排序算法
排序算法解决的问题,是把一组无序数字(例如[2, 5, 3, 1])按从小到大排列。插入排序(Insertion Sort)是最简单的排序算法之一,也是很多人学习的第一个算法。它的思路很朴素:从前往后遍历,把每一个数与排在它前面的数逐个比较,若比前面的数小,就将其向前移动,直到插入到正确位置。
举例来说,处理数组[2, 5, 3, 1]时,算法会先把5与前面的2比较(无需移动),再把3与前面的5、2比较并插入到2与5之间,最后把1一路移动到数组最前面,得到[1, 2, 3, 5]。
Python 版实现
先看插入排序的 Python 实现,一共 8 行:
# Python program for implementation of Insertion Sort def insertionSort(arr): for i in range(1, len(arr)): key = arr[i] j = i-1 while j >=0 and key < arr[j] : arr[j+1] = arr[j] j -= 1 arr[j+1] = key return arr核心逻辑是:用key保存当前待插入元素,j从i-1开始向前扫描,凡比key大的元素都后移一位,最后把key放到空出的位置。
照搬到 Solidity 后出现了 BUG:uint 下溢(underflow)
将上面的 Python 代码"逐行翻译"成Solidity,函数、变量、循环等一一对应,只需要 9 行代码:
// 插入排序 错误版 function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) { for (uint i = 1;i < a.length;i++){ uint temp = a[i]; uint j=i-1; while( (j >= 0) && (temp < a[j])){ a[j+1] = a[j]; j--; } a[j+1] = temp; } return(a); }把这段代码放到 Remix 上运行,输入[2, 5, 3, 1],结果却直接报错!这正是原文档特别强调的警示:90% 以上的人用Solidity写插入排序都会出错。
下图是 Remix 中运行错误版合约时"decoded output"的解码失败界面(来源 10_InsertionSort/img/10-1.jpg,该图与 Languages/pt-br/10_InsertionSort/img/10-1.jpg 内容相同):
图中可以看到:合约接收到的输入数组是uint256[]类型的[2, 3, 5, 1],但输出解码时报出Failed to decode output: Error: over-flow (faults) overFlow, operation: toNumber(),并附带一个远超uint256最大值(2^256-1)的畸形数字。也就是说,合约内部的运算已经产生了不可预期的异常结果。
根因:Solidity 的 uint 是无符号整数,减到负值即触发 underflow
排查几小时后,问题终于定位:Solidity中最常用的变量类型是uint(无符号整数),它无法表示负数。而在插入排序算法中,变量j在循环结束时有可能递减到-1:
- 外层循环处理到某个元素时,内层
while循环条件(j >= 0) && (temp < a[j])里,j >= 0对无符号整数来说永远为真; - 于是
j不断执行j--,一旦j从0变为-1,由于uint类型下溢(underflow),-1会被包装成uint能表示的最大值2^256 - 1(即约1.15 × 10^77); - 此时再访问
a[j],索引超出数组长度,且j的取值已经完全失控,最终导致输出解码失败、交易回滚。
这正是上图中出现那个天文数字的原因——它不是合法的排序结果,而是uint下溢后的错误索引与错误数据。这个案例直观说明:Solidity 默认不做整数溢出检查的运行时保护时,uint的减法下溢会带来灾难性后果(注:Solidity 0.8 起默认对算术运算做溢出检查,但本案例的问题在于把负数语义强加给了无符号类型,属于逻辑层面的错误)。
正确的 Solidity 插入排序
修复的思路是:让j永远无法取到负值。做法是把j的初始值加 1,比较时改用a[j-1],并把循环条件从j >= 0改为j >= 1:
// 插入排序 正确版 function insertionSort(uint[] memory a) public pure returns(uint[] memory) { // note that uint can not take negative value for (uint i = 1;i < a.length;i++){ uint temp = a[i]; uint j=i; while( (j >= 1) && (temp < a[j-1])){ a[j] = a[j-1]; j--; } a[j] = temp; } return(a); }两版代码的差异集中在一处:
| 版本 | j 初始值 | 循环条件 | 比较对象 | 赋值位置 |
|---|---|---|---|---|
| 错误版 | j = i-1 | j >= 0(恒真,隐患) | temp < a[j] | a[j+1] = a[j] |
| 正确版 | j = i | j >= 1(j 最小为 0) | temp < a[j-1] | a[j] = a[j-1] |
正确版中j的取值范围被限制在1 ~ i,j--最多递减到0,绝不会触及负数,从而彻底规避了uint下溢。在 Remix 中重新运行正确版,输入[2, 5, 3, 1],即可得到排序结果[1, 2, 3, 5],与预期完全一致。
仓库中的源码与编译环境
- 合约完整源码位于 10_InsertionSort/InsertionSort.sol,其中同时保留了错误版
insertionSortWrong与正确版insertionSort,并包含ifElseTest、forLoopTest、whileTest、doWhileTest、ternaryTest全部控制流示例,可直接对照阅读。葡萄牙语版本见 Languages/pt-br/10_InsertionSort/InsertionSort.sol。 - 仓库根目录 foundry.toml 将
solc固定为0.8.34,该合约声明pragma solidity ^0.8.34,可同时在 Remix(选用 0.8.34 及以上版本)或使用 Foundry 的forge build环境下编译验证。 - 仓库根目录 scripts/run-forge-tests.sh 提供了对全仓库合约运行测试的脚本,可用于自动化验证。
总结
这一讲我们完成了两件事:一是系统梳理了Solidity的if-else、for、while、do-while、三元运算符以及continue/break等控制流结构;二是用Solidity实现了插入排序,并亲历了一次经典的踩坑过程——因为uint是无符号整数,照搬其他语言的写法让变量j减到-1,触发underflow导致合约报错,最终通过改写循环边界条件修复。
插入排序看起来简单,实际写对并不容易。这正是Solidity的特性:坑很多,每个月都有项目因为这些不起眼的小 bug 损失几千万甚至上亿美元。掌握好基础、多动手练习,是写出高质量Solidity代码的前提。建议读者在 10_InsertionSort/InsertionSort.sol 源码基础上,自行尝试把错误版改为正确版、再尝试排序不同输入数组,深入体会无符号整数边界条件的处理。
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考