The Wildcat Protocol - Tricko'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: 79/131

Findings: 1

Award: $16.66

🌟 Selected for report: 0

🚀 Solo Findings: 0

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/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarket.sol#L42-L77 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketConfig.sol#L149-L159

Vulnerability details

Impact

The borrower can frontrun lenders deposits and preemptively decrease the interest rate, potentially deceiving lenders into engaging in unfavourable loans they would typically avoid. Because the lack of on-chain check for minimum interest rate during deposits there is no way for lenders to safeguard against this form of manipulation. Even if they employ off-chain monitoring tools, this might go unnoticed, leading to prolonged exposure of their funds in the market with minimal to no interest earned.

Proof of Concept

Let's examine the typical borrower-lender process. A borrower creates a market, and lenders search for a suitable market meeting their criteria (interest rates, reserve ratios, etc). They decide to deposit their funds into this market by utilizing the deposit or depositUpTo methods. However, it's important to note that these methods only verify the amount parameter and do not inspect any other market parameters. This is because it is assumed that the lender has already conducted this validation prior to calling the method. The issue arises because this verification is not done on-chain during the execution of the deposit call. Consequently, this enables a malicious borrower to frontrun these deposit calls and alter the parameters before they are executed, ultimately leading to lenders entering unfavourable loan agreements.

Consider the following example scenario for a first deposit in a newly create market, which is simpler since we do not need to consider the reserveRatio. The same attack is possible in a market with existing debt, but it necessitates the borrower managing the required reserveRatio due to the reduction in annualInterestBips. Nonetheless, it can still be executed. In this scenario, Bob plays the role of the malicious borrower, and Alice is the lender. A Proof of Concept (POC) for this scenario is provided at the end of this report.

  1. Bob creates a new market with annualInterestBips set to 1000 (other parameters not relevant to this example will be omitted).
  2. Alice submits a transaction calling depositUpTo(1e18) function to deposit 1e18 tokens into the market.
  3. Bob observes Alice's transaction in the mempool and attempts to frontrun her by calling the setAnnualInterestBips(0) function to reduce interest rates to zero (as the market is new and has no debt, there is no penalty to reserve ratios).

Should Bob's frontrunning attempt succeed, Alice gets into a loan with 0% APR. If the lender remains unaware of being deceived, the borrower can utilize an interest-free loan for an extended duration. Even if the lenders employs off-chain monitoring to track AnnualInterestBipsUpdated events and respond to potential interest rate adjustments by the borrower, there is still a possibility of them being deceived, because in this attack the interest rate is modified prior to their deposit, meaning that if the lender only checks for rate changes after their deposit confirmation block, they may remain unaware of the borrower's manipulation.

Tools Used

Manual Review

Consider adding a new argument minAnnualInterestBips to deposit and depositUpTo, so lenders can ensure that their deposit will only be executed if the current annualInterestBips of the market meets or exceeds their specified requirement. Below is an example implementation for reference:

diff --git a/WildcatMarket.sol b/WildcatMarket.mod.sol
index 1723465..1790954 100644
--- a/WildcatMarket.sol
+++ b/WildcatMarket.mod.sol
@@ -38,20 +38,25 @@ contract WildcatMarket is
    *
    *      Reverts if the market is closed or if the scaled token amount
    *      that would be minted for the deposit is zero.
    */
   function depositUpTo(
     uint256 amount
+    uint256 minAnnualInterestBips
   ) public virtual nonReentrant returns (uint256 /* actualAmount */) {
     // Get current state
     MarketState memory state = _getUpdatedState();

     if (state.isClosed) {
       revert DepositToClosedMarket();
     }

+    if (state.annualInterestBips < minAnnualInterestBips) {
+      revert AnnualInterestBipsBelowMin();
+    }
+
     // Reduce amount if it would exceed totalSupply
     amount = MathUtils.min(amount, state.maximumDeposit());

     // Scale the mint amount
     uint104 scaledAmount = state.scaleAmount(amount).toUint104();
     if (scaledAmount == 0) revert NullMintAmount();

POC

Run the following POC in /test folder of the contest repository.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.20;

import './BaseMarketTest.sol';
import 'src/interfaces/IMarketEventsAndErrors.sol';
import 'src/libraries/MathUtils.sol';
import 'src/libraries/SafeCastLib.sol';
import 'src/libraries/MarketState.sol';
import 'solady/utils/SafeTransferLib.sol';

contract WildcatMarketTest is BaseMarketTest {
  using stdStorage for StdStorage;
  using MathUtils for int256;
  using MathUtils for uint256;

  function test_poc() external {
    //Setup
    address alice = makeAddr("alice");
    address bob = parameters.controller;
    uint256 amount = 1e18;
    _authorizeLender(alice);
    asset.mint(alice, amount);
    vm.prank(alice);
    asset.approve(address(market), amount);

    //Initial annualInterestBips set to 1000
    assertEq(market.annualInterestBips(), 1000);

    //Bob frontruns alice's tx and sets annualInterestBips to 0
    vm.prank(bob);
    market.setAnnualInterestBips(0);
    assertEq(market.annualInterestBips(), 0);

    //Alice deposits to the market with annualInterestBips = 0
    vm.expectEmit(address(market));
    emit Deposit(alice, 1e18, 1e18);
    vm.prank(alice);
    market.depositUpTo(amount);
  }
}

Assessed type

Other

#0 - c4-pre-sort

2023-10-28T14:31:57Z

minhquanym marked the issue as duplicate of #443

#1 - c4-judge

2023-11-07T12:33:54Z

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