Amount Transformations Reference¶

This document provides detailed amount transformation formulas with Solidity code snippets for navigating the actual contract source.

Table of Contents¶


Core Math Operations¶

WadRayMath¶

File: contracts/protocol/libraries/math/WadRayMath.sol

uint256 internal constant WAD = 1e18;    // 18 decimals for tokens
uint256 internal constant RAY = 1e27;    // 27 decimals for indices
uint256 internal constant HALF_RAY = RAY / 2;

// Standard half-up rounding
function rayMul(uint256 a, uint256 b) internal pure returns (uint256) {
    return (a * b + HALF_RAY) / RAY;
}

function rayDiv(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 halfB = b / 2;
    return (a * RAY + halfB) / b;
}

// v4+ variants
function rayMulFloor(uint256 a, uint256 b) internal pure returns (uint256) {
    return (a * b) / RAY;
}

function rayMulCeil(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 result = (a * b) / RAY;
    if ((a * b) % RAY != 0) result++;
    return result;
}

function rayDivFloor(uint256 a, uint256 b) internal pure returns (uint256) {
    return (a * RAY) / b;
}

function rayDivCeil(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 result = (a * RAY) / b;
    if ((a * RAY) % b != 0) result++;
    return result;
}

PercentageMath¶

File: contracts/protocol/libraries/math/PercentageMath.sol

uint256 internal constant PERCENTAGE_FACTOR = 1e4;  // 100.00%

function percentMul(uint256 value, uint256 percentage) internal pure returns (uint256) {
    uint256 halfPercentage = percentage / 2;
    return (value * percentage + halfPercentage) / PERCENTAGE_FACTOR;
}

Collateral Token Transformations¶

Supply: amount → scaledAmount¶

Files:

  • contracts/protocol/libraries/logic/SupplyLogic.sol:executeSupply()

  • contracts/protocol/tokenization/AToken.sol:_mintScaled()

Transformation:

User Input (WAD decimals)
    ↓
amount.rayDiv(index) → scaledAmount (RAY decimals)
    ↓
Store: _scaledBalance[user] += scaledAmount

Solidity:

// SupplyLogic.sol
uint256 scaledAmount = amount.rayDiv(index);  // v1-3: half-up, v4+: floor
IAToken(reserveCache.aTokenAddress).mint(
    msg.sender,
    onBehalfOf,
    amount,
    index
);

// AToken.sol
function _mintScaled(address user, uint256 scaledAmount, uint256 index) internal {
    _scaledBalance[user] += scaledAmount;
}

Withdraw: scaledAmount → amount¶

Files:

  • contracts/protocol/libraries/logic/SupplyLogic.sol:executeWithdraw()

  • contracts/protocol/tokenization/AToken.sol:_burnScaled()

Transformation:

_scaledBalance[from] (stored scaled)
    ↓
_scaledBalance[from].rayMul(index) = userBalance (WAD)
    ↓
Validation: amount <= userBalance
    ↓
amount.rayDiv(index) → scaledAmountToBurn
    ↓
_scaledBalance[from] -= scaledAmountToBurn

Solidity:

// SupplyLogic.sol
uint256 userBalance = IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender);
if (amount == type(uint256).max) {
    amount = userBalance;
}
uint256 scaledAmount = amount.rayDiv(index);  // v1-3: half-up, v4+: ceil

// AToken.sol
function _burnScaled(address from, uint256 amount, uint256 index) internal {
    uint256 scaledAmount = amount.rayDiv(index);
    _scaledBalance[from] -= scaledAmount;
}

Transfer: scaledAmount Preservation¶

File: contracts/protocol/tokenization/AToken.sol:_transfer()

uint256 scaledAmount = amount.rayDiv(index);  // v4+: uses rayDivFloor

_scaledBalance[from] -= scaledAmount;
_scaledBalance[to] += scaledAmount;

