16.函式過載
16_01.過載
函式過載(
overloading
):即函式名字相同,但輸入的引數型別不同的函式可以同時存在;(被視為是不同的函式)
Solidity
不允許修飾器modifier
過載;過載的函式經過編譯之後,由於不同的引數型別,都變成了不同的函式選擇器(
selector
,29節有介紹);
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract A{
// 無傳入引數,輸出"No parameter"
function saySomething() public pure returns (string memory) {
return "No parameter";
}
// 傳入string,輸出string
function saySomething(string memory str) public pure returns (string memory){
return str;
}
}
16_02.實參匹配
呼叫過載函式時,會把輸入的實際資料和函式引數的型別進行匹配,若出現多個匹配的過載函式,會報錯;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract A{
uint256 data = 0;
// 傳入引數是uint8
function Add(uint8 num) public {
data += num;
}
// 傳入引數是uint256
function Add(uint256 num) public {
data += num;
}
function callAdd() public pure returns (string memory){
// 50即可用是uint8,也可以是uint256
// 因此編譯會報錯
Add(50);
return "call Add function sucess";
}
}
單獨將Add
兩個函式編譯是不會報錯的:
但是呼叫它們其中一個的時候,編譯會報錯:
17.庫合約
同其他語言裡面的庫函式,在Solidity
中還有個重要作用就是能夠減少gas
;
和普通合約的區別:
- 不能有狀態變數
- 不能夠繼承或被繼承
- 不能接收以太幣
- 不可以被銷燬
庫合約中的函式若被設定為public
或external
,則在呼叫函式時會觸發一次delegatecall
;
若被設定為internal
,則不會觸發;
若被設定為private
,由於是私人的,只能庫合約內部自己訪問;
常用的一些庫合約:
- Strings:將
uint256
轉為string
;- Address:判斷某個地址是否為合約地址;
- Create2:更安全的使用
Create2 EVM opcode
;- Arrays:跟陣列相關的庫合約;
17_01.Strings庫合約
此庫合約是將uint256
型別轉換為相應的string
型別的程式碼庫,樣例程式碼:
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
轉換為string
;toHexString()
:將uint256
轉換為hex
,再轉換為string
;
17_02.使用庫合約
有兩種使用的方式;
- 使用
using A for B
;
為型別B
新增庫合約A
;新增完後,B
型別變數的成員便自動新增了庫A
中的函式,可以直接呼叫;
呼叫時,這個變數會被當作第一個引數傳遞給函式;
- 透過庫合約名稱來直接呼叫函式;
比如:Strings.toString(xxx);
;
示例:
contract A{
// 使用using A for B
using Strings for uint256;
function getString_1(uint256 num) public pure returns (string memory){
return num.toString();
}
// 透過庫合約名來呼叫
function getString_2(uint256 num) public pure returns (string memory){
return Strings.toHexString(num);
}
}
18.Import
import
可以在一個檔案中引用另一個檔案的內容,提高程式碼的可重用性和組織性;
- 透過檔案的相對位置可以引用:
import './xxx.sol';
; - 透過原始檔網址匯入網上的合約全域性符號;
import 'https://xxxxx/xxx.sol';
; - 透過
npm
的目錄匯入:import '@openzeppelin/contracts/access/Ownable.sol';
; - 透過指定
全域性符號
匯入合約特定的全域性符號:import {XXX} from './xxx.sol';
;
// ---------------------------Demo.sol----------------------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract B{
function sayHello() public pure returns (string memory){
return "Hello!";
}
}
// ---------------------------test.sol----------------------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 透過檔案相對位置import
import './Demo.sol';
// 透過`全域性符號`匯入特定的合約
// 'B'是Demo.sol中合約的名稱
import {B} from './Demo.sol';
// 透過網址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
// 引用OpenZeppelin合約
import '@openzeppelin/contracts/access/Ownable.sol';
contract A {
// 成功匯入Address庫
using Address for address;
// 宣告Demo.sol中的合約變數
// 要使用合約名稱
B b = new B();
// 呼叫引入的Demo.sol中合約B的函式
function callImport() public view {
b.sayHello();
}
}
19.接收ETH
Solidity
支援兩種特殊的回撥函式:receive()
和fallback()
;
主要在兩種情況使用:
- 接收ETH
- 處理合約中不存在的函式呼叫(代理合約
proxy contract
)
在0.6.x版本之前,語法上只有
fallback()
函式,用來接收使用者傳送的ETH以及在被呼叫函式簽名沒有匹配到時呼叫;0.6版本之後,Solidity才將其拆分為
receive()
和fallback()
。
19_01.接收ETH函式-receive
receive
函式是在合約收到ETH轉賬時會被呼叫的函式,一個合約最多隻能有一個;
宣告的方式和一般函式不一樣,不需要function
關鍵字,且不能有任何引數,不能返回任何值,必須包含external
和payable
;
receive
函式最好不要執行太多邏輯,因為對方呼叫send
和transfer
方法傳送ETH的話,gas
會被限制在2300,receive
太複雜可能會觸發Out of gas
報錯;用
call
就可以自定義gas
執行更復雜的邏輯。
示例(在receive
中傳送一個事件):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract A {
event Received(address Sender, uint Value);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
在老版本中,有些惡意合約,會在
receive
函式中嵌入惡意消耗gas
的內容或者使得執行故意失敗的程式碼,導致一些包含退款和轉賬邏輯的合約不能正常工作;
19_02.回退函式-fallback
fallback
函式會在呼叫合約中不存在的函式時被觸發;
可用於接收ETH,也可用於代理合約(proxy contract
);
同receive
函式一樣,不需要function
關鍵字,但必須包含external
,一般也會使用payable
來修飾;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract A {
event fallbackCalled(address Sender, uint Value, bytes Data);
fallback() external payable {
emit fallbackCalled(msg.sender, msg.value, msg.data);
}
}
19_03.兩者區別
首先,它們倆都能夠接收ETH;,它們觸發的規則如下:
/ 是 --> receive()
/ 是 --> receive()是否存在
接收ETH --> msg.data是否為空 \ 否 --> fallback()
\ 否 --> fallback()
只有
msg.data
為空且receive()
存在時,才會使用receive()
;兩者都不存在時,向合約傳送ETH會報錯;(但仍然可以透過帶有
payable
的函式向合約傳送ETH)
有receive
函式時,轉賬時data
為空:
轉賬時data
不為空:
20.傳送ETH
Solidity
有三種方式向其他合約傳送ETH:transfer()
,send()
,call()
,其中call
推薦使用;
首先先部署一個接收ETH的合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract ReceiveETH {
// 收到ETH的事件
// 記錄amount和gas
event Log(uint amount, uint gas);
// 接收ETH時觸發的方法
receive() external payable {
emit Log(msg.value, gasleft());
}
// 返回ETH餘額
function getBalance() public view returns (uint){
return address(this).balance;
}
}
部署後執行getBalance()
,發現此時的餘額為0:
20_01.transfer
用法:接收方地址.transfer(傳送的ETH數額)
;
transfer
的gas限制是2300,足夠用於轉賬,前提是接收方的fallback
和receive
不能太複雜;transfer
如果轉賬失敗,會自動revert
交易(回滾交易);
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Transfer_test{
function transferETH(address payable to, uint256 amount) external payable {
// to是接收方
to.transfer(amount);
}
}
轉賬失敗時:
轉賬成功時(多餘的轉賬會被返回到傳送方合約,並非附帶ETH的錢包):
20_02.send
用法:接收方地址.send(傳送的ETH數額)
;
send
的gas限制同樣是2300;send
如果轉賬失敗,不會revert
;send
的返回值是bool
,代表的是轉賬成功或者失敗,需要額外的程式碼來處理;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Send_test{
// 傳送ETH失敗的錯誤
error SendFailed();
// 傳送ETH
function sendETH(address payable to, uint256 amount) external payable {
bool success = to.send(amount);
if (!success){
// 失敗就revert錯誤
revert SendFailed();
}
}
}
轉賬失敗:
轉賬成功(同樣多餘的ETH退回到傳送方合約):
20_03.call
用法:接收方地址.call{value:傳送到ETH數額}("")
;
call
沒有gas限制,可以支援對方合約fallback
和receive
實現複雜邏輯;call
如果轉賬失敗,不會revert
;call
對返回值是bool
,和send
一樣需要額外程式碼處理;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Call_test{
// 傳送ETH失敗的錯誤
error CallFailed();
// 傳送ETH
function callETH(address payable to, uint256 amount) external payable {
bool success = to.call{value:amount}("");
if (!success){
// 失敗就revert錯誤
revert CallFailed();
}
}
}
轉賬失敗時:
轉賬成功時(同樣多餘的ETH退回到傳送方合約):
21.呼叫其他合約
TestContract
合約,目的是被其他合約所呼叫:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract TestContract {
// 設定私有變數
uint256 private x = 0;
// 交易的事件,記錄amount和gas
event Log(uint amount, uint gas);
// 得到合約賬戶餘額
function getBalance() public view returns (uint){
return address(this).balance;
}
// 設定合約中私有變數值
// 同時可以向其中轉賬
function setX(uint256 num) external payable {
x = num;
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
// 獲得私有變數的值
function getX() external view returns (uint256){
return x;
}
}
部署,並得到合約地址:
呼叫合約的程式碼:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 由於在不同的檔案,所以先匯入
import './test.sol';
contract CallContract{
// 方式一
// 合約名(合約地址).func()
function callSetX(address contract_address, uint256 x) external{
TestContract(contract_address).setX(x);
}
// 方式二
// 合約地址.func()
function callGetX(TestContract contract_address) external view returns(uint x){
x = contract_address.getX();
}
// 方式三
// 建立合約物件的方式,然後呼叫
function callGetX_2(address contract_address) external view returns(uint x){
TestContract tc = TestContract(contract_address);
x = tc.getX();
}
// 呼叫並轉賬
function setXTransferETH(address contract_address, uint256 x) payable external{
TestContract(contract_address).setX{value: msg.value}(x);
}
}
21_01.呼叫方式一
可以在函式中傳入合約地址,生成目標合約的引用,然後再呼叫函式;
-
用法:
合約名(合約地址).func(引數)
; -
合約名和介面都必須保持一致(
TestContract
和setX()
);
// 方式一
// 合約名(合約地址).func()
function callSetX(address contract_address, uint256 x) external{
TestContract(contract_address).setX(x);
}
21_02.呼叫方式二
參考方式一中,將address
型別換為目標合約名
即可;
注意:TestContract contract_address
的底層還是address
型別,生成的ABI
中,呼叫callGetX
時傳入的引數都是address
型別的;
- 用法:
- 引數->
合約名 合約地址
; - 函式內部->
合約地址.func(引數)
;
- 引數->
// 方式二
// 合約地址.func()
function callGetX(TestContract contract_address) external view returns(uint x){
x = contract_address.getX();
}
21_03.呼叫方式三
透過建立合約(物件)的方式;
用法:合約名 變數名 = 合約名(地址);
;
// 方式三
// 建立合約物件的方式,然後呼叫
function callGetX_2(address contract_address) external view returns(uint x){
TestContract tc = TestContract(contract_address);
x = tc.getX();
}
21_04.呼叫並轉賬
如果目標函式是payable
的,那麼便可以向其轉賬;
用法:合約名(合約地址).func{value:xxx}(引數);
;
// 呼叫並轉賬
function setXTransferETH(address contract_address, uint256 x) payable external{
TestContract(contract_address).setX{value: msg.value}(x);
}
22.Call
在20_03
中call
可以用來傳送ETH,同時它還可以呼叫合約;
call
是address
型別的低階成員函式,它用來與其他合約互動;
- 返回值:
(bool, bytes memory)
,分別對應call
是否成功以及目標函式的返回值;
call
是官方推薦的透過觸發fallback
或receive
函式傳送ETH的方法;- 不推薦用
call
來呼叫另一個合約(因為當你呼叫一個不安全的合約時,主動權便不在你的手上;推薦宣告合約變數後呼叫函式21_03
);- 當我們不知道對方合約的原始碼或者
ABI
,就沒法生成合約變數;此時,仍然可以透過call
呼叫對方合約的函式;
22_01.使用規則
用法:目標合約地址.call(位元組碼)
,可以在不知道原始碼或ABI的情況下呼叫;
位元組碼
:利用結構化編碼函式來獲得 -->abi.encodeWithSignature("函式簽名", 具體引數, 具體引數, ...)
;函式簽名
:是函式名(引數型別,引數型別,...)
示例:
abi.encodeWithSignature("f(uint256,address)",x,addr)
在呼叫合約的同時,call
還能知道交易傳送的ETH和gas:
使用方法:目標合約地址.call{value:ETH數額, gas:gas數額}(位元組碼)
,就是在引數前加了大括號,裡面填上傳送的數額;
22_02.透過call呼叫目標合約
目標合約(還是和之前一樣):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract TestContract {
// 設定私有變數
uint256 private x = 0;
// 交易的事件,記錄amount和gas
event Log(uint amount, uint gas);
// 得到合約賬戶餘額
function getBalance() public view returns (uint){
return address(this).balance;
}
// 設定合約中私有變數值
// 同時可以向其中轉賬
function setX(uint256 num) external payable {
x = num;
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
// 獲得私有變數的值
function getX() external view returns (uint256){
return x;
}
}
呼叫setX(uint256 num)
函式,有引數,但無返回值(data
無內容),附帶ETH傳送過去:
呼叫getX()
函式,無引數,但有返回值(data
有內容),不帶ETH:
呼叫一個不存在的函式:
- 當沒有
fallback
函式的情況下(會返回false):
- 當給目標合約新增一個
fallback
函式時,再呼叫它(會返回true):
fallback() external payable { }
完整示例程式碼:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 沒有匯入test.sol
contract CallContract{
// 定義的Response事件
// 輸出call返回的結果和data
event Response(bool success, bytes data);
function callSetX(address payable addr, uint256 x) public payable {
// 呼叫setX函式
// 同時可以傳送ETH
// {}中是傳送的ETH數額
// ()中是利用結構化編碼函式獲得的位元組碼
(bool success, bytes memory data) = addr.call{value:msg.value}(abi.encodeWithSignature("setX(uint256)", x));
emit Response(success, data);
}
function callGetX(address addr) external returns (uint256){
// 呼叫getX函式
// ()中是利用結構化編碼函式獲得的位元組碼
(bool success, bytes memory data) = addr.call(abi.encodeWithSignature("getX()"));
emit Response(success, data);
// 返回data中的值(轉為uint)
return abi.decode(data, (uint256));
}
function callNonExist(address addr) external {
// 呼叫一個不存在的函式
(bool success, bytes memory data) = addr.call(abi.encodeWithSignature("xxx(address)"));
emit Response(success, data);
}
}
23.DelegateCall
delegatecall
委託,和call
差不多,同樣是地址型別的低階成員函式;
23_01.什麼是委託
當使用者A
透過合約B
來call
合約C
時:
-
此時執行的是合約
C
上的函式; -
上下文(
Context
,可以理解為包含變數和狀態的環境)也是合約C
的:-
msg.sender
是合約B
的地址 -
若函式改變了一些狀態變數,產生的效果會用在合約
C
的變數上;
-
而當使用者A
透過合約B
來delegatecall
合約C
時:
- 執行的是合約
C
上的函式; - 上下文仍然是合約
B
的:msg.sender
是合約A
的地址;- 若函式改變了一些狀態變數,產生的效果會用在合約
B
的變數上;
也可以這麼理解:
- 合約B的視角
我合約B"借用"了合約C的某一個函式的功能,來改變我自己這邊的一些狀態;
- 現實世界
使用者A
:投資者
合約B中的狀態變數
:資產
合約C中執行的函式
:風險投資機構
投資者
將他的資產
交給一個風險投資機構
來打理,此時執行的是風險投資機構
,但改變的是投資者
的資產
;
23_02.使用規則
和call
類似:目標合約地址.delegatecall(位元組碼)
;
其中位元組碼
仍是透過abi.encodeWithSignature()
來獲得的;
與call
不一樣的是:delegatecall()
在呼叫時,不能指定傳送的ETH
數額,但能指定gas
數額;
注意:
delegatecall()
有安全隱患,使用時要保證當前合約和目標合約的狀態變數儲存結構相同,並且目標合約安全,不然會造成財產損失。
23_03.什麼情況下用到委託
主要有兩個應用場景:
- 代理合約(
Proxy Contract
)
將智慧合約的儲存合約
和邏輯合約
分開;
儲存合約(代理合約(Proxy Contract)
)儲存所有相關的變數,並且儲存邏輯合約的地址;
邏輯合約(Logic Contract
)中儲存所有的函式,透過delegatecall
執行;
當升級的時候,只需要將代理合約指向新的邏輯合約即可(以太坊官方開發文件中有提到)。
EIP-2535 Diamonds
(鑽石)
鑽石是一個支援構建可在生產中擴充套件的模組化智慧合約系統的標準。鑽石具有多個實施合約的代理合約。詳細資訊:鑽石標準簡介。
23_04.示例
使用者A
透過合約B
委託呼叫合約C
;
被呼叫的合約C
兩個狀態變數和一個可以修改狀態變數的函式:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 被呼叫的合約C
contract C {
// 狀態變數num
uint public num;
// 狀態變數sender
address public sender;
// 設定狀態變數num和sender的值
function setVars(uint x) public payable {
num = x;
sender = msg.sender;
}
}
發起呼叫的合約B
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 沒有匯入test.sol
contract B{
// 必須與合約C的變數儲存佈局相同
// 兩個變數,順序也必須一致
uint public num;
address public sender;
// 透過call來呼叫SetVars函式
// 預計只會改變合約C的變數值
function callSetVars(address addr, uint x) external payable {
(bool success, bytes memory data) = addr.call(abi.encodeWithSignature("setVars(uint256)", x));
}
// 透過delegatecall來呼叫SetVars函式
// 預計只改變本合約(合約B)的變數值
function delegatecallSetVars(address addr, uint x) external payable {
(bool success, bytes memory data) = addr.delegatecall(abi.encodeWithSignature("setVars(uint256)", x));
}
}
驗證
狀態變數的初始值:
在合約B中
呼叫callSetVars
函式,預計只會改變合約C
中的變數值(num
為更改後的值,sender
為合約B的地址):
在合約B中呼叫delegatecallSetVars函式,預計會改變合約B中的變數(num
變為更改後的值,sender
為錢包地址),合約C中的不變:
24.在合約中建立新合約
以太坊上,外部賬戶EOA
(錢包)可以建立智慧合約;此外,智慧合約也可以建立新的智慧合約。
去中心化交易所
Uniswap
就是利用工廠合約(PairFactory)
建立了無數個幣對合約(Pair)
。
Uniswap V2
核心合約中包含兩個合約:
UniswapV2Pair
:幣對合約,用於管理幣對地址,流動性,買賣;UniswapV2Factory
:工廠合約,用於建立新的幣對合約,並管理幣對地址;
24_01.Create
Create
用法:ContractXXX xxx = new ContarctXXX{value:_value}(建構函式引數)
就和new
物件一樣,新new
一個合約,並傳入新合約建構函式
所需要的引數,並且可以附帶ETH(前提建構函式得是payable
的);
極簡Uniswap
用Create
來實現一個極簡版的Uniswap
(真正的Uniswap
不是用這種方式實現的,是24_02
中的方法):
幣對合約(Pair)
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 幣對合約
// 包含3個狀態變數
// 部署時將factory賦值
// 呼叫initToken時更新幣對中兩個代幣的地址
contract Pair{
// 工廠地址
address public factory;
// 代幣0
address public token0;
// 代幣1
address public token1;
// 建構函式,帶有payable
// 將訊息的傳送者賦值為factory
constructor() payable {
factory = msg.sender;
}
// 初始化代幣0和代幣1的地址
function initToken(address _token0, address _token1) external {
// 檢測是否是factory呼叫的
require(factory == msg.sender, "Not real factory use function");
// 代幣地址賦值
token0 = _token0;
token1 = _token1;
}
}
工廠合約(PairFactory)
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import './Pair.sol';
// 工廠合約
// 一個對映將 代幣 和 幣對合約地址 建立聯絡
// 一個陣列 儲存 幣對合約地址
// 利用Create方法建立新的合約
contract PairFactory {
// 對映,address -> address -> address
mapping (address => mapping ( address => address )) public getPair;
// 儲存所有的Pair地址(幣對合約地址)
address[] public allPairs;
// 建立新的幣對合約地址
function createPair(address token0, address token1) external returns (address pairAddr){
// 利用Create方法建立新合約
Pair pair = new Pair();
// 呼叫新合約的initToken方法,並初始化裡面的token0,token1
pair.initToken(token0, token1);
// 獲得當前幣對合約的地址
pairAddr = address(pair);
// 儲存在陣列中
allPairs.push(pairAddr);
// 建立對映
// token0 -> token1 -> 幣對合約地址
getPair[token0][token1] = pairAddr;
// token1 -> token0 -> 幣對合約地址
getPair[token1][token0] = pairAddr;
}
}
利用下面兩個地址作為引數呼叫createPair
函式:
WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC鏈上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
24_02.Create2
上面可以看到Create
方法建立的合約地址是完全不可預測的;
而Create2
方法使我們在部署智慧合約之前就能預測合約的地址(Uniswap
建立 Pair合約(幣對合約)
的方法就是這個)。
Create2
方法的目的是為了讓合約地址獨立於未來事件,不管未來區塊鏈上發生什麼,都可以將合約部署在事先計算好的地址上。
Create原理
新地址 = hash(建立者地址, nonce)
;無論是
EOA
建立還是智慧合約建立,都是這個方法;
建立者地址
是部署的錢包地址
或者合約地址
;
nonce
,對於EOA
是該地址傳送的交易總數,對於合約賬戶
是建立的合約總數,建立時的nonce
為nonce+1
;建立者的地址不會變,但是
nonce
會隨著時間而改變,所以不好預測;
Create2原理
新地址 = hash("0xFF", 建立者地址, salt, initcode)
;
0xFF
:一個常數,避免和Create
衝突;
建立者地址
:呼叫Create2
的當前合約地址;
salt
:一個由建立者指定的bytes32
型別的值,主要目的是用來影響新建立的合約地址;
initcode
:新合約的初始位元組碼(合約的Creation Code和建構函式引數);
Create2用法
ContractXXX xxx = new COntractXXX{salt:_salt, value:_value}(建構函式引數)
;
同樣也是new
,只不過多加入了個salt
;
極簡Uniswap2
使用Create2
來實現一個極簡的Uniswap
;
幣對合約(Pair)
(和之前一樣):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 幣對合約
// 包含3個狀態變數
// 部署時將factory賦值
// 呼叫initToken時更新幣對中兩個代幣的地址
contract Pair{
// 工廠地址
address public factory;
// 代幣0
address public token0;
// 代幣1
address public token1;
// 建構函式,帶有payable
// 將訊息的傳送者賦值為factory
constructor() payable {
factory = msg.sender;
}
// 初始化代幣0和代幣1的地址
function initToken(address _token0, address _token1) external {
// 檢測是否是factory呼叫的
require(factory == msg.sender, "Not real factory use function");
// 代幣地址賦值
token0 = _token0;
token1 = _token1;
}
}
工廠合約(PairFactory)
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import './Pair.sol';
// 工廠合約
// 一個對映將 代幣 和 幣對合約地址 建立聯絡
// 一個陣列 儲存 幣對合約地址
// 利用Create方法建立新的合約
contract PairFactoryV2 {
// 對映,address -> address -> address
mapping (address => mapping ( address => address )) public getPair;
// 儲存所有的Pair地址(幣對合約地址)
address[] public allPairs;
// 建立新的幣對合約地址
function createPairV2(address token0, address token1) external returns (address pairAddr){
// 檢測兩個地址不同
require(token0 != token1, "Identcial Address");
// 將地址按照從小到大排序
(address token_0, address token_1) = token0 < token1 ? (token0, token1) : (token1, token0);
// 計算一個salt
bytes32 salt = keccak256(abi.encodePacked(token_0, token_1));
// 利用Create2方法建立新合約
Pair pair = new Pair{salt: salt}();
// 呼叫新合約的initToken方法,並初始化裡面的token0,token1
pair.initToken(token_0, token_1);
// 獲得當前幣對合約的地址
pairAddr = address(pair);
// 儲存在陣列中
allPairs.push(pairAddr);
// 建立對映
// token0 -> token1 -> 幣對合約地址
getPair[token0][token1] = pairAddr;
// token1 -> token0 -> 幣對合約地址
getPair[token1][token0] = pairAddr;
}
// 預測地址
function calcAddr(address token0, address token1) public view returns (address predictedAddr){
// 檢測兩個地址不同
require(token0 != token1, "Identcial Address");
// 將地址按照從小到大排序
(address token_0, address token_1) = token0 < token1 ? (token0, token1) : (token1, token0);
// 計算一個salt
bytes32 salt = keccak256(abi.encodePacked(token_0, token_1));
// 計算地址
predictedAddr = address(uint160(uint(
// hash
keccak256(abi.encodePacked(
// 四個引數
bytes1(0xff),
address(this),
salt,
keccak256(type(Pair).creationCode)
)))
));
}
}
若部署的合約的建構函式中需要有引數:
比如
Pair pair new Pair{salt:salt}(address(this));
predictedAddr = address(uint160(uint( // hash keccak256(abi.encodePacked( // 四個引數 bytes1(0xff), address(this), salt, // 一起打包,並計算雜湊 keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this)))) ))) ));
還是利用這兩個地址:
WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC鏈上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
事先計算:
驗證:
24_03.應用場景
- 交易所為新使用者預留建立錢包合約的地址;
- 減少不必要的呼叫(知道新合約的地址後,無需再執行
getPair
的跨合約呼叫);
25.刪除合約
25_01.selfdestruct
selfdestruct
命令可被用來刪除合約,並將該合約剩餘的ETH轉到指定地址;
它為了應對合約出錯的極端情況而設計的,最早被命名為
suicide
,後面改為selfdestruct
;在
v0.8.18
版本中,它被標記為"不再建議使用",因為在一些情況下它會導致預期之外的合約語意,但由於目前還沒有替代方案,只對開發者做了編譯階段的警告,相關內容:EIP-6049。然而,在以太坊坎昆(Cancun)升級中,EIP-6780被納入升級以實現對
Verkle Tree
更好的支援。該更新減少了SELFDESTRUCT
操作碼的功能。根據提案描述,當前
SELFDESTURCT
僅會被用來將合約中的ETH轉移到指定地址,而原先的刪除功能只有在合約建立-自毀
這兩個操作處在同一筆交易時才能生效。
所以,目前來說:
- 現在的
seldestrict
僅會被用來將合約中的ETH轉移到指定地址; - 已經部署的合約無法被
SELFDESTRUCT
; - 如果要使用原先的
SELFDESTRUCT
功能,必須在同一筆交易中建立並自毀;
25_02.如何使用selfdeftruct
用法:selfdestruct(addr)
;
其中,
addr
是接收合約中剩餘ETH的地址,並且addr
地址不需要有receive()
或fallback()
也能接收ETH。
25_03.升級前後功能對比
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract SelfDestructDemo{
uint public value = 10;
constructor() payable {}
receive() external payable { }
// 升級之前應該能自毀
// 升級之後只能轉移ETH
function SelfDestruct() external {
selfdestruct(payable(msg.sender));
}
// 獲取餘額
function getBalance() external view returns (uint balance){
balance = address(this).balance;
}
}
升級前:
合約中函式報錯,並且合約中的ETH被轉入指定地址;
升級後:
合約中的ETH被轉入指定地址,但合約中的函式仍能使用;
25_04.同筆交易實現建立-自毀
// SPDX-License-Identifier: MIT
// pragma solidity ^0.8.26;
pragma solidity ^0.8.4;
// DeployDestructDemo合約(還是上一個)
import './Factory.sol';
contract DeployDestructDemo{
struct DemoResult{
address addr;
uint balance;
uint value;
}
constructor() payable {}
function getBalance() external view returns (uint balance){
balance = address(this).balance;
}
// 演示建立-自毀
function demo() public payable returns (DemoResult memory){
// 建立一個新合約
SelfDestructDemo sd = new SelfDestructDemo{value:msg.value}();
// 給返回值賦值
DemoResult memory res = DemoResult({
addr:address(sd),
balance:sd.getBalance(),
value:sd.value()
});
// 新合約呼叫自銷燬
sd.SelfDestruct();
return res;
}
}
26.ABI編碼解碼
ABI
-(Application Binary Interface,應用二進位制介面),是與以太坊智慧合約互動的標準。
資料基於他們的型別編碼,並且由於編碼後不包含型別資訊,解碼時需要註明它們的型別;
編碼:
abi.encode
、abi.encodePacked
、abi.encodeWithSignature
、abi.encodeWithSelector
;解碼:
abi.decode
;
26_01.abi編碼
下面將這4個變數一起打包編碼:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract ABIEncode{
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
function encode() public view returns (bytes memory res){
res = abi.encodeXXX(x, addr, name, array);
}
}
abi.encode(能和合約互動)
將給定引數利用ABI規則編碼;
將每個引數填充為32位元組的倍數的資料,並拼接在一起;
如果要和智慧合約互動,需要使用它;
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
// 結果
//0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/
若將string變成很長:
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
uint[2] array = [3, 4];
// 結果
//0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000047307841414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414100000000000000000000000000000000000000000000000000
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000047(string)
3078414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141
4141414141414100000000000000000000000000000000000000000000000000
*/
abi.encodePacked(不能和合約互動)
將給定引數根據其所需要的最低空間編碼,與abi.encode
類似,但會省略很多0;
但不能與合約互動;
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
// 結果
//0x000000000000000000000000000000000000000000000000000000000000000a5b38da6a701c568545dcfcb03fcb875f56beddc43078414100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x,因為是uint256)
5b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
30784141(string)
00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004(array)
*/
abi.encodeWithSignature(呼叫其他合約時使用)
與abi.encode
類似,但是第一個引數是函式簽名
,keccak
雜湊,編碼時為4位元組,等同於在前面加了個函式選擇器
;
當呼叫其他函式的時候可以使用;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract ABIEncode{
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
function encode() public view returns (bytes memory res){
res = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}
}
// 結果
//0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
e87082f1(函式簽名)
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/
abi.encodeWithSelector
與abi.encodeWithSignature
類似,只不過第一個引數時函式選擇器
,為函式簽名Keccak
雜湊的前4個位元組;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract ABIEncode{
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
function encode() public view returns (bytes memory res){
res = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
}
}
// 結果
//0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
e87082f1(函式簽名)
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/
26_02.abi解碼
abi.decode
用於解碼abi.encode
生成的二進位制編碼,將它還原成原本的引數;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract ABIEncode{
function dncode(bytes memory data) public pure returns (uint x, address addr, string memory name, uint[2] memory array){
(x, addr, name, array) = abi.decode(data, (uint, address, string, uint[2]));
}
}
// 輸入
// 0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
27.選擇器
27_01.calldata
當我們呼叫智慧合約時,本質上是向目標合約傳送了一段calldata
,傳送交易後,可以在詳細資訊的input
中看到此次交易的calldata
:
傳送的calldata
中前4個位元組是函式選擇器(selector)
;
// 上圖中的calldata
// 0x012b48bf000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
012b48bf(函式選擇器)
(因為bytes是動態的,所以會有下面這倆,靜態的不會有,比如address,uint)
0000000000000000000000000000000000000000000000000000000000000020(偏移量,0x20 = 32,從這開始偏移32個位元組)
00000000000000000000000000000000000000000000000000000000000000e0(引數長度,0xe0 = 7 * 32,正好對應上面)
輸入的引數
000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
*/
其實,calldata
就是告訴智慧合約,為要呼叫哪個函式,引數都是什麼;
27_02.selector的生成
基礎型別引數
基礎型別的引數有:uint(uint8, ..., uint256)
、bool
、address
等;
bytes(keccak256("func_name(uint256,bool,...)"));
固定長度型別引數
固定長度型別的引數,比如:uint256[3]
;
bytes(keccak256("func_name(uint256[3])"));
可變長度型別引數
可變長度型別的引數,比如:address[]
、uint[]
、string
、bytes
等;
bytes(keccak256("func_name(bytes,string)"));
對映型別引數
對映型別的引數有:contract
、enum
、struct
等;
contract Demo{} // 需要轉化為address
struct User{ // 需要轉化為tuple型別:(uint256,bytes)
uint256 uid;
bytes name;
}
enum School {SCHOOL1, SCHOOL2} // 需要轉化為uint8
mapping(address => uint) public balance; // 直接轉化為address(第一個型別),因為mapping型別不能直接作為引數
bytes(keccak256("func_name(address,(uint256,bytes),uint256[],uint8),address"))
27_03.使用selector
address(this).call(abi.encodeWithSelector(0x12345678函式簽名, 引數, 引數, ...));
28.Try Catch
28_01.用法
基礎用法
try func_name(){
// call成功的情況下
} catch{
// call失敗的情況下
}
呼叫的函式有返回值
必須這麼使用(需要加上returns
),同時可以使用返回的變數:
try func_name() returns (address addr, uint x){
// call成功的情況下
// 可以使用返回的變數
} catch{
// call失敗的情況下
}
捕捉特殊的異常原因
try func_name() returns (address addr, uint x){
// call成功的情況下
// 可以使用返回的變數
} catch Error(string memory reason){
// 捕捉revert("xxxx")
// 捕捉require(false, "xxxx")
} catch Panic(uint errorCode){
// 捕捉Panic導致的錯誤
// 例如assert失敗、溢位、除零、陣列訪問越界等
} catch (bytes memory lowLevelData){
// 如果發生了revert且上面2個異常匹配失敗,會進入這個分支
// 例如revert()、require(false)、revert(自定義的error)
}
28_02.示例
呼叫合約(合約建立成功,但函式呼叫錯誤)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract OnlyEven{
constructor(uint a){
// 當a = 0時,require會丟擲異常
require(a != 0, "invalid number");
// 當a = 1時,assert會丟擲異常
assert(a != 1);
}
function onlyEven(uint b) external pure returns(bool success){
// 當b為奇數時,require丟擲異常
require(b % 2 == 0, "Odd number");
success = true;
}
}
contract TryCatch{
// 成功事件
event SuccessEvent();
// 丟擲異常時的兩個事件
// 對應require和revert
event CatchEvent(string message);
// 對應assert
event CatchByte(bytes data);
// 合約狀態變數
OnlyEven oe;
// 建構函式
constructor(){
// 賦值為2,應該不會丟擲異常
oe = new OnlyEven(2);
}
function exec(uint amount) external returns (bool success){
try oe.onlyEven(amount) returns (bool _success){
// 成功,返回True
emit SuccessEvent();
return _success;
} catch Error(string memory reason){
// 失敗,捕捉require(false, error_string)
// 比如此處輸入的是奇數,應該返回"Odd number"
emit CatchEvent(reason);
}
}
}
成功:
失敗:
呼叫合約(合約建立失敗)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract OnlyEven{
constructor(uint a){
// 當a = 0時,require會丟擲異常
require(a != 0, "invalid number");
// 當a = 1時,assert會丟擲異常
assert(a != 1);
}
function onlyEven(uint b) external pure returns(bool success){
// 當b為奇數時,require丟擲異常
require(b % 2 == 0, "Odd number");
success = true;
}
}
contract TryCatch{
// 成功事件
event SuccessEvent();
// 丟擲異常時的兩個事件
// 對應require和revert
event CatchEvent(string message);
// 對應assert
event CatchByte(bytes data);
// exec(0) --> 失敗,釋放CatchEvent
// exec(1) --> 失敗,釋放CatchByte
// exec(2) --> 成功,釋放SuccessEvent
function exec(uint num) external returns (bool success){
try new OnlyEven(num) returns (OnlyEven oe){
emit SuccessEvent();
success = oe.onlyEven(num);
} catch Error(string memory reason){
// 捕捉失敗的revert()和require()
emit CatchEvent(reason);
} catch (bytes memory reason){
// 捕捉失敗的assert()
emit CatchByte(reason);
}
}
}
exec(0) --> 失敗,釋放CatchEvent:
exec(1) --> 失敗,釋放CatchByte:
exec(2) --> 成功,釋放SuccessEvent:
參考:https://github.com/AmazingAng/WTF-Solidity