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 logspythonコード(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を作成するには上記のような処理をコード化しなければいけないことが分かる。
