A cat-and-mouse game: how to frontrun a transaction in the future

DeFi Security Summit 2024 · 8 Nov 2024 · 19:35

Qi Su, a security engineer at Flashbots, explains how MEV bots front-run attacking transactions, from the Curve incident in 2023 to the KyberSwap hack, and why that has become less effective as private mempools hide transactions. He focuses on two-phase hacks, where an attacker deploys a contract and later sends a trigger transaction, and argues fuzzing is the right way to find the profitable path inside such a contract in the 12 seconds before the next block. He ends with taint analysis and concolic execution as next steps.

Speakers

Topics

From DeFi Security Summit 2024, hosted by DeFi Security Summit.

Transcript

Uh Is my my Okay, cool. Hi everyone. Uh my name is Shi and I'm a security engineer at Flashbots. So, in recent years, we have seen several cases where MEV bots front-run attacking transactions. One famous example is the Curve Finance incident in 2023. The hack was due to the Viper compiler bug. Several MEV bots, they front-run the hacking transaction, and some of them re- returned the funds to the Curve. And similarly, the security company BlockSec has also front-run a few attacking transactions, rescuing funds from hackers in 2023.

And also earlier in this year, uh during the KyberSwap hack, $5.7 million were returned by front-run bots to the protocol. So, in these instances, MEV bots were used as a force for the good, uh out-pacing attackers to protect users' assets. However, in 2024, the number of and frequency of these stories declined. Our team has been exploring the Ethereum MEV world since early this year, and we have successfully prevented a few cases using the new methodologies. So, in this talk, I will share how front-running works, why it has become less efficient, and the potential future directions we're exploring.

First, let's look at a normal um life cycle of a transaction and how front-running works. Assume you're a user and you want to send a transaction onto the chain, and you have to send it sign the transaction first, and um send it to a builder. Sometimes a RPC node, or sometimes a uh RPC provider. So, block builders collect and sort transactions and send them to build a block. And then, the validator, when they receive blocks, they would choose one block to finalize it onto the chain.

And usually, they pick the one who's offering the biggest reward. So, in real world scenario, there cannot be only one actor here. There cannot be only one transaction, one builder, or one validator. Builders will receive a ton of transactions, put them in the mempool, where it is a buffer for putting pending transactions. They would They would form a block, and then this block will give a max value, and this is MEV, maximum extractable value.

Also, there are more than one builder. Validators receive different proposed blocks from different builders, and they will choose the one that offers the biggest reward. So, let's look at the Let's look at the front running. So, in the early days, the process was severely abused because transactions are in the public mempool, and transactions in the public mempool they are basically visible to everyone. MEV bots can monitor for profitable transactions, like large trades or arbitrage opportunities, and just copy them with a higher gas price in order to be included in the first position of the block. So, this leads to a gas auction war, where the original transaction sender will be outbid by bots. Say, I

have a super powerful algorithm to find the optimal path to during for different pools, but a copycat, he will just copy my transaction and add one gas to it, and he will outbid me. So, all my computation power means nothing. And yeah, that's that's really bad for the arbitrage bots. And this is the process. When the front runner sees the transaction sent to the builder or a public mempool, he will just copy it and with a little bit more gas to it. So, the builder will put the transaction in front of the original transaction and send it to the validator, and the validator will put this block to the chain. And because the first transaction is doing the exactly

same thing to the second transaction. So the first transaction will succeed success and the second one will just revert. This is really bad for the ecosystem. So this environment doesn't incentivize beneficial MEV actions. No one wants to balance the prices between different pools. And block builders they consume most of the profits in terms in terms of the gas. And normal users they also suffer from sandwich attacks. To mitigate these issues private mempool came into play. So there's this group of builder who promised not to share the pending transactions with others. That's pretty self-explanatory. So private mempool they don't leak transactions to others

reducing the risk of front running. This was beneficial because MEV bots now had to compete fairly. For obvious arbitrage opportunities the MEV bot with better swap path or strategy would win. The essence of private mempool also helped hide attack transactions. And also let's look at how the process goes. So the front runner now cannot see the transaction sent by the user to the private mempool. So if there are two builders one is with a private mempool and one is not the one with private mempool will build a block that will have a higher reward to the validator and the validator will choose this block

