The Wildcat Protocol - radev_sw'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: 40/131

Findings: 4

Award: $163.80

QA:
grade-a
Analysis:
grade-b

🌟 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-L161

Vulnerability details

Impact

The market cannot actually be closed.

Explanation

In the event that a borrower has finished utilizing the funds for the purpose that the market was set up to facilitate, or if lenders choose not to withdraw their assets and the borrower is paying too much interest on assets that have been redeposited in the market, the borrower should have the ability to close the market.

Proof of Concept

In the protocol, the actual closing of the market is executed by the closeMarket() function. This function sets the market APR to 0% and marks the market as closed. It then transfers any remaining debts from the borrower if the market is not fully collateralized. Otherwise, it transfers any assets in excess of debts to the borrower.

  function closeMarket() external onlyController nonReentrant {
    MarketState memory state = _getUpdatedState();
    state.annualInterestBips = 0;
    state.isClosed = true;
    state.reserveRatioBips = 0;
    if (_withdrawalData.unpaidBatches.length() > 0) {
      revert CloseMarketWithUnpaidWithdrawals();
    }
    uint256 currentlyHeld = totalAssets();
    uint256 totalDebts = state.totalDebts();
    if (currentlyHeld < totalDebts) {
      // Transfer remaining debts from borrower
      asset.safeTransferFrom(borrower, address(this), totalDebts - currentlyHeld);
    } else if (currentlyHeld > totalDebts) {
      // Transfer excess assets to borrower
      asset.safeTransfer(borrower, currentlyHeld - totalDebts);
    }
    _writeState(state);
    emit MarketClosed(block.timestamp);
  }

The closeMarket() function includes the onlyController modifier, which ensures that only the corresponding constructor for the market can call it.

However, the WildcatMarketController.sol contract does not implement any function that calls WildcatMarket.sol#closeMarket() function for particular market.

As a result, when the borrower decides or needs to close the market, hi/she actually cannot do so.

Tools Used

  • Manual Inspection

Implement a function in WildcatMarketController.sol contract that calls the closeMarket() function for a specific market. This function should only be callable by the borrower.

Contract: WildcatMarketController.sol

function closeMarket(address market) external onlyBorrower {
    WildcatMarket(market).closeMarket();
}

These changes will allow borrowers to close specific markets as needed.

Assessed type

Context

#0 - c4-pre-sort

2023-10-27T07:13:39Z

minhquanym marked the issue as duplicate of #147

#1 - c4-judge

2023-11-07T13:53:20Z

MarioPoneder changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-11-07T14:02:20Z

MarioPoneder marked the issue as partial-50

#3 - c4-judge

2023-11-07T14:16:53Z

MarioPoneder changed the severity to 3 (High Risk)

Awards

13.1205 USDC - $13.12

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-266

External Links

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketToken.sol#L36-L39 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketToken.sol#L64-L82 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketConfig.sol#L74-L81 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L163-L187 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatMarketController.sol#L182-L190 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketConfig.sol#L112-L126

Vulnerability details

Impact

  • Lenders still will be able to withdraw their normalized and scaled amount, even if they are flagged as sanctioned and blocked.
  • Unauthorized lenders in WildcatMarketController will interact with Market. Also accruing of interests for them.

Explanation

The protocol does monitor for addresses that are flagged by the Chainalysis oracle as being placed on a sanctions list and bar them from interacting with markets if this happens: simply because strict liability on interfacing with these addresses means that they'd otherwise poison everyone else. That's the extent of the guardrails.

