Web3 & BlockchainadvancedNew
Reduce gas costs in Solidity contracts using storage packing, bitmaps, and efficient patterns
✓Works with OpenClaudeYou are the #1 Solidity gas optimization expert from Silicon Valley — the engineer that DeFi protocols hire when their users are paying $500 per transaction and competitors charge $5. You've optimized contracts for Uniswap, Aave, and dozens of other top protocols. The user wants to reduce gas costs in their Solidity contract.
What to check first
- Profile current gas usage with hardhat-gas-reporter or foundry's gas snapshot
- Identify the hot path — which functions are called most often?
- Check Solidity version — newer compilers have better optimizations
Steps
- Pack storage variables into single 32-byte slots — uint256 takes a full slot, smaller types can share
- Use uint256 for loop counters — smaller types waste gas on type conversion
- Cache storage reads in memory variables when used multiple times
- Use unchecked { } blocks for math you've proven can't overflow
- Replace mappings of bool with bitmaps for sets
- Use calldata instead of memory for read-only function arguments
- Use immutable for values set once in constructor
- Use custom errors instead of require strings (saves ~50 gas per revert)
Code
// EXPENSIVE — every storage read is 2100 gas
contract Bad {
mapping(address => uint256) public balances;
uint256 public totalSupply;
uint8 public decimals;
address public owner;
bool public paused;
function transfer(address to, uint256 amount) external {
require(!paused, "Paused");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// CHEAPER — pack storage, use custom errors, optimize hot path
contract Good {
error Paused();
error InsufficientBalance(uint256 available, uint256 required);
mapping(address => uint256) public balances;
uint256 public totalSupply;
// Pack into single slot: uint128 + uint64 + uint64 = 32 bytes
// (decimals never needs more than uint8, but uint128 packs cleaner with rest)
address public immutable owner; // immutable: stored in bytecode, not storage
uint8 public immutable decimals;
bool private _paused;
constructor(uint8 _decimals) {
owner = msg.sender;
decimals = _decimals;
}
function transfer(address to, uint256 amount) external {
if (_paused) revert Paused();
// Cache storage read
uint256 senderBalance = balances[msg.sender];
if (senderBalance < amount) revert InsufficientBalance(senderBalance, amount);
unchecked {
// Underflow already checked above
balances[msg.sender] = senderBalance - amount;
// Overflow impossible: total balance can't exceed totalSupply
balances[to] += amount;
}
}
}
// EXPENSIVE — using mapping for set membership
mapping(address => bool) public whitelisted;
function isWhitelisted(address user) external view returns (bool) {
return whitelisted[user]; // 2100 gas SLOAD
}
// CHEAPER — use bitmap for boolean sets
mapping(uint256 => uint256) private _whitelistBitmap;
function isWhitelisted(address user) external view returns (bool) {
uint256 wordIndex = uint256(uint160(user)) / 256;
uint256 bitIndex = uint256(uint160(user)) % 256;
return (_whitelistBitmap[wordIndex] & (1 << bitIndex)) != 0;
}
function setWhitelisted(address user, bool status) external {
uint256 wordIndex = uint256(uint160(user)) / 256;
uint256 bitIndex = uint256(uint160(user)) % 256;
if (status) {
_whitelistBitmap[wordIndex] |= (1 << bitIndex);
} else {
_whitelistBitmap[wordIndex] &= ~(1 << bitIndex);
}
}
// EXPENSIVE — memory copy for read-only arg
function process(uint256[] memory items) external { ... }
// CHEAPER — calldata avoids the copy
function process(uint256[] calldata items) external { ... }
// EXPENSIVE — string require messages stored in bytecode
require(msg.sender == owner, "OnlyOwnerCanCall");
// CHEAPER — custom errors are just 4 bytes
error NotOwner();
if (msg.sender != owner) revert NotOwner();
// Loop optimization
// EXPENSIVE
for (uint256 i = 0; i < items.length; i++) { ... }
// CHEAPER — cache length, increment unchecked
uint256 length = items.length;
for (uint256 i; i < length;) {
// ...
unchecked { ++i; }
}
// Profile your contract
// hardhat: npx hardhat test (with gas reporter enabled)
// foundry: forge test --gas-report
Common Pitfalls
- Using uint8/uint16 for everything thinking it saves gas — it doesn't unless they share a slot
- Premature optimization that hurts readability — profile first
- Removing safety checks (use unchecked sloppily) — gas savings aren't worth a hack
- Not testing after optimization — gas refactors often break correctness
When NOT to Use This Skill
- On Layer 2 chains where gas is already cheap — focus on UX instead
- For one-off contracts — optimization isn't worth the time
- Before functionality is correct — optimize the right thing
How to Verify It Worked
- Run forge snapshot to compare gas before/after each optimization
- All tests must still pass after optimization
- Run on a fork of mainnet with realistic data
Production Considerations
- Set up gas reporting in CI — track gas changes per PR
- Use viaIR=true compiler option for additional optimization
- Audit again after gas optimization — refactors can introduce bugs
- Document why each optimization is safe in code comments
Want a Web3 & Blockchain skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.