- 示例工程
- 区块链
【免费下载链接】Dapp-Learning
Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.
本文基于 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、CollateralFactorRiskManager:风险管理模块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 做了两个分支:
- 若调用者是 creator(即主入口合约
Euler.sol),说明是模块内部通过emitViaProxy_Transfer/emitViaProxy_Approval等方式发日志,fallback 直接用log0~log4内联汇编将该事件的 topic 与 data 记录在该模块自己的 Proxy 地址上,而不是堆叠在主入口合约上; - 若是外部用户调用,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 一致):
Storage 载入:将
lastInterestAccumulatorUpdate、underlyingDecimals、interestRateModel、interestRate、reserveFee、pricingType、pricingParameters、reserveBalance、totalBalances、totalBorrows、interestAccumulator等字段读入缓存。精度统一:
underlyingDecimalsScaler = 10**(18 - underlyingDecimals),maxExternalAmount = MAX_SANE_AMOUNT / underlyingDecimalsScaler;再调用callBalanceOf得到poolSize,若poolSize <= maxExternalAmount则poolSize *= scaler统一为 1e18 精度,否则置 0(防溢出保护)。复利累积:当
block.timestamp != lastInterestAccumulatorUpdate时标记dirty = true,并计算:deltaT = block.timestamp - lastInterestAccumulatorUpdatenewInterestAccumulator = 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 持有者份额"的方式计入,而非凭空增发。
溢出检查与回写:仅当
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); }流程三步走:
- 若
lastAverageLiquidityUpdate == 0,说明该用户此前从未添加过流动性(无抵押资产),不更新; - 计算上次更新到当前时间的
deltaT,若为 0(同一区块内重复触发),不更新; - 刷新更新时间戳,并调用
computeNewAverageLiquidity重新计算平均流动性。
例如在 EToken 的 mint 流程 中,第一步就是updateAverageLiquidity(account),保证任何借贷行为前用户的平均流动性都是最新的。
8.2computeNewAverageLiquidity
按时间加权算法计算窗口内平均流动性:
AVERAGE_LIQUIDITY_PERIOD = prevDuration + currDuration NewAverageLiquidity = (prevDuration * prevAverageLiquidity + currDuration * currAverageLiquidity) / AVERAGE_LIQUIDITY_PERIODuint 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); }逻辑拆解:
currDuration为本次更新的时间跨度,且最大不超过窗口 24 小时(deltaT超过窗口则截断);prevDuration = AVERAGE_LIQUIDITY_PERIOD - currDuration,即上一次快照在窗口内还占用的时间;- 当前流动性
currAverageLiquidity即抵押价值超出负债价值的部分,最小为 0(资不抵债时按 0 计); - 最终结果是新旧两段流动性按各自持续时长加权的平均值——既不是瞬时值,也不是简单滑动平均,而是"滚动窗口内的时间加权平均"。
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 协议最核心的工程与算法决策:
- 模块化代理:通过
_createProxy统一创建模块代理、登记trustedSenders,结合 Proxy fallback 的call → dispatch → delegatecall调用链,实现"逻辑分散、状态集中"的分模块升级架构; - 子账户零存储:用 XOR 派生地址、OR 校验隶属,256 个子账户无需任何 mapping 存储;
- 精度与防溢出:统一 1e18 内部精度、
MAX_SANE_AMOUNT上限、encodeAmount编码,在复利累积与流动性计算中全程防溢出; - 复利与准备金:
initAssetCache一次性完成利息累积、债务膨胀、准备金计提,并用dirty标记控制回写; - 时间加权平均流动性: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.
相关推荐
Transformer-XL应用场景探索:从文本生成到代码补全的10个用例
Transformer XL应用场景探索:从文本生成到代码补全的10个用例 Transformer XL是一种突破性的语言模型,它通过引入循环机制和记忆增强技术
深度学习NLP大模型Diem 区块链账户冻结机制深度解析:AccountFreezing 模块源码与实战指南
Diem 区块链账户冻结机制深度解析:AccountFreezing 模块源码与实战指南 账户冻结(Account Freezing)是合规型区块链网络的必备能
区块链金融科技Diem Move 框架源码解读:AccountLimits 账户限额模块与滑动时间窗口机制
Diem Move 框架源码解读:AccountLimits 账户限额模块与滑动时间窗口机制 0x1::AccountLimits 是 Diem(原 Libra
区块链金融科技
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考