EXTCODESIZE 检查
EVM 认为对一个不存在的合约的调用总是成功的,即使这个地址根本不是合约(比如一个普通钱包地址),EVM 也不会报错,而是:
不执行任何代码
返回成功(success = true)
返回数据为空(returndata = “")
所以为了避免上述问题,Solidity 在高级调用时会自动插入一个检查:
在调用前,先用 EXTCODESIZE 操作码查询目标地址是否有代码。
如果 code size == 0 → revert
如果 code size > 0 → 正常调用
但是有例外:
- 如果要对返回数据进行 ABI 解码,则会跳过 EXTCODESIZE 检查,直接调用。
因为即使对方是空地址,CALL 也会返回空数据("")
当你尝试用 ABI 解码空数据时,解码器会失败并 revert
所以“让解码器报错”和“提前检查”效果一样,但后者省了一次 EXTCODESIZE 操作(节省 gas)
-
低级调用(.call, .delegatecall 等)没有 EXTCODESIZE 检查,低级调用是“裸操作”,完全信任开发者
-
预编译合约(如 ECDSA 签名验证 0x1、SHA256 0x2 等)是 EVM 内置的特殊地址,没有字节码(extcodesize == 0),使用高级调用调用预编译合约时会 revert,调用失败,因此调用预编译合约要用低级调用。
1
2
3
4
5
6
7
8
9
|
// 假设有一个接口指向 0x1(ecrecover 预编译)
IECRecover ecrecover = IECRecover(0x1);
bytes32 result = ecrecover.someFunction(...); // ← Solidity 会先检查 extcodesize(0x1)
// 发现为 0 → 直接 revert!调用失败
bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", to, amount);
(bool success, bytes memory ret) = address(0x1).call(data);
require(success);
// 然后手动 abi.decode(ret, ...),调用成功
|
外部函数调用
feed.info{value: 10, gas: 800} 只在本地设置 value 和随函数调用发送的 gas 数量,最后的括号执行实际调用。所以 feed.info{value: 10, gas: 800} 不会调用函数, value 和 gas 的设置也会丢失,只有 feed.info{value: 10, gas: 800}() 执行了函数调用。
1
2
3
4
5
6
7
8
9
10
11
12
|
pragma solidity >=0.6.2 <0.9.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public { feed = addr; }
function callFeed() public { feed.info{value: 10, gas: 800}(); }
//要使用value、gas,需要对info函数用payable修饰
}
|
溢出
1
2
3
4
5
6
7
|
//上溢:当一个数超过类型最大值时
uint8 a = 255;
a = a + 1; // 255 + 1 = 256,但 uint8 最大是 255 → 溢出
//下溢:当一个数低于类型最小值时
uint8 b = 0;
b = b - 1; // 0 - 1 = -1,但 uint8 不能为负 → 下溢
|
Solidity 0.8.0 之前对于溢出的行为:静默回绕
1
2
3
4
5
|
// Solidity < 0.8.0
uint8 x = 255;
x++; // x 变成 0(255 → 0)
uint8 y = 0;
y--; // y 变成 255(0 → 255)
|
攻击者可利用此漏洞盗取资金,漏洞,因此开发者必须使用 SafeMath 库手动检查。
Solidity 0.8.0+ 对于溢出自动 revert,如果还想使用旧的“回绕行为”,可以用unchecked
1
2
3
4
|
uint8 x = 255;
unchecked {
x++; // x = 0,不会 revert
}
|
try/catch
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
|
pragma solidity >=0.8.1;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// 如果有10个以上的错误,就永久停用该机制。
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// 如果在getData中调用revert,
// 并且提供了一个原因字符串,
// 则执行该命令。
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// 在发生Panic异常的情况下执行,
// 即出现严重的错误,如除以零或溢出。
// 错误代码可以用来确定错误的种类。
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// 在使用revert()的情况下,会执行这个命令。
errorCount++;
return (0, false);
}
}
}
|
常量变量(constant)和不可改变的变量(immutable)
状态变量可以被声明为 constant 或 immutable,这两种变量在合约构建完成后不能被修改。
对于 constant 变量,其值必须在编译时固定,也可以在文件级别定义 constant 变量。
对于 immutable 变量,仍然可以在构造时分配。与普通的状态变量相比,constant 和 immutable 的燃料成本要低得多。
目前支持常量和不可变量的类型是 字符串类型 (仅用于常量)和 值类型。
view
view:只读函数,可以读取状态变量但禁止修改状态变量。为了实现不修改,EVM 提供了 STATICCALL 操作码,在 STATICCALL 执行期间,任何尝试修改状态的操作都会导致 revert,这是运行时(runtime)级别的保护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 普通合约
contract DataStore {
uint public value = 100;
function getValue() public view returns (uint) {
return value; // 只读,合法
}
}
contract Caller {
function test() public view returns (uint) {
DataStore store = DataStore(0x...);
return store.getValue(); // ← 编译为 STATICCALL 只读调用
}
}
//即使 getValue() 内部不小心写了 value = 200;,EVM 也会在运行时 revert(因为 STATICCALL 禁止状态修改)。
|
DELEGATECALL 没有运行时(runtime)级别的保护,仅编译时检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 库合约
library MathLib {
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
function readStorage() public view returns (uint) {
// 注意:库函数访问的是调用者的 storage!
return SomeContract(msg.sender).value();
}
}
contract SomeContract {
uint public value = 42;
function compute() public view returns (uint) {
return MathLib.add(10, 20); // ← 编译为 DELEGATECALL 代理调用
}
}
|
合约的 getter 方法被自动标记为 view
pure

函数重载
1
2
3
4
5
6
7
8
|
function f(uint8 val) public pure returns (uint8 out);
function f(uint256 val) public pure returns (uint256 out);
//报错,因为 50 可以转为 uint8、uint256,不知道调用哪个f函数
f(50)
//不报错,调用f(uint256 val)
f(uint256)
|