架构

合约架构

模块系统

除了少量的调度逻辑(参见 Euler.sol),合约被组织成模块,存放在contracts/modules/ 中。

使用模块有几个原因:

  • 代理间接层,用于调度来自 ETokens 和 DTokens 等子合约的调用(见下文)

    • 每个代币都必须有自己的地址来兼容 ERC-20,即使所有存储都存在于Euler合约中

  • 合约升级

    • 模块可以升级,例如可以立即升级所有EToken

  • 避免达到约 24kb 的最大合约大小限制

有关模块 ID 的注册表,请参见文件 contracts/Constants.sol。模块分为 3 类:

  • 单代理模块:这些模块只能由单个地址访问。例如,市场激活是通过调用 Markets 模块的单个代理上的函数来完成的。

  • 多代理模块:这些模块有很多地址。例如,每个 EToken 都有一个地址,但是对它们的任何调用都被分派到单个 EToken 模块实例。

  • 内部模块:这些是Euler系统内部调用的模块,没有任何公共代理。这些仅对它们的升级功能有用,以及在测试/开发期间存储非生产代码的能力。例如 RiskManager 和利率模型 (IRM) 模块。

由于模块是基于委托来调用的,因此它们的构造函数中不应有任何与存储相关的初始化。在它们的构造函数中唯一应该做的就是初始化不可变变量,因为这些变量被嵌入到合约的字节码,而不是存储中。模块也不应该定义任何存储变量。在极少数情况下他们需要私有存储(即利率模型状态),他们应该使用非结构化存储。

代理

模块不能直接调用。相反,它们必须通过代理调用。所有代理都由相同的代码实现:contracts/Proxy.sol。这是一个非常简单的合约,它将其请求连同原始 msg.sender 一起转发到主 Euler 合约地址。调用是通过普通的 call() 完成的,因此执行发生在 Euler 合约的存储环境中,而不是代理的存储环境中。

代理包含转发所需的最少逻辑量。这是因为它们不可升级。理想情况下,它们应该很小,以最大限度地降低 gas 成本,因为其中许多将被部署(每个激活的市场至少 2 个)。

Euler合约确保所有对它的请求都来自一个已知的可信代理地址。使地址变得可信的唯一方法是Euler合约本身创建它们。这样,代理发送的原始msg.sender就可以信任了。

代理唯一要做的另一件事就是接受来自指示它们发出日志消息的Euler合约的信息。例如,如果调用 EToken 代理的 transfer 方法,则必须从 EToken 代理的地址而不是主 Euler 地址记录 Transfer 事件。

代理/模块系统提供的一个重要特性是单个存储环境(即主Euler合约)可以具有多个可能发生冲突的函数 ABI 命名空间,这对于像传统可升级代理或钻石标准这样的系统是不可能的。例如,Euler 提供了多个 ERC-20 接口,但不用担心 EToken 和 DToken(它们必须具有相同的选择器)的 balanceOf() 方法会发生冲突。

更多信息请查看docs/proxy-protocol.md

调度

除了代理之外,contracts/Euler.sol 是唯一不能升级的代码。它是主Euler合约的实施。本质上,它的唯一工作是成为Euler存储的占位符地址,并将delegatecall() 传递给适当的模块。

当它调用一个模块时,contracts/Euler.sol:dispatch() 将一些额外的数据附加到它从代理接收的msg.data 的末尾:

  • 它自己的msg.sender视图,对应于代理的地址。

  • 从(受信任的)代理传入的msgSender,对应于调用代理的原始msg.sender

它附加到数据末尾的原因是,这些额外信息不会干扰模块完成的 ABI 解码:Solidity 的 ABI 解码器可以容忍额外的尾随数据并忽略。这允许我们在与代理通信时直接使用从“solc”输出的模块接口,同时仍然允许函数提取代理地址和代理看到的原始“msg.sender”。

由于模块是由 delegatecall() 调用的,代理地址通常可用于模块代码作为 msg.sender,那么为什么需要将其传递给模块呢?这是因为批处理请求允许用户在不通过代理的情况下调用方法(见下文)。

模块

安装程序

使用的第一个模块是安装模块。该模块用于引导安装其余模块,稍后可用于升级模块从而添加新功能和/或修复错误。

目前这些功能是封闭的,因此upgradeAdmin地址是唯一可以升级模块的地址。但是,安装程序模块本身也是可升级的,因此随着我们向更高级别的去中心化迈进,这种逻辑可能会受到限制。

EToken

