news 2026/10/13 5:55:51

Euler BaseLogic 源码解析:模块化代理、子账户体系与流动性核算机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Euler BaseLogic 源码解析:模块化代理、子账户体系与流动性核算机制
  • 示例工程
  • 区块链

【免费下载链接】Dapp-Learning

Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.

项目地址:https://gitcode.com/gh_mirrors/da/Dapp-Learning
点击查看免费下载

本文基于 Dapp-Learning 仓库中 defi/Euler/contracts/BaseLogic.md 展开,结合仓库内 Euler 协议总览、模块代理模式、RiskManager、Markets 等文档与源码片段,系统拆解 Euler 借贷协议的公共逻辑层(BaseLogic)——它是所有模块共用的基础工具集:模块代理创建、子账户地址派生、市场进入管理、资产配置解析、利息累积与平均流动性核算。读完本文,你将理解 Euler 如何用按位异或实现子账户、如何通过 Proxy 委托调用完成模块化升级、以及 TWAP 定价与利息复利累积在底层合约中的落地方式。

一、BaseLogic 在 Euler 中的定位

Euler 是一个模块化的去中心化借贷协议,其合约并非单一大合约,而是由一个主入口合约(Euler.sol,存储全部状态)加若干功能模块组成。根据 defi/Euler/contracts/README.md,Euler 一共 9 个模块:

  • DToken:债务凭证
  • Etoken:抵押凭证
  • Exec:执行合约(可批量执行操作)
  • Governance:治理模块
  • Installer:模块安装器,涉及模块的设置和权限
  • Liquidiation:清算模块
  • Markets:资产市场,存储资产在 Euler 借贷市场中的相关参数,例如BorrowFactor、CollateralFactor
  • RiskManager:风险管理模块
  • Swap:交易模块(用户 EToken 的交易,不是 underlying)

BaseLogic是这些模块共享的"公共逻辑层",它不负责某一具体业务,而是提供各模块都会复用的基础设施函数,例如创建模块代理、解析资产配置、刷新利息累积器、更新用户平均流动性等。正因为状态与逻辑分离、逻辑再被各模块复用,Euler 才能实现"按需分模块升级"的模块化可升级架构(详见 defi/Euler/ModuleProxyMode.md)。

二、模块代理创建:_createProxy

各模块都通过_createProxy函数创建自己的 Proxy 合约实例。该函数在 defi/Euler/contracts/BaseLogic.md 中的实现如下:

