LoginSignup
17
19

More than 1 year has passed since last update.

etherscan で DeFi トランザクションを集計して税計算を行う(だいたいできた. 進捗 89%)

Last updated at Posted at 2020-12-12

優先度高い TODO

  • log 取得方法を調べる
  • log データを解析して Uniswap などでのコインの交換の情報など詳細情報を得る
  • log の取得を自動化する

背景

DeFi 取引したら税計算(損益計算)どうするの...?
(flash loan とか furucombo とか使ったら, トランザクションたくさんでめんどくさすぎでは...)

Ethereum しか使っておらずとりあえず確定申告間に合わせたい(個人向け)

zerion で各トランザクションの時価も含めて履歴を CSV で出してくれます.

とりあえずこの CSV を使って自前で計算するか税理士などに丸投げして間に合わせましょう.

時間をかけてじっくりきちんとやりたい, BSC など他のチェーンも対応したい

ZCash プールマイニングで得られた報酬履歴を zchain でスキャンし課税計算に使う(進捗 95%)
https://qiita.com/syoyo/items/07e6f6360eaf6db94e17

を参考にして, とりあえず etherscan でトランザクションを集計してみます.

etherscan で API 使ってみます.

Ethereumのエクスプローラー(Etherscan)のAPIでアドレスの取引を出力する(イーサリアム)
https://qiita.com/Naomasa/items/32d4382a7e4ff838284d

ありがとうございます.

あと, 最近 etherscan で CSV エクスポートできるようになったようです.
とりあえずは CSV でいいかもしれません.

ここでは, python 使って処理してみます.
(内部では http で API 叩いているだけなので, 慣れたら http request or JSON-RPC に移行でも良いでしょう)

python library

etherscan-python がメンテもされていて pip もあるので, こちらを使います.

$ pip install etherscan-python

残高を確認してみます.
値は wei なので, 10e+18 で割って eth にします.

from etherscan import Etherscan

addr='youraddr'

eth = Etherscan(open("etherscan-apikey.txt", "r").read())

balance = eth.get_eth_balance(address=addr)

# wei -> eth
print(float(balance) / float(10e18))

Voala! お手軽で便利ですね〜.
ether 変換など, float でやると丸め誤差とかでるので decimal module を使うのがよさそうです.

トランザクションを得る.

とりあえず普通のトランザクションを得てみます.

startblock, endblock で取得するブロックの範囲を指定します.
sort でソートを指定します. asc(古いブロックの情報が先)がよいでしょう.

本来ですと日付とかで絞り込めればいいのですがめんどくさい, もしくは難しそう(各ブロックの timestamp で二分技探索とかしないとだめかも)のでひとまず 99999999 で block 全体を指定しています,

from etherscan import Etherscan
import json
import pprint

addr='address'

eth = Etherscan(open("etherscan-apikey.txt", "r").read())

balance = eth.get_eth_balance(address=addr)

txs = eth.get_normal_txs_by_address(address=addr, startblock=0, endblock=99999999, sort='asc')
print(len(txs))

for tx in txs:
    pprint.pprint(tx)
{'blockHash': 'xxx',
 'blockNumber': 'xxx',
 'confirmations': 'xxx',
 'contractAddress': '',
 'cumulativeGasUsed': 'xxx',
 'from': 'xxx',
 'gas': '50000',
 'gasPrice': '1000000000',
 'gasUsed': '21000',
 'hash': 'xxx',
 'input': '0x',
 'isError': '0',
 'nonce': 'xxx',
 'timeStamp': 'xxx',
 'to': 'xxx',
 'transactionIndex': 'xxx',
 'txreceipt_status': '1',
 'value': 'xxxx'}
...

Voala! 結構すぐ結果が帰ってきます.

同様にして, ERC20 token transaction も取得しましょう.

処理する

あとは etherscan の結果とかとにらめっこしながら(etherscan だとコントラクトアドレスには AAVE とかタグがついているので判別やりやすい), それぞれの取引の時価計算などしていけばいいはずです!

transaction

通常のトランザクションです.

internal transaction

コントラクトから自身のアドレスに発行されたものです. 何らかのトークン付与とか?
1 wei が送られてきたのがありました :smiley:

ERC20, ERC721

ERC20 のトークン転送なども考慮しましょう(gas 手数料の計算にも必要)

transaction, internal transaction, ERC20/ERC721 transfers を全部考慮すれば ETH 残高は一致します.

input data のデコード

input data = 関数への引数情報みたいなかんじです.

