foreign
[Music]
hey guys my name is Alex I am a lead
developer at tally ho and I'm here to
talk to you about evm assembly so the
two things we're going to cover in this
talk is first like what is evm assembly
and second how to read the op codes how
to read op codes and trace an evm
transaction
but I think before we jump into that
it's pretty important to tackle a
question of why do we care like why
would we care about UVM assembly I think
that there's a few reasons so
any of the code that we write as
developers is pretty far abstracted from
the code that a machine actually
interperson executes and the closer we
get to this real code and the further we
we get from our abstractions the more we
can reinforce our mental model of the of
what the machine is actually doing and I
think in general learning from first
principles is a fantastic way to
progress as a programmer and to just
build solid and bulletproof mental
models of what your code is doing so
what is evm assembly uh well let's start
with what is the evm so you've probably
heard of evm zke evm evmls there's a lot
of terminology terminology out there
right now but at its core the evm is
just a stack machine it's something that
takes in instructions builds up a stack
and then operates on that stack most of
the operations consume values from the
stack so add takes two values from the
stack adds them you're left with one
value multiply takes two values from the
stack adds them you're left with one
value
Etc but there are exceptions to this I
think the most notable one is push so we
have push one through push 32 which
pushes between 1 and 32 bytes onto the
stack respectively so let's go a little
bit more in depth into the evm stack
machine so stack machine has a depth of
1024 items each item is a 256-bit word
if you're not sure what a word is that's
okay and it's not really relevant for
this talk it just it is a slot on the
stack machine
um during execution the evm has a memory
that does not persist between
transactions it also has storage that
does persist between transactions when
we talk about writing to the blockchain
that's what we mean we are storing
things in the evm and
uh also compiled to Mark contract by
code executes a number of UVM op codes
so we have some that you might be
familiar with if you've looked at x86
assembly like xor and add and sub and we
also have a number of blockchain
specific opcodes in the evm and we'll
dive into more of these a little bit
later and then I think the last
important thing about the stack machine
is that each operation costs a certain
number of gas so when we pay gas for our
transactions we're essentially paying a
little bit or a lot of gas for every
single operation that the stack machine
executes all right so now that we have
some understanding of what an evm is uh
what is assembly so assembly is
something that lives in between the code
that we as developers write and between
the code that a machine interprets so on
one hand we have solidity which is nice
uh some people think easy ish to read
easy-ish to reason about and then on the
other end of the on the other end of the
spectrum we have bytecode which is
pretty much impossible for humans to
read but very easy and very efficient
for machines to read and then in between
solidity and by code we have assembly so
this is kind of what it looks like and
it is a Intermediate Language or an
intermediate representation and what
that means is it is something that lives
in between the code that we write and
byte code and the uh the op codes that
you see there are what are actually
running in the evm so the solidity or
the Viper that we're
uh writing is compiled down to opcodes
and then running in the evm and that's
what that pattern is what lets us have
multiple smart contract languages that
compile down to evm and then
um are able to be executed
or sorry the compile down to assembly
and then are able to be executed
all right so uh let's trace the
transaction I have a website up here
www.evm.codes you might find it useful
you don't need it to follow along but
it's a fantastic reference that has
every single op code in the evm how much
gas it costs and what it does
all right so here I've written this uh
incredibly useful smart contract
um we have one storage variable total
Supply and we have a public function
that anybody can call that sets uh total
Supply to eight so very simple smart
contract not much going on here not even
sure why anybody would use it right so
uh the op codes representing this
contract should be pretty simple right
well we can take a look so salsi
provides us with a handy little command
where we can pass a contract to salsi
with the opcodes flag and it will output
a opcode representation of that contract
so our simple contract becomes
that which is kind of unreadable but by
the end of this presentation uh or this
talk you guys will be able to read some
parts of it
um some that are common to all
transactions that we see uh in the all
transactions that we see in the evm and
some that are specific to the contract
that we wrote so let's go ahead and
trace a specific transaction imagine
that we've deployed our contract at some
address and then we're using ethers to
call
ultrasoundmoney.set Total Supply the
transaction that we defined in the
beginning great we submitted the
transaction it's been mined it's part of
the blockchain uh let's take a look at
it so uh but get has this very handy
command called debug Trace transaction
if you're here for the previous call
you're probably more familiar uh
familiar with it uh than most and what
we can do is we can pass in the
transaction hash where we sent uh where
we invoke that sent total Supply
function and get an opcode Trace as well
as a like a historic representation of
the stack of that transaction so
basically find out everything that
happened in it
um so let's trace the op codes of that
transaction you'll see here that there
are some lines
between these op codes and I've divided
them into four sections so that is more
so just for clarity because I want to
focus on three parts that are common to
every transaction and one part that is
comma to ours and I also want to note
that uh what I'm going to show you is
not necessarily a one-to-one
representation of what you'll see when
you send debug Trace transaction for
example uh we're going to be converting
hexadecimal numbers to decimals because
hexadecimals are hard for humans to read
we're going to be showing booleans as
true or false but that's just so that
What's Happening Here is uh so it's
easier to follow along with what's
happening here so let's start with the
top this little section so we have push
one zero x80 what we're doing here is
we're pushing 128 onto the stack 128
sorry 0x80 is represented
because the hexadecimal representation
of 128 then we're going to push 64 on
the stack and then we're going to call a
function called M store which is going
to store the value 128 at the offset 64
in memory so what's going on here we
don't have 128 we don't have 64 on the
stack or we don't have 128 or 64 in our
contract so why is this happening
well what's happening is solidity uses
the memory area between address 0 and
address 0x7f or 127 for internal
purposes and stores data starting at
address 0x80 or 128 so this is solidity
doing some boilerplate internal memory
management for us fantastic we don't
have to worry about this as solidity
developers this is just a straight win
wonderful all right so now we're going
to jump to line uh 25 in our list of op
codes between line 3 and 25 there's some
more boilerplate stuff there's a
validation of uh message.value and
making sure like you can't send ether to
a non-payable function but that is not
really relevant to us because our
function isn't payable so we're going to
go ahead and skip that
um great so uh on 25 we're going to push
4 onto this deck once again where is
this four coming from and then 26 we're
going to encounter our first blockchain
specific uh opcode and what we're doing
here is we're pushing the size of the
input data onto the stack and as you can
see we pushed four onto the stack now we
have four on the stack again so that
means our input data size is four bytes
but where did this come from we didn't
send any ether and we didn't send any
arguments along with our function call
because there were no parameters it was
just the public function that we invoke
so why is our call data size four well
the answer to this is that when we call
get total Supply in ethers under the
hood ethers is going to Hash that get
total Supply function into its function
signature and send that along with the
input data when we send that transaction
all right so we know that we have four
and we know that our cost data sizes for
what's going on next well LT as you may
have guessed checks if uh input data if
the input data is less than four and
what's actually happening is it's
looking at the top value of the stack
looking at the second from Top Value of
the stack and seeing if uh the second is
less than the Top Value in our case it's
not because 4 is not less than four so
uh we have we push false onto the stack
remember this would be represented as a
zero or one
and then we push 0x 280 or 38 onto the
stack
we'll come back to that in just a second
the next instruction that we see is Jump
I and I think of that as jump if so what
that instruction is telling us is if the
second from the top value is true
jump to the program counter represented
by the Top Value on the stack
a simple way to think about that is if
the second from the top value is true
jump to line 38.
um since the second from the top value
is not true
we don't jump anywhere and
uh this like check executes and
essentially What's Happening Here
is that since functioning signatures are
four bytes in length length if the call
data size is less than four bytes we
know that we can't possibly be calling a
valid function and
um uh then the function gets reverted if
the call data set is size is less than
four
so with that idea of function signatures
in mind let's take a look at the next
section
so call data load is another
blockchain specific opcode similar to
call data size except instead of pushing
the size of the call data onto the stack
it puts you pushes the actual call data
onto the stack remember that the size of
our call data was four bytes so here we
have a four byte hexadecimal value on
our stack that seems relatively
arbitrary but some of you are probably
correctly guessing that we are actually
pushing our function function signature
onto the stack uh on the next line we
have Push four and we push that same
exact function function signature
What's Happening Here
is uh the contract is checking okay I'm
being sent this function signature does
it match uh does it match this function
signature
and if the answer is yes uh represented
by the op code equal so equal takes two
values from the stack and returns sorry
the top two values from the stack and
returns true or false if they're equal
uh we hit a one or a zero and we in our
case it's true
so once again uh we're in that jump I
situation so we have true on the stack
we push 45 under the stack and now on
line 36
we are excuse me online 36 we're going
to jump to line 45 because
uh line 32 was equal to line 33 and hey
uh we know that this is the function
um this is the function that we wrote
Because it's the function that we tried
to invoke fantastic and like this is how
the evm determines which functions to
call if you have many functions in your
smart contract you're going to have many
lines of like of call data load does it
equal this function signature does equal
this function signature does it equal
this one and then if it matches one
it'll jump to that line and if it
doesn't it'll result revert because you
cannot call a function that doesn't
exist awesome okay so now we're finally
in that very simple function that we
wrote If you recall it was total Supply
equals eight
um well we see on line 45 is a common
command called jump dest and all that
does is it marks that line 45 is a valid
destination to jump to you cannot jump
to any arbitrary destination you need to
jump to a valid one or you'll revert so
this just lets us know that hey we can
jump here what's the next thing we see
push 8 onto the stack all right awesome
this is definitely code that we wrote
total Supply equals eight I remember
eight This Is Us cool but after that we
push zero onto the stack so where does
the zero come from what are we talking
about well if you recall in our very
simple contract we only have one storage
variable and that storage variable
because it's the only one defined is in
the first storage slot so uh the first
storage slot or the zero with or uh
storage slot via array indexing right so
this is awesome we have everything that
we need we have eight we have zero all
we have to do now is save this and we're
done right but wait a second we see that
in our transaction we duplicated we ran
Duke two so we duplicated the second
from the top word of the stack okay
then we swapped the first and second
words on the stack okay uh interesting
and then all right great so now we ran s
store so s store store and storage
um we're saving eight to the zero with
storage slot that's what we initially
wanted to do and but then we also have a
pop after that because we have this
extra eight on here and like what is
this eight doing why why is it here uh
this doesn't make a lot of sense right
like why are we duplicating and swapping
here uh why are we popping at the end uh
the solidity is doing extra memory
management that we don't want right
because we want to be as gas efficient
as possible and be as um uh yeah be as
gas efficient as possible so we should
jump down into Yule and try to optimize
this code right uh well uh let's talk
about gas optimization uh using yule
um if you're not familiar Yule is simply
a language that we use to write assembly
code in solidity it's not exactly like
writing op codes not exactly like
writing evm assembly it has for Loops if
and switch statements and it also
disallows some commands uh like jump
statements because they quickly become
very very difficult to reason about
um and yeah all right so we added you we
have this optimize set total Supply
function it's awesome uh we have an s
store with a zero and an eight so we're
saving eight to the zeroth slot this is
great this is going to be way better
right uh go ahead and let's go ahead and
measure our um our contract so once
again or let's go ahead and look at the
op code representation of our contract
first so once again
uh we take a look at the op codes and we
see that in fact optimized set total
Supply does have less opcodes than
subtotal Supply this is fantastic we
know that op codes cost gas we see that
uh set total Supply and optimize that
total Supply have the same op codes
except set total supply has more uh we
just like saved a lot of time and money
right but let's like measure it to be
extra extra sure
um so here uh we uh this is I think hard
hat gas reporter and We've ran the set
total Supply function 100 times and we
see that the average gas cost is 22 599.
cool and then hey now we ran optimize
set total Supply and we see that the gas
cost is 23 591. awesome a gas we have
just saved our users like an enormous
amount of money right well uh not really
I kind of rubbed you guys here
um so uh if you notice optimizer enabled
is equal to false if you're not familiar
with optimize or something that you can
use in hard hat and other tools when
you're deploying smart contracts so
let's go ahead and enable our Optimizer
in hard hat we will set runs at 200
enable it and re-measure and in fact
when we have the optimizer enabled it
looks like the two functions cost
approximately the same amount of gas
right so all our time spent studying you
will and learning it and learning about
a store was wasted right well uh I
wouldn't argue that exactly but I will
say that optimizing smart contracts is
hard and chances are that you're not
going to do a better job than the
compiler unless you really know what
you're doing
contrast containing assembly are
generally harder to reason about in
order to audit than contracts written in
solidity and Viper so what you might
gain and gas optimization you will
probably be making a trade-off in a
contract or user security and the other
thing that's important is if you're
writing your own assembly code always
measure measure and make sure that your
implementation is better than the
compilers because chances are there are
some very very very smart people working
on the compilers and optimizers that
know something that you don't when it
comes to memory management or safety
um and I guess that's essentially my
last Point remember a lot of the memory
management stuff solidity does under the
hood is there for safety reasons and
just because an OP code looks like it's
unnecessary doesn't mean that it
actually is
that being said optimizing in Yule and
an assembly is definitely something that
is needed and useful especially in D5
and on mainnet where our gas costs are
you know
lower now but quite higher than a lot of
the other chains
um but yeah I guess do so at your own
risk so uh thank you guys once again my
name is Alex I'm a lead developer at
tally ho and we are hiring solidity and
typescript developers so if this kind of
stuff is interesting to you and you're
looking for a change uh please reach out
and then finally I want to give a big
thanks to Gilbert Garza he had a lecture
at Xerox macro that inspired this talk
and there are a few research resources
on here if you would like to dig deeper
on any of the topics uh covered today
thanks so much thank you um we would
have some time for questions if you like
why the optimizing function spend the
same amount of gas if it has more
uh sorry less up codes so the app codes
that we were looking at were for the uh
for the unoptimized functions and we
were looking at them because it is like
actually it's easier and possible to
dissect them in a talk so when we saw
those two uh op code I guess have like
optimized what was it set total Supply
and set total Supply those are the
unoptimized app codes and then and then
when uh after the optimizer runs the
opcode representation of those two
functions actually becomes the same what
were the those two op codes two extra up
goes doing the swaps we're doing on the
unoptimized uh
compiled version the the optimize
compile version
um was a
effectively the same as our op codes
um secure I can show you
so here we have uh our unoptimized set
total Supply and our optimized set total
Supply
in our code where the optimizer has not
run before after the optimizer runs uh
both of these functions
um have this have the same exact op code
and in fact even though we have two
functions in our contract the optimizer
is smart enough to know that they're
doing basically the same thing so the
when we were talking earlier about
uh these selectors over here right
um so this selector jumps to a
destination and then there would be a
different function selector for another
function
um for for the other function they would
actually both jump to the same
destination because effectively what
they are doing is the same uh what would
be some good use cases for managing the
assembly code within contracts and do
you recommend any good resources for
learning you'll yeah so I think that the
the best use case that I have seen is a
fairly common one where uh if you're
adding two numbers that you are 100 sure
will not overflow the uh the integer
limit then in assembly you can uh you
can add them with a uh there's an
unchecked flag which basically tells the
uh the compiler to not check for uh
integer overflow I believe that in
solidity
nine point something in some
fairly recent version of solidity uh uh
safe
math became
I I guess like
well there are a lot of checks that run
under the hood
you can turn off those checks if you're
sure that you don't need them but and
that'll save gas but once again that can
be very dangerous and as far as uh
resources for learning UL I think just
like reading smart contracts and trying
to figure out what's going on there is
the best way to do it there are
unfortunately no good resources to you
or to learn you will or really evm
assembly and which is that I want to
just talk just like yeah
just to like follow up on that have you
heard of um trim yeah yeah okay so my
question is this right like say for
instance if I'm writing trim and I'm
basically you know trans transpiling my
smart contract into into trim like
um what are the I guess like the pros
and cons to that as opposed to doing it
um you know your style like within
solidity and then don't use Yule
unfortunately I can't uh I can't answer
that question because I'm not familiar
enough with trim I do believe that uh
Gilbert was the the person that I
mentioned anything in my talk was the
person that developed trim
um or one of the developers and I would
encourage you to reach out to him on
Twitter but I'm not familiar enough with
shrimp to answer that hey thanks for the
talk
um I saw somebody asked about you today
there was an awesome Workshop about that
it's like two hours long you should
watch that and then uh I had a question
for Optimizer runs what number do you
recommend
what number
um I just choose 200 because it's
standard I know there's I know that
there is a or it's not standard it's
just like standard in what I've written
I know that there is a there's
eventually a trade-off size between uh
contract size and and gas efficiency
that you get if you set your Optimizer
to like I don't know 500 000 a million
um or maybe less but I generally go with
200. um I don't work in the D5 space so
I'm not like too concerned with ultra
Ultra Ultra optimization but 200 seems
to be a good number for what I'm seeing
if that was it thank you so much thank
you guys
[Applause]