Aave

Theme

Aave V3 Tokens

At the heart of Aave's accounting are specialized ERC20-compliant tokens: aTokens (representing supplied liquidity) and Debt Tokens (representing borrowed funds). These tokens are more than simple balance keepers; they incorporate Aave's core interest accrual mechanisms and specific DeFi functionalities like EIP712 signatures and credit delegation.

The Core Mechanism: Scaled Balances

Before diving into the specifics of each token, it's crucial to understand the Scaled Balance mechanism employed by both aTokens and Debt Tokens.

  • Problem: Continuously updating every user's balance as interest accrues would be incredibly gas-intensive.
  • Solution: Instead of storing the actual balance, Aave stores a scaled balance for each user. Simultaneously, the protocol maintains a global index for each reserve:
    • Liquidity Index (liquidityIndex): Tracks the cumulative interest earned by suppliers. Starts at 1 Ray (1e27) and increases over time based on the supply rate.
    • Variable Borrow Index (variableBorrowIndex): Tracks the cumulative interest accrued on variable-rate borrows. Starts at 1 Ray (1e27) and increases based on the variable borrow rate.
  • Calculation:
    • When a user interacts (supply, borrow, transfer), their current balance is calculated: currentBalance = scaledBalance * currentIndex.
    • The amount corresponding to the action (e.g., amount supplied) is then converted to a scaled amount: scaledAmount = amount / currentIndex.
    • This scaledAmount is added to or subtracted from the user's stored scaledBalance.
    • The user's "last updated index" is also stored (implicitly within ScaledBalanceTokenBase._userState's additionalData field) to calculate accrued interest correctly between actions.
  • Result: Interest accrual is reflected by simply updating the global index for the reserve. Individual user storage writes only happen when a user actively interacts with the protocol.

The base contract ScaledBalanceTokenBase.sol implements this core logic.

// File: /src/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol

// Returns the internally stored scaled balance (balance / index)
function scaledBalanceOf(address user) external view override returns (uint256) {
  return super.balanceOf(user); // Inherited balanceOf stores the scaled balance
}

// Returns the scaled total supply (totalSupply / index)
function scaledTotalSupply() public view virtual override returns (uint256) {
  return super.totalSupply(); // Inherited totalSupply stores the scaled total supply
}

// Internal mint function using scaled math
function _mintScaled(
  address caller,
  address onBehalfOf,
  uint256 amount, // Amount in underlying units
  uint256 index   // Current reserve index (liquidity or variable borrow)
) internal returns (bool) {
  uint256 amountScaled = amount.rayDiv(index); // Convert to scaled amount
  // ... calculate balanceIncrease based on previous index ...
  _userState[onBehalfOf].additionalData = index.toUint128(); // Store current index for user
  _mint(onBehalfOf, amountScaled.toUint128()); // Mint scaled amount (calls base _mint)
  // ... emit events with actual amounts ...
}

// Internal burn function using scaled math
function _burnScaled(address user, address target, uint256 amount, uint256 index) internal {
  uint256 amountScaled = amount.rayDiv(index); // Convert to scaled amount
  // ... calculate balanceIncrease based on previous index ...
  _userState[user].additionalData = index.toUint128(); // Store current index for user
  _burn(user, amountScaled.toUint128()); // Burn scaled amount (calls base _burn)
  // ... emit events with actual amounts ...
}

1. AToken (AToken.sol)

  • Purpose: Represents a user's supplied liquidity in a specific reserve. It's an interest-bearing token; holding aTokens means your underlying deposit is earning the supply interest rate.
  • Key Interfaces: IAToken, IScaledBalanceToken, IInitializableAToken, IERC20, IERC20Detailed.
  • Core Logic: Inherits heavily from ScaledBalanceTokenBase for the core ERC20 and scaled balance logic, and EIP712Base for permit functionality.

Key Functions & Mechanics:

  1. initialize(...): Sets up the aToken, linking it to the Pool, underlying asset, treasury, incentives controller, and setting metadata (name, symbol, decimals). Called once by the PoolConfigurator.
  2. mint(caller, onBehalfOf, amount, index):
    • Called only by the Pool contract during a supply operation.
    • Delegates to _mintScaled, providing the current liquidityIndex from the Pool.
    • Increases the user's scaled balance and the token's scaled total supply.
  3. burn(from, receiverOfUnderlying, amount, index):
    • Called only by the Pool contract during a withdraw or liquidationCall (if receiveAToken is false).
    • Delegates to _burnScaled, providing the current liquidityIndex.
    • Decreases the user's scaled balance and the token's scaled total supply.
    • Crucially, it also calls IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount) to send the actual underlying asset back to the user/liquidator.
  4. balanceOf(user):
    • Overrides the base implementation to return the current, interest-accrued balance.
    • Fetches the user's stored scaled balance (super.balanceOf(user)).
    • Fetches the current liquidityIndex from the Pool (POOL.getReserveNormalizedIncome(_underlyingAsset)).
    • Returns scaledBalance.rayMul(currentIndex).
  5. totalSupply():
    • Similar to balanceOf, calculates the current total supply based on the stored scaled total supply and the current liquidityIndex.
  6. _transfer(from, to, amount, validate):
    • Handles internal transfer logic for both transfer and transferFrom.
    • Fetches the current liquidityIndex.
    • Updates the stored scaled balances of both sender and receiver using super._transfer(from, to, amount.rayDiv(index).toUint128()).
    • Updates the additionalData (last updated index) for both users.
    • If validate is true (for user-initiated transfers): Calls POOL.finalizeTransfer(...) to allow the Pool to perform a health factor check after the transfer, ensuring the sender doesn't become undercollateralized by transferring away collateral aTokens.
    • Emits relevant Transfer and BalanceTransfer (scaled amount) events.
  7. permit(...):
    • Implements EIP-2612, allowing users to approve spending via an off-chain signature, saving a transaction compared to the standard approve + transferFrom flow. Uses EIP712Base for domain separation and nonce tracking.
  8. transferUnderlyingTo(target, amount):
    • A utility function callable only by the Pool. Used during borrow and flashLoan operations to instruct the aToken contract (which holds the liquidity) to send underlying assets to the borrower/receiver.
  9. handleRepayment(user, onBehalfOf, amount):
    • Hook callable only by the Pool after a repayment using underlying assets or during a flash loan repayment. In the base AToken, this is empty, but derived contracts could implement logic here (e.g., staking).
  10. mintToTreasury(amount, index):
    • Called only by the Pool to allocate accrued protocol fees (reserve factor) to the designated treasury address by minting new aTokens. Uses _mintScaled.

