Design Mistakes

DeFi Security Summit 2023 · 15 Jul 2023 · 21:42

Hari Mulackal walks through quirks and design mistakes in the EVM and the history behind them. He opens with the Ethereum deposit contract, whose balance never goes down, to show why tracking the total ETH supply is hard. He then covers returndatacopy and a Solidity optimizer bug it helped cause, and argues that transient storage scoped to a whole transaction creates deadlock-like surprises, suggesting scoping it to each call instead.

Speakers

Topics

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

Transcript

so I want to start the talk right away with um an infinite money glitch in ethereum so what if I told you uh there is a contract on ethereum that actually lets you deposit either in it and later on you can actually withdraw whatever you deposited sometimes you can withdraw more sometimes you can withdraw only less but at no point um and it's in its lifetime though the balance of this contract ever go down would you believe me how many people think that this is impossible raise your hands seems like a lot of people think that um but turns out

would you believe me more if I told you there is 50 billion dollars in this contract right now it has eight leading zeros and if you look at the um the contract on either scan and see how it was deployed it was uh deployed using a tornado cash funded wallet and the person who wrote this contract is uh sitting here right now with us um anyone any guesses what is this contract yes it is the ethereum B contain contract so what is going on here so if you if I look at into the the either scan chart for um the balance and eat um of this contract it only ever goes up

it never went down even though you can actually withdraw from this um here's a different chart um this time from uh names and AI um you can see that the balance actually goes down at some point um uh around around April were the withdrawals was enabled um in the beacon chain so again what is going on here um so the withdrawals are actually processed on the the big contain rather than on the you know proof of work chain what sorry rather than on the execution chain from the

contract and the big contain simply just like gives you the Eid without really like changing the balance of this contract and this contract only stays as this like black hole which lets you like deposit it and run a validator um and you know nothing else really happens so here is like an infinite molecule it's um you you deposit third to eat into this contract you run a validator for however long you want to and then you withdraw 32 eat and all of a sudden you just um increase the balance of the deposit control by 328 without really like uh you know without out of nowhere I don't know how to profit from this but

yeah there you go so what why is this a big deal um it's interesting because calculating the total supply of heat is a very difficult problem in case of Bitcoin there's a very nice way to show that it can only be at most 21 million um but with ethereum it's not that easy there are contracts like there are accounts like 0x dead 0x0 which people use for burning eat would you actually consider the Eid in these accounts to be part of the chain if you if you think about it from the protocol point of view you actually have

to consider this to be part of you know part of Eid in the chain um nothing is like truly burned if you just uh send something into like zorlex dead um so if you try to compute the total amount of heat in in uh ethereum protocol so what you can do is uh at the beginning you can look at how much Eid was at the Genesis and then you can look at how much Eid was generated uh on minor Rewards um the eip1459 actually introduce the way to like properly burn eat for the first time in the protocol um this was e that you know unlike sending into like a valid uh like 0x0 or Xerox debt you this is actually removed

from the protocol uh this is either that you spend on the base fee uh but then there was the B contain as well which introduced like more emissions um to the validators um and also like slashing tensile and and there was the merge which actually merged the proof of work chain and the the beacon chain um and this introduced this exception that the balance of the deposit contract only ever goes up you can never like make it go down so you can't really like look at every single account on ethereum and just like add up the balances because then you're double counting this uh uh the E that you withdraw from the

beacon chain um we can say so the moral of the story is tracking the supply of Eid is pretty hard you have to work around a lot of these exceptions um so yeah so what is this talk about um I want to talk about a lot of quirks in the evm I want to talk about some of the design mistakes I mean these are subjective things and some of the history behind it there is often a real reason where somebody thought some design was good at some point and then later figure out maybe it's not so good anymore I want to start with return data copy

it's a very important instruction in the evm this is what enables proper Dynamic data types in in solidity without this there's no Dynamic data types very very underrated instruction so let's think about what happens when there is an outer bonds access in an evm in general if you look at memory if you read from an offset that's beyond whatever is the current allocator memory which is you know M size you actually get zero um on an out of bounds axis in memory gives works as if you know there was nothing if you look at call data you know you can do a call that uh copy of a region

