Lesson 10 · Code Literacy

Both Sides of the Boundary: Solidity & ethers.js

An engineering leader reads two languages: the contract (Solidity, on-chain) and the app that calls it (TypeScript via ethers.js, off-chain). The ABI is the seam between them. Let's read both.

Why this, now? You're TypeScript-fluent with rusty Solidity. To review your team's work and interview well, you don't need to write production contracts — you need to read a contract confidently and understand exactly how the off-chain app reaches across to it. This lesson makes the on/off-chain boundary (Lesson 5) concrete in code, in a stack that'll feel immediately familiar.

One tooling fact first (it's a hiring signal)

Web3.js was sunset in March 2025 by its maintainer ChainSafe.1 For new work the field uses ethers.js (most popular, TypeScript-friendly) or viem (newer, fully type-safe, smaller). If a candidate reaches for Web3.js on a greenfield project, that's a small but real currency flag worth probing. We'll use ethers.js (v6).

The three-layer picture

SOLIDITY CONTRACTABIethers.js (TypeScript) on-chain, the rules the interface your app's hands (what's enforced) (the contract's (reads & calls it) machine-readable API)

The ABI (Application Binary Interface) is the key idea: it's a machine-readable description of the contract's functions and events — the ERC-20 interface from Lesson 3, turned into JSON the client uses to encode calls. Your TypeScript never sees Solidity; it sees the ABI.

Side 1 — Reading Solidity (as a reviewer, not an author)

You're not writing this; you're interrogating it. Here's the Lesson 2 registry again, annotated with what a reviewer looks for:

contract — on-chain
contract ShareRegistry {
    mapping(address => uint256) public balances;   // state: the ledger
    address public agent;                          // ← WHO is privileged?

    modifier onlyAgent() {                          // ← access control (Lesson 6 #1 risk)
        require(msg.sender == agent, "not agent");
        _;
    }

    function forcedTransfer(address from, address to, uint256 amt)
        public onlyAgent {                            // ← privileged: who can call it? guarded?
        balances[from] -= amt;
        balances[to]   += amt;
        emit Transfer(from, to, amt);              // ← event = off-chain feed (Lesson 3)
    }

    event Transfer(address indexed from, address indexed to, uint256 amt);
}
A reviewer's checklist when reading any contract: ① Which functions change state (cost gas) vs are view (free)? ② Every privileged function — is it guarded by a modifier, and who is the authority? (access control, Lesson 6). ③ What does each require enforce — and what happens on failure (revert, Lesson 2)? ④ What events are emitted (your off-chain truth, Lesson 3)? ⑤ Anything that handles value or external calls (reentrancy surface). You can read for these without writing a line.

Side 2 — Calling it from TypeScript (ethers.js v6)

Three objects are the whole mental model — and they map straight onto Lesson 2:

ethers objectWhat it isMaps to (Lesson 2)
ProviderA read-only connection to a node. Query state, call view functions.Free reads — no gas, no transaction
SignerWraps an account + its key. Signs and sends state-changing transactions.The EOA + its private key (custody!)
Contractaddress + ABI + (Provider or Signer). Gives you typed methods.A handle to the contract account
read — free, no key, no gas
import { ethers } from "ethers";

// read-only: a Provider is enough
const provider = new ethers.JsonRpcProvider(RPC_URL);

// the ABI — human-readable form (this IS the interface, Lesson 3)
const abi = [
  "function balanceOf(address) view returns (uint256)",
  "function transfer(address to, uint256 amount)",
  "event Transfer(address indexed from, address indexed to, uint256 value)",
];

const token = new ethers.Contract(ADDRESS, abi, provider);
const bal: bigint = await token.balanceOf(user);   // view → free, instant
write — needs a Signer (a key), costs gas, returns a tx
// state change: connect a Signer (holds the key — Lesson 6 custody)
const signer = await provider.getSigner();
const token = new ethers.Contract(ADDRESS, abi, signer);

const tx = await token.transfer(to, ethers.parseUnits("100", 18));
// tx is SENT but not yet final — mining is async
const receipt = await tx.wait();   // wait for confirmation (a block)
// if a require() failed on-chain, this REVERTS and throws (Lesson 2)
listen — the off-chain reconciliation feed (Lessons 3 & 5)
// subscribe to events — how your back office stays in sync
token.on("Transfer", (from, to, amount, event) => {
  reconcile(from, to, amount);   // update off-chain books
});
Notice how little is new: it's just async TypeScript calling typed methods. The concepts doing the work — free reads vs gas-costing writes, the key behind the Signer, reverts, events as the sync feed — are all things you already learned. ethers.js is a thin, familiar skin over the model.

Where this connects to the rest

In code you see……which is the concept
provider read vs signer writeview (free) vs state-changing (gas) — Lesson 2
The ABI arrayThe interface — Lesson 3
The Signer's keyCustody / Fireblocks / MPC — Lesson 6
tx.wait() can throwRevert / atomicity — Lesson 2
.on("Transfer", …)Events as the off-chain bridge — Lessons 3 & 5
A backend Signer (not a browser wallet)Server-side signing → the agent key → secure it (Lesson 6)

The Marketnode lens & interview angles

Retrieve it (don't peek)

From memory. Interleaves Lessons 2, 3 & 6.

1. In ethers.js, why is a Provider enough to read a balance but a Signer needed to transfer?
2. What is the ABI's role between the contract and your TypeScript app?
3. Your backend uses an ethers Signer to mint tokens. What's the key security concern?

Primary source

The canonical reference, well-written and TypeScript-first: ethers.js v6 — Getting Started. For the Solidity side, the Solidity docs (skim "Contracts" and "Visibility and Getters"). Context on the tooling shift: ChainSafe — Web3.js sunset; and the modern alternative viem.

I'm your teacher — ask me. Want me to set up a tiny runnable ethers.js + local-node sandbox so you can actually call a contract from TypeScript? Or contrast ethers vs viem for a real stack decision? Or read a real ERC-3643 contract from GitHub with you? Just ask.