# 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 下跌保證。