BTC
ETH
HTX
SOL
BNB
查看行情
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt

一文簡析捕獲Synthetix MEV的詳細過程

ECN以太坊中国
特邀专栏作者
本文約8520字,閱讀全文需要約13分鐘
MEV 有時被認為是神秘莫測的超級程序員的領域,但它不一定是這樣的。它可以是有趣和刺激的。
AI總結
展開
MEV 有時被認為是神秘莫測的超級程序員的領域,但它不一定是這樣的。它可以是有趣和刺激的。

來源| bertcmiller.com

圖片

圖片

圖片

一級標題

一級標題

第一步:確定機會範圍

我不是Synthetix 的專家,因此第一步是了解我的工作內容。具體來說:

  • 我找出相關的合約

  • 我在Sythetix 博客上通讀它們功能的概述性文章,並蒐索了所有的文檔

  • 我確信我理解了即將要實現的治理變更

  • 我查看這些函數,並找出那些看起來相關的

這個階段工作的總結是,Synthetix 已經試驗了使用ETH 作為抵押品來鑄造sUSD 和sETH。你可以在合約中存入ETH 並鑄造這些資產,只要你注意你的抵押品價值不會跌至低於你貸款的一定水平。

但是,一年後,協議投票決定結束這個試驗。當有數以百萬計的未償貸款時,他們怎麼能這樣做呢?好吧,你可以讓任何頭寸都變得可償還。事實上,在一個漫長的警告期後,貸款會從在一個區塊裡是安全的,變成可被任何人償還,無論抵押品的價值是多少!這會觸發一筆從"pDAO"一級標題

一級標題

第二步:了解機會

現在我了解了基本機制,並且有了一些我認為相關的函數了。然後,我又深入了解我將調用哪些函數,我需要什麼數據,以及如何生成該數據。

我需要兩個函數:

  • setLoanLiquidationOpen():僅能由合約所有者(pDAO) 調用。允許對未關閉的貸款進行償還。

  • liquidateUnclosedLoan():需要一個貸款ID 和賬戶地址,並償還該貸款。在setLoanLiquidationOpen() 被調用後可以被任何人使用。

請注意,還有其他函數的,但我很快發現它們並不相關。現在,我需要解決我要如何選擇償還哪些貸款,以及這樣做我需要多少sETH/sUSD。以下的函數開啟工作所需的大部分東西:

  • openLoanIDsByAccount():返回所有與某個賬戶相關的所有公開貸款ID

  • getLoanInformation():返回一個給定ID 和所有者的貸款數據

然而,存在兩個隱藏的困難。首先,這些合約不會告訴你哪些地址有未償還的貸款,這給我帶來一定難度。幾分鐘內,我找到一個解決方法,就是在Etherscan 上下載所有與這些合約想法u你的交易,並用Excel 創建一個與這些合約交互過的唯一賬戶列表。從那裡,我建立了以下管道:

  • 使用getLoans 找出該地址是否有未償貸款,如果有,記錄它們的貸款ID

  • 使用getLoanInformation 找出支持該貸款的抵押價值,它們發行了多少sUSD/sETH,以及它們的貸款有多少利息

  • 按貸款金額排序,首先列出最大額度的貸款,並把數據結構保存到json 文件中。

第二個難題是償還一筆貸款能拿回多少抵押的ETH 並不能馬上了解。因為有未償貸款的函數,你可以粗略估算到,但我需要更精確的數據。要實現這點,我研究了償還貸款的代碼,並了解清楚相關數字是如何產生的。

以上的所有數據都是我可以在鏈上獲取或計算的。但這樣做太耗費gas 了。由於我將與別人競爭合約的gas 效率,對我來說,盡可能地把邏輯移到鏈下,以最小化gas 消耗是極其重要的。

經過幾次迭代後,我知道我需要從鏈上獲取的最小數據量,這是我可以鏈下解析的一些變量,以告知我在智能合約上輸入的內容。但是,獲取這個信息的函數非常複雜,而且查詢所有貸款所花的時間比處理一個區塊的時間還長。這是不可行的。為了解決這個問題,我寫了一份簡短的智能合約,把許多數據請求集中在一起,這提高了超過10 倍的速度。這是其中一個函數:

function batchGetLoanInformation(address[ ] calldata _addresses, uint256[ ] calldata _loanIDs, address _contractAddress) external view returns (    uint256[ ] memory,    uint256[ ] memory){    uint256[ ] memory totalRepayment = new uint256[ ](_addresses.length);    uint256[ ] memory totalCollateralLiquidated = new uint256[ ](_addresses.length);    for (uint i = 0; i < _addresses.length; i++){        uint loanAmount;        uint accruedInterest;        (,,        loanAmount,,,,        accruedInterest,        ) = collateralContract(_contractAddress).getLoan(_addresses[i], _loanIDs[i]);        totalRepayment[i] = loanAmount + accruedInterest;        totalCollateralLiquidated[i] = getCollateralAmountSUSD(sUSD, totalRepayment[i], COLLATERAL);    }    return (totalRepayment, totalCollateralLiquidated);}

總結:

總結:

總結:這個階段的工作是深入了解機會,以及以高效的方式蒐集所有執行所需的數據。二級標題一級標題

二級標題

閃電貸策略

你可能需要一份專門的合約來提取MEV。我在早期寫了一份合約和測試環境,可以用這個環境來更好理解合約,並確保數據是正確的。這幾乎與第二和第四步是同時進行的。

我知道我需要獲得數百萬美元的sUSD/sETH,因此使用閃電貸是必須的。此外,我會燒毀這些合成資產但拿回抵押的ETH。經過一番思考,我意識到無論如何我都要用ETH 兌換其他資產,但我可以選擇在償還之前還是之後進行。有兩條可能的路徑:

  • Option 1 選項1: 通過閃電貸貸出ETH -> 兌換出USDC -> 兌換出sUSD -> 償還sUSD 貸款-> 收到ETH -> 償還ETH 的閃電貸

  • Gas 優化

Gas 優化

Gas 優化

你可以在此處找到我的完整合約,但這是我從dYdX 收到ETH 並進而償還sUSD 貸款後的部分:

 //This is the function called by dydx after giving us the loanfunction callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external {    // Use chi tokens    uint256 gasStart = gasleft();    // Let the executor or the dYdX contract call this function    // probably fine to restrict to dYdX    require(msg.sender == executor || msg.sender == address(soloMargin));    // Decode the passed variables from the data object    (        address[] memory sUSDAddresses,        uint256[] memory sUSDLoanIDs,        uint256 wethEstimate,        uint256 usdcEstimate,        uint256 ethToCoinbase    )        = abi.decode(data,    (        address[],        uint256[],        uint256,        uint256,        uint256    ));    // Swap WETH for USDC on uniswap v3    uniswapRouter.exactOutputSingle(        ISwapRouter.ExactOutputSingleParams(            address(WETH),        // address tokenIn;            usdcTokenAddress,     // address tokenOut;            3000,                 // uint24 fee;            address(this),        // address recipient;            10**18,               // uint256 deadline;            usdcEstimate,         // uint256 amountOut;            wethEstimate,         // uint256 amountInMaximum;            0                     // uint160 sqrtPriceLimitX96;        )    );    // Swap USDC for sUSD on Curve    curvePoolSUSD.exchange_underlying(        1, // usdc        3, // sUSD        usdcEstimate, // usdc input        1); // min sUSD, generally not advisible to make a trade with a min amount out of 1, but its fine here I think because the overall risk of getting rekt is low    // Liquidate the loans    for (uint256 i = 0; i < sUSDAddresses.length; i++) {        sUSDLoansAddress.liquidateUnclosedLoan(sUSDAddresses[i], sUSDLoanIDs[i]);    }    // We got back ETH but must pay dYdX in WETH, so deposit our whole balance sans what is paid to miners    WETH.deposit{value: address(this).balance - ethToCoinbase}();    // Pay the miner    block.coinbase.transfer(ethToCoinbase);    // Use for chi tokens    uint256 gasSpent = 21000 + gasStart - gasleft() + (16 * msg.data.length);    CHI.freeFromUpTo(owner, (gasSpent + 14154) / 41947);}

請看由EmGithub 提供的rawdYdXLiquidator.sol

我花了大量時間嘗試最小化我的gas 消耗。很多我的設計選擇都是以此為依據。關於這份合約的策略,有幾點需要注意

  • 我沒有發送很多單獨償還和兌換的交易,相反,我選擇把多筆償還打包在一個交易裡,這使得我的固定gas 開銷可以在多筆償還里分攤,由此提高我的交易捆的競爭力。

  • 我需要以最佳方式把ETH 兌換成USDC 再兌換成sUSD,並需要決定是在有函數exactInput 還是exactOutput 的Uniswap v3 上交易。無論我怎麼做都會在某個地方產生滑點,因此我選擇有函數exactOutput 的,以避免調用balanceOf。

  • 在這些交易的精確性和gas 效率之間存在折衷。只要我能償還我的閃電貸,缺乏精確性也沒什麼問題,而且因為我要在gas 效率上競爭,我選擇了對它優化。

還有一些“戰術上”的東西需要注意:

  • 對所有東西的批准都前向負載到我的合約裡的構建函數。這樣,我可以在部署的時候支付開銷,並減少執行時使用的gas。

  • 我不從我的賬戶燒毀gas token,而是從我的合約燒毀,同樣是為了提高gas 效率。

  • 函數名稱都是指定的,它們的函數選擇器的前導符為0x,使用函數選擇器也能稍微減少gas 的使用。

  • 與直接添加require 語句相比,函數修改器需要消耗稍微多一點gas。

  • 二級標題

  • 總結:

總結:一級標題

二級標題

償還MEV 和優化gas 價格的經濟學

圖片

圖片

圖片