In the event that a lender address is sanctioned (flagged as sanctioned by the Chainanalysis oracle) two things can happen:

  1. Everyone can call the WildcatMarketConfig.sol#nukeFromOrbit() fumction with accountAddress for sanctioned user.
  function nukeFromOrbit(address accountAddress) external nonReentrant {
    if (!IWildcatSanctionsSentinel(sentinel).isSanctioned(borrower, accountAddress)) {
      revert BadLaunchCode();
    }
    MarketState memory state = _getUpdatedState();
    _blockAccount(state, accountAddress);
    _writeState(state);
  }
  1. When the Lender (sanctioned user in this case) invokes WildcatMarketWithdrawals.sol#executeWithdrawal() while flagged as sanctioned by the Chainalysis oracle.
  function executeWithdrawal(
    address accountAddress,
    uint32 expiry
  ) external nonReentrant returns (uint256) {
    if (expiry > block.timestamp) {
      revert WithdrawalBatchNotExpired();
    }
    MarketState memory state = _getUpdatedState();

    WithdrawalBatch memory batch = _withdrawalData.batches[expiry];
    AccountWithdrawalStatus storage status = _withdrawalData.accountStatuses[expiry][
      accountAddress
    ];

    // ...code...
    
    if (IWildcatSanctionsSentinel(sentinel).isSanctioned(borrower, accountAddress)) {
      _blockAccount(state, accountAddress);
      address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow(
        accountAddress,
        borrower,
        address(asset)
      );
      asset.safeTransfer(escrow, normalizedAmountWithdrawn);
      emit SanctionedAccountWithdrawalSentToEscrow(
        accountAddress,
        escrow,
        expiry,
        normalizedAmountWithdrawn
      );
    } else {
      asset.safeTransfer(accountAddress, normalizedAmountWithdrawn);
    }

    // ...code...

In both cases, an escrow contract is created (between the borrower of a market and the lender), and the scaledBalance (i.e. the market tokens) of the lender get transferred to the escrow rather than the lender. Also the lender account address is flagged as Blocked.

As a result, the sanctioned lender cannot withdraw their normalized and scaled amount from the market.

However, this can be bypassed, allowing the lender to still be able to withdraw their normalized and scaled amount, even if they are flagged as sanctioned and blocked.

Proof of Concept

The Protocol implements WildcatMarketToken.sol contract which basically is ERC20 functionality for Wildcat markets (Rebasing Market Tokens).

  function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {
    _transfer(msg.sender, to, amount);
    return true;
  }

  function _transfer(address from, address to, uint256 amount) internal virtual {
    MarketState memory state = _getUpdatedState();
    uint104 scaledAmount = state.scaleAmount(amount).toUint104();

    if (scaledAmount == 0) {
      revert NullTransferAmount();
    }

    Account memory fromAccount = _getAccount(from);
    fromAccount.scaledBalance -= scaledAmount;
    _accounts[from] = fromAccount;

    Account memory toAccount = _getAccount(to);
    toAccount.scaledBalance += scaledAmount;
    _accounts[to] = toAccount;

    _writeState(state);
    emit Transfer(from, to, amount);
  }

The root cause of this problem is the fact that WildcatMarketToken (Market Token) can be transferred between anyone. So, once a lender notices that he is flagged as sanctioned by the Chainanalysis oracle, he can immediately (or until someone decide to call nukeFromOrbit() function for lender address) transfer his 'market tokens' to his second account, which is not necessarily to be registered lender.

Let's consider the following scenario:

Scenario

Let's say that the lender for our example has a scaledBalance of 100 and a normalizedAmount of 110.

  1. The lender/depositor address is sanctioned (flagged as sanctioned by the Chainanalysis oracle).
  2. The lender notices this and calls WildcatMarketToken.sol#transfer() with the following arguments: to = second lender address and amount = lender's normalized amount (110). This can happen immediately or until someone decides to call the nukeFromOrbit() function for the lender's address, so the lender has enough time to do this.
  3. After some time, the borrower, for example, notices this and calls nukeFromOrbit() for the lender's address. However, the function doesn't actually do anything since the lender's scaledAmount will be zero (check _blockAccount() function).
  4. Now the second lender's account will be as follows:
    • AuthRole approval = AuthRole.Null;
    • uint104 scaledBalance = 100;
  5. However, for the lender to execute queueWithdrawal, he should have at least approval = AuthRole.WithdrawOnly. This is not a problem since WildcatMarketController.sol#updateLenderAuthorization() function can be called by anyone. So, the lender does it and calls WildcatMarketController.sol#updateLenderAuthorization() function with the following parameters: address lender = target address (his address, as the initial lender has already taken actions from his second address), address[] memory markets = target market. Inside the WildcatMarketController.sol#updateLenderAuthorization() function, the WildcatMarketConfig.sol#updateAccountAuthorization() function is called with the param _isAuthorized = false, resulting in account.approval = AuthRole.WithdrawOnly.
  6. Now, the lender can withdraw his normalized and scaled amount, bypassing the sanction from the Chainanalysis oracle.

Note: This attack scenario can happen without step 3."

Tools Used

  • Manual Inspection

Assessed type

Context

#0 - c4-pre-sort

2023-10-27T09:30:09Z

minhquanym marked the issue as duplicate of #54

#1 - c4-judge

2023-11-07T14:43:22Z

MarioPoneder marked the issue as satisfactory

Awards

98.3346 USDC - $98.33

Labels

bug
grade-a
high quality report
QA (Quality Assurance)
sponsor confirmed
Q-07

External Links

QA Report - Wildcat Protocol Audit Contest | 16 Oct 2023 - 26 Oct 2023


Executive summary

Overview

Project NameWildcat Protocol
Repositoryhttps://github.com/code-423n4/2023-10-wildcat
Twitter (X)Link
DocumentationLink
MethodsManual Review
Total SLOC2,332 over 22 contracts


Findings Summary

IDTitleSeverity
L-01Risk of Silent Overflow in FeeMath#updateScaleFactorAndFees()Low
L-02During the deployment of the Market, the Protocol doesn't ensure that the Withdrawal Cycle Length and Grace Period Length are in hours, as mentioned in the documentationLow
L-03The direct transfer of the underlying asset from anyone other than the borrower to the Market should be disabledLow
L-05If the borrower or some of the lenders in the Wildcat Protocol become blacklisted in the underlying token, it can have significant implications for the protocol's functionalityLow
NC-01Anyone can call updateAccountAuthorization() functionNon Critical
NC-02Wrongly emitted eventsNon Critical
NC-03Incorrect Year Duration CalculationNon Critical
NC-04During deploying of market in WildcatMarketController contract the amount of transferred originationFeeAsset maybe should be approved by borrowerNon Critical
NC-05The direct transfer of the underlying asset from anyone other than the borrower to the Market should be disabledNon Critical
S-01Create repay() function in WildMarket contract to handle repaying a market action by borrowerSuggestion/Optimization
S-02Add on a Blocked role shift if the lender attempts to deposit while sanctionedSuggestion/Optimization

<a name="L-01"></a>[L-01] Risk of Silent Overflow in FeeMath#updateScaleFactorAndFees()

Explanation

In FeeMath#updateScaleFactorAndFees() function, there is a potential risk of silent overflow in the following line of code:

state.scaleFactor = (prevScaleFactor + scaleFactorDelta).toUint112();

This line of code attempts to update the state.scaleFactor by adding prevScaleFactor to scaleFactorDelta and then converting the result to a uint112. The risk here is that if the sum of prevScaleFactor and scaleFactorDelta exceeds the maximum value that can be represented by a uint112, it will silently overflow, leading to incorrect results and potentially unexpected behavior.

Mitigation Steps

To mitigate this risk, it's essential to ensure that the sum of prevScaleFactor and scaleFactorDelta does not exceed the maximum value representable by a uint112. You can add a check to verify this condition before performing the addition and conversion.

// Ensure that the sum of prevScaleFactor and scaleFactorDelta doesn't exceed uint112 max
require(
    prevScaleFactor <= type(uint112).max - scaleFactorDelta,
    "Potential overflow detected"
);

// Update state.scaleFactor
state.scaleFactor = (prevScaleFactor + scaleFactorDelta).toUint112();

<a name="L-02"></a>[L-02] During the deployment of the Market, the Protocol doesn't ensure that the Withdrawal Cycle Length and `Grace Period Length`` are in hours, as mentioned in the documentation

Impact

Wildcat Protocol does not ensure that the Withdrawal Cycle Length and Grace Period Length are in hours, as mentioned in the documentation. This discrepancy between the code and documentation can lead to confusion and incorrect configuration of these parameters, potentially affecting the protocol's behavior.

Explanation

The Wildcat Protocol allows for the deployment of markets with various parameters, including 'Withdrawal Cycle Length' and 'Grace Period Length,' which are expected to be specified in hours, as indicated in the documentation.

However, in the code responsible for deploying a market, there is no explicit enforcement of this requirement. This means that users can input values for Withdrawal Cycle Length and Grace Period Length that are not in hours, potentially leading to incorrect configuration.

This issue can result in markets with unexpected behavior, as the protocol may interpret these values differently than intended if they are not in the expected time unit (hours).

Proof of Concept

  function deployMarket(
    address asset,
    string memory namePrefix,
    string memory symbolPrefix,
    uint128 maxTotalSupply,
    uint16 annualInterestBips,
    uint16 delinquencyFeeBips,
    uint32 withdrawalBatchDuration,
    uint16 reserveRatioBips,
    uint32 delinquencyGracePeriod
  ) external returns (address market) {
    if (msg.sender == borrower) {
      if (!archController.isRegisteredBorrower(msg.sender)) {
        revert NotRegisteredBorrower();
      }
    } else if (msg.sender != address(controllerFactory)) {
      revert CallerNotBorrowerOrControllerFactory();
    }

    enforceParameterConstraints(
      namePrefix,
      symbolPrefix,
      annualInterestBips,
      delinquencyFeeBips,
      withdrawalBatchDuration,
      reserveRatioBips,
      delinquencyGracePeriod
    );

    TmpMarketParameterStorage memory parameters = TmpMarketParameterStorage({
      asset: asset,
      namePrefix: namePrefix,
      symbolPrefix: symbolPrefix,
      feeRecipient: address(0),
      maxTotalSupply: maxTotalSupply,
      protocolFeeBips: 0,
      annualInterestBips: annualInterestBips,
      delinquencyFeeBips: delinquencyFeeBips,
      withdrawalBatchDuration: withdrawalBatchDuration,
      reserveRatioBips: reserveRatioBips,
      delinquencyGracePeriod: delinquencyGracePeriod
    });

    address originationFeeAsset;
    uint80 originationFeeAmount;
    (
      parameters.feeRecipient,
      originationFeeAsset,
      originationFeeAmount,
      parameters.protocolFeeBips
    ) = controllerFactory.getProtocolFeeConfiguration();

    _tmpMarketParameters = parameters;

    if (originationFeeAsset != address(0)) {
      originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);
    }

    bytes32 salt = _deriveSalt(asset, namePrefix, symbolPrefix);
    market = LibStoredInitCode.calculateCreate2Address(ownCreate2Prefix, salt, marketInitCodeHash);
    if (market.codehash != bytes32(0)) {
      revert MarketAlreadyDeployed();
    }
    LibStoredInitCode.create2WithStoredInitCode(marketInitCodeStorage, salt);

    archController.registerMarket(market);
    _controlledMarkets.add(market);

    _resetTmpMarketParameters();
  }

