Smart contract vulnerabilities from first principles / Roman Böhringer
EthereumZuri.ch 2023 · April 2023 · 20:58
Roman Böhringer looks at smart contract vulnerabilities from first principles. New vulnerabilities are discovered every week, and while their details differ, they usually share a few underlying problems. Using a handful of examples, he walks through those root causes and how developers can avoid them when writing smart contracts.
Speakers
Topics
From EthereumZuri.ch 2023, hosted by EthereumZuri.ch.
Transcript
foreign [Music] to this talk about the smart contract vulnerabilities from first principles I think you've already heard a bit about the D5 security and now I want to talk a bit more from the code side how are these vulnerabilities actually cost how can you find them as an auditor or how can you avoid them as a developer let me briefly introduce myself I'm Roman I've studied at the eth cyric so pretty close to here I work as a smart contract developer for various protocols
mainly in the D5 space I worked in the past and as a security researcher in my spare time I also participate in a bug Bounty competitions and auditing competitions and if you don't know these kind of formats basically you you have to find where vulnerabilities there to maximize your payout and to maximize your rank because this is depend this depends on how many rare vulnerabilities you find and I noticed after finding many vulnerabilities over the years I noticed that while the Box are somewhat different the underlying issues are always kinda similar so so at some point I I started
to think what are really the underlying issues of of these vulnerabilities and also of the hacks that everyone reads about and I started to to compile a list for them that you can use as a mental heuristic as a developer or also as a security researcher or even as a D5 user depending on on your role in the ecosystem just as a small warning these are all heuristics so they can help you a lot to to get ideas where to look for exploits and what to look for in a code space but of course you cannot directly apply them to every code base and find directly 10 vulnerabilities would be nice if
something like that would exist but sadly it's it doesn't at the moment so let's let's start by by the first one I think everyone that has programmed at some point in uh knows about off by one errors it's like one of the most common errors that you see when programming when iterating over array or in general that that you're just off by one either one too high or one too low and uh surprisingly this is also very common for vulnerabilities or for books that then can be turned into vulnerabilities because of that I always recommend when a comparison is made you should always
think about what happens at the boundary so directly uh when when the value is reached for instance I've shown here an example that I've seen in an audit in the wild and this was uh was an auction platform and basically they they had a modifier for the time before the auction has ended and the time afterwards and these two phases should be distinct and the developer actually assumed that they are distinct however it turns out that when the block timestamp is exactly equal to the auction and this contract was in a very real state where it was the the auction has already ended but it
also hasn't ended yet and this was actually exploitable here to buy a free nft in that case and this code was actually audited before and it wasn't caught there so so I think just with this mentally heuristic you can catch this error because I mean here I also just thought about yeah what happens if the block timestamp is exactly equal to auction and and this was actually a major issue here because you could have exploited that to together free nft so in that case it was quite a financial loss that that could have occurred because of this bug another thing that is very common for
smart contracts is that you have a source and a destination address I mean if you think about tokens for instance that's of course the obvious example but in in many many protocols you you want to do transfers you want to specify sources and destinations and many programs when they code this they assume implicitly that the source will not be equal to the destination and I think that's a pretty normal and human thing to do when we talk about the source and the destination we always mean that they are not equal if just I think that is pretty normal to do as a human but of course when you don't ensure that in the code the user may be able to specify the
same address and for for some protocols this can be where it can result in huge vulnerabilities and this the snippet that I've shown here this is I've not seen this in only one audit I think in four or five if all it's actually a very similar vulnerability to that one where the the balance is cached before and then you you use on Source you don't use the cache balance but on the destination update you use the cash balance and basically what's what this results in is that you can inflate your own balance if you just specify source and destination you can inflate your own balance as you wish and completely break a protocol
like that and I mean also like these kind of vulnerabilities aren't that hard to catch as a developer or as a security researcher if you just have this mental heuristic in mind that you should think about what happens if the source is equal to to the destination another thing that's very common for D5 protocols and in general smart contracts is that you have symmetric code function pairs I mean the the obvious example here are deposit and withdrawal functions but they this can also be administrative functions for instance the pause and then on pause function and when you have these kind of functions
they should always be symmetric or most of the time they should be symmetric there are a few exemptions but most of the time there should be symmetrics so this means that a withdraw function for instance should undo all side effects that the opposite function did and surprisingly this is also quite common that one of the side effects is forgotten maybe it's only a small side effect but oftentimes this can lead to other problems down the road and I I've given a very simple toy example here basically deposit and the withdrawal function and the the deposit function marks addresses over a certain balance as a as a premium addresses and I mean
these kind of patterns are relatively common that you may have a side effect depending on the balance or depending on the amount and then you when you when you look at this code or when you develop this code I really recommend to compare like both functions maybe even line by line because then this is extremely easy to catch when you don't do that this can be very hard to catch because it may be a small side effect but when you really compare them side by side things like that or easy to catch and uh but they they if you don't catch them they they can have like pretty major consequences depending on on the exact code
in smart contracts I mean it's pretty normal that there are rounding errors and in general this is not a problem however it can become very problematic if the surrounding errors are amplifiable in some way and I've I've shown a small example here there's actually there are parentheses missing here should be parentheses here but but let's assume that there are parentheses in the days Delta line and then what this function does is it first calculates how many days have passed since the last interest accrual and then it uses this value to to calculate the interest that is due for for the use or
for the position I mean this is a very common pattern this is also actually more or less from a production a code space where a similar code was in and this generally works if you accrue very rarely I mean if you accrue interest every 200 days for instance this isn't too problematic to do it like that but of course the the problem is that the days Delta that this calculation runs down so if you call this after 20 23.9 hours for instance the day's Delta will be zero and the whole interest will be zero and this was actually the the vulnerability in this protocol this was
a mortgage protocol so pretty high values million or a few millions and then of course if you take out the mortgage there you you have an incentive to just call this function every 23.9 hours because you get a free mortgage like that and I mean paying five or ten dollars for the gas fees is isn't too bad to to get a free mortgage so I think that's a deal that most people would take and there is also this error uh that they this was tested this function and it worked well in the test but the problem was they only tested it with whole days and they didn't think about the rounding errors or what happens if if values are rounded down
and uh think that that's always a good thing to to have this in mind and I mean it especially problematic if you divide before multiplication these are usually the scenarios where things like that happen where you have these amplifiable rounding errors where suddenly instead of one way you you lose one E4 or or things like that so solidity is a kind of special because it doesn't have an option type so you you don't have none or null like in other languages and a very common pattern to to check if state is uninitialized or if some variable is
uninitialized or if some mapping key is initialized is to check if it has the default values or zero for instance in in the case of an INT or address 0 if it's an address and I mean this pattern is generally fine but it can become problematic if there is a way for the user to to set this value to zero because I mean then the you in execute the initialization logic but then the user can reset the value to zero or address zero or whatever the default value for for this particular type is and then if this code is executed the second time the init logic is executed the second time as well and I mean as
you can probably imagine this this logic is usually written with uh in the mind that this should only be executed once so this can often lead to quite large problems not only security problems but sometimes it just leads to a denial of service and leads the smart contract in some weird state where it doesn't function anymore simply because this was never intended so this is also something to to keep in mind when you see such logic I always think about is it possible that the value will have this this default value again and if so the recommendation is to use an explicit
initialization flag so just a Boolean you said that once and when it's set you you do not execute the load check again it has a certain overhead but sometimes it's unavoidable and something that's more testing heuristic is that for many many functions n function calls with the value X should be more or less equal to one function call with the value n times x and for instance if you think about the deposit function in a small contract if you call this function 10 times with one if or if you call it one times with 10 if the
resulting system State Should usually be quite similar that there may be small differences and there might be valid reasons that there are large differences but in most cases the the state should be similar and that's that's also what what you would expect as a user and I mean this is very easy to test this is also very easy to test with fast test you just split up a given test and run a small Loop and then compare the two states nevertheless it's often able to to catch some errors for instance it might be able to to catch surrounding errors or it might be able to catch wrong init logic um and and the this mostly because I've seen like test reads
that only call a function once and the system completely breaks if the function is called the second time but they they don't test this and I mean of course there's also something that you should test don't only call your function once call it multiple times in your test suite and check that nothing breaks like this foreign common in in quite a few smart contracts and that leads to to a lot of vulnerabilities are the handling of duplicates and this is all the the snippet that I've shown here this is also from an audit I did was also not catched in in quite a few prior Audits
and basically what the problem here is is that the developer first cached all the balances of a user provided token array and then paid out these cash balances and this generally works fine but it becomes problematic if the user provides a duplicate in in this list because then his balance will be there two times and he he will get paid out two times or even multiple times and I mean you may wonder why why the program or even use this weird pattern or this use this weird code right he didn't initiate the transfer directly and the reason here
was that that he wanted to follow the check effects interaction pattern very closely so he wanted to do all interactions in the end for security reasons which is generally a good thing to do because of re-entranty attacks but he actually introduced another vulnerability when refactoring the code to to address this this first availability so and I mean this is something that that is really relatively common that you see a lot because oftentimes also developers do this implicit assumption at least they they won't have duplicates but then they forgot to to check for it or to disallow lists that actually contain duplicates
and as you probably noticed by now many vulnerabilities are are caused by assumptions that a developer makes by implicit assumptions that he doesn't then explicitly check in in the code and also this one is is the same uh basically um something that you often see is that the developer assumes he can just query the erc20 balance or the ethereum balance of the contract to get the sum of all deposits that were done for instance over a function however the problem is you you can always just manually transfer tokens to a contract and you can also always
manually transfer if to a contract even if it doesn't have a receive function and this can cause some problems because when when it might may result in situations where some variables are out of sync where the developer assumed that his manually kept variable with the deposits is equal to to the balance off and these discrepancies can be quite problematic and then the actually have have caused the major problems in the past also when calculating exchange rates for instance that you were able to transfer a few tokens before the calculation and things like that so if you see this that the balance is queried either of tokens or if always
think about if this it becomes a problem when when a usage is manually transfers tokens to the contract or if this is properly handled I mean it's not always a problem it can be it can be that these tokens are just lost or or just just transferred to all depositors which is generally fine but what they can cause major problems so these were a few mental heuristics and common things to look for when when you as a developer or you as a security researcher or even as a defy user that just checks the code on ethoscan looks at the Smart contract in the GitHub
repository that I linked here you can also find all of these heuristics and also a few more and you can also use that when you are developing yourself contracts or when you are looking for vulnerabilities yourself in a contract to use basically as a reference sheet and then to to check if you find one one type of these vulnerabilities or attack patterns in in the smart contracts you are looking at thanks a lot for your attention [Applause] [Music] thank you so much Roman serious time for questions there is one hand already here
yeah thanks for the presentation was very interesting um you seem to have some experience with C4 audits so for people that also want to kind of like participate in these type of contests what advice would you give yeah I mean I think for for C4 specifically so so basically first you you have to uh I mean you have to get to a certain level to just find the basic vulnerabilities but afterwards like I said C4 is kind of special because your payout and your your rank is determined on how many Auto security researchers find the vulnerabilities so I would there I would try to really focus on finding rare vulnerabilities start try
to get creative maybe use a few of these uh patterns I personally use these in the past to get attack ideas and a few of the vulnerabilities that I've shown here were actually a solo findings then on on C4 I mean they weren't so obvious like here on a large slide in in font size 20 they were more hidden but the for for C4 and this kind of contest format my recommendation after you have like the basic security knowledge and participated in ctfs is to to really try to get creative try to to think about how could this contract be exploited in maybe even some exotic rays and this usually helps a lot in in this kind of
contests are there more questions if not then well thanks again Roman [Applause] [Music]