2. VariableDebtToken (VariableDebtToken.sol)

  • Purpose: Represents a user's borrowed amount at a variable interest rate for a specific reserve. The balance accrues interest over time based on the reserve's variable borrow rate.
  • Key Interfaces: IVariableDebtToken, IScaledBalanceToken, IInitializableDebtToken, ICreditDelegationToken.
  • Core Logic: Inherits from DebtTokenBase (for credit delegation) and ScaledBalanceTokenBase (for ERC20 and scaled balance logic).

Key Functions & Mechanics:

  1. initialize(...): Sets up the debt token, similar to the aToken, linking it to the Pool, underlying asset, incentives controller, and setting metadata.
  2. mint(user, onBehalfOf, amount, index):
    • Called only by the Pool during a borrow operation or a flashLoan with Mode 2 (debt opening).
    • Credit Delegation Check: If user != onBehalfOf, it means the user (caller of Pool.borrow) is borrowing on behalf of onBehalfOf using delegated credit. It calls _decreaseBorrowAllowance(onBehalfOf, user, amount) (from DebtTokenBase) to reduce the borrowing allowance onBehalfOf previously granted to user.
    • Delegates to _mintScaled, providing the current variableBorrowIndex from the Pool.
    • Increases the onBehalfOf user's scaled debt balance and the token's scaled total supply.
    • Returns isFirstBorrowing flag and the new scaledTotalSupply.
  3. burn(from, amount, index):
    • Called only by the Pool during a repay or liquidationCall.
    • Delegates to _burnScaled, providing the current variableBorrowIndex.
    • Decreases the from user's scaled balance and the token's scaled total supply.
    • Returns the new scaledTotalSupply.
  4. balanceOf(user):
    • Overrides the base implementation.
    • Fetches the user's stored scaled debt balance (super.balanceOf(user)).
    • Fetches the current variableBorrowIndex from the Pool (POOL.getReserveNormalizedVariableDebt(_underlyingAsset)).
    • Returns scaledBalance.rayMul(currentVariableBorrowIndex).
  5. totalSupply():
    • Similar to balanceOf, calculates the current total variable debt based on the stored scaled total supply and the current variableBorrowIndex.
  6. approveDelegation(delegatee, amount):
    • Implements ICreditDelegationToken. Allows a user (msg.sender) to grant another user (delegatee) the permission to borrow up to amount on their behalf (i.e., increasing the delegator's debt). Stores allowance in _borrowAllowances.
  7. delegationWithSig(...):
    • Implements EIP-712 based signature approval for approveDelegation, saving gas. Uses DELEGATION_WITH_SIG_TYPEHASH and EIP712Base.
  8. Disabled ERC20 Functions:
    • transfer, approve, transferFrom, increaseAllowance, decreaseAllowance all explicitly revert(Errors.OPERATION_NOT_SUPPORTED). Debt tokens are non-transferable to maintain the integrity of individual user health factors and liquidation logic.

Base Contracts (IncentivizedERC20, MintableIncentivizedERC20, DebtTokenBase, EIP712Base)

  • IncentivizedERC20.sol: Provides the basic ERC20 storage (_userState, _allowances, _totalSupply) and implements standard view functions (name, symbol, decimals, allowance). Crucially, it introduces the _incentivesController storage slot and modifies _transfer (and requires _mint/_burn in derived contracts) to include calls to incentivesControllerLocal.handleAction(user, totalSupply, oldBalance) whenever a user's balance changes, allowing rewards tracking.
  • MintableIncentivizedERC20.sol: Extends IncentivizedERC20 by adding the internal _mint and _burn functions, which update total supply and user balances while including the handleAction hook for incentives.
  • DebtTokenBase.sol: Provides the logic and storage (_borrowAllowances) for credit delegation (approveDelegation, delegationWithSig, borrowAllowance, _decreaseBorrowAllowance). It also inherits from EIP712Base for the delegation signature functionality.
  • EIP712Base.sol: Provides the foundational logic for EIP-712 domain separation and nonce management, used by both AToken (for permit) and DebtTokenBase (for delegationWithSig).

Conclusion

Aave V3's AToken and VariableDebtToken are sophisticated ERC20 tokens central to the protocol's operation. They leverage the Scaled Balance mechanism for gas-efficient interest accrual. While AToken represents yield-bearing supplied assets and supports standard ERC20 transfers (with Pool validation) and permit, VariableDebtToken represents variable-rate debt, is non-transferable, and includes unique Credit Delegation features. Both integrate seamlessly with the Pool for minting/burning and optionally with an IAaveIncentivesController for rewards. Understanding their internal scaled math and specific functionalities is key to interacting correctly with the Aave protocol.