 I'm Raul Schafranek and I work as a formal verification engineer for one-time verification. And today I want to talk to you about rounding errors and what we can do about them. So very roughly speaking, and I'm really oversimplifying, there are two things that can go wrong when we do approximate arithmetic in contrast to exact arithmetic. The first one is, the rounding error that we do is just too big, too far away from the exact result. And that is a problem in itself. But that is not the topic of today. So I want to talk about the second thing that can go wrong and that is what can happen is that you want to approximate a value from below or you want to come from above but you do it in the wrong direction. In other words, you want to round down but you round it up instead. And that can lead to severe security vulnerabilities. I will show you some examples that you will recognize maybe. But I'm not going to work on these real-world examples. I'm working on a simplified example and make sure to understand the two-way trading problem because otherwise you won't be able to follow my talk. Everything like, get to this point, stay with me until this point and then you can make sense of this talk. So rounding errors, we need to accept them. We cannot do exact arithmetic on the blockchain. It's not feasible. And we found rounding errors in Uniswap. And luckily this rounding error was fixed before Uniswap v1 was deployed. But I'll leave it to your imagination how the blockchain landscape would have looked like if this bug was not caught during an audit. And then we had two more examples, Solana token lending contract and Solana token stable swap. So these bugs were actually caught doing why these contracts haven't deployed and at a peak time there were like three billion assets at risk. So luckily again these were not exploited but they were found by Whitehakers before any serious damage could have been done. So I cannot get into detail into any of those but I promise you if you follow my talk then you will be able to visit the links that I put on the slide and you will be able to make sense of these exploits and how these vulnerabilities and how they could have been exploited if they haven't been fixed. So I want to show you the two-way trading problem. And I promise you this is like the most mathematical slide on my entire talk which is strange because I'm talking about rounding errors, right? Okay but I want to introduce to you to my imaginary friend Alice. She's right here, hi Alice. And I will demonstrate to you the two-way rounding problem, the two-way trading problem. So first I'm going to offer Alice a trade and we do it with exact arithmetic and then we are going to replay the role play and then we are doing it with rounding. So hi Alice. So basic scenarios, we have two currencies, we have dollar and gil. Gil is just a fantasy currency and we have an exchange rate currently that says I can get like two gil for one dollar so exchange rate is two. So I'm Raoul, I have one gil in my pocket. And this is Alice. And hey Alice, do you want to trade with me? I can offer you one gil. How many dollars do I get for that? And Alice do the calculation. Alice thinks he gives me one dollar, he gives me one gil. So I need to divide by the exchange rate. So you get one, you get half a dollar back from me. So now I have half a dollar. So I get back to Alice and say hey Alice, I don't want my half dollar anymore. Can I get my gil back? And I offer Alice the half the dollar and she does the computation and now this time she needs to multiply by the exchange rate and so she ends up with one gil. Everything went fine. I started with one gil and I ended up with one gil, right? So now let's do the same thing with rounding. So and for simplicity, I'm just rounding to like there's no decimal, no digit after the decimal point. That's just for simplicity. So now Alice, I'm Raoul. I have one gil to offer. How many dollars do I get for that? Alice do the computation. But she does a rounding error. Do I have a laser pointer here? No, I don't. So she does a rounding error in this computation. So we divide two by one. This gives us two. And then we divide one by two which gives us 0.5 and we are using rounding to the nearest neighbor here. So that means we are rounding up. So that means I get $1 back from Alice. So now I have $1. I go back to Alice and say, hey Alice, I don't want my dollar anymore. Can I get my gil back? And again, Alice do the calculation. This time she's not even doing a rounding error. But she ends up giving me two gil. And that is the basic problem, right? I started with one gil. I did two traits with Alice and I ended up with two gils in my pocket. So in other words, I just like created money out of thin air. So let's bring this example into the blockchain context. So the important thing here in this example was that I needed two traits. And like in many smart contracts you will see a pair of trading functions like a deposit and a redeem function or deposit and withdraw, stake and an unstake function and so on. And so what happened here, what went wrong? Like now have a look at the red line. So I deposited one gil to Alice and then I immediately redeemed it like in the same transaction and I was able to make two gil out of that. So I created money out of thin air. So now, okay, so we don't want that, right? We need to fix that. So we need to like make a sanity check that we don't get like more money out than we put in and this is the second line here. This is my sanity assumption that when I put one gil into the contract or I should be able to get at most one gil out if I immediately redeem. And of course this concept can be generalized. It shouldn't only hold for one gil, but it should essentially hold for arbitrary amounts that I'm putting into the contract. So this is what the typical implementation of a deposit and a redeem function looks like. And what you can see here is like let's walk over the deposit function real quick. So the deposit function accepts an asset amount and then it converts this asset amount into shares just by multiplying the amount of assets with a current exchange rate. Then we are transferring the asset. We are pulling the assets in from the user. Then we are minting some shares and finally we return the shares that we have minted. And the redeem function is similar. And with what I just told you, you can see or maybe you cannot see it because you cannot see the implementation of the multiplication function. But this contract is suffering from the exact vulnerability that I showed you before. And that was present in a more complicated, more complex setting in this Uniswap contract that I talked about earlier. So this multiplication function and this division function is implemented as rounding to the nearest neighbor. And that is the mistake that we did here. But how do we actually know in which direction we should round? There's a very simple rule of thumb that I can give you. I call it keep the change. That means whenever we are rounding up, whenever we have incoming assets, except assets from the user, then we are going to round up. And whenever we are sending assets out to the user, we are rounding down. And if you follow this rule, that means you will approximate your values in the right direction and users won't be able to create money out of thin air and drain your contracts. That is the simple rule. So that means let's revisit the example from before. So let's walk over the deposit function. So instead of just multiplying, I use now a variation of the multiplication function that always rounds down. And it rounds down because I'm sending the assets out to the user. And for the redeem function here in this example, it's the same. So now, how can we actually be sure that our implementation is correct? I mean, this example was really simple and you were maybe able to follow it on the spot. But when you're a developer and working on a real-world contract, your logic will be more complex. So you want to have tests that ensure that you can actually detect counter examples and achieve a higher level of confidence. So we are now looking at a property test. So that is basically just like a unit test, but it has parameters to it. So it has two parameters, shares per asset, which should just... It's just the current exchange rate and it has another parameter assets. And when this test is run, foundry... By the way, that's a foundry test. I don't know if I have said that. So when foundry wants this test, it will insert, call this test with a bunch of random inputs. And that's the benefit over a unit test. When you have a unit test and you want to detect such a rounding error, you basically need to be lucky and put the right numbers into the unit test and guess the counter example. So with this foundry test, foundry does the guessing for you and can it do much quicker than you ever could? Like it can 1,000 samples or 2,000 samples in a couple of milliseconds. So I want now to have you look at line 14 and 15 and see that it reassembles the property that we specified above. You can see in line 14 that we are executing a deposit function and in the same transaction, we are executing the redeem function. And that is exactly what the property above says. And then there's some boilerplate code to that test as well. It's not mandatory to understand, but if you look at lines 2 to line 7, these are just some assumptions that I make over the inputs. And I put these assumptions there just to avoid arithmetic overflow and arithmetic underflow, because if I went into such a situation, my test would simply revert. And I only want to execute the happy path with this test. And then line 8 to line 12 is just a basic test setup so that my contract is in the state that it can actually fulfill the transfer functions that I'm calling in line 14. So fuzzing is good. And you should actually do it when you test for rounding errors. But like fuzzing is not enough. That's the sad message here. Like this third example from the first slide that I showed you. This example, it was the stable swap contract suffered from this rounding direction of vulnerability, although it was heavily fast. And this excerpt that you see here is from the blog post that explains this vulnerability. And just let me read it out to you. So another interesting takeaway is that fuzzing can give you a false sense of security. Prior to our report, Saber had already deployed comprehensive fuzzers for their swap implementation. A researcher looking at the code coverage alone might come to the incorrect conclusion that such extensively fast code couldn't possibly have a vulnerability. All right, so what else can we do to increase our confidence in our implementation? And one possible solution is that we could use symbolic execution on top of fuzzing. So if you see that table on the left-hand side, there are some properties that fuzzing has on the right-hand side, on the right column, you see some properties of symbolic execution. But I don't want you to think about this slide as fuzzing versus symbolic execution. It's like you can get the best of both worlds if you combine both of these efforts. And we recently, so we, at one-time verification, we have a symbolic execution engine that's called KEVM. It's a symbolic execution engine tailored to the Ethereum virtual machine. And we recently added a feature to that that allows you to put boundary tests into it. And instead of fuzzing over the parameters, so instead of choosing random input variables for the parameters, we do symbolic execution over the parameters. And that has different trade-offs. So the nice thing is that, well, for foundry and for symbolic execution with EVM, you get to specify your tests and specifications in foundry itself, in solidity itself, sorry. So that's easier than having to write your tests in JavaScript or TypeScript. Developers like foundry, especially because of this property. But that also means, when it comes to foundry, that you are somewhat limited to the expressiveness of solidity. There are a bunch of safety properties that you simply cannot express in solidity. And that's one advantage of this symbolic execution approach, that you can escape from the specification format, and you can actually use the KEI language to gain additional expressiveness and express more properties. So foundry fuzzing is extremely fast. It's like you can run 1,000 samples in a couple of milliseconds. And that is really important for developers who want to get instant feedback. And compared to that, symbolic execution is slow. So there's a reason for that. So symbolic execution can give you much more safety guarantees than fuzzing can. But that also means it's computationally much more expensive than fuzzing. So it's slow, but it's not too slow. It works. For example, you could simply integrate it into your CI pipeline and let the prover run on your nightly builds, for example. And this shows the benefit of composing both strategies, like fuzzing with foundry and then symbolic execution with KEVM. I don't want to go over every line in this table, but I want to talk about the false positives and the false negatives. So foundry doesn't have false positives. And what I mean by that is when foundry comes up with a counter example, that means that counter example really works. It breaks your code. So it doesn't come up with a counter example that does not break your code. So there's no false positive. But foundry has false negatives. And that is simply if foundry is not able to choose the right input variables, that means it fails to guess the right counter example and at the end, foundry will tell you that test actually passed. And that is like the false sense of security that you get from using foundry alone. So if you use symbolic execution, we cover 100% of the input domain and we will find that counter example. There's no matter what. So there are no false negatives when you use KEVM. Then there's another trade-off and that is foundry is extremely easy to use. I'd argue it's even easier to use than hardhead or truffle for testing because the smart contract developers are already familiar with solidity, like the language that they use to write their contracts. So that makes foundry very easy to use. Symbolic execution with KEVM is a little bit different. It's very easy to try out. If you have it installed on your machine and you have foundry test specified, you can just try running the KEVM on that. And maybe you're lucky and maybe the KEVM will tell you why your test passed or your test was proven or we found a counter example. But in some cases, you will get a third state that is you didn't pass, you didn't fail, but we are not sure. We don't know. And if you end up in this we don't know state, that is when a human needs to drive the proof forward. And that is actually something that needs some practice. I don't think it's impossible to learn. I learned it, so I'm sure you guys can. But it's harder than just calling a foundry test. One final example of running foundry and running the KEVM, Symbolic Execution Engine, on the same test suite. So on the top image, I just called forge test. And I can see the output that tells me, okay, I was running one test and it passed. I tried 256 samples on that test. That means foundry won this test with 256 different inputs. And then I can use, like after I've won the foundry test, I can run KEVM foundry compile and give it a foundry out directory as a parameter. And what this comment will do is it will turn the foundry test suite into a proof obligation for the Symbolic Execution Engine. Like it's a compile step. And then when I've done that, I can actually try to discharge this proof obligation by running KEVM foundry proof. And the output that you see here is the lucky case that our Symbolic Execution was actually able to discharge the proof obligation. And that's why it says top at the bottom. So, but when a test doesn't pass, you will get a count example that is not as easy to link back to the original code of the test than the foundry country example. Or even worse, it will give you this unknown state and like making sense of this unknown state really requires some practice. You need to learn to read these configurations to read these stuck states. So that's basically it with my talk. I have just one more, a couple of more notes. So I work at runtime verification and we have a research department and we just recently posted some open research challenges on our website, research.wantimeverification.com. And if you are a researcher, go to that website, see if something interests you and we have like multiple ways to collaborate with you. Like if anything interests you. All right. And then like one other announcement, a colleague of mine, Richard Hjorty is in the audience somewhere, I see them. He's giving a workshop on formal methods for the working DeFi DAF tomorrow at 11 a.m. in workshop room number three. So if you like this talk, go ahead and visit Richard's talk. It's highly recommended. All right. And that's it. I think we have some time for questions, do we? We have. Do we have a microphone for questions? Hello. Great, great presentation. I have a couple of questions. Can you go back to the table that you show both like passing and symbolic execution? I have it on the screen, but I don't have it on the projector. There it is. Okay. Great. So you, you put like in the passing column that it requires no in human intervention, but you need someone to write the properties. It's the same for the symbolic execution. Right? So if you have good properties, you will catch good bugs. If you don't have good properties, you will have catch no bugs, right? And this is the same with the examples that you show like passing is not enough. This code was fast, but perhaps they are not using the correct properties. So what is, what is your take on this? Yeah, that's true. So this is not like fully automatic. For example, when you, one is static analysis tool on your code base, there you essentially have to do nothing. You can just like hit a button and one slither on your code base. So for fuzzing, you need to write down the tests and like, like getting the tests, getting the white tests is a challenge on its own. It's not like, doesn't come easy. It has to be practice. And the same is even more to when you do symbolic execution. Because where symbolic execution can also be a footgun if you don't know how to use it appropriately. All right. Yeah, definitely. And the other thing very quickly, you put like false negatives, like on fuzzing, which is, which I agree, and you put no false negative on symbolic execution. Or wherever you said that you could have a third state in which you don't know if it's true or it's false. That sounds like a false negative to me. Like, you don't know the answer. The tool doesn't know the answer. So it is, it is like, you don't know. Yeah, but it doesn't say I discharged this proof application and everything is right. It says you I'm stuck. That is, you should interpret this as I need to put more effort in the proof or in the code to get it to like a final state that says true or false. Yeah, but it's the same for fuzzing. When you say like a pass, like a test that pass, it's simply because you didn't put enough time to run it. So it's a matter of interpretation and it's a little misleading. You could try like fuzzing over like the entire input space and then you will also have like no false negatives. You could try that, but like you will never terminate. But that would work. Come to my talk tomorrow because we'll be going over the right properties. That's basically the next talk. Thank you.