見出し画像

はじめてのAtomic Arbitrage with Rust とon-chain Program

はじめに

記念カキコ… Atomic Arbitrage成功…!

初めてのAtomic Arbitrage

今更なんですが、最近はRust x on-chain programによるAtomic Arbitrageに手をつけていました。

actchainを公開した後に「CEX飽きたな・・・」と思ったのがきっかけでした。

時期的には11月の中旬から下旬くらい。50~60$くらいで開発・検証用のSOL多めに買っといたのラッキーだったなあー!って感じの時期です。

多分この辺り。その後こんなに盛り上がるとは全然思ってなかった。

それから約1ヶ月ちょっと手を動かして、なんとか最初のTransactionを通すに至ったので、このnoteではその過程で躓いたポイントや全体的な雰囲気を書き記していこうと思います。
Atomic Arbボットの作成手順書じゃないので、これを読んでもすぐに作れるようにはならないです!

全体を通した所感としては、Atomic Arbボットを作るにあたって技術的に重要なのデバック力だなと思いました。ただ、デバックは総合格闘技なのでぶっちゃけ全部重要かもしれない。

初期調査編

プログラムは多少できるけど、Rustは書いたことなくて、DEXについては何も知らないんだよね

開発開始時点でのスキル感・知識感はこんな感じ。

  • Python:ちょっとできる

  • Javascript/Typescript:完全に理解している

  • C/C++:多少できる

  • Java:多少できる

  • Rust:書いたことない

  • Defi/DEX:触ったことないしWalletも持ってない

  • Solana:STEPNの時ちょっとSOL買ったことあるくらい

  • Atomic Arbitrage:DEX上のなんかすごいアビトラっていう認識

この中で一番役に立ったなぁと個人的に思うのはC/C++の経験。SolanaのRustプログラムを理解したり、自分でRustを書いたりする上で役に立った。C/C++やったことなかったら倍以上の時間がかかっていたと思う。まあ、あとはJavascript/Typescriptかな。SDK使ったり読んだりするのに役立った。

結構多い属性だと思う。プログラムは割と書けるけどDEXは知らん、って人。

それでSolanaってなんなの?

公式ドキュメントとかブログとかnoteとか片っ端から目を通しました。

ぶっちゃけよくわからなかった

以下のブログでBOT開発に関係ありそうな基本的な概念が紹介されていたので、ここで出てきた名前を重点的にいろいろ調べてました。

躓きポイントとされていた部分:

そのため、BOT が必要とするほとんどの情報は、アカウントから取得することになります。

(中略)

同じように、Solana のアカウントも、そのアカウントを作成したプログラムによって、アカウントに含まれるデータの種類やフォーマットはまったく異なるわけです。

したがって、アカウントから自分が欲しいデータを抽出するには、そのアカウントのデータフォーマットを知っているか、少なくともそこから所望のデータを取り出す手段がなければいけません。

おそらく、ここが、Solana で BOT 作りにチャレンジする人の多くが躓くポイントでしょう。

「あ〜ね。よくわかってないけどやったらわかりそう。うん大体わかるな。きっと。」とか思ってました。わからなすぎてどのくらい大変かを推し量ることもできないくらいにわかってない。

お決まりの画像

DEXでの価格の求まり方はこの一連の記事で雰囲気だけ掴んだ。

…が、「じゃあ、そのアカウントとやから情報持ってきて価格計算するってのは具体的にどうやるわけ?」とWhirlpoolのドキュメントを読むと…

whirlpoolのアカウント構成図

いや、わからんて!

こんな感じで「じゃあ具体的にどうやるの?」と踏み込んだ途端何もわからん、みたいな記憶です。

あまりにわからんので割と早い段階で調べるのは辞めてとりあえずやってみっか!やってみればわかるやろ!となった。

1週間もあればできるかなと思っていた

手でSwapしてみたりStakingしてみたり

まずは手でやりましょうが大事と思っている派なので、一通り触ってみることに。
PhantomとかSolflareとかいくつかWalletを作ってみて、SOLを入れたり出したり、USDCに換えたり戻したり、Stakingに入れたり出したり・・・。

僕の中での比較対象が取引所間での送金ではあるけど、SolanaのTransactionの速さと安さには感心しました。なるほどこれは便利そうと素直に思った。

あとは、CEXの集計はCryptactを使っているので、これまで眠らせていたdefi集計機能を試すなどしていました。

