涉及到動态數組時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僅适合小衆的項目,或者不需要動态數組的項目。