每个市场都有一个 EToken。这是Euler协议中资产代币化的主要接口:

  • deposit:将代币从您的钱包中转入Euler,并获得赚取的利息代币作为回报。

  • withdraw:将您的 EToken 兑换为标的代币,这些代币以及应计的任何利息从 Euler 转移到您的钱包。

此外,ETokens 提供了一个兼容 ERC-20 标准的接口,典型作用是允许您转移和批准 ETokens 的转移。

与 Compound 一样,但与 AAVE 不同的是,这些代币具有静态余额。也就是说,应计利息不会导致从 balanceOf 返回的值增加。相反,随着时间的推移,该固定余额使您有权收回越来越多的标的资产。尽管 AAVE 模型在概念上更好,但经验表明,增加余额代币会给集成商( integrators)带来很多痛苦。特别是,如果您将 X 个 EToken 转移到一个矿池合约中,然后又提取了同一个 X,那么您没有获得任何利息,并且池中还有一些通常不会分配给任何人的剩余 EToken。

Compound 模型的一个缺点是从 balanceOf 返回的值是内部记账单位,对外部用户没有任何意义。当然还有一个 balanceOfUnderlying 方法(名称与 Compound 的方法相同,这可能会成为事实上的标准),它返回标的的数量,并_does_增加块到块。

DToken

每个市场也有一个 DToken。这是Euler协议中债务代币化的主要接口:

  • borrow:如果您有足够的抵押品,Euler 会向您发送标的代币,并向您发放相应数量的债务代币。

  • repay:从您的钱包中转移代币来销毁 DToken,从而减少您的债务。

DTokens 还实现了部分兼容 ERC-20 的接口。与不可转让的 AAVE 不同,DToken 可以 被转让。许可逻辑与 ETokens 相反:虽然您可以在无许可的情况下将您的 ETokens 发送给任何人,但使用 DTokens 您可以在无许可的情况下“获取”任何其他人的 DTokens(假设您有足够的抵押品)。类似地,就像您可以批准另一个地址来获取您的一定数量的 EToken 一样,您可以使用approveDebt() 来授予另一个帐户向您发送一定数量的 DToken 的权限。

使用 approveDebt() 名称代替 ERC-20 approve() 是因为担心某些合约可能无意中允许自己接收“负值”代币。

除了为债务交易和转让提供灵活的平台外,该系统还允许在子账户之间轻松转移债务头寸(见下文)。

与 EToken 不同,DToken 余额_do_ 随着利息的归集而逐块增加。这意味着为了全额还清贷款,您应该将 MAX_UINT256 指定为要还清的金额,以便在偿还交易被开采时产生的所有利息都得到偿还。请注意,大多数 Euler 方法都接受此 MAX_UINT256 值,以指示合约应确定您在交易被挖掘时可以存款/取款/借款/偿还的最大金额。

在代码中,您还将看到INTERNAL_DEBT_PRECISION。这是因为 DToken 的跟踪精度比 EToken 更高(小数点后 27 位对 18 位),因此复利更准确。但是,为了呈现一个常见的外部小数金额,该内部精度对合约的外部用户是隐藏的。请注意,即使标的代币使用的小数位数少于 18,这些小数位数仍保持不变(请参阅下面的小数规范化部分)。

市场

该模块允许您在Euler协议上激活新市场。任何代币都可以被激活,只要它和参考资产之间存在 Uniswap 3 交易对(标准部署中的 WETH 版本 9,尽管可以使用任何 18 位十进制代币)。

它还允许您进入/退出市场,该市场控制您的哪些 EToken 用作您的债务的抵押品。这个术语是特意选择来匹配 Compound 的,因为我们的许多用户已经熟悉 Compound。

与为每个用户保留一个数组和一个映射的 Compound 不同,我们只保留一个数组。经过分析,我们意识到几乎每次对映射的访问都将在交易中完成,该交易_也_扫描数组(通常作为流动性检查),因此映射(几乎)是多余的,因此我们可以在进入市场时消除 SSTORE。此外,我们将长度存储在因其他原因加载的插槽中,而不是普通的以长度为前缀的存储数组。这节省了额外的 SSTORE,因为我们不需要更新数组长度,并在每次流动性检查时保存和 SLOAD(更重要的是柏林分叉后)。更进一步,还有一个优化,将第一个输入的市场地址存储在一个特殊变量中,该变量与这个长度一起打包。

最后,市场模块允许外部用户查询市场配置参数(即抵押因子)和当前状态(即利率)。

