Building Safe Contracts
DeFi Security Summit 2023 · 14 Jul 2023 · 33:00
At DeFi Security 101 2023, Nat Chin, a security researcher at Trail of Bits, explains how to write safe smart contracts, using rock climbing as a running analogy for safety and preparation. The talk covers specifying how every function should behave, unit testing with input ranges and edge cases, and fuzzing with open-source tools such as Slither, Echidna and Medusa so bugs are caught during development rather than at the end.
Speakers
Topics
From DeFi Security Summit 2023, hosted by DeFi Security Summit.
Transcript
all right so we're going to switch to the next session on today's agenda and for all of you who are participating or who started looking at the CTF like we said you can reach us at on the telegram channel that we shared and hopefully you've already got the GitHub but if you don't then reach out to any of the volunteers who have these t-shirts on like this for for the CTF for for any other questions so thank you thank you Rajiv for the Fantastics in the secure way home team for the Fantastic introduction to the CTF and also helping us the volunteer team for the whole device security Summit
uh well this as you some of you know this event last it was started started last year and it's a recurring event and last year we had an opportunity to have Netherland chin from 12 bit doing the security track for us and we agreed to do it again is a leader in the security research she is working on on basically are their security their security tools and kidna are at least later and others and she's gonna present right thanks all right um so yeah I'm Nat I'm from Trailer bits
um and today I'm going to be talking about how to write safe smart contracts and kind of the um I'm just going to close this keys to the key is to writing um safe code a little bit about me I'm Nat um if you want me to find if you want to find me on Twitter my handle is here um at trailer bits I'm a security researcher so that means I do security reviews I work on tooling and I get to attend conferences like these to show
what kind of how we're using tooling and kind of talk about the public resources that we have available just to make sure that they're as accessible and as available as possible um personally I I'd love to climb things I like to fall off things and generally that means I'm falling off rocks just around the world on the trail a bit side um we tend we like to try to essentially help people build safe software um what that means is we do a lot of research and engineering and r d based on how to make sure that code is as safe as possible
um but that means that everything that we learn from our research feeds into the creation and the development of our tools which are all open source um you'll probably be familiar with a few of our tools and like our kind of skill set um Slither Echidna Medusa and a few others that a few other tools that help essentially make that developer experience a little better so it did say it was a bit of a climbing nerd um in climbing generally rock climbing without protection is kind of a life and death situation if you're climbing without bouldering mats
or you're climbing without clips and carabiners it's something that's considered fairly risky the safety considerations that we consider in rock climbing are very similar to that of how to think about like smart contract security we work really hard essentially to try to solve either man-made problems or natural problems but a little bit of beta which is essentially tips and tricks on how to get up a route goes a very long way to actually understanding and solving a problem so in this particular comic um the the bottom beta in the bottom it basically tells you in order to climb a
route to either grow taller or just be better at climbing which obviously is probably not the best advice if we tie the concept of beta back into kind of smart contract security and Security in general um there is a parallel around like for example a stack Overflow question um and like the solution if you consider asking a question on stack Overflow the beta is basically the answer it doesn't necessarily matter what this question is even asking um but we're going to be talking about I think I skipped uh we're going to be talking about how to write safe code and
how to write safe contracts the beta here is probably going to be fairly self-explanatory and kind of something that should be easy to follow along with um and the whole goal of this is to create kind of somewhat of a framework and some more understanding around what would help security researchers in understanding your code base and what would help with making sure your code base has doesn't have bugs while you're developing so essentially allow you to catch your bugs as early as possible so in climbing we also have this concept we have this concept of a start hold
this is where you start with your problem this is where you start with your climb generally this is marked with a start marker like this so in this particular problem you have to start with four four limbs on that big Square volume and you have to essentially jump over to the others this defines essentially what is the end and what is the beginning of your root so in order to start let's figure out where exactly we should start this is an interesting um concept because specifications even though they're probably the dullest to
write or the most important to have for your code by figuring out where you're going there's a much better chance that you're able to determine how your code is not working properly when you create specifications this should include information like what are you actually trying to build what kind what who your users are and how your users are going to end up using your code and especially how your code is intended to be used outlining these mean that you're not necessary you have a Target to reach having having these means you have a
Target to reach and it also means that you have the ability to know what future Implement what future features you might want in your code and what future areas you want to extend your code in so in terms of what a good specification should include there's there's a whole lot of information that could go in these specs um and so this is kind of a highlight of what would be the most important generally when we're writing code we make a lot of assumptions around your code is only supposed to work under certain conditions or a user calls your function with a certain input range or the
endpoint or the caller of your contracts is supposed to be a certain address if you are writing down all of the specific all of the assumptions that you're making and including that in your specifications it means it's much easier in the future for someone to be able to read all those assumptions and understand when this code is supposed to work and what is supposed to happen if those assumptions don't actually hold we'll be coming back to ways that we can verify that the assumptions actually hold and there's a multitude of ways that that can be done but having those those assumptions and having that in your specification
um is going to go a long way to actually understanding um what code you actually want to check and what your what you want your code to do more specifically the other benefit of writing down all of your assumptions is that you're then also able to identify all of your edge cases and cases in which your functions are supposed to work so if you have an idea of how your function or how your code is supposed to work under the proper inputs then your you should be more able to actually analyze the edge cases and cases that are fall outside of that if in the case that you do have any mathematical computation and kind of
assumptions proving any kind of formula is going to be very useful to actually verify to actually check that your math works as you intend one common thing that we see generally is just that the there's an assumption that like a math function is supposed to work under certain inputs but that doesn't actually hold for the entire subset another thing that should be included in these specifications is how a user is supposed to interact with your system so having a list of features and functionality that a user can perform is one thing but also having
the full call stack between what if what function a user calls and what functions and different checks get invoked after that is going to be very useful to actually check that you're doing a all of the data validation that you need to do and B checking that you're actually making all the state changes that you need to make so that ties into Data validation assumptions as well by that we mean if your function is callable by and only by like an only owner modifier than what other checks that you what other checks you do you
need to make based on the other inputs that are coming in for all of the functions where you have even publicly callable functions with like arguments um are you actually doing the proper data validation that needs to be done another aspect that's important to outline is when your code is actually supposed to work so when is your code reverting and when is it when it should be successful by writing down all of the cases in which your code is actually expected to revert that allows you to have a better mental model of your
architecture and allows you to see essentially end to end if your system is interact if you're interacting with your system in one way how what impact does that have on your code base and on the other side specifying when your code base is or when your function caller Etc is expected to not revert outlining these is not just important for implementing automated testing um but is incredibly important for actually understanding how your code is expected to work going more into kind of um the the code side
um having that spec um on all of your publicly callable or all in all of your functions um is useful just because you're able to specify essentially what um understand what the function does what are the inputs that are expected to come in and what are the expected return values from that you have a much better idea of per function what is expected to happen um this is one thing that's it sounds very easy and especially if you have a small code base it may be easier to do but as your code base grows having Nat spec is going to be even more important to actually identify how each of how all of your functions work together and
isolating logic in between those different functions so similar to user flows um another thing that a good specification should have is creating user guides essentially a user guide is outlining how a user is expected to interact with your system if they're going to be specifying for example slippage values how are they defining um how are they determining like what slipped value to choose and kind of examples in terms of how how to choose these inputs the benefit of this is that once you have
um all of this this whole list of what a good spec has it means from a review perspective and from a developer perspective you know what your code does and that assumption is the most important once you have it all written down it's it becomes a lot more concrete as to what the code is expected to do so the the benefits is basically you know what your code is supposed to do and you know it's how it's supposed to work so it's not just you know my code implements a liquidity pool and it does swaps that's a bit too vague to actually verify that it has no or to check that it has no bugs if you have these
detailed specifications it's much much more easy to essentially determine what the impacts are so that brings up an interesting question because if we're writing down everything um as we're creating these specs it doesn't necessarily mean that our spec is is with something that we create once um and then leave it as is the assumptions that um we assert essentially are making for like writing this code is generally something that changes with time is where you know adding new functionality as we're making new changes to the code base making sure that that spec is up to date and essentially updated every time
you change code in the same way that a unit test should be written for every time every function you add is a way that you can make sure that that spec is always live and always reflects essentially your your most recent changes as well as your most recent assumptions there's generally a confusion um with especially code bases where we have outdated documentation and new code send is your code the single source of Truth and if it is how there's there's it's very unclear how to verify how your new code is expected to work in comparison to previous versions that are documented
so that's quite a lot about specifications and that's one tool that we can use to essentially be able to help us make essentially identify what assumptions we've made on our code I'm going to make sure that those are explicit um but there's a lot more tooling that can be used to help us make sure that we've we're actually making sure that our code is safe so also with climbing we have this concept of tooling as well generally on really big walls we'll have like roped carabiners anchor points where essentially you can see on the picture on the right there's just an insane
amount of rope and gear all of these all of these tools are basically used in order to keep climbers safe if they fall they'll end up dangling in midair but they're not going to the ground similarly we have to these kinds of tooling with blockchain security as well one thing with Tooling in specific is there we should be making use of this these tools whenever we're writing new code you don't want your anchor points to look broken and in pieces like this unit testing is one of those anchor points that that can essentially help you determine not with your in combination
with your specs how your code works and what is it supposed to do and does it actually do what you intended to so it's an interesting conversation to have because you need your specs first you need to essentially identify what your expected behavior is and then you need to identify essentially what is the happy path in terms of if a user is going to interact with your system what are the the how is your system expected to work under the proper inputs under the assumption where everything works and the user inputs like all the correct inputs
um the unit test should essentially be testing that entire flow for all of your functions but it should also be testing the unhappy path so this would be if your function is either reverting or not working correctly is it reverting in the right places is it under inputs that are malformed or incorrect does that have any impact on the system with your specs once you've written down all of your assumptions it then becomes very easy to identify what your edge cases are edge cases being cases where you may not have considered
maybe the upper bound of like a un256n like the lower bound um generally we recommend that everyone's testing the full range of inputs so they're testing essentially the expected um input range as well as the edge cases and the unexpected range another thing that is definitely beneficial in unit testing is having at least minimum 100 statement and Branch coverage the benefit of this is that you're able to essentially check your system under the inputs that you've provided so this helps you
um if like you can specify you know certain inputs certain checks that you want the code base to hold true and under those cases um this 100 statement and Branch should help with essentially checking against those specific input ranges the then the benefit of unit testing becomes there are we we should be able to find use unit testing to find um how the spec is different from the actual code um from the spec we should essentially have those list of expected behavior and assumptions and from the code we should be able to check those
another thing is it does force you to write testable code in that it forces you to separate logic into smaller like unit tests into smaller functions that you can easily pick up and test logic against this is beneficial because you can then do unit tests against individual functions and then you can pick up like integration tests with different modules as long as you're updating code these unit tests should be being these unit tests should be written alongside this code it means every time you're writing like a new function you should be checking like the in the
correct input ranges the edge cases and making sure you're hitting that expected coverage this also the benefit of this also means that in the future if you're changing your code you're refactoring um it future proofs you from reintroducing that bug in the future if you do actually reintroduce that bug um your system will essentially tell you that there is like a unit test failure the whole goal of unit testing and these tools in general is basically to identify bugs as they're being introduced during the development process as opposed to thinking about security at the very end there are however limitations of unit
testing and this is generally in that the the coverage of your tests are only as good as the inputs that you've actually tested against so if you're um for example your function works against you a un256 and you've tested maybe a smaller subset of that bound if you're not testing the lower bound or the upper bound those unit tests are not really conclusive so even if you have a 100 statement coverage on that kind of function not testing those extra edge cases means that this that unit testing isn't able to uncover bugs in those areas the other thing of course is that it
doesn't test Integrations between components so if you're if you have a D5 protocol set up you're integrating with multiple other D5 protocols um unit testing is not going to help you identify either how there are deviations in like expected Behavior with those external calls but it's just going to tell you essentially per function it's going back a little bit to claiming um generally with climbing hole we have a series of climbing holds and a jug essentially the best hold that you can get on a route it's the one where you if
you're trying to jump in mid-air those are the jugs or the holds that you're aiming for obviously as you get more and more challenging and challenging routes then you you essentially get into to the bottom part where it gets smaller and smaller and it gets harder to actually hold on to so this is how a jug would be used generally a jump we love them just because they make our lives much easier they make it they make Roots much easier to climb and they're basically like monkey bars but maybe a different shape
so in a similar way static analysis is basically your jug it's your automatic bug finder if you were to imagine like security researchers or Auditors going through code and just maintaining like a growing list of vulnerabilities like going through wrecked news and writing down pretty much all of the breast all of the reasons for previous hacks and identifying like smart contract best practices the accumulation of these essentially determines would give you like a for example a list of like all the ways that protocols have been hacked or like functions have behaved incorrectly if we consider Slither Slither is
basically taking that huge list and applying analysis on your code base all statically so it's basically looking out for any kind of best practices within your code base and especially any kind of violations so we could go through that manually and probably a lot of the time sometimes people do um but one benefit of Slither is that if you're running on a code base it can essentially tell you instances where reentrancy might be possible it's not going to tell you this is 100 exploitable because that's not something that static analysis can do but what it can do it tells you what might be a problem
and so the previous example here is output from Slither action so this is integrated into the GitHub CI platform and what this basically shows is a is a code snippet where and it outlines a potential area where re-entrancy might be possible as security researchers and developers this would be our chance to go in and actually check whether is it making external call is it handing off execution flow to someone else that kind of thing so again Slither is going to analyze your code statically which means it's
just going to check against the best practices but it's not necessarily going to tell you that something is a hundred percent exploitable right now it matches 87 best practices or detectors to essentially determine and find these vulnerabilities so that brings up an interesting question because once we have Slither findings what do you actually do with it um if it's currently exploitable and it's currently a problem that should be a sign that you should just fix your code um essentially if it's for re-entrancy add a non-reentrant card or use checks FX
interactions pattern if the code can potentially be exploitable in the future due to like governance updates or some other code upgrade that might be possible then that would be a case in which you would want to document and make sure that it's explicit that any kind of code update in the future will essentially make will have these impacts on the system so similar to unit testing it also Slither also has its pros and cons it runs quite quickly on code bases which means that it gives us quite a quick view of what might go wrong and it tells you what looks wrong but it's not going
to tell you what is exploitable um it's going to tell you um there are violations of best practices but it's not going to specifically say this is exploitable in this way it requires human interaction to go in triage and determine whether what the impacts are so in climbing we also have this concept of a Crux this is the hardest part of a move or root this is the part where you usually have to fight the most this is essentially the the most challenging part of climbing
and in the blockchain security world this is basically fuzzing fuzzing will stress your code with random inputs so kind of like a cat on a keyboard or a monkey on a keyboard it's basically in like for example that function that takes in a un256 instead of being defined by the input range of a unit test fuzzing essentially going to Generate random pseudo-random values in order to test the breadth of how that function works um fuzzing does also force you to identify how your system is expected to work through the creation of system and variants
in some ways you can think of fuzzing as a robot unit testing in that the fuzzer is going to determine what are the inputs to check but as a developer as a researcher our goal is to essentially Define encode what are those assumptions that we've made on how the code is expected to work um the other area of fuzzing is that it can be slightly different more difficult to actually set up an end-to-end infrastructure and especially as you get into more complicated bugs can be difficult to get stood up so in terms of tooling we have Echidna which is our open source smart contract
fuzzer and it will automatically generate inputs for your functions so determine based on the properties that you define or those assumptions that we were talking about in the documentation section those are essentially converted into code and then Echidna runs on a code base to determine a is it possible for those assumptions to be violated but also if violated what inputs actually cause that violation the benefit of fuzzing is that it provides more complex bugs and quite a large quite an interesting subset of bugs that may not have been uncovered
through unit testing and it can help us better with determining those edge cases generally it's considered more complete as unit testing but the setup and and this the setup and the getting stood up with contracts can be a little bit more difficult so going back a little bit to climbing generally before we hop on a route we're doing a bunch of rope checks safety checks belay checks making sure that we're all tied in making sure all our ropes are in and basically after all of that the comic ends with I don't want to climb this route anymore but we can still climb the route
um tying this back to the blockchain safety side one thing to keep in mind is that blockchain or Security in general is a continuous process so it's not something that you should be doing all your development and then thinking about security it should be something that you're essentially thinking about security as you're writing code generally as a practice write everything down even assumptions that you think might not actually be relevant in the future having those written down in any form is going to be very useful that means adding unit tests as you're adding functions running Slither as you're adding new code
determining and identifying invariance as you're building out your code base and then running fuzzing tests and making sure that those properties hold so we're right at time if you want to learn more this QR code is basically a link to us securecontracts.com and this provides a lot more information around development guidelines incident response plans and how to create them more guidance on how to get a kidney stood up and examples of what those code bases look like and way more So yeah thank you [Applause]