contract ShareRegistry { // STATE — persistent on-chain storage, this contract only mapping(address => uint256) public balances; address public transferAgent; // declared; gates nothing yet // FUNCTION — writes state, costs gas, can revert function transfer(address to, uint256 amount) public { require(balances[msg.sender] >= amount, "insufficient"); balances[msg.sender] -= amount; // msg.sender = GLOBAL balances[to] += amount; } // FUNCTION — view: reads only, free off-chain function balanceOf(address who) public view returns (uint256) { return balances[who]; } }
This is a minimal ledger / share register — the core of every token (ERC-20, ERC-3643, Gateway's ERC20F).
The balances mapping is the on-chain cap table.
| State variable | Context global | |
|---|---|---|
| Examples | balances, transferAgent | msg.sender, msg.value, block.timestamp, tx.origin |
| What it is | Declared by the contract | Injected by the EVM per call |
| Lifetime | Permanent, on-chain | Ephemeral — this call only |
| Writable? | Yes — costs gas | No — read-only |
| Scope | This contract instance (NOT chain-wide) | Ambient call context |
| TS analogy | Instance field persisted to a DB | req.user / thread-local |
"Global" ≠ chain-wide. A state variable is global only within its contract; another contract's
balances is a separate table. The genuinely ambient things are the EVM context globals.
public on a variable → auto-generates a getter function (balances(addr), transferAgent()).public on a function → callable from outside and inside.view → reads state but never writes; free when called off-chain (no transaction).require(cond, "msg") → if false, revert: undo all state changes this call made.view call = free.transferAgent is declared but used by nothing in the snippet — a placeholder for the accountable party.
In a regulated token it becomes load-bearing:
modifier onlyAgent() { require(msg.sender == transferAgent, "not agent"); _; } function forceTransfer(address from, address to, uint256 amt) public onlyAgent { ... } function freeze(address who) public onlyAgent { ... }
This encodes Marketnode's transfer-agent role / ERC-3643's agent: the party who can force-transfer, freeze, mint and burn. See ERC-3643 architecture.
transferAgent is declared but unused — what's missing?" → access-control modifiers + privileged functions.
(2) "Difference between msg.sender and tx.origin?" → immediate caller vs. original signing EOA; using tx.origin
for auth is a classic vuln. See Lesson 6.
Setup: a treasury guards withdrawals with require(tx.origin == owner). The owner is tricked into
calling claimReward() on an attacker's contract.
function withdraw(address to, uint256 amt) public { require(tx.origin == owner, "not owner"); // BUG payable(to).transfer(amt); }
Attack: claimReward() calls treasury.withdraw(attacker, balance). Inside withdraw,
msg.sender = the attacker's contract, but tx.origin = the owner (who signed the outer tx) → the check
passes → drained.
Fix: require(msg.sender == owner) — now the check sees the attacker's contract and reverts.
Rule: authorise with msg.sender; tx.origin is for "which human started this tx", never for auth, because
every contract you call inherits it. Same principle protects Gateway's agent-gated functions.
| Call path | signed by | msg.sender | tx.origin |
|---|---|---|---|
| EOA → treasury | EOA | EOA | EOA |
| EOA → attacker → treasury | EOA only | attacker | EOA |