AAVE v1 源码阅读

1 架构鸟瞰(Bird’s-eye View)
图 4 提供了 Aave V1 在执行层与状态层之间的完整流向:用户与外部协议主要通过 LendingPool 交互,这个无状态的业务控制器只负责校验与流程调度;资金托管、计息、估值与状态存储都被剥离到 LendingPoolCore。为将白皮书中的抽象模型映射到源码,本章按照“入口 → 状态 → 策略”的顺序梳理模块职责与依赖关系,后续章节将沿这些箭头展开函数与事件的细节。

sequenceDiagram User->>LendingPool: deposit/borrow/repay LendingPool->>LendingPoolCore: updateState*() LendingPoolCore-->>LendingPool: 累计索引与可用流动性 LendingPool->>AToken: mint/burn/transfer AToken-->>User: 凭证余额变动 LendingPool->>DataProvider: global/account data Governance->>AddressesProvider: 更新各组件地址
1.1 用户入口与门面层
contracts/lendingpool/LendingPool.sol 对外暴露 deposit / borrow / repay / redeem / swap / flashLoan,内部遵循“权限验证 → 状态更新 → 资产划转”的三段式流程,各种修饰器(onlyActiveReserve、onlyOverlyingAToken 等)统一了安全边界。赎回场景中,用户往往直接与 AToken.redeem 交互,由 aToken 回调 LendingPool.redeemUnderlying 以销毁凭证并释放底层资产。LendingPoolLiquidationManager.sol 不是独立入口,而是通过 delegatecall 挂载到 LendingPool 上下文中执行清算逻辑,既复用了校验,又避免核心合约字节码过大。flashLoan 则在控制器内部完成计费与余额校验,再把控制权交给实现了 IFlashLoanReceiver 的外部合约;FlashLoanReceiverBase.sol 只是这些外部合约的开发模板,负责取核心地址和归还资金,并非协议自身的安全防线。所有依赖由 LendingPoolAddressesProvider 统一注册与注入,确保升级和组件寻址在同一入口完成。

sequenceDiagram User->>LendingPool: 调用入口函数 LendingPool->>AddressesProvider: 查询依赖 (Core/DataProvider/FeeProvider) LendingPool->>LendingPoolLiquidationManager: delegatecall 清算 FlashLoanReceiver->>LendingPool: flashLoan LendingPool-->>FlashLoanReceiver: executeOperation 回调
1.2 状态与估值层
contracts/lendingpool/LendingPoolCore.sol 是 V1 的“保险库”:底层资产直接托管在 Core 中,而非像 V2/V3 那样散布在 aToken。它维护 ReserveData、UserReserveData、liquidityIndex、borrowIndex 等惰性计息变量,并通过 CoreLibrary.sol 与 WadRayMath.sol 完成高精度利息累积与索引更新。tokenization/AToken.sol 将这些核心状态映射成 ERC20 凭证,结合 principalBalance + redirectedBalance 让余额随时间自动增长。LendingPoolDataProvider.sol 聚合视图数据:它直接从 Core 读取原始头寸,借助 IPriceOracleGetter 折算成 ETH 计价的 LTV、Liquidation Threshold、Health Factor 以及可借额度(注意:calculateUserGlobalData 等函数在估算费用时依赖 msg.sender,即结果与调用者相关)。三者形成“状态存储 → 数据聚合 → 头寸表示”的闭环,并把资金与会计逻辑与业务入口解耦。

sequenceDiagram LendingPool->>LendingPoolCore: 读写 ReserveData/UserReserveData LendingPoolCore->>AToken: getReserveATokenAddress() AToken->>LendingPoolCore: balanceOf()/normalizedIncome DataProvider->>LendingPoolCore: getReserves()/getUserBasicReserveData DataProvider->>PriceOracle: getAssetPrice()
1.3 策略、治理与注册表
LendingPoolAddressesProvider.sol 扮演注册表(协议的 DNS):它保存最新的 LendingPool、Core、Configurator 等地址,为可升级系统提供解耦。DefaultReserveInterestRateStrategy.sol 基于储备利用率(Kink 曲线)与 ILendingRateOracle 输出稳定/浮动利率,FeeProvider.sol 统一计算开仓费用;LendingPoolConfigurator.sol 则拥有极高权限,负责 initReserve、启用/禁用借款、调整 LTV、Liquidation Threshold、利率曲线等风险参数。通过注册表、策略合约和治理配置三件套,协议可以在不触碰资产托管层的前提下根据市场环境调整策略,与上游的价格预言机与数据提供层一道,形成完整的“参数 + 策略 + 注册表”控制面板。

sequenceDiagram Governance->>AddressesProvider: setLendingPoolImpl()/... LendingPool->>AddressesProvider: getLendingPoolCore() Configurator->>LendingPoolCore: enableReserveAsCollateral() Core->>InterestRateStrategy: calculateInterestRates() LendingPool->>FeeProvider: calculateLoanOriginationFee()
本章确定了合约之间的依赖图谱。接下来将以每个合约为单位,结合白皮书对应章节,分析函数实现、状态变迁以及跨模块的接口契约。
2 LendingPool:入口逻辑
拆解关键流程(deposit/borrow/repay/flashLoan/liquidationCall),说明校验顺序、与 Core/DataProvider/FeeProvider 的交互、常见修饰器。可穿插函数调用图或伪代码。

sequenceDiagram User->>LendingPool: 调用入口函数 LendingPool->>DataProvider: calculateUserGlobalData() LendingPool->>FeeProvider: calculateLoanOriginationFee() LendingPool->>LendingPoolCore: updateState*() LendingPool->>AToken: mint/burn/redeem LendingPool-->>User: 事件 + 资产划转结果
2.1 deposit
存款是 V1 流动性的来源,对应白皮书 §3.1 图 6 的“资产转移 → aToken 铸造 → 状态更新”。LendingPool.deposit 在入口层仍然先做安全校验,再把状态计算、凭证铸造与资金托管分发给下层组件:
-
权限与状态检查: nonReentrant、onlyActiveReserve、onlyUnfreezedReserve、onlyAmountGreaterThanZero共同保证储备可用、金额有效、防止重入攻击。 -
识别首次存款者:读取 aToken.balanceOf(msg.sender)判定isFirstDeposit,方便 Core 在更新状态时将useAsCollateral从 0 切换到 1。 -
状态更新: core.updateStateOnDeposit刷新储备的流动性指数和时间戳,并在首次存款时开启抵押标记;实际余额的变动由随后transferToReserve的资金转账体现。 -
aToken 铸造: aToken.mintOnDeposit将_amount直映为 ERC20 余额,不需要额外汇率,因为 aToken 余额后续会乘liquidityIndex自动增长。 -
资金划转与事件: core.transferToReserve将资产真正存入保险库(ERC20 路径调用safeTransferFrom,ETH 路径根据msg.value进账并退还多余金额),随后Deposit事件把储备、金额、推荐码广播给前端或积分系统。 -
资产类型处理:函数被标记为 payable方便 ETH 存款,而 ERC20 存款所需的授权与转账逻辑全部封装在 Core 内部,协议代码显式区分两种资产路径。 -
隐式前置条件:用户在存 ERC20 前必须先对 LendingPoolCoreapprove足够额度;协议以组合操作的形式优化 gas 成本,因此需要前端流程引导。
/** * @dev 将基础资产存入储备池中。同时会铸造相应数量的覆盖资产(aToken) * @param _reserve reserve 的地址 * @param _amount 存入金额 * @param _referralCode integrators are assigned a referral code and can potentially receive rewards. **/ function deposit( address _reserve, uint256 _amount, uint16 _referralCode ) external payable nonReentrant onlyActiveReserve(_reserve) // 检查储备池是否处于活跃状态 onlyUnfreezedReserve(_reserve) // 检查储备池是否处于冻结状态 onlyAmountGreaterThanZero(_amount) // 检查存入金额是否大于0 { // 获取 aToken 合约地址 AToken aToken = AToken(core.getReserveATokenAddress(_reserve)); // 判断是否是第一次存入 bool isFirstDeposit = aToken.balanceOf(msg.sender) == 0; // 更新状态 core.updateStateOnDeposit( _reserve, msg.sender, _amount, isFirstDeposit ); // 按特定兑换率以1:1比例向用户铸造AToken aToken.mintOnDeposit(msg.sender, _amount); // 将资产转账到核心合约 core.transferToReserve.value(msg.value)(_reserve, msg.sender, _amount); //solium-disable-next-line // 发出存款事件 emit Deposit( _reserve, msg.sender, _amount, _referralCode, block.timestamp ); }
sequenceDiagram User->>LendingPool: borrow(_reserve, _amount, _rateMode) LendingPool->>LendingPoolCore: isReserveBorrowingEnabled()/getReserveAvailableLiquidity() LendingPool->>LendingPoolDataProvider: calculateUserGlobalData() LendingPool->>FeeProvider: calculateLoanOriginationFee() LendingPool->>LendingPoolCore: updateStateOnBorrow(...) LendingPoolCore-->>LendingPool: 最终借款利率、borrowBalanceIncrease LendingPool->>User: transferToUser(_reserve, _amount)
sequenceDiagram User->>LendingPool: deposit(_reserve, _amount) LendingPool->>LendingPoolCore: updateStateOnDeposit(...) LendingPoolCore-->>LendingPool: 索引更新完成 LendingPool->>AToken: mintOnDeposit(msg.sender, _amount) AToken-->>User: 增加 aToken 余额 LendingPool->>LendingPoolCore: transferToReserve(...)


