The Wildcat Protocol - smiling_heretic'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: 76/131

Findings: 2

Award: $16.72

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarket.sol#L142

Vulnerability details

Impact

market.closeMarket should allow the borrower to repay all debts and then stop paying interest on deposits of lenders (i.e. set APR to zero). The purpose of this function is described in more detail here.

Assuming that constraints.minimumAnnualInterestBips is non-zero and correctly enforced, closeMarket might be the only way for the borrower to exit the market and stop paying interest.

However, because of onlyController modifier on market.closeMarket and no way to call this function from the WildcatMarketController contract, borrower is unable to close the market.

This will force the borrower to pay interest forever (or at least as long as lenders want, or resolve the issue with help of lawyers).

Proof of Concept

  1. Examine this line of code to see that the access control modifier on market.closeMarket function is onlyController.
  2. Examine the following lines of code (1, 2, 3, 4) to make sure that this modifier indeed enforces that msg.sender is the WildcatMarketController contract that has deployed the market.
  3. Look through all external calls in WildcatMarketController contract and notice that none of them allow to call market.closeMarket function (or simply search for "closeMarket" in this file).

Tools Used

Manual Analysis

Change onlyController modifier on closeMarket function to onlyBorrower. Alternatively, add a function to WildcatMarketController that allows the borrower to close the market.

Assessed type

Access Control

#0 - c4-pre-sort

2023-10-27T07:06:14Z

minhquanym marked the issue as duplicate of #147

#1 - c4-judge

2023-11-07T13:59:11Z

MarioPoneder marked the issue as partial-50

#2 - c4-judge

2023-11-07T14:16:53Z

MarioPoneder changed the severity to 3 (High Risk)

Awards

16.6643 USDC - $16.66

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
sufficient quality report
edited-by-warden
duplicate-196

External Links

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatMarketController.sol#L468

Vulnerability details

Impact

From the Main Invariants section of the competition description, we learn that "Market parameters should never be able to exit the bounds defined by the controller which deployed it."

Also in Attack Ideas, there's "Consider ways in which market interest rates can be manipulated to produce results that are outside of controller-specific limits".

These constraints are enforced when deploying new markets. However, they're not enforced in the controller.setAnnualInterestBips function.

This allows the borrower to exit bounds defined by the controller for market.annualInterestBips by simply calling the controller.setAnnualInterestBips function with annualInterestBips argument from outside of these bounds.

Proof of Concept

Edit constants in test/shared/TestConstants.sol file:

// uint16 constant MinimumAnnualInterestBips = 0;
// uint16 constant MaximumAnnualInterestBips = 10_000;

uint16 constant MinimumAnnualInterestBips = 1_000;
uint16 constant MaximumAnnualInterestBips = 8_000;

Add the following test to the test file: test/WildcatMarketController.t.sol:

    function test_setAnnualInterestBips_DecreaseBelowConstraints() public {
        vm.prank(borrower);
        controller.setAnnualInterestBips(address(market), DefaultInterest - 10);
        _check(
            DefaultInterest - 10,
            9000,
            DefaultReserveRatio,
            block.timestamp + 2 weeks
        );

        MarketParameterConstraints memory constraints = controller
            .getParameterConstraints();
        assertEq(constraints.minimumAnnualInterestBips, 1000);
        assertEq(market.annualInterestBips(), 990);
        assertLt(
            market.annualInterestBips(),
            constraints.minimumAnnualInterestBips
        );
    }

Run forge test --match-test test_setAnnualInterestBips_DecreaseBelowConstraints

Tools Used

Foundry, manual analysis

In controller.setAnnualInterestBips function, enforce that market.annualInterestBips stays between constraints.minimumAnnualInterestBips and constraints.maximumAnnualInterestBips.

Another thing to consider is: is setting reserveRatioBips to 90% really ok, even if it happens to be outside of controller's constraints (constraints.minimumReserveRatioBips, constraints.maximumReserveRatioBips)?

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-10-27T14:10:42Z

minhquanym marked the issue as primary issue

#1 - c4-pre-sort

2023-10-27T17:14:23Z

minhquanym marked the issue as sufficient quality report

#2 - laurenceday

2023-10-31T23:23:58Z

Valid finding, well done everyone!

#3 - c4-sponsor

2023-10-31T23:33:10Z

laurenceday (sponsor) confirmed

#4 - c4-judge

2023-11-07T12:22:58Z

MarioPoneder marked the issue as satisfactory

#5 - c4-judge

2023-11-07T12:38:58Z

MarioPoneder marked issue #196 as primary and marked this issue as a duplicate of 196

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