to finalize it onto the chain. But does this eliminate all front running? So according to our observations many hacking attempts happen in a two-phase style. So the attacker first deploys a contract and does some preparation then sends the final attacking transaction which is a trigger transaction. We call these preparation phase and the triggering phase. So in both phases the actions that the hacker performs are usually encoded in the contracts themselves.

We have seen many MEV bots exploiting these attacking transactions attacking contracts for quite some time. Essentially, whenever a new contract is deployed onto the chain, MEV bots will try to exploit them. They will try call every function inside this contract. So, how do they do it? Calling a function involves extracting the function signatures. Since most contracts are often written in Solidity or sometimes very little of them written in Viper, they follow this standard ABI conventions where you hash the signature of the function. So, if the function call happened to be the final triggering function, the MEV bot can front run the transaction in the

future. They will know what uh calling which function will make them profit. So, the attack and defense game between the contract deployer and MEV bots begins here. Let's assume the deployer has to use this two-phase attack for some reason. They're not using some like attacking in the constructor. Uh then, how can they prevent MEV bots from exploiting their contract? And this is really a problem of program analysis. Imagine in Web 2, when you downloaded a software or program, how does your antivirus program detect if the program is performing some malicious actions or not?

And let's look at a few examples contracts to try to like fool the MEV bots. This is a very common pattern we have seen. A MEV bot can easily bypass this. So, here you have two requirements. The transaction origin and the message sender has to be some fixed address there, otherwise it will revert. But really, this has a very simple solution is that you use a address hint. Usually, these hacking contracts they are comparing the authority to their own addresses. So, you just need to pass in the contract creator.

And this is another type of authentication. Maybe this is This looks like it a little bit complicated, but it's not. So, it's hashing the message sender and comparing to hash. Although hash is not reversible, what we can do is on the bytecode level, we just flip the equal sign. Flip it to less than or greater than, that would be equivalent to a not equal to. And this This requirement is just It can be bypassed very easily.

So, to conclude, triggering functions with no parameters are free to exploit by MEV bots. All a MEV bot needs to do is to explore all potential paths in the smart contract, and there must be one profitable path. So, contract deployers have to of a more complex functions. So, this is the compact function that a contract deployer had thought of. So, it takes one parameter you'd in 256A, and it this parameter is a direct parameter related to the attack.

Maybe there are some other parameters there, but if the parameter passed in is related to the hacking process, it's much harder to find the correct input because, you know, random inputs may not have to uh trigger the vulnerability inside the contract. It has to be a very exact value. However, we found that fuzzing is perfect for these types of tasks. So, before going into how fuzzing works here, let's define what is what is fuzzing. It contains of like five steps. First is to generate generate a random input. This random is not like truly random from you that random.

Um It's really It has some heuristics and smart generations, but anyway, let's just assume we have this random input and we execute the program or the smart contract with this input and we observe and analyze the execution collect interesting information from the uh execution trace. Then we decide if this input is either interesting or the input is the one we are desired to. And uh if so, if it's interesting some certain conditions are met, we just exit.

Otherwise, we repeat and using the heuristics information we collected to generate another input. So, fuzzing is a technique we where we try different inputs to see if it behaves in a certain way. In web two, say a binary executable, maybe this is memory corruption. And in web three, in the context of auditing, this might be invariant breaking. Uh and I know like many solidity development today now contain this process of writing invariants. And before they launch, they will use fuzzer to try to break these invariants to indicate a bug. And in the map world of web three, uh the purpose of fuzzing is really path

exploring. We want to find the specific path that lead to a profitable transaction. And it's apparent that the key is to generate a good input. Generally, inputs can involve various strategies. For example, you can just use random and that's uh basically the luck you need. And also, you have some constant values. These are very special values. Maybe sometimes they do some good uh effects on the fuzzing. For example, some small values or some addresses of zero or max of some uh types etc. And the final thing is heuristics. This is the one uh makes different fuzzers um

effective or not. So, for example, you can use a multiple of 1 to the uh 18th. Uh this is 10 to the 18th, sorry. Uh so, this is uh a very common constant in the math world or the Ethereum world. And also, there are values collected during execution. For example, if you encountered a equal equal check during the execution, maybe the this value is very interesting. Maybe it's a password or something. And also, we adopted some uh methodologies uh open-sourcing our EthiPhuzz. Our EthiPhuzz was basically for um pre-op pre-deployment fuzzing, but it has also a lot of methodologies adopted from the uh

