solidity基础002
关键词:constructor&Modifier event 继承 接口 异常 重载 库合约 引用 回调
构造函数constructor和修饰器Modifier
constructor
定义:是一种特殊函数 每个合约可以定义一个,并且在部署合约时自动运行一次。
可用于初始化合约参数:
1 | // SPDX-License-Identifier: MIT |
Modifier
定义:类似于decorator,声明函数拥有的特性,并减少代码冗余
主要使用场景: 运行函数前的检查,例如地址,变量,余额等。
定义一个叫做onlyOwner的modifier:
1 | //定义modifier |
带有onlyOwner修饰符的函数只能被owner地址调用
eg:
1 | //改变owner |
在以上函数中,由于onlyOwner修饰符的存在,只有原先的owner可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。
事件event
- 响应: 应用程序(ether.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。
- 经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas。
作用
- 日志记录:事件在链上作为日志记录保存,不能被智能合约读取,但可供外部观察。
- 通知机制:前端应用、DApp 等可以监听事件来响应合约的变化,如更新用户余额、确认交易等。
- 优化 Gas 消耗:事件的存储成本低于状态变量的修改,因此在某些应用场景下,使用事件记录是更高效的选择。
声明事件
1 | Event + 事件名称 + (变量类型 变量名,变量类型 变量名,...) |
以REC20代币合约的Transfer事件为例:
1 | event Transfer(address indexed from,address indexed to,uint256 value); |
释放事件(Emit Event)是指在区块链上发布特定事件通知的操作。
通过释放事件,合约可以在发生某些操作(如状态改变、资金转移等)时,向链上日志系统发送记录。
事件通常用于通知外部应用程序,如前端应用或监听工具,便于监控合约状态的变化
关键字
1 | Emit |
eg:
1 | //定义_transfer函数,执行转账逻辑 |
EVM用日志log来存储solidity事件,每条日志记录包括主题topics和数据data两部分
5.1 主题topics(?)
用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)
eg: 例如对于上面的transfer事件,它的事件哈希:
1 | keccak256("Transfer(address,address,unit256)") |
除了事件哈希,主题还可以包含至多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 virtual
父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
1.2 override:
子合约重写了父合约中的函数,需要加上override关键字。
注意:用override修饰public变量,会重写与变量同名的getter函数
先写一个简单的A合约
再定义一个B合约,让他继承A合约
1 | contract B is A |
规则:
继承时要按辈分最高到最低的顺序排。
eg:比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成
1
contract Erzi is Yeye, Baba
而不能写成contract Erzi is Baba, Yeye,不然就会报错。
如果某一个函数在多个继承的合约里都存在,在子合约里必须重写,不然会报错
重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字
1 | override(Yeye, Baba) |
4. 修饰器的继承
用法与函数继承类似,在相应的地方加virtual和override关键字即可。
5. 构造函数的继承
子合约有两种方法继承父合约的构造函数:
1. **在继承时声明父构造函数的参数**
eg:
1 | contract B is A(1) |
- 在子合约的构造函数中声明构造函数的参数
eg:
1 | contract C is A { |
子合约有两种方式调用父合约的函数:
- 直接调用
子合约直接用父合约名.函数名()的方式来调用父合约函数
eg: Yeye.pop()
1 | function callParent() public{ |
- Super关键字
子合约可以利用**super.函数名()**来调用最近的父合约函数。
eg:
当Solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop()
1 | function callParentSuper() public{ |
指一个派生类同时有两个或两个以上的基类。
1 | /* |
1 | contract Yeye{ |
(当然现实辈分关系具体不是这样,只是代指三个层次)
所谓钻石,即虽然Baba,Mama都是Yeye的子合约,但整个过程中,God合约只会被调用一次
(因为solidity强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序)
抽象合约
如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体**{}中的内容,则必须将该合约标为abstract**,不然编译会报错。
未实现的函数需要加virtual,以便子合约重写。拿插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上。
1 | constract Sort{ |
接口(interface)
规则:
1. 接口不能包含状态变量
1. 不能包含构造函数
1. 不能继承除接口外的其他合约
1. 所有函数都必须是external且不能有函数体
1. 继承接口的非抽象合约必须实现接口定义的所有功能
接口提供了两个重要的信息:
- 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)。
- 接口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而存在
他和普通合约主要有以下几点不同:
- 不能存在状态变量
- 不能够继承或被继承
- 不能接收以太币
- 不可以被销毁
库合约中的函数可见性如果被设置为public或者external,则在调用函数时会触发一次delegatecall。而如果被设置为internal,则不会引起。对于设置为private可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。
delegatecall:
delegatecall 是一种特殊的低级函数调用,用于将当前合约的上下文(包括msg.sender和msg.value等)传递给另一个合约的函数执行。delegatecall 允许合约在不改变调用者上下文的情况下执行另一个合约的代码。
delegatecall 的作用
- 共享存储:
delegatecall是调用另一个合约的代码,并在调用者合约的存储上下文中执行。这意味着被调用合约的代码会对调用合约的存储变量进行读写。 - 保持调用者上下文:
msg.sender和msg.value等上下文信息保持不变,依旧指向调用者,这和普通的合约调用不同。 - 代码重用:使用
delegatecall可以使多个合约共享同一段逻辑代码,通过代理模式实现合约的代码复用。
Strings库合约
Strings库合约是将uint256类型转换为相应的string类型的代码库
如何利用:
用using for 指令
1 | using A for B |
用于附加库合约(从库A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。
注意: 在调用的时候,这个变量会被当作第一个参数传递给函数
直接通过库合约名称调用函数
1 | //利用using for指令 |
常用库合约:
Strings:将**uint256**转换为**String**
Address:判断某个地址是否为合约地址
Create2:更安全的使用**Create2 EVM opcode**
Arrays:跟数组相关的库合约
引用Import
引用(import)在代码中的位置为: 在声明版本号之后,在其余代码之前
import语句可以帮助我们在一个文件中引用另一个文件的内容
用法
- 通过源文件相对位置导入
1 | //通过文件相对位置import |
- 通过源文件网址导入网上的合约的全局符号
1 | //通过网址引用 |
通过npm的目录导入
通过指定全局符号导入合约特定的全局符号
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 { ... } |
二者区别
合约接收ETH时,msg.data为空且存在Receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable
receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错
(你仍可以通过带有payable的函数向合约发送ETH)
在这个场景中,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
transfer()
send()
call(),其中call()是被鼓励的用法。
首先构造发送ETH合约SendETH,并在其中实现payable的构造函数和receive()
1 | contract SendETH{ |
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 | //用transfer()发送ETH,_to填reveive合约地址,amount填ETH转账金额 |
2. send
用法
1 | 接收方地址.send(发送ETH数额) |
· send()的gas限制是2300,足够用于转账,但对方合约的**fallback()或receive()**函数不能实现太复杂的逻辑。
· send()如果转账失败,不会revert。
· send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下
1 | error SendFailed(); // 用send发送ETH失败error |
3.Call
用法
1 | 接收方地址.call{value: 发送ETH数额}("") |
· call()没有gas限制,可以支持对方合约**fallback()或receive()**函数实现复杂逻辑。
· call()如果转账失败,不会revert。
· call()的返回值是(bool, bytes),其中bool代表着转账成功或失败,需要额外代码处理一下
1 | error CallFAiled()// 用call发送ETH失败error |
· call没有gas限制,最为灵活,是最提倡的方法;
· transfer有2300 gas限制,但是发送失败会自动revert交易,是次优选择;
· send有2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。
调用其他合约
如何调用
可以利用合约的地址和合约代码(或接口)来创建合约的引用:_Name(_Address),其中_Name是合约名,应与合约代码(或接口)中标注的合约名保持一致,_Address是合约地址。然后用合约的引用来调用它的函数:_Name(_Address).f(),其中f()是要调用的函数。
1.传入合约地址
我们可以在函数里传入目标合约地址,生成目标合约的引用,然后调用目标函数。
以调用OtherContract合约的setX函数为例,我们在新合约中写一个callSetX函数,
传入已部署好的OtherContract合约地址_Address和setX的参数x:
1 | function callSetX(address _address,uint256 x)external{ |
复制OtherContract合约的地址,填入callSetX函数的参数中,成功调用后,调用OtherContract合约中的getX验证x变为123
2.传入合约变量
我们可以直接在函数里传入合约的引用,只需要把上面参数的address类型改为目标合约名
比如上述的OtherContract
(ps: 该函数参数OtherContract _Address底层类型仍然是address,生成的ABI中、调用callGetX时传入的参数都是address类型)
例子:
通过传入合约变量调用目标合约的函数
1 | function callGetX(OtherContract _Address) external view returns(uint x){ |
复制OtherContract合约的地址,填入callGetX函数的参数中,调用后成功获取x的值
3. 创建合约变量
1 | function callGetX(address _Address) external view returns(uint x){ |
复制OtherContract合约的地址,填入callGetX2函数的参数中,调用后成功获取x的值
4.调用合约并发送ETH
如果目标合约的函数是payable的,那么我们可以通过调用它来给合约转账:
1 | _Name(_Address).f{value: _Value}() |
其中**_Name是合约名,_Address是合约地址,f是目标函数名,_Value是要转的ETH数额(以wei**为单位)。
OtherContract合约的setX函数是payable的,在下面这个例子中我们通过调用setX来往目标合约转账。
1 | function setXTransferETH(address otherContract, uint256 x) payable external{ |
利用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调用合约举例
- Response事件
1 | // 定义Response事件,输出call返回的结果success和data,方便观察返回值 |
调用setX函数
定义callSetX函数来调用目标合约的setX(),转入msg.value数额的ETH,并释放Response事件输出success和data:
1
2
3
4
5
6
7
8
9function 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,也就是空。
调用getX函数
所以我们还需要调用getX()函数用于返回目标合约X(uint256)的值
1 | function callGetX(address _addr) external returns(uint256){ |
从Response事件的输出,我们可以看到data为0x0000000000000000000000000000000000000000000000000000000000000005。而经过abi.decode,最终返回值为5。
- 调用不存在的函数
如果给call输入的函数不存在于目标合约,那么目标合约的fallback函数会被触发。
1 | function callNonExist(address _addr)external{ |
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. 以上都可以 是正确答案。
*

解析
假设 SendETH 合约中 callETH 函数的代码如下:
1 | function callETH(address payable _to) external payable { |
在这种情况下,以下是执行步骤和各合约的 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 合约保留。
以下是更详细的解释:
初始交易的 2 ETH:Vitalik 发送的 msg.value 是 2 ETH,这笔资金在调用 SendETH 合约的 callETH 函数时传入。
发送 1 ETH 给 ReceiveETH:callETH 函数中使用了 call{value: 1 ether}(“”) 向 ReceiveETH 合约转账 1 ETH。
剩余的 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 是错误的使用方式,因为它缺少了有效的地址实例(目标地址)和调用的数据。

解析
1 | OtherContract other = OtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138); |
o 这种写法直接实例化了 OtherContract 合约。因为 OtherContract 合约实现了 IOtherContract 接口,这种写法允许我们调用 OtherContract 中的所有公共函数。
1 | IOtherContract other = IOtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138); |
o 这种写法使用了 IOtherContract 接口进行实例化,可以用来调用 IOtherContract 中声明的函数。只要 OtherContract 实现了 IOtherContract 接口,这种方式也是正确的。
因此,**(1) 和 (2) 均是正确的调用方式**,可以使用任意一种方法来调用合约。

解析
· 选项 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 函数中添加访问控制。


