remix编译运行solidity2
昨天我简要学习了全局变量和函数、表达式与控制结构,storage / memory / calldata三者差异,constructor构造函数 / modifier修饰器,payable函数,event+emit 事件与触发事件。然后在remix练习了几个简单的小合约,1.counter计数器,bank简单存取款器,struct+mapping 结构体和映射关系,双向mapping小合约用于地址是否关注的查询。然后我了解到了solidity比python要难,struct类似C++的结构体,solidity需要考虑gas费,要考虑资源消耗还要考虑安全性,不能只考虑可用性。
错误处理
ErrorDemo.sol
1 | |
balanceOf是什么意思?
balanceOf[msg.sender] = 1000;
这行代码本质上就是:
给当前调用者(msg.sender)在 balanceOf 这个“账本”里存一条记录:余额 = 1000。
require的错误处理

在上面的图中,输入一个eth地址,输入amount为10000,点击transact,于是就出现报错。
报错如下图所示,Insufficient balance
如果把amount改成1,点击transact,就没有报错。
看到sol的17-19行代码
1 | |
这就是require的错误处理
revert手工抛错
现在在withdraw函数的amount那里输入100000,点击transact。
报错提示要取100000 wei,但只剩下890 wei,所以报错了。
现在在withdraw函数的amount那里输入1,点击transact。
因为余额超过了1,所以没有报错。
这就是revert手工抛错
assert用于断言永远为真(仅内部检查)
balanceOf = 用户的余额(会变)
totalSupply = 总发行量(示例里不会变)
withdraw 只改 balanceOf,不改 totalSupply
所以断言永远为真
如果 assert 失败,说明代码有 bug,而不是用户输入问题。
总结
在错误处理的sol文件中,我学习了require,revert,assert的错误处理。
Fallback合约
功能:
receive() 接受 ETH
fallback() 记录调用信息
getBalance() 查询余额
WalletWithFallback.sol
1 | |
测试fallback.sol

如上图所示,在value下方输入1,右边选择ether作为单位,然后点击下方的transact
最后点击getBalance,即可获得余额为 10^ 18 wei
receive()
只负责在纯转账且没有附带 calldata 时接收 ETH。
例如:
在 Remix 部署后,打开 “Deploy & Run”
找到 “Value” 输入框填 1 Ether
点击合约旁的 transact (灰色按钮,不是调用函数)
→ receive() 触发
→ 合约成功收到 ETH
receive() 必须是:
external
payable
没有参数
没有返回值
fallback()
fallback 会在两种情况被触发:
| 情况 | 是否触发 fallback |
| 找不到被调用的函数 | 是 |
| 调用合约并携带 calldata(不是纯转账) | 是 |
| 纯转账但 msg.data 为空 | 否,触发 receive |
fallback 通常用于:
记录未知调用
代理合约
防止调用失败
兼容某些库 transfer/ send 的行为
在你的代码里,fallback 会发出 event,方便你在 Remix 查看。
getBalance()
返回当前合约持有的 ETH 数量。
address(this).balance 就是查看合约地址余额。
fallback 会发出 event,方便你在 Remix 查看。那么event在那里查看呢?
部署合约后,在右侧的 Deployed Contracts 区域里,你调用了函数(例如 fallback 被触发)。
在下面会出现一条新的 交易记录(Transactions)。
点击这一条交易记录,会展开详细信息。
展开后你能看到类似内容:

