Why Every Byte Counts on the Blockchain Gas isn’t just some abstract developer thing — it’s actual money. Every transaction you run, every function call you make on Ethereum (or any EVM-based chain) comes with a price. That price? Gas. Now sure, if you’re new to Solidity, it might not seem like a huge deal at first. But trust me — once your contracts start getting used thousands of times, every little inefficiency turns into real costs. Even something as small as reading a variable too many times can hit your users in the wallet. This post walks through five simple-but-powerful ways to save gas. And no, they’re not magic tricks — just smart coding habits that pay off over time. The Foundation: Understanding Gas Mechanics Alright, quick refresher before we jump in. If you want to optimize gas, you need to know where it’s going. Here’s the breakdown: Storage (Persistent Data): This is your contract’s hard disk. New writes cost around 20,000 gas. Just updating something? That’s about 5,000. Still not cheap. Memory (Temporary Data): Like RAM. It’s cleared after function execution and much cheaper than storage — but if you use a lot of it, it adds up. Calldata (Read-Only Input): Hands down the cheapest option. It stores input data from external calls and doesn’t let you modify it. 5 Gas Optimization Patterns Pattern 1: Storage Packing (Yeah, Like Tetris) The EVM likes to store things in 32-byte slots. That’s 256 bits per slot. So, if you’re declaring a bunch of small variables one after another — like bool, uint8, etc.—and spacing them out in the contract, Solidity might just throw each one into its own full slot. Wasteful, right? The Fix: Pack smaller variables together so they sit side-by-side in one slot. You’ll instantly save gas, especially on deployment. // Inefficient - Uses 3 separate storage slots.contract InefficientPacking { uint256 public largeData; // Slot 1 uint8 public status; // Slot 2 (wastes 31 bytes) bool public isActive; // Slot 3 (wastes 31 bytes)}// Optimized - All smaller variables fit into a single slot.contract OptimizedPacking { uint128 public tokenBalance; uint128 public feeAmount; uint8 public status; bool public isActive; bool public isApproved;} Pattern 2: Cache Storage Reads in Memory This one’s pretty straightforward, and yet a lot of folks miss it. Here’s the thing: reading from storage isn’t free. If you’re doing it multiple times inside a single function, guess what? You’re paying for each read — even if it’s the exact same value. The Fix: Pull the value into a local variable once. Do your work there. Then write it back to storage (if needed) at the end. // Inefficient - Reads 'userPoints[msg.sender]' from storage three times.function updatePoints(uint256 reward) public { userPoints[msg.sender] = userPoints[msg.sender].add(reward); // Read 1, Write 1 if (userPoints[msg.sender] > MAX_POINTS) { // Read 2 userPoints[msg.sender] = MAX_POINTS; // Write 2 }}// Optimized - Reads from storage once, writes to storage once.function updatePointsOptimized(uint256 reward) public { uint256 currentPoints = userPoints[msg.sender]; currentPoints = currentPoints.add(reward); if (currentPoints > MAX_POINTS) { currentPoints = MAX_POINTS; } userPoints[msg.sender] = currentPoints; } Pattern 3: Use calldata (Don’t Just Default to memory) Ever noticed how Solidity makes you choose between memory and calldata for function parameters like arrays or strings? A lot of devs just default to memory because it feels familiar. But here’s the catch: when you use memory, Solidity makes a full copy of the input. And yep—copying data costs gas. The Fix: If you’re not modifying the input, use calldata. It avoids the copy step and keeps things lean. // Inefficient - Uses 'memory'. The array is copied from calldata into memory.function verifyUsers(address[] publicKeys) public view returns (bool) { // ... logic that only reads 'publicKeys'}// Optimized - Uses 'calldata'. The array is read directly from the transaction input.function verifyUsersOptimized(address[] calldata publicKeys) public view returns (bool) { for (uint i = 0; i < publicKeys.length; i++) { require(publicKeys[i] != address(0), "Invalid address"); } return true;} Pattern 4: Short-Circuit Conditionals (Put the Cheap Stuff First) This one’s sneaky. Let’s say you’ve got a require statement with two conditions. One is quick and easy—like checking a local variable. The other is a heavy function call (maybe signature verification or an external lookup). If you put the expensive one first, it gets executed even if the cheap one fails. The Fix: Put the least expensive (or most likely to fail) condition first. // Inefficient - Expensive check runs even if the cheaper one would fail.function accessRestrictedResource() public { require(verifySignature(msg.sender) && isWhitelisted(msg.sender), "Access denied"); }// Optimized - The cheap check runs first, skipping the expensive one if it fails.function accessRestrictedResourceOptimized() public { require(isWhitelisted(msg.sender) && verifySignature(msg.sender), "Access denied");} Pattern 5: Use constant and immutable Wisely Got a value that never changes? Like a max supply or owner address? If you declare it as a regular state variable, Solidity puts it in storage. And every time you access it, you’re burning gas — over and over again. The Fix: Use constant for compile-time values. Use immutable for values that are set once at deployment. Both are much cheaper at runtime. // Inefficient - These values are read from expensive storage slots every time they're used.contract InefficientConstants { uint256 public MAX_SUPPLY = 100000000; address public OWNER_ADDRESS; constructor(address _owner) { OWNER_ADDRESS = _owner; }}// Optimized - Values are baked into the contract bytecode, saving runtime gas.contract OptimizedConstants { uint256 public constant MAX_SUPPLY = 100000000; address public immutable i_owner; constructor(address _owner) { i_owner = _owner; } function getOwner() public view returns (address) { return i_owner; }} Security Considerations (Don’t Trade Safety for Speed) Here’s the thing: gas optimization is great — until it isn’t. If you’re chasing micro-savings at the cost of security or clarity, you’re heading into dangerous territory. Readable > Fast (Sometimes): If an auditor can’t read your logic easily, that’s a red flag. Saving 10 gas isn’t worth the confusion. Unchecked Math: Sure, unchecked saves gas by disabling overflow checks—but only use it when you’ve proven it’s safe. Overpacking Variables: Yeah, you can cram multiple bits into a single slot. But if you’re doing wild bitwise operations just to save a few gas units, ask yourself: is this code still safe? Maintainable? Auditable? Bonus: Use the Compiler Optimizer Solidity’s compiler has a built-in optimizer that can shrink your bytecode and improve gas performance. How It Works: It simplifies expressions, inlines functions, and eliminates redundancy. The runs Setting: runs=1: Optimizes for cheaper deployment runs=20000: Optimizes for cheaper execution (runtime gas) If your contract’s going to be used heavily — like in a token or protocol — go with 20000. It costs more to deploy, but saves users money on every interaction. Conclusion Writing smart contracts that just “work” isn’t enough anymore. You’ve got to think about gas — because your users definitely will. With just a few smart choices, you can make your contracts cheaper to run and friendlier to the people using them. These five patterns are a great starting point. Use gas reporters like Hardhat or Foundry to measure improvements. Keep your code clear, safe, and optimized. Have questions or ideas? Contact us at: [email protected] explore more: www.ancilar.com 5 Gas Optimization Patterns Every Solidity Developer Must Master was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this storyWhy Every Byte Counts on the Blockchain Gas isn’t just some abstract developer thing — it’s actual money. Every transaction you run, every function call you make on Ethereum (or any EVM-based chain) comes with a price. That price? Gas. Now sure, if you’re new to Solidity, it might not seem like a huge deal at first. But trust me — once your contracts start getting used thousands of times, every little inefficiency turns into real costs. Even something as small as reading a variable too many times can hit your users in the wallet. This post walks through five simple-but-powerful ways to save gas. And no, they’re not magic tricks — just smart coding habits that pay off over time. The Foundation: Understanding Gas Mechanics Alright, quick refresher before we jump in. If you want to optimize gas, you need to know where it’s going. Here’s the breakdown: Storage (Persistent Data): This is your contract’s hard disk. New writes cost around 20,000 gas. Just updating something? That’s about 5,000. Still not cheap. Memory (Temporary Data): Like RAM. It’s cleared after function execution and much cheaper than storage — but if you use a lot of it, it adds up. Calldata (Read-Only Input): Hands down the cheapest option. It stores input data from external calls and doesn’t let you modify it. 5 Gas Optimization Patterns Pattern 1: Storage Packing (Yeah, Like Tetris) The EVM likes to store things in 32-byte slots. That’s 256 bits per slot. So, if you’re declaring a bunch of small variables one after another — like bool, uint8, etc.—and spacing them out in the contract, Solidity might just throw each one into its own full slot. Wasteful, right? The Fix: Pack smaller variables together so they sit side-by-side in one slot. You’ll instantly save gas, especially on deployment. // Inefficient - Uses 3 separate storage slots.contract InefficientPacking { uint256 public largeData; // Slot 1 uint8 public status; // Slot 2 (wastes 31 bytes) bool public isActive; // Slot 3 (wastes 31 bytes)}// Optimized - All smaller variables fit into a single slot.contract OptimizedPacking { uint128 public tokenBalance; uint128 public feeAmount; uint8 public status; bool public isActive; bool public isApproved;} Pattern 2: Cache Storage Reads in Memory This one’s pretty straightforward, and yet a lot of folks miss it. Here’s the thing: reading from storage isn’t free. If you’re doing it multiple times inside a single function, guess what? You’re paying for each read — even if it’s the exact same value. The Fix: Pull the value into a local variable once. Do your work there. Then write it back to storage (if needed) at the end. // Inefficient - Reads 'userPoints[msg.sender]' from storage three times.function updatePoints(uint256 reward) public { userPoints[msg.sender] = userPoints[msg.sender].add(reward); // Read 1, Write 1 if (userPoints[msg.sender] > MAX_POINTS) { // Read 2 userPoints[msg.sender] = MAX_POINTS; // Write 2 }}// Optimized - Reads from storage once, writes to storage once.function updatePointsOptimized(uint256 reward) public { uint256 currentPoints = userPoints[msg.sender]; currentPoints = currentPoints.add(reward); if (currentPoints > MAX_POINTS) { currentPoints = MAX_POINTS; } userPoints[msg.sender] = currentPoints; } Pattern 3: Use calldata (Don’t Just Default to memory) Ever noticed how Solidity makes you choose between memory and calldata for function parameters like arrays or strings? A lot of devs just default to memory because it feels familiar. But here’s the catch: when you use memory, Solidity makes a full copy of the input. And yep—copying data costs gas. The Fix: If you’re not modifying the input, use calldata. It avoids the copy step and keeps things lean. // Inefficient - Uses 'memory'. The array is copied from calldata into memory.function verifyUsers(address[] publicKeys) public view returns (bool) { // ... logic that only reads 'publicKeys'}// Optimized - Uses 'calldata'. The array is read directly from the transaction input.function verifyUsersOptimized(address[] calldata publicKeys) public view returns (bool) { for (uint i = 0; i < publicKeys.length; i++) { require(publicKeys[i] != address(0), "Invalid address"); } return true;} Pattern 4: Short-Circuit Conditionals (Put the Cheap Stuff First) This one’s sneaky. Let’s say you’ve got a require statement with two conditions. One is quick and easy—like checking a local variable. The other is a heavy function call (maybe signature verification or an external lookup). If you put the expensive one first, it gets executed even if the cheap one fails. The Fix: Put the least expensive (or most likely to fail) condition first. // Inefficient - Expensive check runs even if the cheaper one would fail.function accessRestrictedResource() public { require(verifySignature(msg.sender) && isWhitelisted(msg.sender), "Access denied"); }// Optimized - The cheap check runs first, skipping the expensive one if it fails.function accessRestrictedResourceOptimized() public { require(isWhitelisted(msg.sender) && verifySignature(msg.sender), "Access denied");} Pattern 5: Use constant and immutable Wisely Got a value that never changes? Like a max supply or owner address? If you declare it as a regular state variable, Solidity puts it in storage. And every time you access it, you’re burning gas — over and over again. The Fix: Use constant for compile-time values. Use immutable for values that are set once at deployment. Both are much cheaper at runtime. // Inefficient - These values are read from expensive storage slots every time they're used.contract InefficientConstants { uint256 public MAX_SUPPLY = 100000000; address public OWNER_ADDRESS; constructor(address _owner) { OWNER_ADDRESS = _owner; }}// Optimized - Values are baked into the contract bytecode, saving runtime gas.contract OptimizedConstants { uint256 public constant MAX_SUPPLY = 100000000; address public immutable i_owner; constructor(address _owner) { i_owner = _owner; } function getOwner() public view returns (address) { return i_owner; }} Security Considerations (Don’t Trade Safety for Speed) Here’s the thing: gas optimization is great — until it isn’t. If you’re chasing micro-savings at the cost of security or clarity, you’re heading into dangerous territory. Readable > Fast (Sometimes): If an auditor can’t read your logic easily, that’s a red flag. Saving 10 gas isn’t worth the confusion. Unchecked Math: Sure, unchecked saves gas by disabling overflow checks—but only use it when you’ve proven it’s safe. Overpacking Variables: Yeah, you can cram multiple bits into a single slot. But if you’re doing wild bitwise operations just to save a few gas units, ask yourself: is this code still safe? Maintainable? Auditable? Bonus: Use the Compiler Optimizer Solidity’s compiler has a built-in optimizer that can shrink your bytecode and improve gas performance. How It Works: It simplifies expressions, inlines functions, and eliminates redundancy. The runs Setting: runs=1: Optimizes for cheaper deployment runs=20000: Optimizes for cheaper execution (runtime gas) If your contract’s going to be used heavily — like in a token or protocol — go with 20000. It costs more to deploy, but saves users money on every interaction. Conclusion Writing smart contracts that just “work” isn’t enough anymore. You’ve got to think about gas — because your users definitely will. With just a few smart choices, you can make your contracts cheaper to run and friendlier to the people using them. These five patterns are a great starting point. Use gas reporters like Hardhat or Foundry to measure improvements. Keep your code clear, safe, and optimized. Have questions or ideas? Contact us at: [email protected] explore more: www.ancilar.com 5 Gas Optimization Patterns Every Solidity Developer Must Master was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story

