During my time at KryptoMind LLC, one of my key responsibilities was auditing and optimizing smart contracts. Through careful analysis and refactoring, we reduced gas fees by 30% for end users while simultaneously improving security. Here’s how we did it, with real examples you can apply to your own contracts.
Understanding Gas: The Hidden Tax
Every operation in Ethereum costs gas. A simple token transfer might cost 21,000 gas, while complex DeFi interactions can consume millions. At 50 gwei gas price and $2,000 ETH, that’s real money:
21,000 gas × 50 gwei × $2,000 / 1,000,000,000 = $2.10
Multiply that by thousands of users, and gas optimization becomes critical.
Anti-Pattern #1: Unnecessary Storage Writes
The Problem: Storage is the most expensive operation in Ethereum.
Before Optimization
// ❌ BAD: Multiple storage writes
contract BadToken {
mapping(address => uint256) public balances;
uint256 public totalSupply;
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount; // SSTORE: ~5,000 gas
balances[to] += amount; // SSTORE: ~20,000 gas
totalSupply = totalSupply; // SSTORE: ~5,000 gas (useless!)
}
}
After Optimization
// ✅ GOOD: Eliminated unnecessary write
contract GoodToken {
mapping(address => uint256) public balances;
uint256 public totalSupply;
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount; // SSTORE: ~5,000 gas
balances[to] += amount; // SSTORE: ~20,000 gas
// Removed useless totalSupply update
}
}
Savings: ~5,000 gas per transfer
Anti-Pattern #2: Reading Storage Multiple Times
The Problem: Every storage read (SLOAD) costs 2,100 gas in EVM.
Before Optimization
// ❌ BAD: Multiple storage reads
contract BadStaking {
mapping(address => uint256) public stakes;
function calculateReward(address user) public view returns (uint256) {
uint256 reward = 0;
if (stakes[user] > 100 ether) { // SLOAD: 2,100 gas
reward = stakes[user] * 10 / 100; // SLOAD: 2,100 gas (again!)
} else if (stakes[user] > 10 ether) { // SLOAD: 2,100 gas (again!)
reward = stakes[user] * 5 / 100; // SLOAD: 2,100 gas (again!)
}
return reward;
}
}
After Optimization
// ✅ GOOD: Cache storage in memory
contract GoodStaking {
mapping(address => uint256) public stakes;
function calculateReward(address user) public view returns (uint256) {
uint256 stakedAmount = stakes[user]; // SLOAD: 2,100 gas (once)
uint256 reward = 0;
if (stakedAmount > 100 ether) {
reward = stakedAmount * 10 / 100; // MLOAD: 3 gas
} else if (stakedAmount > 10 ether) {
reward = stakedAmount * 5 / 100; // MLOAD: 3 gas
}
return reward;
}
}
Savings: ~6,300 gas per call (3 avoided SLOADs)
Anti-Pattern #3: Using uint8 Instead of uint256
The Counterintuitive Truth: Smaller integers don’t save gas; they cost more!
Before Optimization
// ❌ BAD: Using uint8 thinking it saves gas
contract BadCounter {
uint8 public count = 0;
function increment() public {
count += 1; // Extra gas for conversion and masking!
}
}
After Optimization
// ✅ GOOD: Use uint256 (EVM's native word size)
contract GoodCounter {
uint256 public count = 0;
function increment() public {
count += 1; // Optimized for EVM
}
}
Why? The EVM operates on 256-bit words. Using smaller types requires extra operations to mask unused bits.
Exception: Pack multiple variables in a single slot:
// ✅ GOOD: Struct packing saves storage
struct User {
uint128 balance; // First slot (128 bits)
uint128 reward; // First slot (128 bits) - SHARED!
uint256 lastClaim; // Second slot (256 bits)
}
// Uses 2 storage slots instead of 3
Anti-Pattern #4: Unbounded Loops
The Problem: Loops that grow with array size can exceed gas limits.
Before Optimization
// ❌ BAD: Unbounded loop
contract BadAirdrop {
address[] public recipients;
function airdrop(uint256 amount) public {
// What if recipients has 10,000 addresses?
for (uint256 i = 0; i < recipients.length; i++) {
transfer(recipients[i], amount);
}
}
}
After Optimization
// ✅ GOOD: Paginated processing
contract GoodAirdrop {
address[] public recipients;
uint256 public lastProcessed = 0;
function airdrop(uint256 amount, uint256 batchSize) public {
uint256 end = lastProcessed + batchSize;
if (end > recipients.length) {
end = recipients.length;
}
for (uint256 i = lastProcessed; i < end; i++) {
transfer(recipients[i], amount);
}
lastProcessed = end;
}
}
Better Alternative: Use a pull pattern (users claim instead of push):
// ✅ BEST: Pull pattern
contract BestAirdrop {
mapping(address => uint256) public allocations;
mapping(address => bool) public claimed;
function claim() public {
require(!claimed[msg.sender], "Already claimed");
require(allocations[msg.sender] > 0, "No allocation");
claimed[msg.sender] = true;
transfer(msg.sender, allocations[msg.sender]);
}
}
Anti-Pattern #5: String Comparisons
The Problem: Strings are expensive to compare.
Before Optimization
// ❌ BAD: String comparison
contract BadRoles {
mapping(address => string) public roles;
function isAdmin(address user) public view returns (bool) {
// Very expensive!
return keccak256(bytes(roles[user])) == keccak256(bytes("admin"));
}
}
After Optimization
// ✅ GOOD: Use bytes32 or enums
contract GoodRoles {
enum Role { None, User, Admin }
mapping(address => Role) public roles;
function isAdmin(address user) public view returns (bool) {
return roles[user] == Role.Admin; // Cheap integer comparison
}
}
Real-World Example: Optimized ERC20 Token
Here’s a production-grade ERC20 implementation with optimizations:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract OptimizedToken {
string public constant name = "Optimized Token";
string public constant symbol = "OPT";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply;
balanceOf[msg.sender] = _initialSupply;
emit Transfer(address(0), msg.sender, _initialSupply);
}
function transfer(address to, uint256 amount) external returns (bool) {
return _transfer(msg.sender, to, amount);
}
function transferFrom(address from, address to, uint256 amount)
external
returns (bool)
{
uint256 allowed = allowance[from][msg.sender];
require(allowed >= amount, "Insufficient allowance");
// Update allowance before transfer (checks-effects-interactions)
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - amount;
}
return _transfer(from, to, amount);
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function _transfer(address from, address to, uint256 amount)
private
returns (bool)
{
require(to != address(0), "Transfer to zero address");
// Cache balances in memory
uint256 fromBalance = balanceOf[from];
require(fromBalance >= amount, "Insufficient balance");
// Unchecked math for gas savings (safe after checks)
unchecked {
balanceOf[from] = fromBalance - amount;
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
}
Key Optimizations:
- Constants: name, symbol, decimals (saves 2,100 gas per read)
- Cached storage: Read balanceOf once, use memory
- Unchecked math: Safe after require checks
- Infinite approval: Skip update if max uint256
Security: The Non-Negotiable
Gas optimization should never compromise security. Here are critical security patterns:
Reentrancy Protection
// ✅ Use checks-effects-interactions pattern
function withdraw(uint256 amount) public {
// Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// Effects
balances[msg.sender] -= amount;
// Interactions (external calls last)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// ✅ Or use ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Safe is ReentrancyGuard {
function withdraw(uint256 amount) public nonReentrant {
// Safe from reentrancy
}
}
Integer Overflow Protection
// ✅ Solidity 0.8+ has built-in overflow checks
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // Reverts on overflow
}
// ✅ Use unchecked only when safe
function addSafe(uint256 a, uint256 b) public pure returns (uint256) {
unchecked {
return a + b; // Only if you've proven no overflow possible
}
}
Access Control
// ✅ Use OpenZeppelin's Ownable or AccessControl
import "@openzeppelin/contracts/access/Ownable.sol";
contract Protected is Ownable {
function criticalFunction() public onlyOwner {
// Only owner can call
}
}
Testing & Auditing
Before deploying, always:
1. Gas Profiling
// hardhat.config.js
module.exports = {
gasReporter: {
enabled: true,
currency: "USD",
gasPrice: 50,
},
};
2. Unit Tests
describe("OptimizedToken", function () {
it("Should transfer tokens efficiently", async function () {
const [owner, addr1] = await ethers.getSigners();
const Token = await ethers.getContractFactory("OptimizedToken");
const token = await Token.deploy(1000);
const tx = await token.transfer(addr1.address, 100);
const receipt = await tx.wait();
console.log("Gas used:", receipt.gasUsed.toString());
expect(await token.balanceOf(addr1.address)).to.equal(100);
});
});
3. External Audits
For production contracts handling real value:
- Certik: Comprehensive audits
- OpenZeppelin: Trusted auditors
- Trail of Bits: Security experts
The Results
After optimizing our smart contracts:
- 30% reduction in average gas fees
- Zero security vulnerabilities in audits
- Higher user adoption due to lower costs
- Better code maintainability
Key Takeaways
- Cache storage reads: SLOAD costs 2,100 gas; MLOAD costs 3 gas
- Minimize storage writes: SSTORE costs 5,000-20,000 gas
- Use uint256: EVM’s native word size
- Avoid unbounded loops: Use pagination or pull patterns
- Security first: Never sacrifice security for gas savings
- Test everything: Gas profiling + comprehensive tests
- Get audited: External audits for production contracts
Tools & Resources
- Remix: Browser-based IDE with gas profiling
- Hardhat: Development environment with gas reporter
- Foundry: Fast testing framework
- Slither: Static analysis tool
- MythX: Automated security analysis
Smart contract optimization is about balance: minimize gas without compromising security or readability. Always measure, test, and audit.
Building on Ethereum or other EVM chains? I’d love to discuss your smart contract optimization challenges.