f(x) 协议 v2 审计

这是一份OpenZeppelin对AladdinDAO的fx-protocol-contracts代码库进行的审计报告,审计范围包括核心池机制、价格预言机和资产管理等方面。报告中发现了包括一个严重、两个高危在内的46个问题,并提出了相应的改进建议,例如修复递归漏洞、优化价格预言机设计以及遵循ERC-3156标准等,主要目的是保障系统的安全和稳定性。

目录

概要

TypeDeFi/Stablecoin时间线 从 2025-03-27 到 2025-05-05 语言 Solidity 总问题 46 个(已解决 5 个)严重性:危急问题 1 个(已解决 1 个)严重性:高问题 2 个(已解决 2 个)严重性:中问题 7 个(已解决 2 个)严重性:低问题 13 个(已解决 0 个)注释 & 附加信息 23 个(已解决 0 个)

范围

OpenZeppelin 审计了 AladdinDAO/fx-protocol-contracts 仓库,提交哈希为 56a47ea

以下文件在审计范围内:

 contracts/
├── core/
│   ├── pool/
│   │   ├── AaveFundingPool.sol
│   │   ├── BasePool.sol
│   │   ├── PoolConstant.sol
│   │   ├── PoolErrors.sol
│   │   ├── PoolStorage.sol
│   │   ├── PositionLogic.sol
│   │   └── TickLogic.sol
│   ├── FlashLoans.sol
│   ├── FxUSDBasePool.sol
│   ├── FxUSDRegeneracy.sol
│   ├── PegKeeper.sol
│   ├── PoolManager.sol
│   ├── ProtocolFees.sol
│   ├── ReservePool.sol
│   └── SavingFxUSD.sol
├── fund/
│   ├── strategy/
│   │   ├── AaveV3Strategy.sol
│   │   └── StrategyBase.sol
│   ├── AssetManagement.sol
│   └── IStrategy.sol
└── price-oracle/
    ├── BTCDerivativeOracleBase.sol
    ├── ETHPriceOracle.sol
    ├── LSDPriceOracleBase.sol
    ├── SpotPriceOracleBase.sol
    ├── StETHPriceOracle.sol
    ├── WBTCPriceOracle.sol
    └── interfaces/
        ├── IPriceOracle.sol
        ├── ISpotPriceOracle.sol
        └── ITwapOracle.sol

系统概述

f(x) 协议允许用户有效创建杠杆头寸。它通过促进头寸的杠杆作用以及将其聚合为易于重新平衡的单元来实现这一点。这最大限度地降低了头寸资产价格波动时完全清算的风险。这些头寸用 fxUSD 代币标示,该协议旨在将这些代币与 USDC Hook,从而扩大 fxUSD 代币在更广泛的 DeFi 市场中的吸引力。该协议很大,包含许多合约。因此,鉴于协议的规模和复杂性,可以查阅其文档以深入了解整个系统的机制。

本次审计涵盖了 f(x) 协议“2.0”升级的新功能,这些功能大致可分为三个部分:

  • 核心池机制:这些合约位于 core 目录中,为构建头寸和维持债务代币(fxUSD)的Hook价值提供核心功能。

  • 价格预言机基础设施:协议依赖资产的实时定价来正确评估头寸并维持 fxUSD-USDC Hook。price-oracle 目录中的合约致力于将价格馈送集成到协议中。

  • 资产管理和重新分配基础设施:在头寸开放且抵押品已存入的情况下,协议旨在通过将这些资金存入其他 DeFi 协议来赚取利息。fund 目录中的合约旨在使此过程尽可能高效和无缝。

核心机制

该协议的核心是 AaveFundingPool 合约。这些合约中的每一个都处理特定抵押资产的杠杆头寸。目前,有一个用于 WBTC 的 AaveFundingPool 合约和一个用于 stETH 的合约。这些合约处理整个头寸市场的会计,按其抵押程度对其进行组织。

此外,这些合约还为各种市场行为提供入口点,例如创建新头寸、调整现有头寸、重新平衡贬值的头寸以及清算超出可接受抵押范围的头寸。并非所有网络参与者都可以访问这些入口点,并且由 PoolManager 合约控制。

PoolManager 合约是所有池的所有头寸的抵押品实际所在的位置。它负责正确地将代币移入和移出池,并将市场操作分派到相应的 AaveFundingPool 合约。它为其持有的资产提供闪电贷,并具有用于创建、调整和结束头寸的入口点。为了标示每个头寸的价值,开启头寸时会铸造 fxUSD 代币,每个代币代表一美元的债务。

fxUSD 合约在此范围内表示为 FxUSDRegeneracy 合约。并且由于每个头寸都通过借入的资产进行超额抵押,因此每个 fxUSD 代币也经过超额抵押,并且在通过有意选择或通过市场压力(即,重新平衡或清算)向下调整头寸时会被销毁。

这些重新平衡和清算来自 FxUSDBasePool 合约,称为稳定池。该合约通常负责维护协议健康的许多活动。如果 fxUSD 或 USDC 的相对价格离Hook太远,则稳定池会促进市场操作以买卖这些代币,以使相对价格恢复为 1。费用会存入此合约,随着时间的推移增加其价值。

此核心层中的其他合约包括 PegKeeper,它充当稳定池市场操作的中间人,ReservePool 合约涵盖抵押不足债务造成的任何抵押品损失,以及 SavingFxUSD 合约,称为 fxSAVE,它是稳定池周围的 ERC-4626 “包装器”,可自动再投资任何应计费用。将稳定池份额包装到 fxSAVE 中实际上涉及一个两步过程,涉及一个称为 gauge 的非范围合约。

价格预言机

f(x) 2.0 价格预言机从包括 Chainlink、Uniswap、Curve 和 Balancer 在内的多个数据源获取价格,以计算现货价格和锚定价格。锚定价格主要基于 Chainlink 的预言机价格馈送,使用预定义的编码,聚合器地址、比例和心跳由 owner 角色设置。虽然现货价格是从多个池中获取的,但仅考虑这些价格的最低价格(minPrice)和最高价格(maxPrice)。

最低价格和最高价格都允许与锚定价格的最大偏差阈值为 maxPriceDeviation。对于 WBTC 池,最大偏差为 2%,而对于 stETH 池,最大偏差为 1%。如果最低或最高价格偏差超过阈值,则改用锚定价格。minPrice 用于对头寸、清算和重新平衡进行操作,而 maxPrice 用于 redeem 操作,以避免套利。

资产管理

