Skip to content

The Contract Lifecycle

The deployment page got a contract onto an address: the last stage of the pipeline this part has been walking. This page zooms all the way out and looks at the whole arc — birth, life, and death — as a single story, and ties every earlier page into it.

There is a reason this comes last. Each stage of a contract’s life exercises exactly one idea you have already met: deployment writes the immutable code; a live call runs that code, reads and writes storage, and emits events; an “upgrade” is not really a change to the code at all but a redirection built on delegatecall; and destruction was, historically, a way to erase the account entirely. Through all of it runs the book’s one thread: every one of these events advances the shared world state and its 32-byte root — and every one of them costs gas, paid by someone, forever.

A contract is not a process that starts and stops. It is data in the world state that gets written once, poked many times, and — occasionally — deleted. Here is the whole life in one diagram:

AUTHOR DEPLOY LIVE (the long middle) DEATH
┌───────┐ ┌──────────────┐ ┌───────────────────────────────┐ ┌──────────────┐
│ .sol │──▶│ CREATE tx │──▶│ call → run code → SSTORE/LOG │──▶│ selfdestruct │
│ solc │ │ code stored │ │ call → run code → SSTORE/LOG │ │ (deprecated) │
│ bytes │ │ at 0xC0de… │ │ call → read-only (view) │ │ or just │
└───────┘ └──────────────┘ │ ... (days, years) │ │ disuse │
└───────────────────────────────┘ └──────────────┘
│ │ │ │
off-chain root R₀ → R₁ each write: Rₙ → Rₙ₊₁ root Rₖ → Rₖ₊₁
(no chain (code now on (state advances, (account gone,
state yet) chain) logs indexed) or neutered)

Read the bottom row: the only stages that touch the chain are the ones that move the state root. Authoring is free and off-chain. Deployment moves the root once (code appears). Every state-changing call moves it again. Read-only calls move nothing. Destruction moves it one last time. The lifecycle is a walk through a sequence of state roots — and that walk is exactly what a stranger replays to verify the contract’s entire history.

Birth: deployment writes code to an address

Section titled “Birth: deployment writes code to an address”

A contract’s life begins with a transaction that has no to address — the signal to the EVM that this is a create, not a call. The transaction carries init code: a small program that runs once, sets up any constructor state, and returns the runtime code — the bytes that will actually live at the address. (The deployment page unpacks init-vs-runtime and how the address is derived by CREATE and CREATE2.)

In the companion crate ethmini, deployment is one arm of the state-transition function. Notice that it does exactly two things: derive an address, then write the code into a fresh account.

TxKind::Deploy { code, value } => {
let addr = contract_address(tx.from, tx.nonce); // deterministic address
if *value > 0 {
self.transfer_value(tx.from, addr, *value); // optionally endow it
}
self.account_mut(addr).code = code.clone(); // ← code written ONCE
Ok(Receipt { status: Success, created: Some(addr), .. })
}

That single line self.account_mut(addr).code = code.clone() is the whole of “birth.” After it runs, the account’s code field is non-empty, so is_contract() is now true, and the world’s state root has moved from R₀ to R₁ because a new, code-bearing account is now part of the state. The code field is written here and — per the immutability rule — never assigned again. There is no UPDATE code opcode anywhere in the EVM.

Deployment is the one moment you pay to store code permanently, and real Ethereum charges for it by the byte. That is why contract size is a budget item, and why the roadmap for this part kept flagging the code-size limit.

Once code is at the address, the contract enters its long middle: a sequence of calls, possibly spread across years, from anyone. Each call is a transaction (or an internal call from another contract) that names the address, hands in call data, and triggers the code. The contract sits inert between calls — it is data, not a daemon — and each call spins up a fresh EVM execution against the contract’s current storage.

