Solidity极简入门: 19. 接收ETH receive和fallback
Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用:
- 接收ETH
- 处理合约中不存在的函数调用(代理合约proxy contract)
注意⚠️:在solidity 0.6.x版本之前,语法上只有 fallback() 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 0.6版本之后,solidity才将 fallback() 函数拆分成 receive() 和 fallback() 两个函数。
我们这一讲主要讲接收ETH的情况。
接收ETH函数 receive
receive()只用于处理接收ETH。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。
当合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300,receive()太复杂可能会触发Out of Gas报错;如果用call就可以自定义gas执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。
我们可以在receive()里发送一个event,例如:
1 | // 定义事件 |
有些恶意合约,会在receive() 函数(老版本的话,就是 fallback() 函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。
回退函数 fallback
fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }。
我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:
1 | // fallback |
receive和fallback的区别
receive和fallback都能够用于接收ETH,他们触发的规则如下:
1 | 触发fallback() 还是 receive()? |
简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。
receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。
1 | // SPDX-License-Identifier: MIT |
==就算value等于0,我填上calldata,他也会触发fallback(),也就是说想要有msg.data(calldata)就必须有fallback()函数==
Remix 演示
首先在 Remix 上部署合约 “Fallback.sol”。
“VALUE” 栏中填入要发送给合约的金额(单位是 Wei),然后点击 “Transact”。

可以看到交易成功,并且触发了 “receivedCalled” 事件。

“VALUE” 栏中填入要发送给合约的金额(单位是 Wei),”CALLDATA” 栏中填入随意编写的
msg.data,然后点击 “Transact”。
可以看到交易成功,并且触发了 “fallbackCalled” 事件。

