Writing Smart Contracts without Solidity
ETHGlobal·Sun, Oct 9, 2022, 12:00 AM
Join Gilbert Garza for a talk titled, "Writing Smart Contracts without Solidity," at the DevTools Summit, part of ETHOnline 2021, a month-long virtual hackathon with over 1,000 engineers, designers, and creators building new protocols, learning at summits and workshops, and gaining valuable feedback from ecosystem professionals along the way. ---------------------------------------------------- Follow us: https://twitter.com/ETHGlobal Check us out: https://ethglobal.com Email us at: hello@ethglobal.com
Transcript
[Music] [Applause] [Music] and next up we have gilbert and this is a very interesting top that i'm super excited about gilbert is going to be talking about how do you actually write smart contracts without solidity we're going to go into some internals about how all this works under the hood and without further ado let's welcome gilbert i'll ask you to turn your camera on and we're good to go all right thanks a lot let me share my screen real quick perfect all right hello everyone my name is gilbert i am a smart contract auditor and instructor at optilistic at optilistic we do smart contract audits and we also train people how to write smart contracts and then place them but enough about me let's talk about smart contracts without solidity so we're going to go over several things in this talk first we're going to go over why should you learn even opcodes as a smart contract developer we're going to go over how to deploy a smart contract in other words how to write opcodes for deploying a smart contract without solidity we're going to take a quick look at a little new language called trim and then we're going to implement a full smart contract um using these op codes so first why learn evm opcodes well essentially you want to become a better solidity engineer what does that mean if you know opcodes then you are better prepared for low-level hacks and issues solidity's compiler has had problems in the past if you understand opcodes you'll better understand why those things happen second you want to have a deeper understanding of common design patterns especially related to proxies proxies use opcodes all the time that are not available normally in solidity and it's great to have a internalized understanding of how smart contracts actually run when you're making transactions to these contracts okay so to start we're going to look at what i call basm which is bayer assembly and basically what this is is we want to be able to write these op codes without writing the raw bytecode so on the left you have the bytecode on the right we have basm so we definitely want to write what's on the right so that we don't have to uh well i mean it's it's pretty straightforward you don't want to write what's on the left and um yeah this is this is pretty much all it is there's pretty much no features in this language all it does is that it takes your opcodes and then converts it into its respective bytecodes and puts it all together so let's use this to actually write some code so for the first contract that we're going to look at we are going to deploy a contract and it's going to be a very simple contract there's not going to be any functions in this contract all it's going to do is whatever transaction that's made to this contract it's just going to return the number nine uh just blindly it doesn't even care it doesn't care about what function you're trying to call doesn't care about your arguments it'll just return the number nine so as you probably know when you deploy a smart contract you deploy the code of that smart contract but also some additional initial initialization by code so here we have some init by code and the way that it works is that the bytecode that you use to deploy needs to return the runtime bytecode so in other words in this example the init by code we have here the goal of this init by code it can do other stuff but ultimately it needs to return the data that represents the byte code down there so when we make a deployed transaction the runtime by code is not going to run at all only the init by code runs just because of the way we wrote it and then it will return it so let's look a little bit at the details of how this works so first we have a code copy and this copies into memory a piece of the entire bytecode that you're running in your create smart contract transaction so the first argument um here what i have here is um evm is a stack based language so all this code that you see on the left is pushing onto a stack and then when you run code copy it just pops off the top three items in the stack to do whatever it needs to do so on the right hand side we just have a syntax that's a little bit more readable for those who are not that familiar with top codes so this code copy takes three arguments and the first one is where you're going to copy to when you do code copy you are copying your bytecode data into memory so zero just says stick it into this file and as you can see this is going to copy some code and we're going to see exactly which code is copied the second argument is the offset of your contract by code so this we hard code 0x0c because it's 13 bytes before getting to that point so that second half is our runtime code we want to start there when we do the copy then lastly we tell it how many bytes to copy so last over here we have 10 bytes because our runtime code is 10 bytes so that will copy 10 bytes into our memory slot which is at 0x0 now that it's in memory we can use the return by code or the return op code to return it and return takes two parameters first one is the memory offset we just wrote to 0x0 so we want to start returning data from that point and then secondly the length which is again 10 because there's 10 bytes of memory that we need to return and that's pretty much it so this memory or this data that we're returning the green box this is the code that the evm will take and store at your new contract address now whenever anyone makes any sort of transaction to your contract address is going to be running that specific bytecode which again is the green box so this works but there's a problem we are hard coding some bytes and when you're writing code you definitely do not want to be manually counting bytes so that you can um just get it to run adding one line of code will cause these uh values to be wrong and that's really bad so that is the motivation behind the language trim so trim is a little language that i wrote um in a few days and the idea is just to have a mostly purely syntax transformation so that you can write your code in a little nicer manner you're still dealing with opcodes with trim there's not any fancy function calls or stacks or memory management it's almost purely syntactical so instead of writing what you see on the left which is the code we just saw you can optionally write the thing on the right basically the rule is anytime you have a parenthesis now you're in s expression land and you can reverse the order of your arguments and it'll just translate it back to the left so that's pretty straightforward if you don't have parentheses then you're still in opcode land for example the stop on the right that is just a plain op code feature number two is labels labels is how we solve the problem of manually counting bytes so on the left we have 0c and 0a these are hardcoded values based on the length of our code now on the right we can write this hashtag runtime and that just represents a location in your code so the last runtime on the right that is the actual label and then when you reference runtime within an s expression it will fill in that location for you so this is a way to get around manually counting bytes uh the first line we subtract the total code size with the position of runtime and that will give us our runtime code size and we dupe it so that we can use twice and then we code copy from zero to the runtime position with that length and return to links so that's pretty handy and the last feature which with trim is instead of manually putting in strings you can just write the string and it will translate it to that push statement for you so again these features are mostly syntactical um it's not going to manage memory for you you're still writing upgrades okay so lastly we want to look at a example of writing a full contract so here we are going to emulate or we're going to implement a version of this in opcodes so here's the solidity code this is a little bit different from the standard greeter contract that you get from generating a new hard hat project the main difference is it uses bytes 32 instead of string and the reason for that is just for teaching purposes string is a lot more complicated than bytes 32 so i avoided that by just using bites 32 directly the other thing to note about this code is it has initialization so our bytes32 greeting variable is initialized with hello evm so that's something else that we're going to have to implement as well so let's do it all right so on the left we have our initialization code and it's pretty much the same as what we just saw and two slides ago the only addition we have is the s store we are storing in store slot zero our string hello evm and that's pretty much it that's all you need you just need these three op codes one off code for store one opcode for push zero and another op code for pushing the string value and then that's your initialization that because it's initialization code it's not going to make it into your runtime code which is good now on the right hand side we have a reference for api encoded function calls of greet this is a function id for greet and then on sect reading that does have an argument so you have the function id plus the value of that bytes32 um and that's what that looks like we're going to have we're going to be using that or referencing it when we write the code to access that all right so we just omit the net code for the slideshow didn't make more room the first thing we want to do is copy the function id onto the stack and this is the transaction data so on the right this reference i didn't mention it's the data that you send with the transaction so if you're making a transaction to our contract most of the time you're going to be encoding your data using api and that's what we see on the right hand side so if they're calling greek then it's going to look like that i can use my mouse can i if they're calling greet then this is going to look like this right here if they're calling sec greeting with this parameter it's going to look like that in either case we need to handle different functions in our contract and the conventional way to do that is by using an api encoded function id so we need to see if the first four bytes of our transaction data or our call data is either this here or if it's this here and based on that we're going to run different code so that's what this first one does call data copy will read data from your call data and the offset 04 is the length that you're reading oh zero is the starting point and once you see here this is the position in memory that you're writing to or you're copying to so zero we want to start at the beginning obviously because the function id is the first part of your call data four we want to read four bytes four bytes is eight hex characters so that we read the function id and nothing else and then this is um actually 28 because this just makes it easier to deal with called data copy um we want to write it as a u and 256 which is 32 bytes but we're only writing four bytes um if we don't we're just doing this so that we can slot it into a very specific spot in memory to make things easier and then here we are loading from memory position zero and m load will load 32 bytes so it will catch the data that we just wrote after that we want to see if it matches a known function id and the reason we do that is because we're going to have we're using trim now we're going to have two different labels one label for our greek code and one label for our set greeting code so this here this is pushing a known function id onto the sac and then it's duplicating the input function id and if they match then it's going to jump to that label so eq will take the top two items of the stack and if it's true it'll push a one if it's false it'll push a zero and jump i will only jump if the top of the stack is not zero so in other words if this matches the call data's function id then it's going to jump to green and we're going to do the same thing for sec reading so if this does not match then jump id or sorry then jump i will not jump and then we'll move on to the next one and then we're going to check well does our input match this function id and if it does then we jump to set greeting otherwise we revert but this this logic here this is how solidity gets compiled down um obviously solidity compiles into more bytecode than this uh with some extra features but this is essentially how it works it's basically a case statement does the function id match this go here does it match this go here and that's it so now let's get into writing the actual code for these functions um it's a convention to have a jump destination um op code for places you expect to be jumped to it actually doesn't do anything but i guess it makes the code a little nicer to read and for greet the only thing we need to do for gree is we need to load the current greeting so first we load the storage at position zero and then we store that into memory because we need to return from memory and then we return it so in other words we're loading from storage into memory the current greeting and then we're returning that value and that's the entirety of our greet function sec reading is pretty similar in fact it's even simpler call data load will load again 32 bytes from your call data and here four is the offset so we are skipping four bytes because we want to skip the function id and then the next 32 bytes we know is our parameter so we load that onto the stack that comma should be there that's a typo but we load the string onto the stack and we store that into storage slot 0 and that updates the storage that's the entirety of the set greeting function then we here we have a stop because just in case later we want to add more functions um we don't want to accidentally run over you know the the normal problem which which statements you just kind of go over your case so that's what stop is done that's what stop is doing there um so yeah that's the that's the entirety of the contract but how did we do so if you compile this in solidity you end up with 404 bytes and the code that we just wrote is only 76 bytes so that's pretty cool um of course this is uh not without it's trade-offs we are not checking call data length for example solidity checks call data links by default that avoids for example accidentally setting an owner address to zero without providing a zero without providing an address and some other stuff solidity also makes sure that all your functions are not payable by default which means that it generates byte code to revert if the transaction has either value in it it does that for for all functions unless you specify payable uh so there's trade-offs there but it's pretty cool we can write some mob codes and we can be how many times is this like four times uh smaller than the default solidity by code generation uh but yeah so hopefully that gives you a good taste of how to write smart contracts with outsolidity with plain old opcodes we use trim as a language just to make things a little bit easier to read and write but ultimately it's still just goods so learning this stuff will make you a better engineer it will help you internalize how things work at the low level lowest level of the evm and um hop codes are just good to know in general because things like proxies and static calls those are all using opcodes that are not available in normal solidity code and lastly we teach smart contract engineering and security at optilistic so if you or anyone else is interested in becoming a smart contract engineer we take mostly senior engineers we only make exceptions for special cases uh but yeah if you're interested then visit our website and contact us awesome thank you gilbert a couple quick questions where can people learn about trim um and could you uh talk a little bit more about some resources to sort of play with it or debug things and just how people should think about trying it out when they're doing the not quite directly yes so um preparing for this was a little bit rushed i'll be honest um [Music] trim is not published yet but it will be soon and we also have a really cool um in browser tool for kind of writing trim and it just automatically runs your code as you type it and it shows you the results it shows you your memory it shows you your stack usage your gas usage and all of that um yeah if you're interested in knowing when it does get released uh just shoot us an email and optimistic and i'll put you on a list awesome wonderful well thank you so much for that amazing demo and presentation and can't wait to see how to play with trim thank you very much [Music] [Applause]
Automatic transcript — names and jargon may be misspelled.