关键词:constructor&Modifier event 继承 接口 异常 重载 库合约 引用 回调

构造函数constructor和修饰器Modifier

constructor

定义:是一种特殊函数 每个合约可以定义一个,并且在部署合约时自动运行一次。

可用于初始化合约参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MyToken {
string public name;
string public symbol;
uint256 public totalSupply;
address public owner;

//带参数的构造函数用于初始化状态变量
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name; // Token name
symbol = _symbol; // Token symbol
totalSupply = _initialSupply; // 设置初始代币供应量
owner = msg.sender; // 将部署者设置为合约的拥有者owner
}
}

Modifier

定义:类似于decorator,声明函数拥有的特性,并减少代码冗余

主要使用场景: 运行函数前的检查,例如地址,变量,余额等。

定义一个叫做onlyOwner的modifier:

1
2
3
4
5
//定义modifier
modifier onlyOwner{
require(msg.sender == owner);//检查调用者是否为owner地址
_;// 如果是的话,继续运行函数主体;否则报错并revert交易
}

带有onlyOwner修饰符的函数只能被owner地址调用

eg:

1
2
3
4
//改变owner
function changeOwner(address_newOwner) external onlyOwner{
owner = _newOwner;// 只能owner地址运行这个函数,并改变owner
}

​ 在以上函数中,由于onlyOwner修饰符的存在,只有原先的owner可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。

事件event

  1. 特点

  • 响应: 应用程序(ether.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。
  • 经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas
  1. 作用

    • 日志记录:事件在链上作为日志记录保存,不能被智能合约读取,但可供外部观察。
    • 通知机制:前端应用、DApp 等可以监听事件来响应合约的变化,如更新用户余额、确认交易等。
    • 优化 Gas 消耗:事件的存储成本低于状态变量的修改,因此在某些应用场景下,使用事件记录是更高效的选择。
  2. 声明事件

1
Event + 事件名称 + (变量类型 变量名,变量类型 变量名,...)

​ 以REC20代币合约的Transfer事件为例:

1
2
3
4
5
event Transfer(address indexed from,address indexed to,uint256 value);
//from:转账地址
//to:接收地址
//value:转账数量
//其中from&to前面带有indexed关键字,他们会保存在以太坊虚拟机日志的topics中,方便之后检索
  1. 释放事件

释放事件(Emit Event)是指在区块链上发布特定事件通知的操作。

通过释放事件,合约可以在发生某些操作(如状态改变、资金转移等)时,向链上日志系统发送记录。

事件通常用于通知外部应用程序,如前端应用或监听工具,便于监控合约状态的变化

关键字

1
Emit

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义_transfer函数,执行转账逻辑
function_tranfer(
address from,
address to;
uint256 amount
)external{

_balance[from] = 10000000;//给转账地址一些初始代币
_balances[from] -= amount; // from地址减去转账数量
_balances[to] += amount; // to地址加上转账数量


//释放事件
emit Transfer(from,to,amount);

}
  1. EVM日志

EVM用日志log来存储solidity事件,每条日志记录包括主题topics和数据data两部分

​ 5.1 主题topics(?)

用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)

​ eg: 例如对于上面的transfer事件,它的事件哈希:

1
2
3
keccak256("Transfer(address,address,unit256)")

//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

除了事件哈希,主题还可以包含至多3个indexed参数,也就是Transfer事件中的from和to。

​ 5.2 数据data

事件中不带 indexed的参数会被存储在 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topics 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topics 更少。

​ 5.3 在Etherscan上查询事件

当尝试用**_transfer()函数在Sepolia测试网络上转账100代币,可以在Etherscan上查询到相应的tx**:网址

​ 点击Logs按钮,就能看到事件明细

​ Topics里面有三个元素,[0]是这个事件的哈希,[1]和[2]是我们定义的两个indexed变量的信息,即转账的转出地址和接收地址。Data里面是剩下的不带indexed的变量,也就是转账数量。

继承

  1. 规则

1.1 virtual

​ 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。

1.2 override

​ 子合约重写了父合约中的函数,需要加上override关键字。

注意:用override修饰public变量,会重写与变量同名的getter函数

  1. 简单继承

​ 先写一个简单的A合约

​ 再定义一个B合约,让他继承A合约

1
contract B is A
  1. 多重继承

