Aaveの清算を観測する(清算BOT開発に向けて)

DEX(分散型取引所)やDeFiのレンディング/証拠金取引で言う「清算(liquidation)は、担保価値が下がって"このままだと貸し倒れ(または不良債権)になる"状態のポジションを、第三者が強制的に解消してプロトコルの健全性を守る仕組み。
その第三者がliquidator(清算人)で、うまく実行できると報酬を獲得できる。


全体像(清算までの流れ)

1.ユーザーがポジションを作る
・例:担保としてETHを預けて、USDCを借りる(レンディング)
・例:担保を入れて、レバレッジでロング/ショートする(DEXのパーペチュアル等)

2.市場が動いて、担保率が下がる/含み損が増える
・すると「この人のポジションは危ない」という状態になる

3.ある閾値を下回ると「清算可能」になる
・「担保率(Health Factor)」や「維持証拠金率」などの条件がプロトコルごとに定義されている

4.liquidatorが清算トランザクションを投げる
・ポジションの一部または全部が強制的にクローズされ、債務(借り)が返済される
・その見返りにliquidatorが報酬を得る

liquidatorの儲け(報酬)の正体

1.清算ボーナス(Liquidation Bonus / Incentive)
清算人が債務を肩代わりして返済した分に対して、担保を割引で受け取れる仕組み。
・例:1000USDCを返済すると、1000USDC相当の担保ではなく清算ボーナスを加えて受け取れる(具体例を後述)
・清算ボーナスからガス代や手数料を引いた残りが利益

2.手数料(清算手数料)として一部がもらえる
DEXのパーペチュアルなどだと、清算時に発生する手数料の一部を清算人に配分する設計もある

3.フラッシュローンを使った裁定っぽい利益
清算を実行するのに必要な資金をフラッシュローンで一時的に借り、
・借りた資金で債務を返済→担保を受け取る
・担保をDEXで売る→フラッシュローンを返す
・残りが利益
という形にすることが多い。

清算の具体例(レンディング系でイメージ)

Aave v3でETHを担保にUSDCを借りる例:
ETH
・LTV:80%
・Liquidation Threshold(LT):82.5%
・Liquidation Bonus(清算ボーナス率):5%(一般的には5%だが、固定ではない)
※LTV:Loan to Value(借入率:担保価値に対して、いくらまで借りていいか)
※LT:割引担保率(どこで生産可能になるか、HF計算用)
※HF:Health Factor

Aさん
・ETH価格:4000USDC
・担保:1ETH
とすると、借りられる最大額は
4000×LTV(0.8)= 3200USDC
・借入:3200USDC
とすると、このときのHFは
4000×LT(0.825)/ 3200 = 1.03125 > 1(LTV < LTとなっているのは借りた直後に即清算を防ぐため)

Aave v3の場合、close factorを下記の基準で遷移させている
0.95 < HF < 1 → 50%
HF <= 0.95 → 100%
ただし、借金が2000ドル未満の場合は全額返済となる。

つまり、0.95 < HF < 1かつ借金が2000ドルより大きい場合は、liquidatorは借入額の50%まで返済できるようになる(50%より少なく返済も可能)。

ETHが3840USDCまで下落したとき、
・担保価値:3840
・割引後担保価値:3840×LT(0.825)= 3168
・借金:3200
→HF = 3168/3200 = 0.99

liquidator
・返済額:1600USDC(3200×close factor:0.5)
・受け取る担保量:1600×1.05(清算ボーナス率:5%) / 3840 = 0.4375 ETH(1680USDC相当)からボーナス部分の10%がprotocol fee(プロトコルの取り分)として引かれ、1672USDC相当のETH受け取りとなる。
・この差額72USDC相当のETHからガス代や手数料を引いた残りがliquidatorの利益となる。

さらに話を続けると、
このときAさんに残った担保資産と借入金は、
・残担保:1 - 0.4375 = 0.5625ETH
・残担保価値:0.5625 × 3840 = 2160USDC相当
・残債:3200 - 1600 = 1600USDC
つまり、清算後のHFは、2160×0.825/1600 = 1.11375 > 1
となり、改善される。

次に、ETHが3200USDCまで下落したとする。
残担保価値:0.5625×3200 = 1800
割引後担保価値:1800×0.825 = 1425
残債:1600
なので、HF=1485/1600=0.928 <= 0.95を満たし、
全額清算(close factor = 100%)ゾーン。

