solidity学习笔记03

EXTCODESIZE 检查

EVM 认为对一个不存在的合约的调用总是成功的,即使这个地址根本不是合约(比如一个普通钱包地址),EVM 也不会报错,而是:

不执行任何代码 返回成功(success = true) 返回数据为空(returndata = “")

所以为了避免上述问题,Solidity 在高级调用时会自动插入一个检查:

在调用前,先用 EXTCODESIZE 操作码查询目标地址是否有代码。 如果 code size == 0 → revert 如果 code size > 0 → 正常调用

但是有例外:

  1. 如果要对返回数据进行 ABI 解码,则会跳过 EXTCODESIZE 检查,直接调用。

因为即使对方是空地址,CALL 也会返回空数据("") 当你尝试用 ABI 解码空数据时,解码器会失败并 revert 所以“让解码器报错”和“提前检查”效果一样,但后者省了一次 EXTCODESIZE 操作(节省 gas)

  1. 低级调用(.call, .delegatecall 等)没有 EXTCODESIZE 检查,低级调用是“裸操作”,完全信任开发者

  2. 预编译合约(如 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)
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计