remix编译运行solidity2

上一篇:remix编译运行solidity

昨天我简要学习了全局变量和函数、表达式与控制结构,storage / memory / calldata三者差异,constructor构造函数 / modifier修饰器,payable函数,event+emit 事件与触发事件。然后在remix练习了几个简单的小合约,1.counter计数器,bank简单存取款器,struct+mapping 结构体和映射关系,双向mapping小合约用于地址是否关注的查询。然后我了解到了solidity比python要难,struct类似C++的结构体,solidity需要考虑gas费,要考虑资源消耗还要考虑安全性,不能只考虑可用性。

错误处理

ErrorDemo.sol

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract ErrorDemo {
// Custom Error(节省 gas,通常在复杂项目中使用)
error NotEnoughBalance(uint256 requested, uint256 available);

uint256 public totalSupply;
mapping(address => uint256) public balanceOf;

constructor() {
totalSupply = 1000;
balanceOf[msg.sender] = 1000;
}

// 1. 使用 require:输入验证 /权限 / 状态验证
function transfer(address to, uint256 amount) external {
require(to != address(0), "Invalid address");
require(balanceOf[msg.sender] >= amount, "Insufficient balance");

balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}

// 2. 使用 revert:手工抛错(推荐在复杂逻辑中使用)
function withdraw(uint256 amount) external {
uint256 bal = balanceOf[msg.sender];

if (amount > bal) {
revert NotEnoughBalance(amount, bal);
}

balanceOf[msg.sender] -= amount;
}

// 3. 使用 assert:用于断言永远为真(仅内部检查)
// totalSupply 永不减少(根据业务逻辑)
function internalInvariantCheck() external view returns (bool) {
// assert 失败代表代码存在 bug
assert(totalSupply >= 1000);
return true;
}

// 4. 显示触发 revert(手工示例)
function forceFail() external pure {
revert("Manually reverted");
}
}

balanceOf是什么意思?

balanceOf[msg.sender] = 1000;

这行代码本质上就是:

给当前调用者(msg.sender)在 balanceOf 这个“账本”里存一条记录:余额 = 1000。

require的错误处理


在上面的图中,输入一个eth地址,输入amount为10000,点击transact,于是就出现报错。

报错如下图所示,Insufficient balance

如果把amount改成1,点击transact,就没有报错。

看到sol的17-19行代码

