solidity学习笔记02

error、revert、event

  • error 用于自定义错误,然后由开发者抛出给客户端

  • panic 表示“程序内部严重故障”,由 EVM 或编译器自动抛出,开发者无法自定义 Panic 消息

  • revert 关键字类似 Java 的 throw,用于主动终止当前交易的执行,并回滚所有状态更改。它可以配合自定义错误(custom error) 使用,也可以单独使用。

  • event 关键字用于定义事件,事件将被发送给客户端(区块链网络)

 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
//定义一个名为Coin的智能合约
contract Coin {

    /*
    关键字 "public"会自动生成一个函数,使得该变量可以被其他合约访问。生成的函数大致如下:
    function minter() external view returns (address) { return minter; }
    */
    address public minter;

    //mapping可以被看作是被初始化的哈希表,每一个可能的键从一开始就存在,并被映射到一个值,其字节表示为全零的值。但是无法获得所有的键或所有的值
    mapping(address => uint) public balances;

    // 使用event关键字定义事件,允许客户端对声明的特定合约变化做出反应
    event Sent(address from, address to, uint amount);

    // 构造函数代码只有在合约第一次部署创建时运行,因此部署者将成为minter,才有权限铸币。
    constructor() {
        minter = msg.sender;
    }

    // 向一个地址发送一定数量的新创建的代币,但只能由合约创建者minter调用
    function mint(address receiver, uint amount) public {
        require(msg.sender == minter);
        balances[receiver] += amount;
    }

    // 使用 error 关键字自定义错误
    error InsufficientBalance(uint requested, uint available);

    // 从调用者那里发送一定数量的代币到一个地址也就是转账
    function send(address receiver, uint amount) public {
        if (amount > balances[msg.sender])
        // 使用 revert 抛出自定义的错误(类似 throw),终止并恢复所有变化
            revert InsufficientBalance({
                requested: amount,
                available: balances[msg.sender]
            });

        balances[msg.sender] -= amount;
        balances[receiver] += amount;

        //发送事件,以太坊客户端如网络应用,可以监听区块链上发出的这些事件,事件一旦发出,监听器就会被动收到参数 from, to 和 amount,这使得跟踪交易成为可能。
        emit Sent(msg.sender, receiver, amount);
    }
}

EVM 存储数据的三个区域

  • 存储

    每个账户都有一个称为存储的数据区,持久化存储。存储是一个键值存储,将 256 位的字映射到 256 位的字。 无法枚举存储,读取的成本相对较高,初始化和修改存储的成本更高。

  • 内存

    合约在每次消息调用时都会获得一个新清除的实例,内存是线性的,可以在字节级寻址,但读的宽度限制在 256 位,而写的宽度可以是 8 位或 256 位。

  • EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为 栈(stack) 的区域执行。 栈最大有 1024 个元素,每个元素长度是一个字(256 位)。

委托调用和库

存在一种特殊的消息调用,被称为委托调用(delegatecall),比如 msg.sender 和 msg.value。

这意味着合约可以在运行时动态地从不同的地址加载代码。存储、当前地址、余额仍然指的是调用合约,只是代码取自被调用的地址。

创建、停用、自毁

从区块链上删除代码的唯一方法是当该地址的合约执行 selfdestruct 操作。 存储在该地址的剩余以太币被发送到一个指定的目标,然后存储和代码被从状态中删除。如果有人向被删除的合约发送以太币,以太币就会永远丢失。

单位和全局可用变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
1 wei == 1

1 gwei == 1e9

1 szabo == 1e12 (Solidity 0.7.0 版本中被正式弃用)

1 finney == 1e15 wei (Solidity 0.7.0 版本中被正式弃用)

1 ether == 1e18 == 1ETH

由于 `闰秒` 会造成不是每一年都等于365天,甚至不是每一天都有24小时,而且因为闰秒是无法预测的,所以需要借助外部的预言机来对一个确定的日期代码库进行时间矫正!因此在 0.5.0 版本中删除了后缀 years

1 == 1 seconds

1 minutes == 60 seconds

1 hours == 60 minutes

1 days == 24 hours

1 weeks == 7 days

