xanoutas/solidity-security-findings huggingface dataset:
Blacklisted Assets Can Be Used as Origination Fee Assets in Hooks Templates
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The HooksFactory contract allows blacklisted assets to be set as origination fee assets when creating or updating hooks templates. This bypasses the intended blacklist restrictions, potentially enabling interaction with prohibited assets during market deployments. While the protocol controls the initial setting of the origination asset in templates, this vulnerability could lead to:

1: Regulatory compliance violations if blacklisted assets are used in market creations.

2: Undermining of the entire blacklist mechanism, reducing its effectiveness as a security measure.

3: Potential financial risks or loss of trust if markets are created using blacklisted assets.

4: Widespread impact, as multiple markets could be affected if created from a template with a blacklisted origination fee asset.

## Proof of Concept
The following test demonstrates that a blacklisted asset can be successfully set as an origination fee asset in a hooks template:

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

import "forge-std/Test.sol";
import "../src/HooksFactory.sol";
import "../src/WildcatArchController.sol";

contract HooksFactoryBlacklistTest is Test {
    WildcatArchController public archController;
    HooksFactory public hooksFactory;
    address public owner;
    address public blacklistedAsset;
    address public hooksTemplate;

    function setUp() public {
        owner = address(this);
        blacklistedAsset = address(0x1);
        hooksTemplate = address(0x2);

        // Deploy ArchController
        archController = new WildcatArchController();

        // Deploy HooksFactory
        hooksFactory = new HooksFactory(
            address(archController),
            address(0), // sanctionsSentinel
            address(0), // marketInitCodeStorage
            0 // marketInitCodeHash
        );

        // Register HooksFactory as a controller factory
        vm.prank(archController.owner());
        archController.registerControllerFactory(address(hooksFactory));

        // Blacklist an asset
        vm.prank(archController.owner());
        archController.addBlacklist(blacklistedAsset);
    }

    function testBlacklistedOriginationFeeAsset() public {
        // Attempt to add a hooks template with a blacklisted origination fee asset
        vm.prank(archController.owner());

        // If this doesn't revert, it means the blacklist check is missing
        hooksFactory.addHooksTemplate(
            hooksTemplate,
            "Test Template",
            address(0x3), // feeRecipient
            blacklistedAsset, // originationFeeAsset
            100, // originationFeeAmount
            100 // protocolFeeBips
        );

        // If we reach here, the blacklisted asset was accepted
        HooksTemplate memory template = hooksFactory.getHooksTemplateDetails(h
FIFO queue is not strict enough 
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Current implementation slightly favours current expiry batch over unpaid old ones.

## Proof of Concept
Within `_getUpdateState`, there's a check if there's an outstanding expiry batch and if there is, an attempt to fulfil a part of is made.

```solidity
    if (state.pendingWithdrawalExpiry != 0) {
      uint32 expiry = state.pendingWithdrawalExpiry;
      WithdrawalBatch memory batch = _withdrawalData.batches[expiry];
      if (batch.scaledAmountBurned < batch.scaledTotalAmount) {
        // Burn as much of the withdrawal batch as possible with available liquidity.
        uint256 availableLiquidity = batch.availableLiquidityForPendingBatch(state, totalAssets());
        if (availableLiquidity > 0) {
          _applyWithdrawalBatchPayment(batch, state, expiry, availableLiquidity);
          _withdrawalData.batches[expiry] = batch;
        }
      }
    }
```

In order to value the FIFO order, it does only check for available liquidity, which basically takes into account previous unpaid batches.

Consider however the following scenario:

1. There's a previous fully unpaid batch for 1% of the funds within the contract. 
2. A new withdraw request is made also for 1% of the funds, creating a new expiry batch.
3. Update state is invoked. Currently there are 2% of the funds within the contract (assume protocol fees have just been withdrawn). As there's enough funds to cover for the previous unpaid batch and the protocol fees, a payment is made to the new batch, basically filling it.
4. The newer batch has been filled before the old one. Now there's needed intervention from the borrower in order to fill the old unpaid batch, despite there being enough funds in the contract for the order. Actions from borrower's side may often take long as they're likely to be DAOs/ multi-sigs. DAO proposals often take days before being able to be executed.
5. Time goes by and 1% in fees are accrued. Wildcat protocol takes their fees (as they're prioritized over unpaid withdraw batches).
6. In the end, the funds are distributed and the newer batch has been filled while the old one is not. 



## Tools Used
Manual review

## Recommended Mitigation Steps
Within `_getUpdateState` check if there's unpaid batches and if there's available liquidity, pay them.


## Assessed type

Context
Inconsistency across multiple repaying functions causing lender to pay extra fees.
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Inconsistency across multiple functions can cause borrower to pay extra fees

## Proof of Concept
Within functions such as `repay` and `repayAndProcessUnpaidWithdrawalBatches`, funds are first pulled from the user in order to use them towards the currently expired, but not yet unpaid batch, and then the updated state is fetched.

```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);
  }
```

However, this is not true for functions such as `closeMarket`, `deposit`, `repayOutstandingDebt` and `repayDelinquentDebt`, where the state is first fetched and only then funds are pulled, forcing borrower into higher fees.

```solidity
  function closeMarket() external onlyBorrower nonReentrant sphereXGuardExternal {
    MarketState memory state = _getUpdatedState();    // fetches updated state

    if (state.isClosed) revert_MarketAlreadyClosed();

    uint256 currentlyHeld = totalAssets();
    uint256 totalDebts = state.totalDebts();
    if (currentlyHeld < totalDebts) {
      // Transfer remaining debts from borrower
      uint256 remainingDebt = totalDebts - currentlyHeld;
      _repay(state, remainingDebt, 0x04);             // pulls user funds
      currentlyHeld += remainingDebt;
```

This inconsistency will cause borrowers to pay extra fees which they otherwise wouldn't.

PoC:
```solidity
  function test_inconsistencyIssue() external {
      parameters.annualInterestBips = 3650;
      _deposit(alice, 1e18);
      uint256 borrowAmount = market.borrowableAssets();
      vm.prank(borrower);
      market.borrow(borrowAmount);
      vm.prank(alice);
      market.queueFullWithdrawal();
      fastForward(52 weeks);

      asset.mint(borrower, 10e18);
      vm.startPrank(borrower);
      asset.approve(address(market), 10e18);
      uint256 initBalance = asset.balanceOf(borrower); 

      asset.transfer(address(market), 10e18);
      market.closeMarket();
      uint256 finalBalance = asset.balanceOf(borrower);
      uint256 paid = initBalance - finalBalance;
      console.log(paid);

  } 

    function test_inconsistencyIssue2() external {
      parameters.annualInterestBips = 3650;
      _deposit(alice, 1e18);
      uint256 borrowAmount = market.borrowableAssets();
      vm.prank(borrower);
      market.borrow(borrowAmount);
      vm.prank(alice);
      market.queueFullWithdrawal();
      fastForward(52 weeks);

      asset.mint(borrower, 10e18);
      vm.startPrank(borrower);
      asset.approve(address(marke
`FixedTermLoanHook` looks at `block.timestamp` instead of `expiry`
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/FixedTermLoanHooks.sol#L848


# Vulnerability details

## Impact
`FixedTermLoanHook` looks at `block.timestamp` instead of `expiry`

## Proof of Concept
The idea of `FixedTermLoanHook` is to only allow for withdrawals after a certain term end time. However, the problem is that the current implementation does not look at the expiry, but instead at the `block.timestamp`

```solidity
  function onQueueWithdrawal(
    address lender,
    uint32 /* expiry */,
    uint /* scaledAmount */,
    MarketState calldata /* state */,
    bytes calldata hooksData
  ) external override {
    HookedMarket memory market = _hookedMarkets[msg.sender];
    if (!market.isHooked) revert NotHookedMarket();
    if (market.fixedTermEndTime > block.timestamp) {
      revert WithdrawBeforeTermEnd();
    }
```

This creates inconsistencies such as forcing users not only to wait until term's end, but also having to wait an extra `withdrawalBatchDuration` before they're able to withdraw their funds.


## Tools Used
Manual review

## Recommended Mitigation Steps
Check the `expiry` instead of `block.timestamp`


## Assessed type

Context
Role providers can bypass intended restrictions and lower expiry set by other providers
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/FixedTermLoanHooks.sol#L413


# Vulnerability details

## Proof of Concept
If we look at the code comments, we'll see that role providers can update a user's credential only if at least one of the 3 is true:
- the previous credential's provider is no longer supported, OR
- the caller is the previous role provider, OR
- the new expiry is later than the current expiry

```solidity
  /**
   * @dev Grants a role to an account by updating the account's status.
   *      Can only be called by an approved role provider.
   *
   *      If the account has an existing credential, it can only be updated if:
   *      - the previous credential's provider is no longer supported, OR
   *      - the caller is the previous role provider, OR
   *      - the new expiry is later than the current expiry
   */
  function grantRole(address account, uint32 roleGrantedTimestamp) external {
    RoleProvider callingProvider = _roleProviders[msg.sender];

    if (callingProvider.isNull()) revert ProviderNotFound();

    _grantRole(callingProvider, account, roleGrantedTimestamp);
  }
```

This means that a role providers should not be able to reduce a credential set by another role provider.

However, this could easily be bypassed by simply splitting the call into 2 separate ones:

1. First one to set the expiry slightly later than the currently set one. This would set the role provider to the new one.
2. Second call to reduce the expiry as much as they'd like. Since they're the previous provider they can do that.

## Recommended Mitigation Steps
Fix is non-trivial.


## Assessed type

Context
The penalty APR to be returned can be manipulated
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The penalty APR that is meant to be incurred can potentially be manipulated.
In certain scenarios, a borrower or another party could deliberately manipulate the timing of these updates to minimize the penalty.

## Description
The `delinquency` penalty retuned can be manipulated potentially by the borrower.

The function [`updateScaleFactorAndFees`](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L416) is called within the [`getUpdatedState`](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L406) function to accrue interest and calculate penalties, including the **penalty APR**, which is applied when a market enters delinquency.

```solidity
function updateScaleFactorAndFees(
    MarketState memory state,
    uint256 delinquencyFeeBips,
    uint256 delinquencyGracePeriod,
    uint256 timestamp
  )
    internal
    pure
    returns (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee)
  {
    baseInterestRay = state.calculateBaseInterest(timestamp);

    if (state.protocolFeeBips > 0) {
      protocolFee = state.applyProtocolFee(baseInterestRay);
    }

    if (delinquencyFeeBips > 0) {
      delinquencyFeeRay = state.updateDelinquency(
        timestamp,
        delinquencyFeeBips,
        delinquencyGracePeriod
      );
    }

    // Calculate new scaleFactor
    uint256 prevScaleFactor = state.scaleFactor;
    uint256 scaleFactorDelta = prevScaleFactor.rayMul(baseInterestRay + delinquencyFeeRay);

    state.scaleFactor = (prevScaleFactor + scaleFactorDelta).toUint112();
    state.lastInterestAccruedTimestamp = uint32(timestamp);
  }
```

The problem, however, lies within the [`updateDelinquency`](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/libraries/FeeMath.sol#L52) function, where a borrower or another party can manipulate the delinquency penalty. This is because when the [`updateTimeDelinquentAndGetPenaltyTime`](https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/libraries/FeeMath.sol#L88) function returns the **`timeWithPenalty`**, it uses a `min` value calculation, meaning the penalty time will be based on the smallest value between `secondsRemainingWithPenalty` and `timeDelta`.

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/libraries/FeeMath.sol#L120C4-L121C66
```solidity
 // Only apply penalties for the remaining time outside of the grace period.
    return MathUtils.min(secondsRemainingWithPenalty, timeDelta);
```


The issue is that `timeDelta` is the time difference between the last update timestamp and the current timestamp, and this can be manipula
A `borrower` can remove itself from role providers of Hooks
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/AccessControlHooks.sol#L249-L259
https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/FixedTermLoanHooks.sol#L286-L296


# Vulnerability details

## Impact
Once the borrower is removed from the role providers, it cannot be added again, resulting in the borrower having no way to directly grant roles.

## Proof of Concept
When a hook instance of `AccessControlHooks‎` or `FixedTermLoanHooks‎` is deployed, the deployer(borrower) itself will be added as the default role provider:
```solidity
  constructor(address _deployer, bytes memory /* args */) IHooks() {
    borrower = _deployer;
    // Allow deployer to grant roles with no expiry
    _roleProviders[_deployer] = encodeRoleProvider(
      type(uint32).max,
      _deployer,
      NotPullProviderIndex
    );
    ...
  }
```
The borrower of the hook instance can add new role provider or remove existing role provider as their wish. 
However, when the borrower itself is removed from the role providers, it cannot be added back, as the borrower is unlikely to be a valid `IRoleProvider` address, and the  call `IRoleProvider#isPullProvider‎()` will always revert:
```solidity
  function addRoleProvider(address providerAddress, uint32 timeToLive) external onlyBorrower {
    RoleProvider provider = _roleProviders[providerAddress];
    if (provider.isNull()) {
@>    bool isPullProvider = IRoleProvider(providerAddress).isPullProvider();
      // Role providers that are not pull providers have `pullProviderIndex` set to
      // `NotPullProviderIndex` (max uint24) to indicate they do not refresh credentials.
      provider = encodeRoleProvider(
        timeToLive,
        providerAddress,
        isPullProvider ? uint24(_pullProviders.length) : NotPullProviderIndex
      );
      if (isPullProvider) {
        _pullProviders.push(provider);
      }
      emit RoleProviderAdded(providerAddress, timeToLive, provider.pullProviderIndex());
    } else {
      // If provider already exists, the only value that can be updated is the TTL
      provider = provider.setTimeToLive(timeToLive);
      uint24 pullProviderIndex = provider.pullProviderIndex();
      if (pullProviderIndex != NotPullProviderIndex) {
        _pullProviders[pullProviderIndex] = provider;
      } else {}
      emit RoleProviderUpdated(providerAddress, timeToLive, pullProviderIndex);
    }
    // Update the provider in storage
    _roleProviders[providerAddress] = provider;
  }
```

Copy below codes to [AccessControlHooks.t.sol](https://github.com/code-423n4/2024-08-wildcat/blob/main/test/access/AccessControlHooks.t.sol) and run forge test --match-test test_addBorrowerAsRoleProvider:
```solidity
  function test_addBorrowerAsRoleProvider() external {
    //@audit-info borrower(address(this)) is valid role provider
    assertNotEq(RoleProvider.unwrap(hooks.getRoleProvider(address(this))), 0);
    //@audit-info borrower remove itself from role providers
    h
No lender is able to exit even after the market is closed
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/FixedTermLoanHooks.sol#L848-L868
https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/FixedTermLoanHooks.sol#L943-L946


# Vulnerability details

## Impact
Lenders might not be able to exit even after the market is closed
## Proof of Concept
When a borrower creates a market hooked by a [fixed-term hook](https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/FixedTermLoanHooks.sol), all lenders are prohibited from withdrawing their assets from the market before the fixed-term time has elapsed.
The borrower can close the market at any time. However, `fixedTermEndTime` of the market is not updated, preventing lenders from withdrawing their assets if `fixedTermEndTime` has not yet elapsed.

Copy below codes to [WildcatMarket.t.sol](https://github.com/code-423n4/2024-08-wildcat/blob/main/test/market/WildcatMarket.t.sol) and run forge test --match-test test_closeMarket_BeforeFixedTermExpired:
```solidity
  function test_closeMarket_BeforeFixedTermExpired() external {
    //@audit-info deploy a FixedTermLoanHooks template
    address fixedTermHookTemplate = LibStoredInitCode.deployInitCode(type(FixedTermLoanHooks).creationCode);
    hooksFactory.addHooksTemplate(
      fixedTermHookTemplate,
      'FixedTermLoanHooks',
      address(0),
      address(0),
      0,
      0
    );
    
    vm.startPrank(borrower);
    //@audit-info borrower deploy a FixedTermLoanHooks hookInstance
    address hooksInstance = hooksFactory.deployHooksInstance(fixedTermHookTemplate, '');
    DeployMarketInputs memory parameters = DeployMarketInputs({
      asset: address(asset),
      namePrefix: 'name',
      symbolPrefix: 'symbol',
      maxTotalSupply: type(uint128).max,
      annualInterestBips: 1000,
      delinquencyFeeBips: 1000,
      withdrawalBatchDuration: 10000,
      reserveRatioBips: 10000,
      delinquencyGracePeriod: 10000,
      hooks: EmptyHooksConfig.setHooksAddress(address(hooksInstance))
    });
    //@audit-info borrower deploy a market hooked by a FixedTermLoanHooks hookInstance
    address market = hooksFactory.deployMarket(
      parameters,
      abi.encode(block.timestamp + (365 days)),
      bytes32(uint(1)),
      address(0),
      0
    );
    vm.stopPrank();
    //@audit-info lenders can only withdraw their asset one year later
    assertEq(FixedTermLoanHooks(hooksInstance).getHookedMarket(market).fixedTermEndTime, block.timestamp + (365 days));
    //@audit-info alice deposit 50K asset into market
    vm.startPrank(alice);
    asset.approve(market, type(uint).max);
    WildcatMarket(market).depositUpTo(50_000e18);
    vm.stopPrank();
    //@audit-info borrower close market in advance
    vm.prank(borrower);
    WildcatMarket(market).closeMarket();
    //@audit-info the market is closed
    assertTrue(WildcatMarket(market).isClosed());
    //@audit-info however, alice can not withdraw her asset due to the unexpired fixed term.
    vm.ex
`WildcatMarketConfig#nukeFromOrbit()` reverts even if the lender has been marked as sanctioned
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The borrower of the market might not be able to force the sanctioned lender into a withdrawal request.

## Proof of Concept
When a borrower creates a market hooked by a [fixed-term hook](https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/FixedTermLoanHooks.sol), all lenders are prohibited from withdrawing their assets from the market before the fixed-term time has elapsed.
If a lender is marked as sanctioned, anyone can call [`WildcatMarketConfig#nukeFromOrbit()`]() to force it into a withdrawal request. However, the call might revert if `fixedTermEndTime` has not yet elapsed.

Copy below codes to [WildcatMarket.t.sol](https://github.com/code-423n4/2024-08-wildcat/blob/main/test/market/WildcatMarket.t.sol) and run forge test --match-test test_nukeFromOrbit_BeforeFixedTermExpired:
```solidity
  function test_nukeFromOrbit_BeforeFixedTermExpired() external {
    //@audit-info deploy a FixedTermLoanHooks template
    address fixedTermHookTemplate = LibStoredInitCode.deployInitCode(type(FixedTermLoanHooks).creationCode);
    hooksFactory.addHooksTemplate(
      fixedTermHookTemplate,
      'FixedTermLoanHooks',
      address(0),
      address(0),
      0,
      0
    );
    
    vm.startPrank(borrower);
    //@audit-info borrower deploy a FixedTermLoanHooks hookInstance
    address hooksInstance = hooksFactory.deployHooksInstance(fixedTermHookTemplate, '');
    DeployMarketInputs memory parameters = DeployMarketInputs({
      asset: address(asset),
      namePrefix: 'name',
      symbolPrefix: 'symbol',
      maxTotalSupply: type(uint128).max,
      annualInterestBips: 1000,
      delinquencyFeeBips: 1000,
      withdrawalBatchDuration: 10000,
      reserveRatioBips: 10000,
      delinquencyGracePeriod: 10000,
      hooks: EmptyHooksConfig.setHooksAddress(address(hooksInstance))
    });
    //@audit-info borrower deploy a market hooked by a FixedTermLoanHooks hookInstance
    address market = hooksFactory.deployMarket(
      parameters,
      abi.encode(block.timestamp + (365 days)),
      bytes32(uint(1)),
      address(0),
      0
    );
    vm.stopPrank();
    //@audit-info lenders can only withdraw their asset one year later
    assertEq(FixedTermLoanHooks(hooksInstance).getHookedMarket(market).fixedTermEndTime, block.timestamp + (365 days));
    //@audit-info alice deposit 50K asset into market
    vm.startPrank(alice);
    asset.approve(market, type(uint).max);
    WildcatMarket(market).depositUpTo(50_000e18);
    vm.stopPrank();
    //@audit-info alice is marked as sanctioned
    MockSanctionsSentinel(WildcatMarket(market).sentinel()).sanction(alice);
    assertTrue(MockSanctionsSentinel(WildcatMarket(market).sentinel()).isSanctioned(borrower, alice));
    //@audit-info however, borrower can not force alice into a withdrawal request due to the unexpired fixed term.
    vm.expec