Key Principle
Smart contracts are "Immutable computer programs that run deterministically in the context of an EVM… on the decentralized Ethereum world computer" (Chapter 7). Once deployed, code cannot change — the only "edit" is deploying a new instance — so "in smart contracts, bugs literally cost money" (Chapter 7). A contract account "owns itself," executing only as its code prescribes; there is no protocol-level creator backdoor, so any privilege (owner, admin, kill switch) must be explicitly coded or it does not exist. Transactions are atomic: "Transactions are atomic, regardless of how many contracts they call" — success commits all state, any failure rolls back "as if the transaction never ran," yet the failed attempt is still recorded and gas is still spent (failures are safe, not free).
Why This Matters
The EVM stores only bytecode — solc compiles .sol source to "a hex-serialized binary that can be submitted to the Ethereum blockchain… That hex string IS the contract" (Chapter 7). To call deployed code an application needs exactly two things: the ABI and the deployment address. The ABI's function selector is "the first 4 bytes of the Keccak-256 hash of the function's prototype" (Chapter 6/7) — without it, an outside caller cannot construct correctly encoded calldata. Because deployed code is permanent and public, the language's safety features (the pragma, visibility, payable, modifiers, require/assert/revert) are guardrails against shipping unfixable bugs.
Good Examples
- Version pragma guard.
pragma solidity ^0.4.19;— the caret allows any minor revision (0.4.20) but not a major one (0.5.0); the compiler errors on incompatibility. Pragmas "never enter EVM bytecode — they are purely a compile-time check" (Chapter 7). Pins immutable code to its intended compiler semantics. - Behavior keywords as correctness gates.
viewpromises no state modification;pureneither reads nor writes storage;payableaccepts ether and "non-payable functions reject incoming payments outright" (Chapter 7). Gettingpayablewrong silently breaks any contract meant to receive funds. - Function modifier (write access control once). The
_;placeholder is replaced by the modified function's code:
"access-control logic written once and applied consistently is far easier to audit" (Chapter 7).modifier onlyOwner { require(msg.sender == owner); _; } function destroy() public onlyOwner { selfdestruct(owner); } - Constructor + SELFDESTRUCT lifecycle. The
constructorkeyword (v0.4.22+) runs once during creation then is discarded;selfdestruct(address recipient)is "the only way a contract can be deleted, and it is not present by default" (Chapter 7) — so its absence is itself a permanence guarantee.[DATED 2018: SELFDESTRUCT gutted by EIP-6780 (Dencun 2024); name-style constructors removed in Solidity 0.5.] - Events bridge state to off-chain UIs.
event Withdrawal(address indexed to, uint amount);emitted viaemit Withdrawal(msg.sender, withdraw_amount);— light clients "watch" events because they "can't read contract storage cheaply" (Chapter 7). - estimateGas before mainnet.
contract.myMethod.estimateGas(arg1, arg2, {from: account})× gas price → wei → ether, "to avoid any surprises when deploying contracts to the mainnet" (Chapter 7). Treat as guidance, not guarantee: Turing-completeness means a function's gas "can vary wildly across calls/execution paths."
Counterpoints
- Visibility is callability, not secrecy. "Any function or data inside a contract is always visible on the public blockchain… The keywords described here only affect how and when a function can be called" (Chapter 7). Marking a variable
privateto hide a key/password leaks it —privateblocks calls, not reading the chain. - The constructor renaming landmine. Up to v0.4.21 the constructor was a function named after the contract; any rename/typo "silently demotes it to an ordinary public method — leaving
ownerunset AND letting any third party call it to hijack the contract" (Chapter 7). - Call/send/delegatecall risk hierarchy.
transfer(amount)throws on any error (safest);send(amount)returnsfalseand "must check return value";call(payload)is "unsafe — recipient can consume all forwarded gas (OOG halt)." Calling other contracts ranges safest-to-most-dangerous:new(known interface) → address castFaucet(_f)("Much more dangerous than creating the contract yourself") → rawcall/delegatecall(the reentrancy path).delegatecall"runs the callee's code inside the caller's storage and msg context" — "a library call is alwaysdelegatecall," powerful but the mechanism behind the most severe contract takeovers (Chapter 7). - Access control: msg.sender, never tx.origin.
msg.senderis the immediate caller — "not necessarily the originating EOA."tx.originis flagged unsafe: "using tx.orgin would allow malign contracts to destroy your contract without your permission" (Chapter 7) [sic: source typo]. - Out-of-gas costs real money for zero state change. On exceeding the limit: OOG exception, state reverted, "all gas already spent is taken as a fee, not refunded" (Chapter 7) — a direct incentive to bound loops and external calls.
Key Quotes
"Any function or data inside a contract is always visible on the public blockchain… The keywords described here only affect how and when a function can be called." — Antonopoulos & Wood, Chapter 7 "Transactions are atomic, regardless of how many contracts they call." — Antonopoulos & Wood, Chapter 7 "you must explicitly add this command… this is the only way a contract can be deleted, and it is not present by default." — Antonopoulos & Wood, Chapter 7
Rules of Thumb
- Audit before deploy — immutability makes bugs permanent; deployment is a one-way door.
- Pin the compiler with
pragma; never trust whatever compiler happens to be installed. - Mark fund-receiving functions
payable; remember the fallback is the only path for plain incoming payments. - Use
requirefor input gating,assertfor internal invariants; both revert all state changes up the call chain. - Use
transferoversendover rawcall; check return values when you must use the lower-rungs. - Factor generic concerns (
owned,mortal) into small base contracts an auditor can reason about. - Avoid unbounded loops and unscrutinized external libraries — both reintroduce out-of-gas / availability bugs.
Related References
- Transactions — Ethereum's Sole Canonical State-Change Input - ABI selector encoding of
data, value/data combinations, gas fields - Vyper — Safety by Omission and Readability as a Security Property - the auditability-first inverse of Solidity's expressiveness
- Smart Contract Vulnerability Catalog - reentrancy, delegatecall takeovers, tx.origin phishing
- Cryptography — Keys, Addresses & the One-Way Trap - ecrecover, keccak256 built-ins