全局变量

  • blockhash(uint blockNumber) returns (bytes32): 给某个区块生成哈希值 - 只对最近的 256 个区块有效。在 0.4.22 版本中被废弃,在 0.5.0 版本中被删除。

  • blobhash(uint index) returns (bytes32): 用于在智能合约中读取当前交易附带的 Blob 数据的哈希值,让合约能验证和引用链下或 L2 提交的“大块数据”(Blobs)而不暴露原始数据内容,而无需将这些数据全部存入昂贵的链上存储。 ( EIP-4844 )。

  • block.basefee (uint): 当前区块的基本费用 ( EIP-3198 和 EIP-1559 )

  • block.blobbasefee (uint): 当前区块的 blob 基础费用( EIP-7516 和 EIP-4844)

  • block.chainid (uint): 当前链的 ID

  • block.coinbase (address payable): 当前区块矿工的地址

  • block.difficulty (uint): 当前区块的难度值( EVM < Paris )。对于其他 EVM 版本,它是 block.prevrandao 的一个废弃的别名,将在下一个重大改变版本中被删除。

  • block.gaslimit (uint): 当前区块的燃料上限

  • block.number (uint): 当前区块的区块号

  • block.prevrandao (uint): 由信标链提供的随机数( EVM >= Paris )(见 EIP-4399 )。

  • block.timestamp (uint): 当前区块的时间戳,自 Unix epoch 以来的秒数

  • gasleft() returns (uint256): 剩余燃料,前身是 msg.gas, 在 0.4.21 版本中被弃用,在 0.5.0 版本中被删除。

  • msg.data (bytes): 完整的调用数据

  • msg.sender (address): 消息发送方(当前调用)

  • msg.sig (bytes4): 调用数据的前四个字节(即函数标识符)。

  • msg.value (uint): 随消息发送的 wei 的数量

  • tx.gasprice (uint): 交易的燃料价格

  • tx.origin (address): 交易发送方(完整调用链上的原始发送方)

错误处理

  • assert(bool condition) 用于测试,内部错误。如果条件不满足,会导致异常,状态回滚

  • require(bool condition) 用于输入或外部组件的错误。如果条件不满足,状态回滚。

  • require(bool condition, string memory message) 提供一个错误消息。

  • revert() 终止运行并状态回滚。

  • revert(string memory reason) 提供一个解释性的字符串。

密码函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//计算 (x + y) % k,加法会在任意精度下执行,并且加法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。
addmod(uint x, uint y, uint k) returns (uint)

//计算 (x * y) % k,乘法会在任意精度下执行,并且乘法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。
mulmod(uint x, uint y, uint k) returns (uint)

//计算输入的 Keccak-256 哈希值。
keccak256(bytes memory) returns (bytes32)

//计算输入的 SHA-256 哈希值。
sha256(bytes memory) returns (bytes32)

//计算输入的 RIPEMD-160 哈希值。
ripemd160(bytes memory) returns (bytes20)

//利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
/**函数参数对应于签名的 ECDSA 值
r = 签名的前32字节
s = 签名的第二个32字节
v = 签名的最后1个字节
ecrecover 返回一个 address,而不是 address payable。*/

address 类型的成员

 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
//以 Wei 为单位的 地址类型 的余额。
<address>.balance  uint256 

//在 地址类型 的代码(可以是空的)。
<address>.code  bytes memory 

//地址类型 的代码哈希值
<address>.codehash  bytes32 

//向 地址类型 发送数量为 amount 的 Wei,失败时revert,自动回滚,发送2300燃料的矿工费,不可调节。
<address payable>.transfer(uint256 amount)

//向 地址类型 发送数量为 amount 的 Wei,失败时返回 false,不会自动revert,2300燃料的矿工费用,不可调节。
<address payable>.send(uint256 amount) returns (bool)

/**
在 EVM(以太坊虚拟机)中,所有合约交互最终都通过以下操作码实现:
CALL → 普通调用
DELEGATECALL → 代理调用
STATICCALL → 只读调用

Solidity 的 .call()、.delegatecall()、.staticcall() 就是对上述操作码的封装,这些函数都是低级别的函数,应该谨慎使用。因为任何未知的合约都可能是恶意的,调用未知合约就意味着把控制权交给了该合约,而该合约又可能回调到原合约中,所以要准备好在调用返回时改变您合约的状态变量。
*/

//低级调用,向目标地址发起普通消息调用,执行其代码,使用目标合约的存储、余额和上下文,返回是否成功的结果和数据,发送所有可用燃料,燃料费可调节。应避免在执行另一个合约函数时使用 .call(),因为它绕过了类型检查、函数存在性检查和参数打包.
<address>.call(bytes memory) returns (bool, bytes memory)

//代理调用,在当前合约的上下文中,执行目标合约的代码,返回是否成功的结果和数据,发送所有可用燃料,燃料费可调节。
<address>.delegatecall(bytes memory) returns (bool, bytes memory)

//发起一次只读调用,禁止任何状态修改(包括发送 ETH、写 storage 等),返回是否成功的结果和数据,发送所有可用燃料,燃料费可调节。
<address>.staticcall(bytes memory) returns (bool, bytes memory)

合约相关

  • this 指当前合约,可以显示转换为地址类型

  • super 指上级合约

  • selfdestruct(address payable recipient) 销毁当前合约,将其资金发送到给定的 地址类型 并结束执行。

  • 每个合约定义后,其名称本身就是一个类型。

1
2
3
4
5
6
//如果 Child 继承自 Parent,那么 Child 实例可隐式转为 Parent 类型:
Parent p = new Child();

//任何合约实例都可以显式转为 address:
MyToken token = new MyToken();
address addr = address(token);

保留关键词

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计