Blockchain 📂 Ethereum · 2 of 2 38 min read

Smart Contract Lifecycle & Security — Deployment, Gas & Vulnerabilities

A practical security-focused guide to Ethereum smart contracts. Covers the full lifecycle from writing and compiling to deployment, plus gas optimization techniques. Then dissects the five most dangerous vulnerability classes — reentrancy, integer overflow, front-running, timestamp dependency, and broken access control — each with a vulnerable-vs-safe Solidity example. Ends with upgradeable proxy patterns and their trade-offs.

Section 01

Smart Contracts — Code That Is Law

The Vending Machine With No Owner
A vending machine is the perfect real-world smart contract. You insert coins, press a button, and the machine automatically gives you a snack — no shopkeeper, no trust, no negotiation. The rules are baked into the machine: pay the right amount, get the right item, every time. Nobody can reach in and change the price after you've paid.

A smart contract is a vending machine made of code, living on Ethereum. It holds funds, enforces rules, and executes automatically when conditions are met — with no company behind it and no way to cheat. But there is a catch the vending machine doesn't have: once deployed, a smart contract is permanent and public. A single bug can drain millions, and there's no "undo." This tutorial covers the full lifecycle of a contract and the security landmines every developer must avoid.

Smart contracts power everything on Ethereum — DeFi, NFTs, DAOs, and stablecoins. But their immutability makes them uniquely unforgiving. History is littered with hacks that drained hundreds of millions because of a single overlooked line. We'll walk through deployment, gas optimization, and the most dangerous vulnerability classes, each with a practical example.

💰
Why Smart Contract Security Is Life-Or-Death

Billions of dollars have been lost to smart contract exploits: The DAO hack (2016) drained $60M via reentrancy; the Parity wallet freeze (2017) locked $280M forever via an access-control bug. Because contracts are immutable and hold real money, a security mistake is not a bug report — it's an irreversible theft. "Move fast and break things" is a catastrophic mindset here.


Section 02

The Smart Contract Lifecycle

Every smart contract travels through a well-defined lifecycle, from source code to a permanent, running program on-chain. Understanding each stage is the foundation for writing, deploying, and maintaining contracts safely.

Animated Diagram — From Source Code To Live Contract
1. WRITE Solidity 2. COMPILE to bytecode 3. TEST + audit 4. DEPLOY to mainnet 5. LIVE immutable ⚠ Once LIVE, the code can never be changed — get it right before deploying
Write in Solidity, compile to EVM bytecode, test and audit thoroughly, then deploy. After deployment the code is frozen forever — testing is your only safety net.
📝 The Five Stages In Detail
Write
Author the contract in Solidity (or Vyper). Define state variables, functions, and access rules.
Compile
The compiler turns human-readable code into EVM bytecode plus an ABI (the interface other apps use to call it).
Test & Audit
Run unit tests, fuzz testing, and a professional security audit. This is where bugs must be caught — there's no fixing them later.
Deploy
Send a deployment transaction. The contract gets a permanent address and its bytecode is stored on-chain forever.
Interact
Users and other contracts call its functions. The contract runs autonomously, holding funds and enforcing its logic.

Section 03

Deployment — Birth Of A Contract

Deployment is a special transaction with no recipient (to field empty) whose data field contains the contract's bytecode. The EVM runs the constructor, stores the code, and assigns the contract a unique address derived from the deployer's address and nonce.

Animated Diagram — The Deployment Transaction
DEPLOYER (EOA) DEPLOY TX to: (empty) data: bytecode EVM runs constructor initializes state stores code CONTRACT LIVE 0x5FbDB2...9fF8 permanent address
A deployment transaction carries bytecode instead of a recipient. The EVM runs the constructor once, stores the code, and mints a permanent contract address.
// A minimal Solidity contract
pragma solidity ^0.8.20;

contract SimpleStorage {
    uint256 private value;          // state stored on-chain

    // constructor runs ONCE at deployment
    constructor(uint256 _initial) {
        value = _initial;
    }

    function set(uint256 _v) public {
        value = _v;
    }

    function get() public view returns (uint256) {
        return value;
    }
}
🔮
Test On A Testnet First — Always

