xanoutas/solidity-security-findings huggingface dataset:
Disabled  SphereXEngine Proctection.
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/WildcatArchController.sol#L61


# Vulnerability details

## Impact
In WildcatArchController.sol file "WildcatArchController" contract constructor is passing  "SphereXEngine's" engine address = 0 which will disable all SphereXEngine's protection. 
## Proof of Concept
1. In WildcatArchController.sol file "WildcatArchController" contract's 
```
 constructor() SphereXConfig(msg.sender, address(0), address(0)) {
```
2.The address of the SphereXEngine - this is the address to which data is sent, and which classifies the transaction (at various points of execution).
If this value is address(0), the engine will be bypassed, essentially disabling its protection.
From SphereXEngine's github 
## Tools Used
Manual review
## Recommended Mitigation Steps
1. Pass SphereXEngine's engine address in constructor  of WildcatArchController .


## Assessed type

Library
`WildcatArchController::updateSphereXEngineOnRegisteredContracts` will never execute because `sphereXEngine` is set as `address(0)` and never updated.
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/WildcatArchController.sol#L84


# Vulnerability details

## Impact

The `WildcatArchController::updateSphereXEngineOnRegisteredContracts` updates SphereX engine on registered contracts and add them as allowed senders on the engine contract. In this function there is a sole conditional `if` statement that ensures  `sphereXEngine != address(0)`

```javascript
//src/WildcatArchController
//ln#84

  function updateSphereXEngineOnRegisteredContracts(
    address[] calldata controllerFactories,
    address[] calldata controllers,
    address[] calldata markets
  ) external spherexOnlyOperatorOrAdmin {
    address engineAddress = sphereXEngine(); //@audit Will return 0.
    bytes memory changeSphereXEngineCalldata = abi.encodeWithSelector(
      ISphereXProtectedRegisteredBase.changeSphereXEngine.selector,
      engineAddress
    );
    bytes memory addAllowedSenderOnChainCalldata;
    if (engineAddress != address(0)) { //@ audit won't work as engine is a hardcoded `address(0)` with no implemented method to update `engine` address
      addAllowedSenderOnChainCalldata = abi.encodeWithSelector(
        ISphereXEngine.addAllowedSenderOnChain.selector,
        address(0)
      );
    }

//SNIPPED

}

```

But in the `WildcatArchController::constructor` the `SphereXConfig` is initialiazed by providing its `constructor` arguments as so

```
//src/WildcatArchController
//ln#61-63
//@audit                      admin       operator    engine
constructor() SphereXConfig(msg.sender, address(0), address(0)) {  //@audit SphereXConffig `engine` set as 0
    _initializeOwner(msg.sender);
  }

```
Thus this line `address engineAddress = sphereXEngine();` will return `0`.

The impact of this is that `WildcatArchController::updateSphereXEngineOnRegisteredContracts` might never execute.


## Tools Used

Manual review.

## Recommended Mitigation Steps

Perform either of the following

* Implement a method to set `sphereXEngine` after deployment.
* Provide `sphereXEngine` address during deployment.



## Assessed type

Other
Markets do not update delinquency timer correctly if the account goes underwater through interest accrual
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L406-L465
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L541-L542


# Vulnerability details

## Impact
Whenever there is an interaction with a protocol, the protocol updates it's own state since the last interest accrual happened. One of the important parts of the state is calculating if the account of the borrower is underwater. In such cases, the protocol starts counting the delinquency timer, and if the timer passes a threshold the borrower has to pay a penalty.

In case an account has gone underwater through interest accrual from borrower to lenders, the protocol does not count up the delinquency timer until the next state accrual happens. This means that if the distance between two interactions are far away from each other, protocol might miss-calculate the time the borrower has been underwater.

This is important especially because:
(1) There might be many borrowing pools since the liquidity is not centralized in wildcat, therefore, there is no need for constant interaction with the protocol. If the providers have provided their assets, and borrower has borrowed what they need, there will be instances that no one might send a transaction to the protocol for days, weeks, or even months! (Can be seen in [V1 Markets](https://app.wildcat.finance/borrower))

(2) The users depend on Wildcat to calculate the correct amount in such cases, however, `updateScaleFactorAndFees` considers the duration since last time stamp to be either all underwater, or all healthy. The Wildcat protocol is misusing the library and therefore causing incorrect calculations.

(3) Every second in delinquency counts twice if the market isn't closed, once when it is counting up and once when it is counting down.

## Proof of Concept
The PoC below shows that even if a large amount of time passes, the wildcat would not start counting the time until the next interaction with the protocol:
```solidity
  function test_borrowDelinquency() external {
    vm.prank(alice);
    market.depositUpTo(50_000e18);
    assertEq(market.borrowableAssets(), 40_000e18, 'borrowable should be 40k');
    vm.prank(borrower);
    market.borrow(40_000e18); // borrow the max amount, the reserve ratio is 20% here

    vm.warp(block.timestamp + 1000000000); // During this time the market is definitely delinquent

    market.updateState();

    vm.warp(block.timestamp + 1000);

    MarketState memory state = market.currentState();

    assertEq(state.timeDelinquent, 1000);
  }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
In case the last time protocol was contacted with, the borrower wasn't underwater, and now is underwater, the protocol needs to calculate when that happened through interest accrual, and break the update into two sections.











## Assessed type
debit owned by borrower  not cleared after full repayment of loan 
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarket.sol#L226


# Vulnerability details

Current implementation lacks the mechanism to clear borrowers debt either by full repayment of debt owned or  closing of market .

This is problematic because if the same market approves a new depositor , internal calculations that rely of the debt amount will be affected since the market will still have pending debt.

## Proof of Concept
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarket.sol#L226

```solidity
function test_Poc() external {
    // Create a lender address
    address lender = vm.addr(0x123);

   uint owed = market.totalDebts() - market.totalAssets();
   assertEq(owed, 0 );

    // Authorize the lender to interact with the market
    _authorizeLender(lender);

    // Lender deposits 100 units into the market
    _deposit(lender, 100 ether);

    // Verify the lender's balance in the market and the market's asset balance
    assertEq(market.balanceOf(lender), 100 ether);
    assertEq(asset.balanceOf(address(market)), 100 ether);

    // Borrower borrows 70 units from the market
    vm.startPrank(borrower);
    market.borrow( 70 ether);
  uint owedAfterBorrow = market.totalDebts() - market.totalAssets();
   assertEq(owedAfterBorrow, 70 ether);
    vm.stopPrank();

    // Fast forward time to allow for withdrawal
    fastForward(parameters.withdrawalBatchDuration + 1);
    market.updateState();

    // Lender requests a full withdrawal of their funds
    vm.prank(lender);
    market.queueFullWithdrawal();
    uint256 expiry = block.timestamp + parameters.withdrawalBatchDuration;

    // Fast forward time to make the market delinquent
    fastForward(20 days);
    market.updateState();

    // Borrower prepares to repay delinquent debt
    vm.startPrank(borrower);
    asset.mint(borrower, 100 ether);
    asset.approve(address(market), 100 ether);
    
    // Verify the market is in a delinquent state
    MarketState memory state0 = market.previousState();
    assertEq(state0.isDelinquent, true);

    // Borrower repays the delinquent debt
    market.repayDelinquentDebt();

    // Borrower prepares to execute the withdrawal
    asset.mint(borrower, 100 ether);
    asset.approve(address(market), 100 ether);
    
    // Execute the withdrawal for the lender
    market.executeWithdrawal(lender, uint32(expiry));
    vm.stopPrank();

    // Update and check the market state
    market.updateState();
    MarketState memory state = market.previousState();
    assertEq(state.isDelinquent, false);

    // Check the lender's final balance (should be 0 in the market)
    uint256 finalBalance = asset.balanceOf(lender);
    assertEq(market.balanceOf(lender), 0);    

    // Borrower repays the remaining debt
    vm.startPrank(borrower);
    asset.mint(borrower, 100 ether);
    asset.approve(address(market), 100 ether)
WildcatSanctionsSentinel:: createEscrow allows unregistered market to used to create escrow 
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/WildcatSanctionsSentinel.sol#L121


# Vulnerability details

In `WildcatSanctionsSentinel.sol`, the `createEscrow()` function has two issues :

  - There  is currently no   check that `msg.sender` is a registered market 
  - Does not account for markets that are removed using the `removeMarket()` function.

## Impact
In `WildcatArchController.sol` , the `isRegisteredMarket` function is defined and its used to check if  a market is registered in the  wildcard system  and  the `removeMarket( )`  function which is   also defined , is  used to remove a market.

However , there is an oversight which allows unregistered and removed markets  to  to be used by borrowers to create escrow.

This is problematic as  malicious market   will still be able to interact with the market in various ways.

## Proof of Concept
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/WildcatSanctionsSentinel.sol#L121

## Tools Used
Manual Review
## Recommended Mitigation Steps

```solidity
  function createEscrow(
    address borrower,
    address account,
    address asset
  ) public override returns (address escrowContract) {
+   if (!IWildcatArchController(archController).isRegisteredMarket(msg.sender) && !IWildcatArchController(archController).isRemovedMarket(msg.sender)) {
      revert NotRegisteredMarket();
    }
```


## Assessed type

Other
User could withdraw more than supposed to, forcing last user withdraw to fail
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketWithdrawals.sol#L248


# Vulnerability details

## Impact
User could withdraw more than supposed to and force last user's withdraw to fail.

## Proof of Concept
Within Wildcat, withdraw requests are put into batches. Users first queue their withdraws and whenever there's sufficient liquidity, they're filled at the current rate. Usually, withdraw requests are only executable after the expiry passes and then all users within the batch get a cut from the `batch.normalizedAmountPaid` proportional to the scaled amount they've requested a withdraw for. 

```solidity
    uint128 newTotalWithdrawn = uint128(
      MathUtils.mulDiv(batch.normalizedAmountPaid, status.scaledAmount, batch.scaledTotalAmount)
    );
```

This makes sure that the sum of all withdraws doesn't exceed the total `batch.normalizedAmountPaid`.

However, this invariant could be broken, if the market is closed as it allows for a batch's withdraws to be executed, before all requests are added.

Consider the market is made of 3 lenders - Alice, Bob and Laurence.
1. Alice queues a larger withdraw with an expiry time 1 year in the future.
2. Market gets closed.
3. Alice executes her withdraw request at the current rate.
4. Bob makes queues multiple smaller requests. As they're smaller, the normalized amount they represent suffers higher precision loss. Because they're part of the whole batch, they also slightly lower the batch's overall rate.
5. Bob executes his requests.
6. Laurence queues a withdraw for his entire amount. When he attempts to execute it, it will fail. This is because Alice has executed her withdraw at a higher rate than the current one and there's now insufficient `state.normalizedUnclaimedWithdrawals`


Note: marking this as High severity as it both could happen intentionally (attacker purposefully queuing numerous low-value withdraws to cause rounding down) and also with normal behaviour in high-value closed access markets where a user's withdraw could easily be in the hundreds of thousands.

Also breaks core invariant: 
> The sum of all transfer amounts for withdrawal executions in a batch must be less than or equal to batch.normalizedAmountPaid

Adding a PoC to showcase the issue: 

```solidity
  function test_deadrosesxyzissue() external {
    parameters.annualInterestBips = 3650;
    _deposit(alice, 1e18);
    _deposit(bob, 0.5e18);
    address laurence = address(1337);
    _deposit(laurence, 0.5e18);
    fastForward(200 weeks);

    vm.startPrank(borrower);
    asset.approve(address(market), 10e18);
    asset.mint(borrower, 10e18);

    vm.stopPrank();
    vm.prank(alice);
    uint32 expiry = market.queueFullWithdrawal();       // alice queues large withdraw

    vm.prank(borrower);
    market.closeMarket();                               // market is closed

    market.executeWithdrawal(alice, expiry);     // alice withdraws at the current rate

    vm.startPrank(bob);
`state.scaleFactor` is calculated incorrectly when handling the current expired withdrawal batch
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketBase.sol#L409-L431


# Vulnerability details

## Impact
- The amount of assets paid for the current expired withdrawal batch is less than expected
- The amount of reserved assets for unpaid withdrawal batches is insufficient to fully repay all unpaid withdrawal batches
- The interest paid by the borrower is less than it should be
## Proof of Concept
The Wildcat protocol specifies that the [Withdrawal Payment](https://docs.wildcat.finance/technical-overview/security-developer-dives/core-behaviour#withdrawal-payment) should operate as follows:
>The scaled tokens associated with a withdrawal request are subtracted from a lender's balance immediately, but those tokens are not burned until they are honored, meaning they only stop accruing interest once underlying assets have been reserved to pay for them. The batch owns these scaled tokens and accrues their interest until they are burned by a payment, and the interest is distributed pro-rata to the lenders in the batch.
>
>As assets become available, they can be paid to the withdrawal batch. A check for (and payment of) available assets occurs:
>
>- when a lender adds a request to a batch,
>
>- during the state update at the start of a transaction (for the current batch but not for unpaid (already expired) batches),
>
>- upon a call to repayAndProcessUnpaidWithdrawalBatches (for unpaid batches).

Let's take a deep dive into the above situations:
- when a lender adds a request to a batch:
Once either [`WildcatMarketWithdrawals‎#queueWithdrawal‎()`](https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketWithdrawals.sol#L135-L148) or [`WildcatMarketWithdrawals‎#queueFullWithdrawal‎()`](https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketWithdrawals.sol#L153-L178) is called, if there is any available liquidity, it will be used to pay the batch of the current withdrawal request:
```solidity
120:    uint256 availableLiquidity = batch.availableLiquidityForPendingBatch(state, totalAssets());
121:    if (availableLiquidity > 0) {
122:      _applyWithdrawalBatchPayment(batch, state, expiry, availableLiquidity);
123:    }
```
Since **`state` has been updated through `_getUpdatedState()` to reflect the latest value at the time of `block.timestamp`**, the paid amount of the asset will be calculated with the latest value of `state.scaleFactor` in [`_applyWithdrawalBatchPayment()`](https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketBase.sol#L665-L695):
```solidity
  function _applyWithdrawalBatchPayment(
    WithdrawalBatch memory batch,
    MarketState memory state,
    uint32 expiry,
    uint256 availableLiquidity
  ) internal returns (uint104 scaledAmountBurned, uint128 normalizedAmountPaid) {
    ...
680:@>    normalizedAmountPaid = MathUtils.mulDiv(scaledAmountBurned, state.scaleFactor, RAY).toUint128();
    ...
  }
```
- upon a call t
lenders are unable to get full repayment amount 
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketWithdrawals.sol#L251
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketWithdrawals.sol#L247


# Vulnerability details

The `executeWithdrawal` function does not account for delinquent debt payment. It calculates the withdrawal amount based on the batch's `normalizedAmountPaid` and the user's `scaledAmount`, but it doesn't factor in any additional funds that may have been repaid during a delinquency period leading to loss of funds for lenders


## Proof of Concept


https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketWithdrawals.sol#L212

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketWithdrawals.sol#L247

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketWithdrawals.sol#L251

The issue is as follows : 
1. Lender deposits 100 units into market.
2. Borrower borrows 70 units.
3. Lender queues full withdrawals.
4. Borrower repays .
5. Lender only receives 30 units .

Paste this in `WildcatMarketWithdrawalsTest.sol::WithdrawalsTest`
```solidity
function test_Poc() external {
    // Create a lender address
    address lender = vm.addr(0x123);

    // Authorize the lender 
    _authorizeLender(lender);

    // Lender deposits 100 units into the market
    _deposit(lender, 100);

    // Verify the lender's balance 
    assertEq(market.balanceOf(lender), 100);
    assertEq(asset.balanceOf(address(market)), 100);

    // Borrower borrows 70 units from the market
    vm.startPrank(borrower);
    market.borrow(70);
    vm.stopPrank();

    
    fastForward(parameters.withdrawalBatchDuration + 1);
    market.updateState();

    // Lender requests a full withdrawal of their funds
    vm.prank(lender);
    market.queueFullWithdrawal();
    uint256 expiry = block.timestamp + parameters.withdrawalBatchDuration;

   
    fastForward(20 days);
    market.updateState();

    // Borrower repay delinquent debt
    vm.startPrank(borrower);
    asset.mint(borrower, 100);
    asset.approve(address(market), 100);
    
    // Verify borrower is  delinquent 
    MarketState memory state0 = market.previousState();
    assertEq(state0.isDelinquent, true);

    // Borrower repays the delinquent debt
    market.repayDelinquentDebt();

   
    asset.mint(borrower, 100);
    asset.approve(address(market), 100);
    
    // Execute the withdrawal for the lender
    market.executeWithdrawal(lender, uint32(expiry));
    vm.stopPrank();

   
    market.updateState();
    MarketState memory state = market.previousState();
   
// borrower is no longer delinquent
    assertEq(state.isDelinquent, false);

    // Check the lender's final balance (should be 0 in the market)
    uint256 finalBalance
Wrong calculation of delinquent penalty time in `updateTimeDelinquentAndGetPenaltyTime` function
Audit: 2024-08-wildcat-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/libraries/FeeMath.sol#L118


# Vulnerability details

## Impact
Because of the incorrect calculation of delinquent penalty time, a borrower gets charged much lower delinquency fee than intended.

## Proof of Concept
Over time, as market operations occur, borrowers accumulate fees. This process is managed by the `FeeMath.updateScaleFactorAndFees` function. While this function updates the scaleFactor and applies the protocolFee, it also implements the delinquency fee based on the market's delinquency status. The calculation is performed by the `updateDelinquency` function, which then utilizes the `updateTimeDelinquentAndGetPenaltyTime` function to determine the duration for which the `delinquencyFeeBips` should be applied.

When `state.isDelinquent` is false, indicating that the current market is healthy, the return value is calculated as follows:

```solidity
  function updateTimeDelinquentAndGetPenaltyTime(
    MarketState memory state,
    uint256 delinquencyGracePeriod,
    uint256 timeDelta
  ) internal pure returns (uint256 /* timeWithPenalty */) {
    // Seconds in delinquency at last update
    uint256 previousTimeDelinquent = state.timeDelinquent;

    if (state.isDelinquent) {
        ...
    }

    state.timeDelinquent = previousTimeDelinquent.satSub(timeDelta).toUint32();

    // Calculate the number of seconds the old timeDelinquent had remaining
    // outside the grace period, or zero if it was already in the grace period.
>>  uint256 secondsRemainingWithPenalty = previousTimeDelinquent.satSub(delinquencyGracePeriod);

    // Only apply penalties for the remaining time outside of the grace period.
    return MathUtils.min(secondsRemainingWithPenalty, timeDelta);
  }
```

In here, `secondsRemainingWithPenalty` is wrong because delinquent state remains active until `previousTimeDelinquent` drops down to zero.
It's also mentioned in wildcat doc: https://docs.wildcat.finance/using-wildcat/delinquency#how-delinquency-triggers
```
The penalty APR associated with a market activates once the grace tracker exceeds the grace period, and remains active until it drops below the same
```

Here's an example:

IF `previousTimeDelinquent = 3 days` and `delinquencyGracePeriod = 5 days`, then it should be: `secondsRemainingWithPenalty = 3 days`, but above code generates `0`.


#### (IMPORTANT NOTE: The `updateTimeDelinquentAndGetPenaltyTime` function has another critical flaw; it assumes that `state.isDelinquent` is up-to-date and has remained unchanged from the last update to the present. This assumption is incorrect and will be addressed as a separate issue submission.)

## Tools Used
Manual Review

## Recommended Mitigation Steps
Update the calculation as follows:
```diff
-    uint256 secondsRemainingWithPenalty = previousTimeDelinquent.satSub(delinquencyGracePeriod);
+    uint256 secondsRemainingWithPenalty = MathUtils.min(previousTimeDelinquent, delinquencyGracePeriod);