风险管理器

这是一个内部模块,意味着它只被其他模块调用,并且没有代理入口点。

激活市场时调用风险管理器,以获取新创建市场的默认风险参数。此外,在每次可能影响用户流动性的操作(取款、借入、转移 E/DToken、退出市场等)之后都会调用它,以确保没有发生流动性违规行为。这个逻辑可以在 BaseLogic 中实现,如果这样,效率会稍高一些,但是升级风险参数将需要升级几乎所有其他模块。

为了检查流动性,该模块必须检索资产价格(请参阅下面的定价部分)。

治理

该模块允许特定权限地址更新市场配置参数,例如 TWAP 间隔、借贷和抵押因子以及利率模型。

最终,此逻辑将得到增强,以支持 EUL 代币驱动的治理。

清算

该模块实现了清算系统(见下文)。

执行

这个模块实现了一些更高级的调用Euler合约的方法(下面进一步描述):

  • 批处理请求

  • 延期流动性检查

它还有一个入口点,用于查询有关帐户流动性状态的详细信息。

Swap

该模块允许用户在 Uniswap V3 和 1inch DEXes 上交换他们存入的标的代币。在幕后,代币直接从池中交换,从而节省了通常用于提取和存回交易资产的气体。从用户的角度来看,交换将改变他们 eToken 的余额。

与延期流动性检查(见下文)相结合,swap模块允许用户在任何抵押品与抵押品交易对上一键建立杠杆多头和空头头寸,并在任何抵押品与非抵押品交易对上进行一键式杠杆空头头寸。

可用的交换方法:

存储和继承

大多数模块都继承自提供通用借贷逻辑相关功能的BaseLogic。该合约继承自BaseModule,它继承自Base,而Base又继承自Storage

几乎所有基本模块中的函数都被声明为私有或内部。这是必要的,这样模块就不会导出意外的函数,并且 Solidity 编译器可以优化掉不需要的函数(并非所有模块都使用所有函数)。

contracts/Storage.sol 包含所有模块使用的存储布局。这种匹配很重要,因为所有模块都是用 Euler 合约环境中的 delegatecall() 调用的。此外,重要的是升级保留存储顺序和偏移量。测试 test/storage.js 有一个实施的开头,来获取 Soldity 编译器的存储布局输出并验证它在各个升级(版本)之间是否一致。部署第一个版本后,我们将“冻结”存储布局并在 test/storage.js 测试中对其进行编码。

定价

Euler 使用 Uniswap 3 作为其定价预言机。为了确保价格不受快照操纵的影响,这需要使用最近一段时间的时间加权平均价格 (TWAP)。

当市场被激活时,风险管理器(RiskManager) 在 uniswap 池上调用increaseObservationCardinalityNext() 来将 uniswap预言机的环形缓冲调整到最小值。默认情况下,这个大小是 144,因为这平均来说足以满足 30 分钟的 TWAP 窗口,假设出块时间为 12.5 秒。

Euler 合约将尝试检索每个工具的“twapWindow”参数的平均价格。如果由于环形缓冲区中最旧的值太新而无法对其进行服务,它将使用最旧的可用价格(我们已确保至少 144 个块龄)。

