cToken
Overview
CTokens are Curvance's unified vault tokens that represent user deposits in various yield-generating strategies. These ERC4626-compliant tokens allow users to deposit assets into Curvance vaults and optionally use them as collateral for borrowing in lending markets. CTokens are designed to be fully liquid, ensuring that assets can be immediately withdrawn or liquidated when needed, while providing seamless integration with the broader Curvance lending ecosystem.
Core Architecture
CTokens follow a hierarchical inheritance structure designed for flexibility and extensibility:
BaseCToken: The foundational abstract contract implementing ERC4626 vault functionality and core Curvance protocol integration.
SimpleCToken: For basic assets that don't generate external rewards (e.g., WETH, stablecoins, liquid staking tokens).
StrategyCToken: Extended functionality for assets that generate yield, supporting automatic reward harvesting and compounding.
StrategyCTokenWithExitFee: Adds an exit fee mechanism to compounding vaults for strategies that require it.
Protocol-Specific Implementations: Custom implementations tailored for different DeFi protocols and yield strategies.
Key Contracts and Interactions
CTokens integrate with several core Curvance contracts to provide comprehensive functionality:
CentralRegistry: Protocol configuration and permissions management, ensuring proper authorization and global settings.
MarketManager: Handles collateral position registration, risk parameters, and borrowing authorization.
Oracle System: Provides accurate asset valuation for collateral and liquidation purposes.
External Protocol Contracts: For staking, providing liquidity, and claiming rewards across various DeFi ecosystems.
Types of cTokens
Simple cTokens
Simple CTokens (SimpleCToken) are designed for assets that don't generate external rewards. They provide a straightforward vault wrapper for assets such as:
Wrapped native assets
Liquid Staking Tokens (LSTs)
Principal tokens from yield-bearing assets
Stablecoins and yield-bearing stablecoins
Basic wrapped tokens
These vaults focus on capital efficiency and gas optimization while maintaining full ERC4626 compliance.
Strategy cTokens
Strategy CTokens (StrategyCToken) extend basic functionality by adding automated yield optimization features. These sophisticated vaults:
Automatically harvest rewards from underlying protocols
Convert rewards back into the underlying asset through optimized swap routes
Reinvest proceeds into the yield-bearing position
Distribute yield to all vault users through a gradual vesting mechanism
Optimize gas costs through batched operations and keeper automation
BorrowableCTokens
See BorrowableCTokens.
Protocol-Specific Implementations
Curvance offers various protocol-specific CToken implementations tailored to different DeFi protocols and their unique characteristics.
Each implementations handles the specific deposit, withdrawal, reward harvesting, and risk management logic optimized for its respective protocol.
State Management
CTokens maintain several key state variables to track vault operations and user positions:
uint256 internal _totalAssets
uint256 internal constant _BALANCE_SLOT_SEED = 0x87a211a2;
uint256 public vestingPeriod;
// Only present in StrategyCTokens and BorrowableCTokens
uint256 internal _vestingData;
Data Flow
Deposit Flow
User calls
deposit()
with underlying assets.Contract calculates shares based on current exchange rate.
For strategy vaults, the
_afterDeposit()
hook stakes tokens in external protocols.Underlying tokens are transferred from user to contract.
Vault shares (cTokens) are minted to the user.
If user selects collateral usage, the vault notifies the MarketManager.
Position is registered and available for borrowing against.
Withdrawal Flow
User calls
withdraw()
with the amount of assets to withdraw.For strategy vaults, the
_beforeWithdraw()
hook is called to unstake tokens from the external protocol.Contract calculates shares to burn based on current exchange rate.
Underlying tokens are transferred from the contract to the user.
Vault shares (cTokens) are burned.
Position adjustments are made if necessary.
Collateral Management Flow
User calls
postCollateral()
to use cTokens as borrowing collateral.Contract verifies the asset is eligible as collateral and under the global cap.
MarketManager is notified of the new collateral position.
User's cTokens are marked as collateral and transfer-restricted.
Colalteral is now available for borrowing calculations.
User can borrow against collateralized position through BorrowableCTokens.
Reward Harvesting Flow (Strategy Vaults)
harvest()
function is called by authorized keepers or DAO.External rewards are claimed from the underlying protocols.
Protocol fees are extracted according to fee schedule.
Rewards are swapped to underlying assets through optimized routes.
New assets are deposited back into the yield strategy.
Yield is distributed to vault users through vesting mechanism.
Exchange rate increases, benefiting all cToken holders.
Yield Distribution Mechanism
Strategy cTokens and BorrowableCTokens employ a sophisticated vesting approach to distribute yield fairly and prevent MEV exploitation:
Gradual Distribution: Harvested rewards are not immediately reflected in vault assets. Instead, rewards vest linearly over a defined period (typically 24 hours).
MEV Protection: This creates a smoother increase in share price and reduces opportunities for front-running harvest transactions.
Fair Distribution: All users benefit proportionally from yield regardless of when they deposited relative to harvest timing.
The vesting mechanism operates through packed data in _vestingData
.
In StrategyCTokens:
VESTING_RATE
(bits 0-175): Rate at which rewards vest per second.VEST_END
(bits 176-215): When current vesting period ends.LAST_VEST
(bits 216-255): Last time vested rewards were claimed.
In BorrowableCTokens:
VESTING_RATE
(bits 0-95): Rate at which rewards vest per second.VEST_END
(bits 96-135): When current vesting period ends.LAST_VEST
(bits 136-175): Last time vested rewards were claimed.DEBT_INDEX
(bits 176-255): Cumulative interest index used to calculate accrued interest on debt positions.
The accrueIfNeeded()
function is called to update internal yield accounting and ensure vesting data reflects the current state.
Security Features
cTokens incorporate multiple layers of security protection:
Collateral Restrictions: Assets posted as collateral cannot be transferred until removed from collateral status.
Pause Mechanisms: Deposit, withdrawal, and harvesting operations can be paused independently for emergency response.
Reentrancy Protection: All critical functions implement comprehensive reentrancy guards.
Slippage Protection: Swap operations and liquidity management include minimum output requirements and deadline protection.
Chain Validation: Protocol-specific implementations validate deployment on correct networks.
Permissioned Operations: Sensitive functions like harvesting and emergency actions are restricted to authorized addresses.
Time Locks: Certain operations include minimum duration requirements to prevent flash loan attacks.
Oracle Integration
cTokens integrate deeply with Curvance's oracle infrastructure for accurate and secure valuation:
Protocol-Specific Adaptors: Custom oracle adaptors for complex assets like LP tokens, principal tokens, and yield-bearing assets.
TWAP Support: Time-weighted average prices provide protection against price manipulation attacks.
Multi-Source Feeds: Integration with multiple price oracle networks for redundancy and accuracy.
Fallback Mechanisms: Secondary pricing methods activate if primary oracles fail or deviate significantly.
Staleness Protection: Automatic detection and handling of stale price data.
Considerations
Collateral Caps: Global limits restrict total exposure to any single asset type, managing protocol-wide risk.
Minimum duration requirements: Collateral must remain posted for at least 20 minutes to prevent certain attack vectors.
Gas Optimization: Functions are designed to minimize gas costs while maintaining security and functionality.
Yield Optimization: Strategy implementations are regularly evaluated and updated to maximize user returns while managing risk.
User Interaction Functions
Share/Assets
convertToShares()
Contract: BaseCToken.sol
Description: Converts an amount of underlying assets to equivalent cToken shares using the current exchange rate.
Function signature:
function convertToShares(uint256 amount) public view returns (uint256)
uint256
amount
The number of underlying tokens to theoretically convert to eTokens
Return data:
uint256
The number of cTokens a user would receive for the given amount of underlying
convertToAssets()
Contract: BaseCToken.sol
Description: Converts an amount of cToken shares to equivalent underlying assets using the current exchange rate.
Function signature:
function convertToAssets(uint256 tokens) public view returns (uint256)
uint256
tokens
The number of eToken shares to theoretically convert to underlying assets
Return data:
uint256
The number of underlying assets the user would receive for redeeming shares
Deposits
deposit()
Contract: BaseCToken.sol
Description: Deposits assets into the vault and receives shares.
Function signature:
function deposit(uint256 assets, address receiver) external returns (uint256 shares)
uint256
assets
The amount of underlying assets to deposit.
address
receiver
The account that should receive the eToken shares.
Return data:
uint256
shares
The amount of cToken shares received by receiver.
Events:
// From cToken
/// `keccak256(bytes("Deposit(address,address,uint256,uint256)"))`.
uint256 internal constant _DEPOSIT_EVENT_SIGNATURE =
0xdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
depositAsCollateral()
Contract: BaseCToken.sol
Description: Deposits assets and marks shares as collateral in one transaction.
Function signature:
function depositAsCollateral(
uint256 assets,
address receiver) external returns (uint256 shares)
uint256
assets
The amount of underlying assets to deposit.
uint256
receiver
The account that should receive the cToken shares.
Return Data:
uint256
shares
The amount of cToken shares received by receiver.
Events:
// From cToken
/// `keccak256(bytes("Deposit(address,address,uint256,uint256)"))`.
uint256 internal constant _DEPOSIT_EVENT_SIGNATURE =
0xdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// Only emitted if this is the user's first position in this cToken.
event PositionUpdated(address cToken, address account, bool open);
depositAsCollateralFor()
Contract: BaseCToken.sol
Description: The depositAsCollateralFor
function enables users to deposit assets and automatically post them as collateral on behalf of another address, returning the amount of pToken shares received by the receiver. Unlike depositAsCollateral
, this function requires explicit delegation permission, where the receiver must have previously approved the caller to act on their behalf. This makes it particularly useful for smart contract integrations, portfolio management tools, and protocols that assist users in capital optimization. The function operates by first checking delegation permissions, then transferring assets from the caller to the vault, minting shares to the receiver, and finally posting those shares as collateral through the marketManager
.
Users should exercise caution when delegating this permission, as delegates could potentially abuse it by repeatedly posting shares as collateral, which could temporarily prevent withdrawals and effectively lock a user's funds. If the caller lacks proper delegation permissions, the function will still deposit assets but won't post them as collateral.
Function signature:
function depositAsCollateralFor(
uint256 assets,
address receiver
) external nonReentrant returns (uint256 shares);
uint256
assets
The amount of underlying assets to deposit.
uint256
receiver
The account that should receive the cToken shares.
Return Data:
uint256
shares
The amount of cToken shares received by receiver.
Events:
// From cToken
/// `keccak256(bytes("Deposit(address,address,uint256,uint256)"))`.
uint256 internal constant _DEPOSIT_EVENT_SIGNATURE =
0xdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// From MarketManager
event PositionUpdated(address cToken, address account, bool open);
mint()
Contract: BaseCToken.sol
Description: Users deposit underlying assets into the market and receive cTokens in return.
Function signature:
function mint(uint256 amount) external returns (uint256)
uint256
amount
The amount of the underlying asset to deposit
Return Data:
uint256
The amount of cTokens minted
Events:
// Emitted from cToken and underlying asset.
event Transfer(address indexed from, address indexed to, uint256 value);
mintFor()
Contract: BaseCToken.sol
Description: Deposits underlying assets into the market, and recipient receives cTokens.
Function signature:
function mintFor(uint256 amount, address recipient) external returns (uint256)
uint256
amount
The amount of the underlying asset to deposit
address
recipient
The account that should receive the cTokens
Return data:
uint256
The amount of cToken minted
Events:
// Emitted from cToken and underlying asset.
event Transfer(address indexed from, address indexed to, uint256 value);
Withdrawals
withdraw()
Contract: BaseCToken.sol
Description: Facilitates the withdrawal of assets from the market and the burning of shares. Initially, it verifies if the owner is eligible to redeem shares within the given market. Upon successful validation, it proceeds to burn the shares and subsequently returns the asset. Importantly, it does not force the withdrawals. If the caller is withdrawing another owner's cTokens, they must first have enough approval.
Function signature:
function withdraw(
uint256 assets,
address receiver,
address owner
) public override nonReentrant returns (uint256 shares) {
uint256
assets
The amount of underlying assets to withdraw.
address
receiver
The account that should receive the assets.
address
owner
The account that will burn their shares to withdraw assets.
Return data:
uint256
shares
The amount of shares that were burned.
Events:
// From cToken
/// @dev `keccak256(bytes("Withdraw(address,address,address,uint256,uint256)"))`.
uint256 internal constant _WITHDRAW_EVENT_SIGNATURE =
0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// From MarketManager
event PositionUpdated(address cToken, address account, bool open);
redeem()
Function: Burns shares to receive assets.
Contract: BaseCToken.sol
Function signature:
function redeem(
uint256 shares,
address receiver,
address owner) external returns (uint256 assets)
uint256
shares
The amount of shares to redeem.
address
receiver
The account that should receive the assets.
address
owner
The account that will burn their shares to receive assets.
Return data:
uint256
assets
The amount of underlying assets sent to the receiver.
Events:
// From cToken
/// @dev `keccak256(bytes("Withdraw(address,address,address,uint256,uint256)"))`.
uint256 internal constant _WITHDRAW_EVENT_SIGNATURE =
0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// From MarketManager
event PositionUpdated(address cToken, address account, bool open);
redeemFor()
Contract: BaseCToken.sol
Description: Used by a delegated user to redeem cToken in exchange for the underlying asset, on behalf of account.
Function signature:
function redeemFor(
uint256 tokens,
address recipient,
address account) external returns (uint256)
Return data:
uint256
The amount of underlying asset redeemed
Events:
// Emitted from cToken and underlying asset.
event Transfer(address indexed from, address indexed to, uint256 value);
// From MarketManager
// Only emitted if positions need to be closed
event PositionUpdated(address cToken, address account, bool open);
redeemCollateral()
Description: Redeems collateralized shares to receive assets.
Contract: BaseCToken.sol
Function signature:
function redeemCollateral(
uint256 shares,
address receiver) external returns (uint256 assets)
uint256
shares
The amount of collateralized shares to redeem.
address
receiver
The account that should receive the assets.
Return data:
uint256
assets
The amount of underlying assets sent to the receiver.
Events:
// From cToken
/// @dev `keccak256(bytes("Withdraw(address,address,address,uint256,uint256)"))`.
uint256 internal constant _WITHDRAW_EVENT_SIGNATURE =
0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// From MarketManager
// Always emitted because collateral is being removed.
event PositionUpdated(address cToken, address account, bool open);
redeemFor()
Description: Withdraws assets, quoted in shares from the market, and burns owner shares, on behalf of owner.
Contract: BaseCToken.sol
Function signature:
function redeemFor(
uint256 shares,
address receiver,
address owner) public returns (uint256 assets)
uint256
shares
The amount of collateralized shares to redeem.
address
receiver
The account that should receive the assets.
address
owner
The account that will burn their shares to withdraw assets.
Return data:
uint256
assets
The amount of underlying assets sent to the receiver.
Events:
// From cToken
/// @dev `keccak256(bytes("Withdraw(address,address,address,uint256,uint256)"))`.
uint256 internal constant _WITHDRAW_EVENT_SIGNATURE =
0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// From MarketManager
event PositionUpdated(address cToken, address account, bool open);
redeemCollateralFor()
Description: Redeems collateralized shares on behalf of an owner.
Contract: BaseCToken.sol
Function signature:
function redeemCollateralFor(
uint256 shares,
address receiver,
address owner) external returns (uint256 assets)
uint256
shares
The amount of collateralized shares to redeem.
address
receiver
The account that should receive the assets.
address
owner
The account that owns the shares being redeemed.
Return data:
uint256
assets
The amount of underlying assets sent to the receiver.
Events:
// From cToken
/// @dev `keccak256(bytes("Withdraw(address,address,address,uint256,uint256)"))`.
uint256 internal constant _WITHDRAW_EVENT_SIGNATURE =
0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 private constant _TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
// From MarketManager
// Always emitted because collateral is being removed.
event CollateralUpdated(uint256 shares, bool increased, address account);
Exchange Rate Calculation
exchangeRate()
Contract: cToken
Description: Returns the current exchange rate without updating interest, showing how many underlying tokens each cToken is worth in WAD format (view function).
Function signature:
function exchangeRate() external view returns (uint256)
Return data:
uint256
Exchange rate from shares to assets in WAD format
exchangeRateUpdated()
Contract: cToken
Description: Returns the current exchange rate after updating interest via _accrueIfNeeded()
, used to determine how many underlying tokens each cToken is worth.
Function signature:
function exchangeRateUpdated() external returns (uint256)
Return data:
uint256
Exchange rate from shares to assets in WAD format.
Interest Accrual Mechanism
Interest/yield is accrued on BorrowableCTokens/StrategyCTokens and distributed to cToken holders through the increasing exchange rate. The accrual process is managed by the accrueIfNeeded
function.
accrueIfNeeded()
Contract: BaseCTokenWithYield.sol
Function signature:
function accrueIfNeeded() public
In BorrowableCToken, this function:
Calculates time elapsed since the last interest accrual.
Fetches the current interest rate from the interest rate model.
Computes the interest amount based on current total borrows.
Updates total borrows with the new interest.
Allocates a portion of interest to protocol reserves based on the
interestFee
.Updates the exchange rate to reflect the new value of each BorrowableCToken.
In StrategyCTokens, this function:
Interest accrues automaically when users interact with the protocol (mint, redeem, borrow, repay) as these functions call accrueIfNeeded
internally.
Compounding Vault Specifics
For compounding vaults like PendleLPCToken, VelodromeVolatileCToken, AerodromeStableCToken.
Vesting Mechanism
Compounding vaults use a vesting mechanism to gradually distribute yield.
When yield is harvested:
Rewards are claimed from the external protocol.
A protocol fee is taken.
Remaining rewards are swapped to the underlying asset.
The yield is distributed over the vesting period (default 1 day).
function harvest(bytes calldata data) external returns (uint256 yield)
data
Encoded swap parameters to convert rewards to underlying assets.
Getting Vault Status
function getYieldInformation() external view returns (
uint256 vestingRate,
uint256 vestingEnd,
uint256 lastVestingClaim,
uint256 debtIndex
);
Returns current information about the vault's yield distribution:
vestingRate
: Percentage rate at which yield is being distributed.vestingEnd
: When the current vesting period ends.lastVestingClaim
: Last time pending vesting rewards were claimed.debtIndex
: The current market debt index for interest calculations.
Considerations
Collateral Caps: Restricts the total exogenous risk from any single asset.
20-Minute Minimum Duration: Collateral must be posted for at least 20 minutes.
Safe Functions: Enhanced protection against reentrancy and other vulnerabilities.
Liquidation Safeguards: Only authorized contracts can seize collateral during liquidations.
Last updated