Solanaチェーンは対応してなかったので()、BSCチェーンとかPolygonチェーンでも同じようにガチャガチャやってどのように集計されるか試してました。

cryptactのdefi履歴集計

ただ結構な割合で自分で項目(e.g., 資金移動・損失・売却など)を選ばないといけず、これじゃあなあという気持ちになった。もしかしてそもそもそういう仕様?

Solanaチェーンのデータを読み込ませる方法もあるらしいが試してはいない。Solanaチェーンの追加に期待。

Jupiter bot編

とりあえず動くBotじゃどうにもならないんだよ

とりあえず動くものを、とgithub漁りを始めました。
早速見つけたのがこちら。

この時点ではJupiterすら知りませんでした。

READMEに従って動かします。

DEFAULT_RPCってなんやねん・・・

`DEFAULT_RPC`って何や・・・?Solanaにアクセスするものでしょうけど、それってどこなの?っていうね。

(Solana RPCについて調べる旅に出る・・・)

ああ〜これが”ノード”ってやつか〜と認識したのがこのタイミング。

無料で試せるRPCを求めて、よくわからんままにHeliusに辿り着き、よくわからんままエンドポイントを発行して、よくわからんままにDEFAULT_RPCに設定して、再挑戦。

う、動いた〜!

まじで何もわかってなかったので、この時点ですでに結構感動してました。

しばらく放っておくと、Transactionも成功して、0.1ドル増えたりしてました。はい、Atomic Arbitrage達成〜…となればよかったんですが、

親の顔より見たSlippage error

全然成功しねぇ。ひたすらFailしてるぅぅぅ。

これがあれね。アビトラ成立しなかったら全部無かったことにするっていうあれね。とここで理解。

・・・そりゃそうだよね。みんなこれでバンバン儲かるなら苦労しないわな。

Jupiterってすごいんだな

ということで、気を取り直してソースを追い始める。
アビトラのコアロジック(計算とかTransactionとか)はほとんどJupiter内部にあって、このbotはGUIとかハンドリングとか外側だけのことに気づく。

https://github.com/ARBProtocol/solana-jupiter-bot/blob/2e69f23818845cd9070d8f3a482e5ecf732d437b/src/bot/swap.js#L5-L28

Jupiter UIを使ってみる。

量入れると・・・
最適パスっぽいの出てくるやんけ

Jupiterすげーじゃん!っていうかこれじゃん欲しいの!ってナチュラルに驚く。

でもこれみんな使ってたら差分出ないよね・・・つまり・・・

Atomic Arbitrageの核心に気づく

JupiterをRustで自前で書くのがいわゆるAtomic Arbitrageの実態なのでは!?

Solanaわからんし、Rustも書いたことねぇ

その後公開されたMagitoさんのNoteにも書いてありましたね。

コントラクトについては、当時すでに存在していたJupiter Aggregatorを利用するという手も検討しましたが、追加でオンチェーンに載せたいロジックが少なからずあったので、今後の拡張性なども考慮して自作の道を選びました。

https://note.com/magimagi1223/n/ncc28d3f049f6

まあ、Solanaの細かいことは置いておいて、Jupiterの中身を読んで、Rustに移植しつつ、諸々速度改善していけばいいわけだ・・・!!
Solanaの知識はあとからついてくる・・・・・・!!
Rustも書いてるうちに覚えていく・・・・・・・・・!!

まだ1週間もあればできるかなと思っている

Rust bot お祈りビルド編

Jupiterを書き写すといってもゼロからは大変なので、とりあえず土台になるものを探す。
そんで見つけたのがこれ。

  • Rust

  • 最終更新も8ヶ月前と比較的新しい

  • 一応READMEもある

これを一旦動かしてみることにする。パーっとコードを見た感じ半分以上わからなかった。

client/: off-chain arbitrage bot code
program/: on-chain swap program
pools/: dex pool metadata
onchain-data/: analysis of other arbitrage swaps
mainnet-fork/: fork mainnet account states to test swap input/output estimates

各ディレクトリの説明と中身のコードから察するに

  • client/はメインのロジックで裁定パスの算出とTransactionの送信

  • program/は噂に聞くon-chain でなんかやるすごそうなやつ

  • pools/はプール情報のJSONたち

  • onchain-data/は分析って書いてあるからまあいいや

  • mainnet-fork/はうーんよくわからんけど、セットアップ系のInstructionが書いてある

