;)
1.assert与require
| 特点 | require |
assert |
|---|---|---|
| 异常类型 | Error | Panic |
| Gas 处理 | 剩余 gas 退回 | 剩余 gas 消耗掉 |
| 用途 | 外部输入条件检查(用户错误) | 内部逻辑保证(代码错误) |
| 信息提示 | 可自定义错误信息 | 不能加提示,只能抛出 Panic特点 |
当 assert 条件为 false 时:
- 会触发一个 Panic 异常(error code
0x01) - 消耗掉函数调用中剩余的所有 gas(不退回)
- 回滚所有状态修改
官方推荐:
assert用于检查 永远应该为 true 的内部条件,也就是“不可能出错的逻辑”。- 如果
assert失败,说明代码里有 bug(比如整数溢出、不变式被破坏)。
Solidity 0.8.x 的变化
在 0.8.x 版本后:
编译器会自动为 整数溢出、下溢 插入
assert,所以这类错误会抛出 Panic,而不是普通错误。例如:
1
2uint8 x = 255;
x += 1; // 自动触发 assert,因为溢出
Panic 错误码(常见)
0x01:assert(false)失败0x11:算术运算溢出或下溢0x12:除以 00x21:转换到不存在的 enum 成员0x31:空数组 pop0x32:数组越界0x41:分配内存过大
2.接收ETH receive和fallback
接收ETH函数 receive
receive()函数是在合约收到ETH转账时被调用的函数。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。
当合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300,receive()太复杂可能会触发Out of Gas报错;如果用call就可以自定义gas执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。
我们可以在receive()里发送一个event,例如:
1 | // 定义事件 |
有些恶意合约,会在receive() 函数(老版本的话,就是 fallback() 函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。
回退函数 fallback
fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }。
我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:
1 | event fallbackCalled(address Sender, uint Value, bytes Data); |
1 | 触发fallback() 还是 receive()? |
| 函数 | 是否自动执行 | 触发条件 | 是否接收 ETH | 典型用法 |
|---|---|---|---|---|
constructor |
✅ 部署时一次 | 部署合约 | ✅ 如果写了 payable |
初始化合约,接受部署时资金 |
receive() |
❌ | 收到 ETH 且 msg.data 为空 |
✅ | 专门收款 |
fallback() |
❌ | 1. 调用不存在函数 2. 收到 ETH 且 msg.data 非空或无 receive |
✅ 如果加了 payable |
容错处理、代理转发 |
receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。
当合约是一个可以接受eth的合约(写有带payable的receive或fallback或constructor),那么其他合约与这个合约交互时,参数地址就要加上payable
| 接收方式 | 是否需要 receive / fallback |
|---|---|
| transfer / send / call(“”) | ✅ 需要 |
| 调用 payable 函数,并携带eth | ❌ 不需要 ,正常情况不会触发回退函数 |
| 构造函数 payable(构造函数带payable,在初始化合约时可以转账) | ❌ 不需要 ,不会触发回退函数,因为回退函数未初始化 |
| selfdestruct 强制转账 | ❌ 不需要 ,不会触发回退函数 |
情况A(直接转账):你走进银行,把钱扔进柜台:”给你钱”
→ 柜员需要决定如何处理(存款?缴费?)
→ 对应 receive()/fallback()
情况B(调用payable函数携带eth):你走进银行:”我要存钱到我的账户”
→ 柜员直接执行存款流程
→ 对应调用 deposit() 函数—不触发回退函数
3.staticcall和call
| 特性 | call |
staticcall |
|---|---|---|
| 能否修改状态 | ✅ 可以 | ❌ 不可以 |
| 能否转账 ETH | ✅ 可以 | ❌ 不可以 |
| 返回值 | (success, returndata) |
(success, returndata) |
| 常见用途 | 执行函数、转账 | 只读查询数据特性 |
1 | contract A { |
call 调用 setX 成功修改了 x。
staticcall 调用 getX 成功拿到返回值 123。
如果用 staticcall 去调 setX 会直接报错。
4.关于原生币转账和回退函数的执行
🌟 为什么 ETH 转账没有“转账逻辑”代码?
因为 ETH 是原生资产(native asset),它的余额不是存储在合约里,而是由 **EVM 在状态树(State Trie)中直接维护 balances[address]**。
也就是说:
- ERC20 代币需要在合约里
balances[address] += amount - 但 ETH 不需要,它没有对应的 Token 合约
- 它的余额是 EVM 内部在执行 CALL 时自动更新的
🧠 ETH 转账的真正流程(在 EVM 层)
当你执行:
1 | address(contract).call{value: 1 ether}(""); |
EVM 执行的步骤如下:
1:CALL 指令准备 value
EVM 读取 CALL 的参数,其中包括:
1 | value = 1 ether |
2:EVM 自动更新账户余额(状态树 State Trie)
EVM 直接执行:
1 | balances[msg.sender] -= value |
注意:这一步完全在 EVM 层,而不是在 Solidity 合约代码中。
3:如果目标是合约,执行 receive / fallback
余额更新后,才进入合约执行:
1 | receive()或fallback() |
4:如果回退函数执行失败,会 revert 整个调用
意味着:
- 余额更新和代码执行是 原子操作
- 回退函数报错 → 整笔 ETH 转账回滚
🎯 关键点总结
✔ ETH 转账没有合约“转账逻辑”,因为它是 EVM 内置的
ETH 是“协议层资产”,不是“合约层资产”。
✔ EVM 在执行 CALL 时自动更新余额
在 CALL 指令中,value 字段自动触发余额变化。
✔ 回退函数不是“转账逻辑”,只是额外的代码执行
真正转账逻辑是:
不在 Solidity
不在你的合约
不在代币合约
而是在 EVM 的内部规范中实现
5.预言机
区块链本身不能主动访问外部数据(价格、天气、体育比分等)。
但是大多数 DApp(交易所、借贷协议、保险)又必须使用真实世界的数据。
典型应用:
- DeFi:资产价格(借贷、清算、AMM、衍生品)
- GameFi:随机数
- NFT:元数据
- 保险:气象、灾害数据
- 预言机自动化:定时任务
于是需要一个可信组件:预言机 = 把链下数据安全地提供给链上的智能合约。
广义预言机 = [数据获取] + [数据验证] + [数据聚合] + [数据上链]
预言机 = 合约用来获得外部状态(价格、天气、利率)的方式
只要合约需要“外部状态”(不属于自己控制范围的数据)就需要预言机。
6.强制转账selfdestruct
用法:是一种合约无法拒绝的转账方式,不需要合约有任何接收函数,在部署攻击合约时带上value,调用attack时,会把攻击合约的所有余额转到目标合约
1 | contract AttackForce{ |
7.合约的状态变量存储解析
soliidty合约的状态变量存储以32个字节为一个单位,状态变量存储是依靠哈希表,存储索引从0开始递增,最大为2^256-1,如果多个变量长度加起来在32字节以内可以一起放在一个存储单位,对于变长数组作为状态变量的特殊情况,存储逻辑如下:变长数组所在索引作为数组的长度存储位置,数组单个元素的存储位置则是hash(数组长度所在索引)+元素在数组的索引
1 | // - 按声明顺序分配存储槽 |
1 | 你的描述:hash(数组长度所在索引) + 元素索引 |
映射存储规则:
1 | mapping(key => value) myMap; // 存储在 slot N |
还有结构体存储规则等,遇到在行搜索
8.构造器版本变化
Solidity 从 0.4.22 版本之前,构造器是名字和合约名一样的函数,此版本之后引入了 constructor 关键字作为构造函数的全新、推荐写法。
| 版本 | 构造函数写法 | 说明 |
|---|---|---|
| v0.4.22 之前 | function 合约名() public { ... } |
函数名必须和合约名完全一致(包括大小写),容易因拼写错误变成普通函数,引发安全漏洞。 |
| v0.4.22 及之后 | constructor(...) { ... } |
使用统一的 constructor 关键字,清晰明了,避免了因函数命名错误带来的风险。 |