由于协议持有大量代币,因此资产管理合约添加了将一个合约的代币存入其他产生收益的合约的功能。AaveV3Strategy 合约将代币存入 Aave 借贷池,而 AssetManagement 合约允许与这些策略集成。反过来,PoolManager 继承了 AssetManagement 合约,允许它在投资者持有其杠杆头寸时赚取收益。

安全模型和信任假设

范围内的合约彼此紧密集成,并且依赖于正确的配置才能正常工作。因此,在本次审计过程中,做出了以下信任假设:

  • 在构建或初始化时设置的变量在引用它们应该引用的协议合约时是正确的。在升级期间,新的实现确保在构建时设置正确的值,特别是对于不可变的变量。
  • 外部协议按描述工作。Chainlink、Curve 和 Aave 都是连接到此范围内的合约的协议。
  • 所有其他 f(x) 协议合约按描述工作,并且协议内不同角色的拥有者以称职和诚信的方式行事。

按合约划分的特权角色

AaveFundingPool

DEFAULT_ADMIN_ROLE 可以:

  • 更改开启、关闭或资助头寸的费用百分比
  • 更改最低和最高抵押、重新平衡和清算的抵押阈值
  • 更改赎回期间可以从任何 tick 中提取的最大金额
  • 设置价格预言机
  • 更改合约中的角色分配

EMERGENCY_ROLE 可以:

  • 暂停或取消暂停创建或增加头寸的能力
  • 允许或禁止直接用抵押品赎回 fxUSD。在这种情况下,抵押品直接来自抵押程度最低的头寸
  • 允许或禁止创建头寸时借款。在这种情况下,借款意味着铸造新的 fxUSD 代币

PoolManager 合约:

  • 允许任何人调用 operateredeem
  • 允许稳定池调用 rebalanceliquidate。如果稳定池的总价值未达到阈值,则可以扩展到任何人

AaveV3Strategy

HARVESTER_ROLE 可以:

  • 从 Aave 借贷池中提取奖励并将其发送到稳定池,如文档中所定义,即使对于已停止的策略也是如此

operator 可以:

  • 将新代币存入 Aave 借贷池策略
  • 从 Aave 借贷池中提取代币
  • 在合约上调用任意代码

operator 角色适用于像池管理器和稳定池等继承 AssetManagement 合约的合约。

FxUSDBasePool

DEFAULT_ADMIN_ROLE 可以:

  • 设置和取消设置资产的策略合约
  • 设置赎回等待期
  • 设置即时赎回费百分比
  • 设置 USDC 被认为与美元脱钩的价格
  • 更改合约中的角色分配

ASSET_MANAGER_ROLE 可以:

  • 将代币存入其策略

PegKeeperSTABILIZE_ROLE 可以:

  • 将池中的 fxUSD 或 USDC 兑换为另一种

FxUSDRegeneracy

PoolManager 可以:

  • 在处理池中的头寸时,铸造或销毁新的 fxUSD 代币

PegKeeperBUYBACK_ROLE 可以

  • 使用 USDC 购买 fxUSD

PegKeeper

BUYBACK_ROLE 可以:

  • 直接从 fxUSD(fxUSDRegeneracy)合约中使用 USDC 购买 fxUSD

STABILIZE_ROLE 可以:

  • 将稳定池中的 fxUSD 或 USDC 兑换为另一种

DEFAULT_ADMIN_ROLE 可以:

  • 设置 f(x) 的 MultiPathConverter 合约(用于)兑换资产的地址
  • 设置 fxUSD/USDC 的 Curve 池的地址
  • 设置 fxUSD 被认为与 USDC 脱钩的价格阈值
  • 更改合约中的角色分配

PoolManager

DEFAULT_ADMIN_ROLE 可以:

  • 更新费用百分比以及这些费用支付给的位置
  • 设置各种集成合约的地址,例如储备池、金库和奖励分配器
  • 分配、更新或停止资产的策略合约
  • 注册新池,供用户在其中创建头寸
  • 更新每个池的债务和抵押品容量
  • 为任何资产添加或删除缩放因子提供者
  • 设置一个阈值金额,如果稳定池跌破该阈值,将允许任何人调用 rebalanceliquidate
  • 更改合约中的角色分配

ASSET_MANAGER_ROLE 可以:

  • 将代币存入其策略

HARVESTER_ROLE 可以:

  • 从池中收取资金费用

EMERGENCY_ROLE 可以:

  • 暂停或取消暂停池中的所有活动(即,操作、赎回、重新平衡和清算)

稳定池允许任何人:

  • 对池中的头寸执行重新平衡和清算

ReservePool

DEFAULT_ADMIN_ROLE 可以:

  • 提取池中的任何代币或 ETH
  • 更改合约中的角色分配

PoolManager 可以:

  • 从储备池发送资金以支付抵押不足的贷款或赎回

SavingFxUSD

DEFAULT_ADMIN_ROLE 可以:

  • 更新合约在将其存入流动性计量合约之前将持有的代币数量
  • 更改合约中的角色分配

CLAIM_FOR_ROLE 可以:

  • 请求为任何人赎回

预言机

owner 可以:

  • 更新允许的最大价格偏差阈值与锚定价格的偏差
  • 更新链上现货编码以获取现货价格

严重性:危急

攻击者可以通过 Redeem 函数锁定用户资金

协议的 PoolManager 合约中的 redeem 函数 允许任何用户销毁 fxUSD 并获得低于市场价格的抵押品作为回报。此函数的目的是通过抑制来防止 fxUSD 稳定币的脱锚情况。在函数调用期间,PoolManager 合约调用 BasePool 合约的 redeem 函数 来计算要返回的抵押品。此函数从顶部 tick 开始清算,直到通过 _liquidateTick 函数 覆盖所需的 rawDebts (fxUSD) 数额。在此函数调用期间,顶部 tick 始终移动到新的父节点,旧节点成为其子节点。

需要注意的是, redeem 函数中没有设置 最低 rawDebt(要销毁的 fxUSD) 要求。这允许攻击者赎回少量的 fxUSD 并为 tick 创建多个节点而不将其从顶部 tick 移动,并将该 tick 的头寸向下推入 100 到 1000 个子节点的螺旋中。

此外,BasePool 合约中的 operate 函数 始终在任何其他计算之前将 头寸更新到来自当前子节点的最新父节点。请注意,_getRootNodeAndCompress 函数 获取头寸的根节点,是一个递归函数,很容易出现堆栈溢出错误。

