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:

  1. BaseCToken: The foundational abstract contract implementing ERC4626 vault functionality and core Curvance protocol integration.

  2. SimpleCToken: For basic assets that don't generate external rewards (e.g., WETH, stablecoins, liquid staking tokens).

  3. StrategyCToken: Extended functionality for assets that generate yield, supporting automatic reward harvesting and compounding.

  4. StrategyCTokenWithExitFee: Adds an exit fee mechanism to compounding vaults for strategies that require it.

  5. 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

  1. User calls deposit() with underlying assets.

  2. Contract calculates shares based on current exchange rate.

  3. For strategy vaults, the _afterDeposit() hook stakes tokens in external protocols.

  4. Underlying tokens are transferred from user to contract.

  5. Vault shares (cTokens) are minted to the user.

  6. If user selects collateral usage, the vault notifies the MarketManager.

  7. Position is registered and available for borrowing against.

Withdrawal Flow

  1. User calls withdraw() with the amount of assets to withdraw.

  2. For strategy vaults, the _beforeWithdraw() hook is called to unstake tokens from the external protocol.

  3. Contract calculates shares to burn based on current exchange rate.

  4. Underlying tokens are transferred from the contract to the user.

  5. Vault shares (cTokens) are burned.

  6. Position adjustments are made if necessary.

Collateral Management Flow

  1. User calls postCollateral() to use cTokens as borrowing collateral.

  2. Contract verifies the asset is eligible as collateral and under the global cap.

  3. MarketManager is notified of the new collateral position.

  4. User's cTokens are marked as collateral and transfer-restricted.

  5. Colalteral is now available for borrowing calculations.

  6. User can borrow against collateralized position through BorrowableCTokens.

Reward Harvesting Flow (Strategy Vaults)

  1. harvest() function is called by authorized keepers or DAO.

  2. External rewards are claimed from the underlying protocols.

  3. Protocol fees are extracted according to fee schedule.

  4. Rewards are swapped to underlying assets through optimized routes.

  5. New assets are deposited back into the yield strategy.

  6. Yield is distributed to vault users through vesting mechanism.

  7. 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)
Type
Name
Description

uint256

amount

The number of underlying tokens to theoretically convert to eTokens

Return data:

Type
Description

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) 
Type
Name
Description

uint256

tokens

The number of eToken shares to theoretically convert to underlying assets

Return data:

Type
Description

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)
Type
Name
Description

uint256

assets

The amount of underlying assets to deposit.

address

receiver

The account that should receive the eToken shares.

Return data:

Type
Name
Description

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)
Type
Name
Description

uint256

assets

The amount of underlying assets to deposit.

uint256

receiver

The account that should receive the cToken shares.

Return Data:

Type
Name
Description

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.

Function signature:

function depositAsCollateralFor(
    uint256 assets,
    address receiver
) external nonReentrant returns (uint256 shares);
Type
Name
Description

uint256

assets

The amount of underlying assets to deposit.

uint256

receiver

The account that should receive the cToken shares.

Return Data:

Type
Name
Description

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)
Type
Name
Description

uint256

amount

The amount of the underlying asset to deposit

Return Data:

Type
Description

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)
Type
Name
Description

uint256

amount

The amount of the underlying asset to deposit

address

recipient

The account that should receive the cTokens

Return data:

Type
Description

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) {
Type
Name
Description

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:

Type
Name
Description

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)
Type
Name
Description

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:

Type
Name
Description

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:

Type
Description

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)
Type
Name
Description

uint256

shares

The amount of collateralized shares to redeem.

address

receiver

The account that should receive the assets.

Return data:

Type
Name
Description

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)
Type
Name
Description

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:

Type
Name
Description

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)
Type
Name
Description

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:

Type
Name
Description

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:

Type
Description

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:

Type
Description

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:

  1. Calculates time elapsed since the last interest accrual.

  2. Fetches the current interest rate from the interest rate model.

  3. Computes the interest amount based on current total borrows.

  4. Updates total borrows with the new interest.

  5. Allocates a portion of interest to protocol reserves based on the interestFee.

  6. 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:

  1. Rewards are claimed from the external protocol.

  2. A protocol fee is taken.

  3. Remaining rewards are swapped to the underlying asset.

  4. The yield is distributed over the vesting period (default 1 day).

function harvest(bytes calldata data) external returns (uint256 yield)
Parameter
Description

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