使用者(外部帳戶,EOA)可以創建智能合約
智能合約也可以建立智能合約
ex: 去中心化交易所uniswap就是利用工廠合約(PairFactory)創建了無數個幣對合約(Pair)。這一講,我會用簡化版的uniswap講如何透過合約創建合約。
在solidity中,
有兩種方法可以在合約中創建新合約,create
和create2
create的用法很簡單,就是new一個合約,並傳入新合約建構函數所需的參數:
Contract x = new Contract{value: _value}(params)
其中Contract是要建立的合約名,x是合約物件(地址),如果建構函數是payable,可以建立時轉入_value數量的ETH,params是新合約建構函數的參數。
說明:
Uniswap V2核心合約中包含兩個合約:
- UniswapV2Pair: 幣對合約,用於管理幣對地址、流動性、買賣。
- UniswapV2Factory: 工廠合約,用於創建新的幣對,並管理幣對地址。
Pair 極簡版:
contract Pair{
address public factory; // 工厂合约地址
address public token0; // 代币1
address public token1; // 代币2
constructor() payable {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
}
PairFactory
合約:
contract PairFactory{
mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
address[] public allPairs; // 保存所有Pair地址
function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
// 创建新合约
Pair pair = new Pair();
// 调用新合约的initialize方法
pair.initialize(tokenA, tokenB);
// 更新地址map
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
智能合约可以由其他合约和普通账户利用CREATE操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算: 创建者的地址(通常为部署的钱包地址或者合约地址)和nonce(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1)的哈希。
新地址 = hash(创建者地址, nonce)
创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。
CREATE2
創建的合約地址由4個部分決定
- 0xFF:一個常數,避免和CREATE衝突
- CreatorAddress: 呼叫CREATE2 的目前合約(建立合約)地址。
- salt(鹽):一個創建者指定的bytes32類型的值,它的主要目的是用來影響新創建的合約的地址。
- initcode: 新合約的初始位元組碼(合約的Creation Code和建構函數的參數)。
新地址 = hash("0xFF",创建者地址, salt, initcode)
``CREATE2 確保,如果創建者使用
CREATE2`和提供的`salt`部署給定的合約`initcode`,它將儲存在新地址中。
Contract x = new Contract{salt: _salt, value: _value}(params)
create2
的實際應用
-
交易所為新用戶預留創建錢包合約地址。
-
由
CREATE2
驅動的factory合約,在Uniswap V2中交易對的創建是在Factory中調用CREATE2完成。 -
代理合約模式:當需要在確定地址部署合約,並能夠在這個地址上進行多次部署(例如不同版本的合約)時,create2 非常有用。
-
靈活重啟合約:在合約被銷毀(例如通過 selfdestruct 操作)後,仍可以使用相同的 salt 和字節碼來重新部署該合約到同一地址。
這樣做的好處是: 它可以得到一個確定的pair地址, 使得Router中就可以透過(tokenA, tokenB)計算出pair地址, 不再需要執行一次Factory.getPair(tokenA, tokenB)的跨合約呼叫。
這一講,我們介紹了CREATE2操作碼的原理,使用方法,並用它完成了極簡版的Uniswap並提前計算幣對合約地址。CREATE2讓我們可以在部署合約前確定它的合約地址,這也是一些layer2專案的基礎。
selfdestruct
命令可以用來刪除智能合約,並將該合約剩餘ETH轉到指定位址。
用法:selfdestruct(_addr);
其中_addr是接收合約中剩餘ETH的地址。_addr地址不需要有receive()或fallback()也能接收ETH。
-
在坎昆升級前,合約會被自毀。
銷毀合約後會把合約裡面的 eth 轉給調用這個合約的人。
-
但升級後,合約依然存在,只是將合約包含的ETH轉移到指定位址,而合約依然能夠呼叫。
寫範例的時候要注意
根據提案,原先的刪除功能只有在合约创建-自毁這兩個操作處在同一筆交易時才能生效。所以我們需要透過另一個合約來控制。
contract DeployContract {
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){
DeleteContract del = new DeleteContract{value:msg.value}();
DemoResult memory res = DemoResult({
addr: address(del),
balance: del.getBalance(),
value: del.value()
});
del.deleteContract();
return res;
}
}
- 注意
- 對外提供合約銷毀接口時,最好設定為只有合約所有者可以調用,可以使用函數修飾符onlyOwner進行函數聲明。
- 當合約中有selfdestruct功能時常常會帶來安全問題和信任問題,合約中的selfdestruct功能會為攻擊者打開攻擊向量(例如使用selfdestruct向一個合約頻繁轉入token進行攻擊,這將大大節省了GAS的費用,雖然很少人這麼做),此外,此功能還會降低用戶對合約的信心。
ABI(Application Binary Interface,應用二進位介面)
是與以太坊智慧合約互動的標準。資料基於他們的類型編碼;
並且由於編碼後不包含類型訊息,解碼時需要註明它們的類型。
Solidity中,ABI编码有4個函數:
- abi.encode
- abi.encodePacked
- abi.encodeWithSignature
- abi.encodeWithSelector
而ABI解码
有1個函數:abi.decode
,用於解碼 abi.encode
的資料。
編碼變數:
uint x = 10;
address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
string name = "0xAA";
uint[2] array = [5, 6];
ABI被設計出來跟智能合約交互,他將每個參數填入為32位元組的數據,並拼接在一起。如果你要和合約交互,你要用的就是abi.encode
function encode() public view returns(bytes memory result) {
result = abi.encode(x, addr, name, array);
}
result
0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000,由於abi.encode將每個資料填入32字節,中間有很多0。
- uint x = 10:
編碼為 32 位元組(64 個十六進位字元):000000000000000000000000000000000000000000000000000000000000000a
- address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71:
編碼為 32 位元組(64 個十六進位字元),地址會被填充到 32 位元組的右邊:0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71
- string name = "0xAA":
字串在 ABI 編碼中會被編碼為動態數據,包含長度和實際數據。 長度(2 個字元):0000000000000000000000000000000000000000000000000000000000000002 實際數據(0xAA 的 ASCII 編碼):3078414100000000000000000000000000000000000000000000000000000000
- uint[2] array = [5, 6]:
編碼為兩個 32 位元組(每個元素一個 32 位元組): 0000000000000000000000000000000000000000000000000000000000000005 0000000000000000000000000000000000000000000000000000000000000006
將給定參數根據其所需最低空間編碼。它類似abi.encode
,但是會把其中填充的很多0省略。比如,只用1位元組來編碼uint8
類型。當你想省空間,並且不與合約互動的時候,可以使用abi.encodePacked
,例如算一些數據的hash時。
0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006
000000000000000000000000000000000000000000000000000000000000000a # uint x = 10
7a58c0be72be218b41c608b7fe7c5bb630736c71 # address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71
30784141 # string name = "0xAA"
0000000000000000000000000000000000000000000000000000000000000005 # uint[2] array[0] = 5
0000000000000000000000000000000000000000000000000000000000000006 # uint[2] array[1] = 6
與abi.encode功能類似,只不過第一個參數為函数签名,例如"foo(uint256,address,string,uint256[2])"。當呼叫其他合約的時候可以使用。
function encodeWithSignature() public view returns(bytes memory result) {
result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}
0xe87082f1
前面多了這個,(函數選擇器)
000000000000000000000000000000000000000000000000000000000000000a # uint x = 10
0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71 # address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71
00000000000000000000000000000000000000000000000000000000000000a0 # 偏移量,指向動態數據的起始位置
0000000000000000000000000000000000000000000000000000000000000005 # uint[2] array[0] = 5
0000000000000000000000000000000000000000000000000000000000000006 # uint[2] array[1] = 6
0000000000000000000000000000000000000000000000000000000000000043 # 偏移量,指向動態數據的起始位置
3078414100000000000000000000000000000000000000000000000000000000 # string name = "0xAA"
函數選擇器是透過函數名稱和參數進行簽名處理(Keccak–Sha3)來識別函數,可以用於不同合約之間的函數調用
與abi.encodeWithSignature功能類似,只不過第一個參數為函數選擇器,例如"0xe87082f1"。當呼叫其他合約的時候可以使用。
function encodeWithSelector() public view returns(bytes memory result) {
result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
}
結果和 encodeWithSignature 一樣
解碼abi.encode
的資料
function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
(dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
}
例子:調用合約的函數
bytes4 selector = contract.getValue.selector; // 函數選擇器
bytes memory data = abi.encodeWithSelector(selector, _x); // 編碼
(bool success, bytes memory returnedData) = address(contract).staticcall(data); // 調用
require(success);
return abi.decode(returnedData, (uint256));
- 對不開源合約進行反編譯後,某些函數無法查到函數簽名,可透過ABI進行呼叫。 例如: 0x533ba33a() 是一個反編譯後顯示的函數,只有函數編碼後的結果,無法查到函數簽名
function 0x533ba33a() public payable {
return stor_14;
}
在這種情況下,就可以透過ABI函數選擇器來調用
bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a));
(bool success, bytes memory returnedData) = address(contract).staticcall(data);
require(success);
return abi.decode(returnedData, (uint256));