天天看點

涉及到動态數組時Solidity與Vyper智能合約互相調用方法

涉及到動态數組時Solidity與Vyper智能合約互相調用

    • 前言
    • 合約編寫
    • 編譯部署
    • 測試腳本

前言

我們知道,目前以太坊的智能合約程式設計語言主要有兩種,一種叫Solidity,大家很熟悉了;一種叫Vyper,雖然是後起之秀,但比較小衆,用到的項目很少。Vyper的安全性其實是高于Solidity的,但為什麼這麼小衆呢?個人覺得最主要的原因是不支援動态數組,這樣使用它來編寫複雜的應用就太麻煩了。

那麼問題來了,如果一個智能合約是Solidity編寫的而另一個智能合約是Vyper編寫的。它們之間要互相調用,但是涉及的接口在Solidity中使用了動态數組作為輸入輸出參數,在Vyper中使用了固定大小的數組作為輸入輸出參數。那麼他們之間怎麼調用呢?

直接調用?會由于參數不比對而調用失敗。那能不能互相調用,怎麼調用呢?凡事要打破沙鍋問到底,要弄明白。因為不管什麼語言寫的智能合約,運作的都是位元組碼,底層調用時都是操作碼,是以個人覺得互相之間是可以調用的。但覺得沒有用,得親自實踐,必須得研究嘗試。經過反複測試,發現它們之間是可以互相調用的,雖然有些限制。

合約編寫

為了進行測試,我們準備了三個合約。合約A,使用Solidity編寫,提供一個輸入輸出參數都是動态數組的接口。合約B,使用VYPER編寫,提供一個輸入輸出參數都是固定大小數組的接口。合約C,使用Solidity編寫,分别調用A和B的接口進行測試。

使用Solidity編寫的合約A和C的源代碼如下:

pragma solidity ^0.6.0;

//Vyper 合約B,使用fixed list。
interface IB {
    function callTwice(uint[] calldata source) external pure returns(uint[] memory target);
}

//合約A 使用Solidity 動态數組
contract A {
    function callTwice(uint[] calldata source) external pure returns(uint[] memory target) {
        uint len = source.length;
        target = new uint[](len);
        for(uint i=0;i<len;i++){
            target[i] = 2 * source[i];
        }
    }
}

//測試合約C,用來分别調用A和B的callTwice
contract C {
    address public a;
    address public b;
    constructor(address _a,address _b) public {
        a = _a;
        b = _b;
    }

    //直接調用Solidity合約的callTwice(uint256[])
    function callTwiceA(uint[] calldata source) external view returns(uint[] memory) {
        return A(a).callTwice(source);
    }

    //直接調用Vyper合約的callTwice(uint256[3])
    function callTwiceB(uint[] calldata source) external view returns(uint[] memory) {
        return IB(b).callTwice(source);
    }

    //原生方法調用Vyper合約的callTwice(uint256[3])
    function callTwiceBRaw(uint[] calldata source) external view returns(uint[] memory result ) {
        source;
        bytes memory bts2 = hex"b638652e";   //bytes4(keccak256(bytes("callTwice(uint256[3])")))
        bytes memory b3 = concat(bts2,bytes(msg.data[68:])); // 4 + offset + length of array
        (bool success, bytes memory returnData) = b.staticcall(b3);
        require(success, "staticcall failed");
        uint[3] memory datas = abi.decode(returnData, (uint[3]));
        result =  new uint[](3);
        for(uint i=0;i<3;i++){
            result[i] = datas[i];
        }
    }

    function concat(bytes memory one, bytes memory two)
            internal pure returns (bytes memory) {
        return abi.encodePacked(one, two);
    }
    
}

           

Vyper編寫的合約B源碼如下:

# @version 0.1.0b16

# Solidity 合約A ,用來調用其callTwice方法
contract IA:
    def callTwice(source:uint256[3]) -> uint256[3]: constant


a:public(address)


@public
def __init__(_a:address):
    self.a = _a


# 自己的callTwice方法,提供給Solidity合約調用
@public
@constant
def callTwice(source:uint256[3]) -> uint256[3]:
    result: uint256[3] =  [0,0,0]
    for i in range(3):
        result[i] = source[i] * 2
    return result


# 直接調用A的callTwice方法,由于參數不符,調用會失敗
@public
@constant
def callTwiceA(source:uint256[3]) -> uint256[3]:
    return  IA(self.a).callTwice(source)


#解析傳回的結果
@private
def uintArrayResponse(unitBytes: bytes[160]) -> uint256[3]:
    result: uint256[3] = [0,0,0]
    for i in range(3):
        start: int128 = 32*(2+i)
        extracted: bytes32 = extract32(unitBytes, start, type=bytes32)
        result[i] = convert(extracted, uint256)
    return result


