Open Dollar - Saintcode_'s results

A floating $1.00 pegged stablecoin backed by Liquid Staking Tokens with NFT controlled vaults.

General Information

Platform: Code4rena

Start Date: 18/10/2023

Pot Size: $36,500 USDC

Total HM: 17

Participants: 77

Period: 7 days

Judge: MiloTruck

Total Solo HM: 5

Id: 297

League: ETH

Open Dollar

Findings Distribution

Researcher Performance

Rank: 6/77

Findings: 1

Award: $2,000.24

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Saintcode_

Also found by: T1MOH, falconhoof

Labels

bug
3 (High Risk)
high quality report
primary issue
satisfactory
selected for report
edited-by-warden
H-02

Awards

2000.2368 USDC - $2,000.24

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/AccountingEngine.sol#L175-L193

Vulnerability details

The AccountingEngine.sol contract serves as the protocol's central component responsible for initializing both debt and surplus auctions. Debt auctions are consistently initiated with a predefined minimum bid referred to as debtAuctionBidSize. This is done to ensure that the protocol can only auction debt that is not currently undergoing an auction and is not locked within the debt queue, as articulated in the comment found on IAccountingEngine:248: "It can only auction debt that has been popped from the debt queue and is not already being auctioned". This necessity prompts the check on AccountingEngine:181:

if (_params.debtAuctionBidSize > _unqueuedUnauctionedDebt(_debtBalance))

This check verifies that there is a sufficient amount of bad debt available to auction.

The issue stems in the line 183, where _settleDebt is called, this aims to ensure that only bad debt is considered for the auction. However, if the remaining bad debt, after settlement, falls below the specified threshold (debtAuctionBidSize <= unqueuedUnauctionedDebt()), the auction still starts with an incorrect amount of bad debt coverage, diluting the protocol Token when it is not yet needed.

Impact

Non-existent debt gets auctioned when it is not necesary which dilutes the protocol token.

PoC

The attached Proof of Concept (PoC) demonstrates two test cases:

  • In the initial test case, a two-call flow is implemented. The "settleDebt" function must be externally triggered before initiating an auction. This design ensures that a check for insufficient debt occurs prior to creating an auction. Consequently, as after calling settleDebt there is inadequate debt, the operation will revert.
  • In the second case, the auctionDebt function is invoked directly. It is expected to behave in the same manner as in the first test case (because the funciton calls _settleDebt internally). However, in this second test case, the execution follows a different path. Rather than replicating the behavior of the initial test case by reverting not starting the auction, the execution succeeds, resulting in the creation of a debt auction even when there is no existing debt.

The following tests have been created using the protocol's test suite:

First test case (original two call flow):

  function test_missing_insuficient_debt_check_part2() public {
    accountingEngine.modifyParameters("surplusTransferPercentage", abi.encode(1));
    accountingEngine.modifyParameters("extraSurplusReceiver", abi.encode(1));

    safeEngine.createUnbackedDebt(address(0), address(accountingEngine), rad(100 ether));

    _popDebtFromQueue(100 ether);

    accountingEngine.settleDebt(100 ether);

    vm.expectRevert();
    uint id = accountingEngine.auctionDebt();

  }

Second test case (vulnerability):

  function test_missing_insuficient_debt_check_part2() public {
    accountingEngine.modifyParameters("surplusTransferPercentage", abi.encode(1));
    accountingEngine.modifyParameters("extraSurplusReceiver", abi.encode(1));

    safeEngine.createUnbackedDebt(address(0), address(accountingEngine), rad(100 ether));

    _popDebtFromQueue(100 ether);

    
    uint id = accountingEngine.auctionDebt();

  }

Both test cases should revert and not let a user create a debt Auction under insufficient debt circumstances, but as stated on the report the second test case succeeds and creates the Auction.

Tools Used

Manual Review

To mitigate this risk, I suggest introducing the check (_params.debtAuctionBidSize > _unqueuedUnauctionedDebt(_debtBalance)) after calling _settleDebt to ensure there exists enough amount of bad in the contract after the settling.

Assessed type

Other

#0 - c4-pre-sort

2023-10-25T23:07:26Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-10-25T23:07:31Z

raymondfam marked the issue as primary issue

#2 - raymondfam

2023-10-25T23:13:59Z

Seems like _params.debtAuctionBidSize > _unqueuedUnauctionedDebt(_debtBalance) check is well in place prior to settling the debt in the second case, but will let the sponsor look into it.

#3 - c4-pre-sort

2023-10-27T04:14:39Z

raymondfam marked the issue as high quality report

#4 - c4-judge

2023-11-04T02:53:17Z

MiloTruck marked the issue as satisfactory

#5 - c4-judge

2023-11-04T02:53:46Z

MiloTruck marked the issue as selected for report

#6 - MiloTruck

2023-11-04T02:57:46Z

Due to the misplacement of the _params.debtAuctionBidSize > _unqueuedUnauctionedDebt(_debtBalance) check before _settleDebt() is called, the protocol will create debt auctions even when the amount of bad debt is below params.debtAuctionBidSize. This leads to dilution of the protocol token as bidders can buy non-existent debt, thereby destabilizing the value of protocol's token. As such, I agree with high severity.

#7 - pi0neerpat

2023-12-18T17:42:05Z

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter