The Wildcat Protocol - joaovwfreire's results

Banking, but worse - a protocol for fixed-rate, undercollateralised credit facilities.

General Information

Platform: Code4rena

Start Date: 16/10/2023

Pot Size: $60,500 USDC

Total HM: 16

Participants: 131

Period: 10 days

Judge: 0xTheC0der

Total Solo HM: 3

Id: 296

League: ETH

Wildcat Protocol

Findings Distribution

Researcher Performance

Rank: 41/131

Findings: 2

Award: $154.33

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: MiloTruck

Also found by: CaeraDenoir, T1MOH, ast3ros, elprofesor, joaovwfreire, rvierdiiev, t0x1c, trachev

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-497

Awards

137.6749 USDC - $137.67

External Links

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatMarketController.sol#L474-L481

Vulnerability details

Impact

Even though the protocol in being built with the intent of providing undercollateralized loans, there are no constraints that limit a user if it wants to use it as an overcollateralized loan platform. The setAnnualInterestBips function at the WildcatMarketController contract will set the reserve ratio to 90% if the market borrower or the market controller decides to lower the annual interest regardless of the original reserveRatio value. [1].

    if (annualInterestBips < WildcatMarket(market).annualInterestBips()) {
      TemporaryReserveRatio storage tmp = temporaryExcessReserveRatio[market];

      if (tmp.expiry == 0) {
        tmp.reserveRatioBips = uint128(WildcatMarket(market).reserveRatioBips());

        // Require 90% liquidity coverage for the next 2 weeks
        WildcatMarket(market).setReserveRatioBips(9000);
      }

However, the function does not account for the fact that the reserve ratio might be bigger than 90%, meaning a borrower can maliciously lure lenders into an overcollateralized pool with very high annual interest rates, then set it to very small amounts per year and withdraw more collateral than what was originally possible.[2]

Proof of Concept

The following test demonstrates how a Market with 95% reserve ratio can lower it's annual interest rates while also decreasing it's reserve ratio to 90%:

  function test_LowerReserveRatioOnLessAnnualInterest() external asAccount(parameters.controller) {

    // 1. Set the reserve ratio to 95%

    uint16 initialReserveRatioBips = 9500; // 95%

    market.setReserveRatioBips(initialReserveRatioBips);

    assertEq(market.reserveRatioBips(), initialReserveRatioBips, "Reserve ratio should be 95%");

  

    // 2. Lower the annual interest rate

    uint16 loweredAnnualInterestBips = 1; // 0.01% for example

    vm.startPrank(borrower);

    controller.setAnnualInterestBips(address(market), loweredAnnualInterestBips);

    assertEq(market.annualInterestBips(), loweredAnnualInterestBips, "Annual interest rate should be lower");

  

    // 3. Check if the reserve ratio is now 90%

    uint16 expectedReserveRatioBips = 9000; // 90%

    assertEq(market.reserveRatioBips(), expectedReserveRatioBips, "Reserve ratio should be 90% after lowering annual rate");

    console.log("* * Finished * *");

  }

Kindly place this test at WildcatMarketController.t.sol line 295 and run it with:

forge test --match-test "test_LowerReserveRatioOnLessAnnualInterest" -vvv

Tools Used

Manual review

Check if the current reserve ratio is bigger than 90% before setting it to 90%. a. If it is, then increase by a different factor AND check if it doesn't overflow the BIP maximum value; b. If it isn't, then it should be okay to maintain the current logic.

Assessed type

Other

#0 - c4-pre-sort

2023-10-27T16:59:53Z

minhquanym marked the issue as duplicate of #75

#1 - c4-judge

2023-11-07T18:30:21Z

MarioPoneder changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-11-07T18:32:19Z

MarioPoneder marked the issue as satisfactory

Awards

16.6643 USDC - $16.66

Labels

bug
2 (Med Risk)
satisfactory
duplicate-196

External Links

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketConfig.sol#L149-L159 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatMarketController.sol#L468-L488 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatMarketController.sol#L410-L415

Vulnerability details

Impact

The function enforceParameterConstraints at the WildcatMarketController contract ensures that annualInterestBips, delinquencyFeeBips, withdrawalBatchDuration, reserveRatioBips and delinquencyGracePeriod are within the allowed ranges and that namePrefix and symbolPrefix are not null. This function is only called during market deployment, at the deployMarket method. [1].

One part of the issue lies on the WildcatMarketConfig contract. This contract has methods that allows the controller and the borrower of a market to change it's parameters, such as updating annualInterestBips and reserveRatioBips without checking the bounds delimited by enforceParameterConstraints. So controllers are able to setReserveRatioBips and to setAnnualInterestBips arbitrarily.[2]

The other part of the issue lies on the WildcatMarketController contract. The setAnnualInterestBips and the resetReserveRatio functions don't check the bounds originally delimited by enforceParameterConstraints during a market's deployment. [3]

Proof of Concept

enforceParameterConstraints checks if annualInterestBips is within bounds determined by maximum and minimum values:

function enforceParameterConstraints(
    string memory namePrefix,
    string memory symbolPrefix,
    uint16 annualInterestBips,
    uint16 delinquencyFeeBips,
    uint32 withdrawalBatchDuration,
    uint16 reserveRatioBips,
    uint32 delinquencyGracePeriod
  ) internal view virtual {
  ...
assertValueInRange(
      annualInterestBips,
      MinimumAnnualInterestBips,
      MaximumAnnualInterestBips,
      AnnualInterestBipsOutOfBounds.selector
    );
...
}

The following functions do not fully substitute the checks made at enforceParameterConstraints: 2:

function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {
    MarketState memory state = _getUpdatedState();

    if (_annualInterestBips > BIP) {
      revert InterestRateTooHigh();
    }

    state.annualInterestBips = _annualInterestBips;
    _writeState(state);
    emit AnnualInterestBipsUpdated(_annualInterestBips);
  }

3:

function setAnnualInterestBips(
    address market,
    uint16 annualInterestBips
  ) external virtual onlyBorrower onlyControlledMarket(market) {
    // If borrower is reducing the interest rate, increase the reserve
    // ratio for the next two weeks.
    if (annualInterestBips < WildcatMarket(market).annualInterestBips()) {
      TemporaryReserveRatio storage tmp = temporaryExcessReserveRatio[market];

      if (tmp.expiry == 0) {
        tmp.reserveRatioBips = uint128(WildcatMarket(market).reserveRatioBips());

        // Require 90% liquidity coverage for the next 2 weeks
        WildcatMarket(market).setReserveRatioBips(9000);
      }

      tmp.expiry = uint128(block.timestamp + 2 weeks);
    }

    WildcatMarket(market).setAnnualInterestBips(annualInterestBips);
  }

Tools Used

Manual review

Include calls to enforceParameterConstraints internal logic to make sure the proposed annualInterestBips are within the intended bounds. One good option is to call assertValueInRange inside the functions that change market configs.

Assessed type

Context

#0 - c4-pre-sort

2023-10-27T14:21:21Z

minhquanym marked the issue as duplicate of #443

#1 - c4-judge

2023-11-07T12:32:27Z

MarioPoneder marked the issue as satisfactory

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