cutting-edge academia side. And next question is why fuzzing? So, the first reason, and I think it is the biggest reason, is it's fast. It's very fast it compared to symbolic execution, for example. And uh you know, in math world uh on Ethereum, each block has a 12 12-second time interval. So, when you see a contract is being deployed, you have 12 seconds to um to be the fastest one to do the triggering on the next block. And it's very accurate compared to static analysis. For example, uh if you're trying to do a full static analysis, a really thorough one, to um to analyze a if a uh hacking contract is uh

exploitable or not, it might lead to false positive, but fuzzing does not because it's using concrete inputs to uh execute. And the last one is it has very low effort to build a prototype. Uh a fuzzing it can be a dumb fuzzer, and sometimes a dumb fuzzer can also lead to some uh profitable transactions. But there are also some limitations of fuzzing. So first one is the number of input parameters. As we see in the previous example, we only have one input A, but if the number of input parameters increases, the search space will increase exponentially.

And the second one is the effectiveness is really depends on the input generation. If there are very poor inputs like truly random inputs, it will never reach the path you you desired. And the last one is is that it's not thorough enough. After all, it's like generating like some pseudo a half random inputs. So some specific path like password generation thing cannot be reached without fuzzing only. And I'd like to point out what we are doing next and we hope that community may take a look at this. So now we are experimenting taint analysis and concolic execution to solve the third

third problem we mentioned before. Sometimes the heuristics can be collected from taint analysis or concolic execution to explore more path. And also we know some other methods that other teams are doing like binary write, they manipulate bytecode. And yeah, and also we we're really like thrilled to see how this cat and mouse game continues where people bring more web2 techniques into this web3 world. Like maybe stack analysis can help some in the MEV world and maybe large language model will help us well.

And yeah, I'm really thrilled to see. And that's all of my talk and I'm open to any questions. I have a question. Hey, thanks for the talk. Uh, I just wanted to ask, uh, you had this slide with authentication and you mentioned that, uh, in that case you could change the byte code to just bypass the check, right?

Mhm. My question is, how do you especially in an environment where you're trying to fuzz very quickly, how do you know which assertions you can bypass or not bypass? Cuz, you know, you you did also mention that the benefit of fuzzing is that it's, uh, reliable. You don't generate a lot of false positives, right? But, if you start changing the byte code, you get false positives potentially. Yeah, so it's really it doesn't matter. Like, flipping one byte is really quick and that's the benefits of fuzzing. Uh, there's not much overhead to actually do like two or three times. I think it happens in nanoseconds.

Okay, but if you need to generate a lot of variations, like if you have to come up with you know, plenty of different enumerations yeah. Yeah, so so for example, for uh, for example, in this case uh, if you fail this, uh, equal and uh, fails this to require, you simply try another like there are two outcomes, equal or not equal. So, basically, if both path fails, uh, this fuzzing campaign fails. But, yeah, that's very low overhead.

Okay thanks. Yeah, I have a question also. I don't know if there is someone else. Uh, it sounds like the the problem sounds like a bit, uh, similar to uh, gaming AI's where you need to find, for example, chess makes me think a bit like uh ideally find the right sequence of moves to to reach the checkmate state. So, is there So, there are some algorithms that fit really well this kind of uh strategies? So, for example, minimax algorithm and so on. Is this something

that you might be exploring in the future? So, for example, applying this kind of strategies to uh fuzzing to reduce the search the size of the search tree of possibilities? Well, uh cuz this is a like analyzing a program is a hard problem problem, and it has no solution overall. And fuzzing is like uh the fastest way of doing it. So, maybe machine learning like it's more, I guess, related to static analysis where you want to have a thorough understanding of the program.

But, in the math world, we need speed. That's the reason. Okay. Uh thanks for the presentation. So, most of the example that you show work because like the attacker are not too sophisticated, right? Because the trigger function is within the bytecode. But, what happened once the attacker gets a bit more better, and the trigger function is not in the bytecode directly, and maybe you know, they delegate code to another contract later on, but you don't know. Uh do you have like a heuristic for that, or have you considered it?

Yeah, that's I think the is the final state of a like uh unbeatable hacker who hides his all his payloads in the final transaction, and this a one-shot transaction, and send it to private mempool, and no no leak from private mempool. That's the final like state of a really sophisticated hacker. But, as uh of like 2024, uh we have see a lot of like two-stage hacking so far, and I think we should like dig into it more. All right. Thanks. Cool. I think it's um it's done. Thank

you.