重载
Solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,Solidity不允许修饰器(modifier)重载。
测试代码Overloading.sol,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pragma solidity ^0.8.21;contract Overloading { function saySomething ( ) public pure returns (string memory ) { return ("Nothing" ); } function saySomething (string memory something ) public pure returns (string memory ) { return (something); } function f (uint8 _in ) public pure returns (uint8 out ) { out = _in; } function f (uint256 _in ) public pure returns (uint256 out ) { out = _in; } }
函数重载
我们可以定义两个都叫saySomething()的函数,一个没有任何参数,输出"Nothing";另一个接收一个string参数,输出这个string。
1 2 3 4 5 6 function saySomething ( ) public pure returns (string memory ) { return ("Nothing" ); } function saySomething (string memory something ) public pure returns (string memory ) { return (something); }
最终重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)。
以 Overloading.sol 合约为例,在 Remix 上编译部署后,分别调用重载函数 saySomething() 和 saySomething(string memory something),可以看到他们返回了不同的结果,被区分为不同的函数。
参数匹配(Argument Matching)
在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256:
1 2 3 4 5 6 function f (uint8 _in ) public pure returns (uint8 out ) { out = _in; } function f (uint256 _in ) public pure returns (uint256 out ) { out = _in; }
我们调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。
库合约
这一讲,我们用ERC721的引用的库合约Strings为例介绍Solidity中的库合约(Library),并总结了常用的库合约。
完整的测试代码Library.sol如下:
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 pragma solidity ^0.8.21;library Strings { bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef" ; function toString (uint256 value ) public pure returns (string memory ) { if (value = = 0 ) { return "0" ; } uint256 temp = value; uint256 digits; while (temp ! = 0 ) { digits+ + ; temp / = 10 ; } bytes memory buffer = new bytes (digits); while (value ! = 0 ) { digits - = 1 ; buffer[digits] = bytes1 (uint8 (48 + uint256 (value % 10 ))); value / = 10 ; } return string (buffer); } function toHexString (uint256 value ) public pure returns (string memory ) { if (value = = 0 ) { return "0x00" ; } uint256 temp = value; uint256 length = 0 ; while (temp ! = 0 ) { length+ + ; temp > > = 8 ; } return toHexString(value, length); } function toHexString (uint256 value, uint256 length ) public pure returns (string memory ) { bytes memory buffer = new bytes (2 * length + 2 ); buffer[0 ] = "0" ; buffer[1 ] = "x" ; for (uint256 i = 2 * length + 1 ; i > 1 ; - - i) { buffer[i] = _HEX_SYMBOLS[value & 0xf ]; value > > = 4 ; } require (value = = 0 , "Strings: hex length insufficient" ); return string (buffer); } } contract UseLibrary { using Strings for uint256 ; function getString1 (uint256 _number ) public pure returns (string memory ) { return _number.toHexString(); } function getString2 (uint256 _number ) public pure returns (string memory ) { return Strings.toHexString(_number); } }
库合约
库合约是一种特殊的合约,为了提升Solidity代码的复用性和减少gas而存在。库合约是一系列的函数合集,由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。
他和普通合约主要有以下几点不同:
不能存在状态变量
不能够继承或被继承
不能接收以太币
不可以被销毁
需要注意的是,库合约中的函数可见性如果被设置为public或者external,则在调用函数时会触发一次delegatecall。而如果被设置为internal,则不会引起。对于设置为private可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。
Strings库合约
Strings库合约是将uint256类型转换为相应的string类型的代码库,样例代码如下:
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 53 54 55 56 57 58 59 library Strings { bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef" ; function toString (uint256 value ) public pure returns (string memory ) { if (value = = 0 ) { return "0" ; } uint256 temp = value; uint256 digits; while (temp ! = 0 ) { digits+ + ; temp / = 10 ; } bytes memory buffer = new bytes (digits); while (value ! = 0 ) { digits - = 1 ; buffer[digits] = bytes1 (uint8 (48 + uint256 (value % 10 ))); value / = 10 ; } return string (buffer); } function toHexString (uint256 value ) public pure returns (string memory ) { if (value = = 0 ) { return "0x00" ; } uint256 temp = value; uint256 length = 0 ; while (temp ! = 0 ) { length+ + ; temp > > = 8 ; } return toHexString(value, length); } function toHexString (uint256 value, uint256 length ) public pure returns (string memory ) { bytes memory buffer = new bytes (2 * length + 2 ); buffer[0 ] = "0" ; buffer[1 ] = "x" ; for (uint256 i = 2 * length + 1 ; i > 1 ; - - i) { buffer[i] = _HEX_SYMBOLS[value & 0xf ]; value > > = 4 ; } require (value = = 0 , "Strings: hex length insufficient" ); return string (buffer); } }
它主要包含两个函数,toString()将uint256转换为10进制的string,toHexString()将uint256转换为16进制的string。
如何使用库合约?
我们用Strings库合约的toHexString()来演示两种使用库合约中函数的办法。
利用using for指令,指令using A for B;可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数:
1 2 3 4 5 6 using Strings for uint256 ;function getString1 (uint256 _number ) public pure returns (string memory ) { return _number.toHexString(); }
1 2 3 4 function getString2 (uint256 _number ) public pure returns (string memory ) { return Strings.toHexString(_number); }
我们部署合约并输入130测试一下,两种方法均能返回正确的16进制string 0x82。证明我们调用库合约成功!
常见的库合约有:
Strings :将uint256转换为String
Address :判断某个地址是否为合约地址
Create2 :更安全的使用Create2 EVM opcode
Arrays :跟数组相关的库合约
Import
在Solidity中,import语句可以帮助我们在一个文件中引用另一个文件的内容,提高代码的可重用性和组织性。本教程将向你介绍如何在Solidity中使用import语句。
Import用法
1 2 3 4 5 6 文件结构 ├── Import.sol └── Yeye.sol // 通过文件相对位置import import './Yeye.sol';
1 2 import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol' ;
1 import '@openzeppelin/contracts/access/Ownable.sol' ;
1 import {Yeye } from './Yeye.sol' ;
引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。
测试导入结果
我们可以用下面这段代码测试是否成功导入了外部源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 pragma solidity ^0.8.21;import './Yeye.sol' ;import {Yeye } from './Yeye.sol' ;import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol' ;import '@openzeppelin/contracts/access/Ownable.sol' ;contract Import { using Address for address ; Yeye yeye = new Yeye(); function test ( ) external { yeye.hip(); } }
Yeye.sol代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pragma solidity ^0.8.21;contract Yeye { event Log (string msg ) ; function hip ( ) public virtual { emit Log("Yeye hip" ); } function pop ( ) public virtual { emit Log("Yeye pop" ); } function yeye ( ) public virtual { emit Log("Yeye" ); } }
测试结果如下:
接收和发送ETH
本讲主要介绍ETH的接收和发送。
接收ETH
Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用:
接收ETH
处理合约中不存在的函数调用(代理合约proxy contract)
注意⚠️ :在Solidity 0.6.x版本之前,语法上只有 fallback() 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 0.6版本之后,Solidit才将 fallback() 函数拆分成 receive() 和 fallback() 两个函数。
接收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 2 3 4 5 6 event Received (address Sender, uint Value ) ;receive ( ) external payable { emit Received(msg .sender , msg .value ); }
有些恶意合约,会在receive() 函数(老版本的话,就是 fallback() 函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。
回退ETH函数fallback
fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。
fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰 ,用于接收ETH:fallback() external payable { ... }。
我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:
1 2 3 4 5 6 event fallbackCalled (address Sender, uint Value, bytes Data ) ;fallback ( ) external payable { emit fallbackCalled(msg .sender , msg .value , msg .data ); }
receive和fallback的区别
receive和fallback都能够用于接收ETH,他们触发的规则如下:
1 2 3 4 5 6 7 8 9 10 11 12 触发fallback() 还是 receive()? 接收ETH | msg.data是空? / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback()
简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。
receive()和payable fallback()均不存在的时候,向合约直接 发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。
测试代码演示
完整的测试代码Fallback.sol,如下:
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 pragma solidity ^0.8.21;contract Fallback { event receivedCalled (address Sender, uint Value ) ; event fallbackCalled (address Sender, uint Value, bytes Data ) ; receive ( ) external payable { emit receivedCalled(msg .sender , msg .value ); } fallback ( ) external payable { emit fallbackCalled(msg .sender , msg .value , msg .data ); } }
部署并执行代码,VALUE栏中输入要发送到合约中的金额,CALLDATA留空,点击Transact,成功触发到receivedCalled事件。
同样,VALUE栏中输入要发送到合约中的金额,CALLDATA输入任意编写的0xabcd,点击Transact,成功触发到fallbackCalled事件。
发送ETH
Solidity有三种方法向其他合约发送ETH,他们是:transfer(),send()和call(),其中call()是被鼓励的用法。
完整的测试代码SendETH.sol如下:
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 53 54 55 pragma solidity ^0.8.21;error SendFailed ( ) ; error CallFailed ( ) ; contract SendETH { constructor ( ) payable {} receive ( ) external payable {} function transferETH (address payable _to, uint256 amount ) external payable { _to.transfer (amount); } function sendETH (address payable _to, uint256 amount ) external payable { bool success = _to.send (amount); if (! success){ revert SendFailed(); } } function callETH (address payable _to, uint256 amount ) external payable { (bool success,) = _to.call {value : amount}("" ); if (! success){ revert CallFailed(); } } } contract ReceiveETH { event Log (uint amount, uint gas ) ; receive ( ) external payable { emit Log(msg .value , gasleft ()); } function getBalance ( ) view public returns (uint ) { return address (this ).balance ; } }
接收ETH合约
我们先部署一个接收ETH合约ReceiveETH。ReceiveETH合约里有一个事件Log,记录收到的ETH数量和gas剩余。还有两个函数,一个是receive()函数,收到ETH被触发,并发送Log事件;另一个是查询合约ETH余额的getBalance()函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 contract ReceiveETH { event Log (uint amount, uint gas ) ; receive ( ) external payable { emit Log(msg .value , gasleft ()); } function getBalance ( ) view public returns (uint ) { return address (this ).balance ; } }
部署ReceiveETH合约后,运行getBalance()函数,可以看到当前合约的ETH余额为0。
发送ETH合约
我们将实现三种方法向ReceiveETH合约发送ETH。首先,先在发送ETH合约SendETH中实现payable的构造函数和receive(),让我们能够在部署时和部署后向合约转账。
1 2 3 4 5 6 contract SendETH { constructor ( ) payable {} receive ( ) external payable {} }
Transfer用法(次优)
用法是接收方地址.transfer(发送ETH数额)。
transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。
transfer()如果转账失败,会自动revert(回滚交易)。
代码样例,注意里面的_to填ReceiveETH合约的地址 ,amount是ETH转账金额:
1 2 3 4 function transferETH (address payable _to, uint256 amount ) external payable { _to.transfer (amount); }
部署SendETH合约后,对ReceiveETH合约发送ETH。
ReceiveETH合约成功接收到转账的ETH。
amount>value,转账失败,发生revert。
Send用法
用法是接收方地址.send(发送ETH数额)。
send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。
send()如果转账失败,不会revert。
send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。
代码样例:
1 2 3 4 5 6 7 8 9 10 error SendFailed ( ) ; function sendETH (address payable _to, uint256 amount ) external payable { bool success = _to.send (amount); if (! success){ revert SendFailed(); } }
amount>value,转账失败,发生revert。
Call用法(提倡使用)
用法是接收方地址.call{value: 发送ETH数额}("")。
call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。
call()如果转账失败,不会revert。
call()的返回值是(bool, bytes),其中bool代表着转账成功或失败,需要额外代码处理一下。
代码样例:
1 2 3 4 5 6 7 8 9 10 error CallFailed ( ) ; function callETH (address payable _to, uint256 amount ) external payable { (bool success,) = _to.call {value : amount}("" ); if (! success){ revert CallFailed(); } }
部署SendETH合约后,对ReceiveETH合约发送ETH。
ReceiveETH合约成功接收到转账的ETH。
amount>value,转账失败,发生revert。
总结
这一讲,我们介绍Solidity三种发送ETH的方法:transfer,send和call。
**call没有gas限制,最为灵活,是最提倡的方法!!!**⚠️⚠️⚠️
transfer有2300 gas限制,但是发送失败会自动revert交易,是次优选择;
send有2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。
参考
https://www.wtf.academy/zh/course/solidity102/Fallback
https://www.wtf.academy/zh/course/solidity102/SendETH