Calls come in two flavours, and the difference is the whole cost story:

  • State-changing calls run code that executes SSTORE and/or emits logs. These cost gas, are packaged in a block, and move the state root. Writing a fresh storage slot is the single most expensive ordinary thing a call can do.
  • Read-only calls (Solidity view/pure, queried via eth_call) run the code but touch no storage and are never mined. They move nothing, cost the caller no on-chain gas, and are how a dapp reads a contract. They are not part of the lifecycle’s state-root walk at all.

ethmini shows the state-changing path precisely. A call to a coded account clones the storage, runs the VM, and — only on a clean halt — commits the new storage back into the account:

let code = target.code.clone();
let storage = target.storage.clone(); // run against a COPY
match vm::run(&code, &storage, *gas_limit) {
Ok(exec) => {
self.account_mut(*to).storage = exec.storage; // ← commit, root moves
Ok(Receipt { status: Success, gas_used: exec.gas_used, .. })
}
Err(reason) => Err(Reverted { gas_used: *gas_limit, reason }), // revert: nothing commits
}

Two lifecycle facts fall straight out of this code. First, a call either commits in full or reverts to nothing — the “run on a copy, swap on success” structure is the revert mechanism in miniature. Second, a reverted call still consumes the sender’s nonce and gas (see the state machine), so even a failed call is a real, recorded lifecycle event — it just leaves storage where it was.

The canonical live-phase example from earlier in this part is the counter. Calling it three times:

== call counter x3 ==
status Success, count now 1 storage[0]: 0 → 1 root R₁ → R₂
status Success, count now 2 storage[0]: 1 → 2 root R₂ → R₃
status Success, count now 3 storage[0]: 2 → 3 root R₃ → R₄

Each call advances both the stored number and the state root. The number 3 does not live in any process — it lives in the contract’s storage, which is the chain’s state. That is the live phase in one image: state that outlives every execution because it is the shared world, fingerprinted anew on every write.

Alongside storage writes, the live phase is where a contract emits events into the logs — the cheaper, off-chain-readable record covered in Events and Logs. A well-built contract logs every meaningful state change so that indexers can reconstruct its history without re-executing it. The lifecycle, seen from outside the chain, is the stream of a contract’s events.

Upgrades: immutable code, mutable behavior

Section titled “Upgrades: immutable code, mutable behavior”

Here is the tension the whole part has been circling. The immutability rule says a contract’s code can never change — that is what lets a stranger trust it. But real software has bugs and needs new features. How can behavior evolve if code cannot?

The answer is that behavior does not evolve by editing code. It evolves by redirection: you deploy a new contract with the new logic, and point users’ interactions at it instead. The trick is to make that redirection invisible, so the address everyone depends on never changes even as the logic behind it does. That is the proxy pattern, and it rests entirely on one opcode: delegatecall.

A normal CALL from contract A to contract B runs B’s code against B’s own storage, in B’s context. delegatecall is the strange sibling: A delegatecalls B, and B’s code runs — but against A’s storage, A’s balance, and A’s msg.sender. It is “borrow that code and execute it here, on my own state.”

CALL A ──▶ B : runs B's code on B's storage (normal)
DELEGATECALL A ──▶ B : runs B's code on A's storage (the trick)
B is a code library; A is the state that persists

That single semantic is the engine of upgradeability:

user ──▶ PROXY (stable address, holds all the storage)
│ delegatecall
LOGIC v1 (just code, no meaningful storage of its own)
upgrade = deploy LOGIC v2, flip the proxy's stored pointer to it:
user ──▶ PROXY ──delegatecall──▶ LOGIC v2

The proxy is a thin contract at a fixed address. It holds all the real storage — balances, mappings, everything users care about — but almost no logic of its own. On every call, it delegatecalls into a separate logic contract, whose address it keeps in one of its own storage slots. Because it is delegatecall, the logic contract’s code executes against the proxy’s storage. To upgrade, you deploy a new logic contract and change that one pointer slot in the proxy. The proxy’s address — the one every user and every other contract references — never moves. The logic swapped underneath.