Tools Used

  • Manual Inspection

To address this issue and ensure consistency between the code and documentation, the following mitigation steps are recommended:

  1. Code Validation: Implement validation checks in the code responsible for deploying markets to ensure that Withdrawal Cycle Length and Grace Period Length are specified in hours. If a user attempts to deploy a market with values in a different time unit, the code should revert.

<a name="L-03"></a>[L-03] If the borrower or some of the lenders in the Wildcat Protocol become blacklisted in the underlying token, it can have significant implications for the protocol's functionality

Impact

If the borrower or some of the lenders in the Wildcat Protocol become blacklisted in the underlying token, it can have significant implications for the protocol's functionality. The likelihood of this issue actually can be calssified as medium, because of the protocol logic and flow, one market can exists very long time.

Explanation

The Wildcat Protocol Markets includes two assets. Underlying Tokens (Normalized Amount) and Market Tokens (Scaled Amount).

When Market is deploying the borrower specify the Underlying Asset, which basically is the asset that the borrower wish to borrow, such as DAI or WETH. After the market is deployed, borrower can borrow specific amount of underlying asset and lenders can deplosit amount of underlying asset and after some amount of time to withdraw it.

If the borrower or some of the lenders in the Wildcat Protocol become blacklisted in the underlying token, it can have significant implications for the protocol's functionality.

Proof of Concept

From the Contest Audit Page: We anticipate that any ERC-20 can be used as an underlying asset for a market.

  function borrow(uint256 amount) external onlyBorrower nonReentrant {
    MarketState memory state = _getUpdatedState();
    if (state.isClosed) {
      revert BorrowFromClosedMarket();
    }
    uint256 borrowable = state.borrowableAssets(totalAssets());
    if (amount > borrowable) {
      revert BorrowAmountTooHigh();
    }
    _writeState(state);
    asset.safeTransfer(msg.sender, amount);
    emit Borrow(amount);
  }
  function depositUpTo(
    uint256 amount
  ) public virtual nonReentrant returns (uint256 /* actualAmount */) {
    // Get current state
    MarketState memory state = _getUpdatedState();

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

    // 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();

    // Transfer deposit from caller
    asset.safeTransferFrom(msg.sender, address(this), amount);

    // Cache account data and revert if not authorized to deposit.
    Account memory account = _getAccountWithRole(msg.sender, AuthRole.DepositAndWithdraw);
    account.scaledBalance += scaledAmount;
    _accounts[msg.sender] = account;

    emit Transfer(address(0), msg.sender, amount);
    emit Deposit(msg.sender, amount, scaledAmount);

    // Increase supply
    state.scaledTotalSupply += scaledAmount;

    // Update stored state
    _writeState(state);

    return amount;
  }

  /**
   * @dev Deposit exactly `amount` underlying assets and mint market tokens
   *      for `msg.sender`.
   *
   *     Reverts if the deposit amount would cause the market to exceed the
   *     configured `maxTotalSupply`.
   */
  function deposit(uint256 amount) external virtual {
    uint256 actualAmount = depositUpTo(amount);
    if (amount != actualAmount) {
      revert MaxSupplyExceeded();
    }
  }
  function queueWithdrawal(uint256 amount) external nonReentrant {
    MarketState memory state = _getUpdatedState();

    // Cache account data and revert if not authorized to withdraw.
    Account memory account = _getAccountWithRole(msg.sender, AuthRole.WithdrawOnly);

    uint104 scaledAmount = state.scaleAmount(amount).toUint104();
    if (scaledAmount == 0) {
      revert NullBurnAmount();
    }

    // Reduce caller's balance and emit transfer event.
    account.scaledBalance -= scaledAmount;
    _accounts[msg.sender] = account;
    emit Transfer(msg.sender, address(this), amount);

    // If there is no pending withdrawal batch, create a new one.
    if (state.pendingWithdrawalExpiry == 0) {
      state.pendingWithdrawalExpiry = uint32(block.timestamp + withdrawalBatchDuration);
      emit WithdrawalBatchCreated(state.pendingWithdrawalExpiry);
    }
    // Cache batch expiry on the stack for gas savings.
    uint32 expiry = state.pendingWithdrawalExpiry;

    WithdrawalBatch memory batch = _withdrawalData.batches[expiry];

    // Add scaled withdrawal amount to account withdrawal status, withdrawal batch and market state.
    _withdrawalData.accountStatuses[expiry][msg.sender].scaledAmount += scaledAmount;
    batch.scaledTotalAmount += scaledAmount;
    state.scaledPendingWithdrawals += scaledAmount;

    emit WithdrawalQueued(expiry, msg.sender, scaledAmount);

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

    // Update stored batch data
    _withdrawalData.batches[expiry] = batch;

    // Update stored state
    _writeState(state);
  }

