
エアドロbotを支える技術
この記事は仮想通貨botter Advent Calendar 2024 シリーズ2 1日目の記事です。
はじめに
高頻度取引、スイング取引、アービトラージ……さまざまな取引をボット化している人が多く、このAdvent Calendarにも多くのボッターたちが記事を書かれています。本記事では、あまり注目されていないエアドロップを狙ったbotについて、どのような技術が必要なのかを解説していきたいと思います。
銘柄選定について
どの銘柄を狙えばいいのか?といった選定方法についてはあまり技術的な内容ではないので今回書きません。とりあえずKudasaiやDEG鯖で話題になっているものから選ぶといいかもしれません。事実としてこの2つを追っておけば、今年の2大エアドロは獲得できるチャンスはありました。
今年はTaikoのkudasai、ハイリキのDEG鯖ってことで良いですか?
— zakk🦇🔉🐼 (@zk_crypto) November 29, 2024
エアドロップとは
仮想通貨のエアドロップ(Airdrop)とは、プロジェクトが自分たちの新しいトークンや仮想通貨を広めるために、特定の条件を満たしたユーザーに無償で配布する方法のことです。新しいユーザーを獲得し、認知度を高めるために利用されるマーケティング手法の一つで、特にDeFi(分散型金融)やWeb3プロジェクトなどでよく見られます。
ChatGPTさんに説明してもらいました。端的にいうと何かしら一定の基準を満たした人に独自のトークンを配ることです。
開発環境
主に使う言語を決める
自分が手慣れた言語を使うのが一番で、きれいなコードを書くというよりも1人でより早くコードが書ける言語を選ぶべきです。
基本的にアトミックアービトラージや高頻度取引など、実行時の速度を求められるものはありません。例外としてあった場合は、そのときだけ別言語を使えばいいと思います。
自分はPython/Go/Rustといろいろ浮気しましたが、最終的に書き慣れているPythonを主に使うようにしました。
モノレポにする
ソフトウェアエンジニアではマイクロサービスアーキテクチャの普及で、コードを機能ごとに多数のリポジトリに分割すること(ポリレポ)が主流となっていました。そのノリで自分も多くのbotをレポジトリ切り分けて開発していました。エアドロ系以外にもアビトラやml系の残骸が多数あります。
bot開発は基本的に一人でしますし、マイクロサービスにする利点はあまりないので、モノレポで今まで作ってきた資産の使いまわしをしたほうがいいと思ったので、途中からは基本的に1つのレポジトリに適当にコミットしています。

シードフレーズなどを暗号化して使う
.envに生のシードフレーズやプライベートキーを書いてる人いないですよね????
細心の注意をしていればそれでもいいかもしれませんが、以下のようなコードで暗号化・復号するものを用意して、.envには暗号化されたシードフレーズを置いていました。
import base64
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
def encrypt(data: bytes):
key = get_random_bytes(32)
cipher = AES.new(key, AES.MODE_EAX)
ciphertext, tag = cipher.encrypt_and_digest(data)
buff = bytearray()
buff += cipher.nonce
buff += tag
buff += ciphertext
out = base64.b64encode(buff)
return key.hex(), out.decode()
def decrypt(base64_data: str, key: str):
body = base64.b64decode(base64_data.encode('utf8'))
nonce = body[:16]
tag = body[16:32]
ciphertext = body[32:]
cipher = AES.new(bytes.fromhex(key), AES.MODE_EAX, nonce)
return cipher.decrypt_and_verify(ciphertext, tag)
最近だとdotenvといった.envの中身をいい感じに暗号化してくれるものも出てきてるのでそれを使うのも良いかもしれません。
分析・調査
どれだけ利用者がいるのか?期待値はどれくらいなのか?足切りされるならどれくらいか?などのエアドロを狙う対象の事前調査のための分析と、エアドロ確定後より早くclaimして売るための調査が必要になってくるので、その技術について書きます。
UIから見れるデータを分析する
公式が提供しているリーダーボードやStats系ダッシュボードをまず見るのがいいと思います。またそのページでリクエストを投げてるAPIを見てみると他の人の傾向やUIでは表示されない生データがみれたりします。
例として、Elixirのリーダーボードを開くと、UIでは順位とポイントぐらいしかわかりませんが、APIを見るとTVLがどれくらいかも乗っていたりします。