Before deploying to Ethereum mainnet (where mistakes cost real money), developers deploy to a testnet like Sepolia or Holesky, using free test ETH from a faucet. It behaves identically to mainnet but with worthless coins — the perfect rehearsal stage. Skipping this step is how expensive disasters happen.


Section 04

Gas Optimization — Writing Efficient Contracts

Every operation in a contract costs gas, and users pay that gas in real ETH. A poorly optimized contract can cost users 10× more per transaction than a lean one. Gas optimization is both a cost-saving art and, sometimes, a security concern (bloated code is harder to audit).

Diagram — Relative Gas Cost Of Common Operations
gas cost SSTORE (new) 20,000 SSTORE (update) 5,000 CALL 2,600 SLOAD (read) 2,100 ADD / arithmetic 3 Storage writes dwarf everything — minimize them to save gas
Writing to storage (SSTORE) is thousands of times more expensive than arithmetic. The golden rule of gas optimization: touch storage as little as possible.
📦
Minimize Storage
biggest win
Storage is the costliest resource. Cache values in memory, batch writes, and pack multiple small variables into a single 256-bit slot.
🔁
Avoid Loops Over Arrays
unbounded = danger
Looping over a growing array can exceed the block gas limit and brick the function. Prefer mappings and pull-based patterns.
🔧
Use Correct Types
pack tightly
Group uint128, bool, and address so they share storage slots. Use calldata over memory for read-only params.
// BAD: two storage writes, reads storage in a loop
function sumBad() public {
    for (uint i = 0; i < items.length; i++) {
        total = total + items[i];   // SSTORE every loop! costly
    }
}

// GOOD: cache in memory, one storage write at the end
function sumGood() public {
    uint256 sum = 0;                 // memory, cheap
    uint256 len = items.length;      // cache length
    for (uint i = 0; i < len; i++) {
        sum += items[i];
    }
    total = sum;                     // single SSTORE
}

Section 05

Security Best Practices — The Big Picture

Smart contract security follows a handful of guiding principles. Master these before diving into specific vulnerabilities — most real-world hacks are just violations of one of these rules.

🛡️ The Core Security Principles
Checks-Effects-Interactions
First check conditions, then update state, and only then interact with external contracts. This single pattern prevents most reentrancy attacks.
Least Privilege
Give every function the minimum access it needs. Guard sensitive functions with strict access control.
Fail Safe
Validate all inputs, use safe math, and revert on anything unexpected rather than continuing in a broken state.
Audit & Test
Use battle-tested libraries (OpenZeppelin), get professional audits, and run fuzz tests. Never roll your own crypto or token logic.

Section 06

Vulnerability 1 — Reentrancy

Reentrancy is the most infamous smart contract bug. It happens when a contract sends ETH to an external address before updating its own state. The receiving contract can "call back" into the original function repeatedly, draining funds before the balance is ever reduced. This is exactly how The DAO lost $60M in 2016.

Animated Diagram — The Reentrancy Attack Loop
VICTIM CONTRACT sends ETH first, updates balance later ATTACKER fallback re-calls withdraw() again 1. sends ETH → 2. ↻ calls withdraw() AGAIN before balance drops The loop repeats until the victim's entire balance is drained 😈
The attacker's fallback function re-enters withdraw() before the victim reduces its recorded balance — looping until the contract is emptied.
// VULNERABLE: sends ETH before updating balance
function withdraw() public {
    uint bal = balances[msg.sender];
    (bool ok, ) = msg.sender.call{value: bal}("");  // ⚠ external call FIRST
    balances[msg.sender] = 0;                              // too late! attacker re-entered
}

// SAFE: Checks-Effects-Interactions + reentrancy guard
function withdraw() public nonReentrant {
    uint bal = balances[msg.sender];
    balances[msg.sender] = 0;                              // update state FIRST
    (bool ok, ) = msg.sender.call{value: bal}("");  // interact LAST
    require(ok, "transfer failed");
}
🛡️
The Fix — Update State Before The Call