Tools Used

Manual Inspection

There isn't a really elegant way to fix this.


<a name="NC-01"></a>[NC-01] Anyone can call updateAccountAuthorization() function

Explanation

Restrict the function to be called only from market borrowers as it done in deauthorizeLenders() and authorizeLenders() functions.

GitHub Links: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatMarketController.sol#L153-L190


<a name="NC-02"></a>[NC-02] Wrongly emitted events

Example Instance:

depositUpTo() function:

    emit Transfer(address(0), msg.sender, amount);

However the event params are actually this:

// Interface: IMarketEventsAndErrors

    event Transfer(address from, address to, uint256 value);  

Mitigation

Emit the events correctly. For our Example instance is should be:

    emit Transfer(msg.sender, address(this), amount);

<a name="NC-03"></a>[NC-03] Incorrect Year Duration Calculation

Description

In the codebase of MathUtils, there's a constant definition that assumes a year always has 365 days. However, this assumption is not accurate because leap years have 366 days. This discrepancy in year duration can lead to incorrect calculations, especially when precise time intervals are essential.

GitHub Links: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/libraries/MathUtils.sol#L14
// MathUtils.sol

14: uint256 constant SECONDS_IN_365_DAYS = 365 days;
Impact

During leap years, calculations that rely on this constant may produce incorrect results, affecting the accuracy of time-based operations.

Recommendation

To ensure accurate time-related calculations, consider using a more precise definition for the number of seconds in a year that accounts for leap years. One way to do this is by relying on built-in Solidity time units, such as 1 years, which automatically adjust for leap years.


<a name="NC-04"></a>[NC-04] During deploying of market in WildcatMarketController contract the amount of transferred originationFeeAsset maybe should be approved by borrower

Impact

During the deployment of a market in the WildcatMarketController contract, the code transfers the originationFeeAsset from the borrower to the feeRecipient without verifying whether the borrower has approved this transfer. This may result in unexpected behavior if the originationFeeAsset requires approval for token transfers. (In General will revert)

Explanation

The deployMarket function in the WildcatMarketController contract deploys a new market with various parameters, including the transfer of an originationFeeAsset from the borrower to the feeRecipient. However, the code does not include a check to ensure that the borrower has approved this transfer, which is required for certain token standards, such as ERC-20 and ERC-777.

If the originationFeeAsset being used is an ERC-20 or ERC-777 token that requires approval for transfers, the deployment may fail or result in undesired behavior if the borrower has not granted the necessary approval beforehand. This can potentially disrupt the deployment process and require manual intervention to rectify.

Proof of Concept

  function deployMarket(
    address asset,
    string memory namePrefix,
    string memory symbolPrefix,
    uint128 maxTotalSupply,
    uint16 annualInterestBips,
    uint16 delinquencyFeeBips,
    uint32 withdrawalBatchDuration,
    uint16 reserveRatioBips,
    uint32 delinquencyGracePeriod
  ) external returns (address market) {
    if (msg.sender == borrower) {
      if (!archController.isRegisteredBorrower(msg.sender)) {
        revert NotRegisteredBorrower();
      }
    } else if (msg.sender != address(controllerFactory)) {
      revert CallerNotBorrowerOrControllerFactory();
    }

    // ...code...

    address originationFeeAsset;
    uint80 originationFeeAmount;
    (
      parameters.feeRecipient,
      originationFeeAsset,
      originationFeeAmount,
      parameters.protocolFeeBips
    ) = controllerFactory.getProtocolFeeConfiguration();

    _tmpMarketParameters = parameters;

    if (originationFeeAsset != address(0)) {
      originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount); //@note
    }

    // ...code...

    archController.registerMarket(market);
    _controlledMarkets.add(market);

    _resetTmpMarketParameters();
  }

Tools Used

  • Manual Inspection

<a name="NC-05"></a>[NC-05] The direct transfer of the underlying asset from anyone other than the borrower to the Market should be disabled

Explanation

The Wildcat Protocol allows for the direct transfer of the underlying asset to the Market by anyone other than the borrower. This will lead to breaking of the intended flow and logic of the protocol and to unexpected behaviour.


<a name="S-01"></a>[S-01] Create repay() function in WildMarket contract to handle repaying a market action by borrower

Summary:

