みなさんbot作ってますか?
DEXで流行っているatomic arbitrageについて、そもそもどういったものなのか解説します。
実際に自分で収益機会があるか確認するハンズオンや、昔勝っていたbotの解説などポロリもあるので確認してみてください。
また実際にatomicに取引を実行できていたjupiter用のコードを公開するので、jupiterでプログラムから取引したいんだよなーって方も是非どうぞ!
動画で確認したい方はyoutubeでも解説しているのでそちらもあわせてご確認ください。
アービトラージ取引とは
アトミックアービトラージの解説の前に、アービトラージという手法をご存知でしょうか?
裁定取引ともよばれており、シンプルに言うと取引所の価格差から収益を上げる方法です。
こちらの図がアービトラージを行っている概念図です。
赤いお店でりんごの売買が100円でできるとします。
青いお店では120円です。
ここで赤いお店でりんごを100円で購入して、青いお店でりんごを120円で売却すると20円の利益が出ます。
これが基本的なアービトラージの仕組みです。
しかしながらこのアービトラージには大きなリスクが存在します。
赤いお店で100円でりんごを買ったあと、青いお店に運搬中に価格が変動する可能性です。
たとえば、青いお店が運搬中に80円に価格を変更してしまうと、青いお店で売っても-20円になってしまいます。
このリスクをなくす仕組みがアトミックアービトラージです。
負けないアトミックアービトラージ
赤いお店で100円で買う工程と120円で売る工程をひとつの取引としてロックすることができます。
このロックがかかると、ロックされた一連の取引が成功すれば当然20円の儲けになります。
その一方で、120円が80円になってしまった場合には、そもそも購入の時点からなかったことになります。
つまり、20円増えるか、取引が失敗するかの二択にすることでリスク無しで利益を取りに行く方法がアトミックアービトラージといえます。
この不可分であることがアトミックという言葉のもととなっています。
そんなうまい話がそこら辺に転がっているのか?そう思いましたね?
Jupiterで鞘を確認しよう!
実はそれを確認する方法があります。すでにsolanaを使ったことがあるソラナニアンは100人中98人がやったことあると思うjupiterグルグルです。
jupiterというのはアグリゲーターと言われるもので、いろいろな取引所から一番レートのいい交換方法を教えてくれます。
では一緒に確認してみましょう!!!
例えば、1ソル(SOL)をUSDCに交換したいとします。
ジュピターアグリゲーターは最適なルートを自動で計算し、最も高いレートで交換できるように導いてくれます。
今回の例では、リフィニティでUSDTに交換し、stabbleでUSDCに変換するルートが最も有利であると示されています。
このように、すごいのは単に一番いいレートを教えてくれるだけじゃなくて、一度違う通貨に変えてからUSDCに変えるとお得だよみたいな交換のルートも教えてくれます。
じゃあここで驚くもの見せます。みなさんも自分で一度やってみてください。
1SOLをUSDCに交換して同量を更にUSDCに交換し直すと
なんとお金が増えます!!!!!!!!!!!!!!
そう、jupiterが最適な経路を選択するおかげでこの僅かな歪みを取ることができるのです。
jupiterグルグルの確認方法がわからない方は、動画のほうが実際のページで操作しているのでわかりやすいかもしれません。
そしてこの素晴らしい仕組みを使って実際にお金を増やすことに成功したのがこちらです。
0.5SOLを0.517SOLと交換しているのが確認できますね!
これが現代の錬金術です!!!!!!!
特にイベントなど、大きなお金が動くときは鞘も大きかったように思います。
実際に儲かったコードについて
それではこのswapを達成したのが実際にどんなコードかみていきましょう。
import { AddressLookupTableAccount, Connection, Keypair, TransactionMessage, sendAndConfirmRawTransaction } from '@solana/web3.js';
import fetch from 'cross-fetch';
import { Wallet } from '@project-serum/anchor';
import * as bs58 from 'bs58';
import { VersionedTransaction } from '@solana/web3.js';
const connection = new Connection('https://api.mainnet-beta.solana.com');
const wallet = new Wallet(Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY || 'ウォレットのプライベートキーをいれる')));
const executeTransaction = async (quoteResponse: any, wallet: Wallet, connection: Connection) => {
const transactiona = await (
await fetch('https://quote-api.jup.ag/v6/swap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
quoteResponse,
userPublicKey: wallet.publicKey.toString(),
wrapUnwrapSOL: true,
computeUnitPriceMicroLamports: 100,
})
})
).json();
const { swapTransaction } = transactiona;
console.log(transactiona);
const swapTransactionBuf = Buffer.from(swapTransaction, 'base64');
var transaction = VersionedTransaction.deserialize(swapTransactionBuf);
// console.log(transaction);
return transaction
}
const main = async () => {
const aida = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
const input = "So11111111111111111111111111111111111111112"
// 非同期リクエストを並列で実行
const [response, rev_response] = await Promise.all([
fetch('https://quote-api.jup.ag/v6/quote?inputMint=' + input + '&outputMint=' + aida + '&amount=5000000&slippageBps=0&maxAccounts=30'),
fetch('https://quote-api.jup.ag/v6/quote?inputMint=' + aida + '&outputMint=' + input + '&amount=13000000&slippageBps=3&maxAccounts=30')
]);
const quoteResponse = await response.json();
const rev_quoteResponse = await rev_response.json();
console.log(rev_quoteResponse.outAmount);
const ratio = rev_quoteResponse.outAmount/quoteResponse.inAmount * quoteResponse.outAmount/rev_quoteResponse.inAmount;
rev_quoteResponse.routePlan.swapInfo
let number = 5000500;
let stringNumber = number.toString();
quoteResponse.routePlan = [...quoteResponse.routePlan, ...rev_quoteResponse.routePlan];
quoteResponse.outputMint = quoteResponse.inputMint;
quoteResponse.outAmount = stringNumber;
quoteResponse.otherAmountThreshold = quoteResponse.inAmount;
console.log(ratio);
// if (ratio > 1.01){
const qtransaction = await executeTransaction(quoteResponse, wallet, connection);
console.log("qtransaction");
const { blockhash } = await connection.getRecentBlockhash();
qtransaction.message.recentBlockhash = blockhash
console.log(qtransaction.message.compiledInstructions);
console.log("happpyo!!!!!!!!!!!!!");
qtransaction.sign([wallet.payer]);
// Execute the transaction
const rawTransaction = qtransaction.serialize()
const txid = await connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
maxRetries: 2
});
console.log(txid)
await connection.confirmTransaction(txid);
console.log(`https://solscan.io/tx/${txid}`);
await new Promise(resolve => setTimeout(resolve, 3000));
return false;
}
// return false;
// }
const loop = async () => {
let shouldStop = false;
while (!shouldStop) {
try{
shouldStop = await main();
}catch(error){
console.error('An error occurred in main:', error);
// エラーが発生しても、shouldStopはfalseのままなので、ループは続行されます。
}
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds
}
}
loop();
まずはjupiterのapiを叩いて、トークンの交換レートとルートを取得します。
行きと帰りの両方を取得することがミソです。
その後、このルートプランを手動でガッチャンコします。
そう!これだけで、アトミックアービトラージができるトランザクションに生まれ変わります!!!
このとき最初のインプット量より小さくなってしまうとお金が減るので、outAmountを設定し、これ以下の枚数のときはtxが失敗するように設定しましょう。
また、今回はif文はコメントアウトしていますが、実際はtxを投げ続けて失敗し続けるとガス代負けしてしますので、apiを叩いたときの期待利益を計算して、鞘が開いているときだけ投げるようにしてください。
ただし、JUP上場戦などいつでも鞘が開くようなイベント時には鞘を検出せず手当たり次第に投げるほうが儲かりました。詳しくはYouTubeの解説をご確認ください。
他のボットが同様の手法を採用し、より高速で効率的な取引が可能になったため、私のボットは競争力を失ってしまいました。
更にジュピターアグリゲーター自体がAPIの仕様を変更したり、エラーメッセージが返されるようになったことで、ボットの運用が困難になりました。
{
error: 'Input and output mints are not allowed to be equal',
errorCode: 'CIRCULAR_ARBITRAGE_IS_DISABLED'
}
おそらく現在では上記のようなレスポンスが返ってくるかと思います。
そのため現在このコードはJupiterでプログラムからスワップするためにお使いいただければと思います。
ただ、実際に勝っていたコードがこんなシンプルな作りであることをお伝えしたく、全コード掲載いたします。
もし記事を気に入ってくださった方はぜひヤメティファンクラブに入ってください!
2025年スタートで、月に一回、損益などをお知らせや、不定期で記事の更新を行いたいと思っています!
ヤメティファンクラブ