​ 规则:

  1. 继承时要按辈分最高到最低的顺序排

    eg:比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成

    1
    contract Erzi is Yeye, Baba

    而不能写成contract Erzi is Baba, Yeye,不然就会报错。

  2. 如果某一个函数在多个继承的合约里都存在,在子合约里必须重写,不然会报错

  3. 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字

1
override(Yeye, Baba)

4. 修饰器的继承

​ 用法与函数继承类似,在相应的地方加virtual和override关键字即可。

5. 构造函数的继承

​ 子合约有两种方法继承父合约的构造函数:

1. **在继承时声明父构造函数的参数**

eg:

1
contract B is A(1)
  1. 在子合约的构造函数中声明构造函数的参数

eg:

1
2
3
contract C is A {
constructor(uint _c) A(_c * _c) { }
}
  1. 调用父合约的继承

​ 子合约有两种方式调用父合约的函数:

  1. 直接调用

​ 子合约直接用父合约名.函数名()的方式来调用父合约函数

eg: Yeye.pop()

1
2
3
function callParent() public{
Yeye.pop();
}
  1. Super关键字

​ 子合约可以利用**super.函数名()**来调用最近的父合约函数。

eg:

​ 当Solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop()

1
2
3
4
function callParentSuper() public{
super.pop();
//此处调用的是Baba.pop()
}
  1. 钻石继承

​ 指一个派生类同时有两个或两个以上的基类。

1
2
3
4
5
6
7
/*
yeye
/ \
baba mama
\ /
me
*/
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
49
50
51
52
contract Yeye{

event Log(string message);

function foo() public virtual{
emit Log("Yeye.foo called");
}
function bar() public virtual{
emit Log("Yeye.foo called");
}
}

//baba继承yeye
contract Baba is Yeye{
function foo() public virtual override{
emit Log("Baba.foo called");
super.foo();
}

function bar() public virtual override{
emit Log("Eve.bar called");
super.bar();
}

//mama继承yeye
contract Mama is Yeye{
function foo() public virtual override{
emit Log("Baba.foo called");
super.foo();
}

function bar() public virtual override{
emit Log("Mama.bar called");
super.bar();
}


//me继承mama baba
contract me is Baba,mama{
function foo() public override(Baba,Mama){
super.foo();
}

function bar() public override(Baba,Mama){
super.bar();
//此处Super.bar会依次调用baba,mama最后是god合约
}
}



}

(当然现实辈分关系具体不是这样,只是代指三个层次)

所谓钻石,即虽然Baba,Mama都是Yeye的子合约,但整个过程中,God合约只会被调用一次

(因为solidity强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序)

抽象合约

​ 如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体**{}中的内容,则必须将该合约标为abstract**,不然编译会报错。

未实现的函数需要加virtual,以便子合约重写。拿插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上。

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

abstract constract Insertsort{
function insertionSort(uint[] memory a)public pure virtual returns(uint[] memory){
/*for(uint i = 0; i < a.length;i++){
uint temp = a[i];
uint j = i;
while(j > 0 && temp <a[j - 1]){
a[j] = a[j - 1];
j--;
}
a[j] = temp;
}
return 0;
*/
}
}

}

/*Solidity中最常用的变量类型是uint,也就是正整数,取到负值的话,会报underflow错误。而在插入算法中,变量j有可能会取到-1,引起报错。这里,我们需要把j加1,让它无法取到负值。*/

接口(interface)

规则

1. 接口不能包含状态变量
1. 不能包含构造函数
1. 不能继承除接口外的其他合约
1. 所有函数都必须是external且不能有函数体
1. 继承接口的非抽象合约必须实现接口定义的所有功能

