乐于分享
好东西不私藏

AAVE v1 源码阅读

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,内部遵循“权限验证 → 状态更新 → 资产划转”的三段式流程,各种修饰器(onlyActiveReserveonlyOverlyingAToken 等)统一了安全边界。赎回场景中,用户往往直接与 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。它维护 ReserveDataUserReserveDataliquidityIndexborrowIndex 等惰性计息变量,并通过 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):它保存最新的 LendingPoolCoreConfigurator 等地址,为可升级系统提供解耦。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 在入口层仍然先做安全校验,再把状态计算、凭证铸造与资金托管分发给下层组件:

  1. 权限与状态检查nonReentrantonlyActiveReserveonlyUnfreezedReserveonlyAmountGreaterThanZero 共同保证储备可用、金额有效、防止重入攻击。
  2. 识别首次存款者:读取 aToken.balanceOf(msg.sender) 判定 isFirstDeposit,方便 Core 在更新状态时将 useAsCollateral 从 0 切换到 1。
  3. 状态更新core.updateStateOnDeposit 刷新储备的流动性指数和时间戳,并在首次存款时开启抵押标记;实际余额的变动由随后 transferToReserve 的资金转账体现。
  4. aToken 铸造aToken.mintOnDeposit 将 _amount 直映为 ERC20 余额,不需要额外汇率,因为 aToken 余额后续会乘 liquidityIndex 自动增长。
  5. 资金划转与事件core.transferToReserve 将资产真正存入保险库(ERC20 路径调用 safeTransferFrom,ETH 路径根据 msg.value 进账并退还多余金额),随后 Deposit 事件把储备、金额、推荐码广播给前端或积分系统。
  6. 资产类型处理:函数被标记为 payable 方便 ETH 存款,而 ERC20 存款所需的授权与转账逻辑全部封装在 Core 内部,协议代码显式区分两种资产路径。
  7. 隐式前置条件:用户在存 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” 并保持流水线式的校验顺序:

  1. 入口修饰器与储备可借性nonReentrantonlyActiveReserveonlyUnfreezedReserveonlyAmountGreaterThanZero 校验基本条件,随后 core.isReserveBorrowingEnabledcore.getReserveAvailableLiquidity 保障储备层允许借款且流动性足够。传入的 _interestRateMode 必须是 1 (STABLE) 或 2 (VARIABLE),否则直接 revert。
  2. 账户总览校验:通过 dataProvider.calculateUserGlobalData(msg.sender) 一次性取回用户的抵押余额、借款余额、总费用、LTV、Liquidation Threshold 以及 healthFactorBelowThreshold 标记。要求抵押余额大于 0 且当前 Health Factor 不低于 1,否则禁止继续借款。
  3. 开仓费用与抵押需求feeProvider.calculateLoanOriginationFee 对借款金额计算开仓费用,并且要求费用 > 0(防止金额过小)。接着使用 dataProvider.calculateCollateralNeededInETH 折算为 ETH 计价的最小抵押物需求,该公式将 _amountborrowFee、已有借款与费用一并纳入,只有当 userCollateralBalanceETH ≥ amountOfCollateralNeededETH 时才能借款。
  4. 稳定利率额外限制:若用户选择稳定利率,core.isUserAllowedToBorrowAtStable 会检查储备是否开启稳定模式、用户抵押资产是否与借款资产过度同质,以及当前金额是否允许;parametersProvider.getMaxStableRateBorrowSizePercent 则限制稳定利率借款在总可用流动性中的占比,防止一笔交易吸干储备。
  5. 状态刷新与资金划转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_VALUEuint256(-1)) 则代表“尽可能还多”:

  1. 读取债务与费用:通过 core.getUserBorrowBalances 得到 principalBorrowBalancecompoundedBorrowBalanceborrowBalanceIncrease,再取出 core.getUserOriginationFee。如果债务为零直接 revert。
  2. 确定还款金额:默认 paybackAmount = compounded + originationFee,若调用者传入 _amount != UINT_MAX_VALUE 且更小,则裁剪,且要求“代还”场景必须显式金额。ETH 储备下 msg.value 至少等于 paybackAmount
  3. 仅偿还费用的分支:当 paybackAmount <= originationFee 时,说明用户只需偿还费用。core.updateStateOnRepay 会以 0 principal 更新状态,并把 vars.paybackAmount 全额通过 core.transferToFeeCollectionAddress 送往 TokenDistributor(注意:此分支下费用从 _onBehalfOf 账户扣除)。
  4. 标准偿还流程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 授权)。
  5. 事件记录:无论是哪条路径,最终都会 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