たとえば Uniswap でどのコインをどれにいくらで交換したい, みたいな情報は transaction の input field に保管されていますが, このデータはバイナリなのでデコードする必要があります.

デコードするためにはフォーマットが既知である必要があります. それぞれの Contract で ABI(フォーマット)が決まっています. この ABI でデコードし, JSON データに戻すとなります.

ABI の定義も JSON で得ることができます. たとえば etherscan で手に入れることができます.

あとはたとえば以下を参考にして

web3 でデコードしてみます.

上記 uniswap の ABI(JSON text) を uniswap.json として保存しているものとします.

特にネットワークを選択する必要はありません(のはず)ので, Web3() で初期化します(Eth が選択されるのかな?)

from web3 import Web3

w3 = Web3()
abi = json.loads(open("uniswap-abi.json").read())
contract = w3.eth.contract(abi=abi)

decoded_input = contract.decode_function_input(tx['input'])
# TODO: JSON 形式にするのを調べる
print(decoded_input)

log のデコード

上記の input だけでは, Uniswap で 0.5 Ether を USDC に変えます, のような情報しかえられず, 実際にどれだけの USDC に変えたのかの情報はありません.

Ethereumのトランザクションを解析してdefiトレードの損益計算する
https://tech.bitbank.cc/20201216/

ありがとうございます. log の取得が必要でした.
log = 関数の返り値みたいな感じです.

etherscan で一応 Log を取ることができます(etherscan-python では対応していないので注意).

topic = ログのリストのようです. トランザクションの input のように ABI でエンコードされています.

log の取得

etherscan では, address(たとえば Uniswap で ETH/USDC をスワップするコントラクトアドレス), もしくは topic のアドレスが必要になります.

したがって自分のアドレスから, 対応する log トランザクションを取得はやややっかいです.

たとえば Uniswap で ETH, USDC を swap しようとすると,

  • Deposit(ETH -> WETH?)
  • Transfer(WETH を送る)
  • Transfer(USDC をもらう)
  • Sync(なんかの同期を取る?)
  • Swap(WETH, USDC 交換)

の 5 個のログがありました.

(時系列は同一トランザクション内なら関係がない(ので, 処理順に並べたいときはちょっとやっかい))

これらでそれぞれ address が異なります.

Deposit, Sync はまあ無視してもよさそうですが, 損益計算するには, Swap か, 2 つの Transfer は必要になります.

たとえば Uniswap ETH/USDC の Swap のアドレスは以下です.

これと, swap したときのトランザクションの block ID を指定して etherscan API にクエリをかけます.

log の解析

eth_event module を使い,
etherscan で得られたログのデータ(JSON)と, コントラクト ABI の JSON を与えれば, ログをデコードできます!
(Etherscan で得られた JSON データを仮定)

import json
import pprint
import eth_event

abi_json = json.loads(open("uni-eth-usdc-abi.json").read())
topic_map = eth_event.get_topic_map(abi_json)
print("topic map:", topic_map)

tx_logs = json.loads(open("eth-usdc-swap-log.json").read())
pprint.pprint(tx_logs['result'])
result = eth_event.decode_logs(tx_logs['result'], topic_map)
pprint.pprint(result)

topic_map は以下のようなのが生成されます.

 '0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822': {'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'},

topic_map ではハッシュが出ますが, これは ABI の項目から算出したもののようです.
(なにかアドレスに関連づいているわけではない)

