捕獲Synthetix MEV 的策略剖析

來源| bertcmiller.com

作者| Robert Miller

幾個月前,臭名昭著的KALEB 在Flashbots 的公共搜索者discord 上發布了以下信息:

KALEB 之前洩露了關於Synthetix 變動政策的重大消息,涉及數十萬美元。在這個機器人運營商的巢穴里分享這種消息就像把紅肉扔給獅子一樣,快速看一下合約就會發現有一筆誘人的錢在裡面。

在接下來的幾周里,我計劃並試圖執行一個策略,以捕獲KALEB 在上面分享的MEV。我將在本文公開我使用的代碼,並講解我的過程和策略。你將無法運行我的代碼來印錢,但這篇文章將告訴你我如何設計新的搜索器,且包含很多關於這樣做重要信息。這自然會比較技術,但我努力使非技術讀者也能理解。

第一步:確定機會範圍

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

我找出相關的合約

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

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

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

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

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

要償還一筆貸款,我需要歸還所借資產(sUSD 或sETH) 的未償金額。作為回報,我會收到支持我所關閉的貸款的抵押ETH。作為償還這些貸款的激勵,我會獲得比我歸還sETH 或sUSD 更多的抵押品價值。由於當時仍有數百萬美元的貸款,這意味著償還者可以賺更多的錢。此外,我將不得不從pDAO 合約裡尾追(backrun) 交易,以便我可以盡可能利用這個機會。

第二步:了解機會

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

我需要兩個函數:

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 倍的速度。這是其中一個函數:

函數batchGetLoanInformation(地址[] 調用數據_地址,uint256[] calldata _loanIDs, address _contractAddress) 外部視圖返回( uint256[] 內存,uint256[] 內存){ uint256[] 內存 totalRepayment = 新 uint256[](_addresses.length); uint256[] 內存 totalCollat​​eralLiquidated = 新 uint256[](_addresses.length); for (uint i = 0; i < _addresses.length; i++){ uintloanAmount; uint 應計利息; (,,loanAmount,,,, accruedInterest, ) = concurrentContract(_contractAddress).getLoan(_addresses)[i], _loanIDs[i]); 還款總額[i] = 貸款金額 + 應計利息; 全部抵押品已清盤[i] = getCollat​​eralAmountSUSD (sUSD, totalRepayment[i], 抵押品); } return (totalRepayment, totalCollat​​eralLiquidated);}

請看由EmGithub 提供的rawsAssetsOracle.sol

你們可以在這裡我的監聽腳本看其使用。現在我能很快判斷我需要多少sETH/sUSD,償還一筆貸款後能拿回多少抵押價值,從而得知這筆貸款的利潤。

總結:這個階段的工作是深入了解機會,以及以高效的方式蒐集所有執行所需的數據。你需要盡力把東西移到鏈下完成,以降低gas 消耗。這部分工作的成果是做出了兩份蒐集所需數據的fast script。

第三步:寫一份執行合約

閃電貸策略

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

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

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

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

鑑於sUSD 僅在Aave 上可用,而ETH 可以在多個閃電貸供應商上可用,問題最終會變成我想使用哪個閃電貸供應商。最終,我選了選項1,因為dYdX 不產生費用,而Aava 會產生一項費用。

Gas 優化

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

// 這是dydx在給我們貸款函數 callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external { // 使用chi代幣後調用的函數 uint256 gasStart = gasleft(); // 讓執行者或 dYdX 合約調用這個函數 // 限制為 dYdX 可能沒問題 require(msg.sender == executor || msg.sender == address(soloMargin)); // 從數據對象(地址[] 內存 sUSD 地址,uint256[] 內存 sUSDLoanIDs, uint256 wethEstimate, uint256 usdcEstimate, uint256 ethToCoinbase ) = abi.decode(data, ( address[], uint256[], uint256, uint256, uint256 )); // 在 uniswap v3 上用 WETH 交換 USDC uniswapRouter.exactOutputSingle( ISwapRouter.ExactOutputSingleParams( address(WETH), // 地址 tokenIn; usdcTokenAddress, // 地址 tokenOut; 3000, // uint24 費用; address(this), // 地址接收者; 10**18, // uint256deadline; usdcEstimate, // uint256 amountOut; wethEstimate, // uint256 amountInMaximum; 0 // uint160 sqrtPriceLimitX96; ) ); // 在曲線 curvePoolSUSD.exchange_underlying( 1, // usdc 3, // sUSD usdcEstimate, // usdc input 1) 上用 USDC 交換 sUSD; // 最小 sUSD,通常不建議進行最小金額為 1 的交易,但我認為在這裡很好,因為獲得 rekt 的總體風險很低 // 清算貸款 (uint256 i = 0; i < sUSDAddresses .length; i++) { sUSDLoansAddress.liquidateUnclosedLoan(sUSDAddresses[i], sUSDLoanIDs[i]); } // 我們取回了 ETH,但必須用 WETH 支付 dYdX,所以存入我們的全部餘額,而不是支付給礦工的費用 WETH.deposit{value: address(this).balance - ethToCoinbase}(); // 支付礦工 block.coinbase.transfer(ethToCoinbase); // 用於 chi 令牌 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。

