見出し画像

君のETHがッ尽きるまで殴るのを止めないッ!

仮想通貨botter Advent Calendar 2024 4日目の記事です。

こんにちは。もうこんな時期なの信じられません。信じたくありません。
へっぽこ野郎という名前で活動してます。ハムスター飼育に一家言を持ってます。

今年も生き残ってアドカレに参加できることが嬉しいです。

今回のネタは競合botの妨害です。妨害ネタってあんまり書かないほうがいい・・・?倫理感が壊れてることに気付いてないかもしれません。妨害といってもEVMのルールに基づいてTx送ってるだけだから良いですよね。良いってことにしてください。

鞘を同じようなbotで取り合ってたんですが、ひょんなことから綻びを見つけて突いてみたお話です。当時の自分の考えを時系列で書き殴っております。

1.どんな鞘?

ブリッジグルグルです。ETHをブリッジすると1~2%くらい増えました。

当時流行りに流行ってたLRTトークンにして脱出すると上手くいきそうってことでbotチャレンジしました。

鞘取りのルートは以下の通りです。WLRTはラップドLRTです。
[Chain01]ETH→WLRTへスワップ
[Chain01]WLRT→LRTへアンラップ(今回はここをbot化します)
[Chain01]LRTをブリッジ
[Chain02]LRTをETHへスワップ
[Chain02]ETHをブリッジ
最初に戻る・・・

そこまで複雑なルートでもないのになぜ鞘があるのか。じつはWLRT→LRTへのアンラップに問題がありました。WLRTコントラクトが保有するLRTが枯渇しちゃってたんですね。

2.ラップ/アンラップって何?

コントラクトが保有するLRTが枯渇って意味分かんねえよってなりますよね。皆さんが馴染み深いWETHを例にします。ETHをラップしたりWETHをアンラップする際何が起こっているんでしょう。

【ラップ】
WETHコントラクトにETHを預けて(deposit)、自身のWETHの残高を増やすこと

【アンラップ】
自身のWETHの残高を減らしてWETHコントラクトからETHを引き出す(withdraw)こと

これ以降、ラップをdeposit、アンラップをwithdrawと呼びます。
WETHコントラクトが大量のETHを保有している理由はコレです。それだけdepositされてるわけですね。裏を返すとwithdrawできるWETHには限りがあるということです。下図を例にすると2,970,510.38…WETHを超えてwithdrawできません。

ETHください

3.ブルーオーシャンなんて無いんだ

WLRTが保有するLRTの残高が0より大きくなったら、全額withdrawするbotを作りました。裁定機会は善意の第三者がdepositしてくれたときです。depositした刹那、withdraw不可能となりチェーンから脱出できなくなるので焦った人もいたと思います。ごめんなさい第三者。

さて、やはり同じことを考える人はいます。このルートを発見した時点で既に3人がbotで鞘を取っていました。競合より早くwithdrawトランザクションを送れば勝ち、負けると何も得られないどころかガス代がかかるデスゲームです。

2人は一度にwithdrawする量が多く設定されているようで1LRT未満は取りにきませんでした。残る1人は閾値低めで0.1LRTくらい。何が何でも鞘を取るという意気込みを感じます。本チェーンはトランザクションを早く通すのにガス代を盛り盛りにするんです。競合のガスよりチョビっとだけ多く設定するんですが、相手も同じように対応してきます。その対応速度が尋常じゃないくらい早い。いつ寝てるんですか。

こんな不毛な争いはそのうち利益が出ないレベルまでガスを盛って相手の心を折るマネーゲームになってしまいます。相手のウォレット残高は私より一桁多いので太刀打ちできません。どうにかしないと・・・と、競合のトランザクションを眺めていると奇妙なことに気づきました。

4.イベントドリブンだ!

競合はWLRTが保有するLRTの残高より少ない量をwithdrawしていました。支払うガス代に対してwithdrawする量を出来るだけ多くできれば利益も増えます。全額withdrawしない理由はありません。ということはWLRTの残高を監視してないわけです。

では競合botは何を監視して動作しているのでしょうか。

少し話を変えます。スマートコントラクトにはeventという仕組みがあります。ブロックチェーンで起こったことをフロント(Webブラウザとか)に伝える仕組みです。これがあるからWombat Exchangeでスワップした際にウォンバットが空に飛んでいく可愛いムービーが見れるんです。

depositトランザクションが完了するとDepositというeventが発行されます。このeventには以下の情報がメモされています。
(1) depositしたアドレス
(2) depositしたLRTの量

このeventはメジャーなWeb3.pyライブラリなどで監視できます。確認するとDeposit eventに記載されたLRTの量と、競合botがwithdrawした量はピタリと一致しました。間違いないでしょう。

