![見出し画像](https://assets.st-note.com/production/uploads/images/164814641/rectangle_large_type_2_96ea93048d2abd1831690bb2ddb41714.jpeg?width=1200)
初めて作ったdex botを振り返る
この記事は仮想通貨botter Advent Calendar 2024 シリーズ1の8日目に掲載させていただく記事です。
はじめに
こんにちは。ちゃまと申します。
1年半程前から、主にアービトラージを主体としたbotを作成しています。
先月は初めてA級となりました。バブルってスゲー。
嬉しかったので振り返りnoteでも書いたろ!とウキウキ思っていたのですが、周りは億ドロが沢山と聞き、調子乗ってサーセンってなりました。
コッカラッス…。
界隈の難しい話はまだまだ分からないことが多いですが、せっかくなのでアドカレには参加させてもらいました!
今回はstAPTというLSTを発行しているdittoというプロトコルについて書いてみます。
Aptosチェーン上のプロトコルで、Aptosの価格が盛り上がっていた今年の3月ごろにはちょっとだけ鞘がありました。(それより前から鞘はあったようですが私が気づいたのはこのころでした。)
この鞘に気づいて当時本当にしょぼいbot(?)を作ったのでその時のお話です。
記憶が正しければ自分が初めて作ったdex botだと思います。
初めて作るdex botがAptosチェーン上なんてやつおるんかって感じですね…w
一応書いておきますが今から見に行っても鞘はないですしこのプロトコルは色々とアレなので触らないほうがいいと思います😇
本題に入る前にまずはdittoのstAPTがどのようなトークンなのか見ていきましょう。
stAPTとは
dittoが発行している非リベース型のリキッドステーキングトークン(LST)
償還するにはInstant UnstakeとDelayed Unstakeがある(後述)
補足: リキッドステーキングトークン(LST)について
トークン(主にそのチェーンのネイティブトークン)をステークするともらえる預かり証トークン
非リベース型とは元のトークン建てで価格が上がっていくタイプのLST
さっぱり分からないという方は以下の動画が参考になると思います。
また、念のため書いておくとAptos上で流通しているstAPTというトークンは2種類あり、Amnisというプロトコルが発行しているstAPTと今回のdittoが発行しているstAPTがありそれぞれ別物です。
某鯖で20APT教と言われていたのは前者の方で今回のstAPTとは異なります。
stAPT(Amnis)のアドレス: 0x111ae3e5bc816a5e63c2da97d0aa3886519e0cd5e4b046659fa35796bd11542a::stapt_token::StakedApt
stAPT(ditto)のアドレス: 0xd11107bdf0d6d7040c6c0bfbdecb6545191fdf13e8d8d259952f53e1713f61b5::staked_coin::StakedAptos
以降、断りなくstAPTと表記した場合は、常にditto上のstAPTを指します。
stAPTの仕様
stAPTをAPTに償還するには上記に記載した通りInstant UnstakeとDelayed Unstakeがあります。(まあ実際には1種類しかないんですが、これについては余談として後述します)
Instant Unstakeは即時に償還可能
Delayed Unstakeは償還までに30日かかる
名前の通りですね。
さて、Instant Unstakeがあるなら、わざわざ償還時に30日待ちたい人などいないので全員Instant Unstakeを利用すると思いますが、Instant Unstakeは以下の制約があります。
償還時にfeeがかかる
償還できる量に限りがある
実際dittoのドキュメントには以下の記載があります。
Ditto will offer an instant unstake option for users in exchange for a fee. This will involve a ringfenced amount of APT held separately for this purpose. It is also worth noting that this pool can be depleted and in this case the feature is no longer available. If this occurs the feature will remain inaccessible until the protocol rebalances its pools.
それぞれもう少し詳しく見ていきます。
償還時のfee
instant unstake時にfeeを課すことで、多くの人がunstakeに殺到しないような仕組みになっています。
このプロトコルのコードは公開されていないのですが、オンチェーンにアセンブリとバイトコードはあるのでそれをこねると手数料の計算式は以下のようになっていることがわかりました。
public fun calculate_fee(
amount: u64,
min_fee_bps: u64,
max_fee_bps: u64,
buffer_balance: u64,
threshold: u64
): u64 {
if (buffer_balance >= threshold) {
amount * min_fee_bps / 10000
} else {
let fee_range = max_fee_bps - min_fee_bps;
let buffer_ratio = buffer_balance * 10000 / threshold;
let dynamic_fee_bps = min_fee_bps + fee_range * (10000 - buffer_ratio) / 10000;
amount * dynamic_fee_bps / 10000
}
}
残高が一定の閾値を下回ると概ね線形にfeeが増加していく仕組みのようですね。
なお、現在はmax_instant_unstake_fee_bpsが0に設定されているためfeeが徴収されていません。
https://explorer.aptoslabs.com/account/0xd11107bdf0d6d7040c6c0bfbdecb6545191fdf13e8d8d259952f53e1713f61b5/resources?network=mainnet
後述しますが、実は当時はこのfeeの存在も仕組みもわかっていなくてこれらの情報はのちのち調べた際に知ったものです。
めちゃくちゃ重要なのにアホですね😇
償還できる量に限りがある
当時注目していたのは主にこちらです。即時に償還できる量には限りがあり、当時はUnstake可能な量がほぼ常に0で枯渇していました。
私が知る限りこの枯渇しているBufferが増加するのは以下の2パターンです。運営がBufferを増やす
運営が不定期でBufferを増やすことがあります。Aptosのバリデータの仕組みに詳しくないですが、運営がステーキング解除を行い実際にそのステーキングが解除されるepoc timeを迎えた際に、適当な枚数だけこのBufferを増加させているのだと思っています。誰かが新規でStakeを行う
誰かが新規でStakeを行うと、一時的にInstant Unstake Bufferが増えていました。上記と同様の理由で実際にステーキングが行われるまでに時間があるのでそれまではBufferに入るという理解です。
![](https://assets.st-note.com/img/1733121604-9k40BEW2qgFlo6mUQ5Vw1Ibp.png?width=1200)
余談
余談ですがこのプロトコルには大変問題があり、Delayed Unstakeが選択できません。
余談なのにプロトコルとしては致命的なんですが本題とそれるので置いておきます()
一応この記事を書くときにちらっと見た感じではコントラクト上は実装されていそうに見えるので動作するのかもしれませんが普通に30日待ちたくないので試してないです。
あとついでに書いておくと、unstake feeは運営都合でサイレントに変更したりします。
また、運営はDiscordでもう反応がないです。
そのため、繰り返しですが触らないほうが良いと思います😇
stAPTで起きていた歪み
当時stAPTは市場でswapした際のレートがditto上で直接ステークしたときのレートより良かったです。
前述したとおり非リベース型のLSTなので最低限の話として元のトークン建てで見たときに価格は上がってないといけないはずです。
つまり、プロトコルが正しくワークしているのであれば少なくとも1APT = 0.x stAPTの交換レートとなるはずですが、市場では 1APT = 1.x stAPTのレートになっていました。
レートがいいというかもはやラグってます。
実際、dittoのdiscordには某アビトラマンの方がSoft Rug Pullって書き込んでました😇
さて、このことを利用すると、以下のアビトラが考えられます。
APTを市場でstAPTにSwapする
stAPTをdittoでAPTにUnstakeする
1に戻る
これにより元のAPTの枚数が増えることになります。
しかしここで問題となるのが、前述の償還できる量に限りがあるという点でした。
ただ、当時は数十秒から数分に一度程度の頻度で通常のステークを行うユーザが誰かしらいました。大抵は1~5APT程度の超小口でした。そしてさらにその数秒~数分後には、Instant Unstake Bufferに入ったばかりのAPTを誰かが償還しているという状況でした。
元のロットが1~5APT程度で、さらにその乖離を取るということなので一回の利益は数円から良くて数十円とかのレベルだと記憶しているのですが、自分もとりあえずブラウザ更新とUnstakeボタンを連打した気がします。
しばらくしてこれを自動化すればよいと気づき、簡単なbotを書きました。
なぜ価格が歪んでいたのか
これまでに書いた通りではありますが、市場価格が本来の適正レートより大幅に安くなっていた理由を改めて整理してみます。主に以下の2点だと思っています。
instant unstakeは事実上枯渇していた
delayed unstakeの機能が解放されていなかった
上記により、stAPTはまともに償還できないトークンとなってしまい、大幅に本来の理論価格から乖離が進行したのだと思います。
あるいは、何らかの理由で価格乖離が進行したため、instant unstakeの在庫が枯渇するまでは裁定が行われていたが、それも尽きて裁定されなくなったのでより価格乖離が進行した。の方が正しいのかもしれませんがまあニワトリと卵ですね笑
実際に動かしたbot
実際に動かしたbotも書いておきます。botというかスニペットレベルですね😇
パッケージ周り(package.jsonとか)も書いてないのでそのままでは動きません。あくまで雰囲気ということでご理解ください。
万が一参考にされる方がいらっしゃいましたら、使っているsdkなどはすでにメンテナンスされていないと思いますので細心の注意を払ってご利用ください。すべて自己責任でお願いします。
import * as aptos from "aptos";
import { truncateBigInt } from "../utils/bigint-rounder";
import {
Ditto,
Client,
types,
utils,
wallet,
payload,
} from "@ditto-research/staking-sdk";
async function checkAptosBufferAmount(aptos_client: Client) {
await Ditto.refreshDittoPool();
let instantUnstakeBuffer = Ditto.dittoPool.aptosBufferAmount;
console.log(`Instant Unstake Buffer: ${instantUnstakeBuffer}`);
const threshold = 100000000n; // 1 stAPT
if (instantUnstakeBuffer > threshold) {
let instantUnstakeBufferAmount = truncateBigInt(instantUnstakeBuffer, 6);
console.log(instantUnstakeBufferAmount);
try {
let tx_res = await aptos_client.instantUnstake(instantUnstakeBufferAmount);
console.log(tx_res);
} catch (error) {
console.error("instant unstake failed:", error)
}
} else {
console.log("Instant Unstake Buffer is not enough to unstake.");
}
}
async function main() {
const DEFULAT_TXN_CONFIG: types.AptosTxnConfig = {
maxGasAmount: 2000n,
gasUnitPrice: 100n,
txnExpirationOffset: 10n,
};
let aptos_wallet: wallet.DittoWallet = new wallet.DittoWallet(
new aptos.HexString(PRIVATE_KEY),
DEFULAT_TXN_CONFIG
);
await Ditto.load(
aptos_wallet,
types.Network.MAINNET,
"https://api.mainnet.aptoslabs.com/v1",
new aptos.HexString("0xd11107bdf0d6d7040c6c0bfbdecb6545191fdf13e8d8d259952f53e1713f61b5"),
5000
);
let aptos_client = new Client(aptos_wallet, types.Network.MAINNET, 1000);
setInterval(() => checkAptosBufferAmount(aptos_client), 5000);
}
main();
このスクリプトの問題点
いや問題点とかじゃなく、問題しかないだろって感じで恥ずかしいんですが振り返っていきます。
Unstakeしかできない
まずここです(致命的)
このスクリプトは5秒に一回Instant Unstake Bufferを確認して1stAPT以上アンステークが可能だったらその時プロトコルにあるだけのstAPTをアンステークするtxnを投げるだけのスクリプトです。Walletの残高確認しない
上述の通り、Unstakeしかしないので自分のWalletにstAPTがあるか確認しません。あらかじめある程度の量のstAPTを割安な市場でswapしておいてWalletに入れておく必要があります。当然足りなかったらまた手でstAPTをWalletに追加が必要です。たまに大口(10APTとかですw)がステークするとWalletにあるstAPTが足りなくてエラーになります。
他にもfeeを考慮していないとか、エラー処理が、などいくらでも突っ込みどころがあるとは思いますがこんなスクリプトでも一応増えるには増えたので当時は嬉しかったのを覚えています。
嬉しいといっても拾えるロットが小さすぎて金額としては全体で数ドルとかそういう次元の話です。
一方、当時の自分を庇うようですが、一応よかったところもありました。
何せアンステークするTxnを投げるだけという超絶シンプルなbotなので当時Defiについてほとんどわかっていなかった自分でもそこまで時間を掛けずに作ることができました。そもそもSDK使ってるだけですしね😇
このスクリプトを書いた数日後にはこの超少額すらステークする人がいなくなり、このスクリプトも意味がなくなりました。
これがもし残高確認をして、適切なアンステーク枚数を計算して、足りなかったら市場でswapもして、などと時間を掛けて作っていたら数ドルすら拾えなかったと思います。
そういう意味ではとりあえず一番重要そうなところだけ書いたのは良かったなあと思っています。
反省点
さて、ここまでアンステークのfeeの話をほとんど書いてきませんでした。
なぜかというと当時feeがかかることがわかっていなかったからです😇
feeの存在についてはstAPTの仕様の項で記述しましたが、あれは後から調べて分かった情報です。
当時は最大で5%程度のfeeが取られていました。
このfeeは運営が自由に変えられるものですし、そもそもコードを公開もせず、feeがいくらかかるか、その計算式はどうなっているのかをドキュメントに記載していない運営こそどうなんだと思いますが、詐○ばかりの仮想通貨でそんなことを言っても仕方がないので単に自分がぬるかったです。
5%近いfeeを払ってもペイするほど乖離は進行していたので結果的に問題はなかったのですが、これ以降プロトコルを調べる際にはfeeについて特に以下を心がけています。
ドキュメントを読む際に必ずfeeについて検索をかけて見落とさないようにする
ドキュメントは信用できないので併せてコードや実際のオンチェーンの挙動を確認する
おわりに
今回は初めて作ったチンパンdex botのお話を書かせていただきました。
実は当時はaptosなんてチェーンじゃなくて某L2チェーンを触っていたほうが利益が出たらしいんですが、当時の私は知る由もなく目の前の数ドル拾うために必死になってました。
まあこんなレベルで行っても他のbotに狩られるだけだったと思うのでよかったのかもしれません。
ちなみに、なんで初手からAptosなんてチェーンでbotを作ったんだというと
EVMはみんな強そうだからやめとこ…
-> じゃあSolanaは?
Solanaも強いbotterがいるらしいし他で…
-> あれ、このAptosとかいうチェーンなら誰もいないかも…?
くらいの大変浅はかな思考で触り始めました。
もちろんそんなわけはなかったんですが😇
さて、長くなってしまったのでそろそろ終わりたいと思います。
当時の記憶を掘り起こしながら書いている部分もあるので細かいところ間違っていたらごめんなさい。
Xもやっているのでよかったらフォローしてください。
ここまで読んでいただいてありがとうございました。