Synthetix MEV 캡처의 세부 프로세스에 대한 간략한 분석
출처 | bertcmiller.com
저자 | 로버트 밀러
그림
KALEB는 이전에 수십만 달러가 관련된 Synthetix의 정책 변경에 대한 주요 뉴스를 유출했습니다. 이 로봇 운영자의 은신처에서 이런 종류의 뉴스를 공유하는 것은 사자에게 붉은 고기를 던지는 것과 같으며 계약서를 잠깐 살펴보면 거기에 유혹적인 금액이 있음을 알 수 있습니다.
첫 번째 레벨 제목
1단계: 기회의 범위 결정
저는 Synthetix의 전문가가 아니므로 첫 번째 단계는 제가 하는 일을 이해하는 것입니다. 구체적으로:
관련 계약서를 찾았습니다.
Sythetix 블로그에서 기능에 대한 개요를 읽고 모든 문서를 검색했습니다.
다가오는 거버넌스 변경 사항을 이해하고 있다고 확신합니다.
기능을 살펴보고 적절해 보이는 기능을 찾습니다.
이 작업 단계의 요약은 Synthetix가 ETH를 sUSD 및 sETH를 발행하기 위한 담보로 사용하는 실험을 했다는 것입니다. 담보 가치가 대출의 특정 수준 아래로 떨어지지 않도록 주의하는 한 계약에 ETH를 예치하고 이러한 자산을 주조할 수 있습니다.
그러나 1년 후, 의정서는 재판을 종료하기로 결정했습니다. 수백만 건의 미결제 대출이 있는데 어떻게 그렇게 할 수 있습니까? 글쎄, 당신은 어떤 위치를 지불 가능하게 만들 수 있습니다. 사실, 긴 경고 기간이 지나면 대출은 담보 가치에 관계없이 한 블록에서 안전한 상태에서 누구에게나 상환할 수 있는 상태로 바뀝니다! 이것은 다음에서 트랜잭션을 트리거합니다."pDAO"공용 트랜잭션 풀로 전송된 트랜잭션을 처리합니다.
첫 번째 레벨 제목
2단계: 기회 파악
이제 기본 메커니즘을 이해하고 관련이 있다고 생각하는 몇 가지 기능이 있습니다. 그런 다음 호출할 함수, 필요한 데이터, 해당 데이터를 생성하는 방법을 드릴다운합니다.
두 가지 기능이 필요합니다.
setLoanLiquidationOpen(): 계약 소유자(pDAO)만 호출할 수 있습니다. 미결제 대출에 대한 상환이 허용됩니다.
liquidateUnclosedLoan(): 대출 ID와 계좌 주소가 필요하며 대출금을 상환합니다. setLoanLiquidationOpen()이 호출된 후에는 누구나 사용할 수 있습니다.
다른 기능이 있지만 관련이 없다는 것을 금방 알게 되었습니다. 이제 상환할 대출을 선택하는 방법과 상환해야 하는 sETH/sUSD 금액을 계산해야 합니다. 다음 기능은 작동하는 데 필요한 대부분의 기능을 켭니다.
openLoanIDsByAccount(): 계정과 관련된 모든 미결 대출 ID를 반환합니다.
getLoanInformation(): 주어진 ID 및 소유자에 대한 대출 데이터를 반환합니다.
그러나 두 가지 숨겨진 어려움이 있습니다. 우선, 이러한 계약은 미결제 대출이 있는 주소를 알려주지 않아 저를 어렵게 만듭니다. 몇 분 안에 해결 방법을 찾았습니다. Etherscan에서 이러한 계약과 관련된 모든 트랜잭션을 다운로드하고 Excel을 사용하여 이러한 계약과 상호 작용한 고유한 계정 목록을 만드는 것입니다. 거기에서 다음 파이프라인을 구축했습니다.
getLoans를 사용하여 이 주소에 미결제 대출이 있는지 확인하고, 그렇다면 대출 ID를 기록하십시오.
getLoanInformation을 사용하여 대출을 뒷받침하는 담보의 가치, 그들이 발행한 sUSD/sETH 금액, 대출 이자가 얼마인지 알아보십시오.
대출 금액별로 정렬하고 가장 큰 대출을 먼저 나열하고 데이터 구조를 json 파일에 저장합니다.
두 번째 어려움은 저당 잡힌 ETH가 대출금으로 얼마나 상환될 수 있는지 즉시 알 수 없다는 것입니다. 미결제 대출 기능이 있기 때문에 대략적인 견적을 얻을 수 있지만 더 정확한 데이터가 필요합니다. 이를 위해 대출 상환 코드를 연구하고 숫자가 어떻게 생성되는지 파악했습니다.
위의 모든 데이터는 체인에서 얻거나 계산할 수 있습니다. 그러나 그렇게 하면 가스가 너무 많이 소모됩니다. 계약의 가스 효율성을 놓고 다른 사람들과 경쟁할 것이기 때문에 가스 소비를 최소화하기 위해 가능한 한 많은 로직을 오프체인으로 옮기는 것이 매우 중요합니다.
몇 번의 반복 후, 오프체인에 가져오는 데 필요한 최소 데이터 양은 내가 스마트 계약에 입력한 내용을 알리기 위해 오프체인에서 해결할 수 있는 일부 변수라는 것을 알았습니다. 그러나 이 정보를 얻는 기능은 매우 복잡하고 모든 대출을 쿼리하는 것은 블록을 처리하는 것보다 오래 걸립니다. 이것은 불가능합니다. 이 문제를 해결하기 위해 많은 데이터 요청을 함께 집계하는 짧은 스마트 계약을 작성하여 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);}
EmGithub에서 제공하는 rawsAssetsOracle.sol을 참조하십시오.
요약하다:
요약하다:이 단계는 기회에 대한 통찰력을 얻고 효율적인 방식으로 실행하는 데 필요한 모든 데이터를 수집하는 것입니다.가스 소비를 줄이기 위해 오프체인으로 옮기는 데 최선을 다해야 합니다.첫 번째 레벨 제목
보조 제목
플래시론 전략
MEV를 인출하려면 전용 계약이 필요할 수 있습니다. 저는 초기에 계약서와 테스트 환경을 작성했으며 이 환경을 사용하여 계약서를 더 잘 이해하고 데이터가 올바른지 확인할 수 있습니다. 이것은 두 번째 및 네 번째 단계와 거의 동시에 수행됩니다.
수백만 달러를 sUSD/sETH로 받아야 한다는 것을 알고 있으므로 플래시 대출은 필수입니다. 또한 이러한 합성 자산을 소각하지만 스테이킹된 ETH를 돌려받을 것입니다. 약간의 생각 끝에 나는 어차피 ETH를 다른 자산으로 교환할 것이라는 것을 깨달았지만 상환 전이나 후에 할 수 있습니다. 두 가지 가능한 경로가 있습니다.
옵션 1 옵션 1: Flash Loan을 통한 ETH 대출 -> USDC 교환 -> sUSD 교환 -> sUSD 대출 상환 -> ETH 수령 -> ETH Flash Loan 상환
옵션 2 옵션 2: 플래시론을 통한 sUSD 대출 -> sUSD 대출 상환 -> ETH 수령 -> USDC 변환 -> sUSD 변환 -> sUSD 플래시론 상환
가스 최적화
가스 최적화
여기에서 내 전체 계약을 찾을 수 있지만 이것은 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을 참조하십시오.
가스 소비를 최소화하기 위해 많은 시간을 보냅니다. 내 디자인 선택의 대부분은 이것을 기반으로 합니다. 이 계약의 전략과 관련하여 몇 가지 유의할 사항이 있습니다.
별도의 상환 및 교환 거래를 많이 보내는 대신 여러 상환을 하나의 거래로 묶는 것을 선택하여 고정 가스 간접비를 여러 상환으로 분산시켜 거래 묶음의 경쟁력을 향상시킵니다.
가장 좋은 방법으로 ETH를 USDC에서 sUSD로 변환해야 하며, exactInput 또는 exactOutput 기능이 있는 Uniswap v3에서 거래할지 여부를 결정해야 합니다. 내가 무엇을 하든지 어딘가에 미끄러짐이 있을 것이므로 balanceOf 호출을 피하기 위해 exactOutput 함수를 선택했습니다.
이러한 거래의 정확성과 가스 효율성 사이에는 상충 관계가 있습니다. 내 플래시 론을 갚을 수 있는 한 정밀도 부족은 괜찮습니다. 그리고 가스 효율성에 대해 경쟁하고 있기 때문에 최적화를 선택했습니다.
또한 알아야 할 몇 가지 "전술적" 사항이 있습니다.
모든 것에 대한 승인은 내 계약의 빌드 기능에 전달됩니다. 이렇게 하면 배포 시 오버헤드 비용을 지불하고 실행에 사용되는 가스를 줄일 수 있습니다.
내 계정에서 가스 토큰을 소각하는 것이 아니라 가스 효율성을 개선하기 위해 계약에서 소각합니다.
함수 이름이 모두 지정되어 있고 함수 선택기의 선행 문자는 0x이며 함수 선택기를 사용하면 가스 사용도 약간 줄일 수 있습니다.
함수 수정자는 require 문을 직접 추가하는 것보다 약간 더 많은 가스를 소비합니다.
코인베이스 전송 대신 가스 수수료를 사용하는 것과 같이 이 계약을 최적화할 수 있는 몇 가지 방법도 있습니다.
요약하다:
요약하다:첫 번째 레벨 제목
보조 제목
MEV 상환 및 가스 가격 최적화의 경제학
잘 설계된 계약과 기회에 대한 확고한 이해를 바탕으로 이를 실현하기 위한 전략을 다듬어야 합니다. 요약하면 Flashbot의 MEV-Geth 클라이언트는 가스 가격이 가장 높은 번들이 낙찰되고 온체인 번들로 제공되는 경매를 효과적으로 실행할 수 있습니다. 이 중요한 사실은 내가 지불하는 총 ETH 금액이 아니라 거래 번들의 가스 가격을 최대화해야 한다는 것을 의미합니다.
그림
이 결과는 약간 놀랍습니다. 처음 4개(30개 중) sUSD 대출만 상환하는 것이 가장 가스 효율적이었습니다. 그 이후의 각 대출은 더 많은 전체 수익을 창출하지만 거래 번들의 가스 가격을 낮추고 경쟁력을 떨어뜨립니다. 다른 사람이 처음 10 sUSD 대출을 한 번에 상환하려고 하면 가스 소비 효율이 거의 30% 감소합니다!
미결제 sETH 대출이 적다는 점을 감안할 때 sUSD 대출만 하고 sUSD와 sETH를 하나의 거래로 결합하지 않는 것이 가장 합리적입니다. 결과적으로 잠재적인 보상이 적고 채굴자에게 지급되는 돈이 적어 상대적으로 가스 비효율적입니다. 나는 이 발견들을 보았을 때 미소를 지을 수 없었다. 남들이 욕심을 내서 대출금을 한꺼번에 다 갚거나, 게을러서 나눠서 갚으면 내가 이긴다.
보조 제목
Flashbots 경매 및 내 번들 주문 전략
상황을 반복하자면: 저는 가스 효율성에 대해 경쟁하고 각 대출 상환에 대한 보상을 극대화하고 싶습니다. 가장 좋은 전략은 여러 번들에서 번들당 여러 상환을 제출하는 것입니다. 이 번들은 Flashbots 경매에서 개별적으로 평가됩니다. 그러나 모든 거래는 pDAO의 거래에 의존하므로 누구나 대출금을 상환할 수 있습니다.
pDAO 트랜잭션이 번들에 없으면 번들이 실패합니다. 그러나 각 번들에 pDAO 트랜잭션이 있으면 하나의 번들만 성공합니다. 즉, 트랜잭션 번들이 성공적으로 래핑된 후 pDAO 트랜잭션을 이중으로 압축하려고 하기 때문에 다른 모든 번들은 유효하지 않습니다. 따라서 첫 번째 트랜잭션 번들에서만 pDAO 트랜잭션을 보낼 수 있는 방법을 찾아야 하지만 동시에 다른 트랜잭션 번들은 pDAO 트랜잭션이 없기 때문에 실패하고 버려지지 않도록 해야 합니다.
해결책은 Flashbots 경매의 미묘함에 있습니다. Flashbots는 검색 엔진이 트랜잭션 번들링 후 광부 수수료를 줄이기 위해 경매에서 "속임수"를 사용하기 시작하기 전에 두 차례의 시뮬레이션을 수행했습니다. 첫째, 모든 트랜잭션 번들은 개별적으로 시뮬레이션되고 가스 가격이 도출되고 실패 여부를 확인합니다. 두 번째 라운드에서는 성공적인 번들이 가스 가격별로 정렬되고 일관성 없는 번들을 찾기 위해 시뮬레이션이 다시 실행되어 번들이 예상보다 낮은 가스 가격을 가지지 않도록 합니다. 원하지 않는 한 병합 후 가스 가격이 인하되는 거래 번들은 없을 것입니다.
나는 위의 검색자와 반대로 할 수 있다는 것을 깨달았습니다. 거래 번들이 예상보다 적은 가스를 지불하는 대신 두 번째 시뮬레이션 라운드에서 더 많은 비용을 지불할 것입니다. 이를 위해 예상대로 첫 번째 번들에 pDAO 트랜잭션을 래핑하지만 나머지 번들에 대해 추가 확인을 수행합니다. 이러한 트랜잭션 번들은 시뮬레이션할 "라운드"를 추론한 다음 그에 따라 실행을 변경합니다. 만약 그들이 "1라운드"에 있다면, 그들은 시도하면 실패할 것이기 때문에 어떤 대출도 상환하지 않을 것이고, 어쨌든 채굴자들에게 지불하여 첫 라운드 시뮬레이션을 통과하기 위해 높은 가스 가격을 얻습니다.
첫 번째 시뮬레이션 라운드를 통과한 후 이러한 트랜잭션 번들은 두 번째 시뮬레이션 라운드에서 pDAO 트랜잭션이 있는 번들을 따릅니다. 이 시점에서 그들은 성공적으로 대출금을 상환할 수 있습니다. 또한 이러한 번들의 가스 가격은 경매에서 예상한 것보다 높지 낮지 않으므로 여기서 구현을 변경하는 것은 문제가 되지 않습니다.
요약하다:
요약하다:첫 번째 레벨 제목
5단계: 실행
이제 실행을 시작할 수 있는 데이터, 계약 및 계획이 있습니다. 기본적으로 이러한 트랜잭션 번들을 여러 개 빌드해야 합니다. 한편으로는 위의 계획을 실행할 수 있고 다른 한편으로는 꼬리 추적을 위해 트랜잭션 풀에서 Synthetix와 관련된 트랜잭션을 수신해야 합니다. 이 시점에서 대부분 구현 문제입니다.
먼저 Blocknative를 이용하여 pDAO 계정으로 관련 거래를 들어보았습니다. pDAO 계정에서 전송된 트랜잭션이 내 봇으로 전송되었습니다.
그런 다음 체인에서 데이터를 가져오고 최적의 트랜잭션 번들 전략을 제시하기 위해 동시에 두 개의 청취 스크립트(sETH 및 sUSD에 대해 하나)를 실행합니다(예: 처음 3개의 sETH 대출을 먼저 상환하고 플래시 대출 X ETH, 마지막 2개 등에 대해 동일하게 수행) 내 계약에 필요한 데이터를 생성합니다. 가격이 변경되거나 누군가가 대출을 종료하여 최적의 전략을 변경하는 경우를 대비하여 매 블록마다 이 작업을 수행해야 합니다. 이러한 결과는 로컬에 저장됩니다.
결국, 내 봇으로 전송된 대기 중인 거래를 가져오고, 내 수신 스크립트에서 최적의 번들 전략 결과를 로드하고, 번들을 자동으로 빌드하고, Flashbots로 보내는 실행 스크립트가 있습니다.
첫 번째 레벨 제목
6단계: 그 순간이 왔다
흥미롭게도 누군가 관련 계약에 트랜잭션을 전송하여 초기에 실패하도록 봇을 속이려고 시도했습니다. 이것이 다른 사람의 봇에도 발생하는지 확실하지 않지만 내 봇은 성공적으로 동축되지 않았습니다.
몇 시간 후 pDAO는 실제 거래를 보냈습니다. 몇 주간의 연구와 준비 끝에 나는 그 순간이 왔다는 것을 알았습니다. 내 리스너 스크립트가 잘 실행되고, 트랜잭션이 수신되고, 트랜잭션 번들이 빌드되고 커밋됩니다.
.....그런데 사고가 났습니다. 여러 연속 블록에 대해 Flashbots 블록이 채굴되지 않았습니다. 기회를 잃었을 뿐만 아니라 어떤 Flashbots 크롤러도 기회를 얻지 못했습니다. 블록 상단에서 Flashbot이 차단되지 않으면 야심 찬 거래 풀 봇이 개입하여 수익성 있는 모든 대출을 가져갑니다.
손실에도 불구하고 내 접근 방식은 여전히 옳다고 생각합니다. 내 강점은 PGA(Best Gas Auction)가 아니라 전략과 새로운 기회를 발견하는 데 있습니다. 그래서 Flashbot을 사용하는 것이 저에게 최고의 우승 기회를 주었습니다. Flashbots의 광범위한 채택을 감안할 때 Flashbots가 아닌 여러 블록을 연속적으로 갖는 것은 꽤 운이 좋지 않습니다.
MEV는 때로 이해하기 어려운 슈퍼 프로그래머의 영역으로 생각되지만 그럴 필요는 없습니다. 재미있고 흥미로울 수 있습니다. 그리고 게임의 규칙은 말하자면 열려 있습니다. 이 글은 제가 플레이하는 게임의 룰을 학습하고, 이를 통해 전략을 세우고, 궁극적으로 그 전략을 실행하는 과정에 관한 것입니다. 비록 졌지만 그 과정에서 많은 것을 배웠고 즐거웠습니다. 여러분도 할 수 있기를 바라며, 다음 게임에서 저와 함께 하시길 바랍니다.
원본 링크: https://bertcmiller.com/2021/09/05/mev-synthetix.html


