Smart Contracts
Code that runs on a machine nobody controls, cannot be patched, and holds real money.
Immutable Code With Money In It
Three constraints shape everything about how this code is written. Execution is deterministic, because every node must reach the same result — no randomness, no clock, no network calls. Everything is public, including variables marked private, which only restricts other contracts, not observers. And every operation costs money, so efficiency is a budget line rather than a nicety.
Solidity
Solidity is the dominant language: statically typed, curly-braced, and unforgiving. A contract looks familiar and behaves differently.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Escrow {
address public immutable seller; // set once, cheap to read
mapping(address => uint256) private deposits;
event Deposited(address indexed from, uint256 amount);
error NotSeller();
constructor(address _seller) { seller = _seller; }
function deposit() external payable {
deposits[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function release(address to) external {
if (msg.sender != seller) revert NotSeller();
uint256 amount = deposits[to];
deposits[to] = 0; // state first
(bool ok, ) = to.call{value: amount}(""); // transfer after
require(ok, "transfer failed");
}
}
| Detail | Why it matters |
|---|---|
msg.sender | The caller — the basis of every access check |
payable | A function may only receive value if marked so |
view / pure | Reads or computes without writing — free when called off-chain |
| Custom errors | Far cheaper than revert strings |
indexed events | Filterable logs, the cheap way to publish data |
Note the ordering in release: state is updated before the external call. That is not style — it is the pattern that prevents reentrancy, and the reason is in the security lesson.
The EVM
The Ethereum Virtual Machine is a stack machine that every node runs identically. Solidity compiles to its bytecode, and the machine's shape explains most of what feels strange about contract code.
| Location | Lifetime | Cost |
|---|---|---|
| Stack | Within one operation | Almost free |
| Memory | One transaction | Cheap, grows quadratically |
| Storage | Forever, on every node | Extremely expensive |
| Calldata | Read-only transaction input | Cheapest place to read from |
| Logs | Permanent, not readable by contracts | Cheap |
Storage is a mapping of 256-bit slots. Two uint128 fields declared next to each other share one slot and one write; the same fields separated by a uint256 cost two. Declaration order is a performance decision.
Token Standards
A token is just a contract tracking balances. Standards matter because they are what let wallets, exchanges and other contracts interact with something they have never seen before.
| Standard | For | Key idea |
|---|---|---|
| ERC-20 | Fungible tokens | Balances and allowances |
| ERC-721 | Non-fungible tokens | One owner per unique id |
| ERC-1155 | Mixed batches | Many token types in one contract |
| ERC-4626 | Yield vaults | A shared deposit and share-price interface |
The ERC-20 approve-then-transferFrom flow is worth understanding because it is the source of so much risk: you grant a contract an allowance, and it can move that much of your balance whenever it likes, until you revoke it. Unlimited approvals are convenient and are how a great deal of value has been drained after an unrelated contract turned out to be compromised.
Upgrade Patterns
Deployed bytecode is immutable, so upgrading means separating storage from logic. A proxy contract holds the state and the address; calls arrive at the proxy and are delegated to an implementation contract that can be swapped.
// The proxy delegates: code from the implementation, storage from the proxy
fallback() external payable {
address impl = _implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch ok case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
Upgradeability is also a trust decision, not just a technique. Whoever can swap the implementation can replace the rules — so the honest question for any protocol is who holds that key, and whether a timelock and a multisig stand between them and your money.
Interview Questions
Why must contract execution be deterministic?
Every node re-executes it and must reach the same state. That rules out randomness, wall-clock time and network calls — anything two nodes could disagree about.
Is a private variable actually private?
No. Storage is public on every node; the modifier only stops other contracts reading it through the language. Anything secret must be hashed or kept off-chain.
Why does variable declaration order matter?
Storage is 256-bit slots. Fields that fit together are packed into one slot and one write if declared adjacently; separated by a full-width field they cost two.
What is the risk of an unlimited ERC-20 approval?
The approved contract can move that balance at any time in the future. If it is later compromised or was malicious, the allowance is still live until revoked.
How does a proxy upgrade work?
The proxy holds storage and delegatecalls into an implementation contract, so logic runs against the proxy's state. Swapping the implementation address changes the code without moving the data.
What is the danger with upgradeable storage?
Layout must only be appended. Reordering or inserting a variable makes the new implementation read a slot that holds something else, silently corrupting state.