New Ethereum talks, every Monday. The week's conference uploads by event, in your inbox.

Loading player…

DevTools Summit | Solidity Best Practices - 10 Tips - Tasuku Nakamura

ETHGlobalSun, Oct 9, 2022, 12:00 AM

Welcome Tasuku Nakamura for a talk titled "Solidity Best Practices - 10 Tips". Part of ETHOnline's DevTools Summit. ETHOnline is our biggest event of the year! Collaborate with thousands of developers and creatives from around the world and win over $300k+ in prizes! We're bringing in 30+ of the biggest teams in web3 to give you access like never before. Learn from ecosystem leaders and build your biggest ideas as the merge nears reality. The future of Ethereum is being written right now, and it's time to make your mark. --------------------------------------------------- Follow us: https://twitter.com/ETHGlobal​ Check us out: https://ethglobal.co​m Email us at: hello@ethglobal.com #ethereum #eth #blockchain #crypto

Transcript

[Applause] [Music] i'm super excited to welcome taz smart contract programmer to talk about validity best practices and 10 tips to become better at developing solidity so without further ado let's welcome test so for today i'll be talking about 10 um solidity best practices uh they're not in any particular order they're not in any particular importance they're just some maybe uh things that i learned while learning study so and i'll share them with you guys today so the first one is to lock compiler version um so the bad practice is to have a compiler version set like this uh with a carrot and this would mean that your solidity version can be anything from 0.8 and that's 10 0.9 uh and the good way to do it is to lock the compiler version it means to set it to a specific version for example here we're setting it to 0.8.13 and the reason why we want to log compiler version is uh i'd say that it's mostly for when you deploy a contract and then try to verify it by the time you deploy a contract you might have forgotten which version you deployed it with so it's really helpful that you lock your version you go back to your code and say oh so i i know that i deployed it at 0.

8.13 so you'll remember it and you'll have a easy time verifying smart contracts uh the other reason is um sometimes solidity will announce uh bugs and if you know your version then you would know if your contract has a bug or not so that is one recommendation to lock your compiler version okay so uh the next one is multiply before i divide um i see a lot of developers uh new developers asking the question if i have a number like x how do i get like 90 percent of x so one way that you might think that you would do it is to say x times ninety percent so it'll be ninety divided by one hundred and here we're dividing ninety over 100 and then multiplying x and the reason why this is bad is because in solidity numbers um there's no decimals so uh numbers round down so when you divide 90 by 100 this will evaluate to zero and zero times any number will be equal to zero so the better way to do this will be to first multiply your number x by an and then divide by 100 this way you will there might be some rounding errors but this way you'll get close to 90 percent of x assuming that x is maybe greater than 100 so that's um multiplying before divide and in in the real world in the real world you might see code something like this something like calculate calculate uh ratio of shares uh the bad way to do it will be to maybe the okay so first of all here let's say that we have a contract and it keeps track of the total shares in the contract and the number of shares per user and the bad way to calculate the ratio of shares is to simply divide the shares of the user by the total shares total total shares will be most likely be greater than the shares of the user so this will most likely return a zero so this is the bad way to write your code a good way to do it is to say that 100 will be equal to 10 to the 18 and then you will first multiply the shares of the user by the multiplier here the multiplier we defined as 10 to the 18. so here we're multiplying the shares by 10 to the 18 and then dividing it by the total shares you'll see code something similar to this inside the curve uh amm contract so they would multiply by some large number and then divide by another number so that their numbers wouldn't round down to zero so that is multiply before dividing so the next best practice that i learned is to don't name local state variables the same and what i what i mean by this is let's say that we have some smart contract and it has some state variable we'll say the state variable name is total supply and we have some function and inside of inside this function we might declare another local variable also the same name as the state variable and let's say that we set it to some number one two three and there will be a lot of code in here and at the end of the code maybe our intention was to update the state variable total supply to 456 but when we execute this code total supply inside this function refers to this local variable so what we're doing here is set it setting the local variable to 456 instead of our intention being to set it to the state variable total supply so this is bad practice to have the same name for the local variable and the state variable and the and the solidity compiler will give you warnings so be sure to read the warnings and the better way to do this is to just simply have a different name from from the state variables and the local variables now i want to mention that uh in viper all state variables so this will be in viper all state variables start with self dot my state variable so you wouldn't have this trouble of keeping track of which variable is a state variable and which one is a local variable so this is one useful feature by room oh okay so as a bonus when you're dealing with peer functions again pure means that the function does not read any state variable or any data from the blockchain so one benefit of using peer function is that inside the peer function you know that it's not going to touch any state variable so there is no name collision between local variables and state variables so that was tip number three uh let's move on to tip number four uh keep contract simple this applies to a lot of programming and especially also applies to smart contract programming and the reason why you want to keep your code simple is that complex codes are hard to analyze so it probably means that the code might be less secure and there might be bugs that are hard to find so you would want to keep your code simple and what are some examples of a complex code well it might be like um inside a function it's calling many other functions and contracts so it's hard to keep track of what's going on inside this function so this is this will be a complex code another example of a complex code might be that the contract is trying to do many things for example it might be this contract might represent a amm so you'll be able to swap tokens and it might also represent like a d5 landing and borrowing so inside this amm contract you might have other functionalities to like to then and borrow and liquidate tokens so um trying to do many things inside one contract is uh is not good practice another this might this is my opinion but it might when i see a lot about bu functions inside the smart contract i think it's um i think it's unnecessary complexity inside the code for b functions you can usually move it to another contract so uh we'll see in a second what i mean by this so good good code will be simple contracts uh it will be simple code uh and inside the contract what i try to aim for is to do as little as possible and if i need to ask the question do i need this function or do i need this view function inside this contract the answer is most likely no and the way i would know it is if i'm asking that question then the answer is probably no if the answer if i don't have to answer that if i don't have to ask that question then it's probably obvious that i do need that function so that's how i decide which function goes inside the contract or not and one thing that i always like to say to myself is yagni which stands for you aren't gonna need it you're not gonna need that extra b function you're probably not gonna need another function inside your contract keep it as simple as possible so a good practice that i noticed uh in some contracts in some d5 projects like uni swap b3 where they split some of their core logic from um view functions so they will split the logic of a price oracle into another contract from the amm contract for example you have some kind of court contract and then as another contract you have a view contract the core contract if it if there's a bug then you can't really fix it but if there is some kind of bug inside the b contract then you can't throw this one away and rewrite another one and then deploy it so i think this is a good pattern to split the core contracts and the b that was keeping contract simple and the next tip is to prioritize security over gas so for example what i mean by this is that sometimes you would see code that are optimized for gas but it becomes really difficult to read the contract so there might be bug but you can't really see the bug because the code is complex uh for this example [Music] here i have some kind of loop and it's trying to optimize for gas so if i take a look at this loop it's looks like a regular for loop but instead of having an increment of the counter inside here i do like uncheck and then increment the counter inside the loop this code is a little bit harder hard to read but it does save gas on the other hand the good practice will be to to prioritize for code readability and security so what you might do is write a regular for loop that everyone is used to seeing so that's security over gas the next tip is avoid unbounded loops and what i mean by unbounded loops here is that there is no upper limit to the number of loops and why is this bad this is bad because there is a there's a limit to the amount of gas that can be inside the block and unbounded loops means that it's going to consume more gas than the block gas limit so at some point your chin at some point your transaction will fail so let's take a look at some examples of the unbounded loop here i have a state variable called accounts and i have a mapping called balances so it will map from account to the balance of the account and to make this array of addresses accounts unbounded they have a function called join and it pushes an address into the accounts state variable so example of a bounded loop will be to loop over all of the accounts and here we're just updating the accounts by adding one why is this bad well imagine if 10 accounts if there are 10 addresses in the account then it might use certain amount of gas if there are 100 then it's going to use more gas and if there are 1000 accounts then it's going to use more gas than that and at some point you'll hit the block gas limit and this function will always fail a better way to to handle unbounded loops is to to have some kind of upper bound to the number of loops for example here inside the um input we're saying start the loop from start and end to that end and we do a loop so this function will never be uh it will never fail because we can always bound it from start to end another example of a unbounded loop this is a subtle one but let's say that we have some kind of function and then inside the function it calls another function called test2 and inside the function test2 it takes in a array of addresses and notice that here we're passing in a state variable uh address array of array of addresses but here we're passing it as memory so what this means is that although the state all of the addresses inside the state variable accounts will be copied over to memory so basically we're running a for loop here so this is our example of an unbounded loop and to show you this uh i wrote the test let me try opening it unbounded loops dot test file and what i did was the first test i i called join three times and then run the test next i call join 10 times and then run the test so what you'll see is that calling going back here what you'll see is that when there are three addresses in the accounts it will use certain amount of gas but when we have 10 addresses addresses in the account it will use more gaps so i'll copy this and open my terminal and then paste it and then you can see here that test guess 2 this is the this is a test with where we have 10 addresses in the account it used up more gas this was a subtle example of an unbounded loop try to avoid it okay so the next one is to don't trust the user inputs what i mean by this is that if you have a function where any user can call and let's say that it takes in some kind of address adjustable contract and then inside it here we can see that it's calling into the token and calling the function transfer from but since we're passing in user input the user can put in any contract address as long as they have a function called transfer from so which means that they can execute any arbitrary code and for example what we can do here is have a user the user might have deployed a malicious contract and then they would call this function and inside that function transfer from we that malicious user will put their malicious code inside here for example for this example we just put log the message called hacked and what will happen is that when the user calls the function deposit it will execute and then they will pass in the address of this malicious contract they will call transfer from and execute this execute this code so this is what i mean by don't trust user input and i cannot remember from the top of my head head but i do remember that there were several d5 hacks where the user were able to put in their contract address to do some kind of hack the next tip is checks effects and then interaction so what i mean by this checks means that first check the inputs effects means that next update the state variables or do some kind of change inside your smart contract and interaction means call other accounts or contracts so the best practice here is to first check user inputs next to update your state variables and lastly to call other external contracts so first of all let's take a look at a bad example a bad example would be to agency over here called withdrawal bad notice that instead of checking it does no check and then next it calls into another contract or transfers the if to message.sender and then lastly it updates the balance of message.sender this is the effect part what you want to do instead is to have something like this check effect and interaction the check will be to check the user input for example amount is greater than zero next will be the effect so updating something inside this contract here we're updating the balance of message.

sender and that's the to call external contract for example here we're sending the eve to message.sender this way yeah this code will will is now vulnerable to re-entrances the attack however this withdrawal bad is vulnerable to re-engine c attack okay the next tip is to use reentrancy guards so in the previous tip we said to use check effects and and then interaction but this does not guarantee that your code is safe from reentrancy attack so whenever you're in doubt then well at least for me whenever i i'm in doubt i always put like a reentrancy log and so that i feel safer than my contract is probably more secure so what do i mean by a re-entrance regard the reentrancy guard is a simple modifier that first checks that the state variable logged is set to false and then it sets it to true and then executes the code and then after the code executes it sets it back to false so that if there was a reagency it would execute this part of the code come back in and this since the lock is true this part will be will fail and this is how it protects from reentrancy let's take a look at an example so here i have a bank you you can deposit into it and you can withdraw notice that this function does not have a reentrance regard whereas this one does um to show you example i wrote a hack to do a reagency and to do a reaction see around this command so i'll be calling this withdrawal function once and uh if the test passes then it means that the re-engine was successful so i'll execute this test and the test was successful so that means that the reagency was successful next i'll put a re-engine c lock on this con on this function called withdrawal and then we'll execute the test again this time the test will fail because the re-entrance the log is protecting that function and you can see here that the test failed so that's reentrancy lock if you're uncertain if your contract is vulnerable or not vulnerable to re to re-entrance the attack then just put a reentrancy lock and feel safer with your contract okay the last tip is pull over push so what do i mean by push and pull push means to that the contract sends if or token to some address on the other hand pull means you allow the caller to withdraw if or token and why do we recommend why do we recommend pull over push well first of all if you do a push it might be vulnerable to re-engine c and there's also a chance that you accidentally send it to the wrong address so let's take a look at some code so a bad way of a example of a this will be an example of a push would be to directly sending each to some address but notice that if we were to put in some wrong address here then this would send eve to the wrong address another example might be to like send if many even though that let's say that some of most of the addresses inside this receiver are regular users but one of them is a malicious user and this person decides to reject all if sent to this address then this function will fail even though only one most of the users are regular users so that's another reason why uh push pushing eve to another address is not recommended so here's an example of sending a token a better approach is to let the user pull the token and what i mean by this well it's going to take two steps first the contract approves the user to withdraw from the contract so here for example i have a function called gay beef and it will increment the if balance for the address 2 and if for some reason we put in the wrong address then we can easily fix this and once this if balance is incremented the the user can call withdrawal eve and this will only decrement the amount of beef that was allocated to them so this will be the user pulling some if into their address and the same example for uh erc20 tokens we can increment the balance of the token for address 2 and let the user pull tolkien and that completes the 10 best practices that i picked up learning solidity amazing that was uh that was great and uh also uh very concise said that to the point now people were super excited there's a two kind of common questions that came up uh one is in your pure functions example um we want to clarify if that's a typo where it says total supply i think there was a it just says to a supply so we wanted to see if that's where the collision wasn't coming from or was actually like a miss a typo um so we'll get that cleared for the person who asked that question um and the second request is if you are able to share uh these 10 examples so the code whether it's through a slide deck or just to get a repo uh fit much appreciate it because they wanna okay themselves as well uh i'll send you the links or maybe you want me to plug it in to uh me directly or in the chat too and we can also communicate that to all the hackers on discord too okay i'll do that uh oh sorry yeah yeah uh oh sorry in case you want to address the first one you're more than welcome to but yes um maybe create a github issue because i think yes we're almost on time yeah yes well thank you so much and uh this was great and uh thank you for doing this thing you're doing this thing super late in coming your time really appreciate it yes all right no problem yeah thanks for uh inviting me

Automatic transcript — names and jargon may be misspelled.