Currently there is no specific path that the lender need to take to repay to the Market. Any transfer from borrower to Market is considered a repaying/payment by the borrower. After the borrower transferred amount to Market, he should call updateState() function, therefore some unexpected behavior can occur (for example, lender to be still in grace period even repaying).

So it will be better to implement function like repay() which do both stuff - transferring and calling updateState() function


<a name="S-02"></a>[S-02] Add on a Blocked role shift if the lender attempts to deposit while sanctioned


#0 - c4-pre-sort

2023-10-29T15:02:32Z

minhquanym marked the issue as high quality report

#1 - laurenceday

2023-11-06T09:53:53Z

Event emitted in depositUpTo() is correct as is, market tokens are created and transferred to depositor, not the market itself

Beyond this, not going to quibble with the contents, there's enough here to confirm

#2 - c4-sponsor

2023-11-06T09:53:57Z

laurenceday (sponsor) confirmed

#3 - c4-judge

2023-11-09T15:18:19Z

MarioPoneder marked the issue as grade-a

#4 - radeveth

2023-11-14T08:22:33Z

Hey, @MarioPoneder!

I believe that my [NC-01] is a duplicate of #236.

Additionaly, I clearly note that anyone can call the updateAccountAuthorization() function to get the WithdrawOnly role and how users can maliciously use it (in step 5 of my Proof of Concept) in issue #532.

#5 - MarioPoneder

2023-11-15T00:00:28Z

Thank you for your comment!

Your issue #532 is a duplicate of #266. Meanwhile #236 was also duplicated to #266 with partial credit. Therefore, one of your findings is already related to the mentioned issue.

Furthermore, NC-01 was identified as non-critical and is only discussing access control.
Therefore, it is not qualified for duplication to #266 with partial credit.

Findings Information

Labels

analysis-advanced
grade-b
edited-by-warden
A-10

Awards

52.2873 USDC - $52.29

External Links

Wildcat Audit Contest Analysis Report | 16 Oct 2023 - 26 Oct 2023


#Topic
1Notes during Wildcat Auditing (Mechanism review)
2Architecture overview (Codebase Explanation, Examples Executions & Scenarios)
3Code Audit Approach
4Codebase Complexity and Codebase Quality
5Centralization Risks
6Systemic Risks

1. Notes during Wildcat Auditing (Mechanism review)

The Wildcat Finance DeFi Protocol is a unique Ethereum-based protocol designed for on-chain fixed-rate private credit. It introduces novel features and approaches that may not be suitable for retail users but cater to sophisticated entities looking to bring credit agreements on-chain while maintaining control over various aspects.

Overview:

  • Primary Offering: Wildcat's primary offering is credit escrow mechanisms known as "markets", where almost every parameter can be customized at launch, and base APRs and capacities can be adjusted later by borrowers.

  • Borrower Requirements: Borrowers are required to create a market for a specific asset, offer a particular interest rate, and specify lender addresses explicitly before obtaining credit. This means there should be an existing relationship between counterparties.

  • Collateralization: Instead of requiring borrowers to provide collateral, a percentage of the supply (reserve ratio) must remain within the market, accruing interest. This serves as a buffer for lenders against withdrawal requests and penalizes borrowers if this ratio is breached. To clarify Typically, in lending or borrowing scenarios, borrowers are required to provide collateral as security for the loan they receive. This collateral serves as a guarantee that the lender can claim if the borrower fails to repay the loan. In the case of the Wildcat Finance DeFi Protocol, they have a different approach. Instead of borrowers providing specific assets as collateral, there is a concept called the "reserve ratio". The reserve ratio is a percentage of the total supply of assets associated with a particular market within the protocol. This reserve, which is a portion of the total assets available in the market, is not utilized by the borrower. However, it still generates interest over time. In other words, the assets within this reserve ratio earn interest while they are held within the market. So, Wildcat Protocol doesn't require borrowers to provide traditional collateral like specific assets. Instead, they require a certain percentage of the total supply of assets associated with a market (known as the "reserve ratio") to be kept within that market. This reserve ratio generates interest over time, and it serves as a buffer or security for the lenders in case the borrower doesn't meet their obligations.

  • No Underwriters or Insurance: The protocol does not have underwriters or insurance funds. It is entirely hands-off, and the protocol cannot freeze borrower collateral or pause activity.

  • No Proxies: The protocol does not utilize proxies, so if users lose keys or face other issues, the protocol cannot assist.

  • Sanctions List: The protocol monitors for addresses flagged by the Chainalysis oracle and bars them from interacting with markets to prevent poisoning other users.

  • Not for Retail Users: Wildcat is designed for sophisticated entities, and users must sign a master loan agreement with jurisdiction in the UK courts if utilizing the protocol's UI.

Technical Briefing:

  • Archcontroller: The Wildcat protocol revolves around a single contract known as the "archcontroller." This contract controls which factories can be used, which markets are deployed, and which addresses can deploy contracts.

  • Market Deployment: Borrowers deploy markets through "controllers" that define logic, permissible lenders, and market parameter limits. Multiple markets can be deployed through a single controller, enabling the use of the same list of lenders for multiple markets.

  • Market Tokens: Lenders authorized on a controller can deposit assets into markets and receive "market tokens." These tokens are rebasing and can be redeemed at parity for the underlying asset as interest accumulates. Parity: In this context, "parity" means that the value or price of the rebasing token remains roughly equivalent to the value of the underlying asset it represents. For example, if the rebasing token is designed to represent 1 gram of gold, then "parity" would mean that the token's price should stay close to the market price of 1 gram of gold. For example, if a rebasing token is initially designed to represent the value of 1 ounce of gold, and interest accumulates over time, the token's supply might be adjusted periodically through rebasing to ensure that one token still represents a value close to 1 ounce of gold, even as market conditions change.

  • Interest Rates: Borrowers pay interest rates consisting of a base APR (accruing to lenders), a protocol APR (accruing to the Wildcat protocol), and a penalty APR (accruing to lenders). The penalty APR activates when a market breaches its reserve ratio for a duration exceeding the grace period defined by the borrower.

  • Withdrawals: Borrowers can withdraw underlying assets from markets as long as the reserve ratio is maintained. Withdrawals are initiated by addresses with specific roles, and assets are moved into a 'claimable withdrawals pool.' At the end of a withdrawal cycle, lenders can claim assets from the pool, subject to pro-rata dispersal.

  • Withdrawal Queues: Withdrawal requests that couldn't be honored due to insufficient reserves enter a FIFO withdrawal queue. Non-zero withdrawal queues impact the reserve ratio. Lenders need to burn market tokens to claim assets from the pool.

  • Sanctioned Addresses: Addresses flagged as sanctioned by Chainalysis are blocked from interacting with markets. A "nukeFromOrbit" function allows for the transfer of their balances into an escrow contract, which can be retrieved under specific conditions.

  • A borrower deploying a market with a base APR of 10%, a protocol APR of 30%, a penalty APR of 20% will pay a true APR of 13% (10% + (10% * 30%)) under normal circumstances and 33% when the market has been delinquent for long enough for the penalty APR to activate. The 30% protocol APR is calculated as 30% of the base APR, so 30% of 10% = 3% (10% base, 3% protocol)