攻击者可以利用上述清算 tick 设计、缺少最低 rawDebt 检查和递归属性来执行以下步骤:

  1. 反复调用 redeem 函数,并销毁最少量的 rawDebt(fxUSD)(例如,销毁大约 150 次 2 wei)。这确保了永远不会更新 顶部 tick ,并且创建了 150 个子节点。

  2. 再次调用 redeem 函数,并以计算出的高额值转移到新的顶部 tick 以定位更多头寸。

  3. 一旦目标 tick 再次成为顶部 tick,请重复步骤 1。

由于此机制,每当用户尝试使用 operate 函数关闭或更新其中一个受影响的头寸时,_getRootNodeAndCompress 将因堆栈溢出错误而失败,因为子节点超过了一定限制。 这个 POC 通过操纵顶部 tick 并锁定与该 tick 对应的所有头寸的资金来演示堆栈溢出行为。用户将无法关闭或更新其头寸,他们只能被重新平衡或清算,因此他们的资金将被锁定。

为了解决根本问题,请考虑迁移到 _getRootNodeAndCompress 函数的非递归版本。为了进一步防止通过 redeem 函数进行的 Gas 消耗攻击,请考虑实施其他检查,例如最低 rawDebt 要求,以确保顶部 tick 始终移动,这将增加任何试图定位 tick 的攻击者的难度。

更新: 已在 pull request #22 中解决。

该团队已迁移到 _getRootNodeAndCompress() 函数的迭代版本,并为 redeemrebalanceliquidate 功能添加了最低 rawDebts 要求。还添加了一个管理函数来压缩节点链,以防通过树结构的链下监控检测到任何意外行为。f(x) 协议团队表示:

我们已实施以下更改以应对观察到的极端情况:

  • 添加了最低原始债务阈值:我们为 redeemrebalanceliquidate 操作引入了最低原始债务要求,以防止清除级别的滥用并确保 tick 移动。
  • Tick 移动检查redeem 函数现在会在 tick 不移动的情况下恢复,以防止陈旧状态转换。
  • 路径压缩函数:我们已将递归 getRootNodeAndCompress 函数替换为非递归内部版本,以避免堆栈溢出。还提供了 public 管理版本,用于手动压缩过度长的链。

上述更改提高了协议的稳定性,同时保留了监控和动态适应极端情况的能力。

严重性:高

闪电贷功能被阻止

FlashLoans 合约的 flashLoan 函数 使用 returnedAmount < amount + fee 条件来验证偿还。但是,returnedAmount 计算为回调后的余额减去贷款前的余额,这仅表示发送的用于支付费用的额外代币。因此,returnedAmount < amount + fee 始终为 true,导致每次闪电贷都恢复,除非借款人以某种方式将整个本金加上费用作为费用退还。

考虑将条件更改为 returnedAmount < fee,以便该函数正确强制执行费用的偿还。

更新: 已在 pull request #17 中解决。此 pull request 通过在代币转账到接收者之后计算 prevBalance 来解决此问题。此外,一旦解决 M-01,将实现符合 EIP-3156 标准。

池子可能受到价格操纵,导致提前清算或套利

协议中的 价格预言机 旨在从 Chainlink 数据馈送中获取抵押品的价格,该数据馈送 充当 anchorPrice,并且还使用多个链上池来获取现货价格,这些现货价格充当 相同的 minPricemaxPriceminPrice 是通过获取所有获取的现货价格中的最低价格得出的,类似地,maxPrice 是通过获取现货价格中的最高价格得出的。

oracle 合约的 getPrice 函数 确保从链上池返回的 minPricemaxPrice 值与 anchorPrice 的偏差不超过 1%。如果价格 偏差超过 1%,它会将各自偏差的 minPricemaxPrice 重置为 anchorPrice。抵押品的 minPrice 用于 操作头寸重新平衡清算 期间。另一方面,maxPrice 用于 redeem 功能中。

但是,尽管允许 minPricemaxPrice 偏差 1%,但只需要一个池来操纵价格。因此,攻击者可以定位 TVL 最低的池或可以在同一交易中进行操纵的池,并以导致 anchorPrice 偏差恰好 1% 且 绕过偏差检查 的方式操纵价格。

这种操纵 1% 价格的行为允许恶意清算人降低 minPrice,以迫使脆弱的头寸提前被清算并产生更多利润。类似地,可以操纵 maxPrice 使其偏差超过 1% 并重置为 anchorPrice,这会在 redeem 功能期间开启套利机会。

通过将所有现货价格压缩为一个 minPrice 和一个 maxPrice,然后强制任何异常值回到锚定,从而否定了预期的多池弹性。在实践中,系统总是会回退到单个被操纵的池或锚定提要。换句话说,仅破坏一个低 TVL 池就会将“多样化”的预言机变成单点故障。

例如,当前的 stETH 价格预言机 依赖于 3 个池来获取 ETH/USD 现货价格:WETH/USDC Uniswap V2WETH/USDC Uniswap V3 0.05%WETH/USDC Uniswap V3 0.3%。攻击者只需要操纵这些池中的一个,并且考虑到 Uniswap V2 池的 TVL 目前为 1900 万美元,攻击者很容易将其价格偏差 1% 并通过清算高价值头寸来产生利润。Uniswap V3 池也可能受到多次交易组合的操纵。

如果可能,请考虑重新设计价格预言机,使其不依赖于单个现货价格。或者,请考虑确保 选定的池(从中获取现货价格)满足基于最大可能清算利润和支付的池的 TVL 的 1% 偏差的费用计算出的最低 TVL 要求。

更新: 已解决。将删除 TVL 较低的 WETH/USDC Uniswap V2 池。此外,f(x) 协议团队已实施足够的风险控制措施,包括一个内部团队来监控所有池的流动性。

f(x) 协议团队表示:

由 f(x) 协议预言机使用的核心价格数据来自 Chainlink。如果该协议出现故障,f(x) 协议管理员需要主动且紧急地干预,停止 f(x) 协议的所有核心功能,例如 operate 仓位、rebalanceredeemliquidate,以避免 f(x) 协议用户的资产损失。当 Chainlink 预言机正常运作时,该协议会为不同的报价资产设置不同的偏差阈值范围。

当报价资产的价格波动不超过此范围时,当前的报价数据将保持不变,这可能会导致 Chainlink 报价与实际市场价格之间出现小范围的价格差异。对于对价格准确性非常敏感的操作,例如 f(x) 协议上的仓位清算,小范围的价格差异也可能导致大型仓位损失过多的本金。因此,Chainlink 提供的数据不适合直接使用。为了解决 Chainlink 价格更新延迟的问题,f(x) 协议补充并整合了来自多个 DEX 的现货价格数据。