Pool.finalizeTransfer(
    underlyingAsset,
    from,
    to,
    amount,                   // unscaled
    fromScaledBalanceBefore,  // scaled
    toScaledBalanceBefore     // scaled
);

Used in flows: Supply, Withdraw, Liquidation


Debt Token Transformations¶

Variable Borrow: amount → scaledAmount¶

Files:

  • contracts/protocol/libraries/logic/BorrowLogic.sol:executeBorrow()

  • contracts/protocol/tokenization/VariableDebtToken.sol:_mint()

Transformation:

User requests borrow amount (WAD)
    ↓
amount.rayDiv(nextVariableBorrowIndex) → scaledAmount (RAY)
    ↓
_scaledBalance[user] += scaledAmount

Solidity:

// BorrowLogic.sol
IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint(
    msg.sender,
    onBehalfOf,
    amount,
    reserveCache.nextVariableBorrowIndex
);

// VariableDebtToken.sol
function _mint(address user, uint256 amount, uint256 index) internal {
    uint256 scaledAmount = amount.rayDiv(index);  // v1-3: half-up, v4+: ceil
    _scaledBalance[user] += scaledAmount;
}

Variable Repay: scaledAmount → amount¶

Files:

  • contracts/protocol/libraries/logic/BorrowLogic.sol:executeRepay()

  • contracts/protocol/tokenization/VariableDebtToken.sol:_burn()

Transformation:

_scaledBalance[user].rayMul(index) = currentDebt (WAD)
    ↓
Calculate payback amount (min(requested, currentDebt))
    ↓
amount.rayDiv(index) → scaledAmountToBurn
    ↓
_scaledBalance[user] -= scaledAmountToBurn

Solidity:

// BorrowLogic.sol
(
    uint256 stableDebt,
    uint256 variableDebt
) = Helpers.getUserCurrentDebt(onBehalfOf, reserveCache);

IVariableDebtToken(reserveCache.variableDebtTokenAddress).burn(
    onBehalfOf,
    paybackAmount,
    index
);

// VariableDebtToken.sol
function _burn(address user, uint256 amount, uint256 index) internal {
    uint256 scaledAmount = amount.rayDiv(index);  // v4+: uses rayDivFloor
    uint256 scaledBalanceBefore = _scaledBalance[user];
    
    if (scaledAmount > scaledBalanceBefore) {
        scaledAmount = scaledBalanceBefore;
        amount = scaledAmount.rayMul(index);
    }
    
    _scaledBalance[user] -= scaledAmount;
}

Stable Borrow (No Scaling)¶

File: contracts/protocol/tokenization/StableDebtToken.sol:mint()

// Stable debt is NOT scaled - stored as principal + timestamp
function _mint(address user, uint256 amount, uint256 rate) internal {
    uint256 previousBalance = _balances[user].principal;
    uint256 balanceIncrease = 0;
    
    if (previousBalance != 0) {
        balanceIncrease = previousBalance.rayMul(
            MathUtils.calculateCompoundedInterest(
                _balances[user].stableRate,
                _balances[user].lastUpdateTimestamp
            )
        ) - previousBalance;
    }
    
    _balances[user].principal = previousBalance + amount + balanceIncrease;
    _balances[user].stableRate = getAverageStableRate(
        previousBalance + balanceIncrease,
        _balances[user].stableRate,
        amount,
        rate
    );
    _balances[user].lastUpdateTimestamp = block.timestamp;
}

Used in flows: Borrow, Repay, Liquidation


Interest Accrual¶

Liquidity Index Update¶

Files:

  • contracts/protocol/libraries/logic/ReserveLogic.sol:_updateIndexes()

  • contracts/protocol/libraries/math/MathUtils.sol:calculateLinearInterest()

Transformation:

OLD: liquidityIndex[t-1]
    ↓
Calculate timeDelta = now - lastUpdateTimestamp
    ↓
