Solidity Programming
Solidity is a high-level, statically-typed programming language designed specifically for writing smart contracts on the Ethereum blockchain and other compatible blockchains. Smart contracts are self-executing programs that automate, enforce, or execute the terms of an agreement.
Key Features of Solidity High-Level Language:
Similar to languages like JavaScript, Python, and C++, making it easier to learn for developers familiar with these languages. Statically Typed:
Variable types must be defined at the time of declaration, enhancing error detection during compilation. Compiled Language:
Solidity code is compiled into Ethereum Virtual Machine (EVM) bytecode, which is executed on the blockchain. Object-Oriented:
Supports concepts like inheritance, libraries, and user-defined data types. Blockchain-Specific:
Includes features to interact with Ethereum accounts, handle cryptocurrencies (e.g., Ether), and access blockchain data (like transaction details or block timestamps).
Tight Variable Packing
One of the most important gas optimization techniques in Solidity is tight variable packing. The Ethereum Virtual Machine (EVM) stores data in 32-byte slots. When you declare state variables, Solidity tries to pack multiple smaller types into a single slot to minimize storage costs.
How It Works
Types smaller than 32 bytes are packed into the same storage slot if they are declared consecutively. For example:
// Inefficient - uses 4 storage slots
contract GasWaste {
uint256 a; // slot 0 (full 32 bytes)
uint128 b; // slot 1 (16 bytes, but takes a full slot)
uint128 c; // slot 2 (16 bytes, but takes a full slot)
bool d; // slot 3 (1 byte, but takes a full slot)
}
// Efficient - uses only 2 storage slots
contract GasEfficient {
uint128 b; // ┐
uint128 c; // ┘ slot 0 (both uint128s packed)
uint256 a; // slot 1 (full 32 bytes)
bool d; // slot 2 (but could be packed further)
}
Even better, pack bool with other small types:
// Most efficient - uses 2 storage slots
contract GasEfficient {
uint128 b; // ┐
uint64 c; // │
uint32 d; // │
bool e; // ┘ slot 0 (all packed into 32 bytes)
uint256 a; // slot 1 (full 32 bytes)
}
Why It Matters
Each storage slot operation has a significant gas cost:
| Operation | Gas Cost |
|---|---|
SLOAD (read) |
~2100 gas |
SSTORE (write from nonzero to zero) |
~20,000 gas (refund on cleanup) |
SSTORE (write zero to nonzero) |
~20,000 gas |
SSTORE (write nonzero to nonzero) |
~5,000 gas |
If your contract uses 4 slots instead of 2, every function that reads or writes those variables costs roughly double the gas. Over thousands of transactions, this adds up to a massive difference.
Rules of Thumb
- Order types by size descending, then group small types together:
uint256, thenuint128+uint128, thenuint64+uint32+uint16+bool, etc. - Use the smallest integer type that safely holds your value (e.g.,
uint32for timestamps,uint8for small counters). - Use
bytes32overbytesorstringfor fixed-size data. - For structs, define fields in packed order since structs also occupy storage slots.
Full Contract Example: User Profile System
Here is a complete contract that manages user profiles. Notice how the gas-wasteful version uses separate variables, while the gas-efficient version packs everything into as few slots as possible.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// GAS-WASTEFUL — uses 6 storage slots per user
contract UserProfilesGasHeavy {
mapping(address => uint256) public balance;
mapping(address => uint256) public lastLogin;
mapping(address => uint256) public flags; // only uses lower 8 bits
mapping(address => uint256) public loginCount; // only needs uint32
mapping(address => uint256) public tier; // only needs uint8
mapping(address => bool) public isActive;
function register(address user, uint256 amount) external {
balance[user] = amount;
lastLogin[user] = block.timestamp;
flags[user] = 0;
loginCount[user] = 0;
tier[user] = 1;
isActive[user] = true;
}
// 6 SSTORE calls → 6 separate storage slots per user
}
// Gas-Efficient — uses 2 storage slots per user
contract UserProfilesGasEfficient {
struct UserData {
uint256 balance; // slot 0 (full 32 bytes)
uint64 lastLogin; // ┐
uint32 loginCount; // │ slot 1 (packed)
uint8 tier; // │
uint8 flags; // │
bool isActive; // ┘
}
mapping(address => UserData) public users;
function register(address user, uint256 amount) external {
users[user] = UserData({
balance: amount,
lastLogin: uint64(block.timestamp),
loginCount: 0,
tier: 1,
flags: 0,
isActive: true
});
}
// 2 SSTORE calls → 2 storage slots per user
// Saves ~100,000 gas per registration
}
Sample Use Case: NFT Marketplace Listing
Consider an NFT marketplace where each listing stores the seller, token ID, price, listing time, and whether the item is a buy-now or auction.
// Packed Listing Struct — fits in 2 storage slots
contract NFTPackedMarketplace {
struct Listing {
address seller; // 20 bytes ┐
uint64 listTime; // 8 bytes │ slot 0 (32 bytes)
uint32 tokenId; // 4 bytes ┘
uint128 price; // 16 bytes ┐ slot 1 (mapping hash)
bool isBuyNow; // 1 byte ┘ (packed in mapping slot remainder)
}
mapping(uint256 => Listing) public listings;
uint256 public listingCounter;
function createListing(
uint32 tokenId,
uint128 price,
bool isBuyNow
) external returns (uint256 id) {
id = ++listingCounter;
listings[id] = Listing({
seller: msg.sender,
listTime: uint64(block.timestamp),
tokenId: tokenId,
price: price,
isBuyNow: isBuyNow
});
}
// To buy, the buyer sends ETH and the listing is marked sold.
// Reading the packed struct costs 2 SLOADs instead of 5+.
function buy(uint256 id) external payable {
Listing storage l = listings[id];
require(l.isBuyNow, "auction only");
require(msg.sender != l.seller, "no self-buy");
require(msg.value >= l.price, "insufficient");
delete listings[id]; // gas refund on cleanup
payable(l.seller).transfer(l.price);
}
}
Without packing, each Listing would occupy 5 separate storage slots:
address (20 bytes → 1 slot), uint256 price (1 slot), uint256 listTime (1 slot), uint256 tokenId (1 slot), bool (1 slot) = 5 slots.
With tight packing, the same data fits into 2 slots, reducing the gas cost of createListing from ~100,000 to ~40,000 and cutting buy reads by 60%.
Tight packing is one of the easiest optimizations to implement and one of the most impactful for reducing deployment and execution costs on Ethereum.
Comments
Leave a comment