liquidator
返済額:1600USDC(全額)
清算で差し引かれる担保:1600×1.05/3200 = 1680/3250 = 0.525 ETH(1680USDC相当)(また、ボーナスの10%はprotocol fee)

清算結果
残担保:0.5625 - 0.525 = 0.0375 ETH(120USDC相当)
残債:0USDC
→Aさんに担保が残るが、プロトコルは借金を回収し終えた(残債0)ので何も問題は無い。
もし、「全額清算=担保全没収」だったら、借金1600USDCに対して担保価値1800USDCなのに200USDC分の価値を理由なく奪うことになり、法的にも経済設計的にもアウトなので。

Ethereum(Base)の構造

AaveのLiquidationCallイベントを検知するには、下記のような流れになる。
・PythonからRPCノードにeth_getLogsを「LiquidationCall(topic0)」条件で問い合わせる
・RPCノードはBlock HeaderのlogsBloomで、そのブロックに該当ログが含まれる可能性をチェックする
・可能性があるブロックについて、ブロック内txに対応するreceiptsを参照し、各receiptのlogs[]を走査してLiquidationCallを見つける
・見つかったlog情報を返す

この流れの理解度を上げるためには、Ethereum / Baseの構造を確認しておいた方が分かりが良い。

全体像

Ethereum / Base(OP stack含む)のBlock構造は:

Block
 ├─ Header
 │    ├─ stateRoot
 │    ├─ transactionsRoot
 │    ├─ receiptsRoot   ← ★ ReceiptsTrie の root hash
 │    └─ logsBloom      ← ★ 2048-bit Bloom filter
 │
 ├─ Transactions[]   ← tx の配列(RLP)
 └─ (Uncles / Ommer)  ← Ethereum L1のみ(Baseには実質なし)

logsBloomとは、「このブロックにどんなlog(event)が含まれているかもしれないか」を超高速に判定するためのビット要約(Bloom filter)」。

ReceiptsTrieとは

ReceiptsTrieは:
・Block内には無く、ノードのローカルDB(LevelDB / Pebble)に保存される
・各transactionのreceiptを値として持つ

ReceiptsTrie(ブロック外の論理構造)
 ├─ Receipt(txIndex=0)
 │    └─ logs[]
 │         └─ Log { address, topics[], data }
 ├─ Receipt(txIndex=1)
 │    └─ logs[]
 └─ ...

そしてそのroot hashだけが

BlockHeader.receiptsRoot

に入る。

Transaction Receiptとは

各トランザクションは、実行後にReceiptを1つ持つ。
Receiptに入っている主要なもの
・status(成功 / 失敗)
・gasUsed
・cumulativeGasUsed
・logs[] ←ここにevent logがある
・logsBloom(Bloom filter)

logの実体構造

1つのlogは、概念的に下記:

Log {
  address:   コントラクトアドレス
  topics[]:  indexed データ(最大4つ)
  data:      非indexed データ(bytes)
}

address
・このlogを発行したコントラクト
topics
・evnet signature + indexed引数
data
・ABI encodeされた残りの引数

スマートコントラクトからlogは読める?

読めない。
Solidityから:
・emit Event(…) →書けるが
・getPastEvents() →不可(読めない)

logはオフチェーン向けの通信手段

RPCはどうやって検索するのか?

・まずはブロック範囲を決める(pythonから投げたfromBlock~toBlock)
・Block HeaderのlogsBloomで「候補ブロック」をふるいにかける
各ブロックのheader.logsBloomに対して、指定されたaddressと指定されたtopics[]が入っている可能性があるブロックだけを残す
・候補ブロックについて、ノードのローカルDBからそのブロックのtransaction receiptsを読み出し、各receiptのlogs[]を走査して、log.addressとlog.topicsが条件一致するものだけを抽出
・一致したlogだけを返す

清算イベントを観測する(Aave@Base)

レンディングについての清算が何か、またliquidatorの収益源が分かったところで、清算イベントを観測し、liquidatorが実際にどのような動きをしているのかを解析する。

プログラムから、
Baseノードに対して「eth_getLogs」という条件付き検索を投げ(下記コードはこれをやっているだけ)、
RPCノード(Baseフルノード / アーカイブノード)が内部で、
・Block HeaderのlogsBloomをチェック(このブロックに該当topicがあり得るかを高速判定)
・可能性があるブロックだけ、receipts trieをロード
・各receiptのlogs[]を走査
・topics[0] == event signatureかつaddress == poolが一致したlogだけレスポンスに詰めて返す
という段取りで
LiquidationCallイベントをキャッチする。