5 Gas Optimization Patterns Every Solidity Developer Must Master

2025/10/14 19:05

Why Every Byte Counts on the Blockchain

Gas isn’t just some abstract developer thing — it’s actual money. Every transaction you run, every function call you make on Ethereum (or any EVM-based chain) comes with a price. That price? Gas.

Now sure, if you’re new to Solidity, it might not seem like a huge deal at first. But trust me — once your contracts start getting used thousands of times, every little inefficiency turns into real costs. Even something as small as reading a variable too many times can hit your users in the wallet.

This post walks through five simple-but-powerful ways to save gas. And no, they’re not magic tricks — just smart coding habits that pay off over time.

The Foundation: Understanding Gas Mechanics

Alright, quick refresher before we jump in. If you want to optimize gas, you need to know where it’s going.

Here’s the breakdown:

  • Storage (Persistent Data): This is your contract’s hard disk. New writes cost around 20,000 gas. Just updating something? That’s about 5,000. Still not cheap.
  • Memory (Temporary Data): Like RAM. It’s cleared after function execution and much cheaper than storage — but if you use a lot of it, it adds up.
  • Calldata (Read-Only Input): Hands down the cheapest option. It stores input data from external calls and doesn’t let you modify it.

5 Gas Optimization Patterns

Pattern 1: Storage Packing (Yeah, Like Tetris)

