想象你在学校走廊里安放了一台“透明自动售货机”:玻璃橱窗让每个路过的人都能清楚看到里面的存货和标价;任何人投币按按钮就能买走饮料;机器自己记账、自己找零;只有拿钥匙的老板能打开后门补货;每一次出货都会“叮咚”一声提醒周围人。这台售货机就是智能合约,而 Solidity 就是写《售货机安装说明书》的语言。 整篇文章我们都会围绕这台售货机展开:先读懂说明书的结构(合约骨架),再认识存货牌上的数据(状态变量),接着设计按钮面板(函数),听清出货时的叮咚声(事件),装上老板专属的权限锁(修饰符),最后通电营业并按时缴电费(部署与 Gas)。每一步都有看得见摸得着的画面,学完你就能亲手在 Remix 里造出自己的第一台链上“售货机”。

岗位关联

学完这节课,你能看懂任何一份 Solidity 合约的骨架,亲手在 Remix 里编译并部署自己的第一个智能合约,并在后续学习 Web3 开发、DeFi 或 AI 链上代理时,拥有一块最扎实的地基。

目录
  1. 可视化演示
  2. 合约结构
  3. 状态变量
  4. 函数进阶
  5. 事件日志
  6. 函数修饰符
  7. 部署实战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示Solidity智能合约开发的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Solidity智能合约开发:合约结构 合约结构与状态变量定义 函数可见性修饰器与权限控制 核心机制可视化 Solidity智能合约开发整体架构与关键组件

合约结构

想象打开这份《售货机安装说明书》的第一页:左上角印着“本说明书适用于型号 V1.0”,对应 Solidity 源文件第一行的版本声明 pragma solidity ^0.8.0;——它的作用是告诉编译器“请用 0.8 系列的规则来解读我”,避免不同版本语法打架。这一行就像机器铭牌上的型号标签,是整个文件的身份证。

翻到第二页,整份说明书的结构一目了然:一台售货机 = 一个主机箱(contract 关键字)+ 若干存货架(状态变量)+ 一排按钮面板(函数)+ 一个喇叭(事件)+ 一组安全锁(修饰符)。在 Solidity 里,每个 .sol 文件的核心就是一个 contract 定义的“合约蓝图”,它把所有成员按职责装进同一个机箱。你可以给机箱起名 VendingMachine,这个名字将来就是链上合约的标识。

拿到这本说明书,你已经知道售货机由哪些大件组成。接下来,我们要认识其中最贵的零件——那块记录库存的“全网同步存货牌”,也就是状态变量。

contract VendingMachine { uint inventory;  address owner;  function buy() {⋯ } }\text{contract}\ \text{VendingMachine}\ \{\ \text{uint}\ \text{inventory};\ \ \text{address}\ \text{owner};\ \ \text{function}\ \text{buy}()\ \{\cdots\}\ \}
通俗说明:这行就像“售货机总装图”——最外层机箱是 contract,箱内挂着声明变量和函数的零件清单。
  1. contract 关键字声明一种新“机型”。这里定义了一个名为 VendingMachine 的合约蓝图,相当于画出整台售货机的设计图。
    contract VendingMachine\text{contract}\ \text{VendingMachine}
  2. 成员变量机箱里预装的两块“存货牌”。inventory 记录剩余饮料数量,owner 记录老板的账户地址。
    uint inventory;address owner;\text{uint}\ \text{inventory};\quad \text{address}\ \text{owner};
  3. 函数成员面板上的执行按钮,按下后触发内部出货逻辑,是机器对外提供的服务。
    function buy() {⋯ }\text{function}\ \text{buy}()\ \{\cdots\}

状态变量

售货机玻璃橱窗里挂着一块电子存货牌,实时显示“可乐还剩 8 瓶”。这块牌子的特别之处在于:它不是挂在机器里的普通纸片,而是一块“全网同步牌”——只要写上去,全球所有节点都会同步更新,而且永不消失、不可篡改。Solidity 中实现这块牌子靠的就是状态变量。一个状态变量声明在合约顶层,就像这台机器永久安装的一枚零件。

存货牌上能写的数据分两类。一类是“单颗话梅”式的值类型:uint 无符号整数、bool 布尔值、address 账户地址,它们是原子数据,无法再拆分,直接刻在牌上。另一类是“整箱零食”式的引用类型:放一串数据用 array 数组,查表用 mapping 键值映射,打包多个字段用 struct 结构体——比如“可乐(产地、生产日期、价格)”就是一个结构体。