って感じだった。programはできることなら触りたくない。特に意味がわからない。poolsの中身もよくわからん。これらのメタ情報ってどこで取れるん。

しかし、clientだけ動かそうとしてもなんかエラーが出て動かなかった記憶。それにclient/README.mdにもほとんど何も書いてない。

レポジトリを物色しているとmainnet-fork/の中に何やらセットアップ系の指示があるのを発見。何をやっているか全くわかってないが、READMEを読んだり、shell script読んだりして実行した。ただ、今見直してみると、これはLocalnetでの検証用のセットアップぽいので不要だったと思う。

client/setup/というディレクトリも見つけ中身を実行。これももちろん何をやっているのかはわからない。そのままでは動かなかったので動くように修正して実行。

その他諸々直して・・・。

サラッと書いてるけど結構修正必要だった気がする。意図的かどうかわからないが、コメントアウトしてる箇所があったり、参照パスが間違えてたり、引数が違ったりした。

例えば、アカウント情報をセットしてる箇所がコメントアウトされてたり

https://github.com/0xNineteen/solana-arbitrage-bot/blob/main/client/src/main.rs#L230

プールのパスが微妙に違ったり

https://github.com/0xNineteen/solana-arbitrage-bot/blob/d6a43d3bf5eaf7174ea533e57f30ed684c559959/client/src/setup/setup_ata.rs#L60-L71

Chunkが大きくて処理できなかったり

https://github.com/0xNineteen/solana-arbitrage-bot/blob/main/client/src/setup/setup_ata.rs#L159

みたいな細かい修正が必要だった。

ぶっちゃけ何をセットアップしてるのかもよくわからないまま完全に雰囲気で直していた。

直してはビルド、直してはビルド、のお祈りビルドである。

Rustも全然わかっていない。雰囲気でunwrapしてた。

修正を繰り返していくうちに何とか動き、signatureが発行された。

思ったよりすんなりいったなという感想

算出価格が合っているのかとか、プールの状態はどうなっているのかとか一切気にしてない。動けばいいんじゃ。

しょっちゅうTransaction送ってんな〜全部失敗してるけどそういうもんなんだよなきっと〜朝まで回しとけば一つくらい成功するやろ〜、なんてことを考えていた気がする。

(翌朝)

・・・なんかUSDC増えてねえしSOL減ってね?

塵すら積もってねぇ
積もったのはエラーメッセージのみ

The given account is owned by a different program than expected.
これがon-chain programのところで出ていた。察するにデプロイされてるこいつは自分のものじゃないから使えないよってことだと思う。

program/は避けては通れないことをここで理解し、自分でデプロイすることを決意。成功Transactionはすぐそこだと信じて・・・。

ちなみにこの時点ではDevnetとかLocalnetを使いこなせてないので、実弾SOLをガンガン消費して実験検証していた。
Associated Token Accountの払い出しを1個ずつ実行して無駄にガス代取られるなどもしていた。
Solanaのガス代は安くて素晴らしいね(とはいえね・・・)。

だんだんSolanaパイセンの怖さがわかってきた

Rust bot オンチェーンプログラム編

SolanaもRustもわかってないのにフレームワークとか厳しいよ

オンチェーンプログラム編突入!ここでは以下のソースの/programにあるon-chain programの自前デプロイを目指しています。

ここのコードは特に意味がわからない。明らかに何らかのフレームワークの上で書かれている。というのが初見の感想。

deriveってなに?なんのためのシングルクォート?account(mut)って?

今思えば半分以上はそもそもRustがわかってないからわからんやつが多いかな。
ということで Anchorっス。

SolanaもRustもわかってないのにフレームワークとか厳しいよ〜、と思いつつ着手する。
(前から思ってたけど、Solanaのライブラリとかフレームワークのドキュメント薄くないすか。Anchorとかこのドキュメント読んで誰が作れるようになるんだろうと思うん・・・。)

ここまでマジで雰囲気でしかやってない

ソースのいくつかの箇所に現在のプログラムがデプロイされているハッシュ値が書いてあるので、そこを直せば良さそうなんだが・・・・このハッシュ値はどこから来たの・・・?なんでもいいの・・・?

https://github.com/0xNineteen/solana-arbitrage-bot/blob/d6a43d3bf5eaf7174ea533e57f30ed684c559959/program/programs/tmp/src/lib.rs#L7