尽管现货价格更接近实时市场状况,但它们更容易被操纵。为了避免任何 DEX 上的价格操纵,f(x) 协议权衡了 Chainlink 价格稳定性和 DEX 现货价格的及时性,并建立了一种平衡机制,形成了当前的 f(x) 协议预言机报价规则,即:f(x) 协议选择所有 DEX 现货价格数据和 Chainlink 提供的锚定价格,以获得最高价格数据 maxPrice 和最低价格数据 minPrice

如果最高价格数据 maxPrice(最低价格数据 minPrice)与 anchorPrice 之间的价格偏差未超过预设参数 maxPriceDeviation,则认为该数据有效。否则,将使用 anchorPrice 作为有效数据。在当前的 f(x) 价格获取规则下,如果恶意攻击者操纵 DEX 的现货价格使其偏离协议预设参数 maxPriceDeviation 的价格范围,则 f(x) 协议最终将使用更可信的 anchorPrice 作为有效价格。

与当前市场上的最佳价格相比,f(x) 协议的定价策略可能会导致用户在价格被操纵或市场价格剧烈波动时损失极小比例的资产。这类似于 AMM 中不可避免的滑点,是在确保系统稳健性的前提下可以接受的折衷方案。另一方面,这种设计可以大大降低现货价格攻击的风险,更好地保护用户的资产安全。因此,总的来说,我们认为 f(x) 协议当前的定价策略最符合用户的利益。

关于“操纵特定池,使其价格与 anchorPrice 恰好偏差 1%,绕过偏差检测,并创造套利机会”的问题,我们分析了 f(x) 协议中使用这些价格的功能模块(以 stETH 资产为例):

使用 minPrice 的功能模块:operate 仓位、rebalanceliquidate 操作。

1. operate 仓位操作minPrice 价格数据仅用于确定存在超额抵押时的用户抵押品价值。即使攻击者操纵最低价格使其偏离 anchorPrice 1%,由于存在超额抵押机制,单笔交易的价格操纵行为也不会对协议产生任何不利影响。

2. rebalanceliquidate 操作:从以上分析可知,理论上可以控制 minPrice 使其偏离 anchorPrice 恰好 1%,但实际上,攻击者需要更多地考虑难度和攻击成本。在大多数情况下,f(x) 协议执行 rebalanceliquidate 操作时,抵押资产的价格会急剧下降。

首先,此时每个 DEX 的现货价格波动很大,并且 Chainlink 对应的 anchorPrice 也会相应变化。很难准确地操纵现货价格,使其与 anchorPrice 之间的价格差在 1% 以内(确保潜在攻击利润最大化)。

其次,即使攻击者恰好构建了一个匹配的 minPrice,由于此时抵押资产的价格急剧下跌,相应池中肯定会有大量的交换交易,因此攻击者赚取的 1% 利润可能不足以应付滑点, 手续费和其他费用, 这进一步压缩了套利空间。

最后,Chainlink 提供的 anchorPrice 结合了当前市场上各种协议或交易所的价格数据,并提供当前市场的平均价格。价格变化本身具有滞后性。当抵押资产的价格下跌时,某些 DEX 的现货价格很可能与 anchorPrice 相差超过 1%。在这种情况下,如果协议直接使用 anchorPrice,则攻击行为将更加复杂,并需要额外的成本(将所有超过 1% 的池价格拉回到 1% 以内)。

总之,攻击者操纵最低价格使其偏离 anchorPrice 1% 的方法面临很大的困难、不确定性和高成本,并且难以实现。

使用 maxPrice 的功能模块:redeem 操作

3. 在即将到来的升级版本中,只有在 fxUSD token 与美元脱钩时才允许执行 redeem 操作。假设当前的 fxUSD 与美元脱钩,尽管这种情况很少见,但此时攻击者需要操纵所有现货价格,以使超过 anchorPrice 的现货价格回落到 anchorPrice,以便兑换更高价值的抵押品。显然,这种攻击需要攻击者付出控制多个池的成本。从最终结果来看,即使攻击者最终使用 anchorPrice 来兑换抵押资产,该价格仍然在市场上合理的范围内,尽管该价格不是用户仓位的最优惠价格,并且没有显着的套利空间。

总体而言,攻击者操纵特定池以控制现货价格并绕过偏差检测以达到套利清算的目的的解决方案非常难以实施,并且需要很高的攻击成本。为了应对预言机价格操纵,f(x) 协议引入了一系列风险控制措施,从获取有效的价格解决方案到链上实时监控,例如:

  • 1. 正如问题中所述,f(x) 协议选择当前市场上流动性良好且 TVL 最高的池作为不同抵押资产的现货价格来源。
  • 2. 对于不同的抵押资产,f(x) 协议结合了 Chainlink 的 anchorPrice 的偏差阈值,并设计了不同的预设参数 maxPriceDeviation
  • 3. 在当前链上运行时,添加了多个观察者,以便在抵押资产价格大幅波动时快速调整用户仓位,从而大大降低了用户仓位被清算的概率。

因此,我们认为 f(x) 协议当前的价格机制在反操纵和市场反映之间实现了合理的权衡,这最符合用户的利益。而且,与此同时,我们有一个内部团队正在监控所有池的流动性,并将移除流动性小的池。

最后,我们想介绍一下当前预言机设计背后的原理:

  • 来自 Chainlink 的锚定价格 (Anchor Price):用作所有比较的基础。
  • 偏差阈值 (Deviation Thresholds):每种资产都有一个最大价格偏差(例如,stETH 为 1%,WBTC 为 2%)。如果现货价格保持在该偏差范围内,则被接受;否则,系统将回退到 anchorPrice
  • 现货价格聚合 (Spot Price Aggregation):现货价格从多个高流动性 DEX 池中收集,以减少操纵风险。

关键缓解点 (Key Mitigation Points)

  • 如果攻击者将单个 DEX 池操纵到超出偏差阈值,则其价格将被忽略。
  • 使用 minPrice 进行清算,使用 maxPrice 进行赎回,确保了反应性和抗操纵性之间的可防御平衡。
  • 只有在脱钩时才启用 redeem,即使那样,套利在经济上也是不可行的。
  • 流动性低的 WETH/USDC Uniswap V2 池已被移除。
  • 选择所有现货来源都是为了深度和可靠性,并且额外的链上观察者有助于应对快速变化的市场。

我们相信,这种方法在市场响应能力和安全性之间实现了强大的权衡。

中等严重程度

闪电贷功能不符合 ERC-3156 标准

ERC-3156 规定:

在回调之后,flashLoan 函数必须从接收者那里获取金额 + 手续费 token,如果失败则必须回滚。

