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

A brief analysis of the detailed process of capturing Synthetix MEV

ECN以太坊中国
特邀专栏作者
This article is about 8520 words, reading the full article takes about 13 minutes
MEV is sometimes thought of as the realm of the elusive super-programmer, but it doesn't have to be that way. It can be fun and exciting.
AI Summary
Expand
MEV is sometimes thought of as the realm of the elusive super-programmer, but it doesn't have to be that way. It can be fun and exciting.

Source | bertcmiller.com

Author | Robert Miller

picture

picture

KALEB previously leaked major news about the policy changes at Synthetix, involving hundreds of thousands of dollars. Sharing this kind of news in this robo-operator's lair is like throwing red meat to a lion, and a quick look at the contract reveals an enticing sum of money in there.

first level title

Step One: Determine the Scope of Opportunity

I'm not an expert on Synthetix, so the first step is to understand what I do. Specifically:

  • I find the relevant contract

  • I read through an overview of their functionality on the Sythetix blog and searched all the documentation

  • I am confident that I understand the upcoming governance changes

  • I look at the functions and look for those that seem relevant

A summary of this stage of work is that Synthetix has experimented with using ETH as collateral to mint sUSD and sETH. You can deposit ETH in the contract and mint these assets, as long as you take care that the value of your collateral does not drop below a certain level of your loan.

But, a year later, the protocol voted to end the trial. How can they do that when there are millions of loans outstanding? Well, you can make any position payable. In fact, after a lengthy warning period, the loan goes from being safe in one block to being repayable by anyone, regardless of the value of the collateral! This triggers a transaction from"pDAO"Addresses transactions sent to the public transaction pool.

first level title

Step Two: Know the Opportunities

Now I understand the basic mechanics and have some functions that I think are relevant. Then, I drill down to what functions I'm going to call, what data I need, and how to generate that data.

I need two functions:

  • setLoanLiquidationOpen(): Can only be called by the contract owner (pDAO). Repayments are allowed on outstanding loans.

  • liquidateUnclosedLoan(): Requires a loan ID and account address, and repays the loan. Can be used by anyone after setLoanLiquidationOpen() has been called.

Note that there are other functions, but I quickly discovered that they were not relevant. Now, I need to work out how I choose which loans to repay, and how much sETH/sUSD I need to do so. The following function turns on most of the things needed to work:

  • openLoanIDsByAccount(): returns all open loan IDs related to an account

  • getLoanInformation(): returns loan data for a given ID and owner

However, there are two hidden difficulties. First of all, these contracts don't tell you which addresses have outstanding loans, which makes it difficult for me. Within minutes, I found a workaround, which was to download all of your transactions with these contracts on Etherscan, and use Excel to create a list of unique accounts that had interacted with these contracts. From there, I built the following pipeline:

  • Use getLoans to find out if there are outstanding loans at this address, and if so, record their loan IDs

  • Use getLoanInformation to find out the value of the collateral backing the loan, how much sUSD/sETH they issued, and how much interest their loan has

  • Sort by loan amount, list the largest loan first, and save the data structure to a json file.

The second difficulty is that how much mortgaged ETH can be repaid for a loan is not immediately known. Because there is a function of outstanding loans, you can get a rough estimate, but I need more precise data. To do this, I studied the loan repayment code and figured out how the numbers were generated.

All of the above data can be obtained or calculated on the chain. But doing so consumes too much gas. Since I will be competing with others for the gas efficiency of the contract, it is extremely important to me to move as much logic off-chain as possible to minimize gas consumption.

After a few iterations, I knew the minimum amount of data I needed to get off-chain was some variables that I could resolve off-chain to inform what I entered on the smart contract. However, the function to get this information is very complex, and querying all loans takes longer than processing a block. This is not feasible. To solve this problem, I wrote a short smart contract that aggregates many data requests together, which resulted in a speedup of more than 10x. Here's one of those functions:

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);}

Please see rawsAssetsOracle.sol provided by EmGithub

Summarize:

Summarize:This phase is about gaining insight into the opportunity and gathering all the data needed to execute it in an efficient manner.You need to do your best to move things off-chain to reduce gas consumption.first level title

