Esoteric gas optimization in Solidity

Programming in Solidity is different from other languages, as each instruction and byte of memory wastes gas – users’ money. There are already a lot of resources on the web with basic code optimization techniques (for example, try to use calldata instead of memory), but I want to show some completely crazy and non-obvious ones.

It will be very difficult to understand what I’m talking about without basic experience in solidity, but maybe these optimizations will show your interest in ethereum programming.

  1. 1 wei in msg.value

https://github.com/AztecProtocol/weierstrudel
> The weierstrudel smart contract requires precisely 1 wei to be sent to it or it will refuse to perform elliptic curve scalar multiplication. No more, no less.

When calling some code on Ethereum, it is possible to send along with the call some amount of the native currency – ether. This happens by setting the value parameter.

AztecProtocol requires users to send exactly 1 wei with each contract call, which saves about 500 units of gas. Why does it work?

The EVM (Ethereum Virtual Machine) has a stack. All arithmetic operations take place through it, for example, to add two numbers, you must first call the assembler instructions PUSH to put the numbers on the stack, and then ADD to add them.

PUSH instruction costs 3 gas.

The CALLVALUE instruction pushes the value of msg.value onto the stack and costs only 2 gas. If the code has a lot of math using a unit, it’s cheaper to take it from msg.value. The protocol as a whole saves about 500 gas for each contract call.

  1. Using negative numbers.

Calldata is a section of memory that lives only within a single transaction, for example, parameters passed to a function from outside. Non-zero bytes in calldata cost four times as much gas as zeros – that’s the EVM standard. Using int and sending negative numbers can be very expensive, as negative numbers have a padding of 0xf bytes at the beginning.

» abi.encode(int(-8)) 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8

  1. Freeing up memory

https://github.com/euler-xyz/euler-contracts/blob/617c1dbaa3f506e881cd9000fd89b1822d4be650/contracts/Base.sol#L62

In addition to the stack, memory can be allocated on the heap, for example, for arrays and structures. Each allocation costs gas, and can cost a pretty penny. Solidity does not automatically free memory, even if it is not used anywhere else. Allocation is implicit. Allocated memory used within a single internal function can greatly increase the call cost of any contract that uses it.

Memory is allocated in consecutive blocks. Memory location 0x40 stores a pointer to the next free block of memory. Any allocations are a simple increment of this pointer.

The compiler does not promise that there will be zero in free memory – any garbage can be there.

With all this in mind, Euler created a modifier that allows you to “free” unused memory. Its mechanism of operation is simple – store the value at address 0x40, execute the function and write the stored value of the memory cell back to 0x40. Thus, any memory allocated inside the function will be considered free.

  1. Packing boolean values ​​into uint256

The bool type in solidity occupies 1 byte in memory, of which only one byte is used. If you need multiple boolean values, you can replace bool with uint32 or uint256 and bitwise arithmetic. So uint256 can store up to 256 boolean values.

  1. Using indexed arguments for events

The event keyword allows you to declare events that can then be thrown during the execution of the contract, and these events will be available from the outside. Marking arguments with the keyword indexed allows you to search through them using filters, but not only – they start to cost less memory. The secret lies in the fact that indexed arguments are put on the stack, and ordinary arguments are put in memory. The cost of new memory goes up quadratically, and using indexed parameters will almost always save gas.

  1. Writing to non-zero storage slots.

Storage is the most expensive gas storage area in Solidity. It consists of slots of 256 bytes. Writing to storage slots that previously held 0 costs 20,000 gas. Writing to non-zero slots costs 5000 gas, 4 times less. Often all 256 bits of a slot are not needed, in which case it is possible to pack both 32 and 64 bit values ​​into a single storage slot. The first entry will cost the full 20,000, but subsequent entries will cost less.

  1. ≠ more expensive than ==

It’s simple – the ≠ operator costs more gas because in the evm bytecode it is represented as using == + NOT , which wastes more gas than just == . If it is possible to express a boolean expression without ==, one can save gas on that.

This can be used, for example, when using the reentrancy guard. This is usually done through a single variable that stores either ENTERED or NOT_ENTERED values. Before calling functions, this variable is checked, and if the value is ENTERED, then the call does not occur. In such a situation, it is cheaper to use == than ≠.

  1. Function Name Optimization

Calling far from all functions costs the same amount of gas. Their names directly affect the amount of gas spent! To call a function, the first 4 bytes of the hash are used from its name and argument types. These 4 bytes are stored in the calldata we mentioned above and are sent as part of the transaction. The more zeros among them, the less gas is required. Thus, the correct choice of function name will require less gas.
https://emn178.github.io/solidity-optimize-name/

  1. Inline assembly division

Using the assembly opcode div is always cheaper than using the solidity division operator by almost 100 gas.

The compiler always inserts a null check, which costs extra gas. If you know the hidden invariants of your code and that the variable to be divided by will never be equal to 0, then you can get rid of this check – the compiler itself cannot do this. You can write division in assembler via the div opcode.

result = numerator / can_never_be_zero; // expensive
assembly {
  result := div(numerator, can_never_be_zero; // cheap
}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *