如何调用闭源合约中的方法

说明

有时候,我们需要调用没有开源(verify)的合约。一般情况下,有以下几种解法:

  • 寻找abi,通过abi的到函数原型调用
  • 寻找函数选择器,通过函数选择器反查函数原型。这种情况有一定的概率不一定能够查到函数原型。
  • 反编译合约

寻找ABI

一般项目下,如果项目存在前端交互,那么前端很有可能就会存在abi文件。最为常见的就是NFT相关的项目。因为NFT项目一般都需要在页面上点击mint。一般情况,ABI 会存储在某个JS文件中。通过source面板,根据某些特定的关键词有机会找到abi文件。

获得ABI信息之后,可以利用hashex对abi进行解析,解析可以看到所有的函数以及函数对应的参数。选择对应的函数,填写好参数之后,就可以得到对应的十六进制的input data。

解析函数选择器

函数选择器就是对函数使用kec计算得到的结果,使用etherscan看到的类似于MethodID: 0x095ea7b3中的0x095ea7b3就是函数选择器。所谓函数选择器,其实是对“函数原型”的签名的前4字节(用16进制表示则为8个字符)。

为了节约空间(每笔交易data都要永久存储在链上),对函数调用是经过特定编码后简化的。(同一个合约内不同函数的selector相同的概率很低)。函数选择器的计算方法:被调用函数原型 keccak-256 Hash值的前4个子节。

1
2
3
4
5
from web3 import Web3
func = 'approve(address,uint256)'
result = Web3.keccak(text=func)
selector = (Web3.toHex(result))[:10]
print(selector) # 0x095ea7b3

在etherscan上面查看对应的selector

1
2
3
4
5
Function: approve(address guy, uint256 wad)

MethodID: 0x095ea7b3
[0]: 0000000000000000000000001e0049783f008a0085193e00003d00cd54003c71
[1]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

函数选择器4个字节是不可逆的。所以,所谓的“反向解析”其实就类似于“MD5”破解用的彩虹表。常见的反向解析网站有:

https://www.4byte.directory/

https://raw.githubusercontent.com/ethereum-lists/4bytes/master/signatures/a9059cbb

幸运的话,如果找到对应的函数原型,那么就可以调用了。

如果通过反查的方式也没有找到,那么就需要根据具体的场景猜测函数原型。比如一个是一个nft mint相关的场景,那么函数名称有可能是mintmintwl等等,函数参数有可能是uint256 amountaddress mintto,这个时候只能根据个人的经验猜测了。

反编译合约

常用的反编译合约的网站是:https://ethervm.io/decompile

以官方给出的示例合约为例来展示,0x949a6ac29b9347b3eb9a420272a9dd7890b787a3 。最终得到反编译后的合约是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
contract Contract {
function main() {
memory[0x40:0x60] = 0x60;

if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;

if (var0 == 0x2a0f7696) {
// Dispatch table entry for 0x2a0f7696 (unknown)
if (msg.value) { revert(memory[0x00:0x00]); }

var var1 = 0x0081;
var var2 = msg.data[0x04:0x24] & 0xffff;
var1 = func_00CC(var2);
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = var1;
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
} else if (var0 == 0x5b6b431d) {
// Dispatch table entry for Withdraw(uint256)
if (msg.value) { revert(memory[0x00:0x00]); }

var1 = 0x00c0;
var2 = msg.data[0x04:0x24];
Withdraw(var2);
stop();
} else if (var0 == 0x9f1b3bad) {
// Dispatch table entry for Receive()
var1 = 0x00ca;
Receive();
stop();
} else { revert(memory[0x00:0x00]); }
}

function func_00CC(var arg0) returns (var r0) {
var var0 = 0x00;

if (arg0 & 0xffff != storage[0x01] & 0xffff) { return 0x00; }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x02;
return storage[keccak256(memory[0x00:0x40])];
}

function Withdraw(var arg0) {
if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }

var temp0 = arg0;
var temp1 = memory[0x40:0x60];
var temp2;
temp2, memory[temp1:temp1 + 0x00] = address(msg.sender).call.gas(!temp0 * 0x08fc).value(temp0)(memory[temp1:temp1 + memory[0x40:0x60] - temp1]);

if (temp2) { return; }
else { revert(memory[0x00:0x00]); }
}

