# AAVE V3 Code Review
- Protocol 概要
- AAVE 是一個去中心化借貸平台,使用者存入資產可以獲得利息並作為抵押品貸出其他資產。
- 存入資產會獲得利息代幣 aToken 作為憑證,領出時提供 aToken 即可領出資產。
- aToken 和資產的比例以 index 換算。隨著利息累積,index 增加,aToken 可以換出更多資產。
- 貸出資產需要支付利息,並獲得不可轉移的 debtToken。
- loan to value (ltv) 決定可以貸出上限,通常是抵押價值的 60%-80%
- 當價格波動讓 loan to value 發生改變,超過 liquidation threshold 時,就會發生清算。
- 任何人都可以清算,清算獲得清算獎勵。
- 協議的收入是借貸的利差,reserve factor
- EMode 同類型的資產可以提高抵押率,提高資本效率,EX: 抵押 DAI 借 USDT
- protocol
- libraries
- configuration
- ReserveConfiguration.sol # 修改 reserve 的 index, rate 等紀錄的 function
- UserConfiguration.sol # 修改使用者借貸資產的紀錄 function
- logic
- BorrowLogic.sol
- EModeLogic.sol // 4
- Generic.sol // 3
- LiquidationLogic.sol // 6
- PoolLogic.sol // 2
- ReserveLogic.sol // 1
- SupplyLogic.sol
- ValidationLogic.sol // 5
- types
- DataTypes.sol // 定義資料結構
- pool
- Pool.sol # 借貸、清算等功能
- PoolStorage.sol # 紀錄每個使用者和 reserve 資訊
- PoolConfigurator.sol # 修改 reserve 參數
- tokenization # 利息代幣、債券貸幣
- base
- ScaledBalanceTokenBase.sol # index 轉換利息代幣和資產
- AToken.sol
- VariableDebtToken.sol
- 順序:
- AToken、DebtToken、InterestRateModel
- Pool Storage
- DataTypes
- UserConfiguration&ReserveConfiguration
- Logic
- AToken:
- 轉移利息代幣、underlying asset、轉移貸幣給國庫
- 清算時的強制轉移
- 不涉及 借款、贖回邏輯
```solidity=
/// @inheritdoc IAToken
function mint(
address caller,
address onBehalfOf,
uint256 amount,
uint256 index
) external virtual override onlyPool returns (bool) {
return _mintScaled(caller, onBehalfOf, amount, index);
}
/// @inheritdoc IAToken
function transferOnLiquidation(
address from,
address to,
uint256 value
) external override onlyPool {
// Being a normal transfer, the Transfer() and BalanceTransfer() are emitted
// so no need to emit a specific event here
_transfer(from, to, value, false);
emit Transfer(from, to, value);
}
```
- DebtToken <- DebtTokenBase + ScaledBalanceTokenBase
- balanceOf 是 underlying 的數量,super.balanceOf 才是債券代幣數量
- 不涉及 貸款、還款邏輯
```solidity=
function transferFrom(
address,
address,
uint256
) external virtual override returns (bool) {
revert(Errors.OPERATION_NOT_SUPPORTED);
}
```
- IntersetRateModel
- 兩階段線性模型,超過 OPTIMAL_USAGE_RATIO 則提高斜率
- 分為 variableDebt 和 stableDebt
- 更新 currentLiquidityRate
![](https://i.imgur.com/7dzoQIa.png)
```solidity=
/**
* @dev Calculates the overall borrow rate as the weighted average between the total variable debt and total stable
* debt
* @param totalStableDebt The total borrowed from the reserve at a stable rate
* @param totalVariableDebt The total borrowed from the reserve at a variable rate
* @param currentVariableBorrowRate The current variable borrow rate of the reserve
* @param currentAverageStableBorrowRate The current weighted average of all the stable rate loans
* @return The weighted averaged borrow rate
**/
// liquidityRate = _getOverallBorrowRate()
// 考慮 variable debt 和 stable debt 的平均報酬
function _getOverallBorrowRate(
uint256 totalStableDebt,
uint256 totalVariableDebt,
uint256 currentVariableBorrowRate,
uint256 currentAverageStableBorrowRate
) internal pure returns (uint256) {
uint256 totalDebt = totalStableDebt + totalVariableDebt;
if (totalDebt == 0) return 0;
uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul(currentVariableBorrowRate);
uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul(currentAverageStableBorrowRate);
uint256 overallBorrowRate = (weightedVariableRate + weightedStableRate).rayDiv(
totalDebt.wadToRay()
);
return overallBorrowRate;
}
}
```
# PoolStorage: 幫 Pool 管理 reserve 和 user 資訊的地方
- _reserves: reserveAddress -> reserveData # 資金池資訊
- _usersConfig: user -> userConfigData # 使用者資訊
- _reservesList: reserveId -> reserveAddress # 資金池建立索引
- _eModeCategories: eModeId -> eModeCategory # 同類資產被歸類在同一個 eModeCatetory
- _usersEModeCategory: user -> eModeId # 使用者對應的 eMode ID
# DataTypes
- Data
```solidity=
struct ReserveData {
//stores the reserve configuration
ReserveConfigurationMap configuration;
//the liquidity index. Expressed in ray
uint128 liquidityIndex;
//the current supply rate. Expressed in ray
uint128 currentLiquidityRate;
//variable borrow index. Expressed in ray
uint128 variableBorrowIndex;
//the current variable borrow rate. Expressed in ray
uint128 currentVariableBorrowRate;
//the current stable borrow rate. Expressed in ray
uint128 currentStableBorrowRate;
//timestamp of last update
uint40 lastUpdateTimestamp;
//the id of the reserve. Represents the position in the list of the active reserves
uint16 id;
//aToken address
address aTokenAddress;
//stableDebtToken address
address stableDebtTokenAddress;
//variableDebtToken address
address variableDebtTokenAddress;
//address of the interest rate strategy
address interestRateStrategyAddress;
//the current treasury balance, scaled
uint128 accruedToTreasury;
//the outstanding unbacked aTokens minted through the bridging feature
uint128 unbacked;
//the outstanding debt borrowed against this asset in isolation mode
uint128 isolationModeTotalDebt;
}
struct ReserveConfigurationMap {
//bit 0-15: LTV
//bit 16-31: Liq. threshold
//bit 32-47: Liq. bonus
//bit 48-55: Decimals
//bit 56: reserve is active
//bit 57: reserve is frozen
//bit 58: borrowing is enabled
//bit 59: stable rate borrowing enabled
//bit 60: asset is paused
//bit 61: borrowing in isolation mode is enabled
//bit 62-63: reserved
//bit 64-79: reserve factor
//bit 80-115 borrow cap in whole tokens, borrowCap == 0 => no cap
//bit 116-151 supply cap in whole tokens, supplyCap == 0 => no cap
//bit 152-167 liquidation protocol fee
//bit 168-175 eMode category
//bit 176-211 unbacked mint cap in whole tokens, unbackedMintCap == 0 => minting disabled
//bit 212-251 debt ceiling for isolation mode with (ReserveConfiguration::DEBT_CEILING_DECIMALS) decimals
//bit 252-255 unused
uint256 data;
// 01000101111100...
}
struct UserConfigurationMap {
/**
* @dev Bitmap of the users collaterals and borrows. It is divided in pairs of bits, one pair per asset.
* The first bit indicates if an asset is used as collateral by the user, the second whether an
* asset is borrowed by the user.
*/
uint256 data;
// 01010101...
}
struct EModeCategory {
// each eMode category has a custom ltv and liquidation threshold
uint16 ltv;
uint16 liquidationThreshold;
uint16 liquidationBonus;
// each eMode category may or may not have a custom oracle to override the individual assets price oracles
address priceSource;
string label;
}
enum InterestRateMode {
NONE,
STABLE,
VARIABLE
}
struct ReserveCache {
uint256 currScaledVariableDebt;
uint256 nextScaledVariableDebt;
uint256 currPrincipalStableDebt;
uint256 currAvgStableBorrowRate;
uint256 currTotalStableDebt;
uint256 nextAvgStableBorrowRate;
uint256 nextTotalStableDebt;
uint256 currLiquidityIndex;
uint256 nextLiquidityIndex;
uint256 currVariableBorrowIndex;
uint256 nextVariableBorrowIndex;
uint256 currLiquidityRate;
uint256 currVariableBorrowRate;
uint256 reserveFactor;
ReserveConfigurationMap reserveConfiguration;
address aTokenAddress;
address stableDebtTokenAddress;
address variableDebtTokenAddress;
uint40 reserveLastUpdateTimestamp;
uint40 stableDebtLastUpdateTimestamp;
}
```
- UserConfiguration&ReserveConfiguration: 更改 reserve、User 的狀態,都是用 bit 處理
- UserConfigurationMap 存 user 對每一個 asset 的使用狀態,分為 collateral 和 borrow
ex: (0,1) 代表 有 borrow 沒有 collateral。
- 主要功能
- isUsingAsCollateralOrBorrowing
- isBorrowing
- isUsingAsCollateral
- isUsingAsCollateralOne
- isUsingAsCollateralAny
- ...
- reserveIndex 紀錄 這個 pair 在 bit 裡的位置。
```solidity=
uint256 bit = 1 << (reserveIndex << 1) // setUsingAsCollateral
uint256 bit = 1 << ((reserveIndex << 1) + 1) // setBorrowing
```
index = 3
00000001 原本
01000000 左移 6 位,第三個 pair 的 borrow 狀態
10000000 左移 7 位,第三個 pair 的 collateral 狀態
- UserConfigurationMask 是只取 borrow 或 collateral 其中一個的紀錄
> 0x5555... = 01010101...
> 0xAAAA... = 10101010...
- 檢查、賦值得運算都是用 <<、>>、|、& 完成
- n & (n-1) == 0 用在是否只使用一種資產。
```solidity=
/**
* @notice Validate a user has been using the reserve for borrowing
* @param self The configuration object
* @param reserveIndex The index of the reserve in the bitmap
* @return True if the user has been using a reserve for borrowing, false otherwise
**/
function isBorrowing(DataTypes.UserConfigurationMap memory self, uint256 reserveIndex)
internal
pure
returns (bool)
{
unchecked {
require(reserveIndex < ReserveConfiguration.MAX_RESERVES_COUNT, Errors.INVALID_RESERVE_INDEX);
return (self.data >> (reserveIndex << 1)) & 1 != 0;
}
}
/**
* @notice Checks if a user has been supplying only one reserve as collateral
* @dev this uses a simple trick - if a number is a power of two (only one bit set) then n & (n - 1) == 0
* @param self The configuration object
* @return True if the user has been supplying as collateral one reserve, false otherwise
**/
function isUsingAsCollateralOne(DataTypes.UserConfigurationMap memory self)
internal
pure
returns (bool)
{
uint256 collateralData = self.data & COLLATERAL_MASK;
return collateralData != 0 && (collateralData & (collateralData - 1) == 0);
}
```
case 1
100000 = collateralData
011111 = collateralData - 1
000000 = collateralData & (collateralData - 1)
case 2
101010 = collateralData
101001 = collateralData - 1
111100 = collateralData & (collateralData - 1)
- Params
- ExecuteLiquidationCallParams
- ExecuteSupplyParams
- ExecuteBorrowParams
- ExecuteRepayParams
- ExecuteWithdrawParams
- ExecuteSetUserEModeParams
- FinalizeTransferParams
- CalculateUserAccountDataParams
- ValidateBorrowParams
- ValidateLiquidationCallParams
- CalculateInterestRatesParams
- InitReserveParams
# ReserveLogic
Reserve 是 token 的借貸池,每一個 token 都會有人借入和貸出,由 Pool 管理眾多 reserve。Reverse 需要紀錄利率、債務、利息收入等資訊,並分配項目收入至 treasury。Reserve 的狀態被記錄在 ReserveData 內,在狀態變更時會產生 ReserveCache 提供緩存。
```solidity=
/**
* @notice Initializes a reserve.
* @param reserve The reserve object
* @param aTokenAddress The address of the overlying atoken contract
* @param stableDebtTokenAddress The address of the overlying stable debt token contract
* @param variableDebtTokenAddress The address of the overlying variable debt token contract
* @param interestRateStrategyAddress The address of the interest rate strategy contract
**/
// 初始化
function init(
DataTypes.ReserveData storage reserve,
address aTokenAddress,
address stableDebtTokenAddress,
address variableDebtTokenAddress,
address interestRateStrategyAddress
) internal {
require(reserve.aTokenAddress == address(0), Errors.RESERVE_ALREADY_INITIALIZED);
reserve.liquidityIndex = uint128(WadRayMath.RAY);
reserve.variableBorrowIndex = uint128(WadRayMath.RAY);
reserve.aTokenAddress = aTokenAddress;
reserve.stableDebtTokenAddress = stableDebtTokenAddress;
reserve.variableDebtTokenAddress = variableDebtTokenAddress;
reserve.interestRateStrategyAddress = interestRateStrategyAddress;
}
// 收入用單利計算
function getNormalizedIncome(DataTypes.ReserveData storage reserve)
internal
view
returns (uint256)
{
...
return
MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul(
reserve.liquidityIndex
...
}
// 債務用複利計算
// 差值到三階微分
// (1+x)^n = 1+n*x+[n/2*(n-1)]*x^2+[n/6*(n-1)*(n-2)*x^3...
function getNormalizedDebt(DataTypes.ReserveData storage reserve)
internal
view
returns (uint256)
{
...
return
MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul(
reserve.variableBorrowIndex
...
}
// 執行借貸功能前的第一步驟,一定要先更新 reserve 狀態
function updateState(
DataTypes.ReserveData storage reserve,
DataTypes.ReserveCache memory reserveCache
) internal {
_updateIndexes(reserve, reserveCache);
_accrueToTreasury(reserve, reserveCache);
}
// 依據 liquidity 加入和拿走的量計算新的 liquidity rate / borrow rate 等等。
function updateInterestRates(
DataTypes.ReserveData storage reserve,
DataTypes.ReserveCache memory reserveCache,
address reserveAddress,
uint256 liquidityAdded,
uint256 liquidityTaken
) internal {
UpdateInterestRatesLocalVars memory vars;
...
(
vars.nextLiquidityRate,
vars.nextStableRate,
vars.nextVariableRate
) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).calculateInterestRates(
DataTypes.CalculateInterestRatesParams({
...
liquidityAdded: liquidityAdded,
liquidityTaken: liquidityTaken,
...
})
);
reserve.currentLiquidityRate = vars.nextLiquidityRate.toUint128();
reserve.currentStableBorrowRate = vars.nextStableRate.toUint128();
reserve.currentVariableBorrowRate = vars.nextVariableRate.toUint128();
...
}
// reserveFactor 是 項目方的抽成比例,計算方式是把債務利息累積的量乘上 factor 加進 accruedToMint
function _accrueToTreasury(
DataTypes.ReserveData storage reserve,
DataTypes.ReserveCache memory reserveCache
) internal {
AccrueToTreasuryLocalVars memory vars;
...
//debt accrued is the sum of the current debt minus the sum of the debt at the last update
vars.totalDebtAccrued =
vars.currTotalVariableDebt +
reserveCache.currTotalStableDebt -
vars.prevTotalVariableDebt -
vars.prevTotalStableDebt;
vars.amountToMint = vars.totalDebtAccrued.percentMul(reserveCache.reserveFactor);
if (vars.amountToMint != 0) {
reserve.accruedToTreasury += vars
.amountToMint
.rayDiv(reserveCache.nextLiquidityIndex)
.toUint128();
}
}
// 根據 liquidityRate 和 variableBorrowRate 更新 index 和 timestamp
// 有利息或債務變動才更新 reserve
function _updateIndexes(
DataTypes.ReserveData storage reserve,
DataTypes.ReserveCache memory reserveCache
) internal {
...
//only cumulating if there is any income being produced
if (reserveCache.currLiquidityRate != 0) {
uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(
reserveCache.currLiquidityRate,
reserveCache.reserveLastUpdateTimestamp
);
reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul(
reserveCache.currLiquidityIndex
);
reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128();
//as the liquidity rate might come only from stable rate loans, we need to ensure
//that there is actual variable debt before accumulating
if (reserveCache.currScaledVariableDebt != 0) {
...
// 一樣的動作,利用 reserveCache.currVariableBorrowRate 更新 reserve.variableBorrowIndex
}
//solium-disable-next-line
reserve.lastUpdateTimestamp = uint40(block.timestamp);
}
/**
* @notice Creates a cache object to avoid repeated storage reads and external contract calls when updating state and
* interest rates.
* @param reserve The reserve object for which the cache will be filled
* @return The cache object
*/
// 複製一份 reserve data 避免對 reserve 做出改動也減少直接讀取全域變數的 gas fee
function cache(DataTypes.ReserveData storage reserve)
internal
view
returns (DataTypes.ReserveCache memory)
{
DataTypes.ReserveCache memory reserveCache;
// ReserveCache.XXX = reserve.XXX
return reserveCache;
}
}
```
# PoolLogic
管理、創建 Reserve。
```solidity=
function executeInitReserve(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.InitReserveParams memory params
) external returns (bool) {
require(Address.isContract(params.asset), Errors.NOT_CONTRACT);
// 如果 reservesData[params.asset] 已經被初始化過,此功能會失敗
reservesData[params.asset].init(
params.aTokenAddress,
params.stableDebtAddress,
params.variableDebtAddress,
params.interestRateStrategyAddress
);
...
// 紀錄 ID
reservesData[params.asset].id = params.reservesCount;
reservesList[params.reservesCount] = params.asset;
return true;
}
/**
* @notice Mints the assets accrued through the reserve factor to the treasury in the form of aTokens
* @param reservesData The state of all the reserves
* @param assets The list of reserves for which the minting needs to be executed
**/
function executeMintToTreasury(
mapping(address => DataTypes.ReserveData) storage reservesData,
address[] calldata assets
) external {
for (uint256 i = 0; i < assets.length; i++) {
address assetAddress = assets[i];
DataTypes.ReserveData storage reserve = reservesData[assetAddress];
...
// 計算 treasury 的量
uint256 accruedToTreasury = reserve.accruedToTreasury;
if (accruedToTreasury != 0) {
reserve.accruedToTreasury = 0; // 重設
uint256 normalizedIncome = reserve.getNormalizedIncome(); // 理論 index,沒有更新狀態
uint256 amountToMint = accruedToTreasury.rayMul(normalizedIncome); // debt 在經過上次更新時候成長的收入??
IAToken(reserve.aTokenAddress).mintToTreasury(amountToMint, normalizedIncome); // 鑄造 aToken,amountToMint 是 underlying amount,normalizedIncome 是 index
}
}
}
/**
* @notice Drop a reserve
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param asset The address of the underlying asset of the reserve
**/
function executeDropReserve(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
address asset
) external {
DataTypes.ReserveData storage reserve = reservesData[asset];
...
// validation
// 確定資產清空
...
reservesList[reservesData[asset].id] = address(0);
delete reservesData[asset];
}
```
# GenericLogic
```solidity=
struct CalculateUserAccountDataVars {
uint256 assetPrice;
uint256 assetUnit;
uint256 userBalanceInBaseCurrency;
uint256 decimals;
uint256 ltv;
uint256 liquidationThreshold;
uint256 i;
uint256 healthFactor; // 會回傳
uint256 totalCollateralInBaseCurrency; // 會回傳
uint256 totalDebtInBaseCurrency; // 會回傳
uint256 avgLtv; // 會回傳
uint256 avgLiquidationThreshold; // 會回傳
uint256 eModeAssetPrice;
uint256 eModeLtv;
uint256 eModeLiqThreshold;
uint256 eModeAssetCategory;
address currentReserveAddress;
bool hasZeroLtvCollateral; // 會回傳
bool isInEModeCategory;
}
/**
* @notice Calculates the user data across the reserves.
* @dev It includes the total liquidity/collateral/borrow balances in the base currency used by the price feed,
* the average Loan To Value, the average Liquidation Ratio, and the Health factor.
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param eModeCategories The configuration of all the efficiency mode categories
* @param params Additional parameters needed for the calculation
* @return The total collateral of the user in the base currency used by the price feed
* @return The total debt of the user in the base currency used by the price feed
* @return The average ltv of the user
* @return The average liquidation threshold of the user
* @return The health factor of the user
* @return True if the ltv is zero, false otherwise
**/
function calculateUserAccountData(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.CalculateUserAccountDataParams memory params
)
internal
view
returns (
uint256,
uint256,
uint256,
uint256,
uint256,
bool
)
{
...
CalculateUserAccountDataVars memory vars;
// 確認是否有 EMode,有則取得相關資訊
if (params.userEModeCategory != 0) {
(vars.eModeLtv, vars.eModeLiqThreshold, vars.eModeAssetPrice) = EModeLogic(...)
}
// 掃過所有的 reserve
while (vars.i < params.reservesCount) {
...
vars.currentReserveAddress = reservesList[vars.i];
...
// 取得現在的 reserveData
DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress];
(
vars.ltv,
vars.liquidationThreshold,
,
vars.decimals,
,
vars.eModeAssetCategory
) = currentReserve.configuration.getParams(); // 取得參數
// decimals 決定最小 unit
unchecked {
vars.assetUnit = 10**vars.decimals;
}
// price 的來源有兩個,一個是 eMode 一個是一般的 price oracle
vars.assetPrice = vars.eModeAssetPrice != 0 &&
params.userEModeCategory == vars.eModeAssetCategory
? vars.eModeAssetPrice
: IPriceOracleGetter(params.oracle).getAssetPrice(vars.currentReserveAddress);
// 是否 用作抵押品 & 是否有 清算上限
if (vars.liquidationThreshold != 0 && params.userConfig.isUsingAsCollateral(vars.i)) {
// 取得分開的 抵押價值
vars.userBalanceInBaseCurrency = _getUserBalanceInBaseCurrency(
...
);
vars.totalCollateralInBaseCurrency += vars.userBalanceInBaseCurrency;
// 使用者目前是否在 eMode 內
vars.isInEModeCategory = EModeLogic.isInEModeCategory(
...
);
// 總和 loan to value
if (vars.ltv != 0) {
vars.avgLtv +=
vars.userBalanceInBaseCurrency *
(vars.isInEModeCategory ? vars.eModeLtv : vars.ltv);
} else {
vars.hasZeroLtvCollateral = true;
}
// 總和 清算上限
vars.avgLiquidationThreshold +=
vars.userBalanceInBaseCurrency *
(vars.isInEModeCategory ? vars.eModeLiqThreshold : vars.liquidationThreshold);
}
// 總和 債務
if (params.userConfig.isBorrowing(vars.i)) {
vars.totalDebtInBaseCurrency += _getUserDebtInBaseCurrency(
...
);
}
unchecked {
++vars.i;
}
}
// 總和 / 總抵押價值 = 平均
unchecked {
vars.avgLtv = vars.totalCollateralInBaseCurrency != 0
? vars.avgLtv / vars.totalCollateralInBaseCurrency
: 0;
vars.avgLiquidationThreshold = vars.totalCollateralInBaseCurrency != 0
? vars.avgLiquidationThreshold / vars.totalCollateralInBaseCurrency
: 0;
}
// healthFactor = 總清算上限 / 總債務; 大於 1 才不會被清算
vars.healthFactor = (vars.totalDebtInBaseCurrency == 0)
? type(uint256).max
: (vars.totalCollateralInBaseCurrency.percentMul(vars.avgLiquidationThreshold)).wadDiv(
vars.totalDebtInBaseCurrency
);
return (
vars.totalCollateralInBaseCurrency,
vars.totalDebtInBaseCurrency,
vars.avgLtv,
vars.avgLiquidationThreshold,
vars.healthFactor,
vars.hasZeroLtvCollateral // True if the ltv is zero, false otherwise
);
}
```
# EModeLogic
```solidity=
/**
* @notice Updates the user efficiency mode category
* @dev Will revert if user is borrowing non-compatible asset or change will drop HF < HEALTH_FACTOR_LIQUIDATION_THRESHOLD
* @dev Emits the `UserEModeSet` event
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param eModeCategories The configuration of all the efficiency mode categories
* @param usersEModeCategory The state of all users efficiency mode category
* @param userConfig The user configuration mapping that tracks the supplied/borrowed assets
* @param params The additional parameters needed to execute the setUserEMode function
*/
function executeSetUserEMode(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
mapping(address => uint8) storage usersEModeCategory,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ExecuteSetUserEModeParams memory params
) external {
// validation 是否借貸同一個 category 類的資產
ValidationLogic.validateSetUserEMode(...);
// 設定新的 category ID
uint8 prevCategoryId = usersEModeCategory[msg.sender];
usersEModeCategory[msg.sender] = params.categoryId;
// 如果原本是 EMode 就要檢查, EMode 是比較寬鬆的狀態,由 EMode 進到不是代表有可能會超過清算標準,需要檢查
// 反之,則不用
if (prevCategoryId != 0) {
ValidationLogic.validateHealthFactor(...);
}
...
}
```
# ValidationLogic
針對 Protocol 內的所有行為進行檢查,檢查沒過會自動 revert transaction。
```solidity=
/**
* @notice Validates the action of setting efficiency mode.
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param eModeCategories a mapping storing configurations for all efficiency mode categories
* @param userConfig the user configuration
* @param reservesCount The total number of valid reserves
* @param categoryId The id of the category
**/
function validateSetUserEMode(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.UserConfigurationMap memory userConfig,
uint256 reservesCount,
uint8 categoryId
) internal view {
...
// if user is trying to set another category than default we require that
// either the user is not borrowing, or it's borrowing assets of categoryId
// 搜尋所有的 reserve ,確定只有貸出 category 內的 asset
if (categoryId != 0) {
unchecked {
for (uint256 i = 0; i < reservesCount; i++) {
if (userConfig.isBorrowing(i)) {
DataTypes.ReserveConfigurationMap memory configuration = reservesData[reservesList[i]]
.configuration;
require(
configuration.getEModeCategory() == categoryId,
Errors.INCONSISTENT_EMODE_CATEGORY
);
}
}
}
}
}
/**
* @notice Validates the health factor of a user.
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param eModeCategories The configuration of all the efficiency mode categories
* @param userConfig The state of the user for the specific reserve
* @param user The user to validate health factor of
* @param userEModeCategory The users active efficiency mode category
* @param reservesCount The number of available reserves
* @param oracle The price oracle
*/
function validateHealthFactor(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.UserConfigurationMap memory userConfig,
address user,
uint8 userEModeCategory,
uint256 reservesCount,
address oracle
) internal view returns (uint256, bool) {
(, , , , uint256 healthFactor, bool hasZeroLtvCollateral) = GenericLogic.calculateUserAccountData(...);
require(
healthFactor >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD, // HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18
Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD
);
return (healthFactor, hasZeroLtvCollateral);
}
```
# LiquidationLogic
- 清算的邏輯是貸款者抵押品不足以維持抵押率的時候,清算者可以清算。清算者以折扣價格購買貸款者的抵押品,折扣的比例隱含執行清算的獎勵。
- 我抵押 100 USD 的 ETH 拿到 aETH。
貸出 80 USDC 並拿到代表債權的 debtToken
- 抵押品價值價格下跌至 85,需要清算。
清算者把 debtToken 燒掉,並拿走我 85 USD 的 aETH 或 ETH。
之後支付 80 USDC 給 reserve。 5 美元的差額就是執行清算的收益。
- 我損失 85 USD 的 aETH,保留 80 USDC => -5 USD
清算者支付 80 USDC,獲得 85 USD 的 aETH/ETH => 5 USD
- 進階用法: 寫智能合約執行閃電貸,發現機會從 AAVE 借出 80 USDC 執行清算,拿到 ETH 馬上換成 USDC 還款,不需初始資金。
- validate health factor
- liquidate 有兩種方法
- 燒掉抵押者的利息代幣,liquidator 拿到 underlying asset
- 直接轉走抵押者的利息代幣到 liquidator 的地址
- 把抵押者的另一部分利息代幣轉移到 treasury
- _burnCollateralATokens: 燃燒抵押憑證,把抵押品強制轉移到 liquidator 手上
_liquidateATokens: 把抵押憑證強制轉移到 liquidator 手上。
_calculateAvailableCollateralToLiquidate: 用需要清算的 debt 計算需要動用的 collateral 數量,然後在加上 liquidationBonus。如果有 liquidationProtocolFeePercentage,會從 liquidator 的獎勵中扣除,轉給 treasury。
_burnDebtTokens: 檢查 variable debt 和 stable debt。先還 variable debt 的部分,不夠在用 stable debt。
```solidity=
/**
* @notice Function to liquidate a position if its Health Factor drops below 1. The caller (liquidator)
* covers `debtToCover` amount of debt of the user getting liquidated, and receives
* a proportional amount of the `collateralAsset` plus a bonus to cover market risk
* @dev Emits the `LiquidationCall()` event
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param usersConfig The users configuration mapping that track the supplied/borrowed assets
* @param eModeCategories The configuration of all the efficiency mode categories
* @param params The additional parameters needed to execute the liquidation function
**/
function executeLiquidationCall(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(address => DataTypes.UserConfigurationMap) storage usersConfig,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.ExecuteLiquidationCallParams memory params
) external {
...
// 計算 healthFactor,小於 1 代表可以清算
(, , , , vars.healthFactor, ) = GenericLogic.calculateUserAccountData(...);
// 計算需要被清算的債務
(vars.userVariableDebt, vars.userTotalDebt, vars.actualDebtToLiquidate) = _calculateDebt(...);
// validate
ValidationLogic.validateLiquidationCall(...);
// 取得價格來源、清算獎勵 by collateral
(
vars.collateralAToken,
vars.collateralPriceSource,
vars.debtPriceSource,
vars.liquidationBonus
) = _getConfigurationData(...);
// 使用者的抵押品數量
vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user);
// 計算清算的抵押品和債務數量
(
vars.actualCollateralToLiquidate,
vars.actualDebtToLiquidate,
vars.liquidationProtocolFeeAmount
) = _calculateAvailableCollateralToLiquidate(...);
// 全部清算則更改 borrow 狀態
if (vars.userTotalDebt == vars.actualDebtToLiquidate) {
userConfig.setBorrowing(debtReserve.id, false);
}
// 燃燒債務代幣
_burnDebtTokens(params, vars);
...
// 取得抵押品方式有種,一種是 aToken,一種是 underlying asset
if (params.receiveAToken) {
_liquidateATokens(reservesData, reservesList, usersConfig, collateralReserve, params, vars);
} else {
_burnCollateralATokens(collateralReserve, params, vars);
}
...
// 如果抵押品全部被清算,更改狀態
// If the collateral being liquidated is equal to the user balance,
// we set the currency as not being used as collateral anymore
if (vars.actualCollateralToLiquidate == vars.userCollateralBalance) {
userConfig.setUsingAsCollateral(collateralReserve.id, false);
...
}
// 清算者以折扣購買債權
// Transfers the debt asset being repaid to the aToken, where the liquidity is kept
IERC20(params.debtAsset).safeTransferFrom(
msg.sender,
vars.debtReserveCache.aTokenAddress,
vars.actualDebtToLiquidate
);
...
}
```
# SupplyLogic
- 進到簡單的部分,supply 的時候有兩個要檢查的,reserve 是不是開啟的狀態還是說已經到達 supply cap 上限。再來是能不能當作抵押品,如果是 isolation 就不能。
- 這裡不計算 supply 價值,單純給利息代幣而已
- withdraw 時,先檢查是否超過存入上限,之後燃燒利息代幣,再進行 health factor 的檢查。如果 withdraw 會造成 health factor 低於 1 ,交易會被逆轉。
- finalizeTransfer 看不到 transfer 的痕跡
# BorrowLogic
- borrow 時檢查 debt 是否超過 borrow cap,計算 collateral 是否足夠,
- 檢查完之後才 transferUnderlying 出去
- repay 時先燃燒 debtToken 然後 燃燒利息代幣或轉移 underlying asset
- 對使用 stableDebt 的人來說,可以進行轉倉,RebalanceStableBorrowRate
- 也可以進行 stableDebt 和 variableDebt 的互換
# FlashloadLogic
- 先借 underlying 出來,進行操作之後再還回去,如果資金不夠償還,可以建立 debt,但要確定 collateral 是否足夠。
# IsolationModeLogic
- 那入風險性資產時,不能使用其他資產同時抵押,只能借出特定資產
# 看完有幾個問題
- 為甚麼需要 stable debt
stable debt 是以更高額的固定利率進行借貸,可以免去利息的波動
- updateState 和 updateInterestRate 的順序有甚麼重要的
- updateState 是更新 index,也就是這段時間累積的利息
- updateInterestRate 是流動性改變之後需要更新的數字,攸關之後借貸的成本和收益
- updateState 會在 updateInterestRate 之前
- priceSource 哪裡來
[chainlink aggregator](https://docs.chain.link/docs/matic-addresses/)
- priceSentinal
# 附錄
- 觀看重點: eMode、liquidation、flashloan、isolationMode、borrow&supply
- eMode: 借貸資產都是同一個 token 的衍生品,或價格基本一樣。
- isolationMode: 只有一個 collateral,同時此 collateral 為限定高風險,有規定的 debtCeiling。
- 真的寫的很好,Pool 繼承 PoolStorage。管理所有使用者、所有 asset 相關資料。Pool 內的 borrow、supply相關函式,對 storage 狀態直接操作。
- 每次 call funciton 的時候,會傳入 storage 相關資料,這裡是 pass by reference,所以可以直接改變狀態。有時候不想被改變狀態的時候,會另外製造 cache(),建立緩存。
- 運算、檢查細節寫在 logic 資料夾內。
- validation: 檢查是否可以執行
- borrow: 貸款、還款
- supply: 抵押、提領
- reserve: 更新 index、interset rate
- liquidation: 清算
- researveConfiguration Mask 的用法是把該部分遮住,保留其他 bit 資訊。
### ScaledBalanceTokenBase
- ERC20 base,但是加入 scale 因子,轉換 token 跟 underlying asset 的比例。
- additionalData 是上一次更新的 index。
> atoken : underlying token = 1 : index
> atoken * index = underlying token
- Code
```solidity=
function _mintScaled(
address caller,
address onBehalfOf, // user
uint256 amount, // underlying asset
uint256 index // liquidityindex
) internal returns (bool) {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT);
uint256 scaledBalance = super.balanceOf(onBehalfOf); // 確定之前是否有餘額
uint256 balanceIncrease = scaledBalance.rayMul(index) - // 原本的 balance 變化
scaledBalance.rayMul(_userState[onBehalfOf].additionalData);
_userState[onBehalfOf].additionalData = index.toUint128(); // 更新 index
_mint(onBehalfOf, amountScaled.toUint128()); // 鑄造利息代幣
uint256 amountToMint = amount + balanceIncrease; // 紀錄增加的 underlying 數量: 利息+新存入的數量
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0);
}
function _burnScaled(
address user,
address target,
uint256 amount,
uint256 index
) internal {
uint256 amountScaled = amount.rayDiv(index); // 計算要燃燒的利息代幣
require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT);
uint256 scaledBalance = super.balanceOf(user); // 計算原有的利息貸幣
uint256 balanceIncrease = scaledBalance.rayMul(index) - // 抵押品增加數量
scaledBalance.rayMul(_userState[user].additionalData);
_userState[user].additionalData = index.toUint128(); // 更新 index
_burn(user, amountScaled.toUint128()); // 燃燒利息代幣
if (balanceIncrease > amount) { // event
uint256 amountToMint = balanceIncrease - amount;
emit Transfer(address(0), user, amountToMint);
emit Mint(user, user, amountToMint, balanceIncrease, index);
} else {
uint256 amountToBurn = amount - balanceIncrease;
emit Transfer(user, address(0), amountToBurn);
emit Burn(user, target, amountToBurn, balanceIncrease, index);
}
}
```
### AToken 繼承 ScaledBalanceTokenBase
- onlyPool() modifier
## 討論
- Ethereum AAVE
![](https://i.imgur.com/Qn4sTal.png)
- Ethereum Compound
![](https://i.imgur.com/qTZGjME.png)
- BSC VENUS
![](https://i.imgur.com/YoB1oeQ.png)
- USDT 利率在兩個平台都較高約 2.23%,低利率穩定幣的利率在 0.4%-0.8% 之間,兩者差距約 1.5%
- 設計一個抵押兌換平台,使用者抵押高利率代幣(不是穩定幣本身),兌換相關穩定幣,達到槓桿的效果。
- 高利率代幣是風險較大的 USDT、DAI,低利率是風險較低的幣種 USDC 等等。
- 抵押兌換平台的概念是套利者抵押高利息代幣,以一定成本換出穩定幣,以購買更多高利息代幣,達到槓桿的目的。
- 我們以債券統稱這些利息代幣。在兌換平台中,需要有兩個參與者,一是提供穩定幣供人槓桿的角色,二是抵押債券並借出穩定幣的角色。借出穩定幣的利率由供需決定。
- 利息是以存入的穩定幣計算。比方說,存入穩定幣 USDC 會獲得我們平台頒發的USDC債券,贖回時可以領回更多數量的 USDC。
- 假設 USDT 債券價格崩跌,導致發生清算事件。A 抵押 100 美元的 USDT BOND,借出 80 美元的 USDC,而 USDT BOND 下跌至 85 美元,觸發清算條件。清算者取得 A 的所有 USDT BOND,並以存入折扣價 80 美元 USDC 當作購買,保護 USDC 價值。
如果清算不及,75 美元時才發生清算,清算者一樣以 70 USDC 取得 USDT BOND,清算者獲利 5 美元,但是 USDC 數量由 80 下降至 70。
- Default swap,接受 USDC BOND 抵押,借出 USDT。存入 USDT 取得 1.7% 的利息,比 AAVE 2.0% 低,只要 USDT 下跌,獲得 USDC BOND 作為補償。
- A 方抵押 100 美元的 USDC BOND,領出 100 美元的 USDT。如果 USDT 上漲,發現 USDC BOND 不夠償還,清算 USDC BOND。如果要贖回 USDC BOND,需要繳回等值的 USDT。
A 方 獲得: USDC BOND 0.3% - USDT 成本 1.7% + USDT AAVE 2.0 % = 0.6%
B 方 獲得 USDT 1.7% + USDT 下跌保證。