Follow Checks-Effects-Interactions: zero out the balance before sending ETH, so a re-entrant call sees a balance of zero and gets nothing. For extra safety, add a reentrancy guard (OpenZeppelin's nonReentrant modifier), which locks the function while it runs. Two simple defenses that would have prevented the $60M DAO hack.


Section 07

Vulnerability 2 — Integer Overflow & Underflow

Before Solidity 0.8, numbers could silently wrap around. Subtract 1 from a uint that is 0 and instead of an error you get the maximum possible value — a number with 78 digits. Attackers exploited this to mint themselves near-infinite tokens or bypass balance checks.

Animated Diagram — Underflow Wraps 0 To The Maximum
balance 0 balance - 1 underflow! 2²⁵⁶ − 1 115792089... a 78-digit number! Subtracting below zero wraps to a colossal number — an attacker's dream
In old Solidity, 0 minus 1 didn't error — it wrapped around to the maximum uint value. An attacker with "0" tokens could suddenly appear to hold astronomically many.
// VULNERABLE (Solidity < 0.8): silent wrap-around
function transfer(address to, uint amt) public {
    balances[msg.sender] -= amt;   // ⚠ underflows if amt > balance
    balances[to] += amt;             // could overflow too
}

// SAFE (Solidity >= 0.8): reverts automatically on overflow
// ...or use OpenZeppelin SafeMath on older versions
function transfer(address to, uint amt) public {
    require(balances[msg.sender] >= amt, "insufficient");
    balances[msg.sender] -= amt;   // safe: 0.8 reverts on underflow
    balances[to] += amt;
}
Solidity 0.8+ Fixed This By Default

Since Solidity 0.8.0, all arithmetic automatically reverts on overflow or underflow — the wrap-around bug class is gone by default. On older code, developers used OpenZeppelin's SafeMath library. Always use a modern compiler version, and never wrap arithmetic in unchecked { } unless you have proven it cannot overflow.


Section 08

Vulnerability 3 — Front-Running

Ethereum transactions sit in a public mempool before being mined, visible to everyone. A front-running attacker watches for a profitable pending transaction, then submits their own with a higher gas tip so it gets mined first — jumping ahead to steal the opportunity. This is a huge part of what's now called MEV (Maximal Extractable Value).

Animated Diagram — Jumping The Queue
PUBLIC MEMPOOL Victim's trade gas tip: 20 Gwei Attacker copies it gas tip: 90 Gwei ↑↑ NEXT BLOCK 1. Attacker's tx 2. Victim's tx higher tip = sorted first By paying a higher tip, the attacker's copycat trade executes first
Because pending transactions are public, an attacker sees a profitable trade and pays a higher gas tip to have their copy mined ahead of the victim's.
🔒
Commit-Reveal
hide intent
Submit a hash of your action first, then reveal the details later. Attackers can't front-run what they can't see.
🔢
Slippage Limits
bound the damage
Set a maximum acceptable price (minAmountOut) so a front-run trade can't force you into a terrible rate.
📡
Private Mempools
skip the queue
Send transactions through private relays (like Flashbots) so they never appear in the public mempool for attackers to spot.

Section 09

Vulnerability 4 — Timestamp Dependency

Contracts sometimes use block.timestamp for randomness, deadlines, or lottery winners. But block producers can nudge the timestamp by a few seconds. Any logic that depends on a precise time — especially "random" number generation — can be manipulated by a validator to favor themselves.

⏱️
Never Use block.timestamp For Randomness

A validator choosing which block to produce can shift block.timestamp slightly. If your lottery picks a winner using block.timestamp % players, a malicious validator can pick a timestamp that makes themselves win. The blockchain has no safe source of on-chain randomness by itself — use an oracle like Chainlink VRF for verifiable randomness instead.

// VULNERABLE: validator can bias the "random" winner
function pickWinner() public {
    uint winner = block.timestamp % players.length;  // ⚠ manipulable
    payable(players[winner]).transfer(prize);
}

// SAFE: use Chainlink VRF for tamper-proof randomness
// request randomness from the oracle, then in the callback:
function fulfillRandomWords(uint reqId, uint[] memory rand) internal {
    uint winner = rand[0] % players.length;  // verifiable, unbiased
    payable(players[winner]).transfer(prize);
}
💡
When block.timestamp Is Fine

Timestamps are acceptable for coarse checks where a few seconds don't matter — like "is this deadline more than a day away?" The rule of thumb: never depend on block.timestamp for anything where a 15-second manipulation could change the outcome or reward.


Section 10

Vulnerability 5 — Broken Access Control

Access control bugs are among the costliest of all. If a sensitive function — one that withdraws funds, mints tokens, or changes ownership — isn't properly restricted, anyone can call it. The Parity wallet disaster of 2017 froze $280M forever because a critical function lacked proper protection.

Animated Diagram — The onlyOwner Gate
OWNER STRANGER 🔑 onlyOwner withdraw() sensitive fn The modifier checks msg.sender == owner; strangers are reverted at the gate.
An access-control modifier like onlyOwner acts as a gate: the legitimate owner passes through to the sensitive function; everyone else is rejected before any harm is done.
// VULNERABLE: anyone can call this and steal everything
function withdrawAll() public {          // ⚠ no restriction!
    payable(msg.sender).transfer(address(this).balance);
}

// SAFE: guard with an access-control modifier
modifier onlyOwner() {
    require(msg.sender == owner, "not authorized");
    _;
}

function withdrawAll() public onlyOwner {   // only owner passes
    payable(owner).transfer(address(this).balance);
}
🔐
Use OpenZeppelin's Battle-Tested Modifiers

Don't hand-roll access control. OpenZeppelin's Ownable and AccessControl contracts provide audited, standard patterns for owner-only and role-based permissions. They handle ownership transfer, renouncement, and role management correctly — edge cases that home-grown code routinely gets wrong.


Section 11

Upgradeable Contracts — Fixing The Unfixable

Contracts are immutable — so how do teams fix bugs or add features? The answer is the proxy pattern. A permanent proxy contract holds all the funds and state but delegates its logic to a separate implementation contract. To upgrade, you simply point the proxy at a new implementation. The address users interact with never changes.

Animated Diagram — The Proxy Upgrade Pattern
USER always same PROXY holds funds+state fixed address LOGIC v1 old (has a bug) retired LOGIC v2 new & fixed active delegatecall → Upgrade = repoint the proxy from Logic v1 to v2. Users' address and funds never move.
The proxy is the permanent front door holding state and funds; the logic lives in a swappable implementation. Upgrading just points the proxy at new, fixed code.
✅ Upgradeability Benefits
Fix bugs after launch
Add new features over time
Keep the same user-facing address
State & funds are preserved
❌ Upgradeability Risks
Admin key can change the rules
Reduces trustlessness
Storage-layout mistakes corrupt data
Bigger attack surface
⚖️
Upgradeability Is A Double-Edged Sword

The power to upgrade is also the power to change the rules on users. If a single admin key controls the upgrade, that key becomes a honeypot — and a point of centralization that contradicts "trustless" ideals. Serious projects protect upgrades behind a multisig or a timelock + DAO governance, so no single person can swap the logic unilaterally. Use OpenZeppelin's audited proxy libraries and never upgrade without re-auditing.


Section 12

Golden Rules — Smart Contract Security

🛡️ Non-Negotiable Truths
1
Deployed code is permanent. There is no "undo." Test exhaustively on a testnet and get a professional audit before deploying to mainnet — never after.
2
Follow Checks-Effects-Interactions. Update your state before making external calls, and add a reentrancy guard. This alone prevents the most famous class of hacks.
3
Use Solidity 0.8+ for safe math. Overflow and underflow revert automatically. Only use unchecked when you have proven arithmetic cannot wrap.
4
Assume the mempool is public. Anyone can see and front-run your pending transactions. Use commit-reveal, slippage limits, or private relays for sensitive actions.
5
Never trust block.timestamp for randomness. Validators can nudge it. Use an oracle like Chainlink VRF for any outcome where seconds of manipulation would matter.
6
Guard every sensitive function. Missing access control lets anyone drain or brick your contract. Use OpenZeppelin's Ownable / AccessControl, not hand-rolled checks.
7
Minimize storage to save gas. Storage writes are thousands of times costlier than computation. Cache in memory, pack variables, and avoid unbounded loops.
8
Upgradeability trades trust for flexibility. Protect proxy upgrades with a multisig or timelock + governance, and re-audit before every upgrade.
You have completed Ethereum. View all sections →