The EVM likes to store things in 32-byte slots. That’s 256 bits per slot. So, if you’re declaring a bunch of small variables one after another — like bool, uint8, etc.—and spacing them out in the contract, Solidity might just throw each one into its own full slot. Wasteful, right?

The Fix: Pack smaller variables together so they sit side-by-side in one slot. You’ll instantly save gas, especially on deployment.

// Inefficient - Uses 3 separate storage slots.
contract InefficientPacking {
uint256 public largeData; // Slot 1
uint8 public status; // Slot 2 (wastes 31 bytes)
bool public isActive; // Slot 3 (wastes 31 bytes)
}
// Optimized - All smaller variables fit into a single slot.
contract OptimizedPacking {
uint128 public tokenBalance;
uint128 public feeAmount;
uint8 public status;
bool public isActive;
bool public isApproved;
}

Pattern 2: Cache Storage Reads in Memory

This one’s pretty straightforward, and yet a lot of folks miss it.

Here’s the thing: reading from storage isn’t free. If you’re doing it multiple times inside a single function, guess what? You’re paying for each read — even if it’s the exact same value.

The Fix: Pull the value into a local variable once. Do your work there. Then write it back to storage (if needed) at the end.

// Inefficient - Reads 'userPoints[msg.sender]' from storage three times.
function updatePoints(uint256 reward) public {
userPoints[msg.sender] = userPoints[msg.sender].add(reward); // Read 1, Write 1
if (userPoints[msg.sender] > MAX_POINTS) { // Read 2
userPoints[msg.sender] = MAX_POINTS; // Write 2
}
}
// Optimized - Reads from storage once, writes to storage once.
function updatePointsOptimized(uint256 reward) public {
uint256 currentPoints = userPoints[msg.sender];
currentPoints = currentPoints.add(reward);
if (currentPoints > MAX_POINTS) {
currentPoints = MAX_POINTS;
}
userPoints[msg.sender] = currentPoints;
}

