这是一份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
合约,允许它在投资者持有其杠杆头寸时赚取收益。
范围内的合约彼此紧密集成,并且依赖于正确的配置才能正常工作。因此,在本次审计过程中,做出了以下信任假设:
AaveFundingPool
DEFAULT_ADMIN_ROLE
可以:
EMERGENCY_ROLE
可以:
PoolManager
合约:
operate
和 redeem
rebalance
和 liquidate
。如果稳定池的总价值未达到阈值,则可以扩展到任何人AaveV3Strategy
HARVESTER_ROLE
可以:
operator
可以:
operator 角色适用于像池管理器和稳定池等继承 AssetManagement
合约的合约。
FxUSDBasePool
DEFAULT_ADMIN_ROLE
可以:
ASSET_MANAGER_ROLE
可以:
PegKeeper
的 STABILIZE_ROLE
可以:
FxUSDRegeneracy
PoolManager
可以:
PegKeeper
的 BUYBACK_ROLE
可以
PegKeeper
BUYBACK_ROLE
可以:
fxUSDRegeneracy
)合约中使用 USDC 购买 fxUSDSTABILIZE_ROLE
可以:
DEFAULT_ADMIN_ROLE
可以:
MultiPathConverter
合约(用于)兑换资产的地址PoolManager
DEFAULT_ADMIN_ROLE
可以:
rebalance
和 liquidate
ASSET_MANAGER_ROLE
可以:
HARVESTER_ROLE
可以:
EMERGENCY_ROLE
可以:
稳定池允许任何人:
ReservePool
DEFAULT_ADMIN_ROLE
可以:
PoolManager
可以:
SavingFxUSD
DEFAULT_ADMIN_ROLE
可以:
CLAIM_FOR_ROLE
可以:
owner
可以:
协议的 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 检查和递归属性来执行以下步骤:
反复调用 redeem
函数,并销毁最少量的 rawDebt(fxUSD)(例如,销毁大约 150 次 2 wei)。这确保了永远不会更新 顶部 tick ,并且创建了 150 个子节点。
再次调用 redeem
函数,并以计算出的高额值转移到新的顶部 tick 以定位更多头寸。
一旦目标 tick 再次成为顶部 tick,请重复步骤 1。
由于此机制,每当用户尝试使用 operate
函数关闭或更新其中一个受影响的头寸时,_getRootNodeAndCompress
将因堆栈溢出错误而失败,因为子节点超过了一定限制。 这个 POC 通过操纵顶部 tick 并锁定与该 tick 对应的所有头寸的资金来演示堆栈溢出行为。用户将无法关闭或更新其头寸,他们只能被重新平衡或清算,因此他们的资金将被锁定。
为了解决根本问题,请考虑迁移到 _getRootNodeAndCompress
函数的非递归版本。为了进一步防止通过 redeem
函数进行的 Gas 消耗攻击,请考虑实施其他检查,例如最低 rawDebt
要求,以确保顶部 tick 始终移动,这将增加任何试图定位 tick 的攻击者的难度。
更新: 已在 pull request #22 中解决。
该团队已迁移到 _getRootNodeAndCompress()
函数的迭代版本,并为 redeem
、rebalance
和 liquidate
功能添加了最低 rawDebts
要求。还添加了一个管理函数来压缩节点链,以防通过树结构的链下监控检测到任何意外行为。f(x) 协议团队表示:
我们已实施以下更改以应对观察到的极端情况:
- 添加了最低原始债务阈值:我们为
redeem
、rebalance
和liquidate
操作引入了最低原始债务要求,以防止清除级别的滥用并确保 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
,并且还使用多个链上池来获取现货价格,这些现货价格充当 相同的 minPrice
和 maxPrice
。minPrice
是通过获取所有获取的现货价格中的最低价格得出的,类似地,maxPrice
是通过获取现货价格中的最高价格得出的。
oracle 合约的 getPrice
函数 确保从链上池返回的 minPrice
和 maxPrice
值与 anchorPrice
的偏差不超过 1%。如果价格 偏差超过 1%,它会将各自偏差的 minPrice
或 maxPrice
重置为 anchorPrice
。抵押品的 minPrice
用于 操作头寸、重新平衡 和 清算 期间。另一方面,maxPrice
用于 redeem
功能中。
但是,尽管允许 minPrice
和 maxPrice
偏差 1%,但只需要一个池来操纵价格。因此,攻击者可以定位 TVL 最低的池或可以在同一交易中进行操纵的池,并以导致 anchorPrice
偏差恰好 1% 且 绕过偏差检查 的方式操纵价格。
这种操纵 1% 价格的行为允许恶意清算人降低 minPrice
,以迫使脆弱的头寸提前被清算并产生更多利润。类似地,可以操纵 maxPrice
使其偏差超过 1% 并重置为 anchorPrice
,这会在 redeem
功能期间开启套利机会。
通过将所有现货价格压缩为一个 minPrice
和一个 maxPrice
,然后强制任何异常值回到锚定,从而否定了预期的多池弹性。在实践中,系统总是会回退到单个被操纵的池或锚定提要。换句话说,仅破坏一个低 TVL 池就会将“多样化”的预言机变成单点故障。
例如,当前的 stETH 价格预言机 依赖于 3 个池来获取 ETH/USD 现货价格:WETH/USDC Uniswap V2、WETH/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
仓位、rebalance
、redeem
和liquidate
,以避免 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
仓位、rebalance
和liquidate
操作。1.
operate
仓位操作:minPrice
价格数据仅用于确定存在超额抵押时的用户抵押品价值。即使攻击者操纵最低价格使其偏离anchorPrice
1%,由于存在超额抵押机制,单笔交易的价格操纵行为也不会对协议产生任何不利影响。2.
rebalance
和liquidate
操作:从以上分析可知,理论上可以控制minPrice
使其偏离anchorPrice
恰好 1%,但实际上,攻击者需要更多地考虑难度和攻击成本。在大多数情况下,f(x) 协议执行rebalance
和liquidate
操作时,抵押资产的价格会急剧下降。首先,此时每个 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 规定:
在回调之后,
flashLoan
函数必须从接收者那里获取金额 + 手续费 token,如果失败则必须回滚。
flashLoan
没有获取 token,而是期望调用者已经返回了 token。这将阻止合约与任何兼容的 IERC3156FlashBorrower
合约集成,因为他们不会在这里将 token 返回给合约。
ERC-3156 进一步规定:
flashFee
函数必须返回 amount token 的贷款费用。如果不支持该 token,则flashFee
必须回滚。
这里“支持”一词的使用是模棱两可的,因为它没有指定最大贷款额为零意味着 token “不受支持”。但是,ERC 确实将 maxFlashLoan
中返回零与 token 不受支持混为一谈。因此,在 maxFlashLoan
将返回零的情况下,flashFee
必须回滚。当前,该函数只是计算给定金额的一部分,而不管 token 如何。
考虑修复 flashLoan
和 flashFee
函数,使其符合上述 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 已存入策略并产生收益。所有外部功能,如 deposit
、redeem
等,在继续之前都会使用 sync
修饰符更新此 totalStableToken
变量。
但是,FxUSDStabilityPool
合约的 previewDeposit、previewRedeem 和 nav 函数使用过时的 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 的生产部署包含一个受保护的启动机制,该机制确保运行时总供应量永远不会为零。因此,我们认为这个问题在实践中已得到有效缓解。
ETHPriceOracle
的 getPrice()
函数、LSDPriceOracleBase
的 getPrice()
函数、BTCDerivativeOracleBase
的 getPrice()
和 getExchangePrice()
函数,都使用 (anchorPrice - minPrice) / minPrice > maxDeviation
公式计算最小价格偏差。如果偏差高于允许的最大偏差,则最小价格将重置为锚定价格。
但是,此计算不正确。它检查与 minPrice
的偏差,这使得偏差具有限制性,并使其始终小于允许的与锚定价格的最大偏差。正确的公式是检查与 anchorPrice
而不是 minPrice
的偏差:(anchorPrice - minPrice) / anchorPrice > maxDeviation
。
在计算最小价格偏差时,请考虑检查与 anchorPrice
而不是 minPrice
的偏差。
更新:已在 pull request #20 中解决。
PoolManager
合约中的 operate
函数 允许任何用户打开、关闭或更新他们的仓位。如果用户想要关闭他们的仓位,他们可以简单地使用最小金额的 int256
(即 type(int256).min
参数,用于 newDebt
和 newColl
),同时提供他们的 positionId
参数。如果 positionId
参数为 0,则认为是要打开的新仓位。
但是,可以观察到,没有创建任何仓位的用户可以使用 type(int256).min
参数调用 operate
函数,用于 newDebt
和 newColl
,positionId
= 0,并使用最新的 positionId
铸造空头仓位。由于事件垃圾邮件,这可能会给链下分析造成歧义。这不是理想的行为,因为打开此类仓位也不会收取协议费用。
考虑在 operate
函数中添加进一步的检查,以避免铸造空头仓位。
更新:已确认,未解决。f(x) 协议团队表示:
我们确认用户可以触发带
type(int256).min
值的operate
函数,用于newDebt
和newColl
,且positionId = 0
,从而导致铸造“空”仓位的问题。虽然此行为不会对协议造成任何功能或经济风险(无抵押品、无债务、无系统影响),但我们同意这可能会导致链下事件噪音或索引歧义。解决方案:
- 当前的实现不会对零值操作收取协议费用。
- 为了保持清晰的链下索引并避免不必要的事件垃圾邮件,我们计划在未来的版本中实施一个过滤器,拒绝无操作状态转换(即,新仓位的零抵押品和零债务)。
这是一个非关键的 UX 级别问题,不会影响资金或协议逻辑,但我们重视反馈,并将提高集成商和索引器的清晰度。
如果 L2 排序器离线,用户将失去对读/写 API 的访问权限,从而导致 L2 网络上的应用程序实际上无法使用,除非他们直接通过 L1 乐观 Rollup合约进行交互。
虽然 L2 本身可能仍在运行,但继续在这种状态下服务应用程序是不公平的,因为只有一小部分用户可以与它们交互。为了防止这种情况,Chainlink 建议 将其排序器正常运行时间信息流集成到部署在 L2 上的任何项目中。这些信息流有助于检测排序器停机时间,从而使应用程序能够做出适当的响应。
代码库中的几个预言机调用可能会在排序器停机期间返回不准确的数据,包括:
FxUSDBasePool.sol
中的 AggregatorV3Interface(aggregator).latestRoundData
。SpotPriceOracleBase.sol
中的 AggregatorV3Interface(aggregator).latestRoundData
。为了在 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 计算错误。 根据其功能意图和低影响,我们认为当前的实现方式已经足够了。
为了清楚地识别将使用哪个 Solidity 版本来编译合约,pragma 指令应在文件导入中保持固定和一致。
范围内没有文件使用固定的 Solidity 版本,并且它们使用的版本也不同。 因此,请考虑在所有文件中使用相同的固定 pragma 版本。
更新:已确认,计划解决。f(x) 协议团队表示:
我们承认该问题,并计划在未来的更新中解决。
_takeAccumulatedPoolFee
中的误导性返回值ProtocolFees
合约的 _takeAccumulatedPoolFee
函数返回一个 fees
变量,该变量被覆盖了三次:首先是 accumulatedPoolOpenFees
,然后是 accumulatedPoolCloseFees
,最后是 accumulatedPoolMiscFees
。因此,返回值仅反映了最后一个类别。此外,没有内部或外部调用者使用此返回值,这使其毫无意义且可能令人困惑。
考虑删除未使用的返回值,以提高代码库的清晰度和可维护性。
更新: 已确认,计划解决。f(x) 协议团队表示:
我们承认该问题,并计划在未来的更新中解决。
updateOnchainSpotEncodings
setter 函数应验证其输入。 但是,非空检查仅在某些预言机中执行:
prices.length == 0
时,BTCDerivativeOracleBase.updateOnchainSpotEncodings
会回滚。ETHPriceOracle.updateOnchainSpotEncodings
对 prices.length
没有检查。LSDPriceOracleBase.updateOnchainSpotEncodings
仅对一个 spotType
应用检查。这种不一致使可以设置空编码或格式错误的编码,这可能导致读取函数回滚或返回无效数据。
考虑在每个 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
随后被 FlashLoans
和 PoolManager
合约继承,这两个合约实现了暂停功能。 此外,在整个代码库中,开发人员的意图似乎是继承接口以实现合约。 例如,AaveFundingPool
继承 IAaveFundingPool
和 IPool
,ETHPriceOracle
继承 IPriceOracle
等。但是,AaveV3Strategy
合约没有继承 IStrategy
接口,因此打破了明显的惯例。
考虑让 StrategyBase
继承 IStrategy
,并让 FlashLoans
继承 PausableUpgradeable
,同时从 ProtocolFees
中移除它。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认这个问题,并计划在未来的更新中解决它。
在代码库中,发现了多个使用 gap 变量的合约实例:
Gap 变量允许继承的合约在未来扩展其存储,而不会与继承合约的存储发生冲突。 由于代码库已投入生产,因此务必确保任何存储更改都反映在 gap 变量中。
为了更好地降低新部署的可升级合约中发生存储冲突的风险,请考虑使用 namespace storage 或使用 Solitidy 0.8.29 版本 中提供的自定义存储布局。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认由于非结构化的 gap 变量而导致的未来存储布局冲突的理论风险。 但是,受影响的合约已经部署,更改存储布局将对数据完整性和合约行为构成重大风险。 展望未来,我们计划采用命名空间存储技术或 Solidity 0.8.29+ 中引入的结构化布局支持,以降低未来迭代中的此类风险。
PegKeeper 在调用 buyback
和 stabilize
函数时设置一个存储变量 context
,并在这些调用完成时恢复它。 该合约使用此变量来确保 onSwap
函数是在其他函数的上下文中被调用的。 这正是 瞬时存储 的用例之一,它在 Solidity 0.8.24 版本 中可用。
为了节省这些调用中的 gas,请考虑在瞬时存储而不是永久存储中设置上下文。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认这个问题,并计划在未来的更新中解决它。
在整个代码库中,发现了多个缺少文档字符串的实例:
AaveFundingPool
合约 应该在上面有 NatSpec 注释。AaveFundingPool.sol
中,initialize
函数AaveV3Strategy.sol
中,POOL
, INCENTIVE
, ASSET
, ATOKEN
, 和 principal
状态变量,以及 totalSupply
, deposit
, withdraw
, 和 kill
函数AssetManagement.sol
中,ASSET_MANAGER_ROLE
和 allocations
状态变量,以及 kill
, alloc
, 和 manage
函数。FxUSDBasePool.sol
中,initialize
和 updateInstantRedeemFeeRatio
函数FxUSDRegeneracy.sol
中,initialize
和 initializeV2
函数IStrategy.sol
中,totalSupply
, deposit
, withdraw
, kill
, 和 harvest
函数PegKeeper.sol
中,initialize
函数PoolManager.sol
中,initialize
和 initializeV2
函数ReservePool.sol
中,receive
函数SavingFxUSD.sol
中,execute
和 initialize
函数StrategyBase.sol
中,HARVESTER_ROLE
和 operator
状态变量,以及 harvest
和 execute
函数考虑彻底记录所有作为任何合约公共 API 一部分的函数(及其参数)。 即使不是公共的,实现敏感功能的函数也应明确记录。 编写文档字符串时,请考虑遵循 以太坊自然规范格式 (NatSpec)。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认这个问题,并计划在未来的更新中解决它。
在整个代码库中,发现了多个不完整的文档字符串的实例:
BTCDerivativeOracleBase.sol
中,getBTCDerivativeUSDAnchorPrice
函数的 isRedeem
参数未记录。PoolManager.sol
中,registerPool
函数的 collateralCapacity
和 debtCapacity
参数未记录。ReservePool.sol
中,withdrawFund
函数的 amount
参数未记录。考虑彻底记录所有作为合约公共 API 一部分的函数/事件(及其参数或返回值)。 编写文档字符串时,请考虑遵循 以太坊自然规范格式 (NatSpec)。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认这个问题,并计划在未来的更新中解决它。
一些存储槽的文档描述方式可能会导致混淆。 例如,在 ProtocolFees
合约中,_miscData
上方的注释显示了槽的组件布局,其中零索引位于左侧,而普遍的惯例是显示零索引位于右侧的槽。 左侧被指定为最高有效位 (MSB) 加剧了这种混淆,这在标准惯例下是正确的,但如果布局颠倒则不正确。
在整个代码库中,发现了多个此类误导性存储槽描述的实例:
PoolStorage
合约中的 miscData
, rebalanceRatioData
, indexData
, shareData
, 和 positionMetadata
ProtocolFees
合约中的 _miscData
AaveFundingPool
合约中的 fundingMiscData
为避免混淆,请考虑反转存储描述,使索引 0 从右侧开始(与 Solidity 惯例对齐),或者交换注释中最高有效位和最低有效位的位置,以匹配当前从左到右的描述。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认这个问题,并计划在未来的更新中解决它。
在整个代码库中,发现了多个在更新状态时没有发出事件的函数实例:
AssetManagement.sol
中的 kill, alloc, 和 manage 函数BTCDerivativeOracleBase.sol
中的 updateOnchainSpotEncodings 函数LSDPriceOracleBase.sol
中的 updateOnchainSpotEncodings 函数考虑在这些函数中执行状态更改时发出事件,以提高透明度和更好的监控能力。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认这个问题,并计划在未来的更新中解决它。
_transferOut
中的静默短缺AssetManagement
合约的 _transferOut
函数旨在将精确数量的资产转移给接收者,如果合约持有的余额不足,则回退到相关的策略。 它首先发送其手头的 token,然后调用策略的 withdraw
函数来弥补短缺。
但是,在 AaveV3Strategy
合约中,当被要求提取超过策略可用流动性的数量时,withdraw
函数不会回滚。 相反,它只是提取尽可能多的数量。 因此,_transferOut
可能会在没有错误的情况下完成,同时转移的数量少于预期的数量,从而违反了它要么完全成功要么回滚的期望。
考虑在 _transferOut
函数中添加一个撤回后健全性检查,以验证是否已转移全额,否则回滚。
Update:** 已确认,计划进行修复。f(x) 协议团队声明:
我们承认这个问题,并计划在未来的更新中解决它。
在整个代码库中,发现了多个不正确或不准确的注释实例:
BTCDerivativeOracleBase
, ETHPriceOracle
, 和 [LSDPriceOracleBase
](https://github.com/AladdinDAO/fx-protocol-contracts/blob/56a47eab8d10334e479df83a2b13a8- 在 AaveFundingPool.sol
中,name_
参数AaveFundingPool.sol
中,symbol_
参数BTCDerivativeOracleBase.sol
中,encodings
参数ETHPriceOracle.sol
中,encodings
参数FxUSDBasePool.sol
中,_name
参数FxUSDBasePool.sol
中,_symbol
参数FxUSDRegeneracy.sol
中,_name
参数FxUSDRegeneracy.sol
中,_symbol
参数FxUSDRegeneracy.sol
中,_minOuts
参数LSDPriceOracleBase.sol
中,encodings
参数ProtocolFees.sol
中,pools
参数SavingFxUSD.sol
中,params
参数考虑使用 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
中,发现了多个不必要的类型转换实例:
uint256(newRatio)
转换uint256(newRatio)
转换uint256(newRatio)
转换uint256(newRatio)
转换uint256(newRatio)
转换uint256(newRatio)
转换为了提高代码库的整体清晰度和意图,请考虑删除任何不必要的类型转换。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在整个代码库中,大多数作用域合约的函数排序不一致。
AaveFundingPool
合约,位于 AaveFundingPool.sol
中AaveV3Strategy
合约,位于 AaveV3Strategy.sol
中AssetManagement
合约,位于 AssetManagement.sol
中BTCDerivativeOracleBase
合约,位于 BTCDerivativeOracleBase.sol
中BasePool
合约,位于 BasePool.sol
中ETHPriceOracle
合约,位于 ETHPriceOracle.sol
中FlashLoans
合约,位于 FlashLoans.sol
中FxUSDBasePool
合约,位于 FxUSDBasePool.sol
中FxUSDRegeneracy
合约,位于 FxUSDRegeneracy.sol
中LSDPriceOracleBase
合约,位于 LSDPriceOracleBase.sol
中PegKeeper
合约,位于 PegKeeper.sol
中PoolManager
合约,位于 PoolManager.sol
中PoolStorage
合约,位于 PoolStorage.sol
中PositionLogic
合约,位于 PositionLogic.sol
中ProtocolFees
合约,位于 ProtocolFees.sol
中ReservePool
合约,位于 ReservePool.sol
中SavingFxUSD
合约,位于 SavingFxUSD.sol
中SpotPriceOracleBase
合约,位于 SpotPriceOracleBase.sol
中StrategyBase
合约,位于 StrategyBase.sol
中WBTCPriceOracle
合约,位于 WBTCPriceOracle.sol
中为了提高项目的整体可读性,请考虑按照 Solidity 风格指南的函数布局和顺序 推荐的标准,在整个代码库中标准化排序。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
++i
) 可以节省循环中的 Gas在整个代码库中,发现了多个优化循环迭代的机会:
考虑使用前缀递增运算符 (++i
) 而不是后缀递增运算符 (i++
) 来节省 gas。 此优化跳过了在递增操作之前存储值的步骤,因为表达式的返回值被忽略。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在整个代码库中,发现了多个未使用的导入实例
import { ITwapOracle } from "./interfaces/ITwapOracle.sol";
在 ETHPriceOracle.sol 中导入了未使用的别名 ITwapOracle
import { ITwapOracle } from "./interfaces/ITwapOracle.sol";
在 LSDPriceOracleBase.sol 中导入了未使用的别名 ITwapOracle
考虑删除未使用的导入,以提高代码库的整体清晰度和可读性。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在整个代码库中,发现了多个状态变量缺少显式声明的可见性的实例:
fxSAVE
状态变量spotPriceOracle
状态变量为了提高代码清晰度,请考虑始终显式声明状态变量的可见性,即使默认可见性与预期可见性相符。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
命名返回值变量是一种声明变量的方式,这些变量旨在函数体中使用,目的是作为该函数的输出返回。 它们是显式的内联 return
语句的替代方案。
在整个代码库中,发现了多个未使用的命名返回值变量的实例:
AaveFundingPool.sol
中,getOpenRatio
函数的 ratio
和 step
返回变量PositionLogic.sol
中,getPositionDebtRatio
函数的 debtRatio
返回变量考虑删除这些未使用的命名返回值变量,除非它们应该被使用。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在整个代码库中,发现了多个未使用的错误的实例:
PoolErrors.sol
中的 ErrorRebalanceOnLiquidatablePosition
错误PoolErrors.sol
中的 ErrorInsufficientCollateralToLiquidate
错误ReservePool.sol
中的 ErrorRatioTooLarge
错误ReservePool.sol
中的 ErrorRebalancePoolAlreadyAdded
错误ReservePool.sol
中的 ErrorRebalancePoolNotAdded
错误为了提高代码库的整体清晰度、意图和可读性,请考虑使用或删除任何当前未使用的错误。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
msg.sender
使用不一致在某些情况下,合约可能会使用 _msgSender
和 _msgData
函数,因为它们允许元交易,并且已经重写了这些方法以提取原始消息的 sender/data
。 应手动检查合约中 _msgSender/msg.sender
和 _msgData/msg.data
的一致使用情况。 这是因为任何不一致都可能是一个错误,并可能对执行元交易产生意想不到的后果。
在 StrategyBase
合约的 onlyOperator
修饰符中,正在使用 msg.sender
而不是 _msgSender
。
考虑手动检查 msg.sender
和 msg.data
的任何不一致使用情况,并更新这些实例以遵循整个代码库中的一致行为。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在整个代码库中,发现了多个未使用的常量的实例:
AaveFundingPool.sol
中,INTEREST_RATE_OFFSET 常量AaveFundingPool.sol
中,TIMESTAMP_OFFSET 常量PoolConstant.sol
中,X60 常量PoolConstant.sol
中,X96 常量为了提高代码库的整体清晰度和意图,请考虑删除未使用的常量。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在整个代码库中,发现了多个直接使用字面数字的实例:
AaveFundingPool.sol
中的 50000000000000000
字面数字BasePool.sol
中的 500000000000000000
字面数字PegKeeper.sol
中的 995000000000000000
字面数字对于具有这么多位数的字面数字,请考虑使用 Ether 后缀、时间后缀 或 科学计数法。 这将有助于提高可读性并防止可能产生意外后果的误导性代码。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在 AaveFundingPool
合约的 initialize
函数中,正在使用一个具有未解释含义的字面值(1e9)正在使用。
考虑定义和使用 constant
变量来代替使用字面量,以提高代码库的可读性。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
在智能合约中提供特定的安全联系方式(例如电子邮件或 ENS 名称)可以大大简化个人在代码中发现漏洞时进行沟通的过程。 这种做法非常有益,因为它允许代码所有者指定漏洞披露的通信渠道,从而消除了由于缺乏如何操作的知识而导致沟通不畅或未能报告的风险。 此外,如果合约包含第三方库并且这些库中出现错误,则维护人员可以更轻松地联系到相关人员以了解问题并提供缓解说明。
考虑在每个合约定义的顶部添加一个包含安全联系方式的 NatSpec 注释。 建议使用 @custom:security-contact
约定,因为它已被 OpenZeppelin Wizard 和 ethereum-lists 采用。
更新: 已知悉,计划解决。f(x) 协议团队声明:
我们已知悉该问题,并计划在未来的更新中解决。
自从 Solidity 0.8.18 以来,开发人员可以在映射中使用命名参数。 这意味着映射可以采用 mapping(KeyType KeyName? => ValueType ValueName?)
的形式。 这种更新后的语法提供了更透明的映射用途表示。
在整个代码库中,发现了多个没有命名参数的映射实例:
AssetManagement
合约中的 allocations
状态变量FxUSDBasePool
合约中的 redeemRequests
状态变量FxUSDRegeneracy
合约中的 markets
状态变量PoolManager
合约中的 poolInfo
、rewardSplitter
和 tokenRates
状态变量PoolStorage
合约中的 [positionData
](https://github.com/AladdinDAO/fx-protocol-contracts/blob/56a47eab8d10334e479df83a2b13a8b68ce390e9/contracts/core/pool/- PoolManager
合约的 onlyFxSave
modifier 确保了调用者是 fxBase
合约,而不是 fxSAVE
合约。因此,考虑将该 modifier 重命名为 onlyFxBase
。AaveV3Strategy
合约的 totalSupply
函数 返回 AToken
的 balanceOf
,这相当于“存入的总资产 + 产生的收益”,因为该函数稍后用于检查 AssetManagement
合约中策略管理的总资产。 考虑重命名该函数,使其名称与其行为相匹配,并且在将来添加更多策略时不会造成混淆。AssetManagement
合约的 manage
函数 仅作为存款函数。 考虑将其重命名为 deposit
。考虑实施上述重命名建议,以提高代码库的可读性和可维护性。
更新: 已确认,计划解决。 f(x) 协议团队表示:
我们承认这个问题,并计划在未来的更新中解决它。
在 TickLogic
中,_getTick
函数 花费一行代码计算一个新的比率,但它永远不会使用该比率。 考虑删除这个多余的行。
更新: 已确认,计划解决。 f(x) 协议团队表示:
我们承认这个问题,并计划在未来的更新中解决它。
f(x) 协议 v2.0 引入了一种新型稳定币实现——fxUSD——具有改进的稳定动态和先进的Hook机制,以及其去中心化交易平台 xPosition。 该协议依赖于复杂的机制来维持稳定,包括批量再平衡、清算、赎回以及基于“tick”的方法来维持每个基础池的杠杆头寸。
它利用特殊的稳定池、Hook维持者、融资费用和收割机制来奖励系统的维护者并减轻脱钩的风险。 该系统采用基于 tick 的方法独特地管理 xPosition,该方法将头寸分组到大约 0.15% 的价格范围内,以提高更新头寸的效率。 使用多个来源来确定底层抵押代币的价格,包括链下预言机和链上现货价格的组合。
与 f(x) 协议的某些集成(例如 Gauge
、Convex Vault
、SpotPriceOracle
、RevenuePool
、Treasury
、fTokens
和 MarketV2
)不在本次审计范围内。 此外,该协议在很大程度上预计通过外部闪贷提供商(例如Morpho
和Balancer
)使用外围facet合约来为用户头寸提供资金,这也不在本次审计范围内。
在整个审计过程中,主要重点是验证 tick 逻辑以及关于再平衡、清算和赎回的各种极端情况,同时评估系统的整体经济稳定性。 该系统尽管依赖于多个合约,但反映了强大的架构和高弹性。
发现了一个严重性问题、两个高严重性问题和多个中低严重性问题。 严重性问题操纵了 tick 和节点逻辑以阻止 operate 功能。 在高严重性问题中,一个是定价方案容易受到单个可操纵流动性池的影响,这可能导致头寸的早期清算。 另一个完全阻止了协议的闪贷功能。
与 f(x) 协议团队的合作非常顺利且高效。 他们的响应能力和上下文清晰度有助于理解该协议和设计选择背后的更广泛的原因。
- 原文链接: blog.openzeppelin.com/fx...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!