重载
Solidity
中允许函数进行重载(overloading
),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,Solidity
不允许修饰器(modifier
)重载。
测试代码Overloading.sol
,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // SPDX-License-Identifier: MIT 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; library Strings { bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ function toString(uint256 value) public pure returns (string memory) { // Inspired by OraclizeAPI's implementation - MIT licence // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol 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); } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. */ 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); } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed 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 for操作使用库 using Strings for uint256; function getString1(uint256 _number) public pure returns(string memory){ // 库函数会自动添加为uint256型变量的成员 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"; /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ function toString(uint256 value) public pure returns (string memory) { // Inspired by OraclizeAPI's implementation - MIT licence // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol 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); } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. */ 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); } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed 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 for指令 using Strings for uint256; function getString1(uint256 _number) public pure returns(string memory){ // 库合约中的函数会自动添加为uint256型变量的成员 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; // 通过文件相对位置import import './Yeye.sol'; // 通过`全局符号`导入特定的合约 import {Yeye} from './Yeye.sol'; // 通过网址引用 import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; // 引用OpenZeppelin合约 import '@openzeppelin/contracts/access/Ownable.sol'; contract Import { // 成功导入Address库 using Address for address; // 声明yeye变量 Yeye yeye = new Yeye(); // 测试是否能调用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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; // 第10讲合约继承中的Yeye合约 contract Yeye { event Log(string msg); // 定义3个function: hip(), pop(), yeye(),Log值为Yeye。 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); // 接收ETH时释放Received事件 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 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; contract Fallback { /* 触发fallback() 还是 receive()? 接收ETH | msg.data是空? / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback */ // 定义事件 event receivedCalled(address Sender, uint Value); event fallbackCalled(address Sender, uint Value, bytes Data); // 接收ETH时释放Received事件 receive() external payable { emit receivedCalled(msg.sender, msg.value); } // fallback 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; // 3种方法发送ETH // transfer: 2300 gas, revert // send: 2300 gas, return bool // call: all gas, return (bool, data) error SendFailed(); // 用send发送ETH失败error error CallFailed(); // 用call发送ETH失败error contract SendETH { // 构造函数,payable使得部署的时候可以转eth进去 constructor() payable{} // receive方法,接收eth时被触发 receive() external payable{} // 用transfer()发送ETH function transferETH(address payable _to, uint256 amount) external payable{ _to.transfer(amount); } // send()发送ETH function sendETH(address payable _to, uint256 amount) external payable{ // 处理下send的返回值,如果失败,revert交易并发送error bool success = _to.send(amount); if(!success){ revert SendFailed(); } } // call()发送ETH function callETH(address payable _to, uint256 amount) external payable{ // 处理下call的返回值,如果失败,revert交易并发送error (bool success,) = _to.call{value: amount}(""); if(!success){ revert CallFailed(); } } } contract ReceiveETH { // 收到eth事件,记录amount和gas event Log(uint amount, uint gas); // receive方法,接收eth时被触发 receive() external payable{ emit Log(msg.value, gasleft()); } // 返回合约ETH余额 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 { // 收到eth事件,记录amount和gas event Log(uint amount, uint gas); // receive方法,接收eth时被触发 receive() external payable{ emit Log(msg.value, gasleft()); } // 返回合约ETH余额 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 { // 构造函数,payable使得部署的时候可以转eth进去 constructor() payable{} // receive方法,接收eth时被触发 receive() external payable{} }
Transfer用法(次优)
用法是接收方地址.transfer(发送ETH数额)
。
transfer()
的gas
限制是2300
,足够用于转账,但对方合约的fallback()
或receive()
函数不能实现太复杂的逻辑。
transfer()
如果转账失败,会自动revert
(回滚交易)。
代码样例,注意里面的_to
填ReceiveETH
合约的地址 ,amount
是ETH
转账金额:
1 2 3 4 // 用transfer()发送ETH 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(); // 用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(); } }
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(); // 用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(); } }
部署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