ハイリキのエアドロで話題のlutwidseさんの記事とかも参考になると思います。
オンチェーン分析をする
UIでは提供されていない情報はオンチェーンから分析する必要があります。
代表的な分析ツールとして、Duneがありますが対応していないチェーンがあったりするので、Primo Dataから他のツールを探してみてもいいかもしれません。それでもない場合はRPCと直接やりとりするなど頑張ってください。
Duneなどを使う場合は、複雑なクエリは必要なく、シンプルなクエリで調べることができます。
以下はElixirのLPにどれくらい入れてる人がいるのか?を調べるクエリで、去年1月時点では1000人程度しかいなく、ElixirのValidator登録者数が1万超えているのにLP入れてる人が少なすぎて穴場でした。(実際、この期間のポイント効率はよかったです。)

エアドロClaimの準備
まずどこのサイトでエアドロがclaimできるのかを調べる必要があります。色んなサービスがありますが以下のようなところでサブドメインを調べるのが一番楽です。
サブドメインではなく、別のドメインになっているパターンもあるので、そも場合は、ソースコードに遷移先リンクが埋め込まれていないかを調べたりします。
対象のページが見つかったら、ほとんどのエアドロはmerkle proof形式でclaimすることが多いので、アドレスに対応したproofを返すAPIを探します。
claim時間ギリギリまで公開してないパターンも多いですが、事前に公開されていれば、エアドロclaimしてCEXにtransferするみたいなスクリプトを事前に準備することができます。
エアドロClaim時の実例として$ARBで以前記事書いてたりするので、こちらも参考にどうぞ。
スマートコントラクトとのやり取り
ABIなしでtxを投げる
どう投げるのかはQASHさんのNoteを参考にしてください。
ABIを使わずにスマートコントラクトを取り扱うことは多いので、簡単にinputdataが作れる/デバッグできる関数を作って多用しました。
実際に使っていたサンプルは以下です。
# hexから簡単にinputdataを組み立てる関数
def build_data(method: str, lines: list[int]):
return method + "".join([f"{line:064x}" for line in lines])
# 使用例
data = build_data("0x9aaab648",
[
0xa353a6a9a72bca2cd5c4bc14f7c692bf307c37c9c99e38603be72b732cc7ce16,
0x76a2c8,
0xddf8d9ad5fc4c84af55add8f217dce56972620b50ee454e9482c72f7f0bb2267,
0x6b741d,
])
print(data)
# 0x9aaab648a353a6a9a72bca2cd5c4bc14f7c692bf307c37c9c99e38603be72b732cc7ce16000000000000000000000000000000000000000000000000000000000076a2c8ddf8d9ad5fc4c84af55add8f217dce56972620b50ee454e9482c72f7f0bb226700000000000000000000000000000000000000000000000000000000006b741d
# inputdataからetherscanのフォーマットで表示する例
def print_data_for_etherscan(data: str):
print(f"MethodID: {data[:10]}")
size = int(len(data[10:]) / 64)
lines = [data[10 + 64 * i : 10 + 64 * (i + 1)] for i in range(size)]
for idx, line in enumerate(lines):
print(f"[{idx}]:\t{line}")
print_data_for_etherscan(data)
# MethodID: 0x9aaab648
# [0]: a353a6a9a72bca2cd5c4bc14f7c692bf307c37c9c99e38603be72b732cc7ce16
# [1]: 000000000000000000000000000000000000000000000000000000000076a2c8
# [2]: ddf8d9ad5fc4c84af55add8f217dce56972620b50ee454e9482c72f7f0bb2267
# [3]: 00000000000000000000000000000000000000000000000000000000006b741d
よく使う機能はWrapperを用意する
トークンのERC20やNFTのERC721など、共通のインターフェースをもっているので、何度も使うことがあります。そのようなコントラクトはクラスに定義してあげることで開発効率があがります。
例えばERC20というクラスを定義して、保有量やdecimalを確認したり、approveのtxを投げるなどなど。
以下のコード例を置いておきますが、EVMClientというWrapperが別途あるのでそのまま動きませんのが参考にしてみてください。
class ERC20:
def __init__(self, evm_client: EVMClient, token_address: str) -> None:
self.evm_client = evm_client
self.token_address = token_address
async def balance_of(self, address: str):
data = utils.build_data("0x70a08231", [int(address, 16)])
res = await self.evm_client.call(self.token_address, data)
if isinstance(res, str):
return int(res, 16)
else:
raise ValueError(f"Expected a str type, but received a {type(res)} type.")
async def decimals(self):
data = utils.build_data("0x313ce567", [int(self.token_address, 16)])
res = await self.evm_client.call(self.token_address, data)
if isinstance(res, str):
return int(res, 16)
else:
raise ValueError(f"Expected a str type, but received a {type(res)} type.")
async def allowance(self, owner: str, spender: str):
data = utils.build_data("0xdd62ed3e", [int(owner, 16), int(spender, 16)])
res = await self.evm_client.call(self.token_address, data)
if isinstance(res, str):
return int(res, 16)
else:
raise ValueError(f"Expected a str type, but received a {type(res)} type.")
async def approve(self, wallet: Wallet, sender: str, amount: int):
data = utils.build_data("0x095ea7b3", [int(sender, 16), amount])
txhash = await self.evm_client.send_tx(wallet, self.token_address, data)
return txhash
# 使用例
token = ERC20(client, token_address)
decimals = await token.decimals()
amount = await token.balance_of(wallet.address())
スマートじゃない方法でやる
自分みたいなソフトウェアエンジニアは手動でやることをweb3pyに書き換えてきれいにコーディングしたくなります。
そんなことはせず、公式が用意してくれてるサンプルに一手間加えるだけでもいいんです。
Taikoの最終版にたどり着いた、これでええやんというShell Scriptを置いておきます。
#!/bin/bash
while IFS= read -r line; do
private_key=$(echo "$line" | cut -d' ' -f2)
address=$(echo "$line" | cut -d' ' -f1)
echo $address
curl -s https://rpc.jolnir.taiko.xyz -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' | jq -r .result | printf "%d" $(cat) | sed -i -e "s/BLOCK_ID=.*/BLOCK_ID=$(cat)/g" .env
sed -i -e "s/L1_PROPOSER_PRIVATE_KEY=.*/L1_PROPOSER_PRIVATE_KEY=$private_key/g" .env
sed -i -e "s/L2_SUGGESTED_FEE_RECIPIENT=.*/L2_SUGGESTED_FEE_RECIPIENT=$address/g" .env
sudo docker compose up -d
echo "sleep..."
sleep 1200
done < list.txt
おわりに
これまでいろいろと挙げてきましたが、ある程度プログラミングができる方にとっては「え、これだけでいいの?」と思われる内容かもしれません。ただ、実際に利益が出るかどうかは、プロジェクト側がどのような基準でエアドロップを実施するかに大きく左右される点が、アビトラbotやMLbotなどとは大きく異なる部分だと思います。また、利益を得られるまでには、取り組み始めてから1年以上かかることも珍しくありません。
このジャンルは、根気強く取り組む必要がある一方で、大きな利益を得られる可能性も秘めています。一発当たればリターンは非常に大きいので、ぜひ一度チャレンジしてみてはいかがでしょうか。