Library
Mastering Ethereum: Building Smart Contracts and DApps · 11 of 15
Mastering Ethereum: Building Smart Contracts and DApps
blockchain CRITICAL

Solidity Essentials — Writing Immutable, Self-Owning Contracts

solidity abi modifiers visibility payable delegatecall events gas

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. view promises no state modification; pure neither reads nor writes storage; payable accepts ether and "non-payable functions reject incoming payments outright" (Chapter 7). Getting payable wrong silently breaks any contract meant to receive funds.
  • Function modifier (write access control once). The _; placeholder is replaced by the modified function's code:
    modifier onlyOwner { require(msg.sender == owner); _; }
    function destroy() public onlyOwner { selfdestruct(owner); }
    "access-control logic written once and applied consistently is far easier to audit" (Chapter 7).
  • Constructor + SELFDESTRUCT lifecycle. The constructor keyword (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 via emit 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 private to hide a key/password leaks it — private blocks 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 owner unset 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) returns false and "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 cast Faucet(_f) ("Much more dangerous than creating the contract yourself") → raw call/delegatecall (the reentrancy path). delegatecall "runs the callee's code inside the caller's storage and msg context" — "a library call is always delegatecall," powerful but the mechanism behind the most severe contract takeovers (Chapter 7).
  • Access control: msg.sender, never tx.origin. msg.sender is the immediate caller — "not necessarily the originating EOA." tx.origin is 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 require for input gating, assert for internal invariants; both revert all state changes up the call chain.
  • Use transfer over send over raw call; 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