# Common Solidity Mistakes That Waste Gas and Compromise Security

> The most frequent Solidity mistakes that inflate gas costs and create security vulnerabilities — with practical fixes for each pattern developers get wrong.

- **Published:** 2026-06-11
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** solidity, gas-optimization, security, common-mistakes, smart-contracts
- **Canonical URL:** https://clixo.sh/blog/common-solidity-mistakes-that-waste-gas

Solidity gives you enough rope to hang yourself. The language compiles almost anything, and subtle mistakes do not generate compiler errors — they generate expensive transactions or exploitable contracts. Most of the patterns below are common in code written by developers who know Solidity but have not yet internalized the EVM's cost model or its security constraints.

## Common Solidity Mistakes That Waste Gas

### Reading State Variables Multiple Times in a Loop

Storage reads cost 100–2100 gas depending on whether the slot is warm or cold. Reading the same state variable inside a loop multiplies that cost:

```
// Expensive — reads storage on every iteration
for (uint256 i = 0; i < totalItems; i++) {
    process(items[i]);
}
```

Cache state variables in local memory variables before the loop:

```
uint256 length = totalItems;  // one storage read
for (uint256 i = 0; i < length; i++) {
    process(items[i]);
}
```

This is the single highest-impact gas optimization for most contracts with loops.

### Using `uint8` and `uint16` to "Save Space"

Developers assume smaller integer types save gas. They save storage space when packed correctly, but they cost *more* gas in computation because the EVM operates on 256-bit words and must mask smaller values:

```
// More expensive to compute than uint256
uint8 counter;
counter++;
```

Use `uint256` for standalone variables that are not packed with other values in storage. Only use smaller types when packing multiple values into a single storage slot.

### Storing Everything On-Chain

State that does not need to be read by other contracts should not be in contract storage. Each new storage write (`SSTORE` to a cold slot) costs 20,000 gas. Emitting an event costs roughly 375 gas plus per-byte costs.

If your contract only needs to record that something happened for off-chain consumption, emit an event instead of writing to storage. Storing metadata, descriptions, or images entirely on-chain is almost always the wrong approach.

### Using `string` for Fixed Values

Dynamic-length `string` and `bytes` types involve memory allocation. If your value fits in 32 bytes, use `bytes32` instead:

```
// Wasteful
string public version = "1.0";

// Better
bytes32 public constant VERSION = "1.0";
```

Constants do not occupy storage at all — they are inlined into bytecode.

### Redundant Visibility on Overridden Functions

A function marked both `public` and `virtual` that never needs external calls could be `internal`. Every `public` function adds an entry point to the contract's dispatch table. Declaring functions as `external` (when they are never called internally) is cheaper because `external` parameters use `calldata` rather than requiring a copy to `memory`.

## Common Solidity Mistakes That Compromise Security

### Authorization with `tx.origin`

`tx.origin` returns the address of the original transaction signer, not the immediate caller. A malicious contract can call your function on behalf of a trusted user, and your `tx.origin` check will pass:

```
// Vulnerable
require(tx.origin == owner, "Not authorized");

// Correct
require(msg.sender == owner, "Not authorized");
```

Never use `tx.origin` for access control.

### Unchecked Return Values from External Calls

`.call()` returns a `bool` indicating success. Ignoring it means your contract silently continues after a failed transfer:

```
// Silent failure
target.call{value: amount}("");

// Correct
(bool success, ) = target.call{value: amount}("");
require(success, "Call failed");
```

Low-level calls do not propagate reverts. You must check the return value manually.

### Arithmetic Without Bounds Checking

Solidity 0.8.x has built-in overflow and underflow protection — but only for unchecked arithmetic outside an `unchecked {}` block. If you use `unchecked {}` for gas savings, you must manually verify that the arithmetic cannot overflow or underflow in that context:

```
unchecked {
    // Only safe if you have proven a - b cannot underflow
    uint256 result = a - b;
}
```

Do not use `unchecked` globally across a function. Scope it tightly to operations you have verified.

### Delegatecall to User-Supplied Addresses

`delegatecall` executes code from another contract in the context of your own contract's storage. Calling a user-supplied address with `delegatecall` lets an attacker run arbitrary code with full access to your storage:

```
// Never do this
address impl = userSuppliedAddress;
impl.delegatecall(data);
```

Only `delegatecall` to addresses you control and have audited.

### Missing Input Validation on Array Parameters

Functions that accept arrays and iterate over them without length bounds can run out of gas. Functions that accept two paired arrays without checking that their lengths match will silently misalign:

```
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
    // Always validate this
    require(recipients.length == amounts.length, "Length mismatch");
    require(recipients.length <= 200, "Too many recipients");
    // ...
}
```

### Floating Pragma

`pragma solidity ^0.8.0;` allows compilation with any 0.8.x version. Different patch versions have different compiler behavior, optimizer behavior, and known bugs. Pin to an exact version:

```
pragma solidity 0.8.28;
```

This is a one-line change that eliminates an entire class of environment-specific issues.

```mermaid
flowchart TD
  OP["EVM Operation Cost"] --> ST["SSTORE cold slot: 20000 gas"]
  OP --> SL["SLOAD cold slot: 2100 gas"]
  OP --> EV["Event emit: ~375 gas"]
  OP --> MR["Memory op: 3 gas"]
  ST --> R1["Use events for off-chain data"]
  SL --> R2["Cache in local variable before loops"]
  MR --> R3["Prefer memory over storage reads"]
```

## A Practical Review Habit

Before submitting a pull request with any contract changes, walk through this mental checklist:

- Does any loop read a state variable more than once?
- Does any function call into an external address without a reentrancy guard?
- Does any function accept an address parameter without validating it?
- Is `tx.origin` used anywhere?
- Are all `.call()` return values checked?

These take less than five minutes to check and catch the majority of high-severity issues.

If you want a contract development team that catches these patterns before code review rather than after, [Clixo](https://clixo.sh/services/smart-contract-development) builds Solidity systems where these checks are part of the engineering process from the start.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