2.2 borrow
借款对应白皮书 §3.2 中“抵押额度 → 借款模式 → 资金释放”的流程,V1 中的实现完全落在 LendingPool.borrow。函数使用 BorrowLocalVars 暂存数据,避免 “Stack too deep” 并保持流水线式的校验顺序:
-
入口修饰器与储备可借性: nonReentrant、onlyActiveReserve、onlyUnfreezedReserve、onlyAmountGreaterThanZero校验基本条件,随后core.isReserveBorrowingEnabled、core.getReserveAvailableLiquidity保障储备层允许借款且流动性足够。传入的_interestRateMode必须是 1 (STABLE) 或 2 (VARIABLE),否则直接 revert。 -
账户总览校验:通过 dataProvider.calculateUserGlobalData(msg.sender)一次性取回用户的抵押余额、借款余额、总费用、LTV、Liquidation Threshold 以及healthFactorBelowThreshold标记。要求抵押余额大于 0 且当前 Health Factor 不低于 1,否则禁止继续借款。 -
开仓费用与抵押需求: feeProvider.calculateLoanOriginationFee对借款金额计算开仓费用,并且要求费用 > 0(防止金额过小)。接着使用dataProvider.calculateCollateralNeededInETH折算为 ETH 计价的最小抵押物需求,该公式将_amount、borrowFee、已有借款与费用一并纳入,只有当userCollateralBalanceETH≥amountOfCollateralNeededETH时才能借款。 -
稳定利率额外限制:若用户选择稳定利率, core.isUserAllowedToBorrowAtStable会检查储备是否开启稳定模式、用户抵押资产是否与借款资产过度同质,以及当前金额是否允许;parametersProvider.getMaxStableRateBorrowSizePercent则限制稳定利率借款在总可用流动性中的占比,防止一笔交易吸干储备。 -
状态刷新与资金划转: core.updateStateOnBorrow将债务指数与principalBalance更新为最新值,返回实际借款利率 (finalUserBorrowRate) 与自上次操作以来累积的borrowBalanceIncrease;状态更新后才调用core.transferToUser把_amount下发给借款人,最后Borrow事件记录利率模式、费用和推荐码。
function borrow( address _reserve, uint256 _amount, uint256 _interestRateMode, uint16 _referralCode ) external nonReentrant onlyActiveReserve(_reserve) onlyUnfreezedReserve(_reserve) onlyAmountGreaterThanZero(_amount) { BorrowLocalVars memory vars; // 储备借款开关、利率模式、可用流动性检查 ... // 将利率模式转换为 coreLibrary.interestRateMode vars.rateMode = CoreLibrary.InterestRateMode(_interestRateMode); // 检查储备池中是否有足够的可用金额,可用流动性即为核心合约的余额。 vars.availableLiquidity = core.getReserveAvailableLiquidity(_reserve); ... ( , vars.userCollateralBalanceETH, vars.userBorrowBalanceETH, vars.userTotalFeesETH, vars.currentLtv, vars.currentLiquidationThreshold, , vars.healthFactorBelowThreshold ) = dataProvider.calculateUserGlobalData(msg.sender); // 一次性获取抵押、借款、LTV、健康度 // 抵押>0 与 Health Factor>1 的 require ... vars.borrowFee = feeProvider.calculateLoanOriginationFee( msg.sender, _amount ); // 计算开仓费 ... vars.amountOfCollateralNeededETH = dataProvider .calculateCollateralNeededInETH( _reserve, _amount, vars.borrowFee, vars.userBorrowBalanceETH, vars.userTotalFeesETH, vars.currentLtv ); // 根据 LTV 推导所需抵押 // 抵押覆盖校验 ... /** * 如果用户以稳定利率借款,需要满足以下条件: * 1. 储备池必须启用稳定利率借款 * 2. 用户不能从储备池借款,如果他们的抵押品主要是他们正在借款的货币,以防止滥用。 * 3. 用户只能借款储备池总流动性的一个相对较小、可配置的金额。 */ if (vars.rateMode == CoreLibrary.InterestRateMode.STABLE) { // 检查借款模式是否为稳定利率,并且储备池是否启用了稳定利率借款 ... uint256 maxLoanPercent = parametersProvider .getMaxStableRateBorrowSizePercent(); uint256 maxLoanSizeStable = vars .availableLiquidity .mul(maxLoanPercent) .div(100); require( _amount <= maxLoanSizeStable, "User is trying to borrow too much liquidity at a stable rate" ); } // 更新储备池状态 (vars.finalUserBorrowRate, vars.borrowBalanceIncrease) = core .updateStateOnBorrow( _reserve, msg.sender, _amount, vars.borrowFee, vars.rateMode // 同步更新储备曲线和用户债务指数 ); // 转移储备池中的资产到借款人 core.transferToUser(_reserve, msg.sender, _amount); emit Borrow( _reserve, msg.sender, _amount, _interestRateMode, vars.finalUserBorrowRate, vars.borrowFee, vars.borrowBalanceIncrease, _referralCode, block.timestamp ); }
sequenceDiagram User->>LendingPool: repay(_reserve, _amount, _onBehalfOf) LendingPool->>LendingPoolCore: getUserBorrowBalances() LendingPool->>LendingPoolCore: getUserOriginationFee() LendingPool->>LendingPoolCore: updateStateOnRepay(...) alt 仅偿还费用 LendingPool->>LendingPoolCore: transferToFeeCollectionAddress() else 本金 + 费用 LendingPool->>LendingPoolCore: transferToFeeCollectionAddress() LendingPool->>LendingPoolCore: transferToReserve(...) end LendingPool-->>User: Repay 事件

2.3 repay
repay 负责关闭或部分偿还债务,对应白皮书 §3.3 的“偿还 + 费用回收”。函数既允许借款人自还,也允许别人带着 _onBehalfOf 参数帮忙清算,UINT_MAX_VALUE(uint256(-1)) 则代表“尽可能还多”:
-
读取债务与费用:通过 core.getUserBorrowBalances得到principalBorrowBalance、compoundedBorrowBalance、borrowBalanceIncrease,再取出core.getUserOriginationFee。如果债务为零直接 revert。 -
确定还款金额:默认 paybackAmount = compounded + originationFee,若调用者传入_amount != UINT_MAX_VALUE且更小,则裁剪,且要求“代还”场景必须显式金额。ETH 储备下msg.value至少等于paybackAmount。 -
仅偿还费用的分支:当 paybackAmount <= originationFee时,说明用户只需偿还费用。core.updateStateOnRepay会以 0 principal 更新状态,并把vars.paybackAmount全额通过core.transferToFeeCollectionAddress送往TokenDistributor(注意:此分支下费用从_onBehalfOf账户扣除)。 -
标准偿还流程: paybackAmountMinusFees = paybackAmount - originationFee,随后调用core.updateStateOnRepay写入本金偿还金额、费用和借款增量,并标记是否还清。若存在 origination fee,则先transferToFeeCollectionAddress,剩余本金部分通过core.transferToReserve入库;ETH 路径会把msg.value - fee作为value入参,多余 ETH 在transferToReserve内退还。在此分支中,origination fee 由msg.sender支付(ERC20 需msg.sender对 Core 授权)。 -
事件记录:无论是哪条路径,最终都会 emit Repay,携带_reserve、被还债用户、repayer 地址、偿还本金、费用与borrowBalanceIncrease。
function repay( address _reserve, uint256 _amount,ß address payable _onBehalfOf ) external payable nonReentrant onlyActiveReserve(_reserve) onlyAmountGreaterThanZero(_amount) { RepayLocalVars memory vars; ( // 查询本金、复利余额与利息增量 vars.principalBorrowBalance, vars.compoundedBorrowBalance, vars.borrowBalanceIncrease ) = core.getUserBorrowBalances(_reserve, _onBehalfOf); vars.originationFee = core.getUserOriginationFee(_reserve, _onBehalfOf); vars.isETH = EthAddressLib.ethAddress() == _reserve; ... // require: 仍有债务、msg.sender 代还约束、msg.value 校验 vars.paybackAmount = vars.compoundedBorrowBalance.add( vars.originationFee // 需要偿还的总额 = 债务 + 开仓费 ); if (_amount != UINT_MAX_VALUE && _amount < vars.paybackAmount) { vars.paybackAmount = _amount; } ... // ETH 分支需要携带足量 msg.value // 如果还款金额小于初始费用,直接转给费用接收地址 if (vars.paybackAmount <= vars.originationFee) { // 只剩开仓费待付,principal 不变 core.updateStateOnRepay( _reserve, _onBehalfOf, 0, vars.paybackAmount, vars.borrowBalanceIncrease, false ); ... // core.transferToFeeCollectionAddress(... vars.paybackAmount ...) emit Repay( _reserve, _onBehalfOf, msg.sender, 0, vars.paybackAmount, vars.borrowBalanceIncrease, block.timestamp ); return; } vars.paybackAmountMinusFees = vars.paybackAmount.sub( vars.originationFee // 真正还掉的本金 ); core.updateStateOnRepay( _reserve, _onBehalfOf, vars.paybackAmountMinusFees, vars.originationFee, vars.borrowBalanceIncrease, vars.compoundedBorrowBalance == vars.paybackAmountMinusFees ); // 如果没有支付初始费用,将费用转给费用接收地址 if (vars.originationFee > 0) { ... // core.transferToFeeCollectionAddress(... vars.originationFee ...) } // 发送总 msg.value(如果是 ETH)。 // transferToReserve() 函数会负责将多余的 ETH 退还给调用者。 core.transferToReserve.value( vars.isETH ? msg.value.sub(vars.originationFee) : 0 )(_reserve, msg.sender, vars.paybackAmountMinusFees); emit Repay( _reserve, _onBehalfOf, msg.sender, vars.paybackAmountMinusFees, vars.originationFee, vars.borrowBalanceIncrease, block.timestamp ); }
sequenceDiagram FlashLoanReceiver->>LendingPool: flashLoan(_reserve, _amount) LendingPool->>LendingPoolCore: 查询 availableLiquidityBefore LendingPool->>ParametersProvider: getFlashLoanFeesInBips() LendingPool->>FlashLoanReceiver: transferToUser(_amount) FlashLoanReceiver-->>LendingPool: executeOperation 回调 LendingPool->>LendingPoolCore: 校验 availableLiquidityAfter LendingPool->>LendingPoolCore: updateStateOnFlashLoan(...)

2.4 flashLoan
闪电贷允许外部合约在一次交易内借出资金并归还,入口函数贯彻“余额守恒 + 费用拆分”原则:
-
准备与计费: onlyActiveReserve、onlyAmountGreaterThanZero后,函数根据资产种类读取availableLiquidityBefore(ETH 取address(core).balance,ERC20 读取IERC20(_reserve).balanceOf(address(core))),确认储备足以覆盖_amount。parametersProvider.getFlashLoanFeesInBips返回总费率与协议分润,amountFee = _amount * totalFeeBips / 10000,protocolFee = amountFee * protocolFeeBips / 10000,并要求这两个值都大于 0。 -
借出与回调:将 _receiver强转为IFlashLoanReceiver,通过core.transferToUser把_amount划给receiver,随后调用receiver.executeOperation(_reserve, _amount, amountFee, _params)。此回调必须在同一交易内完成。 -
归还与校验:执行完回调后再次读取 availableLiquidityAfter,要求其等于availableLiquidityBefore + amountFee,即本金 + 总费用都已经回到 Core;若不满足则 revert。 -
状态更新与事件: core.updateStateOnFlashLoan把amountFee - protocolFee计入储备收益、将protocolFee发送到费用收集地址;事件FlashLoan记录_receiver、_reserve、_amount、总费用与协议分润。
function flashLoan( address _receiver, address _reserve, uint256 _amount, bytes memory _params ) public nonReentrant onlyActiveReserve(_reserve) onlyAmountGreaterThanZero(_amount) { // 检查储备池是否有足够的可用流动性 // 避免使用 LendingPoolCore 中的 getAvailableLiquidity() 函数以节省 gas uint256 availableLiquidityBefore = _reserve == EthAddressLib.ethAddress() ? address(core).balance : IERC20(_reserve).balanceOf(address(core)); ... // require: 储备流动性必须覆盖 _amount (uint256 totalFeeBips, uint256 protocolFeeBips) = parametersProvider .getFlashLoanFeesInBips(); uint256 amountFee = _amount.mul(totalFeeBips).div(10000); // 协议费用是 amountFee 中为协议保留的部分 - 剩余部分归存款人所有 uint256 protocolFee = amountFee.mul(protocolFeeBips).div(10000); ... // require: 金额足够大且费用非零 IFlashLoanReceiver receiver = IFlashLoanReceiver(_receiver); address payable userPayable = address(uint160(_receiver)); // 转移资金给接收者 core.transferToUser(_reserve, userPayable, _amount); receiver.executeOperation(_reserve, _amount, amountFee, _params); // 外部合约需在此回调内完成借款逻辑 // 检查核心合约的实际余额是否包含返回的金额 uint256 availableLiquidityAfter = _reserve == EthAddressLib.ethAddress() ? address(core).balance : IERC20(_reserve).balanceOf(address(core)); require( availableLiquidityAfter == availableLiquidityBefore.add(amountFee), "The actual balance of the protocol is inconsistent" ); // 更新储备池状态 core.updateStateOnFlashLoan( _reserve, availableLiquidityBefore, amountFee.sub(protocolFee), protocolFee // 结息:把收益分给存款人,协议抽象余 ); emit FlashLoan( _receiver, _reserve, _amount, amountFee, protocolFee, block.timestamp ); }
sequenceDiagram Liquidator->>LendingPool: liquidationCall(_collateral, _reserve, _user, ...) LendingPool->>LiquidationManager: delegatecall liquidationCall(...) LiquidationManager->>DataProvider: calculateUserGlobalData(_user) LiquidationManager->>LendingPoolCore: getUserUnderlyingAssetBalance()/getUserBorrowBalances() LiquidationManager->>PriceOracle: getAssetPrice() LiquidationManager->>LendingPoolCore: updateStateOnLiquidation(...) alt receive aToken LiquidationManager->>AToken: transferOnLiquidation() else receive underlying LiquidationManager->>AToken: burnOnLiquidation() LiquidationManager->>LendingPoolCore: transferToUser() end Liquidator->>LendingPoolCore: transferToReserve(...)

2.5 liquidationCall
清算入口通过 delegatecall 把执行上下文交给 LendingPoolLiquidationManager,后者在同一个存储上下文中完成所有校验与资产转移:
-
入口与委托: LendingPool.liquidationCall只做储备活跃性检查和nonReentrant保护,然后对addressesProvider.getLendingPoolLiquidationManager()发起delegatecall。如果返回的(returnCode, returnMessage)非 0,会拼接字符串并 revert,确保只要清算失败就回滚整笔交易。 -
健康度与抵押检查:管理器内首先调用 dataProvider.calculateUserGlobalData(_user),只有当healthFactorBelowThreshold == true时才允许继续。随后读取core.getUserUnderlyingAssetBalance(_collateral)、core.isReserveUsageAsCollateralEnabled和core.isUserUseReserveAsCollateralEnabled,确认该抵押品可被清算,再通过core.getUserBorrowBalances(_reserve, _user)检查该用户确实借入了指定的债务资产。 -
可清算额度计算:遵循 LIQUIDATION_CLOSE_FACTOR_PERCENT = 50,vars.maxPrincipalAmountToLiquidate = userCompoundedBorrowBalance * 50%,实际处理金额取_purchaseAmount与上限的较小值。calculateAvailableCollateralToLiquidate利用价格预言机读取_collateral与_reserve的 ETH 价格,再乘以core.getReserveLiquidationBonus(_collateral)得到最多可 seize 的抵押资产数量;若该抵押不足以覆盖_purchaseAmount,函数会下调actualAmountToLiquidate。 -
费用与资产转移:若用户仍有 originationFee,会再调用calculateAvailableCollateralToLiquidate分配一部分抵押作为费用抵押并记账给协议。对于_receiveAToken == true,直接AToken.transferOnLiquidation将 aToken 余额转给清算人;否则先burnOnLiquidation销毁 aToken,再由core.transferToUser发放等值底层资产。同时,清算人偿还的本金(ETH 或 ERC20)由core.transferToReserve.value(msg.value)收进协议,若费用被清算则额外core.liquidateFee发送到TokenDistributor并触发OriginationFeeLiquidated。 -
状态更新与事件: core.updateStateOnLiquidation在资金移动前已经写入债务减少、抵押扣减、fee 分配以及borrowBalanceIncrease,最终LiquidationCall事件会记录 collateral、reserve、用户、实际偿还金额、被扣抵押数量、利息增量、清算人地址以及是否领取 aToken。
// LendingPool.sol function liquidationCall( address _collateral, address _reserve, address _user, uint256 _purchaseAmount, bool _receiveAToken ) external payable nonReentrant onlyActiveReserve(_reserve) onlyActiveReserve(_collateral) { address liquidationManager = addressesProvider.getLendingPoolLiquidationManager(); (bool success, bytes memory result) = liquidationManager.delegatecall( abi.encodeWithSignature( "liquidationCall(address,address,address,uint256,bool)", _collateral, _reserve, _user, _purchaseAmount, _receiveAToken ) ); require(success, "Liquidation call failed"); (uint256 returnCode, string memory returnMessage) = abi.decode( result, (uint256, string) ); require(returnCode == 0, string(abi.encodePacked("Liquidation failed: ", returnMessage))); }
// LendingPoolLiquidationManager.sol function liquidationCall( address _collateral, address _reserve, address _user, uint256 _purchaseAmount, bool _receiveAToken ) external payable returns (uint256, string memory) { LiquidationCallLocalVars memory vars; (, , , , , , , vars.healthFactorBelowThreshold) = dataProvider.calculateUserGlobalData( _user ); if (!vars.healthFactorBelowThreshold) { return ( uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), "Health factor is not below the threshold" ); } vars.userCollateralBalance = core.getUserUnderlyingAssetBalance(_collateral, _user); if (vars.userCollateralBalance == 0) { return ( uint256(LiquidationErrors.NO_COLLATERAL_AVAILABLE), "Invalid collateral to liquidate" ); } vars.isCollateralEnabled = core.isReserveUsageAsCollateralEnabled(_collateral) && core.isUserUseReserveAsCollateralEnabled(_collateral, _user); if (!vars.isCollateralEnabled) { return ( uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), "The collateral chosen cannot be liquidated" ); } (, vars.userCompoundedBorrowBalance, vars.borrowBalanceIncrease) = core.getUserBorrowBalances( _reserve, _user ); if (vars.userCompoundedBorrowBalance == 0) { return ( uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED), "User did not borrow the specified currency" ); } vars.maxPrincipalAmountToLiquidate = vars .userCompoundedBorrowBalance .mul(LIQUIDATION_CLOSE_FACTOR_PERCENT) .div(100); vars.actualAmountToLiquidate = _purchaseAmount > vars.maxPrincipalAmountToLiquidate ? vars.maxPrincipalAmountToLiquidate : _purchaseAmount; ( uint256 maxCollateralToLiquidate, uint256 principalAmountNeeded ) = calculateAvailableCollateralToLiquidate( _collateral, _reserve, vars.actualAmountToLiquidate, vars.userCollateralBalance ); vars.originationFee = core.getUserOriginationFee(_reserve, _user); if (vars.originationFee > 0) { ( vars.liquidatedCollateralForFee, vars.feeLiquidated ) = calculateAvailableCollateralToLiquidate( _collateral, _reserve, vars.originationFee, vars.userCollateralBalance.sub(maxCollateralToLiquidate) ); } if (principalAmountNeeded < vars.actualAmountToLiquidate) { vars.actualAmountToLiquidate = principalAmountNeeded; } if (!_receiveAToken) { uint256 availableCollateral = core.getReserveAvailableLiquidity(_collateral); if (availableCollateral < maxCollateralToLiquidate) { return ( uint256(LiquidationErrors.NOT_ENOUGH_LIQUIDITY), "There isn't enough liquidity available to liquidate" ); } } core.updateStateOnLiquidation( _reserve, _collateral, _user, vars.actualAmountToLiquidate, maxCollateralToLiquidate, vars.feeLiquidated, vars.liquidatedCollateralForFee, vars.borrowBalanceIncrease, _receiveAToken ); AToken collateralAtoken = AToken(core.getReserveATokenAddress(_collateral)); if (_receiveAToken) { collateralAtoken.transferOnLiquidation(_user, msg.sender, maxCollateralToLiquidate); } else { collateralAtoken.burnOnLiquidation(_user, maxCollateralToLiquidate); core.transferToUser(_collateral, msg.sender, maxCollateralToLiquidate); } core.transferToReserve.value(msg.value)( _reserve, msg.sender, vars.actualAmountToLiquidate ); if (vars.feeLiquidated > 0) { collateralAtoken.burnOnLiquidation(_user, vars.liquidatedCollateralForFee); core.liquidateFee( _collateral, vars.liquidatedCollateralForFee, addressesProvider.getTokenDistributor() ); emit OriginationFeeLiquidated( _collateral, _reserve, _user, vars.feeLiquidated, vars.liquidatedCollateralForFee, block.timestamp ); } emit LiquidationCall( _collateral, _reserve, _user, vars.actualAmountToLiquidate, maxCollateralToLiquidate, vars.borrowBalanceIncrease, msg.sender, _receiveAToken, block.timestamp ); return (uint256(LiquidationErrors.NO_ERROR), "No errors"); }
3 LendingPoolCore:资金与索引
LendingPoolCore 保管全部底层资产,并把协议状态拆成 reserves[address] 与 usersReserveData[user][reserve] 两层,业务入口只在 LendingPool 中做权限校验,具体的利率积累、余额记账、资产转移都下沉到 Core 与 CoreLibrary。这一层的所有金额在 WadRayMath 的 ray 精度 (1e27) 下运算,确保利息累积不会丢精度。
3.1 状态结构:ReserveData 与 UserReserveData
-
CoreLibrary.ReserveData同时保存资金侧指标(lastLiquidityCumulativeIndex、lastVariableBorrowCumulativeIndex、currentLiquidityRate、totalBorrowsStable/Variable)和风险参数(baseLTVasCollateral、liquidationThreshold、liquidationBonus、borrowingEnabled、isStableBorrowRateEnabled等)。初始化时init()把两个指数都设为1e27,后续所有收益都在此基础上累乘。 -
CoreLibrary.UserReserveData记录单个用户在某个储备上的本金、借款指数、originationFee和stableBorrowRate,再辅以lastUpdateTimestamp与useAsCollateral标记。入口层通过setUserUseReserveAsCollateral打开或关闭抵押权,所有借款模式切换和稳定利率重平衡都直接写入这份结构。 -
reservesList/getReserves()提供了一个可迭代的储备数组,LendingPoolDataProvider.calculateUserGlobalData就依赖它遍历所有资产。
sequenceDiagram LendingPoolConfigurator->>LendingPoolCore: initReserve() LendingPoolCore->>CoreLibrary: init(ReserveData) LendingPool->>LendingPoolCore: getUserBasicReserveData() LendingPoolCore-->>LendingPool: UserReserveData(useAsCollateral, principal, fee)

3.2 惰性计息与状态刷新
-
每个流转动作都会先 reserves[_reserve].updateCumulativeIndexes(),再调用updateReserveInterestRatesAndTimestampInternal()。updateCumulativeIndexes通过calculateLinearInterest和calculateCompoundedInterest把当前利率与上次时间戳之间的利息累加到两个指数上,只有totalBorrows>0时才真正更新。 -
updateStateOnDeposit与updateStateOnRedeem分别在_amount作为正负流动性注入/抽离后刷新利率;首次存款时会把useAsCollateral切成true,赎回至 0 则反向关闭。 -
updateStateOnBorrow把储备借款指标与用户借款指标拆开处理:updateReserveStateOnBorrowInternal更新totalBorrows、平均利率与时间戳,updateUserStateOnBorrowInternal把用户本金加上新借款,并记录_borrowFee与利率模式,返回最新的borrowBalanceIncrease提供给事件。 -
updateStateOnRepay、updateStateOnLiquidation和updateStateOnRebalance则把刚才的模式镜像回来,按“先储备 -> 后用户 -> 再刷新利率”的顺序执行,保证索引与本金始终匹配。 -
闪电贷路径中, updateStateOnFlashLoan通过cumulateToLiquidityIndex(totalLiquidityBefore, income)把额外收益一次性打入流动性指数,再将protocolFee单独转给TokenDistributor,实现“收益给存款人,抽成给协议”的两段拆分。
关键函数片段如下(留意入口限定 onlyLendingPool,确保 Core 不直接暴露敏感操作):
function updateStateOnDeposit( address _reserve, address _user, uint256 _amount, bool _isFirstDeposit) external onlyLendingPool { // 累积上一个区块的收益,防止旧利率污染当前操作 reserves[_reserve].updateCumulativeIndexes(); // 将本次存入金额计入储备,刷新流动性利率与时间戳 updateReserveInterestRatesAndTimestampInternal(_reserve, _amount, 0); if (_isFirstDeposit) { setUserUseReserveAsCollateral(_reserve, _user, true); // 首次存款直接作为抵押 }}function updateStateOnBorrow( address _reserve, address _user, uint256 _amountBorrowed, uint256 _borrowFee, CoreLibrary.InterestRateMode _rateMode) external onlyLendingPool returns (uint256, uint256) { (uint256 principalBorrowBalance, , uint256 balanceIncrease) = getUserBorrowBalances(_reserve, _user); // 先把储备维度的借款统计更新,否则用户状态会依赖旧的 `totalBorrows` updateReserveStateOnBorrowInternal( _reserve, _user, principalBorrowBalance, balanceIncrease, _amountBorrowed, _rateMode ); // 再记录到用户维度:本金、费用与利率模式 updateUserStateOnBorrowInternal( _reserve, _user, _amountBorrowed, balanceIncrease, _borrowFee, _rateMode ); // 借款等价于抽离流动性,因此在负方向刷新利率 updateReserveInterestRatesAndTimestampInternal(_reserve, 0, _amountBorrowed); return (getUserCurrentBorrowRate(_reserve, _user), balanceIncrease);}
sequenceDiagram LendingPool->>LendingPoolCore: updateStateOnDeposit() LendingPoolCore->>CoreLibrary: updateCumulativeIndexes() CoreLibrary-->>LendingPoolCore: 新的 liquidityIndex/variableIndex LendingPool->>LendingPoolCore: updateStateOnBorrow() LendingPoolCore->>CoreLibrary: updateReserveStateOnBorrowInternal() LendingPoolCore-->>LendingPool: balanceIncrease/borrowRate

3.3 资金入口与费用路径
-
Core 对 ETH 与 ERC20 的资金流分别封装: transferToReserve负责收款(ERC20 走safeTransferFrom,ETH 要求msg.value >= amount并主动退回多余部分);transferToUser则在出金时对 ETH 使用带 50k gas 的call(contracts/lendingpool/LendingPoolCore.sol:439-441)。 -
所有费用都走单独通道: transferToFeeCollectionAddress用于借款开仓费、liquidateFee用于清算费,调用方需要显式传入_destination,从而把费用直接打到TokenDistributor。 -
合约 fallback()禁止 EOA 主动给 Core 汇入 ETH,只有合约才能向它转账,避免用户误充。闪电贷归还逻辑正是借助这一点,通过LendingPool检查 Core 的address(this).balance是否等于借出前余额加手续费。
function transferToUser( address _reserve, address payable _user, uint256 _amount) external onlyLendingPool { if (_reserve != EthAddressLib.ethAddress()) { // ERC20 路径直接将资产转出给用户 ERC20(_reserve).safeTransfer(_user, _amount); } else { // ETH 路径使用 call,固定 50k gas 以降低重入面 (bool result, ) = _user.call.value(_amount).gas(50000)(""); require(result, "Transfer of ETH failed"); }}function transferToReserve( address _reserve, address payable _user, uint256 _amount) external payable onlyLendingPool { if (_reserve != EthAddressLib.ethAddress()) { // ERC20 入金必须通过 `safeTransferFrom`,防止多余 ETH require(msg.value == 0, "User is sending ETH along with the ERC20 transfer."); ERC20(_reserve).safeTransferFrom(_user, address(this), _amount); } else { // ETH 入金需要附带足量 msg.value,并在多付时立刻退款 require(msg.value >= _amount, "The amount and the value sent to deposit do not match"); if (msg.value > _amount) { uint256 excessAmount = msg.value.sub(_amount); (bool result, ) = _user.call.value(excessAmount).gas(50000)(""); require(result, "Transfer of ETH failed"); } }}function transferToFeeCollectionAddress( address _token, address _user, uint256 _amount, address _destination) external payable onlyLendingPool { address payable feeAddress = address(uint160(_destination)); if (_token != EthAddressLib.ethAddress()) { // 借款费的 ERC20 路径直接由 Core 从用户账户划走 require(msg.value == 0, "User is sending ETH along with the ERC20 transfer..."); ERC20(_token).safeTransferFrom(_user, feeAddress, _amount); } else { // ETH 费用需要携带 value,并直接转发给 TokenDistributor require(msg.value >= _amount, "The amount and the value sent to deposit do not match"); (bool result, ) = feeAddress.call.value(_amount).gas(50000)(""); require(result, "Transfer of ETH failed"); }}
sequenceDiagram LendingPool->>LendingPoolCore: transferToReserve() alt ERC20 LendingPoolCore->>ERC20: safeTransferFrom(user, core, amount) else ETH LendingPoolCore->>User: refund excess ETH end LendingPool->>LendingPoolCore: transferToUser() LendingPoolCore-->>User: ERC20/ETH LendingPool->>LendingPoolCore: transferToFeeCollectionAddress() LendingPoolCore-->>TokenDistributor: 协议费用

3.4 利用率 :白皮书 §1.2.3 ↔ getReserveUtilizationRate
公式:(当 )
-
contracts/lendingpool/LendingPoolCore.sol:962-975直接以totalBorrows.rayDiv(availableLiquidity.add(totalBorrows))计算 RAY 精度的 (若 totalBorrows 和 availableLiquidity 均为 0,则 U=0)。这里的rayDiv就是白皮书中提到的“按 精度表示的利用率”。 -
totalBorrows由CoreLibrary.ReserveData.getTotalBorrows()给出,即 ;availableLiquidity为核心合约持有的实际余额(Balance);totalLiquidity = availableLiquidity + totalBorrows,对应白皮书的 。 -
该函数被 updateReserveInterestRatesAndTimestampInternal()调用,将 传入DefaultReserveInterestRateStrategy.calculateInterestRates(),触发白皮书 §1.2.4 描述的分段利率曲线。 -
当 时返回 0,避免了除零情况,也符合白皮书对“无人借款时资金成本为 0”的假设。
3.5 指数体系:白皮书 §1.2.8 的链上实现
,, 为“不落地的指数快照”。
-
contracts/libraries/CoreLibrary.sol:94-155的getNormalizedIncome()、updateCumulativeIndexes()完整复现上述公式: -
calculateLinearInterest(currentLiquidityRate, lastUpdateTimestamp)等价于 。 -
calculateCompoundedInterest(currentVariableBorrowRate, lastUpdateTimestamp)借助WadRayMath.rayPow计算 -
把二者分别乘上 lastLiquidityCumulativeIndex、lastVariableBorrowCumulativeIndex,即可得到白皮书所述的惰性累计。 -
contracts/lendingpool/LendingPoolCore.sol:1407-1476所有updateStateOn*在修改本金前都会先调用reserves[_reserve].updateCumulativeIndexes(),使用的是和白皮书一样的“先补齐指数,再变更本金”的顺序。 -
AToken.balanceOf()依赖core.getReserveNormalizedIncome()(白皮书的 ),将指数差转换为余额增长,见 §4.1。
3.6 利率刷新:白皮书 §1.2.4/§1.2.7 ↔ DefaultReserveInterestRateStrategy
contracts/lendingpool/DefaultReserveInterestRateStrategy.sol:1180-1218 内部重新计算 utilizationRate = totalBorrows.rayDiv(available.add(totalBorrows)),并按照白皮书的 Kink 曲线输出浮动/稳定/流动性利率:
-
当 : -
currentVariableBorrowRate = baseVariableBorrowRate + (U/U_{optimal}) * variableRateSlope1 -
currentStableBorrowRate = marketBorrowRate + (U/U_{optimal}) * stableRateSlope1 -
当 : -
先算 excess = (U - U_{optimal}) / (1 - U_{optimal}) -
变量/稳定利率分别额外叠加 slope2 * excess -
currentLiquidityRate = getOverallBorrowRateInternal(...) * utilizationRate,正是白皮书公式 。
这些新利率由 LendingPoolCore.updateReserveInterestRatesAndTimestampInternal() 写回 ReserveData,再由指数函数消化,形成“公式 → 状态 → 余额”的闭环。
3.7 数据出口与抵押开关
-
Core 自身提供 getUserBasicReserveData、getReserveConfiguration、getReserveNormalizedIncome、getReserveCurrent[Stable|Variable]BorrowRate等视图函数,DataProvider 直接从这里取原始值,外部前端不用触碰复杂的存储结构。 -
isUserAllowedToBorrowAtStable、isReserveBorrowingEnabled、isUserUseReserveAsCollateralEnabled这些布尔接口被入口层频繁调用,用于组合出“是否可以进行稳定利率借款”“余额减少是否被允许”等策略。 -
对于上层治理, LendingPoolConfigurator只是代理,把各种 set/enable/disable 直接委托给 Core 执行,Core 保持最小权限并只负责最终数据写入。
function getUserBasicReserveData(address _reserve, address _user) external view returns (uint256, uint256, uint256, bool){ CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; uint256 underlyingBalance = getUserUnderlyingAssetBalance(_reserve, _user); if (user.principalBorrowBalance == 0) { // 没有借款时只需要返回存款余额与抵押标记 return (underlyingBalance, 0, 0, user.useAsCollateral); } return ( underlyingBalance, // 通过用户的借款指数计算复利后的债务 user.getCompoundedBorrowBalance(reserve), user.originationFee, user.useAsCollateral );}function isUserAllowedToBorrowAtStable( address _reserve, address _user, uint256 _amount) external view returns (bool) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; if (!reserve.isStableBorrowRateEnabled) return false; return // 若用户未将该储备作为抵押或储备本身不可抵押,则允许稳定模式 !user.useAsCollateral || !reserve.usageAsCollateralEnabled || // 否则要求借款金额大于其抵押余额,避免“同资产自借”套利 _amount > getUserUnderlyingAssetBalance(_reserve, _user);}
sequenceDiagram LendingPool->>LendingPoolCore: getUserBasicReserveData() LendingPoolCore-->>LendingPool: (liquidity, borrow, fee, collateralFlag) LendingPool->>LendingPoolCore: isUserAllowedToBorrowAtStable() LendingPoolCore-->>LendingPool: true/false DataProvider->>LendingPoolCore: getReserveConfiguration()

4 AToken 与 DataProvider:凭证与估值
AToken 作为 LendingPoolCore 上资产的凭证,负责把 liquidityIndex 映射到 ERC20 余额,并通过 LendingPoolDataProvider 产生的全局数据来限制转账、计算健康度。本章聚焦两个文件:contracts/tokenization/AToken.sol 与 contracts/lendingpool/LendingPoolDataProvider.sol。
4.1 aToken 的指数余额模型
-
mintOnDeposit、burnOnLiquidation、transferOnLiquidation都只能被LendingPool调用;铸造或销毁之前,cumulateBalanceInternal会把用户的balanceIncrease铸造成一小段额外 aToken,并刷新userIndexes[address] = core.getReserveNormalizedIncome(). -
redeem先累加余额、校验isTransferAllowed,再调用pool.redeemUnderlying把资产解锁,同时在余额被清零时通过resetDataOnZeroBalanceInternal清除利息重定向与用户索引。 -
balanceOf会把本金与重定向来的余额一起乘以core.getReserveNormalizedIncome / userIndex,totalSupply也用同样的指数,所以存款人无需单独领取收益,余额在链上自动随时间增长。
function mintOnDeposit(address _account, uint256 _amount) external onlyLendingPool { // 先把已有利息累积到本金,保证指数与余额同步 (, , uint256 balanceIncrease, uint256 index) = cumulateBalanceInternal(_account); updateRedirectedBalanceOfRedirectionAddressInternal( _account, balanceIncrease.add(_amount), 0 ); // 若用户正在 redirect,先把新增余额同步给重定向地址 _mint(_account, _amount); emit MintOnDeposit(_account, _amount, balanceIncrease, index);}function redeem(uint256 _amount) external { (, uint256 currentBalance, uint256 balanceIncrease, uint256 index) = cumulateBalanceInternal(msg.sender); uint256 amountToRedeem = _amount == UINT_MAX_VALUE ? currentBalance : _amount; require(amountToRedeem <= currentBalance, "User cannot redeem more than the available balance"); require(isTransferAllowed(msg.sender, amountToRedeem), "Transfer cannot be allowed."); updateRedirectedBalanceOfRedirectionAddressInternal( msg.sender, balanceIncrease, amountToRedeem ); _burn(msg.sender, amountToRedeem); // 若余额被清空,重置利息重定向以减少存储 bool userIndexReset = currentBalance.sub(amountToRedeem) == 0 && resetDataOnZeroBalanceInternal(msg.sender); pool.redeemUnderlying(underlyingAssetAddress, msg.sender, amountToRedeem, currentBalance.sub(amountToRedeem)); emit Redeem(msg.sender, amountToRedeem, balanceIncrease, userIndexReset ? 0 : index);}
sequenceDiagram LendingPool->>AToken: mintOnDeposit(account, amount) AToken->>LendingPoolCore: cumulateBalanceInternal(account) LendingPoolCore-->>AToken: normalizedIncome/index AToken-->>User: 铸造 aToken User->>AToken: redeem(amount) AToken->>LendingPool: redeemUnderlying(...)

4.2 健康因子 与 DataProvider:白皮书 §1.1/§1.2.10
白皮书公式:
-
contracts/lendingpool/LendingPoolDataProvider.sol:70-155的calculateUserGlobalData会: -
遍历所有储备,以预言机价格把存款/借款折算成 ETH; -
按照储备的 baseLtv与liquidationThreshold加权求出全局 与 ; -
返回 healthFactor,其内部调用calculateHealthFactorFromBalancesInternal,完全等价于白皮书的 公式。 -
calculateHealthFactorFromBalancesInternal(L322-L334)直接复刻公式,且以HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18作为“是否低于 1”的判断基准。 -
需要注意 currentLiquidationThreshold在储备配置中是以“百分比(0-100)”储存的,因此函数内部先执行collateralBalanceETH.mul(liquidationThreshold).div(100)把抵押价值按阈值折算,再交给wadDiv以 1e18 精度除以借款与费用。白皮书里的 同样是 0-1 的比率,所以链上实现与公式在单位上保持一致。 -
balanceDecreaseAllowed(L180-L247)在 aToken 转账/赎回前调用同样的健康因子计算:把要转出的_amount转为 ETH,重新计算 与新的抵押余额,再判断healthFactorAfterDecrease > 1e18。这就是白皮书 §3.2/§3.3 中“赎回/转移必须保持 ”的实现。 -
calculateCollateralNeededInETH(L258-L284)则给出了“为了新增借款需要多少抵押”的推导,依赖前面求得的加权 LTV,和白皮书 §3.3 的理论一致。
function calculateHealthFactorFromBalancesInternal( uint256 _collateralBalanceETH, uint256 _borrowBalanceETH, uint256 _totalFeesETH, uint256 _currentLiquidationThreshold) internal pure returns (uint256) { if (_borrowBalanceETH == 0) return uint256(-1); return _collateralBalanceETH .wadMul(_currentLiquidationThreshold) .wadDiv(_borrowBalanceETH.add(_totalFeesETH));}
wadMul/wadDiv 保证 以 精度计算,避免了浮点误差。
4.3 清算二次校验:balanceDecreaseAllowed 与 liquidationCall
-
当清算人调用 LendingPool.liquidationCall时,LendingPoolLiquidationManager会再次通过calculateUserGlobalData获取healthFactorBelowThreshold(contracts/lendingpool/LendingPoolLiquidationManager.sol:534-542),只有 才会继续执行,这与白皮书“以健康因子判断是否可被清算”保持一致。 -
calculateAvailableCollateralToLiquidate(L585-L600)是白皮书清算公式的链上版本:当抵押不足以覆盖Amount时,会反推principalAmountNeeded,实现白皮书所述“按比例扣抵押”的机制。 -
Origination fee 清算逻辑(L591-L605)则将费用视作额外的 Amount再跑一次上述公式,确保费用也能按 liquidation bonus 折价买入。
4.4 转账与利息重定向(延续原笔记)
-
所有 _transfer都走whenTransferAllowed修饰器,它会调用balanceDecreaseAllowed,因此上一节的健康因子计算同样适用于普通转账/赎回。 -
executeTransferInternal在实际转账前会分别对from和to做一次cumulateBalanceInternal,并通过updateRedirectedBalanceOfRedirectionAddressInternal把应计利息同步到重定向地址。这样即便用户开启了redirectInterestStream,其委托人也能接收到每一笔余额变化。 -
redirectInterestStream/redirectInterestStreamOf允许用户或被授权者把未来利息重定向到另一个地址,allowInterestRedirectionTo和redirectedBalances则负责授信与记账;清算与赎回完成后若余额归零,利息重定向也会被自动重置。
function executeTransferInternal( address _from, address _to, uint256 _value) internal { require(_value > 0, "Transferred amount needs to be greater than zero"); (, uint256 fromBalance, uint256 fromBalanceIncrease, uint256 fromIndex) = cumulateBalanceInternal(_from); (, , uint256 toBalanceIncrease, uint256 toIndex) = cumulateBalanceInternal(_to); updateRedirectedBalanceOfRedirectionAddressInternal(_from, fromBalanceIncrease, _value); updateRedirectedBalanceOfRedirectionAddressInternal(_to, toBalanceIncrease.add(_value), 0); super._transfer(_from, _to, _value); bool fromIndexReset = fromBalance.sub(_value) == 0 && resetDataOnZeroBalanceInternal(_from); emit BalanceTransfer( _from, _to, _value, fromBalanceIncrease, toBalanceIncrease, fromIndexReset ? 0 : fromIndex, toIndex );}
5 数学底座:WadRayMath 与利率分段策略
5.1 Wad/Ray 精度:白皮书 §1.2.8 的数值基础
公式中的所有利率、指数都以 ray()或 wad()表示
-
contracts/libraries/WadRayMath.sol:37-84定义了wadMul/wadDiv/rayMul/rayDiv,它们分别实现通过在分子加半个单位实现四舍五入(half-up rounding),而非银行家舍入(ties-to-even),从而保证利率/指数乘除不会因为 Solidityuint256整除而失去精度。 -
wadToRay/rayToWad用WAD_RAY_RATIO = 1e9在两个精度之间转换,供calculateHealthFactorFromBalancesInternal等函数把 LTV/HealthFactor(wad)与指数(ray)混用。 -
rayPow则是白皮书 的实现:它通过“二进制快速幂 + 每次迭代的rayMul”计算指数函数,供CoreLibrary.calculateCompoundedInterest调用。因为rayPow只接受 ray 单位,整个协议的复利计算才能保持高精度。
5.2 分段利率策略再访
-
DefaultReserveInterestRateStrategy的 piecewise 函数前面已经在 §3.6 讨论,这里强调该策略与WadRayMath完全绑定: -
所有 slope/基础利率都以 ray 表示; -
utilizationRate、excessUtilizationRateRatio使用rayDiv,避免浮点误差; -
getOverallBorrowRateInternal将稳定/浮动借款金额先wadToRay再rayMul,与白皮书“按金额加权”一致。 -
这种“Ray 精度 + 两段斜率”的组合确保白皮书中的连续函数可以直接映射为 Solidity 算术。
function redirectInterestStreamInternal(address _from, address _to) internal { address currentRedirectionAddress = interestRedirectionAddresses[_from]; require(_to != currentRedirectionAddress, "Interest is already redirected to the user"); ( uint256 previousPrincipalBalance, uint256 fromBalance, uint256 balanceIncrease, uint256 fromIndex ) = cumulateBalanceInternal(_from); require(fromBalance > 0, "Interest stream can only be redirected if there is a valid balance"); if (currentRedirectionAddress != address(0)) { // 旧重定向账户需要减去用户本金,否则会重复计息 updateRedirectedBalanceOfRedirectionAddressInternal(_from, 0, previousPrincipalBalance); } if (_to == _from) { // 将利息重定向给自己意味着撤销重定向 interestRedirectionAddresses[_from] = address(0); emit InterestStreamRedirected(_from, address(0), fromBalance, balanceIncrease, fromIndex); return; } interestRedirectionAddresses[_from] = _to; updateRedirectedBalanceOfRedirectionAddressInternal(_from, fromBalance, 0); emit InterestStreamRedirected(_from, _to, fromBalance, balanceIncrease, fromIndex);}
sequenceDiagram User->>AToken: transfer(to, value) AToken->>LendingPoolDataProvider: balanceDecreaseAllowed() AToken->>LendingPoolCore: cumulateBalanceInternal(from/to) AToken->>AToken: updateRedirectedBalanceOfRedirectionAddressInternal() AToken-->>User: BalanceTransfer 事件 User->>AToken: redirectInterestStream(target) AToken->>LendingPoolCore: cumulateBalanceInternal(user)

4.3 LendingPoolDataProvider 的聚合视图
-
calculateUserGlobalData遍历core.getReserves(),为每个储备读取getUserBasicReserveData和getReserveConfiguration,再结合IPriceOracleGetter估值成 ETH,计算totalLiquidityBalanceETH/totalCollateralBalanceETH/totalBorrowBalanceETH/totalFeesETH、加权 LTV 与 Liquidation Threshold,并输出healthFactor与healthFactorBelowThreshold。 -
balanceDecreaseAllowed、calculateCollateralNeededInETH、calculateAvailableBorrowsETHInternal和calculateHealthFactorFromBalancesInternal是入口层风险控制的支撑:前者用于 aToken 转账/赎回时的余额减少校验,后两者则告诉借款路径“想再借多少需要多少抵押物”“当前抵押还能借多少 ETH”。 -
对前端或分析工具, getReserveData、getReserveConfigurationData、getUserAccountData与getUserReserveData提供了完整的储备与账户视图,免去了直接读 Core 底层存储的复杂度。
function calculateUserGlobalData(address _user) public view returns ( uint256 totalLiquidityBalanceETH, uint256 totalCollateralBalanceETH, uint256 totalBorrowBalanceETH, uint256 totalFeesETH, uint256 currentLtv, uint256 currentLiquidationThreshold, uint256 healthFactor, bool healthFactorBelowThreshold ){ IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); address[] memory reserves = core.getReserves(); for (uint256 i = 0; i < reserves.length; i++) { ( uint256 compoundedLiquidityBalance, uint256 compoundedBorrowBalance, uint256 originationFee, bool userUsesReserveAsCollateral ) = core.getUserBasicReserveData(reserves[i], _user); if (compoundedLiquidityBalance == 0 && compoundedBorrowBalance == 0) continue; ( uint256 reserveDecimals, uint256 baseLtv, uint256 liquidationThreshold, bool usageAsCollateralEnabled ) = core.getReserveConfiguration(reserves[i]); uint256 tokenUnit = 10 ** reserveDecimals; uint256 reserveUnitPrice = oracle.getAssetPrice(reserves[i]); if (compoundedLiquidityBalance > 0) { // 使用预言机价格将存款折算成 ETH uint256 liquidityBalanceETH = reserveUnitPrice.mul(compoundedLiquidityBalance).div(tokenUnit); totalLiquidityBalanceETH = totalLiquidityBalanceETH.add(liquidityBalanceETH); if (usageAsCollateralEnabled && userUsesReserveAsCollateral) { // 只有开启抵押的储备才会累加到 collateral totalCollateralBalanceETH = totalCollateralBalanceETH.add(liquidityBalanceETH); currentLtv = currentLtv.add(liquidityBalanceETH.mul(baseLtv)); currentLiquidationThreshold = currentLiquidationThreshold.add( liquidityBalanceETH.mul(liquidationThreshold) ); } } if (compoundedBorrowBalance > 0) { // 借款和 origination fee 同样按 ETH 计价 totalBorrowBalanceETH = totalBorrowBalanceETH.add( reserveUnitPrice.mul(compoundedBorrowBalance).div(tokenUnit) ); totalFeesETH = totalFeesETH.add( originationFee.mul(reserveUnitPrice).div(tokenUnit) ); } } currentLtv = totalCollateralBalanceETH > 0 ? currentLtv.div(totalCollateralBalanceETH) : 0; currentLiquidationThreshold = totalCollateralBalanceETH > 0 ? currentLiquidationThreshold.div(totalCollateralBalanceETH) : 0; // Health Factor < 1 即可被清算 healthFactor = calculateHealthFactorFromBalancesInternal( totalCollateralBalanceETH, totalBorrowBalanceETH, totalFeesETH, currentLiquidationThreshold ); healthFactorBelowThreshold = healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD;}

sequenceDiagram Frontend->>LendingPoolDataProvider: calculateUserGlobalData(user) DataProvider->>LendingPoolCore: getReserves() loop 每个储备 DataProvider->>LendingPoolCore: getUserBasicReserveData() DataProvider->>LendingPoolCore: getReserveConfiguration() DataProvider->>PriceOracle: getAssetPrice(reserve) end DataProvider-->>Frontend: collateral/borrow/healthFactor AToken->>LendingPoolDataProvider: balanceDecreaseAllowed()
function balanceDecreaseAllowed(address _reserve, address _user, uint256 _amount) external view returns (bool){ balanceDecreaseAllowedLocalVars memory vars; ( vars.decimals, , vars.reserveLiquidationThreshold, vars.reserveUsageAsCollateralEnabled ) = core.getReserveConfiguration(_reserve); if ( !vars.reserveUsageAsCollateralEnabled || !core.isUserUseReserveAsCollateralEnabled(_reserve, _user) ) { return true; } ( , vars.collateralBalanceETH, vars.borrowBalanceETH, vars.totalFeesETH, , vars.currentLiquidationThreshold, , ) = calculateUserGlobalData(_user); if (vars.borrowBalanceETH == 0) { // 没有借款则随意转出 return true; } IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); vars.amountToDecreaseETH = oracle.getAssetPrice(_reserve).mul(_amount).div(10 ** vars.decimals); vars.collateralBalancefterDecrease = vars.collateralBalanceETH.sub(vars.amountToDecreaseETH); if (vars.collateralBalancefterDecrease == 0) { return false; } vars.liquidationThresholdAfterDecrease = vars .collateralBalanceETH .mul(vars.currentLiquidationThreshold) .sub(vars.amountToDecreaseETH.mul(vars.reserveLiquidationThreshold)) .div(vars.collateralBalancefterDecrease); uint256 healthFactorAfterDecrease = calculateHealthFactorFromBalancesInternal( vars.collateralBalancefterDecrease, vars.borrowBalanceETH, vars.totalFeesETH, vars.liquidationThresholdAfterDecrease ); // 只有在转出后 health factor 仍大于阈值时才允许 return healthFactorAfterDecrease > HEALTH_FACTOR_LIQUIDATION_THRESHOLD;}
5 策略与治理模块
入口层与核心层之间还有一组“控制平面”合约,负责利率曲线、费用、参数治理与地址注册,确保状态与策略可以独立升级。
5.1 利率策略:DefaultReserveInterestRateStrategy
-
DefaultReserveInterestRateStrategy在构造时固定OPTIMAL_UTILIZATION_RATE = 80%,把利用率划成两段 Kink 曲线:未超过拐点时按baseVariableBorrowRate + utilization/optimal * slope1增长,一旦突破则用slope2快速抬升利率。 -
稳定利率部分以上述曲线叠加 ILendingRateOracle提供的市场利率;变量利率则直接围绕baseVariableBorrowRate调整。calculateInterestRates返回 (liquidityRate, stableBorrowRate, variableBorrowRate),再由 Core 缓存成储备的当前利率。 -
getOverallBorrowRateInternal把稳定/浮动借款金额按权重换算成单一利率,供liquidityRate = utilization * overallBorrowRate使用,从而把借款收入在指数中摊给存款人。
function calculateInterestRates( address _reserve, uint256 _availableLiquidity, uint256 _totalBorrowsStable, uint256 _totalBorrowsVariable, uint256 _averageStableBorrowRate) external view returns (uint256 currentLiquidityRate, uint256 currentStableBorrowRate, uint256 currentVariableBorrowRate){ uint256 totalBorrows = _totalBorrowsStable.add(_totalBorrowsVariable); uint256 utilizationRate = (totalBorrows == 0 && _availableLiquidity == 0) ? 0 : totalBorrows.rayDiv(_availableLiquidity.add(totalBorrows)); // 稳定利率以预言机的市场借款利率为基线 currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle()) .getMarketBorrowRate(_reserve); if (utilizationRate > OPTIMAL_UTILIZATION_RATE) { uint256 excessUtilizationRateRatio = utilizationRate .sub(OPTIMAL_UTILIZATION_RATE) .rayDiv(EXCESS_UTILIZATION_RATE); // 超过拐点后使用 slope2 快速抬升借款利率 currentStableBorrowRate = currentStableBorrowRate .add(stableRateSlope1) .add(stableRateSlope2.rayMul(excessUtilizationRateRatio)); currentVariableBorrowRate = baseVariableBorrowRate .add(variableRateSlope1) .add(variableRateSlope2.rayMul(excessUtilizationRateRatio)); } else { // 未超过拐点时按 slope1 线性插值 currentStableBorrowRate = currentStableBorrowRate.add( stableRateSlope1.rayMul( utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE) ) ); currentVariableBorrowRate = baseVariableBorrowRate.add( utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE).rayMul(variableRateSlope1) ); } currentLiquidityRate = getOverallBorrowRateInternal( _totalBorrowsStable, _totalBorrowsVariable, currentVariableBorrowRate, _averageStableBorrowRate ).rayMul(utilizationRate); // 存款收益 = 利用率 * 借款平均利率}
sequenceDiagram LendingPoolCore->>InterestRateStrategy: calculateInterestRates(reserveData) InterestRateStrategy->>LendingRateOracle: getMarketBorrowRate() InterestRateStrategy-->>LendingPoolCore: (liquidityRate, stableRate, variableRate) LendingPoolCore-->>Reserves: 更新 currentLiquidityRate/currentBorrowRates

5.2 费用与参数提供者
-
FeeProvider目前把originationFeePercentage固定为 0.25% (0.0025 * 1e18),calculateLoanOriginationFee简单地对_amount做wadMul。如果未来要做折扣或分级收费,也可以通过替换实现来完成。 -
LendingPoolParametersProvider给入口层提供全局的常量,如MAX_STABLE_RATE_BORROW_SIZE_PERCENT = 25(单次稳定利率借款最多占可用流动性的 25%)、REBALANCE_DOWN_RATE_DELTA(触发稳定利率重平衡的阈值)以及闪电贷费率(FLASHLOAN_FEE_TOTAL=35, FLASHLOAN_FEE_PROTOCOL=3000)。这些值直接被LendingPool.borrow、flashLoan和LendingPoolLiquidationManager使用。
function calculateLoanOriginationFee(address _user, uint256 _amount) external view returns (uint256){ // 目前未根据 _user 区分费率,直接返回金额 * 0.25% return _amount.wadMul(originationFeePercentage);}function getFlashLoanFeesInBips() external pure returns (uint256, uint256) { // (总费率, 协议分润);其余部分将流向存款人 return (FLASHLOAN_FEE_TOTAL, FLASHLOAN_FEE_PROTOCOL);}
sequenceDiagram LendingPool->>FeeProvider: calculateLoanOriginationFee(user, amount) FeeProvider-->>LendingPool: amount * originationFee% LendingPool->>ParametersProvider: getFlashLoanFeesInBips() ParametersProvider-->>LendingPool: (totalFee, protocolFee)

5.3 配置器:LendingPoolConfigurator
-
LendingPoolConfigurator只允许addressesProvider.getLendingPoolManager()调用,负责所有储备级别的治理操作。initReserve会部署一个新的AToken、把它注册到 Core,并绑定外部提供的利率策略;removeLastAddedReserve则可删除尾部储备。 -
参数修改方面,包括 enable/disableBorrowingOnReserve、enableReserveAsCollateral、setReserveBaseLTVasCollateral、setReserveLiquidationThreshold/Bonus/Decimals、enableReserveStableBorrowRate、activate/deactivate/freeze/unfreezeReserve等函数。治理合约或多签只需调用 Configurator,就能同步调整 Core 内的所有风险字段。
function initReserve( address _reserve, uint8 _underlyingAssetDecimals, address _interestRateStrategyAddress) external onlyLendingPoolManager { ERC20Detailed asset = ERC20Detailed(_reserve); string memory aTokenName = string(abi.encodePacked("Aave Interest bearing ", asset.name())); string memory aTokenSymbol = string(abi.encodePacked("a", asset.symbol())); // 若底层 ERC20 未提供 name/decimals,可使用 initReserveWithData 直接传自定义值 initReserveWithData( _reserve, aTokenName, aTokenSymbol, _underlyingAssetDecimals, _interestRateStrategyAddress );}function enableBorrowingOnReserve(address _reserve, bool _stableRateEnabled) external onlyLendingPoolManager{ LendingPoolCore core = LendingPoolCore(poolAddressesProvider.getLendingPoolCore()); // 直接调用 Core 的可借开关,顺便设置是否允许稳定利率 core.enableBorrowingOnReserve(_reserve, _stableRateEnabled); emit BorrowingEnabledOnReserve(_reserve, _stableRateEnabled);}function enableReserveAsCollateral( address _reserve, uint256 _baseLTVasCollateral, uint256 _liquidationThreshold, uint256 _liquidationBonus) external onlyLendingPoolManager { LendingPoolCore core = LendingPoolCore(poolAddressesProvider.getLendingPoolCore()); core.enableReserveAsCollateral( _reserve, _baseLTVasCollateral, _liquidationThreshold, _liquidationBonus ); // 事件中会带出新的 LTV、阈值与清算奖励,方便前端追踪 emit ReserveEnabledAsCollateral( _reserve, _baseLTVasCollateral, _liquidationThreshold, _liquidationBonus );}
sequenceDiagram Governance->>Configurator: initReserve(reserve,...) Configurator->>LendingPoolCore: initReserve() Governance->>Configurator: enableBorrowingOnReserve() Configurator->>LendingPoolCore: enableBorrowingOnReserve() Governance->>Configurator: enableReserveAsCollateral() Configurator->>LendingPoolCore: enableReserveAsCollateral()

5.4 注册表:LendingPoolAddressesProvider
-
LendingPoolAddressesProvider是协议的 DNS,所有上层组件在初始化时都会向它拉取地址。setLendingPoolImpl、setLendingPoolCoreImpl、setLendingPoolConfiguratorImpl等函数内部统一调用updateImplInternal,使用InitializableAdminUpgradeabilityProxy把新实现部署到同一代理地址上,实现平滑升级。 -
对于不适合走代理的合约(如 LendingPoolLiquidationManager需要delegatecall),地址提供者则直接_setAddress。同时它还统一存储了价格预言机、借贷利率预言机、FeeProvider、TokenDistributor、LendingPoolManager 等治理位置信息。 -
因为所有组件都依赖这一注册表,升级流程通常是:部署新实现 -> 通过地址提供者设置代理 -> 相关合约在下一次调用时读取到新地址,从而实现模块化演进。
function setLendingPoolImpl(address _pool) public onlyOwner { // 所有实现升级都统一通过 updateImplInternal,保持代理地址不变 updateImplInternal(LENDING_POOL, _pool); emit LendingPoolUpdated(_pool);}function setLendingPoolCoreImpl(address _lendingPoolCore) public onlyOwner { updateImplInternal(LENDING_POOL_CORE, _lendingPoolCore); emit LendingPoolCoreUpdated(_lendingPoolCore);}function setLendingPoolConfiguratorImpl(address _configurator) public onlyOwner { updateImplInternal(LENDING_POOL_CONFIGURATOR, _configurator); emit LendingPoolConfiguratorUpdated(_configurator);}function updateImplInternal(bytes32 _id, address _newAddress) internal { address payable proxyAddress = address(uint160(getAddress(_id))); InitializableAdminUpgradeabilityProxy proxy = InitializableAdminUpgradeabilityProxy(proxyAddress); bytes memory params = abi.encodeWithSignature("initialize(address)", address(this)); if (proxyAddress == address(0)) { // 首次设置时直接部署新的代理,并把实现与 admin 指向当前 provider proxy = new InitializableAdminUpgradeabilityProxy(); proxy.initialize(_newAddress, address(this), params); _setAddress(_id, address(proxy)); emit ProxyCreated(_id, address(proxy)); } else { // 已存在则直接调用 upgradeToAndCall 完成滚动升级 proxy.upgradeToAndCall(_newAddress, params); }}
sequenceDiagram Governance->>AddressesProvider: setLendingPoolImpl(newImpl) AddressesProvider->>Proxy: updateImplInternal() Proxy-->>Governance: ProxyCreated/upgrade event LendingPool->>AddressesProvider: getLendingPoolCore() AddressesProvider-->>LendingPool: proxy address

夜雨聆风