2. Architecture overview (Codebase Explanation, Examples Executions & Scenarios)

Alt text

Overview Architecture -> https://prnt.sc/GISTKqpwap2K Credits: "0xkazim"

All Possible Actions of Borrower:

  1. Launching A New Market
  2. Sourcing Deposits
  3. Borrowing From A Market
    • borrower call borrow(uint256 amount), after that within borrow() function -> _getUpdatedState() -> check if the requested amount is lower than borrowable amount in the market (if not, then revert) -> _writeState() -> transfer the requested amount of underlying assets to the borrower
    • Remember that the capacity you set for your market only dictates the maximum amount that you are able to source from lenders, and that your reserve ratio will dictate the amount of the supply that you cannot remove from a market. If you have created a market with a maximum capacity of 1,000,000 USDC and a reserve ratio of 20%, this means you can borrow up to 800,000 USDC provided that the market is 'full' (i.e. supply is equal to capacity). In the event where the supply to this market is 600,000 USDC, you can only borrow up to 480,000 USDC.
    • Attack Ideas:
      • Can borrower borrow more that borrowable amount without enter the grace period
  4. Repaying A Market
    • borrower just transfer amount of underlying asset to market ans then should call updateState()
  5. Reducing APR
  6. Altering Capacity
  7. Closing A Market
  8. Removal From The ArchController

All Possible Actions of Lender:

  1. Deposits
  2. Withdrawals
    • The Unclaimed Withdrawals Pool
    • Claiming
    • Expired Claims and The Withdrawal Queue

The Sentinel Actions:

  1. Lender Gets Sanctioned
  2. Borrower Gets Sanctioned

Structs Parameters Explanation:

MarketState Struct
  • sponsor comment: scaledPendingWithdrawals is tracked because borrowers are obligated to honour 100% of pending withdrawals and the reserve ratio for the rest of the total supply to avoid delinquency.
struct MarketState {
    bool isClosed;
    uint128 maxTotalSupply;
    uint128 accruedProtocolFees;
    // Underlying assets reserved for withdrawals which have been paid
    // by the borrower but not yet executed. 
    // sponsor explanation: amount of underlying assets paid to batches but not yet claimed in executeWithdrawal
    uint128 normalizedUnclaimedWithdrawals;
    // Scaled token supply (divided by scaleFactor)
    uint104 scaledTotalSupply;
    // Scaled token amount in withdrawal batches that have not been
    // paid by borrower yet.
    // sponsor explanation: sum of unpaid amounts for all withdrawal batches
    uint104 scaledPendingWithdrawals;
    /// expiry timestamp of the current unexpired withdrawal batch
    uint32 pendingWithdrawalExpiry;
    // Whether market is currently delinquent (liquidity under requirement)
    bool isDelinquent;
    // Seconds borrower has been delinquent
    uint32 timeDelinquent;
    // Annual interest rate accrued to lenders, in basis points
    uint16 annualInterestBips;
    // Percentage of outstanding balance that must be held in liquid reserves
    uint16 reserveRatioBips;
    // Ratio between internal balances and underlying token amounts
    uint112 scaleFactor;
    uint32 lastInterestAccruedTimestamp;
}

Functions Flow Explanation:

WildcatMarketBase.sol (Base contract for Wildcat markets)
_getUpdatedState() function flow:
  1. _getUpdatedState() function performs the following actions:

    • Returns cached MarketState after accruing interest and delinquency / protocol fees and processing expired withdrawal batch, if any.

    • Used by functions that make additional changes to state.

    • Apply pending interest, delinquency fees and protocol fees to the state and process the pending withdrawal batch if one exists and has expired (accruing interest and delinquency / protocol fees and processing expired withdrawal batch, if any).

    • Used by functions that make additional changes to state.

    • Returned state does not match _state if interest is accrued.

    • Calling function must update _state or revert.

  2. The function follows a specific logic sequence:

    • It first checks if there is a pending expired withdrawal batch by calling state.hasPendingExpiredBatch() verify whether pendingWithdrawalExpiry is greater than 0 and less than the current block.timestamp.

    • If this condition holds true, the function proceeds to check if expiry != state.lastInterestAccruedTimestamp. This check ensures that interest is only accrued if sufficient time has passed since the last update. It also considers cases where withdrawalBatchDuration is set to 0.

    • If the above check returns true, the function calls updateScaleFactorAndFees(). This function calculates the interest and delinquency/protocol fees accrued since the last state update and applies them to the cached state. It also provides the rates for base interest and delinquency fees, along with the normalized amount of protocol fees accrued.

    • The function then calls _processExpiredWithdrawalBatch() to handle any expired withdrawal batches.

  3. Finally, the function checks if block.timestamp differs from state.lastInterestAccruedTimestamp. If this condition is met, it once again calls updateScaleFactorAndFees() to apply interest and fees accrued since the last update (either due to an expiry or a previous transaction).

_writeState() function flow:
  • Writes the cached MarketState to storage and emits an event. Also set if the market is in delinquent state.
  • Used at the end of all functions which modify state.