Nothing here violates immutability. The proxy’s code is still frozen forever; the logic contracts’ code is each frozen forever. What changed is which frozen code the proxy delegates to — and that pointer is ordinary mutable storage, updated by an ordinary SSTORE. Upgradeability is a design pattern layered on top of immutability, never an exception to it.

The same delegatecall semantics that make proxies possible also make them dangerous, because “run foreign code against my storage” means a mismatch between the proxy’s storage layout and the logic’s storage layout writes to the wrong slots. This is the storage-slot layout rulebook turned into a live hazard.

For most of Ethereum’s history, a contract could end its own life with one opcode: SELFDESTRUCT (originally SUICIDE). Historically it did three things in a single step:

  1. Removed the contract’s code and storage from the world state — the account effectively vanished, its state root contribution erased.
  2. Sent the contract’s entire remaining ETH balance to a designated address.
  3. Refunded gas to the caller, as an incentive to shrink the state that every node must store.

That refund was the point: selfdestruct was the one operation that made the world state smaller, so the protocol paid you to use it. It was used for one-shot contracts, factory-spawned children, and — as the Parity incident showed — sometimes catastrophically.

But selfdestruct broke a property the whole ecosystem had quietly come to rely on: that code at an address is permanent. Proxies, CREATE2 address reuse, and countless integrations assumed a deployed contract stays deployed. An opcode that can erase code out from under dependents is a footgun aimed at the entire composability story.

So Ethereum neutered it. As of the Cancun upgrade (EIP-6780, March 2024), SELFDESTRUCT no longer deletes code and storage in the general case. It now only sends the remaining ETH balance to the target — and it fully self-destructs (erasing code) only if it is called in the same transaction that created the contract. In every other case, the code stays put. The gas refund for it was already removed earlier (in the London upgrade, 2021). In practice, selfdestruct is deprecated: developers are told not to rely on it, and it should be treated as “send my balance out,” not “delete me.”

selfdestruct, BEFORE Cancun selfdestruct, AFTER Cancun (EIP-6780)
┌───────────────────────────┐ ┌────────────────────────────────────┐
│ • delete code + storage │ │ • send remaining ETH out │
│ • send remaining ETH out │ ──▶ │ • delete code ONLY if created in │
│ • refund gas (removed 2021)│ │ the SAME tx; otherwise code stays │
└───────────────────────────┘ └────────────────────────────────────┘

The deeper lesson is architectural: Ethereum decided that permanence of deployed code is more valuable than the ability to reclaim state. A world computer that untrusting strangers build on top of needs its foundations to stay put. ethmini never modeled selfdestruct at all, and that is quietly faithful to where the protocol has landed — code goes in, and it stays.

Most contracts never call selfdestruct. They simply fall out of use: the last interesting call happens, and then no one ever calls them again. The code and storage remain in the world state forever — a real cost, since every full node keeps them — but the contract is “dead” in every sense that matters to users. This is the ordinary end of a contract’s life: not a dramatic destruction, just silence, with its final state frozen into the chain and its last log the last thing it ever said.

Zoom out and price the arc, because that is the second half of the book’s question — what does it cost to run one?

STAGE you pay for... who pays / when
─────────────────────────────────────────────────────────────────────────
authoring nothing on-chain free, off-chain
deployment permanent code storage (per byte) deployer, once
+ running the init code
each write computation + storage growth caller, every call
(SSTORE dominates)
each log computation + bytes logged caller, cheaper than storage
read-only call nothing on-chain (eth_call) free to query
selfdestruct (historically) refunded state; neutered since Cancun
now just moves ETH out
disuse everyone stores it forever the whole network, silently

The through-line: you pay once for code at birth, you pay per call for computation plus any state you grow, and the network pays forever to remember whatever you left behind. Read-only calls are free because they move no state. selfdestruct was the one lever that made state smaller, and the protocol chose to disable it rather than keep a footgun that could erase code others depend on. Every design decision in this part — storage vs. log, small code vs. large, upgradeable vs. frozen — is a choice about where on this table you spend, and how much of it the network carries in perpetuity.

