Ethereumのトランザクションを解析してdefiトレードの損益計算する

これは ビットバンク株式会社 Advent Calendar 2020 の 16 日目の記事です。

はじめに

こんにちはビットバンクでデータ基盤と会計連携システムの開発・運用をしておりますびっとぶりっとです。
その傍ら暗号資産への投資もしており、今年は特にdefiが盛り上がった事もありdefi(decentralized finance)関連のプロダクトに触れる機会がとても多い1年でした。

uniswapのようなdexで売買をすると問題となるのが損益計算ですが、Ethereum上のdexであればZapperZerionのような可視化サービスを使えば下図の様に取引データを可視化できますし、csvで取得して仕訳する事も可能です。

1-4

直近問題となっているのはEthereum互換のBinance Smart Chainに対応した可視化サービスがないため、PancakeswapのようなBinance Smart Chain上のdexで売買した場合、仕訳が非常に困難となっている事です。

そこで本記事においては、損益計算するためにEthereumのトランザクションを解析して仕訳データを取得する方法を書きたいと思います。

Eventlogの損益計算への応用

まずUniswapでETHからERC20トークンを購入する場合のフローを図示するとこうなります。
この図では5ETHを100SNXに変えたケースを示しています。

2-3

Ethereumのコントラクトで定義する関数はプログラミングにおける関数みたいなもので、あるコントラクトの処理中で複数の別のコントラクトの関数を呼んでいることがよくあります。

例えば下記はUniswap(コントラクトアドレス: 0x7a250d5630b4cf539739df2c5dacb4c659f2488d)を用いてETHでERC20トークンを買う場合の関数です

swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline)

この関数中では以下の別のコントラクトの関数が呼ばれます。

WETH(コントラクトアドレス: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2)のコントラクトの

Deposit (index_topic_1 address dst, uint256 wad)
Transfer (index_topic_1 address src, index_topic_2 address dst, uint256 wad)

購入対象のERC20トークン、本ケースにおいてはSNX(コントラクトアドレス: 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F)のコントラクトの

Transfer (index_topic_1 address src, index_topic_2 address dst, uint256 wad)

Uniswapの実際の交換処理を行う(コントラクトアドレス: 0x43ae24960e5534731fc831386c07755a2dc33d47)コントラクトの

Sync (uint112 reserve0, uint112 reserve1)
Swap (index_topic_1 address sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, index_topic_2 address to)

が呼ばれます。

Uniswapでの交換のフローに上記の関数をあてはめると下図のようになります。

3-1

従って、Uniswapでの売買内容はUniswapのコントラクト実行において

  • WETHのコントラクトのTransferでいくらのWETH(ETH)が送られたか?
  • SNXのコントラクトのTransferでいくらのSNXが送り返されたか?

がわかれば判明します。売買内容が判明すればそれを仕訳に使い損益計算することができます。

じゃあどうやってそれらのコントラクト実行のデータを得ることができるかですが、この段落のタイトルにもあるEthereumのトランザクションに含まれるEventlogを解析することで得ることができます。

Eventlogのデコード

実際にEthereumのトランザクションをpythonのdict形式で取得すると、以下の通りエンコードされた形でEventlogを取得できます。エンコードされたままのEventlogを見てもいくらのETHを送っていくらのUNIを送り返されたのか全然わかりません。