接口提供了两个重要的信息:

  1. 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)
  2. 接口id(更多信息见EIP165

另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具,也可以将ABI json文件转换为接口sol文件。

接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。

什么时候使用接口:

我们不需要知道它的源代码,只需知道它的合约地址,用对应的接口就可以与它交互。都可以写模版并且减少代码冗余。

三种抛出异常

1. error

​ 可以在contract之外定义异常。

Eg : 我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:

1
error TransferNotOwner(); *//* *自定义error*

我们也可以定义一个携带参数的异常,来提示尝试转账的账户地址

1
error TransferNotOwner(address sender); // 自定义的带参数的error

在执行当中,error必须搭配revert(回退)命令使用

2. Require

​ 它很好用,唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高

使用方法:

1
require(检查条件,“异常的描述”)

​ 当检查条件不成立的时候,就会抛出异常

3. Assert

比require少个字符串,即不能抛出异常的原因

assert命令一般用于程序员写程序debug,它的用法很简单

1
assert(检查条件)

当检查条件不成立的时候,就会抛出异常。

三种方法的gas比较

error方法gas最少,其次是assert,require方法消耗gas最多

因此,error既可以告知用户抛出异常的原因,又能省gas要多用!

函数重载

实参匹配:调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。

如果出现多个匹配的重载函数,则会报错

overloading:即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。

solidity不允许修饰器(modifier)重载

库合约

​ 库合约是一系列的函数合集,用于提升solidity代码的复用性和减少gas而存在

​ 他和普通合约主要有以下几点不同:

  1. 不能存在状态变量
  2. 不能够继承或被继承
  3. 不能接收以太币
  4. 不可以被销毁

​ 库合约中的函数可见性如果被设置为public或者external,则在调用函数时会触发一次delegatecall。而如果被设置为internal,则不会引起。对于设置为private可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。

delegatecall:

delegatecall 是一种特殊的低级函数调用,用于将当前合约的上下文(包括msg.sendermsg.value等)传递给另一个合约的函数执行。delegatecall 允许合约在不改变调用者上下文的情况下执行另一个合约的代码。

delegatecall 的作用

  • 共享存储delegatecall 是调用另一个合约的代码,并在调用者合约的存储上下文中执行。这意味着被调用合约的代码会对调用合约的存储变量进行读写。
  • 保持调用者上下文msg.sendermsg.value 等上下文信息保持不变,依旧指向调用者,这和普通的合约调用不同。
  • 代码重用:使用 delegatecall 可以使多个合约共享同一段逻辑代码,通过代理模式实现合约的代码复用。

Strings库合约

Strings库合约是将uint256类型转换为相应的string类型的代码库

如何利用

​ 用using for 指令

1
using A for B

​ 用于附加库合约(从库A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。

注意: 在调用的时候,这个变量会被当作第一个参数传递给函数

直接通过库合约名称调用函数

1
2
3
4
5
6
7
8
9
//利用using for指令

using String for uint256;
function getString1(uint256_number)public pure returns(string memory){

//库合约中的函数会自动添加为uint256型变量的成员

return _number.toHexString();
}

常用库合约

Strings:将**uint256**转换为**String**

Address:判断某个地址是否为合约地址

Create2:更安全的使用**Create2 EVM opcode**

Arrays:跟数组相关的库合约

引用Import

​ 引用(import)在代码中的位置为: 在声明版本号之后,在其余代码之前

​ import语句可以帮助我们在一个文件中引用另一个文件的内容

用法

  1. 通过源文件相对位置导入
1
2
//通过文件相对位置import
import './name.sol'
  1. 通过源文件网址导入网上的合约的全局符号
1
2
//通过网址引用
import'url'
  1. 通过npm的目录导入

  2. 通过指定全局符号导入合约特定的全局符号

1
import {name} from'./name.sol'

​ 其中的 name 就是一个 全局符号。它可以是一个具体的合约、库、结构体、枚举或函数的名称。通过 import 语句,可以从指定的文件(例如 name.sol)中导入该符号,以便在当前文件中直接使用。

回调函数receive&fallback

作用

1. 接受ETH
1. 处理合约中不存在的函数调用(代理合约proxy contract)

(所以:fallback 和 receive 函数无法在合约内部直接调用。这些特殊函数只能通过外部调用触发,通常在接收以太币或处理未知的函数调用时自动执行

接收ETH函数receive

a. 在合约收到ETH转账时被调用

b. 一个合约最多有一个receive()函数

c. 声明方式:

1
receive() external payable { ... } [不需要function关键字]

d. receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable

receive()最好不要执行太多的逻辑,receive()太复杂可能会触发Out of Gas报错

回退函数fallback

a. 在调用合约不存在的函数时被触发

b. 可用于接收ETH,也可以用于代理合约proxy contract

c. 声明时不需要function关键字,必须由external修饰,一般也会用payable修饰

Eg: 用于接收ETH:

1
fallback() external payable { ... }

二者区别

  1. 合约接收ETH时,msg.data为空且存在Receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable

  2. receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错

    (你仍可以通过带有payable的函数向合约发送ETH)

1.png

​ 在这个场景中,vitalik 向合约 ReceiveETH 发起了一笔带有 msg.data(0xaa)的低级交互,同时设置了 value 为 100 Wei。让我们分析代码:

  • 合约 ReceiveETH 中定义了一个 receive() 函数,该函数是 external 和 payable 的,但 receive() 函数只能在没有 msg.data 的情况下被调用。
  • 由于 msg.data 不为空(0xaa),这次调用不会触发 receive() 函数。
  • 合约也没有定义 fallback 函数,所以任何带有 msg.data 且没有匹配函数签名的调用将会导致交易失败。

结论

这次调用将失败,并抛出错误,因为合约没有 fallback 函数来处理包含 msg.data 的调用。

所以会出现报错:error:’Fallback’ function is not defined, value和msg.data均发送失败

发送ETH

  1. transfer()

  2. send()

  3. call(),其中call()是被鼓励的用法。

首先构造发送ETH合约SendETH,并在其中实现payable的构造函数和receive()

1
2
3
4
5
6
7
contract SendETH{

//构造函数,payable使得部署的时候可以转eth进去
constructor() payable{}
//receive方法 接受eth时被触发
receive() external payable{}
}

1. transfer

1
接收方地址.transfer(发送eth的数额)

· transfer()gas限制是2300,足够用于转账,但对方合约的**fallback()receive()**函数不能实现太复杂的逻辑。

· transfer()如果转账失败(eg: amount>value),会自动revert(回滚交易)。

amount:通常表示用户或合约希望发送的 ETH 的数量。这里 amount 是一个变量,表示转账时指定的具体金额(单位为 wei)。

value:指交易中随附的 ETH 数量,通常由 msg.value 表示。这是调用合约时由发送方附加的 ETH 数量,通常用于支付给其他地址或完成购买。value 只能在 payable 函数中被使用。

1
2
3
4
//用transfer()发送ETH,_to填reveive合约地址,amount填ETH转账金额
function transferETH(address payable _to,uint256 amount) external payable{
_to.transfer(amount);
}

2. send

用法

1
接收方地址.send(发送ETH数额)

· send()gas限制是2300,足够用于转账,但对方合约的**fallback()receive()**函数不能实现太复杂的逻辑。

· send()如果转账失败,不会revert

· send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下

1
2
3
4
5
6
7
8
9
10
error SendFailed(); // 用send发送ETH失败error
// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
// 处理下send的返回值,如果失败,revert交易并发送error
bool success = _to.send(amount);
if(!success){
revert SendFailed();
}
}

3.Call

用法

1
接收方地址.call{value: 发送ETH数额}("")

· call()没有gas限制,可以支持对方合约**fallback()receive()**函数实现复杂逻辑。

· call()如果转账失败,不会revert

· call()的返回值是(bool, bytes),其中bool代表着转账成功或失败,需要额外代码处理一下

1
2
3
4
5
6
7
8
9
10
11
error CallFAiled()// 用call发送ETH失败error

// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
// 处理下call的返回值,如果失败,revert交易并发送error
(bool success,) = _to.call{value: amount}("");
if(!success){
revert CallFailed();
}
}

· call没有gas限制,最为灵活,是最提倡的方法;

· transfer2300 gas限制,但是发送失败会自动revert交易,是次优选择;

· send2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。

调用其他合约

如何调用

​ 可以利用合约的地址和合约代码(或接口)来创建合约的引用:_Name(_Address),其中_Name是合约名,应与合约代码(或接口)中标注的合约名保持一致,_Address是合约地址。然后用合约的引用来调用它的函数:_Name(_Address).f(),其中f()是要调用的函数。

1.传入合约地址

我们可以在函数里传入目标合约地址,生成目标合约的引用,然后调用目标函数。

以调用OtherContract合约的setX函数为例,我们在新合约中写一个callSetX函数,

传入已部署好的OtherContract合约地址_Address和setX的参数x:

1
2
3
function callSetX(address _address,uint256 x)external{
OtherContract(_Address).setX(x);
}

复制OtherContract合约的地址,填入callSetX函数的参数中,成功调用后,调用OtherContract合约中的getX验证x变为123

2.传入合约变量

我们可以直接在函数里传入合约的引用,只需要把上面参数的address类型改为目标合约名

比如上述的OtherContract

(ps: 该函数参数OtherContract _Address底层类型仍然是address,生成的ABI中、调用callGetX时传入的参数都是address类型)

例子:

通过传入合约变量调用目标合约的函数

1
2
3
function callGetX(OtherContract _Address) external view returns(uint x){
x = _Address.getX();
}

复制OtherContract合约的地址,填入callGetX函数的参数中,调用后成功获取x的值

3. 创建合约变量

1
2
3
4
5
6
function callGetX(address _Address) external view returns(uint x){

//创建变量
OtherContract oc = OtherContract(_Address);//oc为OtherContract别名
x = oc.getX;
}

复制OtherContract合约的地址,填入callGetX2函数的参数中,调用后成功获取x的值

4.调用合约并发送ETH

如果目标合约的函数是payable的,那么我们可以通过调用它来给合约转账:

1
_Name(_Address).f{value: _Value}()

其中**_Name是合约名,_Address是合约地址,f是目标函数名,_Value是要转的ETH数额(以wei**为单位)。

OtherContract合约的setX函数是payable的,在下面这个例子中我们通过调用setX来往目标合约转账。

1
2
3
function setXTransferETH(address otherContract, uint256 x) payable external{
OtherContract(otherContract).setX{value: msg.value}(x);
}

利用call调用合约

call 是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, bytes memory),分别对应call是否成功以及目标函数的返回值。

