
As blockchain networks continue to grow in popularity, the importance of optimizing smart contracts becomes increasingly apparent. Inefficient contracts not only cost more in gas fees but can also lead to network congestion and poor user experience. At HyperLiquid, we've developed and refined numerous optimization techniques to create high-performance, cost-effective smart contracts. This article shares our most effective strategies for smart contract optimization.
Understanding Gas Costs
Before diving into optimization techniques, it's essential to understand how gas costs work on Ethereum and similar platforms. Gas is consumed for:
- Computation: Operations like arithmetic, comparisons, and cryptographic functions
- Storage: Reading from and writing to contract storage
- Memory: Temporary storage during execution
- Contract deployment: One-time cost when deploying a contract
Of these, storage operations are typically the most expensive. A storage write can cost 20,000+ gas, while a storage read costs around 2,100 gas. In contrast, most computational operations cost between 1 and 50 gas.
Storage Optimization Techniques
Given the high cost of storage operations, optimizing how your contract handles storage can yield significant gas savings.
1. Storage Packing
Ethereum storage is organized in 32-byte slots. By default, each state variable occupies a full slot, regardless of its actual size. Storage packing involves organizing variables to fit multiple values into a single slot.
Example of inefficient storage:
uint8 public a; // Uses a full 32-byte slot
uint256 public b; // Uses a full 32-byte slot
uint8 public c; // Uses a full 32-byte slot
Optimized version:
uint8 public a; // These three variables
uint8 public c; // are packed into a
uint256 public b; // single 32-byte slot
The Solidity compiler automatically packs variables when possible, but it's important to declare them in order from smallest to largest to maximize packing efficiency.
2. Use Mappings Instead of Arrays
Arrays in Solidity store their data contiguously, which means operations like push() and pop() require updating multiple storage slots. Mappings, on the other hand, use a hashing function to determine where each value is stored, making them more gas-efficient for many use cases.
Inefficient approach using an array:
address[] public users;
More efficient approach using a mapping:
mapping(uint256 => address) public users;
uint256 public userCount;
3. Minimize Storage Updates
Each storage write operation is expensive. Look for opportunities to avoid unnecessary updates:
- Cache frequently accessed storage variables in memory
- Batch multiple updates into a single transaction
- Use memory for intermediate calculations
Example of caching storage variables:
// Inefficient - multiple storage reads
function addToTotal(uint amount) external {
total += amount;
if (total > limit) {
total = limit;
}
}
// Optimized - single storage read and write
function addToTotal(uint amount) external {
uint currentTotal = total;
currentTotal += amount;
if (currentTotal > limit) {
currentTotal = limit;
}
total = currentTotal;
}
Computational Optimization Techniques
While less impactful than storage optimizations, computational efficiency can still provide meaningful gas savings, especially for complex contracts.
1. Use Fixed-Point Arithmetic for Decimals
Solidity doesn't natively support floating-point numbers. Instead of using external libraries for decimal math, use fixed-point arithmetic with integer scaling.
Example using fixed-point arithmetic with 18 decimals:
// Calculate 5% of a value
function calculateFivePercent(uint256 value) public pure returns (uint256) {
return value * 5 / 100;
}
2. Optimize Loop Operations
Loops can be gas guzzlers, especially when interacting with storage. Consider these optimizations:
- Cache array length outside the loop
- Use unchecked blocks for increment operations (Solidity 0.8.0+)
- Consider batching operations across multiple transactions if needed
Example of optimized loop:
// Inefficient loop
function sumArray(uint[] storage data) public view returns (uint) {
uint sum = 0;
for (uint i = 0; i < data.length; i++) {
sum += data[i];
}
return sum;
}
// Optimized loop
function sumArray(uint[] storage data) public view returns (uint) {
uint sum = 0;
uint length = data.length;
for (uint i = 0; i < length;) {
sum += data[i];
unchecked { i++; }
}
return sum;
}
3. Use the Right Data Types
Choose appropriate data types to minimize computational overhead:
- Use uint256 for most numeric operations (it's the EVM's native word size)
- Use smaller uint types (uint8, uint16, etc.) only when packing storage variables
- Use bytes32 instead of string for fixed-length data
Function-Level Optimizations
How you structure your contract's functions can significantly impact gas costs.
1. Function Visibility
Using the most restrictive visibility modifier reduces gas costs:
- external is cheaper than public for functions not called internally
- internal is cheaper than private (surprisingly)
2. Use Function Modifiers Sparingly
Modifiers inject code into functions, which can increase gas costs. For frequently called functions, consider inline checks instead of modifiers.
3. Short-Circuit Evaluation
Take advantage of short-circuit evaluation in logical expressions to put cheaper checks first:
// Less efficient - expensive operation always executes
function checkConditions(uint value) public view returns (bool) {
return expensiveOperation(value) && cheapOperation(value);
}
// More efficient - expensive operation only executes if necessary
function checkConditions(uint value) public view returns (bool) {
return cheapOperation(value) && expensiveOperation(value);
}
Contract-Level Optimizations
These broader strategies can reduce the overall gas footprint of your smart contract system.
1. Contract Factoring
Split large contracts into smaller, specialized contracts. This approach has several benefits:
- Reduces deployment costs
- Makes upgrades more manageable
- Allows parallel development and testing
2. Proxy Patterns
Use proxy patterns to separate storage from logic, enabling upgrades without migrating data:
- EIP-1967 Transparent Proxy Pattern
- UUPS (Universal Upgradeable Proxy Standard)
- Diamond Pattern (EIP-2535) for complex systems
3. Libraries for Reusable Code
Move common functionality to libraries. Library code is deployed once and reused across contracts, reducing deployment costs.
Advanced Optimization Techniques
For projects where every gas unit matters, consider these advanced techniques.
1. Assembly for Critical Functions
Inline assembly gives you direct access to EVM operations, potentially allowing for more efficient implementations of critical functions. However, this comes with increased complexity and reduced readability.
Example of optimizing a simple storage access using assembly:
// Standard Solidity
function getValue() public view returns (uint256) {
return myValue;
}
// Using assembly
function getValue() public view returns (uint256 result) {
assembly {
result := sload(myValue.slot)
}
}
2. Custom Error Messages
Solidity 0.8.4 introduced custom errors, which are more gas-efficient than error strings:
// Less efficient
require(amount <= balance, "Insufficient balance");
// More efficient
error InsufficientBalance();
if (amount > balance) revert InsufficientBalance();
3. Batching Operations
Combine multiple operations into a single transaction to amortize the fixed costs of transaction execution:
- Batch token transfers
- Update multiple related state variables at once
- Process multiple user actions in a single call
Measuring and Verifying Optimizations
Never optimize blindly. Always measure the impact of your optimizations:
- Use gas reporters in your testing framework
- Conduct before/after gas analysis for each optimization
- Focus on functions that will be called frequently
- Consider the tradeoff between readability and optimization
Conclusion
Smart contract optimization is a balance between efficiency, readability, and maintainability. While gas optimization is important, it shouldn't come at the expense of security or code clarity. Start with the high-impact optimizations like storage efficiency, and progressively apply more advanced techniques as needed.
At HyperLiquid, we've applied these optimization techniques to reduce gas costs by up to 60% in complex DeFi protocols while maintaining security and functionality. If you're developing a gas-intensive application and need expert assistance, our team is available for consultations and audits.
Remember that blockchain platforms and best practices continue to evolve. Stay updated with the latest developments in the Ethereum ecosystem, especially as Ethereum 2.0 and Layer 2 solutions change the optimization landscape.