這份合約還有一些可以被優化的方法,例如使用gas 費用而不用coinbase 轉賬。

0xSisyphus 非常慷慨提出給我借ETH,我就不用使用閃電貸了,這能大大節省gas。但隨著時間推移,大額的貸款都還錢了,因此總的機會就減少了。我決定不接受0xSisyphus 借的錢,因為機會不再大到使這樣做是明智的。

總結:在這個階段,我創建了一份智能合約,以執行捕獲可得的MEV 機會。要做到這點,需要認真思考正確的策略,以及如何最小化gas 的使用。這份合約是經過多次迭代開發出來的,同時我進行數據上的工作,還把它放在了一個測試環境(Hardhat) 裡。

第四步:計劃你的執行

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

有了一份精心設計的合約和對機會的深入了解,我需要改進我實現這個機會的策略。回顧一下,Flashbot 的MEV-Geth 客戶端能有效運行競拍,其中gas 價格最高的交易捆勝出,會被打包到鏈上。這一重要事實意味著,我需要最大化我的交易捆的gas 價格,而不是我支付的ETH 總額。

記住了這點,並使用我之前收集到的數據,我製作了一個電子表格來優化我的gas 價格。我的合約既有固定的gas 開銷,也有可變的gas 開銷。固定的gas 開銷用於取出閃電貸和做兌換。可變的gas 開銷來自我想要償還的貸款數。我很自然地認為在某一點上,償還一筆貸款的邊際收益將低於gas 開銷。我運行了幾次測試,以得出實際數字。以下是我的結果:

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

考慮到未償的sETH 貸款更少了,只做sUSD 貸款而不把sUSD 和sETH 合併到一筆交易是最合理的。因此,潛在的回報更少了,支付給礦工的錢也更少了,這使得它們的gas 效率相對較低。看到這些發現,我不禁笑了。如果其他人貪婪,一下償還了所有的貸款,或很懶惰,分開償還,那麼我就會贏。

然而,其他的貸款還在那裡,且償還起來也是有利可圖!我再次嘗試優化我的gas 價格,發現如果我償還前4 筆sUSD 貸款,接下來最省gas 的做法是一併償還後6 筆最大的sUSD 貸款,再分別償還最大的兩筆sETH 貸款。此外,假設我贏了,我可以使用從之前的交易捆中獲利的ETH,而不再需要閃電貸了。

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

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

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

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

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

通過了第一輪模擬後,這些交易捆將在第二輪模擬中跟在有pDAO 交易的那個交易捆後面。到了這步,它們就能成功償還貸款了。此外,這些交易捆的gas 價格會比競拍預期的更高,而不是更低,因此在這裡改變執行不是問題。

我是如何確定我的交易捆是在哪一“輪”的呢?通過看我合約的餘額。如果在區塊的早期(即在前一個交易捆)我已經成功償還貸款了,那麼我的餘額應該增加了,因為這樣做能從中獲得收益。因此,我增加了一個條件來檢查我是否獲得任何WETH 收益,如果有,則繼續償還貸款。這在測試中是成功的。

總結:這個階段還是關於策略。我使用早期得到的數據、合約和測試環境來思考我要競爭獲得的MEV 機會的經濟學邏輯,以及最優策略會是什麼。通過使用真實數據,我發現了一個令人驚訝的佔有策略,但它很難執行。執行它需要一種新方式來提交交易捆。

第五步:執行

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

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

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

最後,我有了一個執行腳本,它會接收發送到我的機器人的待打包交易信息,並從我的監聽腳本加載出最佳打包交易捆策略的結果,自動構建交易捆,並把它們發送到Flashbots。

剩下的事情就是等待。在這段時間,最高額的sETH 貸款被借款人償還了,因此我關了機器人中的該部分。幾個最高額sUSD 貸款也結束了,這大大減少了預期的回報率。

第六步:關鍵時刻到了

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

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

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

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

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

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

Total
0
Shares
Related Posts