还有一个关键问题:这块牌子挂在哪儿?状态变量默认存放在链上的“永久仓库” storage,写入要花大价钱;函数内部临时使用的局部变量则住在“临时展台” memory,用完即焚,几乎免费。这就是为什么聪明的售货机设计者会尽量减少改写存货牌的次数。

存货牌越贵重,越要谨慎落笔。而顾客和老板真正能操作这块牌子的方式,全在面板的按钮上——那就是函数。

string name="Cola";mapping(address⇒uint) balances;\text{string}\ \text{name} = \text{"Cola"};\quad \text{mapping}(\text{address} \Rightarrow \text{uint})\ \text{balances};
通俗说明:name 是写着“可乐”的标签,balances 是一本“每位顾客剩余次数”的账本,两者都是存货牌家族成员。
  1. string 类型字符串属于引用类型(动态字节数组),声明一个“装名字的盒子”,用来存放饮料名称。
    string name="Cola";\text{string}\ \text{name} = \text{"Cola"};
  2. mapping 映射一本特殊的账本——给定一个地址作为钥匙,立刻查到对应的余额。
    mapping(address⇒uint) balances\text{mapping}(\text{address} \Rightarrow \text{uint})\ \text{balances}
  3. 存储位置状态变量天然住进 storage 仓库,每次改动都伴随高昂的费用账单。
    Gasstore≈20000 per word\text{Gas}_{\text{store}} \approx 20000\ \text{per word}

函数进阶

售货机面板上的几排按钮,是顾客唯一能接触的交互界面。Solidity 中用函数定义按钮的行为。按钮分四种权限:public 按钮贴在最外面,谁都能按;external 按钮专供外部顾客使用,本机内部逻辑不响应它;internal 是机箱内部的齿轮,只有内部部件之间联动;private 连老板手册里都只字不提,只有本合约自己知道它的存在。

机器出厂时要做初始设定,比如“初始库存 8 瓶、老板地址是谁”,这就是构造函数 constructor——它只在部署那一刻执行一次,之后机器运转期间不会再被调用。像售货机在工厂通电前最后一次拧紧螺丝,之后就交给新主人了。

函数还能“回话”:returns 声明返回值类型,把计算结果交还给调用者。调用也分两种方式:外部调用要绕一圈(用 this.f() 或合约名 .f()),内部调用直接写 f() 即可——内部调用不需要发交易,所以能省下额外的 Gas。

按钮设计好了,顾客按下后机器做的事情需要被“见证”——这就轮到那个每次出货都会响的喇叭登场。

function buy(uint x) public returns (bool ok) { ⋯ }\text{function}\ \text{buy}(\text{uint}\ x)\ \text{public}\ \text{returns}\ (\text{bool}\ ok)\ \{\ \cdots\ \}
通俗说明:这行定义了“投币买 x 瓶”的按钮:公开可按、输入数量、按完告诉你成没成功。
  1. 函数名与参数买几瓶?参数 x 告知售货机本次需求数量。
    buy(uint x)\text{buy}(\text{uint}\ x)
  2. 可见性修饰器public 表示按钮裸露在面板最外层,任何账户都能直接调用。
    public\text{public}
  3. 返回值声明returns (bool ok) 表示操作完成后回一声“成功”或“失败”,相当于机器投屏显示结果。
    returns (bool ok)\text{returns}\ (\text{bool}\ ok)

事件日志

顾客投币买走一瓶可乐,“叮咚”一声响,屏幕闪出“已出货:1 瓶”。这声音不是说给机器自己听的——机器内部早就完成了出货逻辑;它是说给周围所有人听的:刚才发生了一笔交易,全世界都可以来查证。Solidity 里这声“叮咚”就是事件 Event。

用 event 关键字先声明“叮咚长什么样”:名字、要广播的参数。再用 emit 触发:当 buy() 执行成功时,emit Purchased(msg.sender, 1); 就会把“谁、买了多少”广播到链上日志。这个喇叭和普通存储不一样:它不占据昂贵的状态变量空间,而是写入专门的“链上广播记录”。

事件日志有个反直觉的性质:合约自己反而无法读取历史事件!喇叭喊完就没了,合约内部逻辑听不见;只有链下应用(前端 DApp、索引器、监控脚本)可以订阅查询。所以事件通常用来做三件事:前端刷新余额、监控异常交易、留下不可篡改的审计痕迹。

