第一节:藏经阁的扫地僧
"气沉丹田,意守气海,这《锁离城》的第三式你练岔了!"
青石板上传来一声暴喝,惊得林小乙手腕一抖,竹扫帚差点戳进丹炉里。
藏经阁前的老槐树下,灰袍老者正捧着本泛黄册子,对着个白衣弟子指指点点。林小乙缩了缩脖子,把扫帚往墙角挪了挪——这已经是本月第七个被骂哭的内门弟子了。
"唐长老又在教人看合约了。"
路过的杂役往炉膛里添了把柴火,火星噼啪炸响:"听说上月铸剑堂的'千机匣'被魔教破了禁制,十万灵石不翼而飞,就是合约里藏了'回马枪'的破绽..."
林小乙耳朵竖得老高,手里竹枝却扫得更勤快。三年前他因灵根残缺被贬为杂役,每日除了清扫丹房就是擦拭剑架,连最基础的《以太门吐纳诀》都没摸过。
第二节:漏洞现形记
暮色四合时,林小乙照例去后山倒药渣。刚转过紫竹林,忽见丹房方向红光冲天,隐约传来金石相击之声。
"不好!"他拔腿狂奔,却见平日里存放丹药的"百草柜"正疯狂吞吐青烟,柜门上的符箓忽明忽暗。这是守护丹房的"固若金汤阵",此刻竟像喝醉般东倒西歪。
"快取玄冰符镇住阵眼!"
唐长老的灰袍猎猎作响,指尖在空中划出道道金纹。林小乙却盯着阵法核心的玉牌发愣——那上面流转的符文,竟与白日里见到的合约图谱有七分相似!
鬼使神差地,他抄起扫帚往东南角的阵旗下一捅。
"咔嚓!"
阵法应声而破,青烟化作三行血字悬浮半空:
require(msg.value >= 1 ether);
if (balanceOf[msg.sender] = 10) {
transfer(owner, 1000);
}
"好个狸猫换太子!"唐长老瞳孔骤缩:"这'固若金汤阵'的合约里,竟把赋值符'='当成了比较符'=='!"
第三节:初识合约十二经
三日后,林小乙跪在戒律堂青砖上,膝盖已没了知觉。
"擅动护山大阵,按律当废去修为..."
"且慢!"
唐长老甩袖踏入殿中,手里攥着片烧焦的竹简:"这小子捅破的漏洞,比魔教安插的暗桩还毒!"
林小乙眼前忽地多出本蓝皮书册,封皮上龙飞凤舞写着《合约心法·卷一》:
「合约十二经」
1.变量如穴位,须明公私(public/private)2.函数似经脉,慎用修饰(modifier)3.循环若走火,当设上限(gas limit)4.映射像暗器,防重入劫(reentrancy)
"从今日起,你每日戌时来藏经阁。"唐长老捻断三根胡须:"先背熟这'四要八忌',再把上月'铸剑堂失窃案'的合约反编译了!"
第四节:第一个智能合约
油灯下,林小乙盯着案上玉简。这是用"锁离城"写的简单赌约:
pragma solidity ^0.8.0;
contract SimpleBet {
address public owner;
mapping(address => uint) public balances;
constructor() {
owner = msg.sender;
}
function bet() public payable {
require(msg.value == 1 ether, "需押注1枚灵石");
balances[msg.sender] += msg.value;
}
function withdraw() public {
require(balances[msg.sender] > 0, "余额不足");
payable(msg.sender).transfer(balances[msg.sender]);
balances[msg.sender] = 0;
}
}
"问题在何处?"唐长老的声音从梁上传来。
林小乙指尖划过"withdraw"函数,突然想起白日见到的血字:"先转账后清零,若遇连环夺命call..."
"啪!"
一粒松子砸中他后脑:"正是重入攻击的命门!当用'检查-生效-交互'三才步法!"
技术彩蛋
文末附真实漏洞案例:
// 危险写法(已弃用)
function withdraw() public {
msg.sender.call{value: balances[msg.sender]}("");
balances[msg.sender] = 0;
}
// 安全写法(使用Checks-Effects-Interactions模式)
function withdraw() public {
uint amount = balances[msg.sender];
balances[msg.sender] = 0; // 先清零
payable(msg.sender).transfer(amount); // 后转账
}
下章预告
《第二章:重入劫》
"铸剑堂"惊现连环盗案,林小乙夜探合约发现致命递归。唐长老传授"防重入三式",却牵扯出二十年前"道君DAO"崩塌秘辛...
(本章涵盖变量声明、函数修饰、重入攻击原理)