IERC20.sol
The base ERC-20 interface defining the 6 functions and 2 events that every token must implement. Every line explained.
Because developers of the future should know the code they interact with.
The v5.0.0 version tag and the path token/ERC20/IERC20.sol tell you this is the base interface at the root of the ERC-20 directory — not in extensions/ like IERC20Metadata.
^0.8.20 matches every other contract in the EffortToken codebase. IERC20 only uses basic Solidity features (function declarations, events, the indexed keyword) that have existed since much earlier versions, but OpenZeppelin aligns all v5.x contracts on the same compiler target for consistency.
The minimal NatSpec comment and interface IERC20 { with no 'is' clause. This is the root interface — nothing above it in the hierarchy. IERC20Metadata extends it, ERC20 implements it, EffortToken inherits the implementation.
The Blueprint Every ERC-20 Token Must Follow
IERC20.sol is the foundational interface defining the ERC-20 standard. It contains zero implementation code — only function signatures and event declarations. The pragma ^0.8.20 matches every other file in the EffortToken codebase. Despite using only basic Solidity features (function declarations, events, the indexed keyword), OpenZeppelin aligns all contracts on the same compiler target for consistency. The declaration interface IERC20 { with no 'is' clause means this is the absolute root of the inheritance tree — nothing sits above it.
This single file is the reason all ERC-20 tokens are interoperable. Every DEX, wallet, bridge, and DeFi protocol can interact with any ERC-20 token because they all implement this exact interface. Without IERC20, every token would have different function signatures, making composability impossible. Over 600,000 tokens on Ethereum mainnet alone implement this interface.
Interfaces compile to zero bytecode and have no runtime cost. However, the interface only defines WHAT functions must exist, not HOW they behave. A malicious contract could implement transfer() to steal funds instead of moving them. Always verify the implementation contract, not just the interface it claims to follow.
- ▸The ^0.8.20 pragma matches every other contract in the EffortToken codebase. IERC20 only uses basic Solidity features, but OpenZeppelin keeps all v5.x contracts on the same compiler target for consistency.
- ▸The 'I' prefix in IERC20 is a naming convention (not enforced by the compiler) indicating this is an interface, not a concrete contract.
- ▸This file has no imports and no inheritance — it is the absolute root of the ERC-20 type hierarchy.
- ✗Thinking an interface provides any implementation — IERC20 is pure type declaration. A contract claiming 'is IERC20' still needs to write every function body.
- ✗Confusing IERC20 with ERC20 — IERC20 is the interface (the spec), ERC20 is OpenZeppelin's concrete implementation of that spec.
- interface
- A Solidity construct that declares function signatures, events, and errors without any implementation. All functions are implicitly external and virtual. Compiles to zero bytecode.
- ERC-20
- Ethereum Request for Comments #20, the fungible token standard. Proposed by Fabian Vogelsteller and Vitalik Buterin in November 2015. Defines 6 functions and 2 events that all compliant tokens must implement.
ERC-20 was not the first token standard proposed for Ethereum — it was preceded by several informal approaches. But its simplicity won out: just 6 functions and 2 events define the entire standard. Today, over 600,000 ERC-20 tokens exist on Ethereum mainnet alone, all implementing this exact interface.
Emitted on every transfer, mint, and burn. Three parameters: from (indexed), to (indexed), and value (not indexed). The NatSpec explicitly notes that value may be zero — an important edge case for indexers.
- ▸from = the sender (address(0) for mints)
- ▸to = the recipient (address(0) for burns)
- ▸value = the amount transferred (may be zero)
- ▸Both from and to are indexed for efficient log filtering
Emitted whenever approve() is called. Three parameters: owner (indexed), spender (indexed), and value (not indexed). The value represents the NEW total allowance, not an increment — it replaces any previous allowance.
- ▸owner = the address granting permission
- ▸spender = the address receiving permission to spend
- ▸value = the new total allowance (replaces the old value, does not add to it)
Two Signals That Power the Entire Token Ecosystem
IERC20 defines exactly two events: Transfer and Approval. These are the on-chain log entries that allow wallets, block explorers, indexers (like The Graph), and DeFi dashboards to track token activity without polling every contract's state. Transfer fires whenever tokens move (including mints from address(0) and burns to address(0)). Approval fires whenever a spending allowance is set or changed. Both use the 'indexed' keyword on address parameters to enable efficient log filtering — you can search for all transfers involving a specific address without scanning every log entry.
Without these events, there would be no way to track token movements off-chain in real time. MetaMask detects incoming tokens by watching Transfer events. Etherscan builds its entire token transfer history from Transfer event logs. DeFi aggregators monitor Approval events to detect when users have approved a protocol to spend their tokens.
The value parameter in Transfer is NOT indexed — you can filter by from and to addresses efficiently, but not by amount. Also note that value may be zero, which means contracts can emit Transfer events with zero value. Some phishing attacks exploit this to spam your wallet with fake transaction histories (zero-value transfer spam).
- ▸The 'indexed' keyword on event parameters creates EVM 'topics' that enable efficient log filtering. Each event can have at most 3 indexed parameters (plus the event signature topic).
- ▸Transfer events with from=address(0) indicate minting. Transfer events with to=address(0) indicate burning. This convention is universally followed but not part of the interface definition.
- ▸Approval events are emitted even when the new allowance equals the old one — this provides a complete audit trail of approve() calls.
- ✗Forgetting to emit Transfer when minting or burning — this breaks all off-chain indexing and makes your token invisible to block explorers and wallets.
- ✗Thinking indexed parameters are stored differently on-chain — they are stored as topics enabling O(1) log filtering, but the full data is always available in the receipt.
- event
- A Solidity declaration that defines an on-chain log entry. Events are stored in the transaction receipt's logs array, not in contract storage. They cost ~375 gas for the first topic + ~375 per additional topic + 8 gas per byte of data.
- indexed
- An event parameter modifier that stores the value as a log 'topic' for efficient filtering. Up to 3 parameters per event can be indexed. For value types (address, uint256), the actual value is stored as a topic.
Zero-value Transfer events were exploited in a widespread 'address poisoning' attack. Scammers sent 0 USDT to thousands of wallets from addresses that looked similar to the victim's real contacts. When victims copied the 'from' address from their transaction history, they sent funds to the attacker instead.
Returns the total number of tokens in existence. No parameters needed. In ERC20.sol, this reads from the private _totalSupply variable that increases on mint and decreases on burn.
Returns the token balance for a specific address. This is the function your wallet calls to show your EFFORT balance. In ERC20.sol, this reads from the private _balances mapping.
Reading the Ledger: How Much Exists, How Much You Own
These two functions are the read-only query interface for any ERC-20 token. totalSupply() returns the total number of tokens currently in existence (minted minus burned). balanceOf(account) returns how many tokens a specific address holds. Both are 'external view' — they read state but never modify it, meaning they cost zero gas when called from off-chain (via an RPC call). These are the functions your frontend calls when displaying token balances.
Every wallet, portfolio tracker, and DeFi protocol calls these two functions constantly. When MetaMask shows your EFFORT balance, it is calling balanceOf(yourAddress). When CoinGecko shows a token's market cap, it calls totalSupply() and multiplies by price. These are the most-called functions on any ERC-20 token.
View functions cannot modify state, but they CAN be called inside state-modifying transactions. Do not make critical decisions based solely on balanceOf() or totalSupply() at a single point in time, because flash loans can temporarily manipulate these values within a single transaction.
- ▸totalSupply() takes no parameters — it returns a global value. balanceOf() takes one parameter — the address to query.
- ▸Both return uint256, which means the raw value includes all 18 decimal places. To get the human-readable balance, divide by 10^decimals.
- ▸'external view' means these cost zero gas when called from off-chain. They only cost gas when called from within another contract's transaction.
- ✗Displaying raw balanceOf() values without dividing by 10^decimals — showing '50000000000000000000' instead of '50.0 EFFORT'.
- ✗Assuming totalSupply() is constant — it changes every time tokens are minted or burned. EffortToken's totalSupply increases with every completed quiz.
- external view
- A function that can only be called from outside the contract and guarantees it will not modify state. When called off-chain (not inside a transaction), view functions cost zero gas.
- uint256
- An unsigned 256-bit integer. The standard return type for token amounts. Can represent values from 0 to 2^256-1 (approximately 1.16 x 10^77).
The largest uint256 value is approximately 1.16 x 10^77. With 18 decimals, the theoretical maximum total supply of an ERC-20 token is about 1.16 x 10^59 tokens. For context, the estimated number of atoms in the observable universe is only about 10^80.
The NatSpec describes three key behaviors: (1) moves value tokens from caller to recipient, (2) returns bool for success, (3) emits a Transfer event. The sender is always msg.sender — implicit, not a parameter.
- ▸to = the recipient address
- ▸value = the raw amount to send (including all 18 decimal places)
- ▸msg.sender = the implicit sender (not a parameter)
- ▸Reverts if sender has insufficient balance (in OpenZeppelin's implementation)
The Core Action: Sending Tokens Directly
transfer(to, value) is the primary function for sending tokens. The caller (msg.sender) sends value tokens to the 'to' address. It returns a boolean indicating success. This is NOT a view function — it modifies state (changes balances), costs gas, and MUST emit a Transfer event. The sender is implicit (msg.sender) — you cannot specify a different 'from' address with this function.
This is the function that gets called when you click 'Send' in any wallet. It is the most fundamental action in the ERC-20 standard. Every direct token transfer you have ever made on Ethereum went through this function. EffortToken's transfer page at /tokens/transfer calls this exact function signature.
The interface specifies a bool return value, but the ERC-20 standard does not mandate that implementations revert on failure — some legacy tokens return false instead of reverting. This inconsistency led to OpenZeppelin's SafeERC20 library, which wraps calls to handle both patterns. Modern tokens (like EffortToken) always revert on failure AND return true on success.
- ▸The 'to' parameter is the recipient address. The 'value' parameter is the raw amount including decimals — to send 50 EFFORT, you pass 50 * 10^18.
- ▸The caller is implicit (msg.sender) — you cannot transfer FROM another address using this function. For that, you need transferFrom().
- ▸The bool return value exists for backward compatibility. Modern tokens revert on failure rather than returning false, making the return value somewhat redundant.
- ✗Not checking the return value of transfer() when calling from another contract — use SafeERC20's safeTransfer() to handle tokens that return false on failure.
- ✗Sending tokens to the zero address (0x0) — OpenZeppelin's ERC20.sol reverts this because it would be equivalent to burning without updating totalSupply.
- ✗Forgetting that transfer() costs gas — it is a state-modifying transaction, not a free read like balanceOf().
- transfer
- The direct token transfer function. Moves tokens from msg.sender to the specified recipient. Must emit a Transfer event on success.
- returns (bool)
- The function returns true on success. Some legacy tokens return false on failure instead of reverting. Modern tokens always revert on failure, making the return value redundant but required by the interface.
The ERC-20 transfer() function was modeled after traditional banking APIs. But unlike a bank transfer, it is atomic and irreversible — either it succeeds completely in a single transaction or it reverts entirely. There is no 'pending' state and no 'undo' button.
Returns how many tokens the spender can still spend from the owner's balance. Read-only (view). The NatSpec notes that this value is zero by default and changes when approve() or transferFrom() are called.
- ▸Returns 0 if no approval has been granted
- ▸Decreases when transferFrom() consumes allowance
- ▸Resets when approve() is called with a new value
- ▸Does NOT decrease when the special value type(uint256).max is used as allowance
Sets a new allowance. The NatSpec contains the most important security warning in the entire ERC-20 standard: the approval race condition (lines 58-63). This is the only place in IERC20 where the NatSpec includes an IMPORTANT warning and links to an external resource.
- ▸Replaces the current allowance entirely (does NOT add to it)
- ▸RACE CONDITION: Changing from N to M allows spender to potentially spend N+M
- ▸MITIGATION: First approve(spender, 0), wait for confirmation, then approve(spender, M)
- ▸The linked GitHub issue #20 is the original 2016 discussion of this vulnerability
- ▸Emits an Approval event with the new allowance value
The Approval Pattern: Delegating Token Spending Rights
These two functions implement the ERC-20 approval mechanism — the ability to authorize a third party (a 'spender') to transfer tokens on your behalf. allowance(owner, spender) is a read-only query returning how many tokens the spender can currently spend from the owner's balance. approve(spender, value) sets a new allowance. This is the foundation of all DeFi: when you 'approve' Uniswap to swap your tokens, you are calling approve(uniswapRouter, amount). The NatSpec on approve() contains a critical security warning about the approval race condition (lines 58-63).
The approve/transferFrom pattern is what makes DeFi composable. Without it, every token interaction would require the user to manually transfer tokens first. Instead, you approve a DEX once, and it can pull tokens when executing your swap. This 'pull' pattern is used by virtually every DeFi protocol: Uniswap, Aave, Compound, and hundreds more.
CRITICAL: The Approval Race Condition. If Alice has approved Bob to spend 100 tokens and wants to change it to 50, Bob could front-run the approve(50) transaction by spending the original 100, then spending the new 50 — getting 150 total. The mitigation is to first approve(0), wait for confirmation, then approve(50). OpenZeppelin's ERC20.sol does NOT enforce this pattern — it is the caller's responsibility. Newer alternatives like EIP-2612 permit() avoid this issue entirely.
- ▸The allowance mapping is a double mapping: owner => spender => amount. Each owner can have different allowances for different spenders.
- ▸approve() replaces the entire allowance — it does not increment it. If you approve(spender, 100) and then approve(spender, 50), the allowance is 50, not 150.
- ▸The race condition mitigation (approve to 0 first) is mentioned directly in the NatSpec — this is one of the few places where the official standard warns about a security issue.
- ▸For unlimited approval (common in DeFi), protocols approve type(uint256).max. OpenZeppelin's ERC20 skips deducting from the allowance when it equals max.
- ✗Changing an allowance directly from N to M without first setting it to 0 — this exposes you to the approval race condition described in the NatSpec.
- ✗Approving type(uint256).max without understanding the risk — if the approved contract is compromised, it can drain all your tokens.
- ✗Confusing allowance() with balanceOf() — allowance is about permission to spend, not ownership of tokens.
- allowance
- The amount of tokens that a spender is authorized to transfer from an owner's balance. Set by approve(), consumed by transferFrom(). Stored in a nested mapping: owner => spender => uint256.
- approve
- Sets the spending allowance for a spender over the caller's tokens. Replaces (does not add to) any existing allowance. Emits an Approval event.
- Approval Race Condition
- A vulnerability where changing an allowance from N to M allows the spender to potentially spend N+M by front-running the approve() transaction. Mitigated by first approving 0, then the desired value.
The approval race condition was first documented in November 2016, just one year after ERC-20 was proposed. Despite being a known issue for over a decade, it was never fixed in the standard itself — the EIP-20 authors chose to document the warning rather than change the interface. This is why the NatSpec contains such an unusually detailed security warning, complete with a link to the original GitHub issue.
The three-parameter transfer function that enables DeFi. Uses the allowance mechanism: msg.sender spends from the 'from' address's balance, sending to 'to'. This is how Uniswap, Aave, and every other DeFi protocol moves your tokens.
- ▸from = the address whose tokens are being spent
- ▸to = the recipient of the tokens
- ▸value = the amount to transfer (deducted from the allowance)
- ▸msg.sender = the approved spender (must have sufficient allowance)
- ▸Emits a Transfer event on success
Closes the interface declaration. These 6 functions and 2 events constitute the complete ERC-20 standard. Any contract declaring 'is IERC20' must provide concrete bodies for all 6 functions.
Delegated Transfers: Moving Tokens on Someone's Behalf
transferFrom(from, to, value) is the second half of the approval pattern. After an owner has called approve(spender, amount), the spender can call transferFrom(owner, recipient, value) to move tokens from the owner to the recipient, deducting from the allowance. This is how DeFi protocols actually move your tokens — you approve them, then they call transferFrom() in the same transaction that executes your swap, lend, or stake. The closing } on line 79 ends the entire IERC20 interface.
This function is the backbone of all DeFi composability. When Uniswap executes a swap, it calls transferFrom(you, pool, amount) using the allowance you previously granted. Without transferFrom(), every DeFi interaction would require two separate transactions: first transfer tokens to the protocol, then tell the protocol to act. The approve + transferFrom pattern makes it atomic.
transferFrom() should always verify that (1) the allowance is sufficient, (2) the from address has enough balance, and (3) the caller (msg.sender) is the approved spender. OpenZeppelin's implementation also emits an Approval event to reflect the updated remaining allowance, creating a complete audit trail. Contracts calling transferFrom() without verifying the return value should use SafeERC20.
- ▸transferFrom() has THREE address roles: 'from' (the token owner), 'to' (the recipient), and msg.sender (the approved spender — implicit, not a parameter).
- ▸The allowance is automatically reduced by value after a successful transferFrom() in OpenZeppelin's implementation. Exception: if the allowance is type(uint256).max, it is not reduced (infinite approval).
- ▸This function emits a Transfer event (from -> to). OpenZeppelin's implementation also emits an Approval event showing the updated remaining allowance.
- ✗Calling transferFrom() without first having the owner call approve() — it will revert with ERC20InsufficientAllowance.
- ✗Thinking 'from' is msg.sender — in transferFrom(), 'from' is the token owner whose tokens are being moved, and msg.sender is the approved spender doing the moving.
- ✗Not accounting for the allowance being consumed — after transferFrom(), the remaining allowance decreases. If you need multiple transfers, approve enough for all of them.
- transferFrom
- The delegated transfer function. Moves tokens from one address to another using a previously set allowance. The caller (msg.sender) must have been approved by the 'from' address.
- allowance mechanism
- The two-step pattern: (1) owner calls approve(spender, amount), then (2) spender calls transferFrom(owner, recipient, amount). This 'pull' pattern enables atomic DeFi operations.
The approve + transferFrom two-step pattern was controversial from day one. Critics argued it was unnecessarily complex and error-prone (the race condition being exhibit A). Alternatives have been proposed — EIP-2612 (permit) uses off-chain signatures, and EIP-777 uses operator-based transfers. But ERC-20's simplicity won the network effect battle, and transferFrom() remains the dominant pattern a decade later.
IERC20.sol — OpenZeppelin v5.0.0 on OP Sepolia
View on Etherscan