Assumptions: The Root of All Bugs
EthCC[9] · 2 Apr 2026 · 11:39
OxFlint_ examines software bugs as gaps between assumptions and real behavior. The presentation presents a framework for developers and auditors to uncover hidden assumptions across abstraction layers and test whether those assumptions hold.
Speakers
Topics
From EthCC[9], hosted by Ethereum France.
Transcript
Noted. Okay. Yes. Well, good morning everyone and welcome to my presentation on assumptions, the root of all bugs. Now, I am Well, Flint, senior security researcher, etc. And my job is to hunt bugs, to look at protocols and see for flaws in the code. And whenever I present findings to my clients, I always see frustration on the faces of developers. Like, always like the question internally, why do we have so many bugs? I tried so hard to write secure secure code and there's still this flaw and this flaw and thank God the auditor was there cuz otherwise would have get to would have gotten wrecked.
And I thought for a long time like how why is this? These are developers are so good, but they still make so many errors. And I think that the answer lies in assumptions. And basically, the gap between assumptions and realities. That's where bugs are born. But what are our assumptions? Assumptions are basically everything that we assume to be true, but haven't verified. They're a basic feature of human cognition. I don't know that this floor can bear my weight, but I assume it can cuz otherwise I can't really function.
It's what Kahneman calls what you see is all there is. The mind takes limited information, makes a best guess, and presents it as established fact. That's what we do every day hundreds and thousands of times. We need to in order to function. But in the world of DeFi, where immutable code carries billions of dollars, the slightest deviation between an assumption and reality can have catastrophic events. These assumptions exist at every level of complexity from the cross-chain, cross-domain to composability, function, logic, and machine level.
For cross-domain assumptions, I can give an example of a recent staking protocol where I was the auditor. The protocol had a unique feature of migration where an off-chain system captured the entire state of the on-chain system through events and held it. And whenever a migration was necessary, a script queried the off-chain protocol, submitted the entire state, and redeployed with the exact complete state as before. Very unique system. And these the components in isolation worked really well. There were audits web 2 and web 3 which confirmed this.
But I know that these teams often work in isolation. So, I ask myself like what assumptions does each team have on the other which need to be true in order for the system to work. The web 2 developers need to assume that the that the events capture the total state of the system. And web 3 developers need to assume that the web 2 system correctly submits the entire system correctly contains the entire state. Which was correct in both cases. But when I went deeper on the off-chain protocol, I saw on the actual SQL query that it filtered on stake events.
Which is a logical assumption. In most staking system, you deposit, you get staked, and the stake event is emitted. So, and anything that's staked that contains the entire stake. But here in this system, you could deposit and then not stake because there are multiple choices for staking pools. So, if the migration had gone ahead, and a small percentage of users, a few thousand, a few million euros or dollars, would have simply been assumed out of existence by the simple deviation between the assumption and reality.
On the composability assumptions, in every ecosystem, may not Ethereum for example, thousands of protocols interact with each other, integrate with each other, and they make assumptions on the others' functioning, which are very often correct, and sometimes not. In the case of Abracadabra and GMX 2, this was such a case. Cuz normally, deposit in 99% of all systems, a deposit is final. It is atomic. Once you have an deposited event, it means yeah, somebody deposited. And Abracadabra assumed this was true for GMX and allowed people to uh take loans with collateral of GMX as well,
deposits at GMX as collateral. But GMX had a unique uh feature where deposits are only pending, and they need to be executed by keeper. An attacker saw the flaw and engineered deposits which could get rejected in GMX, but before they were rejected, he borrowed 30 million in Abracadabra and walked away. Every system alone was correct, but the interaction was based on a simple flaw. Function levels, we can have the example of Euler, the famous hack of donate to reserves, where a very small, insignificant function was added so that users could clear their accounts from some remaining dust.
It was so small that the basic health check to ensure that an action doesn't make your position insolvent uh no, insolvent, was not added because it's harmless. I mean, it's only dust, and who on earth would destroy their own collateral? So, the assumption was this is insignificant. This is not important. But an attacker saw something else. And he managed to destroy so much of his collateral that his position became deeply insolvent, and through self-liquidation with a discount, he managed to steal 300 million from Euler.
Now, most of the assumptions we've seen are kind of complex or delicate or require very in-depth knowledge and logic. But that's not always the case. In a recent audit, I had a developer who had a decimal check smaller than six. And my recommendation was, well, you need to extend this to say smaller than six and smaller than 18. So that the decimal bound decimals are bound on both sides. And as you can see, this was implemented a bit too literal. Because this cannot happen. So the checks become useless.
And when I told him, he was quite frustrated, but also embarrassed. Because he said, "Look, I I watched this code like dozens of time. I was working on it. It's like, how did I not see this?" But when we write code, we write so much code that we don't reread every line every time. We can't. For efficiency reasons, your mind says, "Okay, I know I wrote this." You scan it and you continue. The mind, the eye, sees the intention. But the compiler sees the instruction. And lastly, on machine-level assumptions, we have stringent storage.
Where a developer assumed that a consumed value within the same transaction could be safely repurposed without clearing it. The EVM made no such promise. And an attacker exploited this. So we have assumptions problems on every level of conception with devastating effects. So what can we do about this? Well, the first response is always testing. Because we need tests to find bugs, tests to confirm intended working, more tests, and we find more bugs. But our tools, our tests, inherit our assumptions.
Because what developer what test does the developer normally write? Well, one, the happy path tests. My deposit actually deposits, my withdrawal actually withdraws withdraws. And then a number of tests for the list of edge cases that he has in his head. Like, look, this can go wrong, or I remember this hack. Let's make sure X, Y, and Z. But why would he write a test on an incorrect assumption which he is not conscious of. It makes no sense. If he was conscious of the incorrect assumption, he simply fix the code. So, tests can help in the sense that if you have an incorrect assumption which falls within all those edge cases, it will find it.
But it only covers a small part. Next to it, we have formal verification which is unique because it checks state across well, checks properties across all possible states. So, it does not take into account your assumptions or your uh subjective nature. But at the same time, formal engineers are also humans. They also have assumptions. They need to make assumptions when they look at the design of the protocol. How is it intended to work? What features are enabled and will be enabled? What are reasonable bounds on the inputs, on the configuration parameters set by admins?
And if this is incorrect, then you have a problem. So, formal verification helps really well. But the scope changes from my code is correct to my specification is complete. Which in itself is an assumption. So, if tests can only partially help, perhaps we just need more experience. Yes, this does work this does work. We all know that an experienced senior developer writes less bugs than a junior one. But at the same time, this is a long, costly, and painful process.
Because every lesson learned means a bug missed, either found by an auditor, paid out in a bounty protocol, or you wake up to see that your protocol got wrecked and X amount of funds were stolen. So, it does work, but it is the long and painful process. So, what else can we can we do? I think we can recommend a practice uh to help developers surface these assumptions. Something which I call the inward audit. Now, this is not a magic pill or some AI-powered solution that fixes the world because the problem lies within the human mind assumptions. So, the only thing we can do is look inward and examine ourselves.
This means basically that you pause and you you say, "What do I assume to be true when I'm writing this code? What do I know to be true?" It means practically asking at several times or continuously, "What am I assuming about the architecture? Do I know or do I assume the integrating protocols behavior? What is this function's actual potential influence on the protocol? Am I actually writing what I think that I'm writing or am I making a a blunder? Am I assuming what about the execution environment and finally, above all, what must hold what must hold true for this system not to fail?"
Now, as I've said, this is not a magic pill. This is something a practice which takes time and effort. But on the other hand, what is the alternative? Because probably yes, the sun will rise tomorrow. The view function and the library function work probably. The callback hook probably is harmless. And the auditor will most likely find all the bugs. I'll do my best. But probably is not a good security model. E Suppose to say thank you.
There you go. Okay. So, I think we have some time for questions if there are any. Then enjoy your day. >> Woo!