function Receive() {
var var0 = 0x00;
var var1 = var0;
var var2 = 0x02;
memory[memory[0x40:0x60] + 0x20:memory[0x40:0x60] + 0x20 + 0x20] = 0x00;
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = msg.value;
var var3 = temp0 + 0x20;
var temp1 = memory[0x40:0x60];
var temp2;
temp2, memory[temp1:temp1 + 0x20] = address(var2).call.gas(msg.gas - 0x646e)(memory[temp1:temp1 + var3 - temp1]);

if (!temp2) { revert(memory[0x00:0x00]); }

var temp3 = memory[memory[0x40:0x60]:memory[0x40:0x60] + 0x20] ~ storage[0x01];
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x02;
storage[keccak256(memory[0x00:0x40])] = temp3;
}
}

使用metamask调用智能合约

metamask调整gas limit

在使用metamask调用智能合约之前,需要先设置gas limit。默认的转账操作是固定的 gas limit是21000。但我们其实是在调用智能合约,其所需的gas limit往往高很多。不单独调整的话,会引发 “out of gas”错误,并浪费一笔gas。

metamask发送十六进制数据

首先在 Metamask 的高级设置中打开 Advanced gas controls(用于手动设置 gas limit)和 Show Hex Data(用于输入 input data)

收款人选择智能合约地址(参照其他同类型交易提取对应地址),在 Hex Data 中输入 input data,并记得在最前面加上”0x”,点 Confirm按钮。

使用Python调用智能合约

使用Python调用智能合约也存在两种方式,分别是直接通过函数选择器调用或者是通过abi的方式调用。

直接调用

直接调用的方式是指直接发送input data。(input data就是函数原型和参数值编码后的十六进制的数据)。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
input_data = "0x10f00ae1"  # 对应claimTestTokens()的函数选择器
w3 = Web3(Web3.HTTPProvider(rpc))
nonce = w3.eth.getTransactionCount(from_address)
params = {
'gas': 50000,
'nonce': nonce,
'from': from_address,
'to': contract_address,
'value': 0,
'gasPrice': w3.eth.gas_price,
'chainId': chainId,
'data': input_data
}
signed_tx = w3.eth.account.signTransaction(params, private_key=private_key)
tx_hash = w3.eth.sendRawTransaction(signed_tx)

由于claimTestTokens()没有任何的参数,所以data直接就是claimTestTokens()的函数选择器的内容。

ABI调用

如果我们有合约对应的abi,可以直接利用abi调用合约中的方法。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def MintUSDU(from_address, private_key):
w3 = Web3(Web3.HTTPProvider("your_rpc_url"))
from_address = Web3.toChecksumAddress(from_address)
contract_address = Web3.toChecksumAddress(USDS)
MINT_ABI = '''
[
{
"type": "function",
"stateMutability": "nonpayable",
"outputs": [],
"name": "mint",
"inputs": [
{
"type": "address",
"name": "recipient",
"internalType": "address"
},
{
"type": "uint256",
"name": "amount",
"internalType": "uint256"
}
]
}
]
'''
gas_price = 8
gas_limit = 80000
amount = w3.toWei(random.randint(1, 20) * 10000, 'ether')
params = {
"from": from_address,
"gas": gas_limit,
"nonce": w3.eth.getTransactionCount(from_address),
"maxFeePerGas": w3.toWei(gas_price, 'gwei'),
"maxPriorityFeePerGas": w3.toWei(gas_price, 'gwei'),
"chainId": chainId,
}
contract = w3.eth.contract(address=contract_address, abi=MINT_ABI)
raw_txn = contract.functions.mint(from_address, amount).buildTransaction(params)
signed_tx = w3.eth.account.sign_transaction(raw_txn, private_key=private_key)
txn = w3.eth.send_raw_transaction(signed_tx.rawTransaction)

参考

  1. https://twitter.com/gm365/status/1521058983838380032
文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/09/13/call-method-from-closed-source-contract/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow