Gas Optimization in DeFi: Tools, Techniques, and Lessons from Onchain Perps
Gas optimization is a key challenge in building complex DeFi systems. At Riva Labs, while developing Denaria, a fully onchain perpetual futures protocol, we cut trade execution costs by ~20% through code review, caching, storage layout, and low level tricks. This article shares the tools, techniques, and lessons we learned from optimizing a large Solidity project.


Introduction
At Riva Labs, we take part in the development of Denaria, a fully Onchain Perpetual Futures Protocol. Developing such a complicated system has many potential pitfalls, and one of them is high gas costs. High level operations, such as a trade on Denaria, require a huge amount of lower level computations: from the basic operation of moving the user’s balance, to the more complex operations required to properly distribute liquidity to LPs, update the funding rate, check the margin ratio of the user, and many more computations required for the correct functioning of the system. All these computations come at a cost, and that’s why we put our efforts into optimizing as much as possible every line of code of the most common operations, such as trading.
In this article we want to share with you our journey into the optimization of a large solidity project, which tricks worked best for us and which ones did not work in our case.
Gas monitoring/testing tools
We developed and tested our smart contracts with the Foundry toolkit, an integrated toolchain that streamlines writing and running tests. Foundry’s built-in cheatcodes let us manipulate the test environment: mock callers, shift timestamps, and measure gas usage, among other capabilities. In particular, we used its gas-snapshot utilities to track consumption on specific code paths. During the optimisation phase, we recorded the gas cost of the trade function after each change to verify that every revision produced a measurable improvement. We focused on the trade because it is the most frequently invoked high-level entry point in our system and therefore the main driver of users’ gas costs.
Foundry also provides gas estimates for running each test in your test files. A possible approach to measuring the gas in an accurate way, if the functions you want to test for allow that, is to write a test specifically for the function you’re measuring, executing just the relevant instructions. When running the tests, Foundry will show the gas cost for each one, and, for fuzz tests, it shows both average and median gas consumption for that test.
Example of the result of a normal unit test:
[PASS] testComputeLongReturn() (gas: 519338)
Example of the result of a fuzz test:
[PASS] testFuzzComputeLongReturn(uint256) (runs: 258, μ: 523473, ~: 524271)
Optimization steps
In total, our gas optimization work led us to cut ~20% of the trade function gas cost. Now, in the following section we will cover all of the main steps that we took to optimize our contracts, sorted from most to least impactful.
General disclaimer: Gas vs BytecodeSize, Solidity Optimizer
While optimizing the gas consumption was our main goal, we still had to keep in mind another looming presence: the Spurious Dragon size limit. In short, every contract’s compiled bytecode must be under 24.576 bytes to be deployed onchain.
Our main contract’s bytecode, if no specific steps were made to reduce it, would be over 40KB. After some optimization on this side we managed to shrink it down to 23.5KB.
For this reason we had to balance the usage of gas efficient techniques with the fact that each addition to the code could bring us over that hard limit, thus preventing us from implementing some optimization.
The solidity optimizer
When the solidity compiler compiles your contract, it performs several optimizations in the code to reduce both contract’s size and gas cost of the functions. The operations that the optimizer performs on the code can be specified manually through the parameter optimizerSteps (see Optimizer docs for more info).
There was some work in the past by the solidity compiler team to provide some tools to find the best optimizerSteps for a given project (this issue in the solidty repository is the most advanced work we managed to find), but as of today the process of finding an optimizer step sequence for your project if mostly manual.
Another relevant parameter for the optimizer to be careful about is the optimizerRuns parameter. It is not to be confused with the amount of passes the optimizer will do on the code! It represents a tradeoff between code size (and thus deployment cost) and code efficiency. A value of 1 will provide a small but less efficient code, while a higher value (max 2^32-1) will provide a longer code that is more efficient at call time. We were forced to use the minimum value of this parameter in order to reduce the bytecode size as much as possible and stay under the spurious dragon limit.
Review your logic
It may seem obvious, but the most impactful optimization for us was reviewing the code step by step and reworking the logic to eliminate any unnecessary parts. When you build a complex system like ours, each high level call, such as a trade, corresponds to a lot of intermediate calls to lower-level functions which take care of the necessary checks and operations to make sure that the trade is executed correctly and the system is running safely.
While going through our code we identified some operations that were redundant most of the times during trades, but still needed in some edge cases. By only executing them in these edge cases we cut a relevant part of the gas cost.
This step is a very manual process that we cannot give more precise guidelines on, since it depends on your code structure, but it’s a simple process you should always perform before starting with more low-level optimizations.
Cache values to avoid repeated storage reads or computations.
Now we start heading into the more general optimization techniques that can be applied to reduce gas costs. The one optimization we found being most effective in our code is caching values to avoid repeated computations and storage reads.
One MLOAD opcode (loading a 256-bit word from memory) is always much cheaper than the SLOAD (loading a 256-bit word from storage). For this reason if you have to use a storage variable multiple times in your function it is more optimal from a gas point of view to store it in a memory variable.
Reading values from memory is so cheap that sometimes it’s even more efficient to store in memory an intermediate result of a computation you have to perform multiple times, instead of performing the computation again.
In our experience we mostly utilized this technique in the functions that are responsible for the AMM curve computations, which are a lot of repeated mathematical operations. We measured that caching intermediate values there resulted in several thousands of gas savings.
Take care of packing in the storage layout
The storage in solidity is organized in 256-bits slots. When you declare your storage variables you can order them in such a way that the compiler can pack multiple variables inside the same storage slot.
For example if you storage looks like this:
bool
uint256
bool
uint256
bool
your storage layout is inefficient. That’s because the compiler won’t be able to pack the three bools (8 bytes each) together in the same storage slot, since there are 256 bits uint in the middle. You can reorder the declarations as such:
bool
bool
bool
uint256
uint256
This should result in a more compact storage layout, meaning cheaper accesses and lower bytecode size.
Funny remark: this is probably the only step where LLMs can help you in this journey to speed up the process and make it less tedious. Vibe coding and solidity do not mix well at all, but reordering variables without touching anything else is within the range of capabilities of LLMs.
Use immutable/constant where possible
Using immutable or constant variables reduces significantly the read costs for those variables. In practice this is not always applicable, but if you have some variable that is not supposed to change value (e.g. a variable holding the decimal places of some other variables in your contract) you can use this trick to save some gas.
Use inline assembly where useful
Solidity provides the possibility to insert inline Yul assembly in your contracts, which can save you some gas if you use it properly.
You should identify some parts in your code which are simple functions called repeatedly, but perform simple operations. If these functions can be realistically rewritten using inline assembly you should try that.
Always check that your assembly is better than the compiler’s!
Keep in mind that the solidity optimizer already does translate your solidity code into assembly. Always check that the optimizations you are making actually reduce the gas cost of your functions. Writing inline assembly can be more efficient, usually because it skips some safety checks the operations in solidity automatically implement, which brings us to the next point: Be careful of overflows/underflows. Writing inline assembly usually bypasses these checks, so be careful to not introduce vulnerabilities or bugs in your code!
Additionally, there are many libraries (e.g. Solady) which include useful snippets already written in assembly that you can import and use in your code, check for an already existing implementation before starting to write your own.
Minor optimizations
Other than the ones we explicitly mentioned in this document, we employed many more optimization techniques that resulted in minor gas savings (<3000 gas in total), such as using unchecked math where possible, short circuiting logic conditions.
These techniques did not give us significant results, but they may do in your case, so don’t be afraid to go ahead and try all of these tricks in your smart contracts. There are many resources you can find online that can guide you through the optimization of your smart contracts, you can find the ones we used as reference at the end of this document.
Conclusion
Gas optimisation is rarely a straight line, in our experience it’s mostly disciplined trial and error. We’ve shown you our setup, the metrics we tracked, and the way we iterated; now it’s your turn to push it further.
A few principles to keep in mind:
Start with a clean baseline and a repeatable benchmark.
Change one thing at a time and measure every change.
Verify correctness after each change, build robust unit, fuzz and invariant tests.
Document results as you go so you can revert easily if necessary.
Keep a healthy balance between code efficiency and readability. It’s not always worth it to make your code less human-readable to save 10 gas.
If you need help with gas optimization for your EVM project, or are facing any technical challenge, don’t hesitate to reach out to our team.
Resources

Subscribe to our newsletter
By submitting you email, you confirm that you accept our Privacy Policy

Subscribe to our newsletter
By submitting you email, you confirm that you accept our Privacy Policy

Subscribe to our newsletter
By submitting you email, you confirm that you accept our Privacy Policy
Latest articles

Development
Jul 8, 2025
Gas Optimization in DeFi: Tools, Techniques, and Lessons from Onchain Perps

Development
Jul 8, 2025
Gas Optimization in DeFi: Tools, Techniques, and Lessons from Onchain Perps

Development
Jul 8, 2025
Gas Optimization in DeFi: Tools, Techniques, and Lessons from Onchain Perps

News
Jun 27, 2025
Announcing Riva Labs: A Tech Firm Dedicated to Onchain Apps Development

News
Jun 27, 2025
Announcing Riva Labs: A Tech Firm Dedicated to Onchain Apps Development

News
Jun 27, 2025