[
 AttributeDict({'address': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'blockHash': HexBytes('0x0b8438c9e56c29f76634e327c5b54a869ca8390012683d4fdc2836dd96384337'), 'blockNumber': 11371253, 'data': '0x0000000000000000000000000000000000000000000000006f05b59d3b200000', 'logIndex': 42, 'removed': False, 'topics': [HexBytes('0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c'), HexBytes('0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d')], 'transactionHash': HexBytes('0x0ee97373996e638f3d758e2395ccfc8500337fb1d497caa85b5744390877f36f'), 'transactionIndex': 57}),
 AttributeDict({'address': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'blockHash': HexBytes('0x0b8438c9e56c29f76634e327c5b54a869ca8390012683d4fdc2836dd96384337'), 'blockNumber': 11371253, 'data': '0x0000000000000000000000000000000000000000000000006f05b59d3b200000', 'logIndex': 43, 'removed': False, 'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'), HexBytes('0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d'), HexBytes('0x00000000000000000000000043ae24960e5534731fc831386c07755a2dc33d47')], 'transactionHash': HexBytes('0x0ee97373996e638f3d758e2395ccfc8500337fb1d497caa85b5744390877f36f'), 'transactionIndex': 57}),
 AttributeDict({'address': '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 'blockHash': HexBytes('0x0b8438c9e56c29f76634e327c5b54a869ca8390012683d4fdc2836dd96384337'), 'blockNumber': 11371253, 'data': '0x000000000000000000000000000000000000000000000031312f8cba45418fff', 'logIndex': 44, 'removed': False, 'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'), HexBytes('0x00000000000000000000000043ae24960e5534731fc831386c07755a2dc33d47'), HexBytes('0x0000000000000000000000002095df2d67b1d2eb71b5af6bdf09bec261cc5a89')], 'transactionHash': HexBytes('0x0ee97373996e638f3d758e2395ccfc8500337fb1d497caa85b5744390877f36f'), 'transactionIndex': 57}),
 AttributeDict({'address': '0x43AE24960e5534731Fc831386c07755A2dc33D47', 'blockHash': HexBytes('0x0b8438c9e56c29f76634e327c5b54a869ca8390012683d4fdc2836dd96384337'), 'blockNumber': 11371253, 'data': '0x0000000000000000000000000000000000000000000095093cb83659e3bfce3b00000000000000000000000000000000000000000000014fc93015ff8082717f', 'logIndex': 45, 'removed': False, 'topics': [HexBytes('0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1')], 'transactionHash': HexBytes('0x0ee97373996e638f3d758e2395ccfc8500337fb1d497caa85b5744390877f36f'), 'transactionIndex': 57}),
 AttributeDict({'address': '0x43AE24960e5534731Fc831386c07755A2dc33D47', 'blockHash': HexBytes('0x0b8438c9e56c29f76634e327c5b54a869ca8390012683d4fdc2836dd96384337'), 'blockNumber': 11371253, 'data': '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000031312f8cba45418fff0000000000000000000000000000000000000000000000000000000000000000', 'logIndex': 46, 'removed': False, 'topics': [HexBytes('0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822'), HexBytes('0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d'), HexBytes('0x0000000000000000000000002095df2d67b1d2eb71b5af6bdf09bec261cc5a89')], 'transactionHash': HexBytes('0x0ee97373996e638f3d758e2395ccfc8500337fb1d497caa85b5744390877f36f'), 'transactionIndex': 57})
]

これをデコードしないとわかりようがないのでデコードします。色々な方法がありますが本記事においては、pythonにはpandas等のデータ解析ソリューションが数多く用意されていることからpython用のeth-eventライブラリを用いた方法を紹介します。

必要なモジュールのインストール

pipを用いてインストールします。

# pip3 install web3
# pip3 install eth-event

コントラクトのABI定義用意

まずはコントラクトのABI定義をjsonで用意します。以下は実際に今回のケースで使われる関数のみを抽出して用意しました。
どうやってコントラクトのABI定義のjsonを用意するかは後述します。

[
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": true,
                "internalType": "address",
                "name": "sender",
                "type": "address"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "amount0In",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "amount1In",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "amount0Out",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "amount1Out",
                "type": "uint256"
            },
            {
                "indexed": true,
                "internalType": "address",
                "name": "to",
                "type": "address"
            }
        ],
        "name": "Swap",
        "type": "event"
    },
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": false,
                "internalType": "uint112",
                "name": "reserve0",
                "type": "uint112"
            },
            {
                "indexed": false,
                "internalType": "uint112",
                "name": "reserve1",
                "type": "uint112"
            }
        ],
        "name": "Sync",
        "type": "event"
    },
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": true,
                "name": "src",
                "type": "address"
            },
            {
                "indexed": true,
                "name": "dst",
                "type": "address"
            },
            {
                "indexed": false,
                "name": "wad",
                "type": "uint256"
            }
        ],
        "name": "Transfer",
        "type": "event"
    },
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": true,
                "name": "dst",
                "type": "address"
            },
            {
                "indexed": false,
                "name": "wad",
                "type": "uint256"
            }
        ],
        "name": "Deposit",
        "type": "event"
    }
]