flashLoan 没有获取 token,而是期望调用者已经返回了 token。这将阻止合约与任何兼容的 IERC3156FlashBorrower 合约集成,因为他们不会在这里将 token 返回给合约。

ERC-3156 进一步规定:

flashFee 函数必须返回 amount token 的贷款费用。如果不支持该 token,则 flashFee 必须回滚。

这里“支持”一词的使用是模棱两可的,因为它没有指定最大贷款额为零意味着 token “不受支持”。但是,ERC 确实将 maxFlashLoan 中返回零与 token 不受支持混为一谈。因此,在 maxFlashLoan 将返回零的情况下,flashFee 必须回滚。当前,该函数只是计算给定金额的一部分,而不管 token 如何。

考虑修复 flashLoanflashFee 函数,使其符合上述 ERC-3156。

更新:已确认,计划解决。f(x) 协议团队表示:

我们承认该问题,并计划在未来的更新中解决。

赎回等待可能会被利用

FxUSDBasePool 合约的 requestRedeem 函数 注册来自用户的赎回请求。一旦 redeemCoolDownPeriod 周期过去,用户就可以赎回该数量的 fxBASE token,并根据池中的比例获得 fxUSD 和 USDC。此功能的动机是避免高水平的赎回导致稳定池挤兑。

但是,用户发出的赎回请求没有到期时间。用户可以存入池中并立即调用 requestRedeem 函数来注册他们的赎回请求。之后,他们可以在冷却期到期后的任何时间进行赎回。这将使赎回请求功能无用。

考虑为赎回请求添加到期时间。在此时间之后,不应允许赎回,并且用户应再次请求赎回。

更新:已确认,未解决。f(x) 协议团队表示:

当前的赎回设计可防止在同一区块中发生存款和赎回。只要 redeemCoolDownPeriod 不为零,用户就必须等待才能赎回,这足以阻止立即套利。因此,我们认为目前不需要进行任何进一步的更改。

满容量的池无法清算

PoolManager 合约中的每个池都有一个它可以容纳的最大抵押品容量。如果更改超过容量,_changePoolCollateral 函数会 回滚。当 tick 有坏账时,协议使用来自 ReservePool 合约的资金来弥补债务中未抵押的部分,以便能够促进清算。

通过 PoolManager 合约的 liquidate 函数 清算池时,从储备池提取的资金会添加到当前池的抵押品数量中。如果来自储备池的资金总额和当前余额超过容量(即,capacity<bonusFromReserve+balancecapacity < bonusFromReserve + balancecapacity<bonusFromReserve+balance),则清算将失败。

考虑在扣除清算抵押品后,将储备池的资金添加到池抵押品变量中。

更新:已确认,未解决。f(x) 协议团队表示:

这种情况很少见,考虑到在实践中发生的可能性很低,我们选择目前不进行任何更改。当前的实现方式保持了更简单和更可预测的行为,并且修改储备抵押品逻辑可能会为用户影响最小的边缘情况引入不必要的复杂性。

稳定池中使用过时的 totalStableToken

sync 修饰符totalStableToken 变量更新为最新值,以防稳定 token 已存入策略并产生收益。所有外部功能,如 depositredeem 等,在继续之前都会使用 sync 修饰符更新此 totalStableToken 变量。

但是,FxUSDStabilityPool 合约的 previewDepositpreviewRedeemnav 函数使用过时的 totalStableToken 变量值,这可能导致这些 view 函数返回不正确的返回值。例如,为了防止通货膨胀攻击,previewDeposit 最终可能会返回比用户在 存款期间指定的 minSharesOut 更多的份额,导致回滚。

考虑修改函数来计算 totalStableToken 的最新值。

更新:已在 pull request #21 中解决。

当总供应量为零时,对 fxSAVE 进行份额通货膨胀攻击

ERC-4626 Vault 合约使用 _decimalsOffset 函数来为份额值增加更多精度。问题是,如果底层 token 有 18 位小数(大多数 token 都是如此,包括 fxBASE),则 _decimalsOffset 返回的值为 0。

由于 totalAssets 函数 依赖于 balanceOf(address(this)),当 totalSupply 为 0 时,攻击者可以用 10 wei 的 fxBASE 铸造 10 个份额,然后直接向合约捐赠 100e18 个 fxBASE token,以抬高 fxSAVE 的每股价格。当用户存入 1e18 价值的份额时,他们将获得 0 个份额作为回报,而攻击者可能只会损失 1e17 个份额。可以看出,攻击者可以以大约 1e17 个 fxBASE token 的成本锁定用户 1e18 的资产。

由于该池已经部署,因此这种攻击向量实现的可能性非常低。尽管如此,请考虑将一些 fxSAVE token 发送到死地址,以确保 fxSAVE 的 totalSupply 永远不会为 0。

更新:已确认,未解决。f(x) 协议团队表示:

这个问题只在启动阶段相关。我们对 fxSAVE token 的生产部署包含一个受保护的启动机制,该机制确保运行时总供应量永远不会为零。因此,我们认为这个问题在实践中已得到有效缓解。

价格预言机中最低价格偏差计算不正确

ETHPriceOraclegetPrice() 函数LSDPriceOracleBasegetPrice() 函数BTCDerivativeOracleBasegetPrice()getExchangePrice() 函数,都使用 (anchorPrice - minPrice) / minPrice > maxDeviation 公式计算最小价格偏差。如果偏差高于允许的最大偏差,则最小价格将重置为锚定价格。

但是,此计算不正确。它检查与 minPrice 的偏差,这使得偏差具有限制性,并使其始终小于允许的与锚定价格的最大偏差。正确的公式是检查与 anchorPrice 而不是 minPrice 的偏差:(anchorPrice - minPrice) / anchorPrice > maxDeviation

在计算最小价格偏差时,请考虑检查与 anchorPrice 而不是 minPrice 的偏差。

更新:已在 pull request #20 中解决。

用户可以打开空头仓位

PoolManager 合约中的 operate 函数 允许任何用户打开、关闭或更新他们的仓位。如果用户想要关闭他们的仓位,他们可以简单地使用最小金额的 int256(即 type(int256).min 参数,用于 newDebtnewColl),同时提供他们的 positionId 参数。如果 positionId 参数为 0,则认为是要打开的新仓位。

但是,可以观察到,没有创建任何仓位的用户可以使用 type(int256).min 参数调用 operate 函数,用于 newDebtnewCollpositionId = 0,并使用最新的 positionId 铸造空头仓位。由于事件垃圾邮件,这可能会给链下分析造成歧义。这不是理想的行为,因为打开此类仓位也不会收取协议费用。

考虑在 operate 函数中添加进一步的检查,以避免铸造空头仓位。