that's uh out of bounds for call data and it assumes as if there was nothing over there you would get values I mean you would get zeros the same would call that a lot if you really call that a lot from something about collateral size you get zero however with redundant copy it's different if you actually try to read written data that's uh beyond your bounds you actually get a reward um how did we get here so turns out when the instruction was proposed in in the IP 211 the original proposal actually wanted to keep the semantics to be same as any other instruction in evm that is if there is

an auto bounce access you treat it as if you were reading from zero however there was a suggestion by Nick Johnson from ens and he basically suggested oh this should never happen I mean a reading from out of bounds should be a reward anyway why don't we just reward and this was in turn implemented in the EIP um so the EIP was implemented mainly because of the suggestion and and let's see what happened um because of this um turns out this was the cause of a bargain in the in the salty compiler um I will try to explain what happened um

so salty has an optimization step which allows you to remove um unnecessary rights to memory so if you can figure out that there was a right to memory and you never read from this memory location you actually can't remove this right because uh you can say yes um but it's not that easy anymore with return data copy you also have to check if the if there was an autobounce axis and uh any other instruction doesn't have to like you don't have to do the stack for any other instructions and salty at some point did not do this check and there was this optimizer bug where you can remove the combiner can remove a return at a copy

even though it might actually reward of course this um case is like very hard to happen in real life because in most cases the value of size here would be returned at a size so where um a reward is actually impossible but I just wanted to give an one example of a bug caused by this exceptional Behavior and if you look at go ethereum as well curiously enough the return data copy and the return of the buffer is actually involved in a lot of ebm bugs over there it's not really because of this exceptional case but still it's very curious how this one instruction is the cause of a lot of bugs across the ecosystem

the other thing I wanted to talk about is about transient storage so transient storage is an EIP that's you know currently in progress maybe Implement may actually happen in the next hard Fork but I just wanted to like talk about some Curious like edge cases here um so transient storage in short is it's like storage but only lasts across a transaction so before the 10 seconds starts it's going to be also us and at the end of the transaction with the steroid any um we are going to like zero it anyway like throw it off um one common use case for this is re-entrancy logs um let's talk about let's talk about how

re-entrancy logs are implemented today um so most of the time you just keep a variable in storage um you know let's say a Boolean called lock and at the beginning of a call you can just assign this lock to be true which means we actually lock it and at the end of the call you can just unlock it and before anything happens you can just say you can just check if it's already locked and if it's already locked you just reward and this actually prevents any any re-entrancy but this is actually not how it's implemented because of you know gas so instead of going from like zero to one to zero you actually go from one to two to one and one one being like the the log sorry the

unlock State and um when you when you initialize when you deploy the contract you start by initializing this variable to one this is because of like some um changes in like some issues with the glass refunds that make the second one more uh more optimal um in Practical cases but there is one um one foot gun here which is if you actually forget to unlock you have a contract that's now in deadlock you can you cannot use the contract anymore your contract is stuck so how does a transient store help here

so you can actually implement the same thing but with transient um using transient storage you can just set this log to True at the beginning to indicate a lock position and you can set it to false at the end of the call to unlock it um the same check as before everything is the same except that you work with transient storage however there is one peculiar case here which is if you actually forget to unlock there is actually no Deadlocks anymore so this is actually safer because you know there is no room for Deadlocks anymore let's think about it um which brings you to the question if you don't even have to lock why do we

have to waste hundred gas to actually do the unlock right this brings us to like the question about safety versus chaos um there are a lot of uses uh probably sitting here right now who Skip payable in functions to say like 10 to 20 cash um these users may be tempted to actually like skip the unlock so what does this cause you can actually save 100 guys by you know saving download however this actually creates uh what could be called let's say transitory deadlock where if you try to use the same the contract again in the same transaction you you actually have a deadlock you know you

can't use the same I mean you get a reward um and this transient storage basically introduced this in a very intra transaction State and you know that probably like breaks some some sort of like mental models for you know both developers and you know everyone sitting here um so what is a better design for transient storage you know that can like fix this problem so one way to like fix this is you know instead of thinking about Trends in storage at a transactional level think about it at a call call uh level so at the beginning of a call you have a fresh set of transient storage and at the end of the call everything gets cleared and