まあいいや、適当にsha256突っ込んでdeploy叩いてみるべ・・・

デプロイにSOL足りんよ〜8SOLくらい必要だよ〜

具体的なエラーメッセージ忘れた

結局適当な値でいいのか・・・?ってデプロイだけで8SOL!!!???

ウォレットに十分なSOLが入ってなかったことに安堵しつつ、これデプロイするだけで8SOLはなかなか辛いよ、と恐る恐る調べる。

Rent、ってことは、プログラムを削除すれば戻ってきそうではある。

(今更なんだけど)Localnetでの動作確認をして、削除すればSOLが戻ってくるのを確認。

デプロイ!アビトラ!シグネチャ!エラー!

program_idがなんか違うで〜。

確かこんなエラー

デプロイした時に確かになんかのハッシュ値が発行されていた。あ〜あれをdecrare_idに入れろってことかな?

ビルドしたアーティファクトの中にもtmp-keypair.jsonとかいうファイルがあった。怪しい。

吐き出されたものはくまなく探索

keypairファイルの中身をチェック(最初に公式ドキュメント一通り読んでたのでなんかこういうCLIあったよなというのを覚えていた。公式ドキュメントは義務教育。)

発行されたhash値と一致

ビンゴ!こいつら一致させたらええんやろ〜?

デプロイ!アビトラ!シグネチャ!エラー!

Invoked an instruction with data that is too large

エラーが変わるってのは進んだってことや

Versionを下げるという軟弱な選択肢はないんだよ

調べてみるとこいつは根深いエラーのようだった。

Rust 1.66でVecの構造が変わったからAnchor側とのcompatibilityがなくなった的な感じ

Rustのversionを下げるか、Anchorのversionを上げるかの二択を迫られる。
もちろんversionを下げるなどという軟弱な選択肢はない。

威勢とは裏腹に実際このくらい苦しい

Anchor含むSolana周りの依存関係を全て最新版にしてコンパイル通らないところをしらみつぶしに当たっていく。
雰囲気と祈りでひたすらエラーを直していく。

スマートポインタに変わったらしい
謎のビルドスペル:space=8+8+8+1

この時点ですらSolana/Rust/Anchorの理解度はかなり低いものの、C/C++それなりにやったことある人なら戦えるなとは思った。

Account Not Initialized …

さてさてバージョンアップを終え、再挑戦。

デプロイ!アビトラ!シグネチャ!エラー!

アカウントが初期化されてない・・・。

ググってもアカウント初期化すればいいんやでみたいな回答しか出てこない。だからそれをどうやって・・・。

途方に暮れつつソースコードを徘徊してると、on-chain programの中にそれっぽいstructがあることを思い出す:

https://github.com/0xNineteen/solana-arbitrage-bot/blob/d6a43d3bf5eaf7174ea533e57f30ed684c559959/program/programs/tmp/src/lib.rs#L174-L186

これやろ!明らかにこれやろ!これ呼んでないのが原因では!?

https://github.com/0xNineteen/solana-arbitrage-bot/blob/d6a43d3bf5eaf7174ea533e57f30ed684c559959/client/src/arb.rs#L145-L158

呼んでない!やっぱりこいつ呼んでないよ!!ここStartSwapじゃなくてInitSwapStateやろ!!

雰囲気で書き換える。

大切なのは諦めない気持ち

デプロイ!アビトラ!シグネチャ!エラー!

感動のRevertエラー

うおおおおおおお!ついに正常系のエラーに辿り着いた。Atomicity!!!

(これ実は元のコードで合ってるんですよね〜これが。「InitSwapStateは1回だけ別に実行しておく」「on-chain program側でinit_if_neededで制御する」のどっちかでやる必要があった。元のコードは前者を想定していたわけで。なので毎回InitSwapStateのInstructionを送ると、2回目のTransactionから「このアカウントはすでに初期化済みです」みたいなエラーが出るんですよね。この後しばらくそのエラーが出なかったのは、swapまで成功してないので初期化そのものもなかったことにされてたからなんですねぇ〜。Atomicityです。Atomicity。もちろんこの時はそんなこと知る由もないわけですが。)

(ちなみに一応ここで出てきてるpdaについて調べたけどマジで1ミリもわからなかったよねこの時点では。)

永遠のRevert