叮咚声让整条街都知道刚才卖出了一瓶,但并不是人人都能开后门补货——接下来,给后门装上老板专属的权限锁。

event Purchased(address buyer, uint count);\text{event}\ \text{Purchased}(\text{address}\ \text{buyer},\ \text{uint}\ \text{count});
通俗说明:先定义好喇叭的“播音稿模板”,之后每次出货都按这个模板广播一条消息。
  1. event 声明定义一条广播消息的字段结构,类似于给播音员一份稿件模板。
    event Purchased(address buyer, uint count)\text{event}\ \text{Purchased}(\text{address}\ \text{buyer},\ \text{uint}\ \text{count})
  2. emit 触发在函数体内发出广播,相当于按下喇叭按钮完成一次播音。
    emit Purchased(msg.sender, x);\text{emit}\ \text{Purchased}(\text{msg.sender},\ x);
  3. indexed 索引给某个参数加 indexed 标记,让外部可以按此字段快速检索历史广播,如同给新闻标题加关键词索引。
    event Purchased(address indexed buyer, uint count)\text{event}\ \text{Purchased}(\text{address}\ \text{indexed}\ \text{buyer},\ \text{uint}\ \text{count})

函数修饰符

老板最关心的事:补货这个按钮,绝对不能让学生按。最笨的办法是在每个函数开头手写一遍“检查一下你是不是老板”。而 Solidity 提供了更优雅的封装——修饰符 Modifier,把检查逻辑抽成一块“安全锁贴纸”,随时贴到任何函数上。

定义 modifier onlyOwner() { require(msg.sender == owner); _; } 拆开看:require 是“守卫”,条件不满足就拒绝执行并让整笔交易回滚;_ 是“钥匙孔”,守卫通过后,机器从这里继续执行被修饰函数的原始代码。用的时候只要在函数声明后面贴上 onlyOwner,这个函数就自动挂上了锁。

修饰符还能叠加,像两把锁串联:onlyOwner 检查完身份,再检查 notPaused 机器是否暂停营业,层层设防。一旦 require 失败,不只是返回一个错误,而是让整笔交易回滚——售货机直接断电重启,恢复到按下按钮之前的状态,谁也别想占便宜。

安全锁封装完毕,售货机的图纸已经完整。下一步是实际动手:把图纸送去工厂铸造,通电营业。

modifier onlyOwner { require(msg.sender=owner); _; }\text{modifier}\ \text{onlyOwner}\ \{\ \text{require}(\text{msg.sender} = \text{owner});\ \_;\ \}
通俗说明:这是“老板专用”锁:不是老板就弹回,是老板就放行,继续执行后面的核心动作。
  1. require 守卫条件表达式成立才继续执行,否则整笔交易回滚,所有状态像没发生过一样。
    require(msg.sender=owner)\text{require}(\text{msg.sender} = \text{owner})
  2. 占位符下划线锁芯位置。守卫通过后,钥匙插入锁芯,继续执行被修饰函数的原始函数体。
    _;\_;
  3. 组合使用多个修饰符按先后顺序依次插锁,形成多重防线,如“先验身份再验营业状态”。
    function restock() public onlyOwner notPaused {⋯ }\text{function}\ \text{restock}()\ \text{public}\ \text{onlyOwner}\ \text{notPaused}\ \{\cdots\}

部署实战

图纸画得再好,不铸造成实物就是废纸。Remix IDE 就是你的“售货机制造车间”:打开浏览器访问 remix.ethereum.org,左侧编写代码,点“编译”按钮检查语法并生成字节码,再点“部署”按钮,选择测试账户并确认 Gas 上限,一条交易广播出去,等矿工打包出块确认后——售货机正式通电营业,地址就是你的合约 owner。

整个链上生活的每一笔操作都要缴“电费”,这个费用叫 Gas。计算方式是“用电量 × 电价”:每步 EVM 操作都有固定单价,其中存储操作特别贵(SSTORE 约每字 20000 Gas),而只读查询函数(view)则不花 Gas。Gas 上限是你愿意支付的最高电费,一旦超支交易直接回滚,但已消耗的 Gas 不会退还。

开业后还要防小偷。第一,先更新状态再转账(防“重入攻击”:坏人趁余额未扣反复提款);第二,敏感操作全部挂上 onlyOwner 锁;第三,接收外部地址前先检查地址有效性,防止资金转入黑洞地址。

