Gas optimization tips for Solidity

Ethereum ClassicEthereum Classic
17D Ago
It’s very necessary to understand how to do this because if it costs too much money to execute a function in your smart contract, fewer users will be willing to run your Dapp. gas is the unit used in Ethereum for measuring and limiting computations per block. As of June 2022, and referring to block size and gas, in the [Ethereum documentation about gas]( it’s stated that: >*“Each block has a target size of 15 million gas, but the size of blocks will increase or decrease in accordance with network demand, up until the block limit of 30 million gas (2x the target block size).”* # Function names: Solidity compiler reads and executes function names by their selector. The selector of a function is made up of the first four bytes of the keccak256 hash of the function signature (function name and parameters type). The Solidity compiler will sort all the functions in a contract by their selector when executing the function. For example: function tryThis(uint256 \_value, string\[\] memory \_names) external {} In this case: * Function signature = “tryThis(uint256,string\[\])” * Function selector = keccak256(signature) = 0x7f6ca090. ​ here calling the *green()* function will cost 110 more gas (5x22) than the *red()* function just because of its name. # Caching the data inside the function: **Caching the data inside a function in Solidity can result in lower gas usage, even if it needs more lines of code.** ​ The contract, storageExample, has two functions: **inefficcientSum** and **efficcientSum** Both functions take *\_array*, which is an array of unsigned integers, as an argument. They both set the contract’s state variable, *sumOfArray*, to the sum of the values in *\_array*. **inefficcientSum** uses the state variable, itself, for its calculations. Remember that state variables, such as *sumOfArray*, are kept in storage*.* **efficcientSum** creates a temporary variable in memory, *tempVar*, that is used to calculate the sum of the values in *\_array*. *sumOfArray* is then subsequently assigned to the value of *tempVar*. *efficcientSum* is >50% gas efficient than *inefficcientSum* when passing an array of **only 10 unsigned integers.** # Zero vs non-zero values and gas refunds: Changing a value from 0 to non-zero on Ethereum blockchain is expensive (G*sset* = 20,000 gas), while changing a value from non-zero to 0, can give you a refund in gas value (R*sclear*) (translated into a discount on execution price). It’s important to note that one can only get refunded by up to a maximum of 20% of the total transaction cost, meaning that one will only get a refund if the transaction costs a minimum of 24,000 gas. ## CASE 1 Alice has 10 tokens and Bob has 0 tokens. Alice will send 5 tokens to Bob. This will change Alice balance from a non-zero value (10) to another non-zero (5), and it will change Bob’s balance from 0 to non-zero (10 tokens). * Non-zero to non-zero (5,000 gas\*) + zero to non-zero (20,000 gas) = 25,000 gas ## CASE 2 Alice has 10 tokens, Bob has 0 tokens. Alice will send all her 10 tokens to Bob. This will change Alice balance from a non-zero value to zero, and Bob’s balance from non-zero (0) to non-zero (10). * Non-zero to zero (5,000 gas\*) + zero to non-zero (20,000 gas) — **Refund (4,800 gas)** = 21,200 gas Clearly, the gas cost of the transaction in case 2 is cheaper due to the refund amount rewarded for changing Alice’s balance value from non-zero to 0. This should help you note that **for every non-zero to 0 operations, it’s a good idea to spend at least 24,000 gas elsewhere in the transaction** (if it works within the project’s workflow). This practice is most common with NFTs. Developers will store an NFT’s metadata (its image, attributes, etc.) on a decentralized storage network, like Arweave or IPFS, in place of storing it on-chain. The only data that is kept on-chain is a link to the metadata on the respective decentralized storage network. This link is queryable by the *tokenURI()* function found in all ERC721s that contain metadata. # Memory vs calldata: Storing information inside *calldata* is always less expensive than storing it on *memory*, but it has a clear downside to it. When *calldata* is used, the value stored in it can’t be mutated during the function execution. So, if you need to alter the data of a variable when executing a function call, use *memory* location instead. **If you only need to read the data, you can save some gas by storing it in** ***calldata*****.** ​ # Incrementing/Decrementing by 1 There are four different ways to increment or decrement by 1 using Solidity in the following example: ​ As you can imagine, this is because different op-codes are needed for each one of these different functions (which all achieve the exact same result). The most commonly used is probably the one from V1 contract, but it’s also the most expensive out of the four. **A good recommendation could be to prefer using the pre-increment expression (++number)**, so it increments the value before evaluating, saving some gas in the process. # Unchecked blocks: overflow/underflow: Since the release of Solidity 0.8.0, arithmetic overflow and underflow are taken care of by the Solidity compiler. In this sense, the contracts are more secure (from the arithmetic perspective) but a bit more expensive, This is because behind the curtains there are op-codes checking if the number obtained post-operation makes sense (in an addition, for example, the result must be bigger than at least one of the terms). But why would someone want to use an unchecked block running the risk of an overflow/underflow? For example, in a case where the contract has a function which increments just by one when called (and is preferably not called so frequently), or if the contract imports [Open Zeppelin’s Counters library]( This is safe because the probability of causing an overflow/underflow by incrementing just by one on each transaction is quite close to 0 due to all the time and gas needed for reaching the 2²⁵⁶ number. ​ **we can make use of unchecked blocks of code to save gas when we know the architectural flow of the contract implementation will not allow causing an overflow or underflow under the unchecked conditions.** # Payable vs non-payable: **Payable functions are cheaper than non-payable ones** because for the non-payable ones, the contracts need to have some extra op-codes to be ready to check if another contract or an external account is trying to send ETH to it, and if so, revert the transaction. Payable functions don’t have those extra op-codes ​ # Memory expansion cost: When a contract call needs to use more than 32 kilobytes of memory storage in a single transaction the gas cost will be much higher. ​ To avoid this memory cost explosion, try breaking down the implementation of a transaction into pieces and also, **do not populate arrays with enormous amounts of items in a single transaction.** # Less/greater than or equal to: In Solidity, there is no single op-code for ≤ or ≥ expressions. What happens under the hood is that the Solidity compiler executes the LT/GT (less than/greater than) op-code and afterwards it executes an ISZERO op-code to check if the result of the previous comparison (LT/ GT) is zero and validate it or not. Example: ​ The gas cost between these contracts differs by 3 which is the cost of executing the ISZERO op-code, **making the use of < and > cheaper than ≤ and ≥**. # Operators: and/or When having a *required* statement with 2 or more expressions needed, place the expression that cost less gas first. So, in *require* statements with && or || operators, place the cheapest expression first for execution, so that the second and most expensive expression can (sometimes) be bypassed **Avoid Object Oriented Programming: the CREATE Opcode:** The CREATE opcode is used when creating a new account with the associated code (i.e. a smart contract). It costs *at least* 32,000 gas and is the most expensive opcode on the EVM. It is best to minimize the number of smart contracts used when possible. Below is some code to create a “vault” using an object-oriented approach. Each vault contains a uint256, which is set in its constructor. ​ Each time that *createVault()* is called, a new *Vault* smart contract is created. The value stored in the *Vault* is determined by the argument passed into *createVault().* The address of the new *Vault* contract is then stored in an array, *factory.* Now here is some code that accomplishes the same goal but uses a mapping in place of creating a new smart contract: ​ This difference in implementation leads to a dramatic reduction in gas costs. It should be noted that there are certain times when creating a new contract from within a contract is desirable and is typically done for immutability and efficiency.