ついにAtomic Arbitrageの醍醐味であるAtomicityを体感。
ここまで来ればあとは成功を待つだけや。

・・・

いつまで経っても成功しない。

さてさて臭いものには蓋をしたいものでして、ずっと頭の中にはあったものの見ないフリ、気づかないフリをし続けていた仮説がありました。

裁定パスの計算が合ってない説。

とあるSaberプールでの推定スワップ価格をUIのそれとと恐る恐る見比べてみる。

・・・あってないよね〜・・・。

勝手に自分でやられてるだけなんだけど気分はこういう感じだった

Rust bot プールチェック編

前回のオンチェーンプログラム編では、オンチェーンプログラムを自前でデプロイすることができました。しかし、浮かれるのも束の間。いくら待ってもTransactionが成功しません。それもそのはず。裁定パスの算出に使っている計算結果が実際のものと一致していないのだから!

AMMってなんなの?

最初の方に読んでたこの記事を再度じっくり読み直しつつ、AMMの解説記事を漁る。

実装があるDEXでどう計算してるのかチェックする。

ここでなんか嫌な予感がする。
Aldrinはなんか雰囲気怪しいし、MercurialはMeteoraに変わったっぽいし、Orcaの実装はLegacy版っぽいし、Serumは板取引で意味わからんし・・・。唯一現役AMMっぽい感じがするのがSaberだけなんだが・・・?

なんかあやしい気がしたaldrin

この嫌な予感は後で的中するのだが、この時点ではまあとりあえずSaberだけでも動けばなんとかなるやろって思い込んで進めることに。

この時点でのAMMの理解はこんな感じだった:

  • AMMにはいくつか種類があるがそんなに数は多くない

  • 同じAMMを使っていれば異なるDEXでも同じ値が出てくる

  • パラメーターはAMMによって異なる

となると価格が合わない原因としては:

  1. DEX側の実装が変わった

  2. AMMの計算ロジックにバグがある

  3. パラメーターが違う

ですかね。

MercurialとかOrcaはそもそもDEXが刷新されてそうだから1かな。
AMMの計算ロジックはぶっちゃけよくわかってないからとりあえず2はないとして話を進めたい。
ってことで、3に絞って、現役で動いてそうなSaberのパラメーターをチェックをするか〜、という運びに。

プールのメタ情報をJupiterでチェックしようよ

とあるSaberプールのメタ情報がこんな感じ。

{
  "pool_account": "3nesbuhAwCMGtsyypYtg4oRPwJ3FmHnytC5bqskhPh1x",
  "authority": "AAFwoEvGaFLW6V8E4u4NBGmGnExaboMdJTcguxDrRvDt",
  "pool_token_mint": "Lirav2gsqs7jL1PFRUBp8uKACT8LYjDBV8c6nzchoer",
  "token_ids": [
    "9QBTKuSCDaJjtxYnYcVzoiKENMdJ5DRei5ZUCEeWyZnj",
    "A94X2fRy3wydNShU4dRaDyap2UuoeWJGWyATtyp61WZf"
  ],
  "tokens": {
    "9QBTKuSCDaJjtxYnYcVzoiKENMdJ5DRei5ZUCEeWyZnj": {
      "tag": "",
      "name": "",
      "mint": "9QBTKuSCDaJjtxYnYcVzoiKENMdJ5DRei5ZUCEeWyZnj",
      "addr": "GA7UoHChxwpLd1Sx4pK5jVA575zWAxNveJPPkayrvJV7",
      "scale": 6
    },
    "A94X2fRy3wydNShU4dRaDyap2UuoeWJGWyATtyp61WZf": {
      "tag": "",
      "name": "",
      "mint": "A94X2fRy3wydNShU4dRaDyap2UuoeWJGWyATtyp61WZf",
      "addr": "5rSTM7rrLvoWerY897n3KhbpuQyHV8ZMuS1pHJNAwMnQ",
      "scale": 6
    }
  },
  "target_amp": 250,
  "fee_numerator": 4,
  "fee_denominator": 10000,
  "fee_accounts": {
    "9QBTKuSCDaJjtxYnYcVzoiKENMdJ5DRei5ZUCEeWyZnj": "EJHrPrSogaFUehHo1hWWM9ii1ZZbABE9MLd3kBCo8CVY",
    "A94X2fRy3wydNShU4dRaDyap2UuoeWJGWyATtyp61WZf": "D9N4wzL1JhBbPbW2UoWQuxQ3uu6DvYUnCMYwT6QFemWf"
  }
}