从一张说明书到一台通电商用的售货机,你已经走完了 Solidity 开发者的完整旅程——写码、编译、部署、计费、防盗,样样俱全。现在,轮到你去经营自己的第一台链上售货机了。

Gastx=Gasused×GasPrice,Gasused≥21000+20000×w\text{Gas}_{\text{tx}} = \text{Gas}_{\text{used}} \times \text{GasPrice},\quad \text{Gas}_{\text{used}} \geq 21000 + 20000 \times w
通俗说明:账单 = 用电量 × 电价。每笔交易都有 21000 的起步价,每往链上写一个字再加约 20000。
  1. Gas 计算实际消耗的 Gas 量乘以 GasPrice 单价,就是这笔交易需要支付的手续费。
    Gastx=Gasused×GasPrice\text{Gas}_{\text{tx}} = \text{Gas}_{\text{used}} \times \text{GasPrice}
  2. 基础成本任何交易至少消耗 21000 Gas,相当于打车的起步价,与业务内容无关。
    Gasbase=21000\text{Gas}_{\text{base}} = 21000
  3. 存储成本往链上写一个字(w)额外约 20000 Gas,改写状态变量是全合约最贵的操作。
    Gasstore=20000×w\text{Gas}_{\text{store}} = 20000 \times w

AI 应用场景

场景一:AI 交易代理自动调仓

阶段:模型决策与链上执行

解决的问题:传统量化策略需要人工盯盘并手动提交交易,速度慢且易受情绪影响;AI 代理实时监听链上事件日志,当行情满足策略条件时自动调用合约函数完成买卖,并把每笔成交结果通过事件广播出去形成闭环反馈。

具体案例:一个 DeFi 套利机器人持续订阅 Uniswap 资金池的 Swap 事件,一旦发现价格差超过阈值,立即调用路由合约的 swap 函数完成套利,随后通过 emit 记录成交明细,供监控面板实时刷新。

场景二:AI 辅助智能合约安全审计

阶段:代码质量与安全检测

解决的问题:人工审计合约代码容易遗漏“重入攻击”“权限失控”等漏洞;AI 结合规则库扫描 Solidity 源码,自动定位“先转账后更新状态”的危险模式,并提示开发者补上 require 守卫和 onlyOwner 修饰符。

具体案例:审计团队用 AI 扫描新上线的 VendingMachine 合约,在 buy() 中发现先转账后扣余额的代码行,系统立即以红色警示并给出 checks-effects-interactions 的标准改写建议。

场景三:AI 推理结果上链存证

阶段:模型输出可信存证

解决的问题:AI 输出的判断结果需要不可篡改的证明,传统数据库存在后台被篡改的风险;借助事件机制把模型 ID、样本哈希、推理结果广播到链上日志,任何人可验证结果真伪。

具体案例:医疗影像 AI 将每份报告的哈希值与前缀结果通过 emit DiagnosisRecord 写入链上日志,患者与医院各持一份链上凭证,纠纷时秒级核验。

Python 演示

python50 行
class VendingContract:    """用 Python 模拟一台售货机型智能合约,演示 Solidity 核心概念"""     def __init__(self, owner):        self.owner = owner       # 部署者地址        self.inventory = 10      # 状态变量:库存        self.balance = 0         # 状态变量:合约余额        self.event_log = []      # 事件日志     def is_owner(self, caller):        return caller == self.owner     def buy(self, caller, count):        # 相当于 public 函数:任何人都能调用        if self.inventory < count:            raise ValueError("库存不足,交易回滚")        self.inventory -= count        self.balance += count        self.event_log.append(("Purchased", caller, count))  # emit 事件     def restock(self, caller, count):        # 相当于仅所有者可调用(onlyOwner 修饰符)        if not self.is_owner(caller):            raise PermissionError("非所有者,拒绝补货")        self.inventory += count        self.event_log.append(("Restocked", caller, count))     def withdraw(self, caller, amount):        if not self.is_owner(caller):            raise PermissionError("非所有者,拒绝提现")        if self.balance < amount:            raise ValueError("余额不足,交易回滚")        self.balance -= amount        self.event_log.append(("Withdrawn", caller, amount))  if __name__ == "__main__":    alice = "0xAlice"    bob = "0xBob"    c = VendingContract(alice)    c.buy(bob, 2)       # 顾客购买 2 瓶    c.buy(bob, 1)       # 顾客再买 1 瓶    c.restock(alice, 5) # 所有者补货 5 瓶    try:        c.restock(bob, 1)  # 非所有者补货,会被拒绝    except PermissionError as e:        print("[回滚]", e)    print("库存:", c.inventory, "余额:", c.balance)    for ev in c.event_log:        print("事件:", ev)
目的:用 Python 模拟一台“售货机型”智能合约,直观展示状态变量、权限校验(修饰符)、事件日志与交易回滚的协作方式。
  1. self.inventory = 10定义状态变量:售货机初始库存,对应 Solidity 里声明在合约顶层的状态。
  2. def buy(self, caller, count)定义公开函数:任何账户都能调用,对应 Solidity 的 public。
  3. raise ValueError("库存不足,交易回滚")模拟 require 守卫失败:整笔交易回滚,状态保持不变。
  4. self.event_log.append(("Purchased", caller, count))模拟 emit 事件:把“谁买了多少”追加进链上日志。
  5. if not self.is_owner(caller):权限检查,对应 onlyOwner 修饰符的 require 判断。