請注意,這個結果有點令人驚訝——僅償還前4 筆(共30 筆) 的sUSD 貸款是最省gas 的。此後的每筆貸款都會產生更多的整體利潤,但會降低我的交易捆的gas 價格,並降低其競爭力。如果有其他人試圖一次性償還前10 筆sUSD 貸款,它們的gas 消耗效率會降低接近30%!

二級標題

二級標題

Flashbots 競拍和我的交易捆排序策略

重複一下情況:我既要在gas 效率上競爭,又希望通過償還每筆貸款來最大化我的收益。最佳策略是在每個交易捆中提交幾筆償還,分幾個交易捆進行。這些交易捆會在Flashbots 競拍中被各自評估。然而,每筆交易都取決於來自pDAO 的交易,是它使得貸款可以被任何人償還。

  • 如果pDAO 交易不在交易捆裡,那麼該交易捆就會失敗。但如果我的每個交易捆裡都有pDAO 交易,那麼只有一個交易捆會成功。也就說,在一個交易捆被成功打包後,其他所有的都是無效的,因為它們會試圖對pDAO 交易重複打包。因此,我需要找到方法使得僅在我的第一個交易捆裡發送pDAO 交易,但同時確保我的其他交易捆不會因為它們沒有pDAO 交易而失敗和被扔掉。

解決方案在於Flashbots 競拍的一個細微之處。在搜索器開始對競拍使用“把戲”,降低交易捆合併後的礦工費用,Flashbots 實行兩輪的模擬。首先,所有的交易捆都被單獨進行模擬,得出它們的gas 價格並檢查是否會失敗。在第二輪,成功的交易捆會被按照gas 價格排序,並再次進行模擬,以找出前後不一致的交易捆,確保沒有交易捆的gas 價格是低於預期的。除非你想這樣做,否則你可能永遠不會有一個交易捆是gas 價格在合併後是降低了的。

我意識到我可以做上述搜索器相反的事:我的交易捆不是支付比預期更少的gas 費,它們會在第二輪模擬中支付更多。為了做到這點,我將如預期般在第一個交易捆裡打包pDAO 交易,但要對剩餘的交易捆做額外檢查。這些交易捆將推斷它們將會在第幾“輪”模擬,然後相應改變它們的執行。如果它們在“第一輪”,它們將不會償還任何貸款——因為它們嘗試償還的話會失敗——然後無論如何都給礦工支付費用,以獲得高的gas 價格,通過第一輪的模擬。

一級標題

總結:

總結:一級標題

第五步:執行

現在手上有了數據、合約、以及我可以開始執行的計劃。基本上,我需要構建多個這樣的交易捆:一方面可以執行我上文的計劃,另一方面監聽交易池裡與Synthetix 相關的交易以進行尾追。此時,大部分都是實現上的問題。

首先,我使用了Blocknative 來監聽pDAO 賬戶,以了解相關交易的情況。我讓任何從pDAO 賬戶發出的交易信息都會發送到我的機器人。

然後,我同時運行兩個監聽腳本(一個用於sETH 和sUSD ),以從鏈上獲取數據,得出最優的交易捆策略(例如先償還前3 筆sETH 貸款,閃電貸X 個ETH,並對後2 個做相同的事,等),並生成我的合約需要的數據。我需要在每個區塊如此運作,以防價格改變了或有人關閉了貸款,由此改變了最佳策略。這些結果保存在了本地。

一級標題

一級標題

第六步:關鍵時刻到了

有趣的是,有人試圖通過向相關合約發送交易,想誘使機器人在早期失靈。我不確定這種情況是否也會出現在其他人的機器人上,但我的機器人沒有被誘導成功。

幾個小時後,pDAO 發出了真實的交易。經過數週的研究和準備,我知道關鍵時刻到了。我這邊一切都進行很順利:我的監聽腳本運行的很好,交易被接收,交易捆也被構建和提交了。

.....然後意外發生了。連續多個區塊都沒有Flashbots 區塊被挖出。我不僅因此失去了機會,也沒有Flashbots 搜索器贏得了機會。在區塊頂部沒有了Flashbots 交易捆起阻擋作用,一個雄心勃勃的交易池機器人介入了,並搶走了所有有利可圖的貸款。

儘管輸了,但我認為我的方法仍然是正確的。我的優勢在於策略和發現新機會,而不是參與PGA (最優GAS 費競拍)。因此,使用Flashbot 給了我勝出的最佳機會。鑑於Flashbots 已被廣泛採用,遇上連續好幾個非Flashbots 區塊也是相當不走運了。

MEV 有時被認為是神秘莫測的超級程序員的領域,但它不一定是這樣的。它可以是有趣和刺激的。而遊戲規則,如果你要搜尋它們的話,可以說是開放的。這篇文章是關於我學習我所參與的遊戲的規則的過程,根據這些規則我想出了策略,並最終執行該策略。儘管我輸了,我學到了很多東西,並在此過程獲得了樂趣。我希望你們也可以,並希望你和我一起參與下一輪的遊戲。

原文鏈接:https://bertcmiller.com/2021/09/05/mev-synthetix.html

ETH
Synthetix
歡迎加入Odaily官方社群