target_amp・fee_numerator・fee_denominatorあたりが怪しいな〜。

Jupiter内のコードをチェックすると、DEX毎にAMMクラスが定義してあって、AMMクラスが計算に必要なパラメーーターを持っていて、パラメーターの初期化はプール毎の状態に基づいて行われるって感じだった。

class SaberAmm {
  constructor(stableSwap) {
    this.stableSwap = void 0;
    this.id = void 0;
    this.label = 'Saber';
    this.shouldPrefetch = false;
    this.tokenAccounts = [];
    this.calculator = void 0;
    this.stableSwap = stableSwap;
    this.id = stableSwap.config.swapAccount.toBase58();
    this.calculator = new Stable(TWO, calculateAmpFactor(this.stableSwap.state), [ONE, ONE], new Fraction(this.stableSwap.state.fees.trade.numerator, this.stableSwap.state.fees.trade.denominator));
  }

  getAccountsForUpdate() {
    return [this.stableSwap.state.tokenA.reserve, this.stableSwap.state.tokenB.reserve];
  }

  update(accountInfoMap) {
    let tokenAccountInfos = mapAddressToAccountInfos(accountInfoMap, this.getAccountsForUpdate());
    this.tokenAccounts = tokenAccountInfos.map(info => {
      const tokenAccount = deserializeAccount(info.data);

      if (!tokenAccount) {
        throw new Error('Invalid token account data');
      }

      return tokenAccount;
    });
  }

  getQuote({
    sourceMint,
    destinationMint,
    amount
  }) {
    if (this.tokenAccounts.length === 0) {
      throw new Error('Unable to fetch accounts for specified tokens.');
    }

    const feePct = new Decimal(this.stableSwap.state.fees.trade.asFraction.toFixed(4));
    const [inputIndex, outputIndex] = this.tokenAccounts[0].mint.equals(sourceMint) ? [0, 1] : [1, 0];
    this.calculator.setAmp(calculateAmpFactor(this.stableSwap.state));
    const result = this.calculator.exchange(tokenAccountsToJSBIs(this.tokenAccounts), JSBI.BigInt(amount), inputIndex, outputIndex);
    return {
      notEnoughLiquidity: false,
      inAmount: amount,
      outAmount: JSBI.toNumber(result.expectedOutputAmount),
      feeAmount: JSBI.toNumber(result.fees),
      feeMint: destinationMint.toBase58(),
      feePct: feePct.toNumber(),
      priceImpactPct: result.priceImpact.toNumber()
    };
  }

  createSwapInstructions(swapParams) {
    return [createSaberSwapInstruction({
      stableSwap: this.stableSwap,
      ...swapParams
    })];
  }

あーあるねあるね。ampあるね。

おもむろにブレイクポイント打って・・・

デバックでここやろ!ってなった時こういう感じになりがち
stableswapSdk.calculateAmpFactor(this.stableSwap.state)
> 50
this.stableSwap.state.fees.trade.numerator.toString()
> 4
this.stableSwap.state.fees.trade.denominator.toString()
> 10000

ampがなんなのか1ミリもわかっていないが、とにかく値が違うことはわかった。

他にもいくつかチェックしたところ、target_amp・fee_numerator・fee_denominatorが違うケースがちらほらあった。
メタ情報ファイルをJupiterを使って生成し直したところ、計算結果がUIのそれと一致するようになった。やったぜ。

そもそもパスすら存在しない

さてSaberの価格が合うようになったので、Saberプールのみを対象に意気揚々とアビトラプログラムを起動するも、今度は全くtransactionが送られない。

対象のプールをよく見てみると

  • USDC → ANY

  • ANY → ANY

  • ANY → USDC

のANY→ANYにあたるプールがない。同じDEX内で裁定機会とかあんのかとは思っていたが、そもそもパスすら存在しないとは・・・(Jupiterから取れるやつにないだけかもしれんが)。

さっきの嫌な予感が見事的中することになる。

Aldrinはなんか雰囲気怪しいし、MercurialはMeteoraに変わったっぽいし、Orcaの実装はLegacy版っぽいし、Serumは板取引で意味わからんし・・・。

遅かれ早かれ通る道と腹を括り、 新規DEX開拓の道に進むことを決意。もう雰囲気だけではなんともならないことも覚悟。

もうヤケクソ

Rust bot Whirlpool開拓編

ここに辿り着くまでは誰かの作ったものをなんとか動くようにするだけだったので雰囲気でなんとかなってましたが、ここからはそうはいきません。Solana / Rust / DEX諸々の理解を深めながら進めていく必要が出てきました。

Whirlpoolに決めた!

この時点で考えていた方針:

