Solidity Tips & Tricks

3/2/2025 - Originally 11/2025


Welcome to my collection of Solidity insights! These tips and tricks represent knowledge I gathered during my internship at a DeFi protocol over a year and a half ago. Despite the rapid evolution of this technology, these fundamental practices remain invaluable for both developing and auditing smart contracts today. Whether you're a seasoned developer looking to refine your skills or an auditor seeking to sharpen your security mindset, I hope these practical insights help you build more secure and efficient smart contracts. Each recommendation comes from real-world experience navigating the complexities of decentralized finance.
  1. Use Higher Precision
    Increase the precision of your calculations by using a larger fixed-point base. If you're using 1e18, consider using a higher base like 1e36 for intermediate calculations.
  2. Delay Division
    Always multiply before dividing. This ensures that you’re working with the largest numbers possible, which reduces the chance of truncation and rounding errors.
  3. Compare Expected Withdraw Value with Actual Withdraw Value
    Always validate that the actions taken by the users are in line with the expected behavior.
  4. Complexity Can Be a Breeding Ground for Bugs
    The more complex a contract or function is, the more likely it is to have vulnerabilities.
  5. Check for Stale/Outdated Price Data from Oracles
    Compare timestamps returned from price checks to the current timestamp. Define a maximum time threshold and handle outdated prices accordingly.
  6. User-Friendly Design
    It's crucial to design contracts that minimize the potential for user error. If a function is likely to be misused or misunderstood, it might be worth reconsidering its design.
  7. Avoid Duplicated Calculations
    Several unnecessary calls to other functions may cause extraneous gas usage.
  8. Clean Up Code
    Remove irrelevant comments, or unused code to increase readability.
  9. Revert Early Into the Code
    When a require statement (or assert, revert, etc.) is triggered, the transaction is reverted, and all the gas consumed up to that point is not refunded. The gas used is essentially "wasted". However, the remaining gas that was not yet consumed is returned to the sender. If a transaction is likely to fail due to a certain condition, it's more efficient to check that condition as early as possible in the function. By doing so, you avoid performing unnecessary operations and consuming more gas than needed before hitting the require statement. Also, it is often clearer to handle preconditions and checks at the beginning of a function. This makes the code more readable and ensures that any prerequisites for the function’s logic are met upfront.
  10. Interfaces Run a “Dry Run” or “Simulation” Before the Actual Transaction
    If this simulation detects that the transaction will fail (e.g., due to a require condition not being met), the wallet will typically warn the user that the transaction may fail. This can prevent users from submitting transactions that are doomed to revert, saving them from wasting gas.

    Considerations:

    • Even if a user is warned and chooses to proceed (or if they don't receive a warning), placing the require checks early in the function can still save them gas. If a transaction is going to fail, it's better for it to fail after consuming 10,000 gas units rather than 100,000.
    • Some transactions might be part of a more complex series of calls, like in a DeFi protocol where multiple contracts interact. In such cases, the dry run might not always catch every potential failure, especially if conditions change between the simulation and the actual transaction (e.g., due to other transactions being mined in between).
  11. Understand Assumptions
    Contracts often operate under certain assumptions (e.g., only working with Call options vs working with Put calls). It's essential to validate these assumptions in the code.
  12. Product-Sum Approach for Scalable Reward Distribution with Compounding Stakes (LUSD Protocol)
    Since it would cost a lot of gas to update each depositor of the stability pools balance when liquidations occur, two variables can be used, Product - Cumulative Depletion Factor, and Sum, Accumulated Gains. These two variables are mapped to an epoch, which successfully scales user balances with requiring minimal computation.
    Liquity Formulae Link
  13. Precision Matters
    In systems like Ethereum, which use fixed-point arithmetic, precision is crucial. Small discrepancies can lead to significant vulnerabilities, especially when dealing with very large or very small numbers.
  14. Understand the Math Behind the Code
    It's not enough to just review the code; auditors must understand the mathematical principles and formulas that the code implements.
  15. Always Double-Check Return Values
    Ensure that return values accurately reflect the intended logic and calculations of the function.
  16. Math Operations Require Extra Attention
    Blockchain transactions are immutable. Errors, especially those related to mathematical operations (like rounding errors in division), can lead to significant issues. Always be wary of potential overflows, underflows, and rounding issues.
  17. Follow the Flow of Tokens and Rewards
    In DeFi projects, it's essential to trace how tokens and rewards flow within the system. Ensure that all paths (like fee distributions) lead to the expected outcomes.
  18. Include test suites prior to sending code for an audit (as a builder)

    Quoted from Prisma Finance Audit by Zellic:

    “When building a complex contract ecosystem with multiple moving parts and dependencies, comprehensive testing is essential. This includes testing for both positive and negative scenarios. Positive tests should verify that each function’s side effect is as expected, while negative tests should cover every revert, preferably in every logical branch. Good test coverage has multiple effects.

    • It finds bugs and design flaws early (preaudit or prerelease).
    • It gives insight into areas for optimization (e.g., gas cost).
    • It displays code maturity.
    • It bolsters customer trust in your product.
    • It improves understanding of how the code functions, integrates, and operates — for developers and auditors alike.

    Therefore, we recommend building a rigorous test suite that includes all contracts to ensure that the system operates securely and as intended.”
  19. Always Check for Timestamp Dependence
    Miners can slightly manipulate block timestamps. While the deviation is limited (by the protocol to be within a certain range of the actual time), it can still be exploited in some scenarios.
  20. Post Deployment Checks
    Add post-deployment checks to check the public fields are set correctly and run a smoke and integration test after deployment to each environment.
  21. Allowance Double-Spend Exploit
    Developers of applications dependent on approve()/transferFrom() should keep in mind that they have to set allowance to 0 first and verify if it was used before setting the new value.
  22. Role Changes Should Be Multiple Steps
    In a setRole() function, there should be multiple steps involved as a safety measure to prevent invalid address input.
  23. Optimization Tip: Avoid Zero to One Storage Writes Where Possible
    Initializing a storage variable is one of the most expensive operations a contract can do.
  24. Optimization Tip: Use unsafeAccess in OpenZeppelin's Arrays.sol
    This function allows developers to access an array's element by its index without the usual length check.
  25. Optimization Tip: Use Bitmaps Instead of Bools When a Significant Amount of Booleans are Used
    Using bitmaps instead of individual booleans is a smart optimization technique when dealing with operations that involve marking a large number of entities (like addresses) with a binary state (like claimed/not claimed). It maximizes the efficiency of Ethereum's storage mechanism, leading to reduced gas costs.
  26. Optimization Tip: Avoid Having ERC20 Token Balances Go to Zero, Always Keep a Small Amount
    If an address is frequently emptying (and reloading) it’s account balance, this will lead to a lot of zero to one writes.
  27. Optimization Tip: Make Constructors Payable
    Making the constructor payable saved 200 gas on deployment. This is because non-payable functions have an implicit require(msg.value == 0) inserted in them. Additionally, fewer bytecode at deploy time mean less gas cost due to smaller calldata.
  28. Optimization Tip: Admin Functions Can Be Payable
    We can make admin specific functions payable to save gas, because the compiler won’t be checking the callvalue of the function.
  29. Optimization Tip: Custom Errors are (Usually) Smaller Than Require Statements
    Custom errors are cheaper than require statements with strings because of how custom errors are handled.
  30. Optimization Tip: Prefer strict inequalities over non-strict inequalities, but test both alternatives
    It is generally recommended to use strict inequalities (<, >) over non-strict inequalities (<=, >=).
  31. Optimization Tip: Split Require Statements That Have Boolean Expressions
    When we split require statements, we are essentially saying that each statement must be true for the function to continue executing. If the first statement evaluates to false, the function will revert immediately and the following require statements will not be examined. This will save the gas cost rather than evaluating the next require statement.
  32. Optimization Tip: Use ++i Instead of i++ to Increment
    The reason behind this is in way ++i and i++ are evaluated by the compiler. i++ returns i(its old value) before incrementing i to a new value. This means that 2 values are stored on the stack for usage whether you wish to use it or not. ++i on the other hand, evaluates the ++ operation on i (i.e it increments i) then returns i (its incremented value) which means that only one item needs to be stored on the stack.
  33. Optimization Tip: Make for loop Index Unchecked
    This is an optimization technique that can be used to reduce the gas cost of a for loop. By default, the solidity compiler checks the loop index after each iteration to see if it has reached the end of the loop. This check can be removed by using the unchecked keyword. This can be useful in cases where the loop index is incremented in a way that guarantees it will not go out of bounds, i.e. unchecked ++i

    for (uint256 i = 0; i < someArray.length; unchecked {++i}) { // do something }

  34. Optimization Tip: do-while Loops are Cheaper Than for loops
    do-while loops are cheaper than for loops because they don’t have to check the condition before executing the loop.

    function loop(uint256 times) public pure { if (times == 0) { return; } uint256 i; do { // do something unchecked { ++i; } } while (i < times); }

  35. Optimization Tip: Avoid Unnecessary Variable Casting, variables smaller than uint256 (including boolean and address) are less efficient unless packed
    It is better to use uint256 for integers, except when smaller integers are necessary. This is because the EVM automatically converts smaller integers to uint256 when they are used. This conversion process adds extra gas cost, so it is more efficient to use uint256 from the start.
  36. Optimization Tip: Prefer Very Large Values for the Optimizer
    There's a trade-off involved in selecting the runs parameter for the optimizer. Smaller run values prioritize minimizing the deployment cost, resulting in smaller creation code but potentially unoptimized runtime code. While this reduces gas costs during deployment, it may not be as efficient during execution. Conversely, larger values of the runs parameter prioritize the execution cost. This leads to larger creation code but an optimized runtime code that is cheaper to execute. While this may not significantly affect deployment gas costs, it can significantly reduce gas costs during execution.
  37. Optimization Tip: It is Sometimes Cheaper to Cache Calldata
    Although the calldataload instruction is a cheap opcode, the solidity compiler will sometimes output cheaper code if you cache calldataload. This will not always be the case, so you should test both possibilities.
  38. Optimization Tip: Internal Functions Only Used Once Can Be Inlined to Save Gas
    It is okay to have internal functions, however they introduce additional jump labels to the bytecode. Hence, in a case where it is only used by one function, it is better to inline the logic of the internal function inside the function it is being used. This will save some gas by avoiding jumps during the function execution.
  39. Optimization Tip: Compare array equality and string equality by hashing them if they are longer than 32 bytes
    This is a trick you will rarely use, but looping over the arrays or strings is a lot costlier than hashing them and comparing the hashes.
  40. Optimization Tip: Use gasleft() to Branch Decisions at Key Points
    Gas is used up as the execution progresses, so if you want to do something like terminate a loop after a certain point or change behavior in a later part of the execution, you can use the gasprice() functionality to branch decision making. gasleft() decrements for “free” so this saves gas.
  41. Ensure Accurate Asset Valuation in Complex Financial Systems
    Inadequate or simplistic valuation methodologies can lead to severe financial risks, including the potential for insolvency. Rigorous testing and validation of financial models are essential, especially when multiple variables and parameters are involved.
  42. The Importance of Time-Sensitive State Management in Smart Contracts
    Meticulously consider how a smart contract's state evolves over time and how user interactions at different times can impact that state, as failing to do so can lead to unintended and potentially unfair outcomes. Always validate these aspects through comprehensive testing.
  43. The Importance of Contextual Awareness in Auditing
    When conducting an audit, it's crucial to go beyond the immediate system or codebase under review to understand its interactions with external elements and dependencies. This lesson underscores the need for "Contextual Awareness," which involves considering how a system interfaces with broader infrastructure, other systems, or external services. Failure to account for these interactions can lead to overlooked vulnerabilities or issues, as demonstrated by the case of a smart contract's Dutch auction mechanism not accounting for Layer 2 sequencer downtime. Therefore, a comprehensive audit should always include an evaluation of external factors and their potential impact on the system being audited.
  44. Always Consider Frontrunning Vulnerabilities
    In any system that involves transaction ordering or timing-sensitive operations, frontrunning vulnerabilities can pose significant security risks. Failing to anticipate and mitigate these risks can lead to disrupted transactions and manipulated outcomes, undermining the integrity and fairness of the system. Therefore, it's crucial to design with frontrunning safeguards in mind from the outset.
  45. It Is Critically Important To Handle Decimal Places Consistently And Accurately When Dealing With Token Pricing In Smart Contracts
    Failing to do so can lead to incorrect pricing calculations, which could have significant financial implications. Therefore, it's essential to ensure that your smart contract logic is robust enough to handle tokens with varying numbers of decimal places.
  46. Rebalance with Caution: Timing Matters
    When implementing a rebalancing mechanism for financial assets, consider the timing of component executions carefully. Changes in timing during multi-component rebalances can lead to inefficient pricing and potential losses. Ensure that your contract logic accounts for these timing considerations to maintain the integrity of asset valuations.