xanoutas/solidity-security-findings huggingface dataset:
Role providers cannot be EOAs as stated in the documentation.
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/access/AccessControlHooks.sol#L220
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/access/FixedTermLoanHooks.sol#L254


# Vulnerability details

## Impact

The [Documentation](https://docs.wildcat.finance/technical-overview/security-developer-dives/hooks/access-control-hooks#role-providers) suggests that a role provider can be a "push" provider (one that "pushes" credentials into the hooks contract by calling `grantRole`) and a "pull" provider (one that the hook calls via `getCredential` or `validateCredential`).

The documentation also states that:
> Role providers do not have to implement any of these functions - a role provider can be an EOA.

But in fact, only the initial deployer can be an EOA provider, since it is coded in the constructor. Any other EOA provider that the borrower tries to add via `addRoleProvider` will fail because it does not implement the interface.

## Proof of Concept

PoC will revert because EOA does not implement interface obviously:

```powershell
  [118781] AuditMarket::test_PoC_EOA_provider()
    ├─ [0] VM::startPrank(BORROWER1: [0xB193AC639A896a0B7a0B334a97f0095cD87427f2])
    │   └─ ← [Return]
    ├─ [29883] AccessControlHooks::addRoleProvider(RoleProvider: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 2592000 [2.592e6])
    │   ├─ [2275] RoleProvider::isPullProvider() [staticcall]
    │   │   └─ ← [Return] false
    │   ├─ emit RoleProviderAdded(providerAddress: RoleProvider: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], timeToLive: 2592000 [2.592e6], pullProviderIndex: 16777215 [1.677e7])
    │   └─ ← [Stop]
    ├─ [74243] AccessControlHooks::addRoleProvider(RoleProvider: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 2592000 [2.592e6])
    │   ├─ [2275] RoleProvider::isPullProvider() [staticcall]
    │   │   └─ ← [Return] true
    │   ├─ emit RoleProviderAdded(providerAddress: RoleProvider: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], timeToLive: 2592000 [2.592e6], pullProviderIndex: 0)
    │   └─ ← [Stop]
    ├─ [5547] AccessControlHooks::addRoleProvider(EOA_PROVIDER1: [0x6aAfF89c996cAa2BD28408f735Ba7A441276B03F], 2592000 [2.592e6])
    │   ├─ [0] EOA_PROVIDER1::isPullProvider() [staticcall]
    │   │   └─ ← [Stop]
    │   └─ ← [Revert] EvmError: Revert
    └─ ← [Revert] EvmError: Revert
```

PoC:
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "forge-std/console2.sol";

import {WildcatArchController} from "../src/WildcatArchController.sol";
import {HooksFactory} from "../src/HooksFactory.sol";
import {LibStoredInitCode} from "src/libraries/LibStoredInitCode.sol";
import {WildcatMarket} from "src/market/WildcatMarket.sol";
import {AccessControlHooks} from "../src/access/AccessControlHooks.sol";
import {DeployMarketInputs} from "../src/interfaces/WildcatStructsAndEnums.sol";
import {Hooks
Reserved assets for withdrawals can be lower than they should
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Deposited assets that should be reserved for withdrawals are not properly allocated, causing the withdrawal assets to be lower than they should be.

## Description

When a market is in a `delinquent` state (indicating no obligatory assets available), and there are pending withdrawal requests, the assets available at the time are reserved for withdrawal.

However, due to the way the code is currently structured, the assets reserved for withdrawal may end up being lower than they should , causing the withdrawal to remain in the [withdrawal queue](https://docs.wildcat.finance/using-wildcat/terminology#withdrawal-queue) instead of moving to the [unclaimed withdrawals pool](https://docs.wildcat.finance/using-wildcat/terminology#unclaimed-withdrawals-pool).

Lenders use the [deposit](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarket.sol#L55) function to deposit assets into the protocol. These newly deposited assets can also be used to cure `delinquency`, ensuring there are enough assets in the system. As a result, the borrower may not need to return assets immediately, and these deposits can also be used to fulfil [withdrawal requests](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L430) by other lenders. However, due to the current implementation  of the code, the assets reserved for withdrawal may be less than they should be.

That happens because when the lender deposits, the function to procced the `withdrawal requests` is being called at the beginning of the function rather than when the assets are available in the market, this will make the assets be lower than they should when the next flow returns, potentially removing the delinquency fees and other accrued interest which have already been paid for that same block and timestamp at the beginning of the call.

In the current implementation, the [`getUpdatedState`](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L406) function, which processes withdrawal requests, is called at the beginning of the [`deposit`](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarket.sol#L55) function but not after the assets have been fully deposited and available for withdrawals.

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarket.sol#L59
```solidity
58:	    // Get current state
59:>    MarketState memory state = _getUpdatedState();
	
61:     if (state.isClosed) revert_
Accounts blocked from deposits can ultimately mimic the exact state as a depositor
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept
Invariant states the following:

*Accounts which are blocked from deposits, or which do not have a credential on markets which require it for deposits, should never be able to mint market tokens.*

However, a user who is blocked from depositing still remains able to receive transfers of tokens that another account has deposited, which objectively reaches an identical state within the protocol.

Add the following test to `WildcatMarketToken.t.sol`:

```
  function testTransferToBlockedAccount() external {
    parameters.hooksConfig = parameters.hooksConfig.setFlag(Bit_Enabled_Transfer);
    BaseMarketTest.setUpContracts(true);
    token = IERC20(address(market));
    MarketState memory state;
    _mint(bob, 1 ether);
    _mint(alice, 1 ether);
    //bob's credentials removed & blocked from depositing
    _blockLender(bob);
    vm.prank(alice);
    //alice transfers tokens to bob and state remains identical as if bob had deposited
    token.transfer(bob, 1 ether);
    vm.prank(bob);
    market.queueFullWithdrawal();
  }
```

## Recommended Mitigation Steps
To fully enforce the stated invariant, a blocked depositor should not be able to receive market tokens.


## Assessed type

Invalid Validation
Excessive Interest and Fees Due to Multiple Repayments
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability Detail

The Wildcat protocol design stipulates that borrowers should pay lenders an agreed-upon Annual Percentage Rate (APR). For instance, if a lender deposits 100,000 worth of an asset with a 10% APR and a protocol fee of 10% of the APR, the lender should receive 111,000 worth of the asset after one year (365 days).

This design assumes that the borrower repays the loan in a single transaction at the end of the loan period. However, if the borrower decides to repay the loan in multiple installments, they will pay more interest and fees than expected. This occurs because each time the borrower calls the `repay function`, the accrued interest and protocol fees are calculated. Therefore, subsequent repayments include interest on the previously accrued interest, resulting in the borrower paying a higher total than initially agreed upon.

The more the number of repayments, the more the borrower will pay.

```solidity
  function repay(uint256 amount) external nonReentrant sphereXGuardExternal {
    if (amount == 0) revert_NullRepayAmount();

    asset.safeTransferFrom(msg.sender, address(this), amount);
    emit_DebtRepaid(msg.sender, amount);

@>   MarketState memory state = _getUpdatedState();
    if (state.isClosed) revert_RepayToClosedMarket();

    // Execute repay hook if enabled
    hooks.onRepay(amount, state, _runtimeConstant(0x24));

    _writeState(state);
  }
```

```solidity
function _getUpdatedState() internal returns (MarketState memory state) {
    state = _state;
    // Handle expired withdrawal batch
    if (state.hasPendingExpiredBatch()) {
      console.log("1");
      uint256 expiry = state.pendingWithdrawalExpiry;
      // Only accrue interest if time has passed since last update.
      // This will only be false if withdrawalBatchDuration is 0.
      uint32 lastInterestAccruedTimestamp = state.lastInterestAccruedTimestamp;
      if (expiry != lastInterestAccruedTimestamp) {
        (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee) = state
          .updateScaleFactorAndFees(
            delinquencyFeeBips,
            delinquencyGracePeriod,
            expiry
          );
        emit_InterestAndFeesAccrued(
          lastInterestAccruedTimestamp,
          expiry,
          state.scaleFactor,
          baseInterestRay,
          delinquencyFeeRay,
          protocolFee
        );
      }
      _processExpiredWithdrawalBatch(state);
    }
    uint32 lastInterestAccruedTimestamp = state.lastInterestAccruedTimestamp;
    // Apply interest and fees accrued since last update (expiry or previous tx)
    if (block.timestamp != lastInterestAccruedTimestamp) {
@>      (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee) @>  state
@>       .updateScaleFactorAndFees(
@>         delinquencyFeeBips,
@>          delinquencyGracePeriod,
@>
Inconsistent Access Control Allows Known Lenders to Bypass Deposit Restrictions via Transfers
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/access/AccessControlHooks.sol#L769-L804


# Vulnerability details

## Proof of Concept
There is a inconsistency in how the isBlockedFromDeposits status is checked between deposit and transfer functions in the Wildcat Market contracts. This inconsistency allows known lenders who are blocked from deposits to bypass this restriction by receiving transfers, effectively allowing them to increase their position when they shouldn't be able to.


In the deposit function (via onDeposit in AccessControlHooks.sol), the blocked status is always checked:

```solidity
function onDeposit(address lender, uint scaledAmount, MarketState calldata state, bytes calldata hooksData) external override {
  // ...
  LenderStatus memory status = _lenderStatus[lender];
  if (status.isBlockedFromDeposits) revert NotApprovedLender();
  // ...
}
```

However, in the transfer function (via onTransfer in AccessControlHooks.sol), the blocked status is only checked for non-known lenders:

```solidity
function onTransfer(address /* caller */, address /* from */, address to, uint /* scaledAmount */, MarketState calldata /* state */, bytes calldata extraData) external override {
  // ...
  if (!isKnownLenderOnMarket[to][msg.sender]) {
    LenderStatus memory toStatus = _lenderStatus[to];
    if (toStatus.isBlockedFromDeposits) revert NotApprovedLender();
    // ...
  }
  // ...
}
```

Exploit scenario:

1. Alice is a known lender and has been blocked from deposits (isBlockedFromDeposits is true).
2. Alice can't deposit directly due to the blocked status check.
3. However, Bob can transfer tokens to Alice.
4. The transfer function doesn't check Alice's blocked status because she's a known lender.
5. Alice effectively increases her position in the market, bypassing the intended restriction.

This inconsistency undermines the purpose of the access control system and could potentially be exploited to circumvent market restrictions.


## Recommended Mitigation Steps
Apply consistent access control in both deposit and transfer functions.



## Assessed type

Access Control
Borrower Can Permanently Invalidate Push Provider Credentials Through Block/Unblock Actions
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/access/AccessControlHooks.sol#L447-L449


# Vulnerability details

The current implementation allows the borrower to permanently invalidate credentials provided by push providers through a sequence of blocking and unblocking actions. This vulnerability undermines the access control system and gives the borrower unintended power over lender credentials.

## Proof of Concept
A push provider grants a credential to a lender:

```solidity
function _grantRole(
    RoleProvider callingProvider,
    address account,
    uint32 roleGrantedTimestamp
) internal {
    // ...
    _setCredentialAndEmitAccessGranted(status, callingProvider, account, roleGrantedTimestamp);
}
```

The borrower can then invalidate this credential:

```solidity
function blockFromDeposits(address account) external onlyBorrower {
    LenderStatus memory status = _lenderStatus[account];
    if (status.hasCredential()) {
        status.unsetCredential();
        emit AccountAccessRevoked(account);
    }
    status.isBlockedFromDeposits = true;
    _lenderStatus[account] = status;
    emit AccountBlockedFromDeposits(account);
}
```

The borrower can then unblock the lender, but the credential remains invalidated:

```solidity
function unblockFromDeposits(address account) external onlyBorrower {
    LenderStatus memory status = _lenderStatus[account];
    status.isBlockedFromDeposits = false;
    _lenderStatus[account] = status;
    emit AccountUnblockedFromDeposits(account);
}
```

However, push provider credentials can't be refreshed.

Exploit scenario:

1. Alice receives a credential from a push provider.
2. The borrower calls blockFromDeposits(alice), clearing Alice's credential.
3. The borrower calls unblockFromDeposits(alice), unblocking Alice but not restoring her credential.
4. Alice's push provider credential is now permanently invalidated.
5. Alice can't participate in the market until the push provider manually grants a new credential again.

This allows the borrower to arbitrarily and permanently revoke access granted by push providers, which is likely not the intended behavior of the system.

## Recommended Mitigation Steps
Separate blocking from credential management.


## Assessed type

Access Control
`onExecuteWithdrawal` hook uses an incorrect function signature definition
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/types/HooksConfig.sol#L374-L426
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketWithdrawals.sol#L255
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/access/AccessControlHooks.sol#L133
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/access/FixedTermLoanHooks.sol#L873


# Vulnerability details

### Impact

`onExecuteWithdrawal` function is incorrectly encoded in HooksConfig.sol and as a result, transactions that use the hook will not be successfully executed. 

### Proof of Concept

From the IHooks interface, `onExecuteWithdrawal` is defined as follows using a uint128 `normalizedAmountWithdrawn` parameter.
```solidity
  function onExecuteWithdrawal(
    address lender,
    uint128 normalizedAmountWithdrawn,
    MarketState calldata intermediateState,
    bytes calldata extraData
  ) external virtual;
```

However, when encoding in HooksConfig.sol, we see that a uint256 `scaledAmount` parameter is passed in instead. 
```solidity
  function onExecuteWithdrawal(
    HooksConfig self,
    address lender,
    uint256 scaledAmount,
    MarketState memory state,
    uint256 baseCalldataSize
  ) internal {
```
Also, while encoding the parameters, the `onExecuteWithdrawal` selector is gotten using the interface, but the uint256 scaled amount offset is written into the call data instead.  
```solidity
    address target = self.hooksAddress();
    uint32 onExecuteWithdrawalSelector = uint32(IHooks.onExecuteWithdrawal.selector); //@note
    if (self.useOnExecuteWithdrawal()) {
      assembly {
        let extraCalldataBytes := sub(calldatasize(), baseCalldataSize)
        let cdPointer := mload(0x40)
        let headPointer := add(cdPointer, 0x20)
        // Write selector for `onExecuteWithdrawal`
        mstore(cdPointer, onExecuteWithdrawalSelector)
        // Write `lender` to hook calldata
        mstore(headPointer, lender)
        // Write `scaledAmount` to hook calldata
        mstore(add(headPointer, ExecuteWithdrawalHook_ScaledAmount_Offset), scaledAmount) //@note
        // Copy market state to hook calldata
        mcopy(add(headPointer, ExecuteWithdrawalHook_State_Offset), state, M
```
 
This causes that the defined function selector be different from its expected arguments and as such the hook fails.

### Tools Used
 Manual code review
 
### Recommended Mitigation Steps

```diff
  uint256 internal constant ExecuteWithdrawalHook_Base_Size = 0x0244;
- uint256 internal constant ExecuteWithdrawalHook_ScaledAmount_Offset = 0x20;
+ uint128 internal constant ExecuteWithdrawalHook_NormalizedAmount_Offset = 0x20;

//...
  function onExecuteWithdrawal(
    HooksConfig self,
    address lender,
-   uint256 scaledAmount,
+   uint256 normalizedAmount,

//...
        // Write `scaledAmoun
Sanctioned Addresses Can Bypass Checks Due to Incorrect Assembly Code
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L254-L273
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L769-L792


# Vulnerability details


The `WildcatMarketBase` contract includes critical functions that rely on low-level assembly code to enforce compliance checks, particularly to prevent sanctioned addresses from interacting with the contract. Two primary functions involved are `_isSanctioned()` and `_createEscrowForUnderlyingAsset()`. These functions interact with the `sentinel` contract to verify if an address is sanctioned and to manage escrow creation for underlying assets.

### `_isSanctioned()` Function

The `_isSanctioned()` function is intended to determine if a given `account` is sanctioned by calling the `sentinel.isSanctioned(borrower, account)` function. It uses inline assembly to perform a `staticcall` and then checks the `returndatasize()` to ensure that the call returned the expected 32 bytes.

The relevant portion of the code is:

```
function _isSanctioned(address account) internal view returns (bool result) {
    address _borrower = borrower;
    address _sentinel = address(sentinel);
    assembly {
        let freeMemoryPointer := mload(0x40)
        mstore(0, 0x06e74444) // Function selector for isSanctioned(address,address)
        mstore(0x20, _borrower)
        mstore(0x40, account)
        // Incorrect order of evaluation
        if iszero(
            and(
                eq(returndatasize(), 0x20), // Evaluated before staticcall
                staticcall(gas(), _sentinel, 0x1c, 0x44, 0, 0x20)
            )
        ) {
            returndatacopy(0, 0, returndatasize())
            revert(0, returndatasize())
        }
        result := mload(0)
        mstore(0x40, freeMemoryPointer)
    }
}
```

### `_createEscrowForUnderlyingAsset()` Function

Similarly, the `_createEscrowForUnderlyingAsset()` function is designed to create an escrow for the underlying asset by calling `sentinel.createEscrow(borrower, accountAddress, tokenAddress)`. It also uses inline assembly to perform a `call` and checks the `returndatasize()`.

The code snippet is:

```
function _createEscrowForUnderlyingAsset(address accountAddress) internal returns (address escrow) {
    address tokenAddress = address(asset);
    address borrowerAddress = borrower;
    address sentinelAddress = address(sentinel);

    assembly {
        let freeMemoryPointer := mload(0x40)
        mstore(0, 0xa1054f6b) // Function selector for createEscrow(address,address,address)
        mstore(0x20, borrowerAddress)
        mstore(0x40, accountAddress)
        mstore(0x60, tokenAddress)
        // Incorrect order of evaluation
        if iszero(
            and(
                eq(returndatasize(), 0x20), // Evaluated before call
                call(gas(), sentinelAddress, 0, 0x1c, 0x64, 0, 0x20)
Increasing APR when reserve ratio is zero and borrower uses all the funds moves the account to delinquency
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability Details
The Wildcat lending protocol allows borrowers to increase the Annual Percentage Rate (APR) of a market using the setAnnualInterestAndReserveRatioBips function to encourage additional deposits. However, this behavior is flawed when the borrower's reserve ratio is zero and the market is fully utilized. When the borrower increases the APR without having any reserves, the market immediately moves into a delinquent state. This situation arises because the protocol expects a reserve ratio to be in place to cover interest payments.

As a result, if lenders do not deposit additional funds within the grace period, the borrower will be required to pay the penalty fee. This design flaw can lead to unintended financial consequences for the borrower, who may be penalized even when they increase the APR with the intention of attracting more liquidity.

## Impact
Borrower Penalty: Borrowers who increase the APR with zero reserves face penalties when the market enters a delinquent state, even though they may intend to attract additional deposits.

## Proof of Concept
```solidity
function test_increaseInterest() public {

    asset.mint(josh, 200000 ether);
    asset.mint(borrower,  12000 ether);

    vm.startPrank(josh);
    market.deposit(100000 ether);
    vm.stopPrank();

    vm.startPrank(borrower);
    market.borrow(100000 ether);
    vm.stopPrank();

    fastForward(365 days);

    vm.startPrank(borrower);
    market.setAnnualInterestAndReserveRatioBips(1100, 0);
    vm.stopPrank(); 

    assert(market.currentState().isDelinquent == true);

  }
```



## Recommended Mitigation Steps



## Assessed type

Other