見出し画像

Defiを理解しようシリーズ 第3回: Defi Botterになってみよう ~教えてやる!Botは簡単だ!~

教えてやる!Botは簡単だ!

Botterが何千万も儲けたという話を聞いて「おれもBotterになりてえよ」と歯ぎしりしながら大損してる皆様も多いかと思います。

ダメな自分を変えたかったら勉強しろ!S級Botterを目指せ!

というわけで今回のテーマは「DefiのBotを作ってみよう」です。

戦略を考える

まずは戦略を考えましょう。

今回はサンプルとしてUSDTのdepeg問題でBotを作ってみようと思います。
「USDTのdepeg…?」って方はググってください。

戦略はとてもシンプルで、「USDTのFUDが広まって、みんなCurveからUSDTを一気に抜く。すると清算を巻き込んで価格が一時的にガクっと落ちてまた戻す」

USDT-USDCチャート


という考えのもと、下記のようなBotを作ってみたいと思います。

  1. Curveのpoolを数秒置きに監視する

  2. 価格が一定以下だったらUSDC->USDTにする

  3. 価格が一定上になったらUSDT->USDCにする

サンプルなのでとてもシンプルなモデルにしてます。


環境を準備する

チェーン選定

今回はPolygon mainnetのCurveを使います。こちらです。

本当はETH mainnetを使いたいところですが、サンプルとして使うにはgas代負担が重すぎます…

プログラミング言語

Pythonが一番!Pythonの実行環境を準備しましょう。

Windowsの方(筆者もそう)は、Pythonを生でWindowsに入れるのは非効率なのでやめましょう。WSL2+Ubuntu+VSCodeが超便利です。開発するにはMacが必要とか言う時代は終わった。まだMacで消耗してるの

Databaseとかそういうのとりあえずいりません。普通のPython実行環境だけでOKです。
Web3.py というイーサリアムが操作できる公式ライブラリがあるので、これを使います。

pipenv install web3

RPC

今回は直接Polygon mainnetのRPCをコールします。

EthereumとかだとInfuraかQuicknodeを使うといいです。
フルノードをたてるような猛者Botterは対象読者じゃないぞ!


実装をはじめよう

環境ができたら実装です。おおまかなBot作成の流れは以下になります。

  1. Contractのアドレスを取得

  2. 公式のTech DocかEtherscanのContractのソースコードを読んで仕様を理解する。

  3. Etherscan(今回はPolygonscan)からABIを引っ張ってくる

  4. PythonでABIを読み込んで、Contractを読み書きする

  5. ロジックを作る

  6. ContractにTxを発行する。

今回のBotに必要なContractは、Curveのaave pool (DAI-USDT-USDC)とUSDC ,USDT tokenです。


ソースコード全文

ソースコードです。Pythonのコードはこれだけです。

from ast import Continue
from web3 import Web3
from pathlib import Path

import logging
import json
import schedule
import time

# RPCのURL
RPC_URL = 'https://polygon-rpc.com/'

# Your Address
ACCOUNT_ADDRESS = '**YOUR ADDRESS**'
# Your PK
ACCOUNT_PK = '**YOUR PRIVATE KEY**'


# USDTとUSDCは6
# DAIは18なので注意
DECIMALS = 10 ** 6

# 1回のTrade量 $0.1
TRADE_AMOUNT = int(0.1 * DECIMALS)
# 購入するUSDC-USDTのPrice
BUY_PRICE = 0.90
# 売却するUSDC-USDTのPrice
SELL_PRICE = 0.98

# 何秒おきに実行するか
SCHEDULE_SEC = 10

# スリッページ 1%
MIN_RECEIVE = 0.99

# PoolのIndex
USDT_INDEX = 2
USDC_INDEX = 1

# Contract Addressたち(Polygon mainnet)
# https://polygon.curve.fi/aave
TUSD_POOL_CONTRACT = '0x445FE580eF8d70FF569aB36e80c647af338db351'
USDC_TOKEN_CONTRACT = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'
USDT_TOKEN_CONTRACT = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'

# Tradeの最小値
SELL_MIN_AMOUNT = 0.01 * DECIMALS

# Polygon Chain ID
CHAIN_ID = 137

# ログ初期化
logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

# web3インスタンス生成
web3 = Web3(Web3.HTTPProvider(RPC_URL))

# ABIをjsonファイルからロード
base_path = str(Path(__file__).resolve().parent)
curve_abi = json.load(open(str(base_path) + '/abi/curve.json', 'r'))
erc20_abi = json.load(open(str(base_path) + '/abi/erc20.json', 'r'))

# Contractインスタンス生成
curve_contract = web3.eth.contract(
    address=TUSD_POOL_CONTRACT, abi=curve_abi)
usdc_contract = web3.eth.contract(
    address=USDC_TOKEN_CONTRACT, abi=erc20_abi)
usdt_contract = web3.eth.contract(
    address=USDT_TOKEN_CONTRACT, abi=erc20_abi)


def run_schedule():
    """
        scheduleの実行
    """
    logger.info('bot started..')
    schedule.every(SCHEDULE_SEC).seconds.do(bot_main)
    while True:
        schedule.run_pending()
        time.sleep(1)