function _createProxy(uint proxyModuleId) internal returns (address) { require(proxyModuleId != 0, "e/create-proxy/invalid-module"); require(proxyModuleId <= MAX_EXTERNAL_MODULEID, "e/create-proxy/internal-module"); // If we've already created a proxy for a single-proxy module, just return it: if (proxyLookup[proxyModuleId] != address(0)) return proxyLookup[proxyModuleId]; // Otherwise create a proxy: address proxyAddr = address(new Proxy()); if (proxyModuleId <= MAX_EXTERNAL_SINGLE_PROXY_MODULEID) proxyLookup[proxyModuleId] = proxyAddr; trustedSenders[proxyAddr] = TrustedSenderInfo({ moduleId: uint32(proxyModuleId), moduleImpl: address(0) }); emit ProxyCreated(proxyAddr, proxyModuleId); return proxyAddr; }

关键逻辑说明:

  • 参数校验:proxyModuleId不能为 0,且不能超过MAX_EXTERNAL_MODULEID。模块 ID 分为内部模块与外部模块两类,内部模块(如RiskManager)不在创建时暴露给外部调用。
  • 单实例缓存:对于MAX_EXTERNAL_SINGLE_PROXY_MODULEID以内的模块,一个模块只对应一个 Proxy 实例,已创建过就直接从proxyLookup返回,避免重复部署。以 defi/Euler/contracts/Proxy.md 中展示的 Euler 构造函数为例:Installer模块部署完成后,Euler.sol构造函数即调用_createProxy(MODULEID__INSTALLER)为其创建代理,并把trustedSenders[installerProxy].moduleImpl绑定到 Installer 实现合约。
  • 信任登记:新代理地址被写入trustedSenders,记录其moduleId,moduleImpl稍后由 Installer 在安装实现合约时填充。这构成了"只有被信任的模块代理发来的调用才会被执行"的信任边界。

在 Markets 模块 的doActivateMarket中可以看到它的典型调用方式:激活一个新资产市场时,分别用_createProxy(MODULEID__ETOKEN)与_createProxy(MODULEID__DTOKEN)为该资产创建抵押凭证与债务凭证两个代理。

2.1 模块代理的委托调用链

模块 Proxy 本身几乎没有任何业务函数,所有调用都会落入 fallback。根据 defi/Euler/contracts/Proxy.md 的剖析,Proxy 的 fallback 做了两个分支:

  1. 若调用者是 creator(即主入口合约Euler.sol),说明是模块内部通过emitViaProxy_Transfer/emitViaProxy_Approval等方式发日志,fallback 直接用log0~log4内联汇编将该事件的 topic 与 data 记录在该模块自己的 Proxy 地址上,而不是堆叠在主入口合约上;
  2. 若是外部用户调用,fallback 会将 calldata 重组为dispatch selector + calldata + caller address,再用call调用主入口的dispatch();dispatch再以delegatecall执行对应模块 Implementation 的逻辑。

这样设计的精妙之处在于:函数运行逻辑在模块 Implementation 合约中,而读写的状态全部在主入口合约Euler.sol上,因此模块的安装与升级不会影响协议既有状态。该调用链的全流程示意见 defi/Euler/ModuleProxyMode.md(含部署、安装/升级、用户调用三条流程的图示)。

三、子账户体系:按位异或生成、按位或校验

大多数借贷协议用 mapping 或数组在主账户下罗列子账户序号;Euler 则采用更省 gas 的地址派生方案:不存储任何子账户映射,而是直接通过按位异或(XOR)运算从主账户地址生成子账户地址,校验隶属关系时再用按位或(OR)运算判断。

以 defi/Euler/contracts/BaseLogic.md 中的示例(假设地址为 uint32 类型的0x45)说明:

// primary account 0x45 // subaccounts 0x45 ^ 0x0 // 0x45 1000101 subaccount 0 as primary 0x45 ^ 0x1 // 0x44 1000100 subaccount 1 0x45 ^ 0x2 // 0x47 1000111 subaccount 2 0x45 ^ 0x3 // 0x46 1000110 subaccount 3 ... 0x45 ^ 0xF // 0x4A 1001010 subaccount 15 // subacounts verify get same result 0x45 | 0xF // 0x4F 1001111 0x44 | 0xF // 0x4F 1001111 // not subaccount verify get different result 0x3d | 0xF // 0x3F 111111

对应源码:

function getSubAccount(address primary, uint subAccountId) internal pure returns (address) { require(subAccountId < 256, "e/sub-account-id-too-big"); return address(uint160(primary) ^ uint160(subAccountId)); } function isSubAccountOf(address primary, address subAccount) internal pure returns (bool) { return (uint160(primary) | 0xFF) == (uint160(subAccount) | 0xFF); }

原理拆解:

  • 地址空间划分:一个地址的低 8 位(0xFF)被用来编码子账户序号,因此一个主账户最多拥有 256 个子账户(subAccountId从 0 到 255,校验subAccountId < 256)。subAccountId = 0即主账户本身。
  • 生成:primary ^ subAccountId只翻转低 8 位,得到该序号对应的子账户地址,完全确定、无需存储。
  • 校验:primary | 0xFF与subAccount | 0xFF都等于"把地址低 8 位置 1"的结果。只要subAccount是由primary通过某个0~255序号异或得到的,两者按位或的结果必然一致;而无关地址(如示例中的0x3d)按位或后结果不同,判定为不属于该主账户。

这种方案在 Liquidation 模块 中也有应用:computeLiqOpp第一步就用isSubAccountOf(violator, liquidator)拒绝"自己清算自己的子账户"(e/liq/self-liquidation)。

四、已进入市场列表:getEnteredMarketsArray

在 Euler 中,用户需要"进入"某个市场后才能在该资产上进行抵押与借贷(参见 Markets 模块 的doActivateMarket)。getEnteredMarketsArray负责把用户在marketsEntered[account]中的固定长度存储数组,转换为按实际进入数量裁剪的动态数组:

function getEnteredMarketsArray(address account) internal view returns (address[] memory) { uint32 numMarketsEntered = accountLookup[account].numMarketsEntered; address firstMarketEntered = accountLookup[account].firstMarketEntered; address[] memory output = new address[](numMarketsEntered); if (numMarketsEntered == 0) return output; address[MAX_POSSIBLE_ENTERED_MARKETS] storage markets = marketsEntered[account]; output[0] = firstMarketEntered; for (uint i = 1; i < numMarketsEntered; ++i) { output[i] = markets[i]; } return output; }

值得注意的两个设计:

  • 首个市场单独存储:firstMarketEntered单独存在accountLookup[account]上,而不放在marketsEntered数组里。结合numMarketsEntered计数,可以避免在用户只进入 1 个市场时读写整个数组,节省存储布局。
  • 返回动态数组:函数返回address[] memory,把存储数组拷贝为与numMarketsEntered等长的内存数组,供上层(如RiskManager.computeLiquidity)遍历使用。MAX_POSSIBLE_ENTERED_MARKETS是存储数组的固定容量上限。

该函数是流动性计算的数据入口:RiskManager.computeLiquidity正是先调用getEnteredMarketsArray(account)拿到用户进入的全部市场,再逐个资产计算抵押与负债价值(详见 RiskManager.md)。

五、资产配置解析:resolveAssetConfig

每个市场的资产配置(AssetConfig)保存在underlyingLookup[underlying]中,resolveAssetConfig负责取出配置并填充默认值:

uint32 internal constant DEFAULT_BORROW_FACTOR = uint32(0.28 * 4_000_000_000); uint24 internal constant DEFAULT_TWAP_WINDOW_SECONDS = 30 * 60; function resolveAssetConfig(address underlying) internal view returns (AssetConfig memory) { AssetConfig memory config = underlyingLookup[underlying]; // If address is zero, means config haven't initialized yet require(config.eTokenAddress != address(0), "e/market-not-activated"); if (config.borrowFactor == type(uint32).max) config.borrowFactor = DEFAULT_BORROW_FACTOR; if (config.twapWindow == type(uint24).max) config.twapWindow = DEFAULT_TWAP_WINDOW_SECONDS; return config; }

要点说明:

  • 激活校验:若eTokenAddress为address(0),说明该资产市场尚未激活,直接 reverte/market-not-activated。激活流程见 Markets.md:激活时会设置underlyingLookup[underlying] = params.config并创建 EToken/DToken 代理。
  • 默认值机制:AssetConfig的某些字段用type(uint32).max/type(uint24).max表示"未显式设置",resolveAssetConfig在读取时替换为默认值。这样做的好处是:治理设置配置时无需填充所有字段,未设置的自动回落默认值。
  • 负债系数borrowFactor:它是"真实负债资产"与"负债资产调整值"之间的除数,即liability risk-adjustment = assetValue / borrowFactor。默认值DEFAULT_BORROW_FACTOR = 0.28,数值上0.28 * 4_000_000_000是因为CONFIG_FACTOR_SCALE = 4_000_000_000(见 RiskManager.md)。
  • TWAP 窗口期:默认 30 分钟(30 * 60秒),用于 Uniswap V3 TWAP 预言机的时间窗口。

值得注意的是,新市场激活时 RiskManager 给出的默认参数极为保守(最低安全权限):borrowIsolated = true、collateralFactor = 0、borrowFactor = type(uint32).max、twapWindow = type(uint24).max,这正是为了让resolveAssetConfig的默认值机制接管(见 RiskManager.md)。

六、资产缓存:AssetCache与利息累积

6.1 结构体assetCache

AssetCache是 Euler 中每个资产池在单次操作内的"内存快照",把 storage 中分散的字段一次性载入 memory,减少重复 SLOAD。结构如下:

struct AssetCache { address underlying; uint112 totalBalances; uint144 totalBorrows; uint96 reserveBalance; uint interestAccumulator; uint40 lastInterestAccumulatorUpdate; uint8 underlyingDecimals; uint32 interestRateModel; int96 interestRate; uint32 reserveFee; uint16 pricingType; uint32 pricingParameters; uint poolSize; // result of calling balanceOf on underlying (in external units) uint underlyingDecimalsScaler; uint maxExternalAmount; }

各字段含义:

  • underlying:资产合约地址;
  • totalBalances/totalBorrows:池内总抵押(EToken 总量,内部精度)与总债务(DToken 总量);
  • reserveBalance:准备金余额,来自借款利息按reserveFee计提的部分;
  • interestAccumulator:利息累积器,复利累积的核心变量;
  • lastInterestAccumulatorUpdate:上次更新利息累积器的时间戳(uint40);
  • underlyingDecimals:底层代币精度;underlyingDecimalsScaler = 10^(18 - decimals)用于把余额统一到 1e18 内部精度;
  • interestRateModel/interestRate:当前使用的利率模型模块 ID 与当前利率;
  • reserveFee:准备金费率;pricingType/pricingParameters:定价方式与参数(如 Uniswap V3 池费率档位);
  • poolSize:调用balanceOf得到的池内底层资产余额(外部单位);
  • maxExternalAmount:MAX_SANE_AMOUNT / underlyingDecimalsScaler,用于防溢出上限检查。

6.2 初始化与利息累积:initAssetCache

initAssetCache将 storage 载入缓存,统一精度,并完成利息与准备金的复利更新。核心流程(与 defi/Euler/contracts/BaseLogic.md 一致):

  1. Storage 载入:将lastInterestAccumulatorUpdate、underlyingDecimals、interestRateModel、interestRate、reserveFee、pricingType、pricingParameters、reserveBalance、totalBalances、totalBorrows、interestAccumulator等字段读入缓存。

  2. 精度统一:underlyingDecimalsScaler = 10**(18 - underlyingDecimals),maxExternalAmount = MAX_SANE_AMOUNT / underlyingDecimalsScaler;再调用callBalanceOf得到poolSize,若poolSize <= maxExternalAmount则poolSize *= scaler统一为 1e18 精度,否则置 0(防溢出保护)。

  3. 复利累积:当block.timestamp != lastInterestAccumulatorUpdate时标记dirty = true,并计算:

    • deltaT = block.timestamp - lastInterestAccumulatorUpdate
    • newInterestAccumulator = RPow.rpow(interestRate + 1e27, deltaT, 1e27) * interestAccumulator / 1e27,即按(1 + rate)^deltaT复利增长(1e27为利率的定点数基数)
    • newTotalBorrows = totalBorrows * newInterestAccumulator / interestAccumulator,债务按累积器比例同步膨胀
    • 准备金计提:feeAmount = (newTotalBorrows - totalBorrows) * (reserveFee == type(uint32).max ? DEFAULT_RESERVE_FEE : reserveFee) / (RESERVE_FEE_SCALE * INTERNAL_DEBT_PRECISION);若有计提,则newTotalBalances = poolAssets * newTotalBalances / (poolAssets - feeAmount),newReserveBalance += newTotalBalances - totalBalances——准备金以"稀释 EToken 持有者份额"的方式计入,而非凭空增发。
  4. 溢出检查与回写:仅当newTotalBalances <= MAX_SANE_AMOUNT && newTotalBorrows <= MAX_SANE_DEBT_AMOUNT时才将新值写入缓存(encodeDebtAmount/encodeAmount/encodeSmallAmount为精度编码),并把lastInterestAccumulatorUpdate更新为uint40(block.timestamp)。

function initAssetCache(address underlying, AssetStorage storage assetStorage, AssetCache memory assetCache) internal view returns (bool dirty) { dirty = false; assetCache.underlying = underlying; // Storage loads assetCache.lastInterestAccumulatorUpdate = assetStorage.lastInterestAccumulatorUpdate; uint8 underlyingDecimals = assetCache.underlyingDecimals = assetStorage.underlyingDecimals; assetCache.interestRateModel = assetStorage.interestRateModel; assetCache.interestRate = assetStorage.interestRate; assetCache.reserveFee = assetStorage.reserveFee; assetCache.pricingType = assetStorage.pricingType; assetCache.pricingParameters = assetStorage.pricingParameters; assetCache.reserveBalance = assetStorage.reserveBalance; assetCache.totalBalances = assetStorage.totalBalances; assetCache.totalBorrows = assetStorage.totalBorrows; assetCache.interestAccumulator = assetStorage.interestAccumulator; // Derived state unchecked { assetCache.underlyingDecimalsScaler = 10**(18 - underlyingDecimals); assetCache.maxExternalAmount = MAX_SANE_AMOUNT / assetCache.underlyingDecimalsScaler; } uint poolSize = callBalanceOf(assetCache, address(this)); if (poolSize <= assetCache.maxExternalAmount) { unchecked { assetCache.poolSize = poolSize * assetCache.underlyingDecimalsScaler; } } else { assetCache.poolSize = 0; } // Update interest accumulator and reserves if (block.timestamp != assetCache.lastInterestAccumulatorUpdate) { dirty = true; uint deltaT = block.timestamp - assetCache.lastInterestAccumulatorUpdate; // Compute new values uint newInterestAccumulator = (RPow.rpow(uint(int(assetCache.interestRate) + 1e27), deltaT, 1e27) * assetCache.interestAccumulator) / 1e27; uint newTotalBorrows = assetCache.totalBorrows * newInterestAccumulator / assetCache.interestAccumulator; uint newReserveBalance = assetCache.reserveBalance; uint newTotalBalances = assetCache.totalBalances; uint feeAmount = (newTotalBorrows - assetCache.totalBorrows) * (assetCache.reserveFee == type(uint32).max ? DEFAULT_RESERVE_FEE : assetCache.reserveFee) / (RESERVE_FEE_SCALE * INTERNAL_DEBT_PRECISION); if (feeAmount != 0) { uint poolAssets = assetCache.poolSize + (newTotalBorrows / INTERNAL_DEBT_PRECISION); newTotalBalances = poolAssets * newTotalBalances / (poolAssets - feeAmount); newReserveBalance += newTotalBalances - assetCache.totalBalances; } // Store new values in assetCache, only if no overflows will occur if (newTotalBalances <= MAX_SANE_AMOUNT && newTotalBorrows <= MAX_SANE_DEBT_AMOUNT) { assetCache.totalBorrows = encodeDebtAmount(newTotalBorrows); assetCache.interestAccumulator = newInterestAccumulator; assetCache.lastInterestAccumulatorUpdate = uint40(block.timestamp); if (newTotalBalances != assetCache.totalBalances) { assetCache.reserveBalance = encodeSmallAmount(newReserveBalance); assetCache.totalBalances = encodeAmount(newTotalBalances); } } } }

返回的dirty布尔值用于提示调用方:本次操作是否刷新过利息,从而决定是否需要把缓存写回 storage。

七、利率模型:updateInterestRate

Euler 的利率根据资金利用率utilisation计算,且利率存在上下限MAX_ALLOWED_INTEREST_RATE与MIN_ALLOWED_INTEREST_RATE。利率模型是分段函数(折线),由专门的利率模型模块(如BaseIRMLinearKink)实现,BaseLogic.updateInterestRate只负责计算利用率并委托调用利率模型:

// BaseLogic.sol function updateInterestRate(AssetStorage storage assetStorage, AssetCache memory assetCache) internal { uint32 utilisation; { uint totalBorrows = assetCache.totalBorrows / INTERNAL_DEBT_PRECISION; uint poolAssets = assetCache.poolSize + totalBorrows; if (poolAssets == 0) utilisation = 0; // empty pool arbitrarily given utilisation of 0 else utilisation = uint32(totalBorrows * (uint(type(uint32).max) * 1e18) / poolAssets / 1e18); } bytes memory result = callInternalModule(assetCache.interestRateModel, abi.encodeWithSelector(BaseIRM.computeInterestRate.selector, assetCache.underlying, utilisation)); (int96 newInterestRate) = abi.decode(result, (int96)); assetStorage.interestRate = assetCache.interestRate = newInterestRate; }
  • 利用率计算:utilisation = totalBorrows / (poolSize + totalBorrows),即借款总额占(池内资产 + 借款总额)的比例;空池任意取 0。换算为uint32定点数(type(uint32).max作为 1e18 基数的满刻度)。
  • 模块调用:通过callInternalModule以computeInterestRate选择器调用interestRateModel模块,获得int96格式的新利率。
  • 上下限裁剪:BaseIRM.computeInterestRate在调用实现函数后裁剪:
// BaseIRM.sol function computeInterestRate(address underlying, uint32 utilisation) external returns (int96) { int96 rate = computeInterestRateImpl(underlying, utilisation); if (rate > MAX_ALLOWED_INTEREST_RATE) rate = MAX_ALLOWED_INTEREST_RATE; else if (rate < MIN_ALLOWED_INTEREST_RATE) rate = MIN_ALLOWED_INTEREST_RATE; return rate; } // BaseIRMLinearKink function computeInterestRateImpl(address, uint32 utilisation) internal override view returns (int96) { uint ir = baseRate; if (utilisation <= kink) { ir += utilisation * slope1; } else { ir += kink * slope1; ir += slope2 * (utilisation - kink); } return int96(int(ir)); }

BaseIRMLinearKink是典型的"线性拐点"模型:利用率低于kink时利率以slope1斜率线性增长(低利用率、低利率区间);超过kink后换用更陡的slope2斜率,激励借款人尽快还款、抑制资金池过度使用。利率的上下限裁剪保证了极端情况下利率不会脱离合理区间。

八、流动性核算:时间加权平均流动性

BaseLogic中关于流动性的三个函数构成了"用户平均流动性"的完整核算链路,是 清算 与健康系数计算的基础。

8.1updateAverageLiquidity

更新用户在时间窗口内的平均流动性。平均流动性定义为抵押资产价值减去负债资产价值,时间窗口AVERAGE_LIQUIDITY_PERIOD = 24 * 60 * 60(24 小时):

function updateAverageLiquidity(address account) internal { uint lastAverageLiquidityUpdate = accountLookup[account].lastAverageLiquidityUpdate; if (lastAverageLiquidityUpdate == 0) return; uint deltaT = block.timestamp - lastAverageLiquidityUpdate; if (deltaT == 0) return; accountLookup[account].lastAverageLiquidityUpdate = uint40(block.timestamp); accountLookup[account].averageLiquidity = computeNewAverageLiquidity(account, deltaT); }

流程三步走:

  1. 若lastAverageLiquidityUpdate == 0,说明该用户此前从未添加过流动性(无抵押资产),不更新;
  2. 计算上次更新到当前时间的deltaT,若为 0(同一区块内重复触发),不更新;
  3. 刷新更新时间戳,并调用computeNewAverageLiquidity重新计算平均流动性。

例如在 EToken 的 mint 流程 中,第一步就是updateAverageLiquidity(account),保证任何借贷行为前用户的平均流动性都是最新的。

8.2computeNewAverageLiquidity

按时间加权算法计算窗口内平均流动性:

AVERAGE_LIQUIDITY_PERIOD = prevDuration + currDuration NewAverageLiquidity = (prevDuration * prevAverageLiquidity + currDuration * currAverageLiquidity) / AVERAGE_LIQUIDITY_PERIOD
uint internal constant AVERAGE_LIQUIDITY_PERIOD = 24 * 60 * 60; function computeNewAverageLiquidity(address account, uint deltaT) private returns (uint) { uint currDuration = deltaT >= AVERAGE_LIQUIDITY_PERIOD ? AVERAGE_LIQUIDITY_PERIOD : deltaT; uint prevDuration = AVERAGE_LIQUIDITY_PERIOD - currDuration; uint currAverageLiquidity; { (uint collateralValue, uint liabilityValue) = getAccountLiquidity(account); currAverageLiquidity = collateralValue > liabilityValue ? collateralValue - liabilityValue : 0; } return (accountLookup[account].averageLiquidity * prevDuration / AVERAGE_LIQUIDITY_PERIOD) + (currAverageLiquidity * currDuration / AVERAGE_LIQUIDITY_PERIOD); }

逻辑拆解:

  1. currDuration为本次更新的时间跨度,且最大不超过窗口 24 小时(deltaT超过窗口则截断);
  2. prevDuration = AVERAGE_LIQUIDITY_PERIOD - currDuration,即上一次快照在窗口内还占用的时间;
  3. 当前流动性currAverageLiquidity即抵押价值超出负债价值的部分,最小为 0(资不抵债时按 0 计);
  4. 最终结果是新旧两段流动性按各自持续时长加权的平均值——既不是瞬时值,也不是简单滑动平均,而是"滚动窗口内的时间加权平均"。

8.3getAccountLiquidity

计算用户当前的抵押资产价值与负债资产价值,委托给RiskManager模块的computeLiquidity:

function getAccountLiquidity(address account) internal returns (uint collateralValue, uint liabilityValue) { bytes memory result = callInternalModule(MODULEID__RISK_MANAGER, abi.encodeWithSelector(IRiskManager.computeLiquidity.selector, account)); (IRiskManager.LiquidityStatus memory status) = abi.decode(result, (IRiskManager.LiquidityStatus)); collateralValue = status.collateralValue; liabilityValue = status.liabilityValue; }

RiskManager.computeLiquidity的完整实现见 defi/Euler/contracts/RiskManager.md。其核心思想(更详细的推导见 defi/Euler/LeverageLendingAlgorithmOfEuler.md):

  • 遍历getEnteredMarketsArray(account)返回的每个市场;
  • 抵押价值 = 数量 × 价格 ×collateralFactor;负债价值 = 数量 × 价格 /borrowFactor;
  • Euler 在 Compound 的Collateral Factor基础上引入Borrow Factor,使不同资产的风险调整更灵活:Collateral用CF(值 × CF)调整,Self-Collateral用固定SCF = 0.95调整,Liabilities用BF(值 / BF)调整,Self-Liability用固定SBF = 1调整;
  • 由于 mint 操作会生成等值的Self-Collateral与Self-Liability,计算时先假设某资产的全部债务都是Self-Liability,反推Self-Collateral = Self-Liability / 0.95;若反推出的抵押超过实际抵押数量,则把Self-Collateral重设为实际抵押,超出部分计入常规Liabilities。

最终Liquidity = Collateral_RA - Liabilities_RA,当抵押与负债的风险调整价值相等时HealthScore = 1,处于可清算临界点。该机制同时支持最高 19 倍杠杆的 mint 操作与杠杆做空(详细数值案例见 Euler 协议总览 与 杠杆借贷算法)。下图对比了 Compound 与 Euler 的流动性计算模型:

8.4 平均流动性在清算中的应用

用户的时间加权平均流动性并非仅为统计用途,它直接参与清算折扣的定价。在 Liquidation 模块 的computeLiqOpp中:

  • baseDiscount = UNDERLYING_RESERVES_FEE + (1e18 - healthScore),健康系数越低,基础折扣越大;
  • computeDiscountBooster(liquidator, liabilityValue)用清算人 24 小时平均流动性除以被清算人负债价值得到一个 1e18 精度的乘数,清算人提供的平均流动性越多,享受的清算折扣越大(booster上限为 1e18,即不会超过基础折扣加MAXIMUM_BOOSTER_DISCOUNT);
  • 最终折扣discount = baseDiscount * discountBooster / 1e18,并受MAXIMUM_DISCOUNT上限约束。

清算时以把被清算人健康系数恢复到目标值TARGET_HEALTH(1.25)为基准计算应偿还的债务repay与应获得的抵押yield,且repay中还要扣除UNDERLYING_RESERVES_FEE的准备金费。这与传统"固定折扣 + 优先 gas 拍卖"不同,形成了一种接近荷兰式拍卖的清算机制(详见 defi/Euler/README.md 与 defi/Euler/contracts/Liquidation.md)。

九、价格预言机背景

BaseLogic中resolveAssetConfig提到的twapWindow与AssetCache中的pricingType/pricingParameters均服务于价格获取。根据 RiskManager.md 与 预言机说明,Euler 的定价体系为:

  • PRICINGTYPE__PEGGED = 1:锚定类型,参考资产(当前为 WETH)价格恒为 1;
  • PRICINGTYPE__UNISWAP3_TWAP = 2:从该资产与 WETH 组成的 Uniswap V3 池读取 TWAP 价格,pricingParameters存储池子费率档位;新市场激活时自动遍历[3000, 10000, 500, 100]四档费率,选取流动性最好的池子(见 RiskManager.md);
  • PRICINGTYPE__FORWARDED = 3:PToken 转发定价。

由于 TWAP 价格依赖 Uniswap V3 池的流动性,引用流动性差的池子存在被操纵的风险——项目方因此建立了从 A 到 F 的预言机风险评级体系,任何低于 B 的资产都建议避免使用(详见 defi/Euler/oracle/readme.md)。

十、总结:BaseLogic 的设计启示

BaseLogic虽名为"公共逻辑",却浓缩了 Euler 协议最核心的工程与算法决策:

  1. 模块化代理:通过_createProxy统一创建模块代理、登记trustedSenders,结合 Proxy fallback 的call → dispatch → delegatecall调用链,实现"逻辑分散、状态集中"的分模块升级架构;
  2. 子账户零存储:用 XOR 派生地址、OR 校验隶属,256 个子账户无需任何 mapping 存储;
  3. 精度与防溢出:统一 1e18 内部精度、MAX_SANE_AMOUNT上限、encodeAmount编码,在复利累积与流动性计算中全程防溢出;
  4. 复利与准备金:initAssetCache一次性完成利息累积、债务膨胀、准备金计提,并用dirty标记控制回写;
  5. 时间加权平均流动性:24 小时滚动窗口 + 新旧加权,既避免瞬时波动干扰清算定价,又避免为每个用户存储多条历史快照。

对于希望深入理解模块化借贷协议实现的开发者,建议按 defi/Euler/ModuleProxyMode.md → defi/Euler/contracts/Proxy.md → defi/Euler/contracts/RiskManager.md → defi/Euler/contracts/Markets.md 的顺序阅读,即可串联起"代理架构 → 市场激活 → 资产缓存与利息 → 流动性计算"的完整链路。

  • 示例工程
  • 区块链

【免费下载链接】Dapp-Learning

Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.

项目地址:https://gitcode.com/gh_mirrors/da/Dapp-Learning
点击查看免费下载

相关推荐

上一篇:exo:多台设备组AI集群,RDMA让大模型跑得更快
下一篇:Roc 类型系统探秘:嵌套函数注解中的类型变量遮蔽(Type Variable Shadowing)与编译器快照测试验证

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:55:29

基于价值认同的分布式电能共享交易Matlab仿真与ADMM实现

1. 项目整体设计&#xff1a;价值认同如何嵌入电能共享交易1.1 需求侧电能共享到底解决什么问题传统电力系统里&#xff0c;用户只是被动的“买电者”&#xff0c;电价由电网或售电公司统一定价&#xff0c;用户几乎没有选择空间。但随着分布式光伏、储能、电动汽车这些“产消者…

作者头像 李华
网站建设 2026/10/13 5:53:55

快速幂算法原理与工程实践:从LeetCode 50题看健壮性设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:52:32

C++内联函数深度解析:从编译器行为到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:51:44

嵌入式工程师薪资差距背后:大厂如何定价,从20K到30K的进阶路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:51:42

SpringBoot+Docker+Jenkins自动化部署实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:51:35

C/C++面试三件套:语言、操作系统与网络如何串成一条线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华