我们的博客系列更详细地描述了我们的定价系统:[https://medium.com/euler-xyz/prices-and-oracles-2da0126a138](https://medium.com/euler-xyz/prices-and-oracles -2da0126a138)

挂钩价格

上述 Uniswap 3 定价的一个例外是与参考资产等价的资产。这些资产可以具有“挂钩”的定价类型,这表明它们的价格始终与参考资产为 1:1。目前唯一挂钩的资产是参考资产本身,即 WETH。

价格转发

另一个例外是与另一种资产等价的资产,在这种情况下可以“转发”定价。这目前仅用于 pTokens

流动性延期

通常,在完成可能因违反抵押品规定而失败的操作(即,取出贷款、提取 EToken、退出市场)后,必须检查用户的流动性。这在每次操作后立即通过调用contracts/BaseLogic.sol:checkLiquidity() 来完成,它会调用内部RiskManager 模块的requireLiquidity(),如果账户抵押不足,它将回撤交易。

但是,这种模式会导致某些操作序列不必要地失败。例如,用户必须先存入 EToken 并进入市场,然后才能获得贷款,即使这是在同一个原子交易中完成的。

此外,这会导致不必要的gas消耗。考虑一下,一个用户在同一笔交易中取出两笔贷款:如果每次都检查流动性,这意味着进行了两次单独的流动性检查,每一次都需要访问价格,遍历输入的市场列表,并计算流动性(净资产和负债,转换为参考资产,并按相应的抵押和借入因子进行调整)。

流动性延期是对此的通用解决方案。用户(必须是智能合约,但请参阅下面的批处理请求)可以调用 Exec 模块中的 deferLiquidityCheck() 函数。此函数禁用指定账户的所有流动性检查,然后通过在 msg.sender 上调用 onDeferredLiquidityCheck() 函数重新进入调用者。在执行此回调时,checkLiquidity() 不会检查指定账户的流动性。函数返回后,将检查流动性。

除了gas优化和再融资贷款等正常用例外,这还允许用户获得 闪电贷

特别是对于闪电贷,该协议提供了一个适配器合约FlashLoan,它兼容 ERC-3156标准。适配器在内部使用流动性延期来借入代币,并额外要求在交易中全额偿还贷款。

eToken <> dToken Symmetry

eTokens 和 dTokens 的主要操作分别是存款/取款和借入/还款。但是,还有另一个接口在某些方面更为标的:mint/burn。这些操作同时适用于 eToken 和 dToken。铸币操作会创建等量的 eToken 和 dToken,并将两者分配给用户。销毁操作会销毁等量的 eToken 和 dToken。这些操作可以被认为是向自己借钱和还钱。或者,eTokens 和 dTokens 可以被认为是一种物质和反物质,在铸造时从“无”出现(不需要标的代币)并在销毁时相互抵消。

所有主要操作都可以重新概念化为 mint 和 burn 的变体。例如,如果没有借入(borrow)功能,则可以通过铸币(mint)和提款(withdraw)来实现:铸币将创建 eToken 和 dToken,然后提款将销毁 eToken,只留下 dToken。

  • deposit:铸币,还款

  • withdraw:借,烧掉

  • borrow:铸币,取款

  • repay:存款,烧毁

铸币和燃烧操作有一些实际优势。其中之一是可以通过销毁相应数量的 eToken 和贷款中的 dToken 来用 eToken 而不是标的物偿还贷款。当标的代币缺乏流动性时(可能是因为它已暂停),这可能很有用,但 eToken 仍有市场(顺便一说,清算部分中描述的稳定池是 eToken 到 eToken 市场的示例)。

使用 Swap 模块,Euler 用户可以通过在 Uniswap 上执行外部交换将一个 eToken 换成另一个。这通过避免存款/取款开销来节省用户gas。当与铸币结合使用时,就允许构建杠杆头寸,而无需任何标的代币传输到用户钱包。

eToken/dToken 对称性展露的另一个领域是清算。与清算人发送借来的代币和接收抵押品不同,Euler 的清算流程只是将借来的 dToken 和抵押品 eToken 从违规者转移到清算人。清算人通常会提取抵押品,进行交换,然后偿还以销毁 dToken,但这并不是绝对必要的。例如,如果池中可用的抵押代币不足,或者交换条件暂时不理想,清算人可以选择保留债务。

子账户

为了防止使用相同的抵押品借入多种资产这钟固有的问题,有时需要“隔离”借款。这对于高波动或不受信任的代币尤其重要。

Euler 实施这种借入隔离来保护出借方。但是,这可能会导致次优的用户体验。如果用户想要借入多个资产(并且一个或多个被隔离),则必须创建一个单独的钱包并为其提供资金。尽管拥有许多metamask帐户并没有什么问题,但这可能是一种糟糕的体验,尤其是使用硬件钱包时。

为了改进这一点,Euler支持子账户的概念。每个以太坊地址在 Euler 上有 256 个子账户(包括主账户)。每个子账户都有一个从 0 到 255 的子账户 ID,其中 0 是主账户的 ID。为了计算子账户地址,子账户 ID 被视为uint并与以太坊地址异或。

是的,这将地址的安全性降低了 8 位,但是在metamask中创建多个地址也会降低安全性:如果有人试图暴力破解您的 N>1 个私钥中的一个,那么他们每次猜测成功的机会是 N 倍.虽然必须承认子账户模型较弱,因为找到子账户的私钥可以访问_所有_子账户,但仍然有一个非常舒适的安全边际。

在Euler上,您只需要每个代币批准一次,然后就可以向您的任何子账户充值/还款。在子账户之间转移资产或负债无需批准。也可以使用批处理请求(见下文)对单个交易中的多个子账户进行操作。

Euler UI 将使您一目了然地查看您的子账户的构成,并根据需要重新平衡抵押品以维持您的债务头寸。

批处理请求

能够在单个交易中执行多个操作,有时会很有用。这对于通过摊销固定交易成本来减少 gas 开销很有价值,特别是如果操作涉及对同一存储插槽多次写入(伊斯坦布尔分叉后)和/或同一存储插槽的多次读取(柏林分叉后)。将原子性添加到一系列操作(它们都成功或都失败)也很有用。

在以太坊中,这些好处适用于智能合约,但不适用于 EOA(普通的私钥/公钥对帐户)。这是不幸的,因为许多用户不能/不会部署智能合约钱包。

作为对应的部分解决方案,contracts/modules/Exec.sol:batchDispatch() 函数允许在单个区块链交易中执行一组Euler交互。这是一个“部分”解决方案,因为用户不能在交互之间执行任意逻辑,但对于各种用例来说仍然足够了。

例如,为了向 Euler 提供抵押品,必须执行两个单独的步骤:存入 EToken,并进入该 EToken 的市场。不需要用户进行两个单独的交易,或者实现一个假设的depositAndEnter()函数(这意味着方法组合的爆炸),而可以使用批处理交易。

此外,可以在批处理交易中推迟一个或多个账户的流动性检查。这可以显着节省 gas 成本,并且可以在不需要智能合约的情况下实现类似闪电贷的再平衡。我们计划实施一个内置的交换功能,通过在 Uniswap 上执行交换将一个 EToken 转换为另一个。这将非常节省gas,因为代币不需要从外部钱包转入和移出 Euler 的钱包,并且还可以轻松创建杠杆头寸,即使对于 EOA 用户也是如此。

准备金

与 Compound 和 AAVE 类似,在池中赚取的一部分利息由协议作为费用收取。 Euler 再次使用与 Compound 相同的术语,将收取的费用总额称为“准备金金”。这些费用由治理控制,可以支付给 EUL 代币持有者,用于在资金池无力偿债时补偿出借方,或用于其他有利于协议的用途。

准备金金提供了一种资金缓冲,可以弥补由于头寸太小而无法清算造成的损失,也可以作为治理的资金来源,来实施保险、分配给 EUL 利益相关者或应用于其他有利于协议的目的。

与准备金以标的资产计价的 Compound 不同,Euler 的准备金存储在代表 EToken 余额的内部记账单位中。这意味着它们会随着时间的推移产生利息,就像任何其他 EToken 存款一样。当然,Compound 治理可以定期选择提取他们的准备金并将其重新存入池中以赚取这种利息,但在 Euler 中,它会自动且持续地发生。与 Euler 类似,AAVE 存款在拥有 aToken 的特殊金库账户中赚取准备金利息,但是这比 Compound/Euler 的特殊情况下的准备金模型效率低得多,会涉及多个跨合约调用。在 Euler 中,预留开销主要是两个 SSTORE 操作,到无论如何都会写入的插槽。

当我们向准备金发行“eToken”时,它会增加 eToken 的供应量(使其价值降低)。但是,我们仅在增加总借款后才这样做,以确保通货膨胀低于利息收入,并与该资产配置的准备金成正比。

准备金公式推导

Compound

在 Compound 中,CToken 持有者拥有的资产是总“现金”(池中未分配的基础单位)加上未偿还借款总额(随着利息的归集而增加)减去总准备金(由 Compound 治理拥有):

assetsCompound = totalCash + totalBorrows - totalReserves

在大多数操作之前,会计算自上次操作以来的accruedInterest。在 Compound 中,这被添加到 totalBorrows 中,并且 accruedInterest * reserveFactor 被添加到 totalReserves 中,从而为资产产生新的价值:

newAssetsCompound = assets + accruedInterest - (accruedInterest * reserveFactor)

Compound 的 totalReserves 以标的资产为单位,因此它不会产生利息,但是治理可以投票撤回这些准备金金并重新存入以换取 CTokens。

如果 totalSupply 是所有 CToken 持有者的余额之和,则这些 CToken 余额与基础代币之间的汇率为:

newExchangeRateCompound = newAssetsCompound / totalSupply

Euler

应用利息后,Compound 和 Euler 的新汇率相同,但计算方式不同。 Euler 没有从newAssets中扣除准备金费,而是增加了totalSupply。因此,资产的新价值被计算为几乎没有扣除任何准备金费用:

newAssetsEuler = assets + accruedInterest

在 Euler 中,准备金金以 EToken 为单位进行跟踪,这意味着它们会自动赚取利息。当产生利息时,它会以与 Compound 相同的方式添加到totalBorrows中。但随后,不是将收取的费用添加到totalReserves(导致它从newAssetsCompound中扣除),而是铸造了特殊数量的新 EToken 并记入准备金金,这增加了totalSupply。选择这个新铸造的 EToken 数量是为了增加供应量,刚好足以将 EToken 持有者的reserveFactor比例的利息转移到准备金金。

为了证明这导致的汇率与 Compound 的方法相同,我们可以推导出 Euler 使用 Compound 的值计算 newTotalSupply 的算法:

newExchangeRate = newAssetsEuler / newTotalSupply

newTotalSupply = newAssetsEuler / newExchangeRate

用 Compound 的值替换 newExchangeRate

newTotalSupply = newAssetsEuler / (newAssetsCompound / totalSupply)

newTotalSupply = newAssetsEuler / ((assets + accruedInterest - (accruedInterest * reserveFactor)) / totalSupply)

简化一下:

newTotalSupply = totalSupply * newAssetsEuler / (newAssetsEuler - (accruedInterest * reserveFactor))

最后,准备金余额(以 EToken 计价)因newTotalSupply - totalSupply增加了。

这是代码中使用的算法,除了操作重新排序来避免截断舍入。

PTokens

“受保护”代币的存在是为了向用户提供存入代币并将其用作抵押品的选项,同时不允许将其借出。 PTokens 为用户提供额外的安全性,但其代价是不会从所存资产中获得任何利息。 PToken 存款人无需担心资金池会资不抵债,或他们的资产会在想要取回时被借出。

与其将其用作资产的通用设置,不如由用户决定是否要保护其抵押品。由于 Euler 仅支持每个标的资产使用一个 eToken,因此使用了代币封装合约。用户首先将他们的底层代币封装成 pToken,然后将这些 pToken 存入 Euler,收到“epTokens”,然后可以将其用作抵押品。

pTokens 的另一个用例是防止借用代币来执行与治理相关的攻击。因为防止借用检查发生在 increaseBorrow() 中(而不是在 checkLiquidity() 中),所以 pToken 甚至不能被用来闪电贷。

利率模型

很快添加

清算

借款人必须保持足够的抵押品来支持他们的借款。特别是,每个账户必须保持一个高于 1 的“健康评分”。健康评分的计算方法是将账户的 [风险调整后](https://github.com/euler-xyz/euler-docs/tree/ce95fcac4a5d619eec6a8ad50fddc5b50f0db6c5/ Getting-started/white-paper/README.md#risk-adjusted-borrowing-capacity) 抵押品价值除以其风险调整后的负债价值计算。由于抵押因子降低了抵押品的有效价值,而借入因子增加了负债的有效价值,当健康评分为 1 时,该账户在技术上仍具有偿付能力(资产价值大于负债),但该账户被认为是“违规”。

当账户违规时,任何人都可以调用 Liquidation 模块的 liquidate() 方法(违规账户本身除外,以避免别名错误)。调用此方法的帐户称为“清算人”。这个方法做了两件事:

  1. 将违规者的部分 DToken 转移给清算人。这代表清算人正在接管的债务。

  2. 将违规者的部分 EToken 转移给清算人。这代表清算人没收抵押品以换取债务。

由于抵押和借入因子,减少等值的资产和负债(相对于参考资产 ETH)将导致用户的健康评分增加(某些情况除外)。选择 DTokens/ETokens 的数量刚好足以让用户恢复到更高的健康评分,默认为 1.25。这就是所谓的软清算,与清算固定比例贷款的更简单方法相反。

由于清算人正在承担债务,因此清算后必须检查清算账户的流动性。通常清算人将是一个智能合约,因此它可以自动执行除清算之外的其他操作,特别是可以推迟流动性检查稍后在同一交易中,允许“快速清算”。

希望在没有资本要求的情况下运行的清算机器人可以遵循以下清算模式:

  • 推迟流动性检查

  • 清算账户,接收基础 DToken 和抵押 EToken

  • 提取足够的抵押品来偿还债务

  • 在 Uniswap 等去中心化交易所转换此抵押品

  • 偿还债务,清零 DToken 余额

动态折扣

如果被扣押的债务和抵押品在参考资产(例如 ETH)方面都具有相同的价值,那么执行清算将毫无意义。为了激励清算人,扣押的抵押品数量增加了一定的比例。由于违规者有效地获得了购买抵押品的较低价格,因此该因素被称为“折扣”。

Euler 对此折扣使用动态值而不是固定值。折扣会随着违规者的健康评分降低到“1”以下的程度而增加。例如,如果一个帐户的健康评分下降到“0.98”,那么获得的折扣是“1 - 0.98 = 0.02”,即 2%。

Euler 将 Uniswap3 TWAP 用于所有资产的价格推送,这些资产具有协议价格随时间变得平滑的属性。这是动态折扣如何运作的核心,同时也创建了一种类似荷兰拍卖的机制,能找到可能的最低市场清算折扣水平。

动态折扣示例

假设借款人的健康评分为“1.1”,然后对借入的资产进行了大额互换,这显着提高了其在 Uniswap 上的当前价格。在此交换之后(即,包含交换的整个区块的其余部分)之后,资产的 TWAP 不会改变(因为没有时间过去)。这意味着帐户的健康评分也没有变化。

但是,随着时间的推移和新价格权重的增加,TWAP 会增加,这意味着健康评分会降低(负债变得更有价值)。请注意,这实际上发生在第二颗粒度。如果swap足够大,那么在未来的某个时间点,平均价格将使得健康评分恰好等于1。假设 TWAP 还没有赶上当前价格,那么在任何后续区块中,健康评分都将低于1,因此会有清算机会。

现在,在这一点上,折扣将非常小。如果健康评分为“0.999”,则折扣仅为 0.1%。这种折扣水平很可能不足以使清算变得有价值。首先,由于用于计算资产等值价值的价格是 TWAP,它们还没有考虑到标的资产的当前(非平均)价格。其次,折扣必须补偿清算人的任何执行滑点、gas 成本和其他运营开销。

所有这一切都说明,此时任何人都不太可能进行清算。但随着时间的推移和 TWAP 的增加,健康评分会降低,折扣也会提高。在某个时刻,机器人将确定当前的折扣是否导致有利可图的清算。此时它有两种选择:要么执行平仓并获取小额利润,要么等待折扣进一步增加。如果机器人等待,那么它就有可能将清算机会让给另一个清算人。

准备金

当发生清算时,清算人必须偿还超出软清算金额的少量额外借入资产(由相应的额外折扣金额补偿)。这笔额外金额记入借入资产的准备金。

这样做是为了填补经常清算资产的准备金,因为这可能表明资产波动性较大,产生坏账的风险更高。

另一种选择可能是填充抵押资产的准备金,但是在Euler上,并非所有资产都可以用作抵押品,因此许多资金池将没有机会以这种方式增加其准备金。

抢跑保护

上述荷兰式拍卖机制可以为调用 liquidate() 的任何人提供折扣。这意味着清算是无许可的,出于各种原因是可取的,尤其是因为清算无法被审查。

然而,无许可清算往往受到所谓的“抢跑交易”的影响,即当机器人看到有利可图的新交易并以更高的gas价格为自己提交。虽然抢跑交易不是协议的直接问题,但它可能对生态系统有害:

  • 矿工获取价值意味着操作清算机器人的利润较低,因此这样做的人可能会更少,而那些这样做的人可能不那么激进。

  • 大量资源浪费在失败的交易和抬高清算的gas价格上。

在 Euler 中,我们希望奖励清算机器人的运营商而不是矿工,并将他们的奖励水平降低到竞争市场所能承受的最低水平。为了做到这一点,对将资产存入Euler协议的用户的折扣应用了额外的“奖励”。该奖金通过增加折扣的斜率起作用。例如,如果用户有足够的资产来提供 2 倍的奖金,那么他们将获得 2% 的折扣,而不是获得 1% 的折扣。

如果您正在操作清算机器人,您可以通过保持非零抵押因子资产的平衡,在抢跑运行机器人之前获利。为了获得全额红利,您的风险调整抵押品应至少等于您正在处理的清算的风险调整价值(任何更少的红利都会减少)。

有了奖金和荷兰行动机制,我们希望gas拍卖会很少见,清算的大部分价值将归于通过提供资产使协议受益的用户。

请注意,用于索取奖金的流动性必须在Euler合约中保留一段时间。一天后将获得全部平均流动性(请参阅平均流动性跟踪)这意味着如果有人原子性地提供流动性,清算,然后退出,则不会有任何奖金。

其他详情

平均流动性跟踪

为了提供让投资者在系统中享有特权的清算折扣,Euler 可以选择性地跟踪账户的流动性。要选择加入,帐户应调用 exec 模块中的 trackAverageLiquidity() 函数。这将导致大多数操作,例如存款和取款,消耗更多的gas,但如果账户参与清算,将有资格获得额外的折扣特权。

跟踪的实际值是风险调整后的流动性,即在应用抵押因子和借入因子之后。因此,只有具有非零抵押因子的资产才会有助于流动性。同样,未偿还的借款也会降低平均流动性。

为了防止用户(或抢跑运行的机器人)在清算之前简单地存入大量资金(可能使用闪电贷),Euler会跟踪一段时间内流动性的平均值。例如,在第一次存款后,您的平均流动性将立即为 0。只有在 AVERAGE_LIQUIDITY_PERIOD 秒过去后,您的全部流动性值才会反映。

平均是通过指数移动平均线实现的,在执行任何操作(例如存款)之前使用当前流动性值进行更新。这并不完美,因为平均流动性不会反映更新之间的价格变动。此外,当价格异常高或异常低时,用户可能会伺机进行更新,尽管价格当然是 TWAP,因此更难操纵。

对于上述用例,我们认为这些限制并不重要。也就是说,如果启用,账户的平均流动性可以通过exec.getUpdatedAverageLiquidity()获得,只要您的应用程序可以接受上述限制。

小数点

ERC-20 规范允许合约选择代币支持的小数位数。现在这被广泛认为是有问题的,因为它会导致很多烦人的集成工作(参见 ERC-777 以获得有趣的替代方案)。

与其将其暴露给我们的系统,我们决定将所有代币的小数点标准化为 18 位(Euler目前不支持小数点超过 18 位的代币)。除了简化我们的合约和链下逻辑之外,这还允许更精确的利息累积。

舍入

债务总是向上四舍五入到可能的最小外部单位(18 个十进制标记上的 1 个“wei”)。这意味着在产生任何利息后(即 1 秒后),借款人已经至少欠 1 个单位。当您还款时,这额外的部分将添加到 EToken 池中。这是有效的,因为债务金额的跟踪精度(小数点后 27 位)比外部单位(小数点后 0-18 位)更高。

复利行为

与 Compound 不同,只要用户与代币交互,就会发生复利,Euler 每秒都会确定地复合。欠/赚的金额与合约互动的频率无关,当然,导致利率变化的互动除外。如前所述,根据当前有效利率(由利率模型确定),复利精度精确到小数点后 27 位。

在复利系统中,利息累加器会针对影响资产的所有操作进行机会性更新,这是必要的,因为这是实现复利的方式(在更新之间收取单利)。因为 Euler 精确跟踪每秒的复利余额,所以频繁更新累加器没有任何优势。因此 Euler 仅在实际需要时(在影响债务余额的操作之前)才被动这样做。因此,除了更准确之外,这意味着更少的存储写入。

对利息累加器的外部访问

市场模块中有一个方法,interestAccumulator(),用于检索资产的当前利息累加器。因为累加器,如上所述是延迟更新的,而不是仅仅返回存储的值,所以这个方法计算更新后的累加器给定最近块的时间戳(有时称为“反事实”值)。

尽管返回值采用不透明的内部单位,但它们可用于通过比较快照来确定随时间累积的利率。这有点像使用 Uniswap 风格的“TWAP”:通过将最近的快照除以较旧的快照,计算这两个时间段之间收集的实际利息额。

异常/恶意代币

我们尽量使用“通货紧缩”代币。这些是当您请求转移 X 时,实际转移的代币少于 X 的代币。对于这些,我们在之前和之后检查 Euler 合约在代币中的余额,以确定转移了多少。

相关地,在某些代币上,balanceOf方法可以返回不同的结果,而无需干预操作。在这种情况下,这些标的资产的 EToken 所有者可用的总池将受到影响,但协议本身不会受到影响(假设此类代币的抵押因子为 0,这是默认值)。

由于我们允许激活任意代币,因此我们的威胁模型比 Compound/AAVE 的更大。我们需要担心行为不端的代币,甚至是专门试图从协议中窃取而编写的一次性代币。有关威胁建模的更多说明,请参阅文件 docs/attacks.md

代币可能会返回非常大的值以试图导致数学溢出。这可能是灾难性的,特别是用户可能导致自己的流动性检查失败。在这种情况下,用户可以创建不可清算的头寸。为了防止这种情况,当我们从 balanceOf 收到一个非常大的结果时,我们将该结果视为 0(恶意代币当然也可以这样做)。这样流动性检查至少会成功,允许清算非恶意抵押品。

Last updated