Pattern 3: Use calldata (Don’t Just Default to memory)

Ever noticed how Solidity makes you choose between memory and calldata for function parameters like arrays or strings? A lot of devs just default to memory because it feels familiar.

But here’s the catch: when you use memory, Solidity makes a full copy of the input. And yep—copying data costs gas.

The Fix: If you’re not modifying the input, use calldata. It avoids the copy step and keeps things lean.

// Inefficient - Uses 'memory'. The array is copied from calldata into memory.
function verifyUsers(address[] publicKeys) public view returns (bool) {
// ... logic that only reads 'publicKeys'
}
// Optimized - Uses 'calldata'. The array is read directly from the transaction input.
function verifyUsersOptimized(address[] calldata publicKeys) public view returns (bool) {
for (uint i = 0; i < publicKeys.length; i++) {
require(publicKeys[i] != address(0), "Invalid address");
}
return true;
}

Pattern 4: Short-Circuit Conditionals (Put the Cheap Stuff First)

This one’s sneaky. Let’s say you’ve got a require statement with two conditions. One is quick and easy—like checking a local variable. The other is a heavy function call (maybe signature verification or an external lookup).

If you put the expensive one first, it gets executed even if the cheap one fails.

The Fix: Put the least expensive (or most likely to fail) condition first.

