Skip to content

Latest commit

 

History

History
319 lines (219 loc) · 12.4 KB

File metadata and controls

319 lines (219 loc) · 12.4 KB

24. 在合约中创建新合约

使用者(外部帳戶,EOA)可以創建智能合約

智能合約也可以建立智能合約

ex: 去中心化交易所uniswap就是利用工廠合約(PairFactory)創建了無數個幣對合約(Pair)。這一講,我會用簡化版的uniswap講如何透過合約創建合約。

在solidity中, 有兩種方法可以在合約中創建新合約,createcreate2

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;
    }
}

25. create2

CREATE如何计算地址

智能合约可以由其他合约和普通账户利用CREATE操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算: 创建者的地址(通常为部署的钱包地址或者合约地址)和nonce(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1)的哈希。

新地址 = hash(创建者地址, nonce)

创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。

CREATE2的目的是為了讓合約地址獨立於未來的事件。

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專案的基礎。

26. 删除合约

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的費用,雖然很少人這麼做),此外,此功能還會降低用戶對合約的信心。

27. ABI编码解码

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.encode

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。

  1. uint x = 10:

編碼為 32 位元組(64 個十六進位字元):000000000000000000000000000000000000000000000000000000000000000a

  1. address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71:

編碼為 32 位元組(64 個十六進位字元),地址會被填充到 32 位元組的右邊:0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71

  1. string name = "0xAA":

字串在 ABI 編碼中會被編碼為動態數據,包含長度和實際數據。 長度(2 個字元):0000000000000000000000000000000000000000000000000000000000000002 實際數據(0xAA 的 ASCII 編碼):3078414100000000000000000000000000000000000000000000000000000000

  1. uint[2] array = [5, 6]:

編碼為兩個 32 位元組(每個元素一個 32 位元組): 0000000000000000000000000000000000000000000000000000000000000005 0000000000000000000000000000000000000000000000000000000000000006

abi.encodePacked

將給定參數根據其所需最低空間編碼。它類似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.encodeWithSignature

與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.encodeWithSelector

與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.decode

解碼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]));
}

ABI 的使用

例子:調用合約的函數

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));