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
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 |
|
|
Prevent rounding up debt |
AToken Burn |
|
|
Ensure full repayment |
VToken Mint |
|
|
Round up debt issued |
VToken Burn |
|
|
Round down debt reduction |
Transfer |
|
|
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.