5.イベントだけ発行して誤動作させよう!

WLRTコントラクトにLRTをdepositせずにDeposit eventを発行できれば競合botを誤動作させられそうです。ガス代盛り盛り設定のbotを暴走させてガス代を0にできればbotを止められます。

0枚のLRTをdepositすればイベントだけ発行できます。ただこれはダメでした。先に書いた通り0.1LRT未満のeventには反応しません。

じゃあdepositしてからすぐwithdrawすればいいのでは?トランザクションを2回に分けると相手が先にwithdrawすることもあります。ですのでdepositとwithdrawを確実に同ブロックで行うため妨害用コントラクトを作ることにしました。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

// WLRTのインターフェース
interface IWLRT {
    function depositTo(address asset, address _to, uint256 _amount) external ;
    function withdrawTo(address asset, address _to, uint256 _amount) external;
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

contract WLRTDepositWithdraw {
    using SafeERC20Upgradeable for ERC20Upgradeable;
    IWLRT wlrt = IWLRT(0x0000000000000000000000000000000000000000);
    ERC20Upgradeable lrt = ERC20Upgradeable(0x1111111111111111111111111111111111111111);

    function depositAndWithdraw() external {
        uint256 amount = lrt.balanceOf(msg.sender);
        uint256 before_balance = amount;

        lrt.transferFrom(msg.sender, address(this), amount);
        lrt.approve(address(wlrt), amount);
        wlrt.depositTo(address(lrt), msg.sender, amount);
        wlrt.transferFrom(msg.sender, address(this), amount);
        wlrt.withdrawTo(address(lrt), msg.sender, amount);
        
        uint256 after_balance = lrt.balanceOf(msg.sender);
        require(after_balance == before_balance, "Fail");
    }

    function withdraw() external {
        uint256 amount = lrt.balanceOf(address(this));
        address to = 0x2222222222222222222222222222222222222222;
        lrt.transfer(to, amount);

        amount = wlrt.balanceOf(address(this));
        wlrt.transfer(to, amount);
    }
}

depositTo関数とwithdrawTo関数を1Txで実行できます。工夫した点はdepositした際にWLRTを受け取るアドレスを妨害コントラクトではなく、妨害コントラクトを起動したEOAにした点です。これでeventにメモされる「depositしたアドレス」をEOAにできます。

妨害に気付いた競合がeventにメモされたアドレスをフィルタリングするかもしれません。depositしたアドレスが妨害コントラクトアドレス固定ですと、新たな妨害コントラクトのデプロイを強いられます。EOAにすると妨害コントラクトを起動するアドレスを変えるだけでフィルタリングを貫通することができます。

関数末尾でdeposit前にメモしたLRTの量と、withdraw後のLRTの量を比較しています。一致すればOK、もし不一致ならNGとしてトランザクションを無かったことにしています。また妨害コントラクトにLRTやWLRTが幽閉されてしまった際の緊急脱出機能としてwithdraw関数を設けています。これは重要です。こういったガード機能をつけずに2411ドル失った人を私は知っています。ERC20のアドレス指定できるようにしたほうがいいかも。

6.殴る!

妨害コントラクトを起動するためのbotをPythonで作っていざ!

 

うおおおおおおおおおおおおお。競合botが狙い通り誤動作してTx失敗してます。

ただ長くは続きませんでした。

その後もちょいちょい成功したんですが3日後には塞がれてしまいました。競合がbotを作り直したのか、対策をしたのかは不明です。作るのに1日かかったのに・・・。タイトル未回収で申し訳ありません。盛りました。

7.結び

結局ブリッジグルグルでは原資1ETH用意して+1.6ETHでした。2週間ほどでエッジが消滅したのは残念です。エッジ消滅の原因は運営が十分な量のLRTを用意したことですね。滞りなくブリッジできるようになれば当然乖離も無くなりますから。

bot化したのはwithdraw部分のみでブリッジやスワップは手動でした。botが作動したらdiscordに通知させ、AppleWatchで常に受信してたんですが本当に身体に良くない。睡眠中や会議中も問答無用で通知がくるので。やっぱり全自動化したいですね。

今回の記事では競合の妨害について自身の体験を書き殴りました。体験記ってbot作ったことない人やこれから作ろうと思ってる人にとって非常に有益だと思っていて今後も書けるネタがある限り続けようと思います。

この月は他のアビトラbotもハマってbot収益だけ見ると初のA級を達成できました。ETH現物の下落でドル建てではマイナスだったんですけどね・・・ところでETHのATHまだですか?


いいなと思ったら応援しよう!