The contract lifecycle isn’t a single technology, but the upgradeable-proxy pattern it culminates in is — so the lens goes to that.

  • Why does it exist? Because contract code is immutable by design, yet real software needs to fix bugs and ship features. The proxy pattern reconciles the two: a stable address whose behavior can be swapped by pointing it at new logic.
  • What problem does it solve? It lets a system evolve without forcing every user, integration, and other contract to migrate to a new address — the address they depend on never changes, only the frozen code behind it.
  • What are the trade-offs? You reintroduce mutability — and therefore an admin who can change the code, which is exactly the trust you deployed a contract to avoid. delegatecall also makes storage-layout mismatches catastrophic, and it concentrates risk in the shared logic contract.
  • When should I avoid it? When true immutability is the feature — a contract users trust because no one can ever change it (many DeFi primitives, credibly-neutral infrastructure). A proxy’s upgrade key is a target and a liability; if you don’t need upgrades, a frozen contract is safer and cheaper.
  • What breaks if I remove it? Large, long-lived systems become un-patchable: any bug is permanent (see The DAO), and shipping a v2 means abandoning v1’s address and migrating all state and users by hand.
  1. Walk the four stages of a contract’s life and say, for each, whether it moves the world state root and who pays for it.
  2. The code field is written at deployment and “never assigned again.” Given that, how does an upgradeable contract change its behavior without changing its code? Name the opcode that makes it possible.
  3. Explain in one sentence how delegatecall differs from a normal CALL, and why that specific difference is what makes proxy upgradeability work.
  4. What did selfdestruct historically do (three effects), and how did EIP-6780 (Cancun, 2024) change it? Why did Ethereum decide to neuter it?
  5. State the cost lens across the lifecycle in your own words: what do you pay for once, what per call, and what does the network pay for forever?
Show answers
  1. Authoring — off-chain, moves nothing, free. Deployment — moves the root once (a new code-bearing account appears) and is paid once by the deployer, including a per-byte charge for storing the runtime code permanently. Live calls — each state-changing call (an SSTORE and/or a log) moves the root and is paid by the caller in gas; read-only (view) calls move nothing and cost nothing on-chain. Deathselfdestruct (or an ordinary final state change) moves the root one last time; a contract that dies by disuse moves nothing further but is stored by every node forever.
  2. It never changes its code. It uses a proxy: a stable-address contract that holds all the storage and, on every call, delegatecalls into a separate logic contract whose address it keeps in one storage slot. To “upgrade,” you deploy new logic and flip that pointer slot — the proxy’s frozen code and address are unchanged; only which frozen code it delegates to changes. The enabling opcode is delegatecall.
  3. A normal CALL runs the target’s code against the target’s storage; delegatecall runs the target’s code against the caller’s storage (and balance and msg.sender). That is what lets a proxy borrow the logic contract’s code but apply all its effects to the proxy’s own persistent storage — so the logic is swappable while the state stays put behind the stable proxy address.
  4. Historically selfdestruct (1) deleted the contract’s code and storage, (2) sent its remaining ETH to a target address, and (3) refunded gas. The gas refund was removed in London (2021); EIP-6780 (Cancun, March 2024) made it no longer delete code/storage except when called in the same transaction that created the contract — otherwise it only sends the ETH out. Ethereum neutered it because the ecosystem relies on deployed code being permanent (proxies, integrations, CREATE2), so an opcode that can erase code out from under dependents is a hazard worth more than the state it reclaimed.
  5. You pay once at deployment for storing the runtime code permanently (per byte) plus running the init code. You pay per call for computation plus any state growth, where a fresh SSTORE dominates and events are cheaper; read-only queries are free because they move no state. And the network pays forever to store whatever code and storage you leave behind — even after a contract falls into disuse — which is why selfdestruct (the one operation that shrank state) mattered, and why removing state is now largely off the table.