[{'address': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  'data': [{'decoded': True,
            'name': 'from',
            'type': 'address',
            'value': '0x08c1727f68345d35db1e64fbf606df4f4d03796f'},
           {'decoded': True,
            'name': 'to',
            'type': 'address',
            'value': '0xc0cccec7ba9235e54a5e4baa7b2492258d41095e'},
           {'decoded': True,
            'name': 'value',
            'type': 'uint256',
            'value': 500000000}],
  'decoded': True,
  'name': 'Transfer'},

結果はこんな感じになるので, あとは from or to が自分のアドレスかどうか判断して処理していけばよさそうです!

手順まとめ

  • transaction を取得する
  • Uniswap での swap の場合
    • input デコードのための ABI を取得する(etherscan API で可能)
    • to のアドレスと tx ハッシュあたりから, log 取得のための address, topic address あたりを取得する
    • log を etherscan API(curl)で取得
    • log のデコード
    • 損益を計算する(より正確には Uniswap のコントラクトコードに沿って fee など計算する). もしくはなにか replay 機能を使うなどでコントラクト呼び出しすればいけるか?
  • 他の DeFi サービスは上記 Uniswap の例を参考にしてがんばる

AAVE で lending したもの

たとえば USDC を預け入れした場合は, 同量の aUSDC トークンが付与(自身のアドレスに付与)されます.

ところで, 自身のウォレットの履歴をみたところ, approve のトランザクションが二回発生しているのが気になりました
(最初のはわかるが, 二回目はなんだろう?)

また, aUSDC の付与では複数回で行われることもあるっぽいようです(e.g. 100 aUSDC の場合, 量 0 と 100 の 2 回の deposit があった)

gas 不足で失敗したトランザクション

isError が 1 になっています.
消費した gas を手数料(経費)として扱うことになるでしょうか.

DeFi サービスのデータ(参考用)

最近では AAVE とかでの loan 借り入れ残高を etherscan で確認できるようになったようです.

Screenshot from 2020-12-12 19-55-08.png

ただ貸出(deposit)残高は表示してくれません.

このあたりはそれぞれのサービスの API を叩くなり, コントラクトアドレスの実行履歴(internal transcation?)などからして情報取得する必要があるでしょう.
(e.g. 金利の計算とか, ファーミングされたトークンの残高とか)

yieldwatch(BSC)などでは deposit 中の金利とか表示してくれるので, なにかしらやり方はあるはずです.

流動性ペアトークンの価格(時価)

たとえば Sushi/WETH の SLP(Sushi LP)など.

コントラクトコードを参考にして LP token 量計算式を求めペアを作った時点での時価を求めておく必要があるでしょう(あとで LP 開放したり, 追加で LP 作ったときなどで時価を調整する必要があるため).

法人の場合は, 期末での LP の時価(プール内のトークン量から計算)で計算するのかな?(i.e. Impermanent Loss 込み)

Staking したもの

Staking などでコントラクトに送付したのは, 送付の履歴と対応するトークン(e.g. $SUSHI を stake して $xSUSHI token を得る)しか残りません.
送付履歴をみるなどして, 自前で残高管理をする必要がありそうです.

staking の reward

staking で得られた reward の残高や税計算はどうすればいいかな?
(unstake 時に課税計算?)

rebase トークン

$DIGG みたいな rebase はどうするのかしらん...?
(さらに bDIGG みたいに staking したときの対応とか)

時価計算のための Price oracle

uniswap での交換や, 金利の計算などでそのときのコイン(トークン)の時価などが必要になるでしょう.
CoinGecko とかで価格履歴取得でもいいかもしれませんが, せっかくなので DeFi に適した時価計算したいですね!

多くの DeFi サービスでは ChainLink を使っています.

Does Chainlink store Oracle response for a transaction somewhere on blockchain?
https://ethereum.stackexchange.com/questions/89424/does-chainlink-store-oracle-response-for-a-transaction-somewhere-on-blockchain

Ether on-chain に price oracle の情報が保存されているのでそれにアクセスして頑張って取得すればいけるでしょう!
(ちなみに価格情報は, contract コードを見る限り無理やり文字列にして保存してあるっぽい?)

Ethereum の平均ブロック生成時間は 15 秒くらいですから,

60/15 * 60 (mins) * 24 (hours) * 365(days) = 2102400

と, 高々 210 万ブロック(価格の記録が 1 block づつに生成されたとして)です.
10 万超えてくると, pure python では処理が遅くなりそうですので numpy/scipy/quantlib あたり使ったほうがいいかもですね.

ちなみに,

https://bitni.com/page/chainlink-link-faq#:~:text=A%20new%20block%20on%20the,is%20limited%20to%20about%2070.

Chainlink のブロックチェーン自体も, 15 秒おきに生成されています.

Curve みたいに, トークンペアの場合はどうしたらいいかな?

TODO

  • ERC20 token transaction なども集計する
  • transaction log を解析する
  • flash loan, furucombo など 1 トランザクションで多数の取引をしているものをきちんと処理する
  • DeFi サービス or layer2 とかに記録(たぶん)されているデータの取得方法を調べる(残高 + 金利情報だったり, 付与されたトークン量とか)    各ブロックでの残高を使い正確な損益計算に使う    Archive node を自前で建てるか, archive node アクセスサービス($250/month(!) が多い)を使う   * [x] ローカルで turbo geth で archive node を建てて解析できるようにはしました(2021/04 時点 2TB SSD 必須(1.1 TB くらい領域使う))
  • ChainLink price oracle の情報を取得して時価計算する

税務処理

集計はできそうなことがわかりましたが, 個々の取引の税務処理がどうなるかはケースバイケースでしょう.
詳細は税理士なり国税庁に相談しましょう.

17
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
19