涉及到动态数组时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仅适合小众的项目,或者不需要动态数组的项目。

上一篇:solidity学习过程---参数传递类型


下一篇:Solidity的三种转账方式与比较