Skip to content
Learn · Intermediate

Reading Solidity contracts

A systematic method to understand verified contracts on Etherscan — inheritance, modifiers, storage, and external calls.

Intermediate
12 min read

Start with the contract header

Read the contract declaration line: contract Foo is Bar, Baz. That lists inheritance parents. OpenZeppelin bases bring standard behavior — ERC20, Ownable, Pausable, Governor — each adds functions and storage. Unverified contracts deserve extreme skepticism.

  • Linearization (C3) determines super call order and storage layout merge.
  • imports show which libraries are linked — @openzeppelin/contracts is a good sign.
  • Pragma and compiler version must match verified settings.

Map access control

Find modifiers: onlyOwner, onlyRole, nonReentrant. Trace who can call admin functions. Ownable has one owner; AccessControl supports multiple roles (MINTER_ROLE, PAUSER_ROLE). Upgradeable variants use initializer instead of constructor.

  • owner() and pendingOwner() for two-step Ownable transfers.
  • hasRole(DEFAULT_ADMIN_ROLE, account) gates role grants.
  • Renouncing ownership or admin is irreversible — check if the contract did.
Access.solsolidity
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

// Ownable: single owner — simple protocols
// AccessControl: granular roles — production DAOs and protocols

Trace state variables

Storage variables at contract level define persistent state. Follow mappings and structs that hold balances, debts, and configuration. Constants and immutables do not occupy runtime storage slots the same way — immutables are set once in constructor.

  • Slot packing: uint128 + uint128 can share one 32-byte slot.
  • Proxy storage lives at proxy address — implementation only supplies logic.
  • Gap arrays (__gap) in upgradeable OZ contracts reserve future slots.

Follow external calls

Any call to another contract or ETH transfer is an trust and reentrancy boundary. Look for .call{value:}, transfer, send, and interface calls. Checks-effects-interactions: state updates should precede external calls unless explicitly documented otherwise with guards.

  • delegatecall runs foreign code in your storage context — proxy core.
  • Staticcall is view-only external reads.
  • Low-level call return values must be checked — tokens may not revert on failure.

Read the external interface

Public/external functions are the API. Internal/private helpers implement logic. Start from user-facing entry points (deposit, stake, vote), then admin (setFee, pause, upgrade). NatSpec comments above functions document intent — when present, read them.

  • payable functions accept ETH — check where it goes.
  • view functions are free off-chain — use them in UIs via RPC.
  • Fallback/receive handle plain ETH sends — unexpected ether can break accounting.

Proxy awareness

If Etherscan shows 'Read as Proxy', logic lives at implementation. Admin may sit on ProxyAdmin or inside UUPS upgradeTo. Compare implementation version across upgrades. Storage layout must stay compatible — see upgradeable article.

  • ERC1967 slots store implementation and admin addresses.
  • Implementation constructor is disabled — initializer sets up state.
  • Bricked UUPS: new implementation missing upgrade function locks upgrades forever.

Practice workflow

Pick a verified OpenZeppelin Governor on Etherscan. (1) List parents, (2) note Timelock as executor, (3) read quorum and votingPeriod, (4) find proposal lifecycle functions, (5) skim events for indexer design. Repeat until the pattern is automatic.