更新:已确认,未解决。f(x) 协议团队表示:

我们确认用户可以触发带 type(int256).min 值的 operate 函数,用于 newDebtnewColl,且 positionId = 0,从而导致铸造“空”仓位的问题。虽然此行为不会对协议造成任何功能或经济风险(无抵押品、无债务、无系统影响),但我们同意这可能会导致链下事件噪音或索引歧义。

解决方案

  • 当前的实现不会对零值操作收取协议费用。
  • 为了保持清晰的链下索引并避免不必要的事件垃圾邮件,我们计划在未来的版本中实施一个过滤器,拒绝无操作状态转换(即,新仓位的零抵押品和零债务)。

这是一个非关键的 UX 级别问题,不会影响资金或协议逻辑,但我们重视反馈,并将提高集成商和索引器的清晰度。

低度严重

缺少 L2 排序器正常运行时间检查

如果 L2 排序器离线,用户将失去对读/写 API 的访问权限,从而导致 L2 网络上的应用程序实际上无法使用,除非他们直接通过 L1 乐观 Rollup合约进行交互。

虽然 L2 本身可能仍在运行,但继续在这种状态下服务应用程序是不公平的,因为只有一小部分用户可以与它们交互。为了防止这种情况,Chainlink 建议 将其排序器正常运行时间信息流集成到部署在 L2 上的任何项目中。这些信息流有助于检测排序器停机时间,从而使应用程序能够做出适当的响应。

代码库中的几个预言机调用可能会在排序器停机期间返回不准确的数据,包括:

为了在 Base 链上部署时帮助你的应用程序识别排序器何时不可用,你可以使用跟踪排序器在给定时间点的最后已知状态的数据源。

更新:已确认,计划解决。f(x) 协议团队表示:

我们承认该问题,并计划在未来的更新中解决。

策略分配可能会失败

AssetManagement 合约的 alloc 函数 不验证策略是否支持指定的 token。对于 AaveV3Strategy,发送给它的任何未知 token 都将无法恢复,因为 withdraw()kill() 只能与 ASSET 中指定的 token 交互。

考虑检查策略的 ASSET 是否与 alloc() 中的 asset 相同。

更新:已确认,未解决。f(x) 协议团队表示:

策略分配目前由多重签名治理流程控制。执行在批准前会经过仔细审核。展望未来,随着协议过渡到完全链上治理,我们将增强策略验证逻辑,以使此类故障在结构上不可能发生。

错误的资产策略可能永远无法更新

AssetManagement 合约的 alloc 函数 中,没有验证新的策略地址是否有效并支持 kill() 函数。一旦设置了错误的地址,由于不支持此 Strategy.kill()kill() 函数alloc() 函数 都会回滚以更新策略。

考虑在 alloc() 函数期间对添加的策略地址添加适当的验证,或者允许在不调用 kill 的情况下替换策略。

更新: 已确认,未解决。f(x) 协议团队表示:

此问题与 L-02 属于同一类别。 目前,对策略分配的更新受多重签名限制,并在执行前经过仔细审核。 未来,链上治理增强将引入更严格的验证逻辑,从而提高升级安全性和模块化。

未正确执行仓位的最低债务要求

BasePool 合约的 operate 函数中,需要 newRawDebt 值大于 MIN_DEBT,即 1e9。 然后 newRawDebt 转换为债务份额,这将 rawDebt 除以 debtIndex,从而导致该值小于 1e9 的最低债务份额要求。

但是,在 将此仓位添加到 tick 时,该函数再次检查针对 debtShares 的 1e9 的 MIN_DEBT 要求,这将回滚函数调用。 因此,用户将无法打开金额大于 MIN_DEBT 的仓位,直到转换后的 debtShares 严格高于 1e9。 因此,随着时间的推移,debtIndex 将增加,导致实际的最低债务要求也增加。

考虑将 MIN_DEBT 转换为使用 _addPositionToTick 中的债务指数的 MIN_SHARES 要求。 或者,考虑在调用 _addPositionToTick 之前立即检查 MIN_DEBT 要求,并使用 rawDebts 变量创建,并删除函数内的检查。

更新: 已确认,未解决。f(x) 协议团队表示:

我们认可该观测。 当前的 MIN_DEBT 检查有意地最小化,仅用于避免边缘情况下的 tick 计算错误。 根据其功能意图和低影响,我们认为当前的实现方式已经足够了。

不同的 Pragma 指令

为了清楚地识别将使用哪个 Solidity 版本来编译合约,pragma 指令应在文件导入中保持固定和一致。

范围内没有文件使用固定的 Solidity 版本,并且它们使用的版本也不同。 因此,请考虑在所有文件中使用相同的固定 pragma 版本。

更新:已确认,计划解决。f(x) 协议团队表示:

我们承认该问题,并计划在未来的更新中解决。

_takeAccumulatedPoolFee 中的误导性返回值

ProtocolFees 合约的 _takeAccumulatedPoolFee 函数返回一个 fees 变量,该变量被覆盖了三次:首先是 accumulatedPoolOpenFees,然后是 accumulatedPoolCloseFees,最后是 accumulatedPoolMiscFees。因此,返回值仅反映了最后一个类别。此外,没有内部或外部调用者使用此返回值,这使其毫无意义且可能令人困惑。

考虑删除未使用的返回值,以提高代码库的清晰度和可维护性。

更新: 已确认,计划解决。f(x) 协议团队表示:

我们承认该问题,并计划在未来的更新中解决。

预言机合约中链上现货编码的不一致的健全性检查

updateOnchainSpotEncodings setter 函数应验证其输入。 但是,非空检查仅在某些预言机中执行:

这种不一致使可以设置空编码或格式错误的编码,这可能导致读取函数回滚或返回无效数据。

考虑在每个 updateOnchainSpotEncodings 实现中添加统一的健全性检查,以确保所有预言机的数据完整性。

更新: 已确认,计划解决。f(x) 协议团队表示:

我们承认该问题,并计划在未来的更新中解决。

可能重复发送事件

当 setter 函数不检查要设置的值是否与现有值不同时,可以重复设置相同的值,从而创建了事件垃圾信息发送的可能性。 重复发送相同的事件也可能使链下客户端感到困惑。