デコードスクリプトの用意

decode_eventlog.py というファイル名で以下のスクリプトを用意します

import json
from eth_event import *
from web3 import Web3,HTTPProvider

json_open = open('abi.json', 'r')
json_load = json.load(json_open)
topic_map = get_topic_map(json_load)
#print(topic_map)

web3 = Web3(HTTPProvider('https://xxxxxxxxxxxx'))
tx=web3.eth.waitForTransactionReceipt('0x93b42b9c424846038c16a61f82b4e5ce4159d33715701ea0c121c231fb4e064d')
#print(tx)

result = decode_logs(tx.logs, topic_map)
print(result)

HTTPProvider('https://xxxxxxxxxxxx')の部分は適当なEthereum clientのエンドポイントを指定します。infuraが用意してくれるエンドポイントを使うのがとても楽です

デコードスクリプトの解説

Ethereumではコントラクトの関数名や引数の型はまとめてsha3でハッシュ化されています。
例えば

Transfer(address, address, uint256)

という関数であればsha3でハッシュ化されて

HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef')

となります。

以上を踏まえた上で以下デコードスクリプトの解説をします。

json_open = open('abi.json', 'r')
json_load = json.load(json_open)
topic_map = get_topic_map(json_load)

jsonオブジェクトとしてabi.jsonファイルを読み込み、get_topic_map()に読み込んだjsonオブジェクトを渡すことで本来Eventlog中ではsha3で扱われている関数名や引数名を人間が理解できるascii文字列にマッピングできるマッピングデータを用意します。

web3 = Web3(HTTPProvider('https://xxxxxxxxxxxx'))
tx=web3.eth.waitForTransactionReceipt('0x93b42b9c424846038c16a61f82b4e5ce4159d33715701ea0c121c231fb4e064d')

Ethereum clientからwaitForTransactionReceipt()の引数で指定されるトランザクションデータを取得します。

result = decode_logs(tx.logs, topic_map)

取得したトランザクションデータからマッピングデータを用いてEventlogのデコード結果を取得します。

出力結果

実際に用意したデコードスクリプトを実行してみます。

# python3  decode_eventlog.py

すると以下の通りデコードされたEventlogが出力されます。

[{'address': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  'data': [{'decoded': True,
            'name': 'dst',
            'type': 'address',
            'value': '0x7a250d5630b4cf539739df2c5dacb4c659f2488d'},
           {'decoded': True,
            'name': 'wad',
            'type': 'uint256',
            'value': 8000000000000000000}],
  'decoded': True,
  'name': 'Deposit'},
 {'address': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  'data': [{'decoded': True,
            'name': 'src',
            'type': 'address',
            'value': '0x7a250d5630b4cf539739df2c5dacb4c659f2488d'},
           {'decoded': True,
            'name': 'dst',
            'type': 'address',
            'value': '0x43ae24960e5534731fc831386c07755a2dc33d47'},
           {'decoded': True,
            'name': 'wad',
            'type': 'uint256',
            'value': 8000000000000000000}],
  'decoded': True,
  'name': 'Transfer'},
 {'address': '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F',
  'data': [{'decoded': True,
            'name': 'src',
            'type': 'address',
            'value': '0x43ae24960e5534731fc831386c07755a2dc33d47'},
           {'decoded': True,
            'name': 'dst',
            'type': 'address',
            'value': '0x2095df2d67b1d2eb71b5af6bdf09bec261cc5a89'},
           {'decoded': True,
            'name': 'wad',
            'type': 'uint256',
            'value': 907434665775185629183}],
  'decoded': True,
  'name': 'Transfer'},
 {'address': '0x43AE24960e5534731Fc831386c07755A2dc33D47',
  'data': [{'decoded': True,
            'name': 'reserve0',
            'type': 'uint112',
            'value': 703803001951038214229563},
           {'decoded': True,
            'name': 'reserve1',
            'type': 'uint112',
            'value': 6194156376080322294143}],
  'decoded': True,
  'name': 'Sync'},
 {'address': '0x43AE24960e5534731Fc831386c07755A2dc33D47',
  'data': [{'decoded': True,
            'name': 'sender',
            'type': 'address',
            'value': '0x7a250d5630b4cf539739df2c5dacb4c659f2488d'},
           {'decoded': True,
            'name': 'amount0In',
            'type': 'uint256',
            'value': 0},
           {'decoded': True,
            'name': 'amount1In',
            'type': 'uint256',
            'value': 8000000000000000000},
           {'decoded': True,
            'name': 'amount0Out',
            'type': 'uint256',
            'value': 907434665775185629183},
           {'decoded': True,
            'name': 'amount1Out',
            'type': 'uint256',
            'value': 0},
           {'decoded': True,
            'name': 'to',
            'type': 'address',
            'value': '0x2095df2d67b1d2eb71b5af6bdf09bec261cc5a89'}],
  'decoded': True,
  'name': 'Swap'}]

配列の各要素にあるaddress がコントラクトアドレスを name が関数名を表しています。
従って、

 {'address': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  'data': [{'decoded': True,
            'name': 'src',
            'type': 'address',
            'value': '0x7a250d5630b4cf539739df2c5dacb4c659f2488d'},
           {'decoded': True,
            'name': 'dst',
            'type': 'address',
            'value': '0x43ae24960e5534731fc831386c07755a2dc33d47'},
           {'decoded': True,
            'name': 'wad',
            'type': 'uint256',
            'value': 8000000000000000000}],
  'decoded': True,
  'name': 'Transfer'}

上記の部分がWETH(コントラクトアドレス: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2)のコントラクトのTransfer関数だとわかります。3つ目の引数が数量なので 8,000,000,000,000,000,000 がTransferされたWETHの数量だとわかります。18桁は少数部分なので実際には8WETHですね。

 {'address': '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F',
  'data': [{'decoded': True,
            'name': 'src',
            'type': 'address',
            'value': '0x43ae24960e5534731fc831386c07755a2dc33d47'},
           {'decoded': True,
            'name': 'dst',
            'type': 'address',
            'value': '0x2095df2d67b1d2eb71b5af6bdf09bec261cc5a89'},
           {'decoded': True,
            'name': 'wad',
            'type': 'uint256',
            'value': 907434665775185629183}],
  'decoded': True,
  'name': 'Transfer'}

上記の部分がSNX(コントラクトアドレス: 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F)のコントラクトのTransfer関数だとわかります。3つ目の引数が数量なので 907,434,665,775,185,629,183 がTransferされたSNXの数量だとわかります。18桁は少数部分なので実際には 907.434665775185629183SNX ですね。

デコードしたデータから8ETH売って907.434665775185629183SNX買ったということがわかり仕訳をする事ができます。

後はスクリプト化してcsvに吐き出して損益計算もできますし、pandasのdataframeにして損益計算することもできます。

売買以外にもlendingやyield farming等をやっていた場合はデコードした後の処理においてそのトランザクションがUniswapのコントラクト (コントラクトアドレス: 0x7a250d5630b4cf539739df2c5dacb4c659f2488d)実行なのか別の何かなのかによって仕訳処理を変えるだけです。

コントラクトのABI定義のjsonを用意する方法

今回Eventlogのデコードをするにあたって、デコード対象の関数が属するコントラクトは以下の通りです。

それぞれのリンク先を見ると

4-1

このようにContract ABIというのがありますので、全てのリンク先のContract ABIを1つのjsonにまとめることで用意できます。
その際、デコード対象の関数と関係ないABIが含まれていようが重複があろうが問題ないです。

expand_less