一文簡析捕獲Synthetix 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