in my opinion it's a better design because you you actually don't have to unlock by Design it's it's a very cleaner way to think about um this um and a lot of the invariants you think about can remain assays there is like no notion of intra transaction um you know Deadlocks um yeah that's it about uh transient storage and I want to do one last thing which is like a pet peeve of mine um if you do any like evm tooling this is like one thing that has probably like bit you uh at some point um it's about the it's about the order of operations and shifts

so almost every operations have a pretty common order which is now if you look at division of X Y it's x divided by y however if you look into shifts um they actually have the opposite order if you do a shift of X Y you actually are doing you know y shifted by X why is that this is actually this was actually designed to save gas um how does that how does it actually save gas let's let's look into it so in solidity when you write an expression like you know F divided by G um most of the time what people expect is to evaluate it from left to right

um I'm talking about the side effects of the function not really like the operator precedence that's now of course would be um would be valid as in like any other other language but the the side effects you know fnz can have side effects most people expect it to have to happen before G however if you actually implement it this way it's actually less efficient because you need an extra swap instruction to put the arguments in the correct place in the stack for the division operation so shifts were actually designed to prevent this you know the well if we had to redesign evm all again you you probably can like you know like

swap though um order of every single instruction but you know shifts was particularly designed to prevent this you know one one issue this is like the only instruction with this you know opposite order um and if you have like built some kind of tooling this is something that you have encountered in the past now you have spent like hours trying to debug something only to realize that the order of operations were were the opposite it says one other quirk [Music] um so what do you what do we like take away from all these things um designing the evm is very hard there are so many different users that you know there are people who build the

compilers people who build tooling people who build uh VMS like Criterium and you know other clients people who build uh tooling like Foundry uh people who write code people who want optimize code people who want like optimize for safety so having something designed for all of these people is very hard um yeah just think about it much more deeply before when you see a new proposal that's that's my only takeaway from this thank you thanks so much thanks so much I was deeply crying when you talk about this shift left I remember my hours of misunderstanding why thanks

so great great and inspirable talking about even we go into the roots of blockchain and I believe there are a lot of questions here to our great presenters so guys hands up foreign there if you're doing that stack based system for the t-stores and stuff when it unwinds back up what if the lower level has set the same storage slot what happens just check the IV I don't okay okay one more question here

just uh note if you sort of distract and send to yourself you can burn ethelium so maybe they should implement it there yeah but it's still kind of is it really burn from the protocol is it I see yeah another another Edge case yeah any questions so uh I guess as we go further into the blockchain world right what do you think uh evm will take Inspirations from something like introducing a heap and like uh like aslr or like you know more

security uh into like the VM design like how in the traditional World VMS have these so what are your thoughts yeah it's a good question I think one of the challenges the contract size so that really limits like the design a lot you can't you know you want to you still have to compile things down to like 24 kilobytes so you can't have too much uh too much on the evm it still has to be somewhat small maybe if we can restrict like some of the contract size issues um you could you know have complex constructs but yeah without that I think very complex constructs that require a

lot of bytecode may not be like a good idea yeah because then you have to think about the performance of the nodes you know there are some people expect every transaction to be executed in like a certain amount of like milliseconds and if you if you don't have this invariant then you know you either need like more you you either need to like increase the um you know compute requirements for a node which is very low right now for ethereum other other blockchains don't have this um so yeah that's one thing you know UVM was sort of designed so that anyone can run a node um

so this invariant may be like may not actually hold true with more complex things you know bigger by code sizes and all yeah perhaps one more question there yeah so about the out of other you know other bond rate right I agree that it's bad to have inconsistent you know in a standard but what to do right uh you know if we suppose we are allowed to design another version do you think we should just maybe make it first make a consistent second should we just make it as exception I I don't feel like a great Autobahn that gave you at zero it's it's a is a nice solution but you know really like two

question what should we do and what will be the option yeah I think consistency is very important um so if you're designing things from scratch like keeping inconsistent is very important like either either way um but I personally like the non-rewarding case um I think it's easier to analyze op codes if they don't have a lot of side effects and uh yeah that's what I think okay thank you guys thank you Harry Applause man to really great report about event