cumulatedInterest = 1 + (rate * timeDelta / SECONDS_PER_YEAR)
    ↓
NEW: liquidityIndex[t] = liquidityIndex[t-1].rayMul(cumulatedInterest)

Solidity:

// ReserveLogic.sol
if (reserveCache.currLiquidityRate != 0) {
    uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(
        reserveCache.currLiquidityRate,
        reserveCache.reserveLastUpdateTimestamp
    );
    
    reserve.liquidityIndex = uint128(
        reserveCache.currLiquidityIndex.rayMul(cumulatedLiquidityInterest)
    );
}

// MathUtils.sol
function calculateLinearInterest(uint256 rate, uint40 lastUpdateTimestamp)
    internal view returns (uint256)
{
    uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
    return (rate * timeDifference) / SECONDS_PER_YEAR + WadRayMath.RAY;
}

Borrow Index Update¶

Files:

  • contracts/protocol/libraries/logic/ReserveLogic.sol:_updateIndexes()

  • contracts/protocol/libraries/math/MathUtils.sol:calculateCompoundedInterest()

Transformation: Same pattern as liquidity index, but uses compounded interest

Solidity:

if (reserveCache.currScaledVariableDebt != 0) {
    uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(
        reserveCache.currVariableBorrowRate,
        reserveCache.reserveLastUpdateTimestamp
    );
    
    reserve.variableBorrowIndex = uint128(
        reserveCache.currVariableBorrowIndex.rayMul(cumulatedVariableBorrowInterest)
    );
}

// MathUtils.sol - Uses Taylor series approximation
function calculateCompoundedInterest(uint256 rate, uint40 lastUpdateTimestamp)
    internal view returns (uint256)
{
    uint256 exp = block.timestamp - uint256(lastUpdateTimestamp);
    if (exp == 0) return WadRayMath.RAY;
    
    uint256 expMinusOne = exp - 1;
    uint256 expMinusTwo = exp > 2 ? exp - 2 : 0;
    
    uint256 ratePerSecond = rate / SECONDS_PER_YEAR;
    
    uint256 basePowerTwo = ratePerSecond.rayMul(ratePerSecond);
    uint256 basePowerThree = basePowerTwo.rayMul(ratePerSecond);
    
    uint256 secondTerm = (exp * expMinusOne * basePowerTwo) / 2;
    uint256 thirdTerm = (exp * expMinusOne * expMinusTwo * basePowerThree) / 6;
    
    return WadRayMath.RAY + (ratePerSecond * exp) + secondTerm + thirdTerm;
}

Used in flows: All flows (via ReserveLogic.updateState)


Treasury Accrual¶

Files:

  • contracts/protocol/libraries/logic/ReserveLogic.sol:_accrueToTreasury()

  • contracts/protocol/libraries/logic/PoolLogic.sol:executeMintToTreasury()

Transformation:

scaledDebtIncrease = newScaledDebt - oldScaledDebt
    ↓
debtAccruedScaled = scaledDebtIncrease.rayMul(reserveFactor)
    ↓
reserve.accruedToTreasury += debtAccruedScaled
    ↓
... (later, during mintToTreasury) ...
    ↓
amountToMint = accruedToTreasury.rayMul(liquidityIndex)
    ↓
scaledAmount = amountToMint.rayDiv(liquidityIndex)

Solidity:

// ReserveLogic.sol:_accrueToTreasury()
if (reserveCache.reserveFactor > 0) {
    uint256 scaledTotalDebt = IVariableDebtToken(
        reserveCache.variableDebtTokenAddress
    ).scaledTotalSupply();
    
    uint256 nextScaledVariableDebt = scaledTotalDebt;
    uint256 currScaledVariableDebt = reserveCache.currScaledVariableDebt;
    
    if (nextScaledVariableDebt > currScaledVariableDebt) {
        uint256 debtAccrued = (nextScaledVariableDebt - currScaledVariableDebt)
            .rayMul(reserveCache.reserveFactor);
        
        reserve.accruedToTreasury += uint128(debtAccrued);
    }
}