闪电贷允许外部合约在一次交易内借出资金并归还,入口函数贯彻“余额守恒 + 费用拆分”原则:

  1. 准备与计费onlyActiveReserveonlyAmountGreaterThanZero 后,函数根据资产种类读取 availableLiquidityBefore(ETH 取 address(core).balance,ERC20 读取 IERC20(_reserve).balanceOf(address(core))),确认储备足以覆盖 _amountparametersProvider.getFlashLoanFeesInBips 返回总费率与协议分润,amountFee = _amount * totalFeeBips / 10000protocolFee = amountFee * protocolFeeBips / 10000,并要求这两个值都大于 0。
  2. 借出与回调:将 _receiver 强转为 IFlashLoanReceiver,通过 core.transferToUser 把 _amount 划给 receiver,随后调用 receiver.executeOperation(_reserve, _amount, amountFee, _params)。此回调必须在同一交易内完成。
  3. 归还与校验:执行完回调后再次读取 availableLiquidityAfter,要求其等于 availableLiquidityBefore + amountFee,即本金 + 总费用都已经回到 Core;若不满足则 revert。
  4. 状态更新与事件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,后者在同一个存储上下文中完成所有校验与资产转移:

  1. 入口与委托LendingPool.liquidationCall 只做储备活跃性检查和 nonReentrant 保护,然后对 addressesProvider.getLendingPoolLiquidationManager() 发起 delegatecall。如果返回的 (returnCode, returnMessage) 非 0,会拼接字符串并 revert,确保只要清算失败就回滚整笔交易。
  2. 健康度与抵押检查:管理器内首先调用 dataProvider.calculateUserGlobalData(_user),只有当 healthFactorBelowThreshold == true 时才允许继续。随后读取 core.getUserUnderlyingAssetBalance(_collateral)core.isReserveUsageAsCollateralEnabled 和 core.isUserUseReserveAsCollateralEnabled,确认该抵押品可被清算,再通过 core.getUserBorrowBalances(_reserve, _user) 检查该用户确实借入了指定的债务资产。
  3. 可清算额度计算:遵循 LIQUIDATION_CLOSE_FACTOR_PERCENT = 50vars.maxPrincipalAmountToLiquidate = userCompoundedBorrowBalance * 50%,实际处理金额取 _purchaseAmount 与上限的较小值。calculateAvailableCollateralToLiquidate 利用价格预言机读取 _collateral 与 _reserve 的 ETH 价格,再乘以 core.getReserveLiquidationBonus(_collateral) 得到最多可 seize 的抵押资产数量;若该抵押不足以覆盖 _purchaseAmount,函数会下调 actualAmountToLiquidate
  4. 费用与资产转移:若用户仍有 originationFee,会再调用 calculateAvailableCollateralToLiquidate 分配一部分抵押作为费用抵押并记账给协议。对于 _receiveAToken == true,直接 AToken.transferOnLiquidation 将 aToken 余额转给清算人;否则先 burnOnLiquidation 销毁 aToken,再由 core.transferToUser 发放等值底层资产。同时,清算人偿还的本金(ETH 或 ERC20)由 core.transferToReserve.value(msg.value) 收进协议,若费用被清算则额外 core.liquidateFee 发送到 TokenDistributor 并触发 OriginationFeeLiquidated
  5. 状态更新与事件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 同时保存资金侧指标(lastLiquidityCumulativeIndexlastVariableBorrowCumulativeIndexcurrentLiquidityRatetotalBorrowsStable/Variable)和风险参数(baseLTVasCollateralliquidationThresholdliquidationBonusborrowingEnabledisStableBorrowRateEnabled 等)。初始化时 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 提供给事件。
  • updateStateOnRepayupdateStateOnLiquidation 和 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 计算 
    • 把二者分别乘上 lastLiquidityCumulativeIndexlastVariableBorrowCumulativeIndex,即可得到白皮书所述的惰性累计。
  • 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 曲线输出浮动/稳定/流动性利率:

  1. 当 
    • currentVariableBorrowRate = baseVariableBorrowRate + (U/U_{optimal}) * variableRateSlope1
    • currentStableBorrowRate = marketBorrowRate + (U/U_{optimal}) * stableRateSlope1
  2. 当 
    • 先算 excess = (U - U_{optimal}) / (1 - U_{optimal})
    • 变量/稳定利率分别额外叠加 slope2 * excess
  3. currentLiquidityRate = getOverallBorrowRateInternal(...) * utilizationRate,正是白皮书公式 

