Section 4 of 18
CToken: Storage and State
What You Are Building
The CTokenStorage contract defines every state variable the CToken needs. It is a pure storage layout with no logic. Separating storage from logic is a deliberate architectural choice: it makes the inheritance hierarchy clear, prevents storage slot collisions when adding features, and lets you audit the complete state of a CToken market by reading a single contract.
This section also defines the interfaces for the Comptroller and InterestRateModel, so the CToken can reference them without circular dependencies. You built the InterestRateModel in section 3. The Comptroller comes later in sections 9-11. For now, you only need the interface.
State Variables Explained
The CToken tracks five categories of state.
ERC-20 metadata (name, symbol, decimals): The cToken is itself an ERC-20 token. When you deposit DAI, you get cDAI tokens. These have their own name, symbol, and decimals (typically 8, not 18).
Administrative state (admin, comptroller, interestRateModel): The admin can update the reserve factor and interest rate model. The Comptroller reference is used to check permissions before every operation. The interest rate model reference is used to compute current rates.
Exchange rate seed (initialExchangeRateMantissa): When totalSupply is 0 (no deposits yet), the exchange rate is undefined (division by zero). This value serves as the initial exchange rate, typically 0.02e18 (meaning 50 cTokens per 1 underlying token). This high ratio gives cTokens more decimal precision than the underlying.
Market accounting (accrualBlockNumber, borrowIndex, totalBorrows, totalReserves, totalSupply): These are the global counters that drive the entire protocol.
accrualBlockNumber: The last block when interest was accrued. If this is behindblock.number, interest needs to be accumulated before any operation.borrowIndex: A cumulative interest multiplier. Starts at 1e18 and grows every time interest accrues. This is the key to lazy per-user interest.totalBorrows: Total underlying tokens currently lent out. Grows as interest accrues.totalReserves: Protocol's accumulated revenue from interest. This is the protocol's cut.totalSupply: Total cTokens in existence. Changes when users mint (deposit) or redeem (withdraw).
Per-user state (accountTokens, accountBorrows, transferAllowances): Individual balances, borrow snapshots, and ERC-20 allowances.
The BorrowSnapshot: Lazy Interest
The BorrowSnapshot struct is the most important design pattern in Compound V2. Without it, accruing interest would require iterating over every borrower every block, which is impossibly expensive on-chain.
struct BorrowSnapshot {
uint256 principal; // Borrow balance at time of last interaction
uint256 interestIndex; // borrowIndex at time of last interaction
}
When a user borrows 1,000 DAI and the current borrowIndex is 1.1e18, their snapshot stores principal = 1000e18 and interestIndex = 1.1e18. Ten blocks later, after interest has been accrued, the global borrowIndex might be 1.15e18. The user's current balance is:
currentBorrow = 1000e18 * 1.15e18 / 1.1e18 = 1045.45e18
The user now owes 1,045.45 DAI. This was computed with a single multiplication and division, no matter how many blocks have passed. If you have used Uniswap V2's cumulative price oracle, this is the same pattern: store a growing accumulator globally, compute differences per-user on demand.
Events
The CToken emits events for every state-changing operation. AccrueInterest fires every time interest is accrued. Mint, Redeem, Borrow, RepayBorrow, and LiquidateBorrow fire for each respective operation. Standard ERC-20 Transfer and Approval events are included for wallet compatibility.
Your Task
Build the CTokenStorage contract. This is primarily a state declaration exercise, but getting the types and visibility right matters for every subsequent section.
- Inherit from
ExponentialNoError(the math library from section 2) - Define the
IComptrollerinterface with all hook functions:mintAllowed,redeemAllowed,borrowAllowed,repayBorrowAllowed,liquidateBorrowAllowed,seizeAllowed,transferAllowed, andliquidateCalculateSeizeTokens - Define the
IInterestRateModelinterface withgetBorrowRateandgetSupplyRate - Declare all state variables with correct visibility:
_notEntered(internal), ERC-20 metadata (public), admin and references (public),initialExchangeRateMantissa(internal),reserveFactorMantissa(public), accounting variables (public),accountTokens(internal mapping),transferAllowances(internal mapping) - Define the
BorrowSnapshotstruct withprincipalandinterestIndex - Declare the
accountBorrowsmapping (internal, address to BorrowSnapshot) - Define
protocolSeizeShareMantissaas a public constant (2.8e16, representing 2.8%) - Declare all events:
AccrueInterest,Mint,Redeem,Borrow,RepayBorrow,LiquidateBorrow,Transfer,Approval,NewReserveFactor,NewComptroller,NewInterestRateModel