// PoolLogic.sol:executeMintToTreasury()
for (uint256 i = 0; i < assets.length; i++) {
    uint256 accruedToTreasury = reserve.accruedToTreasury;
    
    if (accruedToTreasury != 0) {
        uint256 amountToMint = accruedToTreasury.rayMul(
            reserveCache.nextLiquidityIndex
        );
        
        IAToken(reserveCache.aTokenAddress).mintToTreasury(
            amountToMint,
            reserveCache.nextLiquidityIndex
        );
        
        reserve.accruedToTreasury = 0;
    }
}

// AToken.sol:mintToTreasury()
uint256 scaledAmount = amount.rayDiv(index);
_mintScaled(address(this), scaledAmount, index);

Used in flows: All flows (via updateState)


Liquidation Calculations¶

Files:

  • contracts/protocol/libraries/logic/LiquidationLogic.sol:executeLiquidationCall()

  • contracts/protocol/libraries/logic/LiquidationLogic.sol:_calculateAvailableCollateralToLiquidate()

Transformation:

debtToCover (in debt asset decimals)
    ↓
Convert to collateral value:
collateralAmount = debtToCover
    .percentMul(100% + liquidationBonus)
    .wadToRay()
    .rayDiv(collateralPrice)
    ↓
Cap at available collateral:
if (collateralAmount > maxCollateral):
    recalculate debtToCover
    ↓
Calculate protocol fee:
fee = (collateralAmount - debtValue)
    .percentMul(liquidationProtocolFee)

Solidity:

// executeLiquidationCall()
uint256 collateralPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(
    params.collateralAsset
);
uint256 debtAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(
    params.debtAsset
);

vars.closeFactor = userConfig.isUsingAsCollateral(vars.debtReserve.id)
    ? DEFAULT_LIQUIDATION_CLOSE_FACTOR
    : MAX_LIQUIDATION_CLOSE_FACTOR;

(
    vars.actualDebtToLiquidate,
    vars.actualCollateralToLiquidate,
    vars.liquidationProtocolFeeAmount
) = _calculateAvailableCollateralToLiquidate(
    collateralReserve,
    debtReserve,
    collateralAssetPrice,
    debtAssetPrice,
    vars.actualDebtToLiquidate,
    vars.userCollateralBalance,
    liquidationBonus
);

// _calculateAvailableCollateralToLiquidate()
function _calculateAvailableCollateralToLiquidate(
    DataTypes.ReserveData storage collateralReserve,
    DataTypes.ReserveData storage debtReserve,
    uint256 collateralAssetPrice,
    uint256 debtAssetPrice,
    uint256 debtToCover,
    uint256 userCollateralBalance,
    uint256 liquidationBonus
) internal view returns (uint256, uint256, uint256) {
    uint256 collateralAmount = debtToCover
        .percentMul(PercentageMath.PERCENTAGE_FACTOR + liquidationBonus)
        .wadToRay()
        .rayDiv(collateralAssetPrice);
    
    uint256 maxCollateralToLiquidate = userCollateralBalance.rayMul(
        collateralReserve.liquidityIndex
    );
    
    if (collateralAmount > maxCollateralToLiquidate) {
        collateralAmount = maxCollateralToLiquidate;
        debtToCover = collateralAmount
            .rayMul(collateralAssetPrice)
            .rayToWad()
            .percentDiv(PercentageMath.PERCENTAGE_FACTOR + liquidationBonus);
    }
    
    uint256 liquidationProtocolFee = collateralReserve.configuration
        .getLiquidationProtocolFee();
    
    uint256 liquidationProtocolFeeAmount = liquidationProtocolFee > 0
        ? (collateralAmount - debtToCover.rayMul(debtAssetPrice).rayToWad())
            .percentMul(liquidationProtocolFee)
        : 0;
    
    return (
        debtToCover,
        collateralAmount - liquidationProtocolFeeAmount,
        liquidationProtocolFeeAmount
    );
}