这些新利率由 LendingPoolCore.updateReserveInterestRatesAndTimestampInternal() 写回 ReserveData,再由指数函数消化,形成“公式 → 状态 → 余额”的闭环。

3.7 数据出口与抵押开关

  • Core 自身提供 getUserBasicReserveDatagetReserveConfigurationgetReserveNormalizedIncomegetReserveCurrent[Stable|Variable]BorrowRate 等视图函数,DataProvider 直接从这里取原始值,外部前端不用触碰复杂的存储结构。
  • isUserAllowedToBorrowAtStableisReserveBorrowingEnabledisUserUseReserveAsCollateralEnabled 这些布尔接口被入口层频繁调用,用于组合出“是否可以进行稳定利率借款”“余额减少是否被允许”等策略。
  • 对于上层治理,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 的指数余额模型

  • mintOnDepositburnOnLiquidationtransferOnLiquidation 都只能被 LendingPool 调用;铸造或销毁之前,cumulateBalanceInternal 会把用户的 balanceIncrease 铸造成一小段额外 aToken,并刷新 userIndexes[address] = core.getReserveNormalizedIncome().
  • redeem 先累加余额、校验 isTransferAllowed,再调用 pool.redeemUnderlying 把资产解锁,同时在余额被清零时通过 resetDataOnZeroBalanceInternal 清除利息重定向与用户索引。
  • balanceOf 会把本金与重定向来的余额一起乘以 core.getReserveNormalizedIncome / userIndextotalSupply 也用同样的指数,所以存款人无需单独领取收益,余额在链上自动随时间增长。
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 会:
    1. 遍历所有储备,以预言机价格把存款/借款折算成 ETH;
    2. 按照储备的 baseLtv 与 liquidationThreshold 加权求出全局  与 
    3. 返回 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 获取 healthFactorBelowThresholdcontracts/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),从而保证利率/指数乘除不会因为 Solidity uint256 整除而失去精度。
  • 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 表示;
    • utilizationRateexcessUtilizationRateRatio 使用 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
  • balanceDecreaseAllowedcalculateCollateralNeededInETHcalculateAvailableBorrowsETHInternal 和 calculateHealthFactorFromBalancesInternal 是入口层风险控制的支撑:前者用于 aToken 转账/赎回时的余额减少校验,后两者则告诉借款路径“想再借多少需要多少抵押物”“当前抵押还能借多少 ETH”。
  • 对前端或分析工具,getReserveDatagetReserveConfigurationDatagetUserAccountData 与 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.borrowflashLoan 和 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/disableBorrowingOnReserveenableReserveAsCollateralsetReserveBaseLTVasCollateralsetReserveLiquidationThreshold/Bonus/DecimalsenableReserveStableBorrowRateactivate/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,所有上层组件在初始化时都会向它拉取地址。setLendingPoolImplsetLendingPoolCoreImplsetLendingPoolConfiguratorImpl 等函数内部统一调用 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
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » AAVE v1 源码阅读

评论 抢沙发

3 + 8 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