The first time a user pays forty dollars to claim a five dollar reward, your gas model stops being an abstraction. Gas is the price of every opcode your contract runs, paid in real money by whoever sends the transaction. Write carelessly and you tax every interaction for the life of the contract. Optimize blindly and you ship something no auditor can read, which is its own, more expensive kind of bug.
Storage is the expensive thing
Nothing you do on the EVM costs like writing to storage. A single cold storage write dwarfs arithmetic, memory, and calldata by orders of magnitude, and you pay it on every update forever. So design the state layout first, not last. Pack related values so several fields share one 32-byte slot: a uint96 amount and a uint160 address fit together; two timestamps and a status flag can share a slot instead of burning three. Read and write those slots together so the EVM can reuse a warm slot rather than paying the cold price twice. And be honest about what actually needs to live on chain. Data you only ever read off chain does not belong in storage at all.
This is the most common overspend we see. Teams write to storage things they only need for history: audit trails, past prices, a log of who did what. Emitting an event costs a fraction of a storage write, and events are fully queryable from any indexer or subgraph. If the contract itself never reads a value during execution, it should be an event, not a state variable. Keep in storage only what the contract must consult to make a decision on chain. Everything else is history, and history belongs in the log.
Batch the work, bound every loop
Every transaction pays a base cost before it does anything useful, so amortize it. A function that accepts an array and processes many items in one call is far cheaper per item than the same work spread across many transactions. The counterpart rule is stricter: never write a loop whose length a user or the passage of time can grow without limit. An unbounded loop is two bugs at once. It is a gas cost that rises until the transaction cannot fit in a block, and it is a denial-of-service vector where one large account can freeze a function for everyone. Prefer pull-based patterns over pushing to a crowd, and paginate anything that could grow.
Optimize the hot path, measure the rest
Not every saved unit of gas is worth its price in complexity. Hand-rolled assembly, packed bit tricks, and clever caching trade readability for a discount, and readability is not free. It is paid at audit time, when a reviewer must convince themselves the trick is safe, and every hour of that review costs more than the gas saved on a function that runs twice a day. So optimize the hot path that runs on every transaction, leave the cold administrative functions plain and obvious, and let numbers settle the argument. Put gas in the test suite: snapshot the cost of your core operations, assert against those snapshots, and fail the build when a change makes a common path more expensive. Gas becomes a value you review in a diff, like any other regression, rather than a surprise a user finds in production.
Cheap and unreadable is not an optimization. It is a debt you repay every time someone has to prove the contract is safe.— Protocore · Blockchain engineering
Gas-aware design is not a bag of tricks applied at the end. It is a small number of decisions made early: lay out storage deliberately, keep history in events, batch what you can and bound what you must, and measure the rest so you optimize the paths that matter and no others. Do that and you get contracts that are cheap to use and still readable enough to trust. Those two properties are not in tension. They come from the same habit of knowing exactly what your code costs.
Have a system to build?
Tell us the problem. We'll come back with an architecture and a plan.
Start a project