secondary title

Flash loan strategy

You may need a dedicated contract to withdraw MEV. I wrote a contract and test environment early on, and I can use this environment to better understand the contract and make sure the data is correct. This is done almost simultaneously with the second and fourth steps.

I know I need to get millions of dollars in sUSD/sETH, so flash loans are a must. Also, I would burn these synthetic assets but get back the staked ETH. After some thought, I realized that I would be exchanging ETH for other assets anyway, but I could choose to do it before or after the repayment. There are two possible paths:

  • Option 1 Option 1: Loan ETH via Flash Loan -> Exchange USDC -> Exchange sUSD -> Repay sUSD Loan -> Receive ETH -> Repay ETH Flash Loan

  • Option 2 Option 2: Loan sUSD through flash loan -> repay sUSD loan -> receive ETH -> convert USDC -> convert sUSD -> repay sUSD flash loan

Gas optimization

Gas optimization

You can find my full contract here, but this is the part after I receive ETH from dYdX and repay the sUSD loan:

 //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);}

Please see rawdYdXLiquidator.sol provided by EmGithub

I spend a lot of time trying to minimize my gas consumption. A lot of my design choices are based on this. Regarding the strategy of this contract, there are a few points to note

  • Instead of sending a lot of separate redemption and exchange transactions, I choose to bundle multiple repayments into one transaction, which allows my fixed gas overhead to be spread among multiple repayments, thereby improving the competitiveness of my transaction bundle .

  • I need to convert ETH to USDC to sUSD in the best way and need to decide whether to trade on Uniswap v3 which has function exactInput or exactOutput. No matter what I do there is going to be slippage somewhere, so I chose to have the function exactOutput to avoid calling balanceOf.

  • There is a tradeoff between the accuracy and gas efficiency of these transactions. The lack of precision is fine as long as I can repay my flash loans, and since I'm competing on gas efficiency, I opted to optimize for it.

There are also some "tactical" things to be aware of:

  • Approvals for everything are forward loaded into my contract's build function. This way, I can pay for the overhead at deployment time and reduce the gas used for execution.

  • I don't burn the gas token from my account, but from my contract, also to improve gas efficiency.

  • The function names are all specified, and the leading character of their function selectors is 0x, and the use of function selectors can also slightly reduce the use of gas.

  • Function modifiers consume slightly more gas than directly adding require statements.

  • There are also some ways this contract can be optimized, such as using gas fees instead of coinbase transfers.

  • Summarize:

Summarize:first level title

secondary title

The Economics of Repaying MEV and Optimizing Gas Prices

Armed with a well-designed contract and a solid understanding of the opportunity, I need to refine my strategy for realizing it. To recap, Flashbot's MEV-Geth client can effectively run an auction where the bundle with the highest gas price wins and gets bundled on-chain. This important fact means that I need to maximize the gas price of my transaction bundle, not the total amount of ETH I pay.

picture

picture

Note that this result is a bit surprising - repaying only the first 4 (out of 30) sUSD loans was the most gas-efficient. Each loan after that will generate more overall profit, but will lower the gas price of my transaction bundle and make it less competitive. If someone else tried to repay the first 10 sUSD loans in one go, their gas consumption would be nearly 30% less efficient!

Given that there are fewer outstanding sETH loans, it makes most sense to only do sUSD loans and not combine sUSD and sETH into one transaction. As a result, there is less potential reward and less money paid to miners, making them relatively gas inefficient. I couldn't help but smile when I saw these discoveries. If everyone else is greedy and pays off the entire loan at once, or is lazy and pays off in splits, then I win.

secondary title

Flashbots auction and my bundle ordering strategy

To repeat the situation: I want to both compete on gas efficiency and maximize my payoff for each loan repayment. The best strategy is to submit several repayments per bundle, in several bundles. These bundles are evaluated individually in the Flashbots auction. However, every transaction depends on a transaction from pDAO, which makes the loan repayable by anyone.

  • If the pDAO transaction is not in the bundle, the bundle will fail. But if I have pDAO transactions in each bundle, only one bundle will succeed. That is, after a transaction bundle is successfully wrapped, all others are invalid because they will try to double-pack the pDAO transaction. Therefore, I need to find a way to only send pDAO transactions in my first transaction bundle, but at the same time ensure that my other transaction bundles don't fail and get thrown away because they don't have a pDAO transaction.

The solution lies in a subtlety of the Flashbots auction. Flashbots performed two rounds of simulations before the search engine started using "tricks" on auctions to reduce miner fees after transaction bundling. First, all transaction bundles are simulated individually, their gas prices are derived and checked for failure. In the second round, successful bundles are sorted by gas price, and the simulation is run again to find inconsistent bundles, ensuring that no bundle has a lower gas price than expected. Unless you want to, you'll probably never have a transaction bundle where the gas price is reduced after the merge.

I realized that I could do the opposite of the searcher above: instead of my transaction bundles paying less gas than expected, they would pay more in the second round of simulation. To do this, I will wrap the pDAO transaction in the first transaction bundle as expected, but do an additional check on the remaining transaction bundles. These transaction bundles will infer what "round" they will be simulated at, and then alter their execution accordingly. If they are in "round one", they won't repay any loans - because they will fail if they try - and then pay the miners anyway to get the high gas price to pass the first round simulation.

After passing the first round of simulation, these transaction bundles will follow the one with pDAO transactions in the second round of simulation. At this point, they can successfully repay the loan. Also, the gas price for these bundles will be higher than the auction expects, not lower, so changing the implementation is not a problem here.

Summarize:

Summarize:first level title

Step Five: Execute

Now I have the data, the contract, and the plan I can start to execute. Basically, I need to build multiple such transaction bundles: on the one hand, it can execute my plan above, and on the other hand, it can listen to the transactions related to Synthetix in the transaction pool for tail tracking. At this point, it's mostly an implementation issue.

First, I used Blocknative to listen to the pDAO account for related transactions. I have any transaction sent from the pDAO account be sent to my bot.

Then, I run two listening scripts (one for sETH and sUSD) at the same time to get data from the chain and come up with the optimal transaction bundle strategy (for example, repay the first 3 sETH loans first, flash loan X ETH, and Do the same for the last 2, etc.) and generate the data my contract needs. I need to do this every block, in case the price changes or someone closes the loan, changing the optimal strategy. These results are saved locally.

In the end, I have an execution script that takes the pending trades sent to my bot, loads the results of the optimal bundled strategy from my listening script, automatically builds bundles, and sends them to Flashbots.

first level title

Step Six: The Moment Has Arrived

Interestingly, someone tried to trick the bot into failing early on by sending a transaction to the relevant contract. I'm not sure if this happens to other people's bots as well, but mine was not coaxed successfully.

A few hours later, pDAO sent the real transaction. After weeks of research and preparation, I knew the moment had come. Everything is going great on my side: my listener script runs fine, transactions are received, transaction bundles are built and committed.

.....and then the accident happened. No Flashbots block has been mined for multiple consecutive blocks. Not only did I lose an opportunity, but no Flashbots crawler won an opportunity. With no Flashbots blocking at the top of the block, an ambitious transaction pool bot steps in and grabs all the profitable loans.

Despite the loss, I think my approach is still correct. My strengths lie in strategy and spotting new opportunities, not in the PGA (Best Gas Auction). So using Flashbot gave me the best chance of winning. Given the widespread adoption of Flashbots, it's pretty unlucky to have several non-Flashbots blocks in a row.

MEV is sometimes thought of as the realm of the elusive super-programmer, but it doesn't have to be that way. It can be fun and exciting. And the rules of the game, if you're looking for them, are open, so to speak. This article is about my process of learning the rules of the games I play, from which I come up with a strategy, and ultimately execute that strategy. Even though I lost, I learned a lot and had fun along the way. I hope you can too, and hope you join me in the next round of games.

Original link: https://bertcmiller.com/2021/09/05/mev-synthetix.html

ETH
Synthetix
Welcome to Join Odaily Official Community