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

Implementation Playbook — Build, Test, Deploy

deployment testing tooling web3 nodes json-rpc

Key Principle

A deployed contract is immutable and usually holds value, so a bug is both expensive and unfixable. The entire workflow exists to exhaust failure modes off-chain and deploy once. "It's easy to write code, but it's very hard to write good and secure code" (Chapter 1).

Why This Matters

There is no undo button. Deployment is a one-way door; testnets, local chains, reused audited libraries, and on-chain inspection are the only error-correction layer a system with no rollback can have. Every step below substitutes a cheap, reversible check for an expensive, irreversible mistake.

[DATED 2018: Truffle/Ganache → Hardhat/Foundry/Anvil; web3.js v1 → v4 + ethers/viem; Parity discontinued; Ropsten/Rinkeby/Kovan testnets retired → Sepolia/Holešky; post-Merge a node pairs an *execution* client with a separate *consensus* client.] Tool names below are historical; the disciplines are durable.

Action Sequence

  1. Self-custody setup. A wallet manages keys; it does not hold funds — funds live on-chain and the private key is the sole means of moving them. Install only from trusted sources (a malicious wallet = total, unrecoverable loss). Use MetaMask (the de facto default), which injects a web3 object into the page and acts as an RPC gateway — the integration seam for the whole DApp ecosystem. Use test-only keys when developing; never test with keys that hold real money.

  2. Select the network. One private key + address works on all networks, but balances are per-network state. This is why testnets allow risk-free practice — the same key holds valueless test ETH and real mainnet ETH with no cross-contamination. Faucets dispense free test ETH.

  3. Choose how to connect to a node (a trust-vs-cost decision, not convenience):

    • Full node — downloads + validates the entire chain; the only trustless mode (query state offline and privately, no third party can mislead/censor). Cost: high disk (~80 GB+ pruned in 2018; ~1 TB+ today), SSD mandatory (syncing is I/O-intensive).
    • Remote client (MetaMask, MEW, MyCrypto) — stores no chain, validates nothing, entirely trusts a full node. All mobile wallets are remote clients. Browser JS wallets are bridges, not vaults — frequent phishing targets; reach them via bookmark, never a search link.
    • Light client — SPV-style; validates block headers + Merkle proofs (more security than a remote client).
    • Dev shortcut: point a script at Infura (a hosted node) so you talk to mainnet/testnets with no local full node.
  4. Talk to the node via JSON-RPCthe programmatic entry point; every higher-level tool ultimately speaks it. Served as HTTP on port 8545, bound to localhost (127.0.0.1) by default — a deliberate security boundary; never expose 8545 publicly without auth/proxy (nodes get drained this way). Request shape: jsonrpc:"2.0", method, params (array, optional), id (echoed; enables batching). RPC results are hex-encoded — always convert before interpreting.

  5. Write → Compile → Deploy.

    • The EVM cannot execute Solidity; compilation to bytecode is mandatory (solc / Remix). Pin the compiler with a pragma — a mismatch blocks compilation outright.
    • Deployment is a transaction, not an API call: a creation transaction whose destination is the zero address 0x0000…0000, carrying compiled bytecode as data. The new contract starts with zero balance, so funding is a separate ordinary transaction to the new address. A contract is an account that runs its code whenever any transaction targets it. (CREATE2 now also enables deterministic counterfactual addresses.)
    • To receive plain ether sends a contract must declare a payable fallback (receive()/fallback() in modern Solidity), or such transfers revert.
  6. Test against a disposable local EVM (the immutability discipline). Pair a test language + runner + chain emulator. Solidity-native tests (ds-test → Foundry) run inside actual bytecode execution, so they see the same gas/revert/event semantics the live contract will — higher fidelity than a JS test crossing an RPC boundary. Local-test-chain config that carries over to Anvil/Hardhat:

    • Gas limit must track mainnet — a --gasLimit below mainnet's block limit makes valid txs fail with spurious "out of gas."
    • Network id + port must match the project config and the running emulator.
    • --mnemonic restores deterministic test accounts.
  7. Inspect on-chain — verify the deploy, don't test on it. These RPC calls (valid in web3 v4 / ethers / viem):

    • eth.getTransactionReceipt(txhash) — recover the new contract's address.
    • eth.getCode(address) — the canonical deploy check: empty = nothing deployed, nonempty = code is live.
    • eth.getPastLogs(options) — full event log history; reconstructs call history.
    • eth.getStorageAt(address, position) — raw storage slot the ABI doesn't expose.
    • Remember internal transactions: a contract can move ether via msg.sender.transfer(...) in a tx with value: 0, so the normal tx list under-reports flow — check Etherscan's Internal Transactions tab.
  8. Reuse audited libraries. On an immutable adversarial platform, the dominant security move is writing less code. OpenZeppelin is the de facto audited library (ERC-20/721, Ownable/Pausable, composable crowdsale mixins). Caveat: passing each unit's isolated tests does not prove the composed contract is correct — integration-test the composition end to end (Solidity multiple inheritance has surprising method-resolution order).

  9. Reconcile immutability with maintainability via the upgradeable proxy pattern. A thin proxy holds state and delegates logic (DELEGATECALL) to a swappable implementation; upgrade = point the proxy at new logic, address unchanged. (ZeppelinOS → now OpenZeppelin Upgrades + transparent/UUPS proxies.) Danger: the callee runs against your storage layout by slot position.

  10. Interact via web3. To read any deployed contract you need exactly three things — provider → (ABI + address) → contract object → .method().call(). The ABI can be pulled from a block explorer; the contract is a public object, so you need neither source nor the deployer's cooperation. .call() on a read-only method is gas-free and needs no signing key — provider + address + ABI suffice. (State-changing calls add the fourth ingredient: a signing key.)

Pre-Deployment Checklist

  • Compiler version pinned with pragma; bytecode compiles clean.
  • All logic tested against a disposable local EVM (Anvil/Hardhat), gas limit set to track mainnet.
  • Composed/inherited contracts integration-tested, not just unit-tested.
  • Audited libraries (OpenZeppelin) reused for standard behaviors; custom glue minimized.
  • payable fallback present iff the contract must receive plain ether.
  • Correct --network confirmed (deploying a throwaway to mainnet is a real, costly mistake).
  • Test-only keys; no real-money keys touched during development.
  • Small test transaction (sub-$1) sent first — "if it has gone wrong, it is better to find out with a small loss."
  • Deploy verified with eth.getCode (nonempty); address recovered via getTransactionReceipt.
  • JSON-RPC port 8545 not exposed publicly.

Key Quotes

"An Ethereum wallet is your gateway to the Ethereum system. It holds your keys and can create and broadcast transactions on your behalf." — Antonopoulos & Wood, Chapter 2

"Although most testing shouldn't occur on deployed contracts, a contract's behavior can be checked via Ethereum clients." — Antonopoulos & Wood, Appendix D/E

"It's not enough to test all the units separately, because the interactions between them might cause behaviors that you didn't expect." — Antonopoulos & Wood, Appendix C/D

Rules of Thumb

  • Deploy once: exhaust failure modes off-chain because bytecode is immutable and bugs are permanent loss.
  • Always know your selected --network.
  • Use web3.toWei(0.5,'ether') instead of typing 18 zeros — manual zeros are "difficult and dangerous."
  • Never expose JSON-RPC port 8545 to the public network.
  • Reuse audited code; write the minimum case-specific glue.

Related References