常见误区

误区Solidity 语法和 JavaScript 太像了,我直接把以前写的 JS 业务逻辑搬上去,改改变量名就能部署合约,上线当老板。

正解语法相似不等于运行环境相似。Solidity 编译后跑在区块链 EVM 上:所有数据公开永久、每次执行都收 Gas、失败自动回滚、部署后无法修改。就像你看过汉堡包装纸的印刷说明,不代表你会操作烤肉机——JS 里可以随便 new 对象、无限循环、依赖外部库,链上全都行不通。必须按“区块链思维”重新设计,而不是翻译代码。

为何常见:官方文档强调“语法类似 JavaScript”,让初学者把精力放在语法迁移上,忽略了执行模型的天壤之别。

误区event 带了一堆参数,肯定可以把业务数据存进去,之后合约内部再读出来继续用。

正解事件写入的是日志(Log),任何合约代码都无法读取历史事件。它更像售货机的“叮咚”广播——喇叭喊完就没了,合约自身根本听不见;只有链下的前端和索引器能订阅记录。要把数据留给自己用,必须写状态变量,只是要付昂贵的 Gas。

为何常见:事件参数看起来像函数返回值,初学者容易误以为它是可回读的存储结构。

误区链上空间无限,把每一步中间结果都存成状态变量,以后复盘一清二楚,反正又不要钱。

正解链上存储极其昂贵,SSTORE 写一个字约 20000 Gas,而且永久占用全网节点硬盘。优化思路是“能不写就不写”:临时计算用 memory 局部变量,对外通告用事件,只在确有必要时才更新状态变量。就像售货机的存货牌每次改写都要请电工上门,不是随手涂鸦的地方。

为何常见:本地开发中内存和数据库几乎免费且随意访问,这种习惯被无意识地带到了链上世界。

误区部署交易失败了,合约没上线,那么这次操作肯定不收费,白嫖验证一下。

正解只要交易被矿工打包执行,无论成功失败都要收 Gas;失败只是状态回滚,Gas 消耗不会退还。就像请师傅上门修机器,拆开发现修不了,上门费照样要付。所以上线前务必在 Remix 和测试网上反复验证。

为何常见:日常支付中“没买到东西就退款”的直觉被错误迁移到了链上执行模型。

误区把变量声明成 private,数据就只有合约自己能看见了,链上其他人无论如何都看不到,可以放心存隐私。

正解private 只阻止其他合约代码访问该成员,区块链本身完全是透明的——所有状态变量、交易参数、事件都在公开账本上可查。真正保护隐私要靠加密、零知识证明等技术。这相当于“藏在抽屉里的钥匙”只是程序员的访问控制,而不是保密保险柜。

为何常见:面向对象语言里 private 确实保护数据不被外部类访问,初学者把“不可访问”错误等同于“不可见”。

面试题

Q1. 在 Solidity 中声明一个智能合约,应使用哪个关键字?

单选题 · easy

  • A.class 声明类
  • B.contract 声明合约← 正确答案
  • C.struct 声明结构体
  • D.module 声明模块

答案:B。contract 是 Solidity 定义合约蓝图的关键字,对应比喻中的“售货机总装图”。class 是面向对象语言的类关键字,struct 只定义数据结构而不含执行逻辑,module 不是 Solidity 关键字。

Q2. 关于状态变量与局部变量,下列说法正确的是?