不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数

当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数

使用规则

1
目标合约地址.call(字节码);

(其中字节码利用结构化编码函数abi.encodeWithSignature获得:

1
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)

函数签名为”函数名(逗号分隔的参数类型)”

1
abi.encodeWithSignature("f(uint256,address)", _x, _addr)

另外call在调用合约时可以指定交易发送的ETH数额和gas数额:

1
目标合约地址.call{value:发送数额, gas:gas数额}(字节码);

利用Call调用合约举例

  1. Response事件
1
2
// 定义Response事件,输出call返回的结果success和data,方便观察返回值
event Response(bool success, bytes data);
  1. 调用setX函数

    定义callSetX函数来调用目标合约的setX(),转入msg.value数额的ETH,并释放Response事件输出successdata

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function callSetX(address payable _addr,uint256 x)public payable{

    //同时还可发送eth,_addr是目标合约的地址
    (bool success,bytes memory data) = _addr{value:msg.value}(
    abi.encodeWithSignature("setX(uint256)", x)
    );

    emit Response(success,data);//释放事件
    }

​ 当我们此时调用callSetX把状态变量_x改为5,参数为OtherContract地址和5,由于目标函数setX()没有返回值,因此Response事件输出的data为0x,也就是空。

  1. 调用getX函数

    所以我们还需要调用getX()函数用于返回目标合约X(uint256)的值