  • 一旦はとりあえず動くものを目指す

  • その後はJupiterをお手本にして対応DEXを増やしていく

  • 高速化や効率化は最後の最後で取り組む

プールが少ないと裁定パスがそもそもないという経験があったので、大きいDEXにしようと思っていました。

DEX SCREENERで取り扱っているSolana DEXは3つ:

  • Orca (以降Whirlpool)

  • Raydium

  • FluxBeam

少ねえなぁおい!と思った

まず「僕でも名前の聞いたことがあるRaydium」or 「一番左側に出ているWhirlpool」の2択に絞り、それぞれ調べた後、OSSを謳っているWhirlpoolにしました(ロゴも可愛いし)。「最終的には中身全部読めばなんとかなるやろ」という軽い気持ちです。

Solana再入門そしてRust修行

この時点で僕はSolanaチェーンの仕組み(アカウントとは何か、transactionはどう発行するのか、スワップはどのように処理されるのか、とか)についてほとんどわかってませんでした。

ドキュメントをいくら読んでも頭に入ってこないので、いいチュートリアルないかな〜と物色していたところ見つけたのがyugureさんによるチュートリアル。

Whirlpoolでいくって決めた後に発見したんですけど、これがあるだけでWhirlpoolから始めるのに強い動機になりますね。神教材でした。

このチュートリアルはTypescriptで書かれている+Whirlpoolが用意したSDKを使うので、単にやるだけだと(RustでAtomic Arbitrageをやろうとしている自分には)足りませんでした。

ので

上から順にスワップに至るまでのチュートリアルをRustで書き直してました。これが良かった。

実際にやったチュートリアルは 011, 012, 013, 014, 021, 022の5つだけです。

  • 01系: Solanaチェーンの基本的なデータ構造と操作

  • 021:スワップ価格算出

  • 022:スワップ実行

って感じ。やっぱり手を動かして、自分で中身や挙動を確認すると、今までドキュメント読んでほ〜んって思っていた箇所が一気に身についていきました。

021, 022ではWhirlpool sdkの中身も翻訳する必要があるので結構大変でしたね。この辺りにWhirlpoolが実際に使っているであろうRust版の関数も落ちているのを見つけていましたが、依存関係を切り出すのがめんどくさかった(Cargoでうまいことできるのかもしれないがそれも知らなかった)+ Rustの修行ってことで自分で書きました。

021の価格算出の翻訳はまず量が多くて大変だった。

Poolのアカウント情報を読み取れない。

Whirlpoolのプールデータを読み込むためのstructを書いてる時のこと。元のソースのデータ構造を見ながら、手元に書き出していく。しかし、いざfetchしてきたbyte arrayをdeserializeしようとすると、

8byteなんか合わへんで〜。

正確なエラーメッセージは忘れた

って具合で落ちてしまう。何度見直しても項目数とそれぞれの型は合っている。

Anchorがなんか入れてる気がするなあ〜。

まあいいや一番後ろに8byte分のフィールド入れてみるか→エラー。
じゃあ一番最初に入れてみるか→通った!

後から知ったけど、Anchorは最初の8byteを使ってインストラクションの判定をやってるみたいで、この8byteはそれだった。


022のスワップ実行も結構ハマるところがあった。

AnchorでデプロイされたプログラムをAnchorクライアントなしでどうやって呼ぶんだ〜?

どうしてAnchorクライアントを使いたくなかったのかは話すと長いので省略しますが、自分のプログラムにTransactionを送るのと同じ要領でWhirlpoolのプログラムにTransactionを送るとこんなエラーが出た。

InstructionFallbackNotFound. 
Error Number: 101. Error Message: Fallback functions are not supported.

Anchor Clinetを使ってやるとできるので、Anchorが内部でなんかやってるはずなんや、と見込み、色々調べる。そしてこれを見つける

What you can do is fetch the instruction discriminator then append it to your data in the first 8 bytes.

さっきの8byteこれやん!
この記事にある通り、最初の8byteに当該の情報を詰め込んでやることでエラー回避成功・・・ふぅ。

パラメーターどうやって渡すんや〜?signerとかwritableって何〜?

Transactionの作成はJupiterのInstruction生成部分は処理が込み入っていてわかりにくかった。多分複数DEXの差分吸収をしてるからだと思う。

なのでTypescriptのチュートリアルの処理とパラメーターの値や順番を見比べっこ。

事前計算の部分はJupiterを写経したので、チュートリアルと微妙に値の持ち方が異なっており、Instruction作成に手間取った。

今書いてみるとなんで手間取ったか不思議なんだけど結構手間取ったなあ〜・・・。

覚えているのだとこんなところ。他にも色々あった気がする。1個ずつエラー出て、調べて、直して、ビルドして、エラー出て、調べて、直して、ビルドして・・・みたいなのをひたすら繰り返しましたね。

ビルド時はマジで祈ってる

この修行が終わる頃にはSolanaチェーンの仕組みはそれなりに理解していて、Rustもスラスラ書けるようになってた気がするのでおすすめです。

愚直に書くことが上達への一番の近道なんですよね結局

Jupiterを参考にしてDEX開拓していくつもりなので、Whirlpoolの処理もJupiter上のものを翻訳しました。Jupiterが使っているWhirlpool SDKと上のチュートリアルで使っているWhirlpool SDKの中身が微妙に違いましたが、大体同じだったのでここはそんなに大変でなかった記憶です。

ちょっと話それますが、みなさんcopilot使ってます?前半雰囲気で戦えたのはあいつがいたからで間違いないんですが、いざ覚えようとするとマジでなんも身につかないのでこの辺りではオフにしてました。

Whirlpoolの実装

元コードにWhirlpoolの実装を加えていきます。

実装項目としては