在整个代码库中,发现了多个此类可能性的实例:

  • _updateRedeemCoolDownPeriod 设置 redeemCoolDownPeriod,并发出一个事件,而没有检查该值是否已更改。
  • _updateInstantRedeemFeeRatio 设置 instantRedeemFeeRatio,并发出一个事件,而没有检查该值是否已更改。
  • _updateConverter 设置 converter,并发出一个事件,而没有检查该值是否已更改。
  • _updateCurvePool 设置 curvePool,并发出一个事件,而没有检查该值是否已更改。
  • _updatePriceThreshold 设置 priceThreshold,并发出一个事件,而没有检查该值是否已更改。
  • _updateThreshold 设置 permissionedLiquidationThreshold,并发出一个事件,而没有检查该值是否已更改。
  • _updatePriceOracle 设置 priceOracle,并发出一个事件,而没有检查该值是否已更改。
  • _updateTreasury 设置 treasury,并发出一个事件,而没有检查该值是否已更改。
  • _updateOpenRevenuePool 设置 openRevenuePool,并发出一个事件,而没有检查该值是否已更改。
  • _updateCloseRevenuePool 设置 closeRevenuePool,并发出一个事件,而没有检查该值是否已更改。
  • _updateMiscRevenuePool 设置 miscRevenuePool,并发出一个事件,而没有检查该值是否已更改。
  • _updateReservePool 设置 reservePool,并发出一个事件,而没有检查该值是否已更改。

考虑添加一个检查,如果设置的值与现有值相同,则回滚事务。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

继承正确性

ProtocolFees 合约继承了 PausableUpgradeable 合约,但未实现任何暂停功能。 ProtocolFees 随后被 FlashLoansPoolManager 合约继承,这两个合约实现了暂停功能。 此外,在整个代码库中,开发人员的意图似乎是继承接口以实现合约。 例如,AaveFundingPool 继承 IAaveFundingPoolIPoolETHPriceOracle 继承 IPriceOracle 等。但是,AaveV3Strategy 合约没有继承 IStrategy 接口,因此打破了明显的惯例。

考虑让 StrategyBase 继承 IStrategy,并让 FlashLoans 继承 PausableUpgradeable,同时从 ProtocolFees 中移除它。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

Gap变量允许未来的存储冲突

在代码库中,发现了多个使用 gap 变量的合约实例:

Gap 变量允许继承的合约在未来扩展其存储,而不会与继承合约的存储发生冲突。 由于代码库已投入生产,因此务必确保任何存储更改都反映在 gap 变量中。

为了更好地降低新部署的可升级合约中发生存储冲突的风险,请考虑使用 namespace storage 或使用 Solitidy 0.8.29 版本 中提供的自定义存储布局。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认由于非结构化的 gap 变量而导致的未来存储布局冲突的理论风险。 但是,受影响的合约已经部署,更改存储布局将对数据完整性和合约行为构成重大风险。 展望未来,我们计划采用命名空间存储技术或 Solidity 0.8.29+ 中引入的结构化布局支持,以降低未来迭代中的此类风险。

使用瞬时存储可以降低Gas成本

PegKeeper 在调用 buybackstabilize 函数时设置一个存储变量 context,并在这些调用完成时恢复它。 该合约使用此变量来确保 onSwap 函数是在其他函数的上下文中被调用的。 这正是 瞬时存储 的用例之一,它在 Solidity 0.8.24 版本 中可用。

为了节省这些调用中的 gas,请考虑在瞬时存储而不是永久存储中设置上下文。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

缺少文档字符串

在整个代码库中,发现了多个缺少文档字符串的实例:

考虑彻底记录所有作为任何合约公共 API 一部分的函数(及其参数)。 即使不是公共的,实现敏感功能的函数也应明确记录。 编写文档字符串时,请考虑遵循 以太坊自然规范格式 (NatSpec)。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

不完整的文档字符串

在整个代码库中,发现了多个不完整的文档字符串的实例:

  • BTCDerivativeOracleBase.sol 中,getBTCDerivativeUSDAnchorPrice 函数的 isRedeem 参数未记录。
  • PoolManager.sol 中,registerPool 函数的 collateralCapacitydebtCapacity 参数未记录。
  • ReservePool.sol 中,withdrawFund 函数的 amount 参数未记录。

考虑彻底记录所有作为合约公共 API 一部分的函数/事件(及其参数或返回值)。 编写文档字符串时,请考虑遵循 以太坊自然规范格式 (NatSpec)。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

注释 & 附加信息

误导性的存储描述

一些存储槽的文档描述方式可能会导致混淆。 例如,在 ProtocolFees 合约中,_miscData 上方的注释显示了槽的组件布局,其中零索引位于左侧,而普遍的惯例是显示零索引位于右侧的槽。 左侧被指定为最高有效位 (MSB) 加剧了这种混淆,这在标准惯例下是正确的,但如果布局颠倒则不正确。

在整个代码库中,发现了多个此类误导性存储槽描述的实例:

为避免混淆,请考虑反转存储描述,使索引 0 从右侧开始(与 Solidity 惯例对齐),或者交换注释中最高有效位和最低有效位的位置,以匹配当前从左到右的描述。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

缺少事件发出

在整个代码库中,发现了多个在更新状态时没有发出事件的函数实例:

考虑在这些函数中执行状态更改时发出事件,以提高透明度和更好的监控能力。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

_transferOut 中的静默短缺

AssetManagement 合约的 _transferOut 函数旨在将精确数量的资产转移给接收者,如果合约持有的余额不足,则回退到相关的策略。 它首先发送其手头的 token,然后调用策略的 withdraw 函数来弥补短缺。

但是,在 AaveV3Strategy 合约中,当被要求提取超过策略可用流动性的数量时,withdraw 函数不会回滚。 相反,它只是提取尽可能多的数量。 因此,_transferOut 可能会在没有错误的情况下完成,同时转移的数量少于预期的数量,从而违反了它要么完全成功要么回滚的期望。

考虑在 _transferOut 函数中添加一个撤回后健全性检查,以验证是否已转移全额,否则回滚。

Update:** 已确认,计划进行修复。f(x) 协议团队声明:

我们承认这个问题,并计划在未来的更新中解决它。

不正确的注释

在整个代码库中,发现了多个不正确或不准确的注释实例:

考虑使用 calldata 作为 external 函数的参数的数据位置,以优化 gas 消耗。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

使用自定义错误

自从 Solidity 0.8.4 版本以来,自定义错误提供了一种更清晰、性价比更高的方案,用于向用户解释操作失败的原因。

在整个代码库中,在以下合约中发现了 revert 的实例:

合约 实例
FxUSDBasePool 2
FxUSDRegeneracy 4
SavingFxUSD 1
AssetManagement 2
StrategyBase 1
BTCDerivativeOracleBase 1
SpotPriceOracleBase 2

这些实例中有很多是带有非描述性消息的 revert。 为了简洁、清晰和节省 gas,请考虑用自定义错误替换这些 revert 消息。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