def bot_main():
    """
        Botのロジック
    """
    # PoolからPriceをとってくる
    usdc_usdt_price = curve_contract.functions.get_dy(
        USDT_INDEX, USDC_INDEX, DECIMALS).call()
    logger.info('usdc_usdt_price:%s',
                usdc_usdt_price/DECIMALS)

    # 保有USDCを取得
    usdc_amount = usdc_contract.functions.balanceOf(
        ACCOUNT_ADDRESS).call()
    logger.info('usdc_amount:%s', usdc_amount/DECIMALS)

    # 保有USDTを取得
    usdt_amount = usdt_contract.functions.balanceOf(
        ACCOUNT_ADDRESS).call()
    logger.info('usdt_amount:%s', usdt_amount/DECIMALS)

    # 保有USDCがTRADE_AMOUNTを下回ったらエラー
    if usdc_amount < TRADE_AMOUNT:
        raise RuntimeError('Not enough usdc')

    # Priceを下回り、USDTのポジションがない場合
    if usdc_usdt_price < BUY_PRICE and usdt_amount <= TRADE_AMOUNT:
        logger.info('Buying USDT...')
        buy_usdt(TRADE_AMOUNT)
    # Priceを上回り、USDTのポジションがある場合
    elif usdc_usdt_price > SELL_PRICE and usdt_amount >= TRADE_AMOUNT:
        logger.info('Selling USDT...')
        sell_usdt(TRADE_AMOUNT)
    else:
        logger.info('Do nothing.')


def buy_usdt(amount):
    """
        購入:USDT->USDC
    """
    tx_receipt = send_buysell_tx(
        from_coin=USDC_INDEX, to_coin=USDT_INDEX, amount=amount)
    logger.info(tx_receipt)
    # statusが1で成功。エラーの場合は無視
    if (tx_receipt['status'] == 1):
        logger.info('bought USDT!')
    else:
        logger.error('error')


def sell_usdt(amount):
    """
        売却:USDT->USDC
    """
    tx_receipt = send_buysell_tx(
        from_coin=USDT_INDEX, to_coin=USDC_INDEX, amount=amount)
    logger.info(tx_receipt)
    if (tx_receipt['status'] == 1):
        logger.info('sold USDT!')
    else:
        logger.error('error')


def send_buysell_tx(from_coin, to_coin, amount):
    """
        購入・売却のTxの発行
    """
    # 交換用のfunction
    func = curve_contract.functions.exchange_underlying(
        from_coin, to_coin, int(
            amount), int(amount * MIN_RECEIVE)
    )
    # ナンスの取得
    nonce = web3.eth.get_transaction_count(ACCOUNT_ADDRESS)
    # トランザクションを作成
    tx = func.build_transaction({
        "chainId": CHAIN_ID,
        "gas": 1000000,
        "gasPrice": web3.eth.gas_price,
        "nonce": nonce,
    })
    # Private Keyでサインする
    signed_tx = web3.eth.account.sign_transaction(
        tx, ACCOUNT_PK)
    # tx_hashの生成
    tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
    # wait_forでTxが完了まで待つ
    tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
    return tx_receipt


if __name__ == "__main__":
    run_schedule()


ABIをとってこよう

まず、Contractとお話するにはABIがかかせません。
そこでPolygonscanからABIをとってきましょう。

Polygonscan->Contract->codeに下記のようなContract ABIというセクションがあります。これをまるっとコピペして、**.jsonで保存しましょう。

ABI

サンプルではabiディレクトリの下に保存しています。

        curve_abi = json.load(open(str(base_path) + '/abi/curve.json', 'r'))
        erc20_abi = json.load(open(str(base_path) + '/abi/erc20.json', 'r'))

今回使うCurve Poolのものはここです。
https://polygonscan.com/address/0x445FE580eF8d70FF569aB36e80c647af338db351#code

USDCとUSDTはDelegateProxyを使用しています。
なのでDelegate先(implementation()で返されるアドレス)のABIを落としてきましょう。
まあどちらもERC20なので流用できます。

BotのMainロジック

以下がBotのメインロジックです

def bot_main():
  1. USDT-USDCのPriceをpoolから取得

  2. 保有しているUSDC, USDTのamountの取得

  3. USDCが残ってない場合、RuntimeError

  4. Priceが0.9以下だった場合、かつUSDTポジがない:USDTを0.1購入

  5. Priceが0.98以上だった場合、かつUSDTポジがある:USDTを0.1売却

  6. 1-5を10秒おきに繰り返す

とてもシンプルですね!

サンプルなので0.1ドルとしていますが、0.1ドルだとうまくいっても0.008ドルしか儲かりません。
実際に儲けてみたい人は1万ドルくらいはぶちこみましょう!!

注意点として、BOTが建てたUSDTのポジかどうかを判断してないので、USDTを既に保有していた場合、スタート時から売却を始めてしまいます。

実行するときはUSDTを保有しないようにしましょう!


ContractのFunctionのcall