// Inefficient - Expensive check runs even if the cheaper one would fail.
function accessRestrictedResource() public {
require(verifySignature(msg.sender) && isWhitelisted(msg.sender), "Access denied");
}
// Optimized - The cheap check runs first, skipping the expensive one if it fails.
function accessRestrictedResourceOptimized() public {
require(isWhitelisted(msg.sender) && verifySignature(msg.sender), "Access denied");
}

Pattern 5: Use constant and immutable Wisely

Got a value that never changes? Like a max supply or owner address?

If you declare it as a regular state variable, Solidity puts it in storage. And every time you access it, you’re burning gas — over and over again.

The Fix: Use constant for compile-time values. Use immutable for values that are set once at deployment. Both are much cheaper at runtime.

// Inefficient - These values are read from expensive storage slots every time they're used.
contract InefficientConstants {
uint256 public MAX_SUPPLY = 100000000;
address public OWNER_ADDRESS;
constructor(address _owner) {
OWNER_ADDRESS = _owner;
}
}
// Optimized - Values are baked into the contract bytecode, saving runtime gas.
contract OptimizedConstants {
uint256 public constant MAX_SUPPLY = 100000000;
address public immutable i_owner;
constructor(address _owner) {
i_owner = _owner;
}
function getOwner() public view returns (address) {
return i_owner;
}
}

Security Considerations (Don’t Trade Safety for Speed)

Here’s the thing: gas optimization is great — until it isn’t. If you’re chasing micro-savings at the cost of security or clarity, you’re heading into dangerous territory.

  • Readable > Fast (Sometimes): If an auditor can’t read your logic easily, that’s a red flag. Saving 10 gas isn’t worth the confusion.
  • Unchecked Math: Sure, unchecked saves gas by disabling overflow checks—but only use it when you’ve proven it’s safe.
  • Overpacking Variables: Yeah, you can cram multiple bits into a single slot. But if you’re doing wild bitwise operations just to save a few gas units, ask yourself: is this code still safe? Maintainable? Auditable?

Bonus: Use the Compiler Optimizer

Solidity’s compiler has a built-in optimizer that can shrink your bytecode and improve gas performance.

  • How It Works: It simplifies expressions, inlines functions, and eliminates redundancy.
  • The runs Setting:
  • runs=1: Optimizes for cheaper deployment
  • runs=20000: Optimizes for cheaper execution (runtime gas)

If your contract’s going to be used heavily — like in a token or protocol — go with 20000. It costs more to deploy, but saves users money on every interaction.

Conclusion

Writing smart contracts that just “work” isn’t enough anymore. You’ve got to think about gas — because your users definitely will. With just a few smart choices, you can make your contracts cheaper to run and friendlier to the people using them.

These five patterns are a great starting point. Use gas reporters like Hardhat or Foundry to measure improvements. Keep your code clear, safe, and optimized.

Have questions or ideas?
Contact us at: [email protected]
explore more:
www.ancilar.com


5 Gas Optimization Patterns Every Solidity Developer Must Master was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.

Disclaimer: The articles reposted on this site are sourced from public platforms and are provided for informational purposes only. They do not necessarily reflect the views of MEXC. All rights remain with the original authors. If you believe any content infringes on third-party rights, please contact [email protected] for removal. MEXC makes no guarantees regarding the accuracy, completeness, or timeliness of the content and is not responsible for any actions taken based on the information provided. The content does not constitute financial, legal, or other professional advice, nor should it be considered a recommendation or endorsement by MEXC.