Ajna Protocol是一个去中心化、无需治理或外部价格源的借贷和交易系统,利用桶的设计允许用户以不同的风险比例进行借贷。协议通过称为Fenwick树的数据结构有效地管理多个借贷池,使其能够动态追踪市场价格。Ajna还允许使用ERC721 NFTs作为抵押品,解决了传统借贷机制中价格操控的问题,从而提高了安全性和效率。
“Ajna Protocol 是一个无托管的、点对点、无权限的借贷和交易系统,运行所需的没有治理或外部价格feeds。”没有治理,没有外部价格feed - 非常吸引人!
许多现代 DeFi 协议正在朝着更无权限和无信任的设计发展。虽然经常讨论去中心化,但这一趋势背后有实际的动机:对 DeFi 协议攻击的很大一部分源于预言机价格操控、配置错误和访问控制问题。以“设计”方式减轻这些风险是极具吸引力的。
Ajna 协议是这种方法的一个典范;让我们来看看它吧!
我们的第一站,当然是 白皮书。Ajna 的关键思想是无权限地创建池,其中包含代币对(用于供应和借贷),类似于 Uniswap 的池。每个池被划分为“桶” - 类似于 Uniswap V3 中的价格刻度 (文章) - 每个刻度代表报价和抵押代币之间的固定比率(每单位抵押的报价代币量)。这种设计使得贷方可以选择一个桶,并以他们选择的 LTV(贷款价值比)来提供流动性。在 Ajna 中,贷方必须将他们的存款保持在接近市场价格的位置(将其放置/移动到“更接近”市场价格的桶),因为过高的抵押价格将导致失去存款(报价代币),以换取较少量的抵押代币(反之亦然,对于低估价桶)。让我们参考白皮书中的图片:
Ajna 使用“利用率”来描述一个桶。已利用的桶是指所有“上层”桶(价格高于当前桶)的存款总和少于池中所有借款人的总债务的桶。从利用的桶中,最低价格或“最低利用价格”(LUP)是一个关键指标。换句话说,所有高于 LUP 的存款在实际上都在被借出。
另一个重要的阈值是“最高阈值价格”(HTP),它是最少抵押贷款的阈值价格。用其他的话来解释,“所有高于 HTP 的存款都在为池中的每笔贷款提供流动性”。
从 LUP 到 HTP 的桶范围在 Ajna 中扮演着重要角色。LUP 的移动受到不同桶中存款和债务的增加或减少的影响。HTP 的移动受到最少抵押贷款被清算的影响。因此,LUP 和 HTP 都用于确定借款人的位置是否有资格被清算,以及贷方是否可以提取其存款。
Ajna 中的清算会从桶中移除债务和抵押,并因此移动 LUP 和 HTC 值,使市场向实际市场价格转变。清算会从“坏”桶中移除债务,只留下抵押价格接近市场价值的桶。
最终的清算价格是通过荷兰拍卖确定的,赢家是第一个接受不断降低价格的参与者。当前“正在清算”的存款被冻结,不能被提取。此外,以高于当前清算拍卖中最低价格的价格存入报价代币是被禁止的。
白皮书对此解释得更清楚,但为了简单:所有这些限制、LUP 和 HTC 旨在根据外部市场价格在桶之间创建一个“顺序”流动性的转移。Ajna 协议通过 “收集” 来自 HTP 和 LUP 之间的“稳定”范围以上和以下桶的额外债务和抵押,以“跟随”抵押的市场价格移动 HTC-LUP 桶范围。
Ajna 中的利率是“市场导出”的,而不是预定义或治理的值。它们是基于池的利用率动态确定的,并应用了利率限制。现在,让我们继续谈论实现。
池的部署
像往常一样,我们的第一个短暂停留将是在池的部署工厂。Ajna 有不同类型的池:一个是由 ERC20PoolFactory.sol 部署的,另一个是由 ERC721PoolFactory.sol 部署的。前者是用于 ERC20 代币,后者是用于用作抵押的 NFT。由于 Ajna 不使用外部预言机,并且贷方的位置被存储为 NFT,因此在桶中与 NFT 的处理(而不是常规的 ERC20 代币)比在其他协议中要简单得多。无需在协议内部估计每个 NFT 的价格;这项工作由协议交易者执行,而桶逻辑保持不变。池不会升级,但用于保持代币对池地址的映射是使用常量种子构建的 (这里,或 这里),可以更改以添加新的类型和版本的池。
在 Ajna 中池的部署是简单且完全无权限的,像在 Uniswap 中一样。 这里可以看到,创建池非常简单,唯一的“治理式”结构是 映射和列表,以避免使用相同代币的重复池(也类似于 Uniswap)。
桶和 Fenwick 树
如上所述,Ajna 需要对多个桶进行操作,并且每个桶有独立的债务/抵押值。在没有专门数据结构的 EVM 和智能合约中,对大范围进行操作是不可行的。根据白皮书,Ajna 至少需要:
有一种专门数据结构用于有效计算索引范围内的和:Fenwick 树(也称为二进制索引树 wiki,在 文章 中解释得很好)。这种树允许用 O(logN) 操作计算数组中任何 (i, j] 元素范围的部分和。这些树被用来有效追踪“运行总”和值,我们需要不断计算在随时间变化的数组中某些元素范围的和。
Ajna 使用 两个 Fenwick 树:
这意味着在 Ajna 中所有改变这些部分和或利息因子的操作不仅更新相应的桶信息,还更新整个树。这看起来可能效率不高,但让我们回忆一下,操作数量保持恒定( log 2 N),其中 N 是桶的数量(7388 个桶)。这导致每棵树约 13 次迭代。
Fenwick 树的使用示例可以在 追加 存款、在 LUP 计算 的 findIndexAndSumOfSum() 函数、以及一些其他地方找到。
借贷
Ajna 中借贷操作的实现使用两个主要入口点:ERC20Pool.sol 和 ERC712Pool.sol,包含用于借贷的 drawDebt() 函数( ERC20 和 ERC721 实现)和 addCollateral() 函数( ERC20 和 ERC721 变体)。这些函数仅处理基本检查和代币转移;Ajna 的主要逻辑位于 LenderActions.sol 和 BorrowerActions.sol。
核心借贷函数是 addQuoteToken() 来自 LenderActions.sol。该函数处理特定桶索引,并 计算 为指定数量的报价代币所需的 LP 代币量(也已 包括 存款费用)。然后,整体存款会 更新 (请注意,这不是一个简单的累加器值,而是在 这里 更新 Fenwick 树)。然后,在保存贷方的 LP 之后( 这里)进行新的 LUP 计算( 这里),该计算用于在 updateInterestState() 函数中更新池状态。
正如白皮书中所描述的(第 8 节“利率”),该函数 计算 EMA(“指数移动平均值”)债务和存款的值。虽然 Ajna 数学模型的细节不是本文的重点,本文侧重于“如何制作”,但强调 Ajna 借贷机制的关键组件是非常重要的:使用 Fenwick 树存储“每个桶”值和计算 EMA 值。这些组件在维持协议内金融指标的高效和准确追踪中起着关键作用。
核心借贷函数位于 Borroweractions.sol 中,调用 drawDebt()。在执行金额检查并验证债务未在拍卖时,函数准备一个 DrawDebtResult 结构。该结构包含抵押和债务的“前”值和“后”值(无论是对于该特定位置还是整个池),以及新的 LUP(“贷款利用价格”)。
新 LUP 的值及整体池状态稍后用于“滑点” 检查(通过用户的 limitIndex_ 参数设定)。这确保新 LUP 没有跌落到指定的界限以下。此外,它还用于主 _isCollateralized() 检查(在 这里 实现)以验证该位置在操作后是否仍然正确抵押。
在借贷过程结束时,特定借款人的贷款会 更新。在此,Ajna 使用了另一种有趣的解决方案:一个“最大堆”数据结构。这种二叉树维护一个预排序的值数组,最大值位于树顶。
贷款在该结构内排序,每次插入/更新贷款时会 重新排序 最大堆树。当添加/更新/删除值时,此数据结构需要重新排序( 示例),但允许通过简单地 访问 根索引来检索具有最高 TP(阈值价格)的存款,使得涉及优先贷款的操作高效。
这或许是文章中最美丽的部分。Ajna 协议中没有外部预言机 :)
让我们回想一下,Ajna 中每个桶之间是债务和抵押的固定比率。Ajna 方法的一个基本原则是,用户的仓位中使用的哪些代币并不重要;固定比率使抵押或债务数量在桶中是“等同”的。桶中的操作可以包括添加报价或抵押代币(以换取 LPB 代币)或移除报价或抵押代币(通过燃烧 LPB 代币)。简单地说,Ajna 对交易者说:“这是一个具有不同价格的债务/抵押代币桶包供你交易,但请不要将最低利用价格(LUP)移得过低,以避免产生不足抵押贷款。”
当然,这给根据市场条件重新平衡整个系统带来了挑战,但这使得协议的核心非常简单和稳定。这种设计可以执行许多 DeFi 操作,比如限价单或做空。此外,缺乏预言机使得在协议内使用 NFT 变得更加容易和安全。
为了启用这些债务->抵押和抵押->债务的交易,LPB 代币在铸造时立即可兑换。我们可以将报价或抵押代币存入一个桶,获得 LPB 代币,然后通过返回所获得的 LPB 代币提取抵押或报价代币 - 所有操作都在单个交易中完成。对报价代币的提取减少了整体存款金额并降低了 LUP,因此这些操作被限制为借贷操作( 这里),而 添加 报价代币则无需限制(因为它会提高 LUP)。
具有“固定价格桶”的设计需要“移动”操作,允许贷款人将在桶之间移动其存款,这在 moveQuoteToken() 函数中实现。
Ajna 中的清算还与 LUP 相关。基本条件是:
对于特定贷款可以重新写为:
因此,要检查一笔贷款是否有资格被清算,我们只需要比较它的 TP(“阈值价格”)< LUP。
Ajna 中的清算以荷兰拍卖的形式组织。价格随着时间不断降低,第一位同意价格的参与者以“按出价支付”的方式完成拍卖。参与者可以使用其他桶中的存款支付这笔操作。因此,Ajna 中的清算不是“经典清算”,而是债务/抵押代币在桶之间的迁移,导致反映当前市场状态的分布。
有两个“踢出”函数:kick() 和 lenderKick。第一个函数直接清算一个 给定 的借款人,而第二个函数则自动选择 最大 借款人进行踢出(Ajna 中的存款者可以踢出如果其存款被移除则会不足抵押的贷款)。两个函数都使用主要的 _kick() 函数,首先 计算 NP(“中性价格”)。正如白皮书中所描述的,NP 是清算荷兰拍卖中的一种“中间点”。过早以高于 NP 的价格清算会导致踢出者的部分代币被没收。随着时间的推移,当价格低于 NP 时,清算变得越来越有利可图。关于当前清算的信息会被 保存 在 双向链表 中,该链表包含借款人的地址。然后,该列表中的拍卖依次用于清算的结算。
Ajna 中另一个重要机制是能够进行闪电贷。贷方提交清算保证金要求一些额外的报价代币。借助闪电贷,贷方可以利用其存款通过获得闪电贷来提交清算保证金,然后用提取的存款偿还闪电贷。为了实现这一点,ERC20Pool 和 ERC721Pool 都继承自 FlashloanablePool,并允许调用 flashLoan() 函数(Ajna 遵循 ERC-3156 规范)。Ajna 的闪电贷仅适用于池的报价代币,因此 Ajna 限制 特定池的“可闪电贷”代币。
Ajna 有一个结构良好且清晰的代码库,所有逻辑层分离且功能简洁易懂。代币操作仅在“池”级别处理,而所有内部机制仅与代币余额的表示进行交互。
Ajna 使用特殊的数据结构来管理操作,例如 Fenwick 树(用于存款和利息累积)、链表(用于清算)或“最大堆”树(用于贷款)。在处理多个借款人和桶以及需要“顶值”或“下注”操作排序时,这些结构是至关重要的。设计自己的 DeFi 协议时,应考虑这些结构。
Ajna 使用一款特殊的 RevertsHelper.sol 库,包含重要检查,触发协议内的回滚。例如,此类复杂检查的 _revertIfAuctionClearable(),它验证拍卖的过期时间以及剩余债务/抵押。
Ajna 协议的数学基于 PRBMath 库( Math.sol),操作 59.18 位小数定点值。非标准计算主要用于根据 LUP 和债务/抵押金额的 EMA(“指数移动平均”)值计算动态利率(示例:函数 updateInterestState())。正如前面提到的,利率不通过治理管理;一切都基于池的利用率和活动进行安排。
Ajna 无疑是一个值得关注的借贷协议,因为它的无权限和去中心化特性。没有外部预言机,没有治理 - 这些特性非常吸引人,尤其是在缺乏稳定预言机基础设施和强大治理机制的新链上工作时。
此外,缺乏预言机使得 Ajna 能够使用与常规 ERC20 代币几乎相同的机制处理 ERC721 资产作为抵押。估计不同 NFT 的价格是一项非常棘手的任务,而预言机价格操控仍然是对借贷协议上最流行的攻击向量之一。
Ajna 的桶机制将债务/抵押比率分为范围并允许资产在风险区域之间转移,使得 Ajna 的池能够在不依赖外部价格操控的情况下追踪外部市场价格。但是,与之前讨论的 CrvUSD 借贷 和 Fluid's Vault 协议不同,Ajna 在执行清算时需要与每个借款人的仓位进行交互,类似于传统的借贷平台(Compound、Aave)。这些交互通过使用 Fenwick 树等专门数据结构进行优化。
DeFi 协议的美在于其算法性质和透明的数学模型,努力在开放市场中实现稳定性和效率。Ajna 代表着向“真正”去中心化金融的重要一步。让我们继续前行吧!
MixBytes 是一支专业区块链审计和安全研究团队,专注于为兼容 EVM 和 Substrate 的项目提供全面的智能合约审计和技术咨询服务。请加入我们在 X 一起关注最新的行业动态和见解。
- 原文链接: mixbytes.io/blog/modern-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!