Used in flows: Liquidation


Flash Loan Premiums¶

Files:

  • contracts/protocol/libraries/logic/FlashLoanLogic.sol:executeFlashLoan()

  • contracts/protocol/libraries/logic/FlashLoanLogic.sol:_handleFlashLoanRepayment()

Transformation:

Flash loan amount: 1000 USDC
    ↓
Calculate premiums:
totalPremium = 1000 * 0.09% = 0.9 USDC
protocolPremium = 1000 * 0.03% = 0.3 USDC
lpPremium = 0.9 - 0.3 = 0.6 USDC
    ↓
Repayment required: 1000 + 0.9 = 1000.9 USDC
    ↓
Treasury accrual (scaled):
treasuryAccrued += 0.3.rayDiv(liquidityIndex)
    ↓
LP distribution (index increase):
newIndex = (oldIndex * totalLiquidity + lpPremium) / totalLiquidity
    ↓
All aToken holders benefit via increased index

Solidity:

// executeFlashLoan()
uint256 totalPremium = amount.percentMul(vars.flashLoanPremiumTotal);
uint256 protocolPremium = amount.percentMul(vars.flashLoanPremiumToProtocol);

IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(
    params.receiverAddress,
    amount
);

require(
    IFlashLoanReceiver(params.receiverAddress).executeOperation(
        params.assets,
        params.amounts,
        premiums,
        msg.sender,
        params.params
    ),
    Errors.INVALID_FLASH_LOAN_EXECUTOR_RETURN
);

_handleFlashLoanRepayment(
    reserve,
    reserveCache,
    params.assets[i],
    params.amounts[i],
    totalPremium,
    protocolPremium
);

// _handleFlashLoanRepayment()
function _handleFlashLoanRepayment(
    DataTypes.ReserveData storage reserve,
    DataTypes.ReserveCache memory reserveCache,
    address asset,
    uint256 amount,
    uint256 premium,
    uint256 protocolPremium
) internal {
    uint256 amountPlusPremium = amount + premium;
    uint256 premiumToProtocol = protocolPremium;
    uint256 premiumToLP = premium - premiumToProtocol;
    
    IERC20(asset).safeTransferFrom(
        msg.sender,
        reserveCache.aTokenAddress,
        amountPlusPremium
    );
    
    reserve.accruedToTreasury += uint128(
        premiumToProtocol.rayDiv(reserveCache.nextLiquidityIndex)
    );
    
    _cumulateToLiquidityIndex(
        reserve,
        reserveCache,
        amount + premium,
        premiumToLP
    );
}

// _cumulateToLiquidityIndex()
function _cumulateToLiquidityIndex(
    DataTypes.ReserveData storage reserve,
    DataTypes.ReserveCache memory reserveCache,
    uint256 totalLiquidity,
    uint256 amount
) internal {
    uint256 liquidityIndex = reserveCache.nextLiquidityIndex;
    uint256 newLiquidityIndex = (liquidityIndex.rayMul(
        totalLiquidity.wadToRay()
    ) + amount.wadToRay()).rayDiv(totalLiquidity.wadToRay());
    
    reserve.liquidityIndex = uint128(newLiquidityIndex);
}

Used in flows: Flash Loan


E-Mode Calculations¶

File: contracts/protocol/libraries/logic/GenericLogic.sol:calculateUserAccountData()

Transformation:

For each collateral asset:
    scaledBalance = _scaledBalance[user]
    unscaledBalance = scaledBalance.rayMul(liquidityIndex)
    
    // E-Mode price override
    price = eMode.active && assetInEMode
        ? eMode.priceSource  // Correlated asset price
        : oraclePrice
    
    valueInETH = unscaledBalance
        .wadToRay()
        .rayMul(price)
        .rayToWad()
    
    totalCollateral += valueInETH