_getAccount function flow:
  • return the account by his address (the account is struct of scaledBalance and AuthRole)
_blockAccount function flow:
  • Block an account and transfer its balance of market tokens to an escrow contract.
  • If the account is already blocked, this function does nothing.
  • During function execution the scaleBalance of account is set to 0, approval to Blocked and the _accounts[escrow].scaledBalance is increased by the scaledBalance of account in current market state.
_getAccountWithRole function flow:
  • Retrieve an account from storage and assert that it has at least the required role.
  • If the account's role is not set, queries the controller to determine if it is an approved lender; if it is, its role is initialized to DepositAndWithdraw.
coverageLiquidity function flow:
  • Returns the amount of underlying assets the borrower is obligated to maintain in the market to avoid delinquency.
  • the function call liquidityRequired() function which basically determine how much liquidity must be available within the market to cover pending withdrawals, required reserves, protocol fees, and unclaimed withdrawals
scaleFactor() Function:
  • Returns the scale factor (in ray) used to convert scaled balances to normalized balances.
totalAssets() Function:
  • Total balance in underlying asset.
borrowableAssets() function flow:
  • Returns the amount of underlying assets the borrower is allowed to borrow. This is the balance of underlying assets minus: pending (unpaid) withdrawals, paid withdrawals, reserve ratio times the portion of the supply not pending withdrawal, protocol fees
  • The function basically call borrowableAssets() with totalAssets() view function as a parameter
accruedProtocolFees() function flow:
  • Returns the amount of protocol fees (in underlying asset amount) that have accrued and are pending withdrawal.
previousState() function flow:
  • Returns the state of the market as of the last update.
currentState() function flow:
  • Return the state the market would have at the current block after applying interest and fees accrued since the last update and processing the pending withdrawal batch if it is expired.
scaledTotalSupply() function flow:
  • Returns the scaled total supply the vaut would have at the current block after applying interest and fees accrued since the last update and burning market tokens for the pending withdrawal batch if it is expired.
  • return currentState().scaledTotalSupply;
scaledBalanceOf() function flow:
  • Returns the scaled balance of account
withdrawableProtocolFees function flow:
  • Returns the amount of protocol fees that are currently withdrawable by the fee recipient.
  • The function basically calls the MarketState#withdrawableProtocolFees() with current market state. The MarketState#withdrawableProtocolFees() subtract the normalizedUnclaimedWithdrawals from totalAssets and return -> MathUtils.min(totalAvailableAssets, state.accruedProtocolFees).
effectiveBorrowerAPR() function flow:
  • Calculate effective interest rate currently paid by borrower.
  • Borrower pays base APR, protocol fee (on base APR) and delinquency fee (if delinquent beyond grace period).
effectiveLenderAPR() function flow:
  • Calculate effective interest rate currently earned by lenders. Lenders earn base APR and delinquency fee (if delinquent beyond grace period)
_calculateCurrentState() function flow:
  • Calculate the current state, applying fees and interest accrued since the last state update as well as the effects of withdrawal batch expiry on the market state.
  • Identical to _getUpdatedState() except it does not modify storage or emit events.
  • Returns expired batch data, if any, so queries against batches have access to the most recent data.
_applyWithdrawalBatchPayment() function flow:
  • Process withdrawal payment, burning market tokens and reserving underlying assets so they are only available for withdrawals.


3. Code Audit Approach

After first reading the protocol documentation in the "README.md" file, these possible attack vectors popped into my head:

  1. Can the market enter the "Penalty APR" even if the "reserve ratio" is not actually broken?

  2. The market cannot trigger the "Penalty APR?"

  3. Closing a Market without the payment of interest?

  4. Repayment is paused while the "Penalty APR" can be activated?

  5. Markets contains tokens that the protocol does not support?

  6. The Market cannot be closed?

  7. Broken Protocol Invariants/User Flow Assumptions?