1 | |
event是什么?
event 是 Solidity 中的“日志输出机制”(Log),写入到区块链的交易日志(Transaction Logs)中,用于让前端、脚本、分析程序监听你的合约行为。
它相当于:
- 数据库的 “append-only” 日志
- 软件系统里的 “事件通知机制”
- 前端可以监听的 “发布消息”
最关键特点:
event 不存在于合约状态(不像 storage / mapping 会消耗 storage),它记录在交易日志(Log)里,因此便宜得多、不可修改、可被订阅。
event 是区块链上的日志记录机制(写入交易日志)。
event 不自动产生,必须由你手动 emit。
正常合约几乎都会大量使用 event,因为前端和分析工具都依赖它。
正常运行合约时,是否会自动生成 event?
不会。
event 是 你想记录、想暴露出来的信息时才 emit 的。
你不写 event,合约就不会自动产生任何 event。
换句话说:
event 永远不会自动产生
必须由开发者写 emit 才会出现
为什么智能合约需要 event?
前端监听事件(最常见)
例如 Uniswap、Aave 等前端 UI 需要知道:
用户是否存入资产
Swap 是否发生
谁转给了谁
某个池子余额是否发生变动
它们不会每秒轮询区块链,而是监听事件
便宜的数据记录
如果你想记录一条信息,方式有两种:
| 方式 | 成本 | 是否可在链上访问 | 是否可被前端读取 |
| 写入 storage | 贵 | 可访问 | 可读取 |
| 写入 event | 便宜 | 不能被合约读 | 能被前端读 |
event 的行为特点(你必须知道)
| 特性 | 说明 |
| 不可更改 | 写入日志后永远不能修改 |
| 不属于 state | 合约本身不能读取 logs |
| 可被索引 (indexed) | 类似数据库的索引,查询更快 |
| 前端、脚本可以监听 | 这是 event 最大的用途 |
| Gas 成本低于 storage | 大日志成本远低于写入 storage |
Caller.sol + Callee.sol
功能:
Callee: 返回一个 uint
Caller: 调用 Callee
使用 interface 方式调用
使用 call 方式调用
重点理解:
为什么 call 是唯一安全的低级调用
为什么 interface 更清晰
msg.sender 的变化
Callee.sol
1 | |

点getNumber, 返回2025
点number,还是返回2025
该合约部署好后,接下来用caller来调用该合约。
上图点击copy address,即可复制该合约的地址,该合约地址在后面有用的。
caller.sol
1 | |