図解:

[Your Python Script]
        |
        | eth_getLogs (条件)
        v
[RPC Node]
   ├─ BlockHeader.logsBloom (高速)
   ├─ ReceiptsTrie
   │    ├─ Receipt #1
   │    │    └─ logs[]
   │    ├─ Receipt #2
   │    │    └─ logs[]
   │    └─ ...
   └─ filter & return matched logs

pythonコード(LiquidationCallイベントを検知し、ログを保存する)

aave_liquidation_watch_http.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Aave v3 (Base) LiquidationCall watcher via HTTP getLogs
- web3.py v7.x compatible
- dotenv (.env) supported
- Reorg-safe (confirmations)
- Block-range chunking to avoid provider limits
"""

import os
import sys
import json
import time
import signal
import argparse
import logging
from typing import Optional

from dotenv import load_dotenv
from web3 import Web3
from hexbytes import HexBytes


# =====================
# Load .env
# =====================
load_dotenv()

# keccak256("LiquidationCall(address,address,address,uint256,uint256,address,bool)")
TOPIC_LIQUIDATION_CALL = (
    "0xe413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286"
)

# =====================
# Utilities
# =====================
def setup_logger(verbose: bool) -> None:
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s | %(levelname)s | %(message)s",
        handlers=[logging.StreamHandler(sys.stdout)],
    )


def to_checksum(addr: str) -> str:
    if not Web3.is_address(addr):
        raise ValueError(f"Invalid address: {addr}")
    return Web3.to_checksum_address(addr)


def _json_default(o):
    # HexBytes / bytes を "0x..." に変換
    if isinstance(o, (HexBytes, bytes, bytearray)):
        return "0x" + bytes(o).hex()
    return str(o)  # 最後の保険(例外を潰してログが止まるのを防ぐ)

def write_jsonl(path: str, obj: dict) -> None:
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(obj, ensure_ascii=False, default=_json_default) + "\n")


def read_last_block(path: str) -> Optional[int]:
    if not os.path.exists(path):
        return None
    try:
        with open(path, "r", encoding="utf-8") as f:
            return int(f.read().strip())
    except Exception:
        return None


def write_last_block(path: str, block_number: int) -> None:
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(str(block_number))


# =====================
# CLI
# =====================
def parse_args():
    p = argparse.ArgumentParser(
        description="Aave v3 (Base) LiquidationCall watcher via HTTP"
    )
    p.add_argument(
        "--http",
        default=None,
        help="Base HTTP RPC (省略時は .env の BASE_HTTP_RPC)",
    )
    p.add_argument(
        "--pool",
        required=True,
        help="Aave v3 Pool address on Base",
    )
    p.add_argument(
        "--out",
        default="aave_liquidations_base.jsonl",
        help="Output JSONL file",
    )
    p.add_argument(
        "--state",
        default=".state/last_block.txt",
        help="Last processed block file",
    )
    p.add_argument(
        "--confirmations",
        type=int,
        default=2,
        help="Reorg safety confirmations",
    )
    p.add_argument(
        "--step",
        type=int,
        default=10,
        help="Blocks per getLogs query",
    )
    p.add_argument(
        "--poll",
        type=float,
        default=6.0,
        help="Seconds between polls",
    )
    p.add_argument("--verbose", action="store_true")
    return p.parse_args()


# =====================
# Main
# =====================
def main():
    args = parse_args()
    setup_logger(args.verbose)

    http_url = args.http or os.getenv("BASE_HTTP_RPC")
    if not http_url:
        logging.error("BASE_HTTP_RPC not found (set in .env or --http)")
        sys.exit(1)

    pool = to_checksum(args.pool)
    w3 = Web3(Web3.HTTPProvider(http_url))

    # ---- sanity check ----
    try:
        latest = w3.eth.block_number
        logging.info("Connected to Base HTTP. latest block=%s", latest)
    except Exception as e:
        logging.error("Cannot read block_number: %s", e)
        sys.exit(1)

    stop = {"flag": False}

    def _sig_handler(signum, frame):
        stop["flag"] = True
        logging.info("Stopping...")

    signal.signal(signal.SIGINT, _sig_handler)
    signal.signal(signal.SIGTERM, _sig_handler)

    # ---- resume state ----
    last = read_last_block(args.state)
    if last is None:
        last = max(0, w3.eth.block_number - args.confirmations - 1)
        write_last_block(args.state, last)

    logging.info("Start from last_block=%s", last)
    if args.step > 10:
        logging.warning("Alchemy Free: step=%s is too large. Forcing step=10.", args.step)
        args.step = 10
    logging.info("Watching Pool=%s (LiquidationCall)", pool)

    # ---- loop ----
    while not stop["flag"]:
        try:
            latest = w3.eth.block_number
            target = max(0, latest - args.confirmations)

            if last >= target:
                time.sleep(args.poll)
                continue

            from_block = last + 1
            while from_block <= target and not stop["flag"]:
                to_block = min(from_block + args.step - 1, target)

                logs = w3.eth.get_logs(
                    {
                        "fromBlock": from_block,
                        "toBlock": to_block,
                        "address": pool,
                        "topics": [TOPIC_LIQUIDATION_CALL],
                    }
                )

                for log in logs:
                    item = {
                        "chain": "base",
                        "pool": pool,
                        "blockNumber": int(log["blockNumber"]),
                        "txHash": log["transactionHash"].hex(),
                        "logIndex": int(log["logIndex"]),
                        "address": log["address"],
                        "topics": [t.hex() for t in log["topics"]],
                        "data": log["data"],
                    }
                    logging.info(
                        "LiquidationCall detected: block=%s tx=%s",
                        item["blockNumber"],
                        item["txHash"],
                    )
                    write_jsonl(args.out, item)

                last = to_block
                write_last_block(args.state, last)
                from_block = to_block + 1

            time.sleep(args.poll)

        except Exception as e:
            logging.warning("Error: %r", e)

            # Alchemy / JSON-RPC のエラー本文を表示(原因特定用)
            try:
                import requests
                if isinstance(e, requests.exceptions.HTTPError) and e.response is not None:
                    logging.warning("HTTP status: %s", e.response.status_code)
                    logging.warning("Response text: %s", e.response.text[:2000])
            except Exception:
                pass

            time.sleep(args.poll)

    logging.info("Exited.")


if __name__ == "__main__":
    main()

.env

BASE_HTTP_RPC=https://・・・ #JSON-RPCエンドポイント

上記のコードを稼働させて、AaveのLiquidationCallイベントを観測する。

(venv) lisk@DESKTOP-DICUQID:~/liquidation_detect$ python3 aave_liquidation_watch_http.py   --pool 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5   --confirmations 5 --poll 10
2026-01-12 08:37:10,784 | INFO | Connected to Base HTTP. latest block=40692641
2026-01-12 08:37:10,784 | INFO | Start from last_block=40692614
2026-01-12 08:37:10,784 | INFO | Watching Pool=0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 (LiquidationCall)
2026-01-12 18:31:51,955 | INFO | LiquidationCall detected: block=40710476 tx=7ccb21936ae4d3b6193de3c0d6b882cbefbbefe6d08da59439c0c66a9c465b62
2026-01-12 20:15:29,134 | INFO | LiquidationCall detected: block=40713583 tx=fb25e8a229b833d5f26e259d9b1e3b5ea9d471a8af291c8ea066793a8e8cd9e1

しばらく稼働させてみると、いくつかLiquidationCallイベントをキャッチした。
aave_liquidations_base.jsonlに保存される。

aave_liquidations_base.jsonl

{"chain": "base", "pool": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "blockNumber": 40686314, "txHash": "3a2c31ebd8be889390acbb6f6f32fe73bb073ac97d03698556e0e6730c08df52", "logIndex": 887, "address": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "topics": ["e413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286", "000000000000000000000000cbb7c0000ab88b473b1f5afd9ef808440eed33bf", "000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "000000000000000000000000ed03a054539155680bf113dd2f74f031b977f8a1"], "data": "0x000000000000000000000000000000000000000000000000000000002a899e7d00000000000000000000000000000000000000000000000000000000000c7b5c00000000000000000000000056631f0d6a5b6b80d1996c5dc2086e2a62759c160000000000000000000000000000000000000000000000000000000000000000"}
{"chain": "base", "pool": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "blockNumber": 40710476, "txHash": "7ccb21936ae4d3b6193de3c0d6b882cbefbbefe6d08da59439c0c66a9c465b62", "logIndex": 1605, "address": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "topics": ["e413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286", "000000000000000000000000d9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca", "000000000000000000000000d9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca", "000000000000000000000000351a414a64fea9bc188b7e7b33d08863092d451d"], "data": "0x000000000000000000000000000000000000000000000000000000000002d97b000000000000000000000000000000000000000000000000000000000002fa4e000000000000000000000000b2e9b0979ea1f0d4e5565644823998be3829da6d0000000000000000000000000000000000000000000000000000000000000000"}
{"chain": "base", "pool": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "blockNumber": 40713583, "txHash": "fb25e8a229b833d5f26e259d9b1e3b5ea9d471a8af291c8ea066793a8e8cd9e1", "logIndex": 971, "address": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "topics": ["e413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286", "000000000000000000000000d9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca", "000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "000000000000000000000000f57fe1c4205e9f8c3ac1e365498c7579e2321710"], "data": "0x0000000000000000000000000000000000000000000000000000000000038fc2000000000000000000000000000000000000000000000000000000000003b8ca000000000000000000000000b2e9b0979ea1f0d4e5565644823998be3829da6d0000000000000000000000000000000000000000000000000000000000000000"}

清算イベントを解析する

イベント定義(Aave v3)

event LiquidationCall(
    address indexed collateralAsset,
    address indexed debtAsset,
    address indexed user,
    uint256 debtToCover,
    uint256 liquidatedCollateralAmount,
    address liquidator,
    bool receiveAToken
);

Solidityのeventは、トランザクションログ(Log)として保存され、ログは主に次の2つで構成される。
・topics[]:検索用(indexed)
・data:それ以外のデータ

topics[0]:
イベント署名のkeccak256ハッシュ

keccak256("LiquidationCall(address,address,address,uint256,uint256,address,bool)")

topics[1]~topics[3]:
indexed引数が順番に入る

topics[1] = collateralAsset
topics[2] = debtAsset
topics[3] = user

※addressは32バイト左パディングされた形で入る

まとめると、

topics = [
  event_signature_hash,   // topics[0]
  indexed_arg_1,          // topics[1]
  indexed_arg_2,          // topics[2]
  indexed_arg_3           // topics[3]
]

data側には、下記が入る。
→ABIデコードしないと読めない
→indexedほど高速には検索できない

data = abi.encode(
  debtToCover,
  liquidatedCollateralAmount,
  liquidator,
  receiveAToken
)

LiquidationCallイベント

aave_liquidations_base.jsonlの1行目は、

{"chain": "base", "pool": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "blockNumber": 40686314, "txHash": "3a2c31ebd8be889390acbb6f6f32fe73bb073ac97d03698556e0e6730c08df52", "logIndex": 887, "address": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", "topics": ["e413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286", "000000000000000000000000cbb7c0000ab88b473b1f5afd9ef808440eed33bf", "000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "000000000000000000000000ed03a054539155680bf113dd2f74f031b977f8a1"], "data": "0x000000000000000000000000000000000000000000000000000000002a899e7d00000000000000000000000000000000000000000000000000000000000c7b5c00000000000000000000000056631f0d6a5b6b80d1996c5dc2086e2a62759c160000000000000000000000000000000000000000000000000000000000000000"}

メタ情報
・chain:"base"
・pool / address:0xA238Dd80C259a72e81d7e4664a9801593F98d1c5
→Aave v3 Pool Proxy(Base)のコントラクトアドレス
・blockNumber:40686314
・txHash:その清算が起きたトランザクション
・logIndex:同一tx内で何番目のログか(順序)

LiquidationCallの中身
・topics[0] = イベント署名ハッシュ(LiquidationCallの識別子)
・topics[1] = collateralAsset(担保トークン)
・topics[2] = debtAsset(返済した借入トークン)
・topics[3] = user(清算されたユーザー)

このログによると、
・担保トークンはcbBTC(Coinbase Wrapped BTC)(0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf)
・返済した借入トークンはUSDC(0x833589fcd6edb6e08f4c7c32d4f71b54bda02913)
・user:0xed03a054539155680bf113dd2f74f031b977f8a1
※topicsに入っているaddressは32byte左ゼロ詰めなので、末尾40hex(20byte)を読めばアドレス

data(非indexed引数:ABI encode)
・debtToCover(返済した借入額)= 0x2a899e7d = 713662077
・liquidatedCollateralAmount(差し押さえ担保量)= 0x0c7b5c = 818012
・liquidator(清算者)= 0x56631f0d6a5b6b80d1996c5dc2086e2a62759c16
・receiveAToken(担保をaTokenで受け取るか)= false(0)
32byteずつ

トークンのdecimalsを反映した表示
・USDCは6decimals→713.662077USDC
・cbBTCは8decimals→0.00818012cbBTC

つまり、
「Base上のAave v3 Poolで、ユーザー(0xed03a054539155680bf113dd2f74f031b977f8a1)のUSDC借入が713.662077USDC分返済され、担保cbBTCが0.00818012cbBTC差し押さえられ、清算者は0x56631f0d6a5b6b80d1996c5dc2086e2a62759c16、aToken受取はfalse(cbBTCで受け取り)」という清算イベント1件のデータとなります。

LiquidationCallイベントで分かるのはここまで!
その後、liquidatorが受け取ったcbBTCをどうしたか、返済に充てたUSDCはどこから調達したか、などは分からない。

ここで、txhashをBaseScanで開いてみる。

https://basescan.org/tx/0x3a2c31ebd8be889390acbb6f6f32fe73bb073ac97d03698556e0e6730c08df52

From:人/BOTの実行者
このEOAがtxを投げている。
Interacted With(To):実行コントラクト(BOT本体)

EOAが実行コントラクトの関数(0xb87b2e03)を呼んだ、という意味。
つまり、このtxがAaveを直接叩いているのではなく、いったん清算BOT用コントラクトを叩いて、そこで、
・フラッシュローン
・清算
・スワップ
・返済
・利益回収
をまとめてやっている(後述)。

下記が詳細。

①フラッシュローンでUSDCを調達(これが清算の原資)
・Balancer V3: Vault→実行コントラクトに713.662077USDC
②Aaveの清算により、借り手の負債が更新される
・清算されたユーザー→Nullに712.726452(variable debt tokenが焼却)
③Aaveの清算により、借り手の担保ポジションが更新される
・清算されたユーザー→Nullに0.00818011(aBascbBTCが焼却)
※なぜ713.662077と712.726452がズレるのか?
→Aaveは利息インデックス等の内部会計が絡むので、表に見える「返済USDC量」と「焼却される債務トークン量」がぴったりと一致しないことが普通にある。同一tx内で整合は取れている。
④清算者が担保cbBTCを受け取る
・Aave→実行コントラクトに0.00818012cbBTC
(ログのliquidatedCollateralAmount=0.00818012cbBTCと一致)
⑤Aaveのプロトコル手数料(protocol fee)が抜かれてTreasuryに行く
・Null→Aave: Treasury Collector V3に0.00000169(aBascbBTC)

⑥Aaveのプロトコル手数料(protocol fee)が抜かれてTreasuryに行く
・清算されたユーザー→Aave: Treasury Collector V3に0.00003158(aBascbBTC)
※⑤⑥が清算ボーナスの一部がプロトコルに取られる動きに対応。

⑦清算する(Aaveの内部会計上、清算で入ってきたUSDCは「aTokenのミント」という形でプール資産として記録される。
実行コントラクト→Aave: aBasUSDC Tokenに713.662077USDC

⑧清算で得たcbBTCを即売りしてUSDCに戻す(Uniswap v4)
・実行コントラクト→Uniswap V4:Universal Routerに0.00818012cbBTC
⑨中継アドレス→実行コントラクトに738.731895USDC
⑩Uniswap V4: Universal Router→中継アドレスに0.00818012cbBTC
※⑧→⑩→⑨という流れで見ると、実行コントラクトがcbBTCを即売りしてUSDCを中継アドレス経由で受け取っていることが分かる

⑪WETHにするためにUSDCをUniswapに送る
実行コントラクト→Uniswap V4: Universal Router
⑫中継アドレス→実行コントラクトにWETH
⑬Uniswap V4: Universal Router→中継アドレスにUSDC
※これも⑪→⑬→⑫の流れで見ると分かりやすい

⑭フラッシュローン返済
実行コントラクト→Balancerに借りた分と同額を返済する。

最後にInternal Txnsで利益(WETH)を引き出し

WETHコントラクト→実行コントラクト
これはWETHのunwrap(withdraw)による「ETH払い出し」

実行コントラクト→EOA
これは利益分ETHの送金

つまり、清算BOTを作成するには上記のような処理をコード化しなければいけないことが分かる。


いいなと思ったら応援しよう!