不必要的类型转换

ProtocolFees.sol 中,发现了多个不必要的类型转换实例:

为了提高代码库的整体清晰度和意图,请考虑删除任何不必要的类型转换。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

合约内部顺序不一致

在整个代码库中,大多数作用域合约的函数排序不一致。

为了提高项目的整体可读性,请考虑按照 Solidity 风格指南的函数布局和顺序 推荐的标准,在整个代码库中标准化排序。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

前缀递增运算符 (++i) 可以节省循环中的 Gas

在整个代码库中,发现了多个优化循环迭代的机会:

考虑使用前缀递增运算符 (++i) 而不是后缀递增运算符 (i++) 来节省 gas。 此优化跳过了在递增操作之前存储值的步骤,因为表达式的返回值被忽略。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

未使用的导入

在整个代码库中,发现了多个未使用的导入实例

考虑删除未使用的导入,以提高代码库的整体清晰度和可读性。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

状态变量可见性未显式声明

在整个代码库中,发现了多个状态变量缺少显式声明的可见性的实例:

为了提高代码清晰度,请考虑始终显式声明状态变量的可见性,即使默认可见性与预期可见性相符。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

未使用的命名返回值变量

命名返回值变量是一种声明变量的方式,这些变量旨在函数体中使用,目的是作为该函数的输出返回。 它们是显式的内联 return 语句的替代方案。

在整个代码库中,发现了多个未使用的命名返回值变量的实例:

考虑删除这些未使用的命名返回值变量,除非它们应该被使用。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

未使用的错误

在整个代码库中,发现了多个未使用的错误的实例:

为了提高代码库的整体清晰度、意图和可读性,请考虑使用或删除任何当前未使用的错误。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

msg.sender 使用不一致

在某些情况下,合约可能会使用 _msgSender_msgData 函数,因为它们允许元交易,并且已经重写了这些方法以提取原始消息的 sender/data。 应手动检查合约中 _msgSender/msg.sender_msgData/msg.data 的一致使用情况。 这是因为任何不一致都可能是一个错误,并可能对执行元交易产生意想不到的后果。

StrategyBase 合约的 onlyOperator 修饰符中,正在使用 msg.sender 而不是 _msgSender

考虑手动检查 msg.sendermsg.data 的任何不一致使用情况,并更新这些实例以遵循整个代码库中的一致行为。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

未使用的常量

在整个代码库中,发现了多个未使用的常量的实例:

为了提高代码库的整体清晰度和意图,请考虑删除未使用的常量。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

字面数字安全

在整个代码库中,发现了多个直接使用字面数字的实例:

对于具有这么多位数的字面数字,请考虑使用 Ether 后缀时间后缀科学计数法。 这将有助于提高可读性并防止可能产生意外后果的误导性代码。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

魔法数字

AaveFundingPool 合约的 initialize 函数中,正在使用一个具有未解释含义的字面值(1e9)正在使用

考虑定义和使用 constant 变量来代替使用字面量,以提高代码库的可读性。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

缺乏安全联系人

在智能合约中提供特定的安全联系方式(例如电子邮件或 ENS 名称)可以大大简化个人在代码中发现漏洞时进行沟通的过程。 这种做法非常有益,因为它允许代码所有者指定漏洞披露的通信渠道,从而消除了由于缺乏如何操作的知识而导致沟通不畅或未能报告的风险。 此外,如果合约包含第三方库并且这些库中出现错误,则维护人员可以更轻松地联系到相关人员以了解问题并提供缓解说明。

考虑在每个合约定义的顶部添加一个包含安全联系方式的 NatSpec 注释。 建议使用 @custom:security-contact 约定,因为它已被 OpenZeppelin Wizardethereum-lists 采用。

更新: 已知悉,计划解决。f(x) 协议团队声明:

我们已知悉该问题,并计划在未来的更新中解决。

映射中缺少命名参数

自从 Solidity 0.8.18 以来,开发人员可以在映射中使用命名参数。 这意味着映射可以采用 mapping(KeyType KeyName? => ValueType ValueName?) 的形式。 这种更新后的语法提供了更透明的映射用途表示。

在整个代码库中,发现了多个没有命名参数的映射实例:

考虑实施上述重命名建议,以提高代码库的可读性和可维护性。

更新: 已确认,计划解决。 f(x) 协议团队表示:

我们承认这个问题,并计划在未来的更新中解决它。

未使用的代码

TickLogic 中,_getTick 函数 花费一行代码计算一个新的比率,但它永远不会使用该比率。 考虑删除这个多余的行。

更新: 已确认,计划解决。 f(x) 协议团队表示:

我们承认这个问题,并计划在未来的更新中解决它。

结论

f(x) 协议 v2.0 引入了一种新型稳定币实现——fxUSD——具有改进的稳定动态和先进的Hook机制,以及其去中心化交易平台 xPosition。 该协议依赖于复杂的机制来维持稳定,包括批量再平衡、清算、赎回以及基于“tick”的方法来维持每个基础池的杠杆头寸。

它利用特殊的稳定池、Hook维持者、融资费用和收割机制来奖励系统的维护者并减轻脱钩的风险。 该系统采用基于 tick 的方法独特地管理 xPosition,该方法将头寸分组到大约 0.15% 的价格范围内,以提高更新头寸的效率。 使用多个来源来确定底层抵押代币的价格,包括链下预言机和链上现货价格的组合。

与 f(x) 协议的某些集成(例如 GaugeConvex VaultSpotPriceOracleRevenuePoolTreasuryfTokensMarketV2)不在本次审计范围内。 此外,该协议在很大程度上预计通过外部闪贷提供商(例如MorphoBalancer)使用外围facet合约来为用户头寸提供资金,这也不在本次审计范围内。

在整个审计过程中,主要重点是验证 tick 逻辑以及关于再平衡、清算和赎回的各种极端情况,同时评估系统的整体经济稳定性。 该系统尽管依赖于多个合约,但反映了强大的架构和高弹性。

发现了一个严重性问题、两个高严重性问题和多个中低严重性问题。 严重性问题操纵了 tick 和节点逻辑以阻止 operate 功能。 在高严重性问题中,一个是定价方案容易受到单个可操纵流动性池的影响,这可能导致头寸的早期清算。 另一个完全阻止了协议的闪贷功能。

与 f(x) 协议团队的合作非常顺利且高效。 他们的响应能力和上下文清晰度有助于理解该协议和设计选择背后的更广泛的原因。

与专家交谈

  • 原文链接: blog.openzeppelin.com/fx...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。