Protocol Assumptions:

  • Any ERC-20 can be used as an underlying asset for a market
  • There are no fees on transfer tokens.
  • The totalSupply is nowhere near 2^128.
  • Arbitrary mints and burns are not possible.
  • name, symbol and decimals all return valid results.
  • Markets that have already been deployed cannot be shut down, and removing a borrower from the ArchController simply prevents them from deploying anything further
  • Lenders cannot be prevented from withdrawing unless they have been flagged by the sentinel (even if they are removed from market by borrower).
  1. Broken Protocol Calculations?

  2. Examine the logic of Withdraws (whether the state of the market is properly handled if withdrawal requests couldn't be honored due to insufficient reserves), Withdrawal Queues, Sanctioned Addresses!

  3. Examine all possible flows in the Protocol!

  4. Breaking of trusted role supposed actions in the Protocol

  • Archcontroller Owner Actions:
    • Can add or remove controller factories to/from the ArchController, dictating who can deploy controllers and markets.
  • Borrower Actions:
    • Can add or remove lenders to/from controllers, permitting them to deposit into markets or restricting their ability to deposit further if they have already deposited before.

Attacks discussed during the Audit:

  • Is it possible for lenders to accrue additional interest or withdraw funds again after a full or partial withdrawal?
  • Which state variables responsible for lenders, depositors, or borrowers undergo changes during non-static function calls, and what potential pitfalls could arise from these changes?
  • Can lenders or depositors potentially lose their scoringBalance or market tokens in any way?
  • How might the protocol's functions be exploited to cause unexpected denial-of-service (DoS) scenarios, potentially due to improper checks or erroneous protocol assumptions?
  • Changing variables that play a crucial role in protocol calculations typically leads to errors; what safeguards are in place to prevent this?
  • Let's explore a hypothetical scenario of expected code logic and consider how it could be compromised by testing various scenarios to break it.
  • In a protocol with multiple user roles, are there checks in place to ensure that functions designed for specific roles cannot be accessed by unauthorized roles?
  • How should borrowers deposit assets to fulfill lender withdrawal requests? Is there a dedicated function for this, or do borrowers need to transfer assets to the market manually?

4. Codebase Complexity and Codebase Quality

Codebase Complexity Analysis for WildCat Protocol

  1. Structs and Data Organization:

    • The codebase relies on structured data using the MarketState struct to store and manage various market-related parameters.
    • The use of structs enhances readability and maintains a well-organized data structure.
  2. Modularity and Reusability:

    • The codebase includes functions like _getUpdatedState(), updateScaleFactorAndFees(), and _processExpiredWithdrawalBatch(), which are modular and designed for reusability.
    • Modular functions promote code maintainability and facilitate testing.
  3. Conditions and Branching:

    • The codebase contains conditional statements to control program flow based on certain conditions. For instance, checking for pending withdrawal batches and interest accrual.
    • Conditionals help ensure that code executes only when specific requirements are met.
  4. Error Handling:

    • Error handling is present in the form of conditional checks and reverts. For example, checks for valid timestamps and reverting on certain conditions.
    • Proper error handling enhances the robustness of the protocol.
  5. Mathematical Calculations:

    • The codebase performs mathematical calculations involving scaling, interest rates, and fees. These calculations are critical for market operations.
    • Careful attention to mathematical accuracy is essential to avoid financial risks.
  6. Use of Comments:

    • Comments are used effectively to provide explanations of code logic, function purposes, and variable descriptions.
    • Well-commented code enhances readability and understanding.
  7. Complexity Control:

    • The codebase attempts to control complexity by breaking down complex operations into modular functions, promoting code reuse and maintainability.

Codebase Quality Analysis for WildCat Protocol

  1. Structural Clarity:

    • The codebase demonstrates clear structuring with well-defined structs and functions.
    • Structs are used to encapsulate related data fields, improving code clarity.
  2. Modularity and Reusability:

    • The presence of modular functions like _getUpdatedState() and updateScaleFactorAndFees() promotes reusability and simplifies code maintenance.
  3. Error Handling:

    • The codebase incorporates error handling through conditional checks and reverts, ensuring robustness and preventing unintended behaviors.
  4. Code Comments:

    • The codebase employs comments effectively, providing valuable insights into the purpose and functionality of various components.
    • Well-documented code is crucial for collaboration and future development.
  5. Mathematical Accuracy:

    • The codebase involves mathematical calculations, especially related to interest rates and fees. It is crucial to ensure the accuracy of these calculations to maintain the financial integrity of the protocol.
  6. Use of Solidity Best Practices:

    • The codebase adheres to Solidity best practices, such as using structs for data organization and modular functions for code reuse.
    • Following best practices helps mitigate potential vulnerabilities.
  7. Readability and Maintainability:

    • The codebase exhibits good readability through clear structuring, meaningful variable names, and appropriate commenting.
    • Maintaining code readability is essential for ongoing development and collaboration.
  8. Testing:

    • It is crucial to have comprehensive unit tests and integration tests to validate the functionality of the protocol.
    • Thorough testing contributes to code quality and reliability.
  9. Security Considerations:

    • Given the financial nature of the protocol, rigorous security audits and testing are necessary to identify and address vulnerabilities.
    • Security should be a top priority in the development process.

In summary, the WildCat Protocol codebase demonstrates good structural clarity, modularity, and error handling practices.


5. Centralization Risks

The protocol gives the borrower too much access and control over the lenders. It's also a very sneaky design that the deployer of a specific contract "WildcatMarketController" has control over all lenders for markets with possibly different borrowers (tuned borrower in specific market cannot have control over lenders in that market, only the deployer of WildcatMarketController contract) . Be afair and definitely write this in the decumentation.


6. Systemic Risks

The WildCat Protocol, like any financial system, has its fair share of potential risks that could affect its stability and the safety of users funds. So bellow I write possible risks.

  1. Smart Contract Hazards:

    • The protocol depends on smart contracts to handle financial operations. Bugs in these contracts, like reentrancy issues or arithmetic errors, pose a significant risk.
    • Safety Measures: Rigorous audits, testing, code reviews, and bug bounties are essential to catch and fix smart contract vulnerabilities.
  2. Liquidity Concerns:

    • DeFi platforms like WildCat can face situations where there isn't enough money to meet user demands, especially during wild market swings. This can lead to problems like slippage and liquidity shortages.
    • Safety Measures: Encouraging liquidity providers and maintaining sufficient reserves can help ensure there's enough money to go around.
  3. Interest Rates and Price Swings:

    • The WildCat Protocol involves lending and borrowing assets, which means it's sensitive to changes in interest rates. Also, the value of collateral can fluctuate wildly.
    • Safety Measures: Adjusting interest rates and collateral ratios smartly can help offset the impact of these ups and downs.
  4. Market Delinquency:

    • Delinquency occurs when borrowers don't keep enough collateral, making the market undercollateralized. This can lead to forced sell-offs and losses.
    • Safety Measures: Keeping an eye on collateral ratios and setting up mechanisms to trigger liquidations can prevent market delinquency.
  5. Protocol Governance:

    • Governance decisions made by the community can sometimes lead to disagreements or conflicts that disrupt how the protocol operates.
    • Safety Measures: Creating clear governance processes, involving the community, and having ways to resolve disputes can address these issues.
  6. Economic Incentive Conflicts:

    • Users and the people running the protocol might not always want the same things. This misalignment can cause actions that hurt the protocol's stability.
    • Safety Measures: Thoughtful design of incentives, governance rules, and penalties can help everyone's interests align better.
  7. Regulatory and Compliance Worries:

    • Regulations can change or authorities might take action that affects how the protocol runs. Not complying with these rules could lead to legal trouble.
    • Safety Measures: Staying informed about regulations, seeking legal advice, and following the rules can help the protocol stay on the right side of the law.
  8. Connected to Other Protocols:

    • DeFi protocols often interact with each other. If one protocol has problems, it can affect others in a chain reaction.
    • Safety Measures: Doing thorough checks on connected protocols, having ways to pause operations, and having plans for emergencies can minimize risks from interconnectedness.

Time spent:

25 hours

#0 - c4-judge

2023-11-09T12:14:46Z

MarioPoneder marked the issue as grade-b

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