aave v3 合约解析1 存款
先看代码
function supply(address asset,uint256 amount,address onBehalfOf,uint16 referralCode) public virtual override {SupplyLogic.executeSupply(_reserves,_reservesList,_usersConfig[onBehalfOf],DataTypes.ExecuteSupplyParams({user: _msgSender(),asset: asset,interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY,amount: amount,onBehalfOf: onBehalfOf,referralCode: referralCode}));}
参数解析
这是合约的入口方法,先看入参:
- asset : 要供应的资产地址,也就是需要存储的token地址。
- amount : 供应的金额。
- onBehalfOf : 存款人的地址,可以是用户自己或其他地址,在发起合约请求之前先要进行token的额度授权,否则后续转账会失败。
- referralCode : 推荐码,当用户通过第三方dApp、钱包或服务与Aave协议交互时,这些集成商可以使用自己的推荐码,Aave协议可以根据推荐码追踪集成商带来的用户活动,并给予相应奖励。
主要逻辑在SupplyLogic.executeSupply,这里涉及到4个参数,_reserves,_reservesList和 _usersConfig[onBehalfOf]这三个参数看状态存储这篇文章相应的部分。第四个参数结构如下:
struct ExecuteSupplyParams {address user;address asset;address interestRateStrategyAddress;uint256 amount;address onBehalfOf;uint16 referralCode;}
- user (address):实际执行供应操作的用户地址,通常是调用 supply() 函数的 msg.sender ,即资产的提供者。
- asset (address):要供应到协议中的资产合约地址,比如USDC、WETH、DAI 等 ERC20 代币的合约地址,必须是协议已经初始化支持的资产。
- interestRateStrategyAddress (address):该资产对应的利率策略合约地址,用于计算该资产的借贷利率,在早期的版本中不同资产可能有不同的利率策略,用以指向不同的策略地址,在最新版本中(V3.4)使用统一的利率策略合约 ,支持不同的利率参数。
- amount (uint256):用户要供应的资产数量,以资产的最小单位计算(如 USDC 为 6 位小数,ETH 为 18 位小数);必须大于 0,且用户必须有足够的余额和授权。
- onBehalfOf (address):指定代表谁进行供应操作,通常情况下 onBehalfOf == user (用户为自己供应), 代理场景下可以不同(A 用户的资产,但代表 B 用户进行供应),aToken 会铸造给 onBehalfOf 地址。
- referralCode (uint16):推荐码,用于跟踪和奖励推荐关系,如果没有推荐关系,通常设为 0;用于协议的推荐奖励机制,可以帮助跟踪用户来源和推广效果。
接下来看executeSupply的逻辑:
function executeSupply(mapping(address => DataTypes.ReserveData) storage reservesData,mapping(uint256 => address) storage reservesList,DataTypes.UserConfigurationMap storage userConfig,DataTypes.ExecuteSupplyParams memory params) external {DataTypes.ReserveData storage reserve = reservesData[params.asset];DataTypes.ReserveCache memory reserveCache = reserve.cache();reserve.updateState(reserveCache);ValidationLogic.validateSupply(reserveCache, reserve, params.amount, params.onBehalfOf);reserve.updateInterestRatesAndVirtualBalance(reserveCache,params.asset,params.amount,0,params.interestRateStrategyAddress);IERC20(params.asset).safeTransferFrom(params.user, reserveCache.aTokenAddress, params.amount);bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(params.user,params.onBehalfOf,params.amount,reserveCache.nextLiquidityIndex);if (isFirstSupply) {if (ValidationLogic.validateAutomaticUseAsCollateral(params.user,reservesData,reservesList,userConfig,reserveCache.reserveConfiguration,reserveCache.aTokenAddress)) {userConfig.setUsingAsCollateral(reserve.id, params.asset, params.onBehalfOf, true);}}emit IPool.Supply(params.asset,params.user,params.onBehalfOf,params.amount,params.referralCode);}
第一步是数据缓存:
DataTypes.ReserveData storage reserve = reservesData[params.asset];
DataTypes.ReserveCache memory reserveCache = reserve.cache();
storage是链上持久化的存储空间;合约的所有状态变量(在函数外部声明的变量)都存储在这里。对 storage 的读写操作( SLOAD 和 SSTORE 操作码)是非常昂贵的,因为它们会改变区块链的状态,需要所有节点达成共识。
memory是临时性的存储空间,它只在函数执行期间存在,函数调用结束后, memory 中的数据就会被清除。主要用于存储函数参数、局部变量和函数执行过程中的临时计算结果。对 memory 的读写操作远比 storage 便宜得多 。
reservesData[params.asset]; 这行代码的作用是从 reservesData 这个 mapping 中,根据 params.asset (资产地址)找到对应的 ReserveData 结构体。 这里的 storage 关键字意味着 reserve 不是 ReserveData 结构体的一个副本,而是一个 指向 storage 中原始数据的引用,如果直接修改 reserve 的成员(例如 reserve.liquidityIndex = ... ),将直接修改链上的状态,这将触发昂贵的 SSTORE 操作。
于是需要进行reserve.cache();一次性地从 storage 中读取 ReserveData 的所有字段,然后在 memory 中创建一个新的 ReserveCache 结构体副本 。这么做的根本原因是为了节省 Gas。通过一次性支付较高的 Gas 成本将所有需要用到的数据从 storage 加载到 memory ,来避免在函数执行过程中反复进行昂贵的 storage 读取操作,从而在总体上显著降低函数的 Gas 消耗。
资产状态更新
第二步是更新资产状态
在执行任何操作之前,必须确保资产的状态是最新的。这一步会调用 ReserveLogic.sol 中的 updateState 函数,根据自上次更新以来的时间,计算并累积利息,更新流动性指数(liquidityIndex )和可变借款指数( variableBorrowIndex )。这保证了后续所有的计算都是基于最新的资产状态。这里需要先读懂aave v3 存款与借款利息的计算方式这篇文章。
function updateState(DataTypes.ReserveData storage reserve,DataTypes.ReserveCache memory reserveCache) internal {// If time didn't pass since last stored timestamp, skip state update//solium-disable-next-lineif (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) {return;}_updateIndexes(reserve, reserveCache);_accrueToTreasury(reserve, reserveCache);//solium-disable-next-linereserve.lastUpdateTimestamp = uint40(block.timestamp);reserveCache.reserveLastUpdateTimestamp = uint40(block.timestamp);}
由于存款的动作造成了存款和借款利率的更新,_updateIndexes函数就是处理更新后的累计利率,具体看aave v3 存款与借款利息的计算方式这篇文章。
我们看下_accrueToTreasury这个函数
function _accrueToTreasury(DataTypes.ReserveData storage reserve,DataTypes.ReserveCache memory reserveCache
) internal {if (reserveCache.reserveFactor == 0) {return;}//calculate the total variable debt at moment of the last interactionuint256 prevTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul(reserveCache.currVariableBorrowIndex);//calculate the new total variable debt after accumulation of the interest on the indexuint256 currTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul(reserveCache.nextVariableBorrowIndex);//debt accrued is the sum of the current debt minus the sum of the debt at the last updateuint256 totalDebtAccrued = currTotalVariableDebt - prevTotalVariableDebt;uint256 amountToMint = totalDebtAccrued.percentMul(reserveCache.reserveFactor);if (amountToMint != 0) {reserve.accruedToTreasury += amountToMint.rayDiv(reserveCache.nextLiquidityIndex).toUint128();}
}
是向协议累积收益的核心函数。将借贷利息的一部分分配给协议 ,这是 Aave 的收入来源机制。
第一步检查储备因子reserveFactor 如果为 0,则直接返回 这个参数存储在ReserveConfigurationMap 第64位(详情见状态存储)
第二步计算上次更新时的总共借出多少钱。
第三步计算本次更新后总共借出多少钱
第四步计算其中差值,代表本次借出多少钱,再通过reserveFactor 计算协议在本次获取的利润,
最后由于利润会转变为储蓄,所以要通过当前的储蓄利息指数转换再累加到协议的总收入上
验证
function validateSupply(DataTypes.ReserveCache memory reserveCache,DataTypes.ReserveData storage reserve,uint256 amount,address onBehalfOf
) internal view {require(amount != 0, Errors.InvalidAmount());(bool isActive, bool isFrozen, , bool isPaused) = reserveCache.reserveConfiguration.getFlags();require(isActive, Errors.ReserveInactive());require(!isPaused, Errors.ReservePaused());require(!isFrozen, Errors.ReserveFrozen());require(onBehalfOf != reserveCache.aTokenAddress, Errors.SupplyToAToken());uint256 supplyCap = reserveCache.reserveConfiguration.getSupplyCap();require(supplyCap == 0 ||((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() +uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <=supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()),Errors.SupplyCapExceeded());
}
这一步是验证
require(amount != 0, Errors.InvalidAmount());
确保存款金额不为零
(bool isActive, bool isFrozen, , bool isPaused) = reserveCache.reserveConfiguration.getFlags();
require(isActive, Errors.ReserveInactive());
require(!isPaused, Errors.ReservePaused());
require(!isFrozen, Errors.ReserveFrozen());
isActive : 储备必须处于活跃状态 ReserveConfigurationMap第56位
!isPaused : 储备不能被暂停 ReserveConfigurationMap第60位
!isFrozen : 储备不能被冻结 ReserveConfigurationMap第57位
require(onBehalfOf != reserveCache.aTokenAddress, Errors.SupplyToAToken());
防止向 aToken 合约本身存款,避免循环引用
uint256 supplyCap = reserveCache.reserveConfiguration.getSupplyCap();
require(supplyCap == 0 ||((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() +uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <=supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()),Errors.SupplyCapExceeded()
);
这段是验证的核心逻辑
supplyCap是ReserveConfigurationMap的116-151位,代表供应上限,当supplyCap == 0 代表没有设置供应上限,此时跳过检查
否则校验当前总供应量 + 新存款 ≤ 供应上限
供应上限 = supplyCap × 10^decimals
同样的reserve.accruedToTreasury是经过收缩的总金额,通过乘以nextLiquidityIndex计算原本的总金额。
更新资产利率和虚拟余额
接下来的一步是更新当前资产的借贷利率和虚拟余额,所谓的虚拟余额看状态存储的相关部分,
function updateInterestRatesAndVirtualBalance(DataTypes.ReserveData storage reserve,DataTypes.ReserveCache memory reserveCache,address reserveAddress,uint256 liquidityAdded,uint256 liquidityTaken,address interestRateStrategyAddress
) internal {uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul(reserveCache.nextVariableBorrowIndex);(uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy(interestRateStrategyAddress).calculateInterestRates(DataTypes.CalculateInterestRatesParams({unbacked: reserve.deficit,liquidityAdded: liquidityAdded,liquidityTaken: liquidityTaken,totalDebt: totalVariableDebt,reserveFactor: reserveCache.reserveFactor,reserve: reserveAddress,usingVirtualBalance: true,virtualUnderlyingBalance: reserve.virtualUnderlyingBalance}));reserve.currentLiquidityRate = nextLiquidityRate.toUint128();reserve.currentVariableBorrowRate = nextVariableRate.toUint128();if (liquidityAdded > 0) {reserve.virtualUnderlyingBalance += liquidityAdded.toUint128();}if (liquidityTaken > 0) {reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128();}emit IPool.ReserveDataUpdated(reserveAddress,nextLiquidityRate,0,nextVariableRate,reserveCache.nextLiquidityIndex,reserveCache.nextVariableBorrowIndex);
}
reserve:储备数据存储
reserveCache:储备数据缓存
reserveAddress:储备的资产地址
liquidityAdded:新增的流动性
liquidityTaken:移除的流动性
interestRateStrategyAddress:利率策略合约地址
首先计算当前总可变债务
uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul(reserveCache.nextVariableBorrowIndex
);
使用缩放债务乘以借贷指数计算实际债务总额, 具体看aave v3 存款与借款利息的计算方式这篇文章。
由于存款后资金池中的总额和资金使用率发生了变化。所以要调用利率策略计算新的借贷利率,并更新当前资产的利率,利率的计算参见aave v3.4 利率计算详解
(uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy(interestRateStrategyAddress
).calculateInterestRates(/* 参数 */);reserve.currentLiquidityRate = nextLiquidityRate.toUint128();
reserve.currentVariableBorrowRate = nextVariableRate.toUint128();
接着更新虚拟余额
if (liquidityAdded > 0) {reserve.virtualUnderlyingBalance += liquidityAdded.toUint128();
}
if (liquidityTaken > 0) {reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128();
}
最后发送事件
emit IPool.ReserveDataUpdated(reserveAddress,nextLiquidityRate,0, // 稳定利率为0nextVariableRate,reserveCache.nextLiquidityIndex,reserveCache.nextVariableBorrowIndex
);
资产转移
用户存款需要把用户的资产转账到资产池中,再返回相应的凭证给用户。首先通过 IERC20(params.asset) 将底层资产地址(如 USDC、ETH 等)转换为 IERC20 接口,调用- 调用安全版本的 transferFrom 方法将用户的钱包地址(params.user)转账相应的金额(params.amount)到Atoken中(reserveCache.aTokenAddress)
IERC20(params.asset).safeTransferFrom(params.user, reserveCache.aTokenAddress, params.amount);bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(params.user,params.onBehalfOf,params.amount,reserveCache.nextLiquidityIndex);if (isFirstSupply) {if (ValidationLogic.validateAutomaticUseAsCollateral(params.user,reservesData,reservesList,userConfig,reserveCache.reserveConfiguration,reserveCache.aTokenAddress)) {userConfig.setUsingAsCollateral(reserve.id, params.asset, params.onBehalfOf, true);}}emit IPool.Supply(params.asset,params.user,params.onBehalfOf,params.amount,params.referralCode);
当用户的底层资产(如 USDC、ETH 等)已经转移到 aToken 合约后,调用 aToken 合约的 mint 函数,为用户铸造等量的 aToken;返回值 isFirstSupply 表示这是否是该用户在这个资产池中的第一次存款
mint参数如下:
- params.user :交易发起者的地址
- params.onBehalfOf :接收 aToken 的地址(可能与用户地址相同,也可能是代表其他人存款)
- params.amount :存款金额
- reserveCache.nextLiquidityIndex :当前的流动性指数,用于计算 aToken 的实际铸造数量
mint函数中会根据nextLiquidityIndex把amount值缩放后发放atoken给用户,取出的时候会根据彼时的nextLiquidityIndex计算本息返还给用户。
抵押存款
接下来是验证是否可以自动将用户的存款设置为抵押品。在 Aave 协议中,当用户存款时,系统可能会自动将其设置为抵押品,设置抵押位(将用户配置中相应资产的抵押位设置为1)并不意味着用户已经实际使用该资产作为抵押品进行了借贷,而是表示:该资产可以被用作抵押品;用户有权 将该资产用作抵押品当用户进行借贷操作时,该资产将会被计入用户的总抵押价值
if (isFirstSupply) {if (ValidationLogic.validateAutomaticUseAsCollateral(params.user,reservesData,reservesList,userConfig,reserveCache.reserveConfiguration,reserveCache.aTokenAddress)) {userConfig.setUsingAsCollateral(reserve.id, params.asset, params.onBehalfOf, true);}
}
这是一种预先授权的机制,让系统知道哪些资产可以在用户借贷时被计算为抵押品。实际的抵押行为发生在用户借贷时,此时系统会根据这些标记为抵押品的资产计算用户的借贷能力。
但是并不是随便一个资产都会被设置抵押品标志,这需要满足一定条件。
function validateAutomaticUseAsCollateral(address sender,mapping(address => DataTypes.ReserveData) storage reservesData,mapping(uint256 => address) storage reservesList,DataTypes.UserConfigurationMap storage userConfig,DataTypes.ReserveConfigurationMap memory reserveConfig,address aTokenAddress
) internal view returns (bool) {if (reserveConfig.getDebtCeiling() != 0) {// ensures only the ISOLATED_COLLATERAL_SUPPLIER_ROLE can enable collateral as side-effect of an actionIPoolAddressesProvider addressesProvider = IncentivizedERC20(aTokenAddress).POOL().ADDRESSES_PROVIDER();if (!IAccessControl(addressesProvider.getACLManager()).hasRole(ISOLATED_COLLATERAL_SUPPLIER_ROLE,sender)) return false;}return validateUseAsCollateral(reservesData, reservesList, userConfig, reserveConfig);
}
首先需要做隔离资产的检查if (reserveConfig.getDebtCeiling() != 0),如果该资产有债务上限(debt ceiling),说明这是一个"隔离资产"。隔离资产详情请参阅 状态存储
IPoolAddressesProvider addressesProvider = IncentivizedERC20(aTokenAddress).POOL().ADDRESSES_PROVIDER();
IncentivizedERC20(aTokenAddress): 获取atoken的合约实例;
.POOL(): 获取创建atoken的pool合约实例(这里获取到的应该就是当前正在执行的合约,不知道为什么要这么写!)
.ADDRESSES_PROVIDER():获取 Aave 协议的地址注册表,里面存储了各个组件的地址,如价格预言机、利率策略等
addressesProvider.getACLManager():获取ACL管理器的地址,ACL管理器是Aave协议中负责权限控制的组件,它管理不同角色和权限。
IAccessControl(addressesProvider.getACLManager()):在Aave协议中, ACLManager 是一个单独部署的合约,它实现了 IAccessControl 接口,专门负责管理整个协议的权限和角色代码位置如下:
hasRole(bytes32 role, address account) : 检查账户是否拥有特定角色,这里是判断存款账户是否是ISOLATED_COLLATERAL_SUPPLIER_ROLE账户。并不是所有得账户都可以把隔离账户设置位抵押品,必须拥有此角色的地址可以将具有债务上限(debt ceiling)的隔离资产设置为抵押品,这个角色的授予通常是经过严格治理流程的,因为它涉及到协议的风险管理,比如可信的集成合约,特定的机构用户,协议管理员;
如果一个ISOLATED_COLLATERAL_SUPPLIER_ROLE用户正在存储一个隔离资产或者当前资产不是隔离资产,则需要进一步判断当前资产是否可以抵押:
function validateUseAsCollateral(mapping(address => DataTypes.ReserveData) storage reservesData,mapping(uint256 => address) storage reservesList,DataTypes.UserConfigurationMap storage userConfig,DataTypes.ReserveConfigurationMap memory reserveConfig
) internal view returns (bool) {if (reserveConfig.getLtv() == 0) {return false;}if (!userConfig.isUsingAsCollateralAny()) {return true;}(bool isolationModeActive, , ) = userConfig.getIsolationModeState(reservesData, reservesList);return (!isolationModeActive && reserveConfig.getDebtCeiling() == 0);
}
首先检查资产的LTV(Loan-to-Value)值是否为0(reserveConfig.getLtv() == 0),LTV是贷款价值比,比如 75% 意味着价值 $100 的抵押品最多能借 $75,如果为0,表示该资产不能作为抵押品,直接返回false。
资产的LTV(贷款价值比)和DebtCeiling(债务上限)是两个不同的参数,前者代表抵押品的折扣,后者代表资产代表该资产的抵押上限。这两个概念需要区分,
- 一个资产可以是隔离资产(DebtCeiling>0),但LTV>0,表示它可以作为抵押品,但有债务上限限制
- 一个资产可以是非隔离资产(DebtCeiling=0),但LTV=0,表示它不能作为抵押品
- 一个资产可以是隔离资产(DebtCeiling>0),且LTV=0,这种情况下设置隔离模式没有实际意义,因为LTV=0使其无法作为抵押品
- 一个资产可以是非隔离资产(DebtCeiling=0)且LTV>0,这是常规抵押品
如果用户当前没有使用任何资产作为抵押品(userConfig.isUsingAsCollateralAny()),则可以使用当前资产作为抵押品,返回true
接下来通过getIsolationModeState判断当前账户是否处于隔离模式,我们回顾一下隔离模式的特征:
假设A用户,抵押了shib借走了一些usdt,如果这个用户只抵押了shib这一种资产,并且该资产被设置了隔离模式债务上限,那么该用户就是隔离用户,其在借贷的时候就进入隔离模式。
function getIsolationModeState(DataTypes.UserConfigurationMap memory self,mapping(address => DataTypes.ReserveData) storage reservesData,mapping(uint256 => address) storage reservesList
) internal view returns (bool, address, uint256) {if (isUsingAsCollateralOne(self)) {uint256 assetId = _getFirstAssetIdByMask(self, COLLATERAL_MASK);address assetAddress = reservesList[assetId];uint256 ceiling = reservesData[assetAddress].configuration.getDebtCeiling();if (ceiling != 0) {return (true, assetAddress, ceiling);}}return (false, address(0), 0);
}
isUsingAsCollateralOne判断当前用户是否只有一个,资产被设置为抵押品。
uint256 internal constant COLLATERAL_MASK =0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;function isUsingAsCollateralOne(DataTypes.UserConfigurationMap memory self
) internal pure returns (bool) {uint256 collateralData = self.data & COLLATERAL_MASK;return collateralData != 0 && (collateralData & (collateralData - 1) == 0);
}
这里回顾一下UserConfigurationMap的数据结构:
256 位的位图,用于高效存储用户在所有储备资产上的状态,每个资产占用 2 位 第 1 位(偶数位)表示用户是否借贷该资产;第 2 位(奇数位) 表示用户是否将该资产用作抵押品2,56位每个资产占用 2 位意味着最多只能存储128种数字货币,尽管市面上主流的、有实际价值的数字资产就远超128种,但Aave通常只支持 主流、高质量 的数字资产,128种已经足够覆盖绝大多数有价值的资产。
转换成二进制就是.....010101
- self.data & COLLATERAL_MASK代表把奇数位上的数据取出来,假设用户已经有两种资产设置被标准位抵押模式,那么得出collateralData = ....0100....0100
- collateralData & (collateralData - 1)就是判断collateralData 中是不是只有一位是1,
- collateralData & (collateralData - 1) = ....0100....0100 & ...0100...0001 得出....0100....0000
- collateralData & (collateralData - 1)!=0代表用户不止一种抵押资产
- collateralData != 0 && (collateralData & (collateralData - 1) == 0)代表用户有且只有一种抵押资产。
_getFirstAssetIdByMask就是在计算从右往左得第一个1在什么位置
然后通过资产位置获取到资产得address,再通过资产得DebtCeiling配置判断当前这个唯一被设置为可抵押得资产是不是隔离资产
uint256 ceiling = reservesData[assetAddress].configuration.getDebtCeiling();if (ceiling != 0) {return (true, assetAddress, ceiling);}
如果ceiling !=0 代表当前账户为隔离账户,否则就是非隔离账户。
return (!isolationModeActive && reserveConfig.getDebtCeiling() == 0);
如果为非隔离用户,且当前存储得资产为非隔离资产,那么就可以设置当前用户存储得资产为可抵押资产。
判断当前资产存款是否设置为抵押位的流程比较复杂,整理成流程图如下:
┌─────────────────────────────┐
│ 用户首次存款资产 │
│ (isFirstSupply = true) │
└───────────────┬─────────────┘↓
┌─────────────────────────────┐
│ 调用validateAutomaticUseAsCollateral
└───────────────┬─────────────┘↓
┌─────────────────────────────┐
│ 检查资产是否为隔离资产 │
│ (reserveConfig.getDebtCeiling() != 0)
└───────────────┬─────────────┘↓┌──────────┴──────────┐↓ ↓
┌────────────┐ ┌─────────────────────┐
│ 是隔离资产 │ │ 不是隔离资产 │
└──────┬─────┘ └──────────┬──────────┘↓ │
┌──────────────────────┐ │
│ 检查用户是否拥有 │ │
│ ISOLATED_COLLATERAL_ │ │
│ SUPPLIER_ROLE角色 │ │
└──────────┬───────────┘ │↓ │┌────────┴────────┐ │↓ ↓ │
┌─────────┐ ┌────────┐ │
│ 有该角色 │ │无该角色│ │
└────┬────┘ └────┬───┘ ││ │ ││ ↓ ││ ┌────────────┐ ││ │ 返回false │ ││ │ 不设为抵押 │ ││ └────────────┘ │↓ ↓
┌─────────────────────────────────────────┐
│ 调用validateUseAsCollateral继续判断 │
└─────────────────┬───────────────────────┘↓
┌─────────────────────────────────────────┐
│ 检查资产LTV是否为0 │
│ (reserveConfig.getLtv() == 0) │
└─────────────────┬───────────────────────┘↓┌──────────┴──────────┐↓ ↓┌────────┐ ┌────────────┐│ LTV=0 │ │ LTV>0 │└───┬────┘ └─────┬──────┘↓ │
┌────────────┐ │
│ 返回false │ │
│ 不设为抵押 │ │
└────────────┘ ↓┌───────────────────────┐│ 检查用户是否已有其他 ││ 抵押品 ││ (!userConfig.isUsingAsCollateralAny()) └───────────┬───────────┘↓┌────────────┴────────────┐↓ ↓┌─────────────┐ ┌──────────────┐│ 无其他抵押品 │ │ 有其他抵押品 │└──────┬──────┘ └───────┬──────┘│ │↓ ↓┌─────────────┐ ┌───────────────────────┐│ 返回true │ │ 检查用户是否处于隔离模式 ││ 设为抵押 │ │ 及当前资产是否为隔离资产 │└─────────────┘ └───────────┬───────────┘↓┌────────────┴────────────┐↓ ↓┌───────────────────────┐ ┌───────────────────────┐│ 非隔离模式且非隔离资产 │ │ 隔离模式或为隔离资产 ││ (!isolationModeActive │ │ ││ && debtCeiling == 0) │ │ │└───────────┬───────────┘ └───────────┬───────────┘↓ ↓┌───────────────────────┐ ┌───────────────────────┐│ 返回true │ │ 返回false ││ 设为抵押 │ │ 不设为抵押 │└───────────────────────┘ └───────────────────────┘