复制callee合约的地址,在上图calleeAddr右侧粘贴,点击call。 即可成功调用callee
在decoded output可以看到2025这个字符串。
在下面callbylowerlever,使用同样的方法,复制callee合约的地址,粘贴,点击call。
同样得到2025的字符串。
下面说明interface调用和call调用的区别
下面给你一套面向初学审计 / 开发者的清晰、正式解释,涵盖你要理解的四个核心点:
- interface 调用
- call 调用
- call 为什么是唯一“安全”的低级调用
- msg.sender 的变化
内容会严格对标你未来审计岗位需要掌握的逻辑。
一、两种跨合约调用方式(interface vs call)
方式 1:通过 interface 调用(推荐)
1 | |
特点:
- 静态类型检查(编译器保证函数存在、类型正确)
- 语义清晰(跨合约调用意图明确)
- 更易审计(函数名、参数、返回类型可读性强)
核心点:
interface 调用是 Solidity 给你的一个“高层调用”,底层帮你生成 ABI 编码,不需要手工编码或字节处理。
方式 2:使用 call 调用(低级调用)
1 | |
特点:
- 需要手动编码函数签名与参数(没有编译器保障)
- 返回值需要自己 decode
- 无法静态检查被调用函数是否存在
典型使用场景:
- 调用未知 ABI 的合约
- 与非标准合约或代理合约交互
- 编写通用路由器或钱包合约
审计记忆点:
call 属于“低级调用”,容易出现:
- 编码错误
- 调用错误函数
- decode 错误
- 忽略了 call 的返回值(造成安全隐患)
二、为什么 call 被认为是唯一“安全”的低级调用?
注意:这里的“安全”指的是执行不 revert、永不 panic 的角度,而不是业务逻辑安全。
在 Solidity 中有三个低级调用:call、delegatecall、staticcall。
其中,只有 call 是通用、稳定、不会在不存在函数时 panic 的调用方式。
原因如下:
1. call 永远不会因为找不到函数而 revert
如果调用的函数不存在,call 的行为是:
- 返回
(false, "") - 交易继续执行,不回滚
- caller 可以通过 bool 判断结果
而其他调用方式可能直接 revert 或触发意外行为。
2. call 是 EVM 层级的底层原生操作
它是以太坊虚拟机的基础操作之一:
- 可以附带 ETH
- 可以更改 state
- 可以在运行时控制 gas
- 可以调用任意地址,即使该地址没有合约代码
所以:
- 钱包
- 执行器
- Router
- Proxy
- 多签
- DeFi 协议
都依赖 call 作为“最后 fallback 手段”。
3. 代理合约(Proxy)依赖 call
所有 upgradeable 合约必须依赖 call 进行转发逻辑。
总结一句话:
interface 是高层语法糖;call 是最低级的“永远可用”的原生接口。
三、为什么 interface 更清晰?
审计与开发中,interface 更清晰的原因主要包括:
1. 可读性强
审计者打开代码就能看到:
- 目标函数
- 参数类型
- 返回值
- 可见性(external)
- 是否 view/pure
call 完全看不到这些信息。
2. 编译器检查
interface 会在编译时进行:
- 参数数量检查
- 参数类型检查
- 是否存在函数
call 完全无法检查这些。
3. 减少风险
call 常见的错误:
- 拼错函数名字符
- 拼错参数顺序
- 拼错函数签名
- decode 错误
- 忘记检查 bool
interface 完全避免这些问题。
四、跨合约调用时 msg.sender 的变化
这是审计者必须完全理解的点:
情况 1:Caller → Callee(普通 external 调用)
Caller 调 Callee 的函数:
在 Callee 内部:
msg.sender = Callertx.origin = 最初发起交易的外部账户
图示:
1 | |
情况 2:使用 interface 调用和使用 call 调用,msg.sender 完全一样
无论你用:
1 | |
还是:
1 | |
结果都一样:
在 Callee 内部:
1 | |
原因:
调用方式不同,但执行路径都是 EVM 的 CALL 操作。
注意:delegatecall 才会改变 msg.sender 的语义
delegatecall 会“借用” caller 的上下文,所以:
1 | |
但你现在学“跨合约调用”,focus 在:
- interface 调用
- call 调用
这两者的 msg.sender 完全一致。
五、总结(你面试时就这么回答)
如果你要面试 Junior Auditor,可以直接这样回答:
interface 调用更安全、清晰、有编译期检查。适合正常业务逻辑的跨合约交互。
call 是唯一“安全”的低级调用,因为:
- 永远不会因为找不到函数而 revert
- 是 EVM 原生操作,最通用
- 能附带 ETH
- Router/Proxy 必须用 call
msg.sender 的变化:
- interface 和 call 调用的本质都是 CALL 操作
- 在被调用合约中
msg.sender始终是 Caller 合约 - 只有 delegatecall 才会把 msg.sender 变成 EOA
如果你愿意,我可以继续教你:
- 写一个好用的 interface + call 对照学习合约
- 一套 Remix 完整测试
- 一套面试题 + 参考答案(junior 审计岗位常问)
什么是跨合约调用
下面给你一个正式、清晰、面向初学审计/开发者的定义,确保你真正理解“跨合约调用”(cross-contract call)是什么,以及它在智能合约安全、架构、审计中的意义。
一、什么是“跨合约调用”——正式定义
跨合约调用(Cross-Contract Call)就是一个合约在执行过程中,调用“另一个合约”的函数或 fallback/receive,从而进入另一个合约的执行上下文。
换句话说:
- Contract A 执行中
- A 调用 B 的某个函数(或 fallback)
- 程序跳转到 B 的代码执行
- 执行结束后返回到 A
核心动作:
合约 A 发起了一次 EVM 的 CALL / STATICCALL / DELEGATECALL / CALLCODE 操作。
二、为什么称为“跨”?
因为执行流程发生了“地址空间切换”:
1 | |
这相当于程序从一个代码库跳转到另一个独立代码库执行。
三、跨合约调用的本质
本质是 EVM 底层操作:
CALLSTATICCALLDELEGATECALLCALLCODE(已废弃)
你现在主要学习的,是最常见的CALL。
对于大部分业务逻辑:
1 | |
四、跨合约调用与普通函数调用的对比
| 类型 | 执行位置 | msg.sender | 需要 ABI 编码 | 能否外部发送 ETH |
| 合约内部函数调用 | 本合约内部 | EOA / 上级调用者 | 不需要 | 不能 |
| 跨合约调用(CALL) | 切换到另一个合约 | 调用的合约地址 | 需要 | 可以 |
关键区别:
跨合约调用需要 ABI 编码,且 msg.sender 会变成调用合约地址。
五、跨合约调用的典型例子
例 1:interface 调用(高层抽象)
1 | |
例 2:低级调用(低层 call)
1 | |
两者都属于跨合约调用。
六、跨合约调用的意义(为什么重要?)
跨合约调用是智能合约编程的核心:
模块化架构:
多个合约各自负责不同业务逻辑,共同构成系统。与外部协议交互:
调用 DeFi:Uniswap、Maker、Aave、Compound
调用 NFT:ERC721、ERC1155
调用 Oracle:Chainlink、Tellor升级代理(Proxy)必须使用跨合约调用:
代理合约 delegatecall 执行逻辑合约。安全审计重点:
大量漏洞都出现在跨合约调用环节,如:- 重入攻击(Reentrancy)
- delegatecall 上下文污染
- 调用失败未检查
- fallback 调用意外触发
所以你必须熟练掌握跨合约调用的执行模型。
七、跨合约调用的执行顺序(可视化)
1 | |
Callee 内部看到的:
1 | |
八、跨合约调用的分类
1)常规外部函数调用(interface → CALL)
安全可靠、编译器检查类型。
2)低级调用(call)
更灵活,风险更高,可附带 ETH。
3)staticcall
只读,不允许修改状态。
4)delegatecall
最危险:使用调用者的存储上下文。
九、面试标准答案(Junior Auditor)
你可以直接背:
跨合约调用是一个合约执行过程中通过 CALL 或 STATICCALL 跳转到另一个合约地址执行代码的行为。它需要 ABI 编码,msg.sender 会变成发起调用的合约地址。跨合约调用是智能合约模块化、与外部协议集成、升级代理、钱包转发的基础,也是审计中大量安全漏洞的来源,如重入和 delegatecall 漏洞。
如果你愿意,我可以继续带你:
- 画一个跨合约调用的可视化流程图
- 写一个测试用完整示例(Caller + Callee)
- 带你在 Remix 实际调用,观察 msg.sender、gas、返回值变化
- 给你跨合约调用的面试题(并附答案)
告诉我你想继续深入哪一部分。
总结
今天学了错误处理require,revert,assert。了解到require是最常用的错误处理
这三个错误处理有什么区别?
fallback有些类似于python里的try except continue语句,用于防止调用失败,兼容异常行为
还学了跨合约调用,interface和call调用,interface更高级,call更低级。最好使用interface调用。
下面我给你一份正式、精准、审计导向的总结,帮助你把今天学的内容形成体系:
- require / revert / assert 的区别
- fallback 是否像 try/except
- 跨合约调用:interface vs call(你理解方向正确,但要更精确)
内容贴合你未来面试和审计工作需要掌握的深度。
一、错误处理:require / revert / assert 的本质区别
这是 Solidity 初级 → 初中级必须吃透的基础。
1. require
用于输入验证 / 前置条件检查
如果条件不满足 → revert 并退还剩余 gas。
典型用途:
- 参数不合法(msg.value、地址、数量)
- 权限检查(onlyOwner)
- 外部调用失败(require(success))
示例:
1 | |
特点:
- 最常用
- 可附带错误信息
- 可用于所有正常业务逻辑
面试回答:require 用于业务逻辑中可预期的输入错误。
2. revert
主动抛出异常(手动触发错误)
功能与 require 完全等价,只是写法不同。
示例:
1 | |
典型用途:
- 多层 if 中出错时返回
- 自定义错误(custom error)
现代写法(节省 gas):
1 | |
面试用一句话总结:
revert 用于主动中断执行,允许更灵活的逻辑结构。
3. assert
用于检查“不可能出错”的内部状态(invariants)
即:
如果 assert 失败 → 表示合约内部逻辑出现致命错误(bug)。
示例:
1 | |
特点:
- 不是给用户输入错误用的
- 不是给外部调用判断用的
- 是给开发者检测内部不变量用的
最关键点:
assert 失败会消耗掉所有剩余 gas(panic),因为它代表严重错误。
审计时常用规则:
- assert 只能用于 invariant,不允许随便使用
- 不能用 assert 去验证用户输入
- 用 assert 表示“这行永远应该成立,否则是 bug”
二、fallback 是否等同于 Python 的 try/except?
你现在的理解方向是对的:
fallback 的功能类似于“保证调用不失败”,属于异常兼容机制的一部分。
但是:
它不是 try/except 的语义替代,而是 EVM 层面的“默认处理方法”。
我们精准一点。
fallback 的本质意义
- 当外部调用一个不存在的函数时
- 或者没有 data 的普通调用(没有 receive 时)
Solidity 会执行 fallback。
fallback 不能让错误“继续执行”,而是一个:
“最后兜底函数”
类似于“一个默认 handler”,但不是异常处理。
类似程度:
- 你说的 try-except-continue 有一点类似:fallback 用来保证“调用地址存在,但函数不存在时不会直接黑洞掉”,而是进入 fallback。
但它不负责捕获你合约内部的错误。
fallback 是“兜底调用接口”。
try/catch 才是“异常捕获语义”。
真正的 try/catch 示例:
1 | |
fallback ≠ try/catch
fallback 是外部调用用的默认入口。
三、你对跨合约调用的理解已经正确,但可以更精准
你说:
interface 更高级
call 更低级
最好使用 interface 调用
是对的,但需要升级为审计级别解释:
1. interface 调用
- 强类型检查
- 编译器验证函数存在
- 自动生成 ABI
- 返回值自动解码
- 没有 selector 字符串错误
- 易读、易审计
智能合约正常业务逻辑都应该使用 interface 调用。
2. call 调用
- 低级 call,不检查函数是否存在
- 需要 encode
- 返回值必须 decode
- 易出错,但非常灵活
- 用在 Router、Proxy、钱包等系统合约中
只有需要“动态调用未知合约地址”时才使用 call。
四、把今天内容打包成一个你能背的“面试标准答案”
下面这段你可以在面试中原文复述:
在 Solidity 中,错误处理主要分为 require、revert 和 assert 三类。
- require 用于检查外部输入的合法性,是最常用的错误处理机制,失败时会 revert 并退还剩余 gas。
- revert 用于主动抛错,与 require 功能一致,但更灵活,适用于复杂条件与自定义错误。
- assert 用于验证合约内部不变量,只要失败就表示合约逻辑 bug,会触发 panic,是最严格的错误。
fallback 函数用于处理“调用不存在函数”或“没有 receive 的 ETH 转账”等场景,是一个兜底入口,不等同于 try/except;真正的异常捕获使用 try/catch。
跨合约调用可以使用 interface 或底层 call。interface 调用具有类型安全、可读性强、错误更少,是推荐方式;call 是低级调用,灵活但容易出错,只适用于需要通用动态调用的场景。
这是你未来面试时的标准表现。
如果你愿意,我可以继续帮你做:
- 画一个错误处理流程图
- 用简单代码演示三种错误的行为差异
- 用 Remix trace 展示 revert 与 assert 的不同机制
- 给你 10 条关于 require/revert/assert 的审计题(附答案)
告诉我你要哪个。