  • オフチェーン実装

    • プールメタ情報の取得

    • 価格算出の実装

    • 自身のオンチェーンプログラムにTransaction発行

  • オンチェーン実装

    • WhirlpoolにTransaction発行

チュートリアルでやったやつ移植してくだけやろ〜瞬殺〜とか思ってましたがこれが意外と大変でした。

ポイント①:Whirlpoolの価格算出に必要な情報の取得に依存関係がある

すでにあったDEXはプールアカウントのデータ内に価格算出に必要な情報が全て詰まっていたので、取得は1回で済むしコードもそのようになっていました。一方で、Whirlpoolはプールアカウントのデータを使って、Tick状態を取得するので2回データ取得を実行しなければなりませんでした。インターフェース含めて修正が必要になったので地味ですがだるかったです。

ポイント②:動かないプールがある

あるだろうと思っていたが、やっぱり結構あった。翻訳も完コピしたわけではなくて「こういうことよね?」と意訳して箇所もそれなりにあったので、いろんなプールでテストすると動かない!ってことが結構あった。Whilrpool SDKと動作比較しながらのバグ修正はだるかった。一部のプールはWhirlpool SDKでも動きませんでした。liquidityがないとかそんな感じだった。

・・・もっと色々あった気がするけどこんなもんか。大変というよりだるかったらしいです。

そして初めてのAtomic Arbitrage

ようやく冒頭のスクショに戻ってきました。感動しました。

散っていったSOLに

QA

Q. どれくらいやってた?

期間としては1ヶ月ちょいくらい。平日の夜、子供が寝た後に余力があれば2・3時間まったりと。

Q. C/C++はどれくらいできるの?

そんなにできないっす。ポインタ・参照渡し・テンプレートとかその辺りの基本的な道具を理解して使えるくらい。

Q. Rustはどうだった?

みんながRust、Rust言う理由は書いてみてやっとわかった。確かに心地いい。所有権周りはC/C++書いてたからか躓かなかった。Javaだけ〜とかpythonだけ〜だとまあ戸惑うかも(特に後者の人)。個人的にはExceptionないっていう仕様の方が戸惑った。

Q. 今更やってどうすんの?

ですよね〜。この先さらに開発するかどうかもわからないっす。まあでも面白かったのでやる気がする。儲かるレベルになるかどうかは知らない。

Q. 記事後半の方がミソっぽいのに雑になってない?

すいません・・・書き疲れました・・・。
ちょっと書き加えました!質問があればぜひ!

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