For each debt asset:
    scaledDebt = _scaledBalance[user]
    unscaledDebt = scaledDebt.rayMul(borrowIndex)
    valueInETH = unscaledDebt.wadToRay().rayMul(price).rayToWad()
    totalDebt += valueInETH

// Calculate health factor
healthFactor = totalCollateral.percentMul(liquidationThreshold).wadDiv(totalDebt)

Solidity:

for (uint256 i = 0; i < reservesDataCount; i++) {
    if (!userConfig.isUsingAsCollateralOrBorrowing(i)) continue;
    
    DataTypes.ReserveData memory reserve = reservesData[reservesList[i]];
    DataTypes.ReserveCache memory reserveCache = _cache(reserve);
    
    uint256 reserveUnitPrice = IPriceOracleGetter(params.oracle).getAssetPrice(
        address(reserveCache.aTokenAddress)
    );
    
    if (userConfig.isUsingAsCollateral(i)) {
        uint256 assetUnitPrice = (params.eModeCategory.priceSource != address(0) &&
            params.eModeCategory.assets[reservesList[i]])
            ? IPriceOracleGetter(params.oracle).getAssetPrice(
                params.eModeCategory.priceSource
            )
            : reserveUnitPrice;
        
        uint256 liquidityBalance = IERC20(reserveCache.aTokenAddress)
            .scaledBalanceOf(params.user)
            .rayMul(reserveCache.nextLiquidityIndex);
        
        uint256 liquidityBalanceETH = liquidityBalance
            .wadToRay()
            .rayMul(assetUnitPrice)
            .rayToWad();
        
        totalCollateralInBaseCurrency += liquidityBalanceETH;
        
        if (params.eModeCategory.priceSource != address(0) &&
            params.eModeCategory.assets[reservesList[i]]) {
            avgLtv = params.eModeCategory.ltv;
            avgLiquidationThreshold = params.eModeCategory.liquidationThreshold;
        } else {
            avgLtv += liquidityBalanceETH * reserveCache.ltv;
            avgLiquidationThreshold += liquidityBalanceETH * reserveCache.liquidationThreshold;
        }
    }
    
    if (userConfig.isBorrowing(i)) {
        uint256 borrowBalance = IERC20(reserveCache.variableDebtTokenAddress)
            .scaledBalanceOf(params.user)
            .rayMul(reserveCache.nextVariableBorrowIndex);
        
        uint256 borrowBalanceETH = borrowBalance
            .wadToRay()
            .rayMul(reserveUnitPrice)
            .rayToWad();
        
        totalDebtInBaseCurrency += borrowBalanceETH;
    }
}

vars.healthFactor = totalDebtInBaseCurrency > 0
    ? totalCollateralInBaseCurrency
        .percentMul(avgLiquidationThreshold)
        .wadDiv(totalDebtInBaseCurrency)
    : type(uint256).max;

Used in flows: All flows (health factor validation), E-Mode Management


Version Differences¶

Operation

Pool contract v1-3

Pool contract v4+

Reason

AToken Mint

rayDiv (half-up)

rayDivFloor

Prevent rounding up debt

AToken Burn

rayDiv (half-up)

rayDivCeil

Ensure full repayment

VToken Mint

rayDiv (half-up)

rayDivCeil

Round up debt issued

VToken Burn

rayDiv (half-up)

rayDivFloor

Round down debt reduction

Transfer

rayDiv (half-up)

rayDivFloor

Floor for safety

Check version in Solidity:

// In Pool.sol constructor
uint8 poolRevision;

// Mainnet deployments
// v3.0.2: 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2
// v3.1.0: Same proxy, new implementation
// v3.2.0: Same proxy, new implementation

Debugging tip: v1-3 vs v4+ can have up to 1 wei difference per operation due to rounding changes.