涉及到动态数组时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智能合约的方法:
- 安装python 3.6以上
- 使用pip安装指定版本的vyper编译器,例如 `pip install vyper==0.1.0b16
- 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相关内容。
这里只是简单讲一下它的测试接口:
- calSelectorByPython 计算函数选择器,合约调用时是根据函数选择器来匹配对应的函数的。
- testNormal 分别使用外部账号正常调用合约A和C的接口,输出正确结果
- testC分为两个操作, 操作1在C中直接调用A合约的接口,注释中提到了是Solidity之间相互调用 ,输出正确结果。操作2在C中直接调用合约B的接口,注释中提到了是Solidity调用Vyper,由于动态数组的问题,这里的参数不会匹配,调用失败。
- testCRaw 在C中使用原生(底层)方法来调用合约B的接口,这里绕过了一些检查,因此调用成功,输出正确。
- testBToA 同testC第二步相反,我们在B中直接调用合约A的相关接口,这里同样由于动态数组的问题,参数不匹配,调用失败。
- testBToARaw 我们在B中通过底层方法调用合约A的相关接口,这里绕过了一些检查,因此调用成功,输出正确。
从上面五个测试中,我们可以得出:涉及到动态数组时,Vyper与Solidity智能合约之间相互直接调用会失败,必须通过底层调用才能成功。
但是底层调用一是复杂费力,二是每个函数都要写单独的匹配方法,无法复用。
结论:当涉及到动态数组时,请使用Solidity,请使用Solidity,请使用Solidity,重要的事情说三遍!
由于这个原因,Vyper仅适合小众的项目,或者不需要动态数组的项目。