总结
这一讲,我介绍了Solidity中的两种特殊函数,receive()和fallback(),他们主要在两种情况下被使用,他们主要用于处理接收ETH和代理合约proxy contract。
Solidity极简入门: 20. 发送ETH
Solidity有三种方法向其他合约发送ETH,他们是:transfer(),send()和call(),其中call()是被鼓励的用法。
接收ETH合约
我们先部署一个接收ETH合约ReceiveETH。ReceiveETH合约里有一个事件Log,记录收到的ETH数量和gas剩余。还有两个函数,一个是receive()函数,收到ETH被触发,并发送Log事件;另一个是查询合约ETH余额的getBalance()函数。
1 | contract ReceiveETH { |
部署ReceiveETH合约后,运行getBalance()函数,可以看到当前合约的ETH余额为0。
==gasleft()是什么

发送ETH合约
我们将实现三种方法向ReceiveETH合约发送ETH。首先,先在发送ETH合约SendETH中实现payable的构造函数和receive(),让我们能够在部署时和部署后向合约转账。
1 | contract SendETH { |
transfer
- 用法是
接收方地址.transfer(发送ETH数额)。 transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。transfer()如果转账失败,会自动revert(回滚交易)。
代码样例,注意里面的_to填ReceiveETH合约的地址,amount是ETH转账金额:
1 | // 用transfer()发送ETH |
部署SendETH合约后,对ReceiveETH合约发送ETH,此时amount为10,value为0,amount>value,转账失败,发生revert。

此时amount为10,value为10,amount<=value,转账成功。

在ReceiveETH合约中,运行getBalance()函数,可以看到当前合约的ETH余额为10。
send
- 用法是
接收方地址.send(发送ETH数额)。 send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。send()如果转账失败,不会revert。send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。
代码样例:
1 | // send()发送ETH |
对ReceiveETH合约发送ETH,此时amount为10,value为0,amount>value,转账失败,因为经过处理,所以发生revert。

此时amount为10,value为11,amount<=value,转账成功。

call
- 用法是
接收方地址.call{value: 发送ETH数额}("")。 call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。call()如果转账失败,不会revert。call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下。
代码样例:
1 | // call()发送ETH |
对ReceiveETH合约发送ETH,此时amount为10,value为0,amount>value,转账失败,因为经过处理,所以发生revert。

此时amount为10,value为11,amount<=value,转账成功。

运行三种方法,可以看到,他们都可以成功地向ReceiveETH合约发送ETH。
总结
这一讲,我们介绍solidity三种发送ETH的方法:transfer,send和call。
call没有gas限制,最为灵活,是最提倡的方法;transfer有2300 gas限制,但是发送失败会自动revert交易,是次优选择;send有2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。
1 | // SPDX-License-Identifier: MIT |
1 | // SPDX-License-Identifier: MIT |
Solidity极简入门: 21. 调用其他合约
调用已部署合约
开发者写智能合约来调用其他合约,这让以太坊网络上的程序可以复用,从而建立繁荣的生态。很多web3项目依赖于调用其他合约,比如收益农场(yield farming)。这一讲,我们介绍如何在已知合约代码(或接口)和地址情况下调用目标合约的函数。
目标合约
我们先写一个简单的合约OtherContract来调用。
1 | contract OtherContract { |
这个合约包含一个状态变量_x,一个事件Log在收到ETH时触发,三个函数:
getBalance(): 返回合约ETH余额。setX():external payable函数,可以设置_x的值,并向合约发送ETH。getX(): 读取_x的值。
调用OtherContract合约
我们可以利用合约的地址和合约代码(或接口)来创建合约的引用:_Name(_Address),其中_Name是合约名,_Address是合约地址。然后用合约的引用来调用它的函数:_Name(_Address).f(),其中f()是要调用的函数。
下面我们介绍4个调用合约的例子,在remix中编译合约后,分别部署OtherContract和CallContract:



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。下面例子实现了调用目标合约的getX()函数。
注意该函数参数OtherContract _Address底层类型仍然是address,生成的ABI中、调用callGetX时传入的参数都是address类型
1 | function callGetX(OtherContract _Address) external view returns(uint x){ |
复制OtherContract合约的地址,填入callGetX函数的参数中,调用后成功获取x的值

3. 创建合约变量
我们可以创建合约变量,然后通过它来调用目标函数。下面例子,我们给变量oc存储了OtherContract合约的引用:
1 | function callGetX2(address _Address) external view returns(uint x){ |
复制OtherContract合约的地址,填入callGetX2函数的参数中,调用后成功获取x的值

4. 调用合约并发送ETH
如果目标合约的函数是payable的,那么我们可以通过调用它来给合约转账:_Name(_Address).f{value: _Value}(),其中_Name是合约名,_Address是合约地址,f是目标函数名,_Value是要转的ETH数额(以wei为单位)。
OtherContract合约的setX函数是payable的,在下面这个例子中我们通过调用setX来往目标合约转账。
1 | function setXTransferETH(address otherContract, uint256 x) payable external{ |
复制OtherContract合约的地址,填入setXTransferETH函数的参数中,并转入10ETH

转账后,我们可以通过Log事件和getBalance()函数观察目标合约ETH余额的变化。

总结
这一讲,我们介绍了如何通过目标合约代码(或接口)和地址来创建合约的引用,从而调用目标合约的函数。
拓展知识:插槽冲突
在计算机科学和软件开发中,“插槽冲突”这个术语可以有几个不同的含义,具体取决于其应用的上下文。以下是几个最常见的场景:
1. 哈希表中的插槽冲突
在哈希表(或哈希映射)的实现中,插槽冲突(也称为哈希冲突)发生在两个不同的键通过哈希函数计算出相同的哈希值或索引时。当这种情况发生时,哈希表需要有一种机制来解决冲突,以确保所有的键都能被正确地存储和检索。常见的冲突解决技术包括:
- 开放寻址:如果一个插槽已被占用,哈希表将寻找下一个空闲的插槽。
- 链表法:每个插槽存储一个链表,所有具有相同哈希值的元素都会被添加到这个链表中。
2. 计算机网络中的插槽冲突
在计算机网络特别是使用时间分割多路访问(TDMA)协议的无线通信系统中,插槽冲突可能发生在多个设备尝试同时使用同一个时间插槽来传输数据时。这种冲突需要通过协调机制来避免或解决,例如使用冲突检测和冲突解决算法。
3. Solidity智能合约中的存储插槽冲突
在Solidity智能合约开发中,插槽冲突是一个特定于合约存储优化的问题。在Ethereum的智能合约中,存储是非常昂贵的资源,每个变量都被分配到一个称为“插槽”的位置。智能合约的变量如果不正确地排序,可能会导致不必要的存储空间浪费。在某些情况下,不同变量或继承的合约之间的不恰当交互也可能导致存储覆盖,这是一种严重的安全问题。
在设计和实现数据结构、通信协议或智能合约时,了解如何处理各种类型的插槽冲突是非常重要的,以确保系统的效率和安全性。如果你有一个特定的上下文或应用场景关于插槽冲突,提供更多的细节可以帮助我给出更具体的解释或解决方案。