1
2
3
4
5
6
7
8
function callGetX(address _addr) external returns(uint256){
(bool success, bytes memory data) = _addr.call(
//可以利用abi.decode来解码call的返回值data,并读出数值。
abi.encodeWithSignature("getX()"));

emit Response(success,data);
return abi.decode(data, (uint256));
}

Response事件的输出,我们可以看到data为0x0000000000000000000000000000000000000000000000000000000000000005。而经过abi.decode,最终返回值为5。

  1. 调用不存在的函数

如果给call输入的函数不存在于目标合约,那么目标合约的fallback函数会被触发。

1
2
3
4
5
6
7
8
9
function callNonExist(address _addr)external{

//call不存在的foo()函数
(bool success, bytes memory data)=_addr.call(
abi.encodeWithSignature("foo(uint256)")
);

emit Response(success,data);
}

call了不存在的foo函数。call仍能执行成功,并返回success,但其实调用的目标合约fallback函数

题:

  • ​ 下列关于事件的说法中,错误的是

(选择一个答案)

A. Solidity中的事件(event)是EVM上日志的抽象。

B. 事件的声明由event关键字开头,然后跟事件名称。

C. 链上存储数据比存储事件的成本低。

D. 应用程序(ether.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。

解析:

  • 选项 A:正确。事件在 Solidity 中是 EVM 上日志的抽象,事件记录在链上日志中,但不直接参与合约逻辑。
  • 选项 B:正确。事件的声明确实是通过 event 关键字定义的。
  • 选项 C:错误。实际上,存储事件的成本比直接在链上存储数据要低,因为事件被存储在交易日志中,而不占用合约的存储空间,这降低了成本。
  • 选项 D:正确。应用程序(如 ethers.js)可以通过 RPC 接口监听这些事件,并在前端作出相应的反应。

正确答案是:

C. 链上存储数据比存储事件的成本低。

  • indexed关键字可以修饰任意类型的变量 选择一个答案 A. 正确 B. 错误

解析:

在 Solidity 中,indexed 关键字不能修饰任意类型的变量。最多只能对事件中的三个参数使用 indexed 修饰,并且它只适用于某些基本类型,例如 address、uint、int 和 bytes 等。复杂的结构体或数组类型无法被 indexed 修饰。所以错误。

  • 如果合约B继承了合约A,合约C要继承A和B,要怎么写?

选择一个答案

A. contract C is A, B

B. contract C is B, A

C. contract C is B

父合约在子合约之前,A为正确选项

合约B继承了合约A,两个合约都有pop()函数,下面选项中,正确调用父合约函数的是:

选择一个答案

A. A.pop();

B. super.pop();

C. 都正确

解析:

在 Solidity 中,当子合约 B 继承了父合约 A,并且两个合约中都存在同名函数 pop() 时,可以通过以下两种方式调用父合约的 pop() 函数:

· A.pop();:直接通过父合约的名称调用父合约的 pop()。

· super.pop();:使用 super 关键字调用父合约的 pop(),特别适合在多重继承的情况下调用父类函数。

因此,两种方式都可以正确调用父合约的 pop() 函数,选C

  • function a() public override{} 意思是

​ 选择一个答案

​ A. 希望子合约重写函数a()

​ B. 函数a()重写了父合约中的同名函数

解析:

在 Solidity 中,override 关键字表示该函数是对父合约中同名函数的重写。因此,function a() public override {} 的意思是 该函数 a() 重写了父合约中的同名函数。

  • 合约B继承了合约A,下面选项中,正确调用父合约构造函数的是:

​ A. constructor(uint _num) { A(_num);}

​ B. constructor(uint _num) { A.constructor(_num);}

​ C. constructor(uint _num) A(_num){}

解析:

在 Solidity 中,如果合约 B 继承了合约 A,并且需要在合约 B 的构造函数中调用合约 A 的构造函数,正确的写法是 constructor(uint _num) A(_num){}。

  • 被导入文件中的全局符号想要被其他合约单独导入,应该怎么编写?

(选择一个答案

​ A. 将合约结构包含

​ B. 包含在合约结构中

​ C. 与合约并列在文件结构中

解析

当文件中的全局符号(例如函数、结构体、枚举等)希望被其他合约单独导入时,需要将这些符号定义在合约之外,即与合约并列在文件结构中,而不是包含在特定合约的内部。

这样一来,这些符号就在文件的全局作用域中,便于其他文件或合约通过 import 语句直接导入和使用,选C。

  • Solidity中import的作用是:

    A. 导入其他合约中的接口

    B. 导入其他合约中的私有变量

    C. 导入其他合约中的全局符号

    D. 导入其他合约中的内部变量

解析

在 Solidity 中,import 关键字用于导入其他文件中定义的全局符号,如合约、库、结构体、枚举等。这使得开发者可以在当前文件中使用其他文件中的符号。

选项分析:

· A. 导入其他合约中的接口:虽然可以导入接口,但 import 并不限于接口。

· B. 导入其他合约中的私有变量:私有变量不能在其他合约中直接访问,import 不能导入私有变量。

· C. 导入其他合约中的全局符号:这是正确答案,因为 import 可以导入各种全局符号。

· D. 导入其他合约中的内部变量:import 不能直接导入内部变量(internal 变量),但可以通过继承的方式访问。

因此,正确答案是 C。

  • 以下import写法错误的是:

​ A. import from “./Yeye.sol”;

 B. import {Yeye} from "./Yeye.sol";
 
 C. import {Yeye as Wowo} from "./Yeye.sol"; 
 
 D. import * as Wowo from "./Yeye.sol";

解析

*在 Solidity 中,import 语句需要指定导入内容或者使用通配符 * 进行导入。选项 A 缺少导入的具体内容,这是错误的写法。正确的写法应当明确指定要导入的符号或使用通配符

选项分析:

· A. import from “./Yeye.sol”;:错误。未指定导入的内容,语法不完整。

· B. import {Yeye} from “./Yeye.sol”;:正确。导入了 Yeye 合约。

· C. import {Yeye as Wowo} from “./Yeye.sol”;:正确。导入并将 Yeye 别名为 Wowo。

· D. import * as Wowo from “./Yeye.sol”;:正确。导入 Yeye.sol 中所有符号,并以 Wowo 作为命名空间。

什么是命名空间?

​ 命名空间帮助开发者将相关的功能或数据组织在一起,使得代码逻辑更加清晰。

​ 通过命名空间的前缀,开发者可以快速了解某个标识符的来源和用途。

因此,A 是错误的导入写法。

  • import导入文件中的全局符号可以单独指定其中的:

    A. 合约

    B. 纯函数

    C. 结构体类型

    D. 以上都可以

解析

在 Solidity 中,import 语句可以单独指定要导入的符号,包括合约、函数、结构体等。例如:

· 合约:可以通过 import { ContractName } from “file.sol”; 来导入文件中的特定合约。

· 纯函数:如果文件中定义了 pure 或 view 的全局函数(从 Solidity 0.6.0 开始支持的功能),也可以通过 import { functionName } from “file.sol”; 来单独导入。

· 结构体类型:可以使用 import { StructName } from “file.sol”; 来单独导入结构体定义。

因此,D. 以上都可以 是正确答案。

*

2.png

解析

假设 SendETH 合约中 callETH 函数的代码如下:

1
2
3
4
5
6
7
8
9
function callETH(address payable _to) external payable {

// 假设发送 1ETH 给 ReceiveETH 合约

(bool success, ) = _to.call{value: 1 ether}("");

require(success, "Transfer failed");

}

在这种情况下,以下是执行步骤和各合约的 ETH 余额变化:

交易初始化:Vitalik 调用 SendETH 合约的 callETH 函数,没有设置 msg.value。SendETH 合约接收到 2 ETH。

转账执行:callETH 函数内部使用了 call{value: 1 ether}(“”),向 ReceiveETH 合约发送 1 ETH。因此,ReceiveETH 合约将接收到 1 ETH,SendETH 合约的余额减少 1 ETH。

最终余额

o SendETH 合约:2 ETH(初始接收) - 1 ETH(发送) = 1 ETH

o ReceiveETH 合约:接收 1 ETH

由于没有设置msg.value,执行完交易后,SendETH 合约的余额为 1 ETH,而 ReceiveETH 合约的余额为 1 ETH

如果设置了msg.value,则SendETH为0ETH,

这是因为 msg.value 是直接随交易发送到 SendETH 合约的,以支付调用 callETH 函数的资金。这笔 2 ETH 会被发送到 SendETH 合约,但 SendETH 合约并不保留这 2 ETH,而是立即在 callETH 函数中使用其中的 1 ETH 进行转账,剩余的 1 ETH 也不被 SendETH 合约保留。

以下是更详细的解释:

  1. 初始交易的 2 ETH:Vitalik 发送的 msg.value 是 2 ETH,这笔资金在调用 SendETH 合约的 callETH 函数时传入。

  2. 发送 1 ETH 给 ReceiveETH:callETH 函数中使用了 call{value: 1 ether}(“”) 向 ReceiveETH 合约转账 1 ETH。

  3. 剩余的 1 ETH:由于 callETH 函数中并没有将剩余的 1 ETH 存入 SendETH 合约的余额(例如未将 msg.value - 1 ether 显式存入),交易结束时,这部分 1 ETH 会被直接退还给调用者(Vitalik),因为 Solidity 中的函数执行完毕后未被使用的 msg.value 会被退还。

总结

因此,交易完成后:

· SendETH 合约没有余额,余额为 0 ETH

· ReceiveETH 合约收到并保留了 1 ETH

  • 下列关于智能合约调用其他智能合约的说法,正确的一项是:

    选择一个答案

    A. 智能合约调用其他智能合约这一功能,主要起到了方便代码复用的作用

    B. 在智能合约A中调用智能合约B,比起从EOA直接调用智能合约B,要更节省gas

    C. 智能合约B中可见性为internal的函数也可以被智能合约A调用

解析

· 选项 A 是正确的。智能合约调用其他智能合约确实可以方便代码复用。这样可以减少重复代码并提高合约的模块化,使开发更加灵活和高效。

· 选项 B 是不正确的。在智能合约 A 中调用智能合约 B 实际上比直接从外部账户(EOA)调用智能合约 B 消耗更多的 Gas,因为这涉及到更多的操作步骤(合约 A 需要发起外部调用),并没有节省 Gas 的效果。

· 选项 C 是不正确的。internal 可见性表示函数只能在同一个合约或继承的合约中调用,不能被其他合约直接调用。因此,合约 A 无法直接调用合约 B 中 internal 的函数。

  • 下面哪种使用方式不正确?

​ A. address(nameReg).call{gas: 1000000}(abi.encodeWithSignature(“register(string)”, “MyName”));

​ B. address(nameReg).call{value: 1 ether}(abi.encodeWithSignature(“register(string)”, “MyName”));

​ C. address.call{gas: 1000000, value: 1 ether}

​ D. address(nameReg).call{gas: 1000000, value: 1 ether}

解析

逐一分析

选项 A:address(nameReg).call{gas: 1000000}(abi.encodeWithSignature(“register(string)”, “MyName”));

o 这是正确的写法,使用了 .call{gas: …} 发送指定的 gas 量,并通过 abi.encodeWithSignature 来对函数调用参数进行编码。此调用会尝试在 nameReg 地址处调用 register(string) 函数,提供字符串 “MyName” 作为参数。

选项 B:address(nameReg).call{value: 1 ether}(abi.encodeWithSignature(“register(string)”, “MyName”));

o 这是正确的写法,使用了 .call{value: …} 发送指定的 value(即 1 ether),并且使用了 abi.encodeWithSignature 对函数和参数进行编码。

o 该调用会在发送 1 ether 的情况下,尝试调用 nameReg 地址上的 register(string) 函数。

选项 C:address.call{gas: 1000000, value: 1 ether}

o 这是不正确的写法。address.call{…} 的语法要求提供被调用的目标地址,而这里没有指定有效地址和编码的数据。正确写法应为 address(target).call{gas: …, value: …}(data)。

o 这里 address 作为数据类型使用是不对的,应该是一个具体的地址实例,比如 address(target)。

选项 D:address(nameReg).call{gas: 1000000, value: 1 ether}

o 这是正确的写法,因为 address(nameReg).call{gas: …, value: …}(data) 提供了 gas 和 value,并调用 nameReg 这个合约地址。

因此,C 是错误的使用方式,因为它缺少了有效的地址实例(目标地址)和调用的数据。

3.png

解析

1
OtherContract other = OtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138);

o 这种写法直接实例化了 OtherContract 合约。因为 OtherContract 合约实现了 IOtherContract 接口,这种写法允许我们调用 OtherContract 中的所有公共函数。

1
IOtherContract other = IOtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138);