单选题 · easy

  • A.状态变量永久存在链上← 正确答案
  • B.局部变量永久存在链上
  • C.二者存储位置完全一致
  • D.状态变量只存在内存中

答案:A。状态变量默认存储在链上 storage,永久保存并全网同步,对应那块“全网同步存货牌”;局部变量只存在于函数执行期间(memory),用完即焚。因此 A 正确,B C D 均错误。

Q3. 想让一个函数既能在合约内部调用、又能被外部账户调用,应设置哪种可见性?

单选题 · mid

  • A.public 内外均可调用← 正确答案
  • B.private 仅本合约内调用
  • C.external 仅外部可以调用
  • D.internal 仅内部与子合约调用

答案:A。public 像贴在售货机外面的按钮,顾客和维修工都能按;private 藏在机箱最深处,external 只面向外部顾客,internal 只给内部齿轮联动。只有 public 同时开放内部与外部调用。

Q4. 以下哪个关键字用来声明一条链上事件?

单选题 · mid

  • A.modifier 声明函数修饰器
  • B.event 声明链上事件← 正确答案
  • C.mapping 声明键值映射
  • D.require 写入条件校验

答案:B。event 声明事件,相当于先制作好售货机“叮咚声”的播音稿模板;modifier 是权限安全锁,mapping 是账本,require 是守卫。只有 event 用于声明事件。

Q5. 触发一条已经定义好的事件,应使用哪个关键字?

单选题 · mid

  • A.emit 触发并广播事件← 正确答案
  • B.call 调用其他合约函数
  • C.log 打印机输出日志
  • D.send 向合约发送以太币

答案:A。emit 是触发事件的专用关键字,如同按下售货机的喇叭按钮广播“出货啦”;call 与 send 用于转账和跨合约调用,log 不属于 Solidity 关键字。

Q6. 在函数中校验“只有合约所有者可以调用”,最合适的写法是?

单选题 · hard

  • A.require(msg.sender == owner)← 正确答案
  • B.require(msg.sender != owner)
  • C.require(msg.value >= 0)
  • D.require(msg.value <= 0)

答案:A。msg.sender 是当前调用者地址,owner 是部署时记录的所有者。要求“调用者等于所有者”才放行,就是给函数挂上 onlyOwner 锁;B 恰好写反,C D 只校验转账金额,与权限无关。

Q7. Remix IDE 在线集成环境主要用于编译什么语言?

单选题 · easy

  • A.Python 数据分析语言
  • B.JavaScript 网页脚本语言
  • C.Solidity 智能合约语言← 正确答案
  • D.C++ 面向对象系统语言

答案:C。Remix 是官方推荐的 Solidity 开发工具,集编写、编译、部署、调试于一体,好比售货机制造车间。Python、JavaScript、C++ 都不是 Remix 的主战场。

Q8. Solidity 中常见的值类型包括哪些?

多选题 · mid

  • A.uint256 无符号整数← 正确答案
  • B.address 账户地址← 正确答案
  • C.bool 布尔真值← 正确答案
  • D.string 字符串

答案:ABC。值类型是“单颗话梅”式的原子数据,uint256、address、bool 都是;string 属于引用类型(动态字节数组),像“一整箱零食”,不能拆分出固定大小的值,因此不入选。

Q9. 关于 Gas 的说法,正确的有哪些?

多选题 · hard

  • A.每笔交易都要支付 Gas← 正确答案
  • B.执行错误会导致状态回滚← 正确答案
  • C.部署合约不会消耗 Gas
  • D.开发者可设置 Gas 上限← 正确答案

答案:ABD。任何被矿工打包的交易都消耗 Gas,部署合约更是一笔大额初始开销,故 C 错;执行失败只是状态回滚、Gas 不退,A 对;开发者可通过交易的 gasLimit 设置上限,如同给出电费预算上限,D 对。

Q10. 防御“重入攻击”(攻击者反复调用提款函数),最稳妥的代码顺序是?

单选题 · extreme

  • A.先更新余额再执行转账← 正确答案
  • B.先执行转账再更新余额
  • C.转账前只检查余额大于零
  • D.每次转账后立即 emit 事件

答案:A。重入攻击利用“转账后余额未更新”的窗口反复提款。正确做法是先扣减余额状态再执行转账(先收钱后交货),即 checks-effects-interactions 模式;B 是典型漏洞写法,C 检查不充分,D 的事件只是记账,不改变执行顺序。