Section 17 of 18
CErc20: Token Integration
What You Are Building
You are building the final integration layer that turns all the internal logic into a deployable contract. CErc20 inherits the entire CToken chain (CTokenStorage, CTokenInterest, CTokenMint, CTokenRedeem, CTokenBorrow, CTokenRepay, CTokenLiquidate) and adds four things: the public entry points, the underlying token transfer implementation, standard ERC-20 functions, and initialization.
This is the contract users actually interact with. Everything built so far has been internal functions. CErc20 wraps them with external functions, reentrancy protection, and the concrete doTransferIn/doTransferOut implementations that move real ERC-20 tokens.
The initialize() Function
In the real Compound V2, cTokens are deployed behind proxies. Initialization happens separately from deployment, so a constructor would not work. We keep this pattern for educational consistency:
function initialize(
address underlying_,
address comptroller_,
address interestRateModel_,
uint256 initialExchangeRateMantissa_,
string memory name_,
string memory symbol_,
uint8 decimals_
) external {
require(admin == address(0), "CErc20: already initialized");
admin = msg.sender;
// ... set all state
borrowIndex = expScale; // 1e18 = no interest accrued yet
_notEntered = true;
}
The initial exchange rate is typically 0.02e18, meaning 1 underlying token equals 50 cTokens. This ratio exists so cToken balances have more precision than underlying balances. Since cTokens typically have 8 decimals and most underlying tokens have 18, the 50:1 ratio maintains precision as the exchange rate grows over time.
doTransferIn and doTransferOut
These are the concrete implementations of the virtual functions declared in CTokenRepay and CTokenRedeem. They bridge between the protocol's internal accounting and actual ERC-20 token movements.
doTransferIn has a critical safety pattern for fee-on-transfer tokens:
function doTransferIn(address from, uint256 amount) internal override returns (uint256) {
uint256 balanceBefore = IERC20(underlying).balanceOf(address(this));
IERC20(underlying).transferFrom(from, address(this), amount);
uint256 balanceAfter = IERC20(underlying).balanceOf(address(this));
return balanceAfter - balanceBefore;
}
If a token charges a 1% transfer fee, requesting 100 tokens only deposits 99. The before/after balance check ensures the protocol uses the actual received amount, not the requested amount. This is the same pattern Uniswap V2 uses in its fee-on-transfer Router support.
doTransferOut is simpler because the protocol controls the transfer:
function doTransferOut(address to, uint256 amount) internal override {
bool success = IERC20(underlying).transfer(to, amount);
require(success, "CErc20: transfer out failed");
}
The nonReentrant Modifier
Every public entry point uses the nonReentrant modifier. This prevents attacks where a malicious token's transfer callback re-enters the CToken before state updates are complete. ERC-777 tokens and tokens with transfer hooks are the primary threat. While Compound historically only supported standard ERC-20s, the guard provides defense in depth:
modifier nonReentrant() {
require(_notEntered, "CToken: re-entered");
_notEntered = false;
_;
_notEntered = true;
}
ERC-20 Functions
The cToken is itself an ERC-20 token. Users can transfer, approve, and trade cTokens. However, transfers are subject to Comptroller approval via transferAllowed. This is because moving cTokens changes the sender's collateral position. If a user has entered a market and is using their cTokens as collateral, transferring them away could make their position undercollateralized. The Comptroller runs the hypothetical liquidity check before allowing the transfer.
This mirrors how Uniswap V2's LP tokens work: LP tokens are freely transferable ERC-20 tokens. But in Compound, transfers have an extra policy layer because collateral has borrowing power.
Public Entry Points
Each public function follows the same pattern: apply nonReentrant, delegate to the internal function:
function mint(uint256 mintAmount) external nonReentrant returns (uint256) {
doTransferIn(msg.sender, mintAmount);
mintInternal(mintAmount);
return 0;
}
The liquidateBorrow entry point casts the collateral address to CTokenInterface, connecting the borrowed market to the collateral market.
Your Task
Build the CErc20 contract inheriting from CTokenLiquidate. Implement:
initialize()that sets all state (underlying, comptroller, interestRateModel, initialExchangeRateMantissa, borrowIndex, accrualBlockNumber, name, symbol, decimals, admin, _notEntered). Require admin == address(0) to prevent re-initialization- The
nonReentrantmodifier using the _notEntered flag - All public entry points:
mint,redeem,redeemUnderlying,borrow,repayBorrow,repayBorrowBehalf,liquidateBorrow, each with nonReentrant getCashPrior()overriding the virtual function to return the underlying token balancedoTransferInwith the before/after balance check for fee-on-transfer safetydoTransferOutusing ERC-20 transfer- ERC-20 functions:
balanceOf,transfer,transferFrom(with allowance check),approve,allowance, and the internal_transferTokensthat checks with comptroller.transferAllowed