o 这种写法使用了 IOtherContract 接口进行实例化,可以用来调用 IOtherContract 中声明的函数。只要 OtherContract 实现了 IOtherContract 接口,这种方式也是正确的。

因此,**(1) 和 (2) 均是正确的调用方式**,可以使用任意一种方法来调用合约。

4.png

解析

· 选项 A:MyContract 是 OtherContract 的子类

o 不正确。MyContract 并没有继承 OtherContract,它只是实例化了 OtherContract 并通过地址直接调用它的函数,因此不构成继承关系。

· 选项 B:MyContract 是 IOtherContract 的一个实现

o 不正确。MyContract 也没有实现 IOtherContract 接口。它只是定义了与 OtherContract 交互的函数,而不是实现 IOtherContract 中的所有接口。

· 选项 C:MyContract 需要 0xd9145CCE52D386f254917e481eB44e9943F39138 的某种许可,才可以调用其中的函数

o 不正确。只要 OtherContract 中的函数是 external 或 public 且无访问权限限制,任何合约或外部账户都可以调用它。OtherContract 中的 setX 和 getX 都没有额外的权限控制。

· 选项 D:MyContract 的函数 call_setX 可以实现,这意味着 OtherContract 中 setX 的权限没有门槛,存在安全隐患

o 正确。OtherContract 的 setX 函数是 external 并且没有权限控制,因此任何合约或账户都可以调用并修改 _x 的值。这确实可能带来安全隐患,尤其是在 _x 变量值的更改可能影响合约逻辑的情况下。如果不希望外部随意调用,应该在 setX 函数中添加访问控制。