# 通過原生方式調用合約A的callTwice方法,注意此方法無法标記為@constant,但是不影響外部擷取結果。
@public
def callARaw(source:uint256[3]) -> uint256[3]:
    funcSig: bytes[4] = method_id("callTwice(uint256[])", bytes[4])
    #offset + lengthOfInputArray + inputArray
    uintBytes: bytes[160] = concat(
                                convert(32, bytes32),
                                convert(3, bytes32),
                                convert(source[0], bytes32),
                                convert(source[1], bytes32),
                                convert(source[2], bytes32)
                            )
    full_data: bytes[164] = concat(funcSig, uintBytes)
     # returns byteArray of offset (32 bytes) + length (32 bytes) + uintArray (32 * 3)
    response: bytes[160] = raw_call(
                                self.a,           # Compound Comptroller address
                                full_data,          # funcSig + offset + lengthOfInputArray + inputArray
                                outsize=160,         # outsize = offset (32 bytes) + length (32 bytes) + addressArray (32 * 1)
                                gas=msg.gas,        # Pass msg.gas for call
                                value=0,            # Make sure to not send ETH
                                delegate_call=False # Not delegate_call
                                # static_call=True
                              )
    return self.uintArrayResponse(response)
           

合約代碼都不是很難,也加上了注釋,這裡就不再講解了。

編譯部署

我們可以使用truffle + ganache來在本地編譯部署智能合約并進行測試。truffle 和 ganache的基本使用相信大家都會了,這裡簡單介紹一下truffle編譯vyper智能合約的方法:

  1. 安裝python 3.6以上
  2. 使用pip安裝指定版本的vyper編譯器,例如 `pip install vyper==0.1.0b16
  3. truffle compile

這裡具體編譯和部署的過程就跳過去了,比較簡單的。

測試腳本

既然涉及到Vyper,而Vyper又使用了Python文法,我們就使用python來測試好了。

前提: 安裝web3.py

測試腳本如下:

from contract import A,B,C
from web3.auto import w3

source = [1,2,3]


#計算函數選擇器,參數示例:func = 'callTwice(uint235[3])'
def calSelectorByPython(_func):
    result = w3.keccak(text=_func)
    selector = (w3.toHex(result))[:10]
    return selector


def testNormal():
    r1 = A.functions.callTwice(source).call() #solidity
    r2 = B.functions.callTwice(source).call() #vyper
    print(r1) # 輸出[2,4,6]
    print(r2) # 輸出[2,4,6]


def testC():
    r1 = C.functions.callTwiceA(source).call() #Solidity => Solidity
    print(r1) # 輸出[2,4,6]
    r2 = C.functions.callTwiceB(source).call() #Solidity => vyper
    print(r2)  #會失敗
   
def testCRaw():
    r = C.functions.callTwiceBRaw(source).call() #Solidity => vyper(raw)
    print(r)  # 輸出[2,4,6]

def testBToA():
    r = B.functions.callTwiceA(source).call() # vyper => solidity
    print(r) #會失敗


def testBToARaw():
    r = B.functions.callARaw(source).call() # vyper => solidity (raw)
    print(r) # 輸出[2,4,6]

           

這裡A,B,C是三個合約的執行個體,具體怎麼生成的我這裡不再列出了,有興趣的讀者可以看一下web3.py相關内容。

這裡隻是簡單講一下它的測試接口:

  1. calSelectorByPython 計算函數選擇器,合約調用時是根據函數選擇器來比對對應的函數的。
  2. testNormal 分别使用外部賬号正常調用合約A和C的接口,輸出正确結果
  3. testC分為兩個操作, 操作1在C中直接調用A合約的接口,注釋中提到了是Solidity之間互相調用 ,輸出正确結果。操作2在C中直接調用合約B的接口,注釋中提到了是Solidity調用Vyper,由于動态數組的問題,這裡的參數不會比對,調用失敗。
  4. testCRaw 在C中使用原生(底層)方法來調用合約B的接口,這裡繞過了一些檢查,是以調用成功,輸出正确。
  5. testBToA 同testC第二步相反,我們在B中直接調用合約A的相關接口,這裡同樣由于動态數組的問題,參數不比對,調用失敗。
  6. testBToARaw 我們在B中通過底層方法調用合約A的相關接口,這裡繞過了一些檢查,是以調用成功,輸出正确。

從上面五個測試中,我們可以得出:涉及到動态數組時,Vyper與Solidity智能合約之間互相直接調用會失敗,必須通過底層調用才能成功。

但是底層調用一是複雜費力,二是每個函數都要寫單獨的比對方法,無法複用。

結論:當涉及到動态數組時,請使用Solidity,請使用Solidity,請使用Solidity,重要的事情說三遍!

由于這個原因,Vyper僅适合小衆的項目,或者不需要動态數組的項目。

繼續閱讀