Priceの取得、トークン保有量の取得を行うために、Contractのfunctionをコールします。
この操作にはPrivate Keyはいりません。AddressだけでOKです。

USDT-USDCのpriceは下記のget_dyで取得できます。
ここで言うpriceとは「USDTをX個交換した場合、USDCをいくつ受け取れるか」です。(第1回 Defi理解しようシリーズ 参照)

今回は1USDTを交換したときのUSDCの受け取り量としています。

usdc_usdt_price = curve_contract.functions.get_dy(
        USDT_INDEX, USDC_INDEX, DECIMALS).call()


USDT, USDCの保有量はbalanceOfで取得できます。
ERC20ならどのトークンもbalanceOfで保有量を取得できます。

usdc_amount = usdc_contract.functions.balanceOf(
        ACCOUNT_ADDRESS).call()

ただ、DECIMALSには注意しましょう
戻り値はintで、Decimalsが6の場合、$1 = 10^6になります。
ステーブルコインでもここの値が違ったりします(今回で言うと、Index 0のDAIは18で、USDT/USDCは6です)
ここ間違うと酷い目にあうので事前に確認しましょう

Polygo

トランザクションの発行

下記のsend_buysell_txが実際にContractにトランザクションを送っているところです。

def send_buysell_tx(from_coin, to_coin, amount):
    """
        購入・売却のTxの発行
    """

まずはContractのfunctionを取得します。Tokenの交換はexchange_underlyingで行います。引数は

  1. 交換元トークンのIndex

  2. 交換先トークンのIndex

  3. 交換する量

  4. 最小受取額(交換量×スリッページ)

となります。トークンのIndexは0がDAI,1がUSDC,2がUSDTとなっています。
このIndexはunderlying_coinsから取得できます。


    func = curve_contract.functions.exchange_underlying(
        from_coin, to_coin, int(
            amount), int(amount * MIN_RECEIVE)
    )


次はナンスを取得します。

nonce = web3.eth.get_transaction_count(ACCOUNT_ADDRESS)

Polygonはときどき詰まるので、Txがつまってるとナンスがだぶってエラーを吐くことがあります。実運用するならエラー処理必須です。

次はFunctionからTransactionを作成します。

 tx = func.build_transaction({
        "chainId": CHAIN_ID,
        "gas": 1000000,
        "gasPrice": web3.eth.gas_price,
        "nonce": nonce,
    })

今回はめんどくさいのでlegacyタイプで送ってますが、EIP-1559で送ったほうがいいです。(maxFeePerGasとmaxPriorityFeePerGas)

NFT mint早押しBotを書くならこの辺のガスのロジックは工夫したほうがいいです。


作成したTransactionをPrivate Keyを使って署名し、hashを生成します。

# Private Keyでサインする
signed_tx = web3.eth.account.sign_transaction(
        tx, ACCOUNT_PK)
# tx_hashの生成
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)

今回は生でPrivate Keyをベタっと書いてますが、これは危険なのでやめましょう。外部ファイルにしてEncryptoするとかなんとかしたほうがいいです。

# Your Address
ACCOUNT_ADDRESS = '**YOUR ADDRESS**'
# Your PK
ACCOUNT_PK = '**YOUR PRIVATE KEY**'


Botで仕様するAddressですが、

**絶対にメインウォレットのアドレスを使うのはやめましょう**

Bot用アドレスは最小限のお金のみ入れ、トークンの保管用のアドレスとは分離しましょう。

Private KeyやAPIキーをうっかりGithubの公開レポにpushしてしまったり、設定間違って多額のTx送ってしまったり、FailするTxをループで大量に送ってしまったり等々、Botにはお金失うトラブルがつきものです。


最後に、Txを送信します。wait_forで結果が返ってくるまで待ちます。
成功するとtx_receiptのstatusが1で返ってきます。

** 事前にUSDTとUSDCをapproveしてください。じゃないとFailします **

    tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)

Polygonが詰まっててタイムアウトしてエラーになることがよくあります。
この辺も実運用ではエラー処理必須です。


これ実行したら儲かるんですか?


まず無理です。

そもそもUSDTが0.90を割ってから0.98に戻るようなボーナスステージは今後2,3年はないでしょう。

市場心理がFear or Greedに極端に傾いたときが、Botterの本領発揮です。
色々なところに歪みが生じてそれをかすめ取って儲けることができます。

そのためには常日頃からマーケットを監視して歪みを探す努力が必要です。

「これが儲かる!」といんたーねっつに書いてる手法は書かれた時点でとっくの昔に終わってます。

免責事項

まあ当然ですが、これを動かした場合に生じた損失について、一切責任おいません。

というか実運用してないし、記事用に適当に書いたコードなのでバグってる可能性あります。
バグ報告してくれれば直します!!


まとめ

ポイントさえ理解すればBot作成なんて簡単だということが理解できたかと思います。

気が向いたら&需要があったら

「今回の記事が理解できなかった初心者向けに1から手取り足取り説明する記事」
or
「ほんとに実運用できるレベルのBot作成講座」

でもやりたいと思います。有料にするけどなたぶん!


この記事が気に入ったらサポートをしてみませんか?