1
2
3
function transfer(address to, uint256 amount) external {
require(to != address(0), "Invalid address");
require(balanceOf[msg.sender] >= amount, "Insufficient balance");

这就是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract WalletWithFallback {

// 记录 fallback 被调用时的信息
event FallbackCalled(address sender, uint value, bytes data);

// 接收 ETH 的 receive 函数,只要 msg.data 为空就触发
receive() external payable {
// 可以不写内容,只要 payable 就能收 ETH
}

// fallback 函数:有 data,或找不到函数签名时触发
fallback() external payable {
emit FallbackCalled(msg.sender, msg.value, msg.data);
}

// 查询当前合约内 ETH 余额
function getBalance() external view returns (uint) {
return address(this).balance;
}
}

测试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": "FallbackCalled"

event是什么?

event 是 Solidity 中的“日志输出机制”(Log),写入到区块链的交易日志(Transaction Logs)中,用于让前端、脚本、分析程序监听你的合约行为。

它相当于:

  1. 数据库的 “append-only” 日志
  2. 软件系统里的 “事件通知机制”
  3. 前端可以监听的 “发布消息”

最关键特点:
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
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Callee {
uint public number = 2025;

function getNumber() external view returns (uint) {
return number;
}
}


点getNumber, 返回2025
点number,还是返回2025

该合约部署好后,接下来用caller来调用该合约。

上图点击copy address,即可复制该合约的地址,该合约地址在后面有用的。

caller.sol

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// 1. 定义接口
interface ICallee {
function getNumber() external view returns (uint);
}

contract Caller {

// 2. 用 interface 调用
function callByInterface(address calleeAddr)
external
view
returns (uint)
{
return ICallee(calleeAddr).getNumber();
}

// 3. 用 low-level call 调用
function callByLowLevel(address calleeAddr)
external
view
returns (uint)
{
(bool ok, bytes memory data) =
calleeAddr.staticcall(
abi.encodeWithSignature("getNumber()")
);

require(ok, "low-level call failed");

return abi.decode(data, (uint));
}
}


复制callee合约的地址,在上图calleeAddr右侧粘贴,点击call。 即可成功调用callee
在decoded output可以看到2025这个字符串。

在下面callbylowerlever,使用同样的方法,复制callee合约的地址,粘贴,点击call。
同样得到2025的字符串。

下面说明interface调用和call调用的区别

下面给你一套面向初学审计 / 开发者的清晰、正式解释,涵盖你要理解的四个核心点:

  1. interface 调用
  2. call 调用
  3. call 为什么是唯一“安全”的低级调用
  4. msg.sender 的变化

内容会严格对标你未来审计岗位需要掌握的逻辑。

一、两种跨合约调用方式(interface vs call)

方式 1:通过 interface 调用(推荐)

1
2
3
4
5
6
7
8
9
interface ICallee {
function getNumber() external view returns (uint);
}

contract Caller {
function callByInterface(address _callee) external view returns (uint) {
return ICallee(_callee).getNumber();
}
}

特点:

  • 静态类型检查(编译器保证函数存在、类型正确)
  • 语义清晰(跨合约调用意图明确)
  • 更易审计(函数名、参数、返回类型可读性强)

核心点:
interface 调用是 Solidity 给你的一个“高层调用”,底层帮你生成 ABI 编码,不需要手工编码或字节处理。

方式 2:使用 call 调用(低级调用)

1
2
3
4
5
6
7
8
contract Caller {
function callByLowLevel(address _callee) external returns (uint) {
(bool ok, bytes memory data) =
_callee.call(abi.encodeWithSignature("getNumber()"));
require(ok, "call failed");
return abi.decode(data, (uint));
}
}

特点:

  • 需要手动编码函数签名与参数(没有编译器保障)
  • 返回值需要自己 decode
  • 无法静态检查被调用函数是否存在

典型使用场景:

  • 调用未知 ABI 的合约
  • 与非标准合约或代理合约交互
  • 编写通用路由器或钱包合约

审计记忆点:
call 属于“低级调用”,容易出现:

  • 编码错误
  • 调用错误函数
  • decode 错误
  • 忽略了 call 的返回值(造成安全隐患)

二、为什么 call 被认为是唯一“安全”的低级调用?

注意:这里的“安全”指的是执行不 revert、永不 panic 的角度,而不是业务逻辑安全。

在 Solidity 中有三个低级调用:calldelegatecallstaticcall
其中,只有 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 = Caller
  • tx.origin = 最初发起交易的外部账户

图示:

1
2
3
4
EOA → Caller → Callee

在 Callee 中:
msg.sender = Caller

情况 2:使用 interface 调用和使用 call 调用,msg.sender 完全一样

无论你用:

1
ICallee(_addr).getNumber();

还是:

1
_addr.call(abi.encodeWithSignature("getNumber()"));

结果都一样:

在 Callee 内部:

1
msg.sender = Caller 合约

原因:
调用方式不同,但执行路径都是 EVM 的 CALL 操作。

注意:delegatecall 才会改变 msg.sender 的语义

delegatecall 会“借用” caller 的上下文,所以:

1
2
3
4
Caller.delegatecall → Callee

在 Callee 中:
msg.sender = EOA(外部账户)

但你现在学“跨合约调用”,focus 在:

  • interface 调用
  • call 调用
    这两者的 msg.sender 完全一致。

五、总结(你面试时就这么回答)

如果你要面试 Junior Auditor,可以直接这样回答:

  1. interface 调用更安全、清晰、有编译期检查。适合正常业务逻辑的跨合约交互。

  2. call 是唯一“安全”的低级调用,因为:

    • 永远不会因为找不到函数而 revert
    • 是 EVM 原生操作,最通用
    • 能附带 ETH
    • Router/Proxy 必须用 call
  3. 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
2
A 合约(当前上下文)  
→ 外部地址 B(另一个上下文)

这相当于程序从一个代码库跳转到另一个独立代码库执行。

三、跨合约调用的本质

本质是 EVM 底层操作:

  • CALL
  • STATICCALL
  • DELEGATECALL
  • CALLCODE(已废弃)

你现在主要学习的,是最常见的CALL

对于大部分业务逻辑:

1
A 调用 B == A 执行了一次 CALL 指令

四、跨合约调用与普通函数调用的对比

| 类型 | 执行位置 | msg.sender | 需要 ABI 编码 | 能否外部发送 ETH |
| 合约内部函数调用 | 本合约内部 | EOA / 上级调用者 | 不需要 | 不能 |
| 跨合约调用(CALL) | 切换到另一个合约 | 调用的合约地址 | 需要 | 可以 |

关键区别:
跨合约调用需要 ABI 编码,且 msg.sender 会变成调用合约地址。

五、跨合约调用的典型例子

例 1:interface 调用(高层抽象)

1
ICallee(callee).foo();

例 2:低级调用(低层 call)

1
callee.call(abi.encodeWithSignature("foo()"));

两者都属于跨合约调用。

六、跨合约调用的意义(为什么重要?)

跨合约调用是智能合约编程的核心:

  1. 模块化架构
    多个合约各自负责不同业务逻辑,共同构成系统。

  2. 与外部协议交互
    调用 DeFi:Uniswap、Maker、Aave、Compound
    调用 NFT:ERC721、ERC1155
    调用 Oracle:Chainlink、Tellor

  3. 升级代理(Proxy)必须使用跨合约调用
    代理合约 delegatecall 执行逻辑合约。

  4. 安全审计重点
    大量漏洞都出现在跨合约调用环节,如:

    • 重入攻击(Reentrancy)
    • delegatecall 上下文污染
    • 调用失败未检查
    • fallback 调用意外触发

所以你必须熟练掌握跨合约调用的执行模型。

七、跨合约调用的执行顺序(可视化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EOA 发起交易
|
v
Caller 合约执行
|
|— CALL 发送到 Callee
|
v
Callee 执行
|
v
返回 Caller
|
v
交易最终完成

Callee 内部看到的:

1
2
msg.sender = Caller 合约地址
msg.value 由 CALL 传递

八、跨合约调用的分类

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(amount > 0, "invalid amount");

特点:

  • 最常用
  • 可附带错误信息
  • 可用于所有正常业务逻辑

面试回答:require 用于业务逻辑中可预期的输入错误。

2. revert

主动抛出异常(手动触发错误)

功能与 require 完全等价,只是写法不同。

示例:

1
2
3
if (balance < amount) {
revert("insufficient balance");
}

典型用途:

  • 多层 if 中出错时返回
  • 自定义错误(custom error)

现代写法(节省 gas):

1
if (balance < amount) revert InsufficientBalance();

面试用一句话总结:
revert 用于主动中断执行,允许更灵活的逻辑结构。

3. assert

用于检查“不可能出错”的内部状态(invariants)

即:
如果 assert 失败 → 表示合约内部逻辑出现致命错误(bug)。

示例:

1
assert(totalSupply >= balances[user]);

特点:

  • 不是给用户输入错误用的
  • 不是给外部调用判断用的
  • 是给开发者检测内部不变量用的

最关键点:
assert 失败会消耗掉所有剩余 gas(panic),因为它代表严重错误。

审计时常用规则:

  • assert 只能用于 invariant,不允许随便使用
  • 不能用 assert 去验证用户输入
  • 用 assert 表示“这行永远应该成立,否则是 bug”

二、fallback 是否等同于 Python 的 try/except?

你现在的理解方向是对的:
fallback 的功能类似于“保证调用不失败”,属于异常兼容机制的一部分。

但是:
不是 try/except 的语义替代,而是 EVM 层面的“默认处理方法”。

我们精准一点。

fallback 的本质意义

  1. 当外部调用一个不存在的函数时
  2. 或者没有 data 的普通调用(没有 receive 时)

Solidity 会执行 fallback。

fallback 不能让错误“继续执行”,而是一个:

“最后兜底函数”
类似于“一个默认 handler”,但不是异常处理。

类似程度:

  • 你说的 try-except-continue 有一点类似:fallback 用来保证“调用地址存在,但函数不存在时不会直接黑洞掉”,而是进入 fallback。

但它不负责捕获你合约内部的错误。

fallback 是“兜底调用接口”。
try/catch 才是“异常捕获语义”。

真正的 try/catch 示例:

1
2
3
4
5
try otherContract.foo() returns (uint v) {
// success
} catch {
// failure
}

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 的审计题(附答案)

告诉我你要哪个。