Skip to content

Instantly share code, notes, and snippets.

@code423n4
Created October 18, 2023 01:07
Show Gist options
  • Save code423n4/265e1707379b9a3f89cf18aa0bffcb7c to your computer and use it in GitHub Desktop.
Save code423n4/265e1707379b9a3f89cf18aa0bffcb7c to your computer and use it in GitHub Desktop.
Best bot finding from 2023-10-wildcat-bot-findings

Winning bot race submission

This is the top-ranked automated findings report, from henry bot. All findings in this report will be considered known issues for the purposes of your C4 audit.

Report

Audit report for 2023-10-wildcat generated by Bot-Henry.

Note: There is a section for disputed findings below the usual findings sections.

Summary

Medium Issues

Total 9 instances over 3 issues:

ID Issue Instances
[M‑01] Centralization risk for privileged functions 7
[M‑02] Return values of transfer()/transferFrom() not checked 1
[M‑03] Unsafe use of ERC20 transfer()/transferFrom() 1

Low Issues

Total 95 instances over 21 issues:

ID Issue Instances
[L‑01] Use Ownable2Step instead of Ownable 1
[L‑02] Function decimals() is not a part of the ERC-20 standard 1
[L‑03] Missing zero address check in constructor 4
[L‑04] Revert on transfer to the zero address 6
[L‑05] SafeTransferLib does not ensure that the token contract exists 1
[L‑06] Using >/>= without specifying an upper bound in version pragma is unsafe 21
[L‑07] Array is push()ed but not pop()ed 1
[L‑08] State variables not limited to reasonable values 1
[L‑09] Missing checks for address(0) when setting address state variables 8
[L‑10] Owner can renounce Ownership 1
[L‑11] Critical functions should use two-step procedure 2
[L‑12] Constructor / initialization function lacks parameter validation 4
[L‑13] Contracts are vulnerable to fee-on-transfer accounting-related issues 8
[L‑14] Functions calling contracts/addresses with transfer hooks should be protected by reentrancy guard 2
[L‑15] Governance functions should be controlled by time locks 10
[L‑16] External function calls within loops 1
[L‑17] Some tokens may revert when zero value transfers are made 4
[L‑18] Use SafeCast to downcast safely 10
[L‑19] Use descriptive constant instead of 0 as a parameter 2
[L‑20] Loss of precision in divisions 2
[L‑21] Code does not follow the best practice of check-effects-interaction 5

Non Critical Issues

Total 1919 instances over 87 issues:

ID Issue Instances
[N‑01] Consider moving msg.sender checks to modifiers 4
[N‑02] Custom errors has no error details 21
[N‑03] Import declarations should import specific identifiers, rather than the whole file 50
[N‑04] Too long functions should be refactored 4
[N‑05] There is no need to initialize variables with 0 12
[N‑06] NatSpec documentation for contract is missing 18
[N‑07] Names of constants should use the UPPER_CASE_WITH_UNDERSCORES style 25
[N‑08] Names of private/internal functions should be prefixed with an underscore 80
[N‑09] Names of private/internal state variables should be prefixed with an underscore 23
[N‑10] Order of functions does not follow the Solidity Style Guide 43
[N‑11] The nonReentrant modifier should occur before all other modifiers 6
[N‑12] Redundant inheritance specifier 1
[N‑13] Strings should use double quotes rather than single quotes 3
[N‑14] Use of override is unnecessary 9
[N‑15] Unused errors 4
[N‑16] Unused events 2
[N‑17] Large numeric literals should use underscores for readability 4
[N‑18] Assembly blocks should have extensive comments 21
[N‑19] Complex casting 6
[N‑20] Constants/Immutables redefined elsewhere 38
[N‑21] Convert simple if-statements to ternary expressions 2
[N‑22] Events should be emitted before external calls 11
[N‑23] Events are emitted without the sender information 12
[N‑24] Event is missing indexed fields 10
[N‑25] Inconsistent floating version pragma 2
[N‑26] Imports could be organized more systematically 5
[N‑27] Imports should use double quotes rather than single quotes 67
[N‑28] @openzeppelin/contracts should be upgraded to a newer version 3
[N‑29] Magic numbers should be replaced with constants 5
[N‑30] Expressions for constant values should use immutable rather than constant 7
[N‑31] Functions not used internally could be marked external 9
[N‑32] Use @inheritdoc for overridden functions 9
[N‑33] Contracts should have NatSpec @author tags 18
[N‑34] Contracts should have @notice tags 18
[N‑35] Contracts should have NatSpec @title tags 18
[N‑36] Event declarations should have NatSpec descriptions 10
[N‑37] NatSpec documentation for function is missing 124
[N‑38] Functions missing NatSpec @param tag 257
[N‑39] Modifiers missing NatSpec @param tag 1
[N‑40] Public variable declarations should have NatSpec descriptions 23
[N‑41] Functions missing NatSpec @return tag 151
[N‑42] Contract name does not follow the Solidity Style Guide 3
[N‑43] Functions and modifiers should be named in mixedCase style 3
[N‑44] Variable names for immutables should use UPPER_CASE_WITH_UNDERSCORES 50
[N‑45] Non-assembly method available 29
[N‑46] Order of contract layout does not follow the Solidity Style Guide 6
[N‑47] Missing zero address check in functions with address parameters 47
[N‑48] Named imports of parent contracts are missing 11
[N‑49] Constants should be put on the left side of comparisons 19
[N‑50] Put all system-wide constants in one file 2
[N‑51] Redundant return statement in a function with named return variables 1
[N‑52] Duplicated require()/revert() checks should be refactored 1
[N‑53] Large multiples of ten should use scientific notation 4
[N‑54] Non-interface files should use fixed compiler versions 19
[N‑55] Consider bounding input array length 3
[N‑56] Unused import 3
[N‑57] Unused named return 8
[N‑58] Use delete instead of assigning values to false 1
[N‑59] Consider using delete rather than assigning zero to clear values 5
[N‑60] Use the latest Solidity version (0.8.19 for L2s) 19
[N‑61] Named mappings are recommended 5
[N‑62] Whitespace in Expressions 6
[N‑63] Use a struct to encapsulate multiple function parameters 3
[N‑64] Returning a struct instead of a bunch of variables is better 1
[N‑65] Addresses shouldn't be hard-coded 1
[N‑66] Events that mark critical parameter changes should contain both the old and the new value 4
[N‑67] Non-public state variables should include comments 35
[N‑68] File is missing NatSpec 7
[N‑69] Modifier declarations should have NatSpec descriptions 7
[N‑70] Empty bytes check is missing 1
[N‑71] Contract functions should use an interface 93
[N‑72] Don't define functions with the same name in a contract 9
[N‑73] Assembly block creates dirty bits 7
[N‑74] Control structures do not follow the Solidity Style Guide 16
[N‑75] Functions contain the same code 4
[N‑76] Missing event for critical changes 2
[N‑77] Consider adding emergency-stop functionality 3
[N‑78] Avoid the use of sensitive terms 2
[N‑79] Consider adding a block/deny-list 11
[N‑80] Enable IR-based code generation 1
[N‑81] Contracts should have NatSpec @dev tags 19
[N‑82] Error declarations should have NatSpec descriptions 19
[N‑83] Functions should have @notice tags 124
[N‑84] Contracts should have full test coverage 1
[N‑85] Large or complicated code bases should implement invariant tests 1
[N‑86] Top-level declarations should be separated by at least two lines 145
[N‑87] Consider adding formal verification proofs 22

Gas Optimizations

Total 422 instances over 36 issueswith 81743 gas saved:

ID Issue Instances Gas
[G‑01] The <array>.length should be cached outside of the for-loop 3 9
[G‑02] Use shift right instead of division if possible 2 40
[G‑03] Use storage instead of memory for structs/arrays 8 33600
[G‑04] Using private for constants saves gas 2 6812
[G‑05] Constructors can be marked as payable to save deployment gas 7 147
[G‑06] State variables only set in the constructor should be declared immutable 2 4194
[G‑07] Using ++X/--X instead of X++/X-- can save gas 12 60
[G‑08] Unnecessary event parameters should be removed 1 358
[G‑09] Unused named return variables without optimizer waste gas 8 72
[G‑10] Use unchecked block for safe subtractions 2 170
[G‑11] internal functions only called once can be inlined to save gas 5 150
[G‑12] Functions that revert when called by normal users can be marked payable 19 399
[G‑13] Use s.x = s.x + y instead of s.x += y for memory structs 19 1900
[G‑14] Operator >=/<= costs less gas than operator >/< 21 63
[G‑15] Usage of ints/uints smaller than 32 bytes incurs overhead 143 7865
[G‑16] Divisions can be unchecked to save gas 4 80
[G‑17] Increments can be unchecked to save gas 12 720
[G‑18] Unused non-constant state variables waste gas 2 -
[G‑19] Using assembly to check for zero can save gas 15 90
[G‑20] Use assembly to write address/contract type storage values 2 100
[G‑21] Use uint256(1)/uint256(2) instead of true/false to save gas for changes 1 17100
[G‑22] Avoid zero transfer to save gas 4 400
[G‑23] Don't emit events inside a loop 2 750
[G‑24] Optimize names to save gas 9 198
[G‑25] Duplicated require()/revert() checks should be refactored to a modifier Or function to save gas 1 -
[G‑26] Reduce gas usage by moving to Solidity 0.8.19 or later 3 -
[G‑27] Newer versions of solidity are more gas efficient 19 -
[G‑28] The result of a function call should be cached rather than re-calling the function 3 300
[G‑29] State variables that are used multiple times in a function should be cached in stack variables 10 970
[G‑30] Use assembly to emit events 36 1368
[G‑31] Use assembly to compute hashes to save gas 4 320
[G‑32] Use calldata instead of memory for immutable arguments 9 2700
[G‑33] Consider activating via-ir for deploying 22 -
[G‑34] Using bools for storage incurs overhead 1 100
[G‑35] Multiple accesses of the same mapping/array key/index should be cached 3 126
[G‑36] State variable access within a loop 6 582

Disputed Issues

The issues below may be reported by other bots/wardens, but can be penalized/ignored since either the rule or the specified instances are invalid.

Total 515 instances over 50 issues:

ID Issue Instances
[D‑01] abi.encodePacked() should be replaced with bytes.concat() 1
[D‑02] Visibility should be set explicitly rather than defaulting to internal 2
[D‑03] Using private rather than public for constants, saves gas 27
[D‑04] Event names should use CamelCase 10
[D‑05] internal functions not called by the contract should be removed 71
[D‑06] Unused named return variables without optimizer waste gas 21
[D‑07] Assembly blocks should have extensive comments 10
[D‑08] Cast to bytes or bytes32 for clearer semantic meaning 1
[D‑09] Passing abi.encodePacked() with dynamic arguments to a hash can cause collisions 1
[D‑10] Change public function visibility to external to save gas 9
[D‑11] NatSpec: Contract declarations should have @notice tags 1
[D‑12] Not initializing local variables to zero saves gas 11
[D‑13] x += y is more expensive than x = x + y for state variables 2
[D‑14] Revert on transfer to the zero address 3
[D‑15] The SafeTransferLib does not ensure that the token contract exists 5
[D‑16] SafeTransferLib does not ensure that the token contract exists 7
[D‑17] Solidity version 0.8.20 or above may not work on other chains due to PUSH0 22
[D‑18] Floating pragma should be avoided 3
[D‑19] SPDX identifier should be the in the first line of a solidity file 22
[D‑20] Timestamp may be manipulation 11
[D‑21] Unused internal functions should be removed to save deployment gas 71
[D‑22] Unused named return 21
[D‑23] Using delete statement can save gas 6
[D‑24] Use != 0 or == 0 for unsigned integer comparison 12
[D‑25] Avoid contract existence checks by using low level calls 29
[D‑26] Consider using named mappings 1
[D‑27] Events that mark critical parameter changes should contain both the old and the new value 6
[D‑28] State variables should include comments 1
[D‑29] safeTransfer function does not check for contract existence 7
[D‑30] Critical functions should use two-step procedure 2
[D‑31] Assembly block creates dirty bits 38
[D‑32] Avoid updating storage when the value hasn't changed 2
[D‑33] Calculations should be memoized rather than re-calculating them 21
[D‑34] Inconsistent spacing in comments 5
[D‑35] Redundant state variable getters 1
[D‑36] keccak256() should only need to be called on a specific string literal once 1
[D‑37] Use SafeCast to downcast safely 2
[D‑38] Use assembly to compute hashes to save gas 1
[D‑39] Use calldata instead of memory for immutable arguments 9
[D‑40] Consider adding emergency-stop functionality 8
[D‑41] Use descriptive constant instead of 0 as a parameter 2
[D‑42] Prevent re-setting a state variable with the same value 1
[D‑43] The deadline timestamp should be considered valid 3
[D‑44] Loss of precision 2
[D‑45] Consider using bytes32 rather than a string 3
[D‑46] Some tokens may revert when large transfers are made 8
[D‑47] unchecked blocks with additions/multiplications may overflow 2
[D‑48] unchecked blocks with subtractions may underflow 2
[D‑49] Numeric values having to do with time should use time units for readability 1
[D‑50] Contracts are vulnerable to rebasing accounting-related issues 7

Medium Issues

[M‑01] Centralization risk for privileged functions

Contracts with privileged functions need owner/admin to be trusted not to perform malicious updates or drain funds. This may also cause a single point failure.

There are 7 instances:

63:   function registerBorrower(address borrower) external onlyOwner {

70:   function removeBorrower(address borrower) external onlyOwner {

106:   function registerControllerFactory(address factory) external onlyOwner {

113:   function removeControllerFactory(address factory) external onlyOwner {

156:   function removeController(address controller) external onlyOwner {

199:   function removeMarket(address market) external onlyOwner {
  • WildcatMarketControllerFactory.sol ( #L195-L200 ):
195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {

[M‑02] Return values of transfer()/transferFrom() not checked

Not all ERC20 implementations revert() when there's a failure in transfer() or transferFrom(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed, may potentially go through without actually transfer anything.

There is 1 instance:

  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);

[M‑03] Unsafe use of ERC20 transfer()/transferFrom()

Some tokens do not implement the ERC20 standard properly. For example Tether (USDT)'s transfer() and transferFrom() functions do not return booleans as the ERC20 specification requires, and instead have no return value. When these sorts of tokens are cast to IERC20/ERC20, their function signatures do not match and therefore the calls made will revert. It is recommended to use the SafeERC20's safeTransfer() and safeTransferFrom() from OpenZeppelin instead.

There is 1 instance:

  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);

Low Issues

[L‑01] Use Ownable2Step instead of Ownable

Ownable2Step and Ownable2StepUpgradeable prevent the contract ownership from mistakenly being transferred to an address that cannot handle it (e.g. due to a typo in the address), by requiring that the recipient of the owner permissions actively accept via a contract call of its own.

There is 1 instance:

  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {

[L‑02] Function decimals() is not a part of the ERC-20 standard

The symbol() function is not a part of the ERC-20 standard, and was added later as an optional extension. As such, some valid ERC20 tokens do not support this interface, so it is unsafe to blindly cast all tokens to this interface, and then call this function.

There is 1 instance:

  • WildcatMarketBase.sol ( #L99 ):
99:     decimals = IERC20Metadata(parameters.asset).decimals();

[L‑03] Missing zero address check in constructor

Constructors often take address parameters to initialize important components of a contract, such as owner or linked contracts. However, without a checking, there's a risk that an address parameter could be mistakenly set to the zero address (0x0). This could be due to an error or oversight during contract deployment. A zero address in a crucial role can cause serious issues, as it cannot perform actions like a normal address, and any funds sent to it will be irretrievable. It's therefore crucial to include a zero address check in constructors to prevent such potential problems. If a zero address is detected, the constructor should revert the transaction.

There are 4 instances:

  • WildcatMarketControllerFactory.sol ( #L72-L76 ):
/// @audit `_archController not checked`
/// @audit `_sentinel not checked`
72:   constructor(
73:     address _archController,
74:     address _sentinel,
75:     MarketParameterConstraints memory constraints
76:   ) {
  • WildcatSanctionsSentinel.sol ( #L24 ):
/// @audit `_archController not checked`
/// @audit `_chainalysisSanctionsList not checked`
24:   constructor(address _archController, address _chainalysisSanctionsList) {

[L‑04] Revert on transfer to the zero address

It's good practice to revert a token transfer transaction if the recipient's address is the zero address. This can prevent unintentional transfers to the zero address due to accidental operations or programming errors. Many token contracts implement such a safeguard, such as OpenZeppelin - ERC20, OpenZeppelin - ERC721.

There are 6 instances:

  • WildcatMarketController.sol ( #L346 ):
346:       originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);
  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);
107:     asset.safeTransfer(feeRecipient, withdrawableFees);

157:       asset.safeTransfer(borrower, currentlyHeld - totalDebts);
171:       asset.safeTransfer(escrow, normalizedAmountWithdrawn);

179:       asset.safeTransfer(accountAddress, normalizedAmountWithdrawn);

[L‑05] SafeTransferLib does not ensure that the token contract exists

There is a subtle difference between the implementation of solady/solmate's SafeTransferLib and OZ's SafeERC20. OZ's SafeERC20 checks if the token is a contract or not, while SafeTransferLib does not:

@dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.

There is 1 instance:

  • WildcatMarketController.sol ( #L346 ):
346:       originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);

[L‑06] Using >/>= without specifying an upper bound in version pragma is unsafe

There will be breaking changes in future versions of solidity, and at that point your code will no longer be compatible. While you may have the specific version to use in a configuration file, others that include your source files may not.

There are 21 instances (click to show):
  • ReentrancyGuard.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatArchController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketControllerFactory.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsEscrow.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsSentinel.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • BoolUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Errors.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FIFOQueue.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FeeMath.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • LibStoredInitCode.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MarketState.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MathUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • SafeCastLib.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • StringQuery.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Withdrawal.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarket.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketBase.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketConfig.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketToken.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketWithdrawals.sol ( #L2 ):
2: pragma solidity >=0.8.20;

[L‑07] Array is push()ed but not pop()ed

Array entries are added but are never removed. Consider whether this should be the case, or whether there should be a maximum, or whether old entries should be removed. Cases where there are specific potential problems will be flagged separately under a different issue.

There is 1 instance:

  • WildcatMarketBase.sol ( #L484 ):
484:       _withdrawalData.unpaidBatches.push(expiry);

[L‑08] State variables not limited to reasonable values

Consider adding appropriate minimum/maximum value checks to ensure that the following state variables can never be used to excessively harm users, including via griefing.

There is 1 instance:

  • WildcatMarketControllerFactory.sol ( #L211-L216 ):
211:     _protocolFeeConfiguration = ProtocolFeeConfiguration({
212:       feeRecipient: feeRecipient,
213:       originationFeeAsset: originationFeeAsset,
214:       originationFeeAmount: originationFeeAmount,
215:       protocolFeeBips: protocolFeeBips
216:     });

[L‑09] Missing checks for address(0) when setting address state variables

There are 8 instances:

97:     archController = IWildcatArchController(parameters.archController);

98:     borrower = parameters.borrower;

99:     sentinel = parameters.sentinel;

100:     marketInitCodeStorage = parameters.marketInitCodeStorage;
  • WildcatMarketControllerFactory.sol ( #L77, #L78 ):
77:     archController = IWildcatArchController(_archController);

78:     sentinel = _sentinel;
  • WildcatSanctionsSentinel.sol ( #L25, #L26 ):
25:     archController = _archController;

26:     chainalysisSanctionsList = _chainalysisSanctionsList;

[L‑10] Owner can renounce Ownership

Each of the following contracts implements or inherits the renounceOwnership() function. This can represent a certain risk if the ownership is renounced for any other reason than by design. Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.

There is 1 instance:

  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {

[L‑11] Critical functions should use two-step procedure

A copy-paste error or a typo may end up bricking protocol functionality, or sending tokens to an address with no known private key. Consider implementing a two-step procedure for critical functions, where the recipient is set as pending, and must "accept" the assignment by making an affirmative call. A straight forward way of doing this would be to have the target contracts implement EIP-165, and to have the "set" functions ensure that the recipient is of the right interface type.

There are 2 instances:

  • WildcatMarketControllerFactory.sol ( #L195-L200 ):
195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {
112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {

[L‑12] Constructor / initialization function lacks parameter validation

Constructors and initialization functions play a critical role in contracts by setting important initial states when the contract is first deployed before the system starts. The parameters passed to the constructor and initialization functions directly affect the behavior of the contract / protocol. If incorrect parameters are provided, the system may fail to run, behave abnormally, be unstable, or lack security. Therefore, it's crucial to carefully check each parameter in the constructor and initialization functions. If an exception is found, the transaction should be rolled back.

There are 4 instances:

  • WildcatMarketControllerFactory.sol ( #L72-L76 ):
/// @audit `_archController`
/// @audit `_sentinel`
72:   constructor(
73:     address _archController,
74:     address _sentinel,
75:     MarketParameterConstraints memory constraints
76:   ) {
  • WildcatSanctionsSentinel.sol ( #L24 ):
/// @audit `_archController`
/// @audit `_chainalysisSanctionsList`
24:   constructor(address _archController, address _chainalysisSanctionsList) {

[L‑13] Contracts are vulnerable to fee-on-transfer accounting-related issues

Some tokens take a transfer fee (e.g. STA, PAXG), some do not currently charge a fee but may do so in the future (e.g. USDT, USDC). The functions below transfer funds from the caller to the receiver via transferFrom(), but do not ensure that the actual number of tokens received is the same as the input amount to the transfer. If the token is a fee-on-transfer token, the balance after the transfer will be smaller than expected, leading to accounting issues. Even if there are checks later, related to a secondary transfer, an attacker may be able to use latent funds (e.g. mistakenly sent by another user) in order to get a free credit. One way to solve this problem is to measure the balance before and after the transfer, and use the difference as the amount, rather than the stated amount.

There are 8 instances (click to show):
  • WildcatMarketController.sol ( #L346 ):
346:       originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);
  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);
60:     asset.safeTransferFrom(msg.sender, address(this), amount);

107:     asset.safeTransfer(feeRecipient, withdrawableFees);

129:     asset.safeTransfer(msg.sender, amount);

157:       asset.safeTransfer(borrower, currentlyHeld - totalDebts);
171:       asset.safeTransfer(escrow, normalizedAmountWithdrawn);

179:       asset.safeTransfer(accountAddress, normalizedAmountWithdrawn);

[L‑14] Functions calling contracts/addresses with transfer hooks should be protected by reentrancy guard

Even if the function follows the best practice of check-effects-interaction, not using a reentrancy guard when there may be transfer hooks opens the users of this protocol up to read-only reentrancy vulnerability with no way to protect them except by block-listing the entire protocol.

There are 2 instances:

  • WildcatMarketController.sol ( #L346 ):
346:       originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);
  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);

[L‑15] Governance functions should be controlled by time locks

Governance functions (such as upgrading contracts, setting critical parameters) should be controlled using time locks to introduce a delay between a proposal and its execution. This gives users time to exit before a potentially dangerous or malicious operation is applied.

There are 10 instances (click to show):
70:   function removeBorrower(address borrower) external onlyOwner {

113:   function removeControllerFactory(address factory) external onlyOwner {

156:   function removeController(address controller) external onlyOwner {

199:   function removeMarket(address market) external onlyOwner {
182:   function updateLenderAuthorization(address lender, address[] memory markets) external {

468:   function setAnnualInterestBips(
469:     address market,
470:     uint16 annualInterestBips
471:   ) external virtual onlyBorrower onlyControlledMarket(market) {
  • WildcatMarketControllerFactory.sol ( #L195-L200 ):
195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {
  • WildcatSanctionsSentinel.sol ( #L56 ):
56:   function removeSanctionOverride(address account) public override {
  • WildcatMarket.sol ( #L142 ):
142:   function closeMarket() external onlyController nonReentrant {
112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {

[L‑16] External function calls within loops

Calling external functions within loops can easily result in insufficient gas. This greatly increases the likelihood of transaction failures, DOS attacks, and other unexpected actions. It is recommended to limit the number of loops within loops that call external functions, and to limit the gas line for each external call.

There is 1 instance:

  • WildcatMarketController.sol ( #L188 ):
/// @audit Calling `updateAccountAuthorization()` within `for` loop
188:       WildcatMarket(market).updateAccountAuthorization(lender, _authorizedLenders.contains(lender));

[L‑17] Some tokens may revert when zero value transfers are made

Despite the fact that EIP-20 states that zero-value transfers must be accepted, some tokens, such as LEND, will revert if this is attempted, which may cause transactions that involve other tokens (such as batch operations) to fully revert. Consider skipping the transfer if the amount is zero, which will also save gas.

There are 4 instances:

  • WildcatMarketController.sol ( #L346 ):
346:       originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);
  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);
60:     asset.safeTransferFrom(msg.sender, address(this), amount);

157:       asset.safeTransfer(borrower, currentlyHeld - totalDebts);

[L‑18] Use SafeCast to downcast safely

When a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. The loss of data may cause incorrect calculations, unexpected state changes, or other unexpected behavior. It is recommended to use the SafeCast library.

There are 10 instances (click to show):
  • WildcatMarketController.sol ( #L484 ):
/// @audit uint256 -> uint128
484:       tmp.expiry = uint128(block.timestamp + 2 weeks);
  • WildcatSanctionsSentinel.sol ( #L72-L83 ):
/// @audit uint256 -> uint160
72:         uint160(
73:           uint256(
74:             keccak256(
75:               abi.encodePacked(
76:                 bytes1(0xff),
77:                 address(this),
78:                 keccak256(abi.encode(borrower, account, asset)),
79:                 WildcatSanctionsEscrowInitcodeHash
80:               )
81:             )
82:           )
83:         )
/// @audit uint256 -> uint32
172:     state.lastInterestAccruedTimestamp = uint32(timestamp);
  • MarketState.sol ( #L110 ):
/// @audit uint256 -> uint128
110:     return uint128(MathUtils.min(totalAvailableAssets, state.accruedProtocolFees));
/// @audit uint256 -> uint112
113:       scaleFactor: uint112(RAY),

/// @audit uint256 -> uint32
114:       lastInterestAccruedTimestamp: uint32(block.timestamp)

/// @audit uint256 -> uint104
510:     uint104 scaledAmountBurned = uint104(MathUtils.min(scaledAvailableLiquidity, scaledAmountOwed));

/// @audit uint256 -> uint104
539:     uint104 scaledAmountBurned = uint104(MathUtils.min(scaledAvailableLiquidity, scaledAmountOwed));
/// @audit uint256 -> uint32
95:       state.pendingWithdrawalExpiry = uint32(block.timestamp + withdrawalBatchDuration);

/// @audit uint256 -> uint128
151:     uint128 newTotalWithdrawn = uint128(
152:       MathUtils.mulDiv(batch.normalizedAmountPaid, status.scaledAmount, batch.scaledTotalAmount)
153:     );

[L‑19] Use descriptive constant instead of 0 as a parameter

Passing 0 or 0x0 as a function argument can sometimes result in a security issue(e.g. passing zero as the slippage parameter). A historical example is the infamous 0x0 address bug where numerous tokens were lost. This happens because 0 can be interpreted as an uninitialized address, leading to transfers to the 0x0 address, effectively burning tokens. Moreover, 0 as a denominator in division operations would cause a runtime exception. It's also often indicative of a logical error in the caller's code.

Consider using a constant variable with a descriptive name, so it's clear that the argument is intentionally being used, and for the right reasons.

There are 2 instances:

84:     deployment = createWithStoredInitCode(initCodeStorage, 0);

103:     deployment = create2WithStoredInitCode(initCodeStorage, salt, 0);

[L‑20] Loss of precision in divisions

Division by large numbers may result in the result being zero, due to solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator.

There are 2 instances:

63:     uint256 newTotalWithdrawn = uint256(batch.normalizedAmountPaid).mulDiv(
64:       status.scaledAmount,
65:       batch.scaledTotalAmount
66:     );

152:       MathUtils.mulDiv(batch.normalizedAmountPaid, status.scaledAmount, batch.scaledTotalAmount)

[L‑21] Code does not follow the best practice of check-effects-interaction

Code should follow the best-practice of check-effects-interaction, where state variables are updated before any external calls are made. Doing so prevents a large class of reentrancy bugs.

There are 5 instances:

  • WildcatMarketController.sol ( #L500 ):
/// @audit `setReserveRatioBips()` is called on line 499
500:     delete temporaryExcessReserveRatio[market];
  • WildcatSanctionsSentinel.sol ( #L114 ):
/// @audit `()` is called on line 110
114:     sanctionOverrides[borrower][escrowContract] = true;
  • WildcatMarket.sol ( #L65 ):
/// @audit `safeTransferFrom()` is called on line 60
65:     _accounts[msg.sender] = account;
/// @audit `createEscrow()` is called on line 172
178:         _accounts[escrow].scaledBalance += scaledBalance;

/// @audit `createEscrow()` is called on line 172
185:       _accounts[accountAddress] = account;

Non Critical Issues

[N‑01] Consider moving msg.sender checks to modifiers

If some functions are only allowed to be called by some specific users, consider using a modifier instead of checking with a require statement, especially if this check is done in multiple functions.

There are 4 instances:

303:       if (!archController.isRegisteredBorrower(msg.sender)) {
304:         revert NotRegisteredBorrower();
305:       }

306:     } else if (msg.sender != address(controllerFactory)) {
307:       revert CallerNotBorrowerOrControllerFactory();
308:     }
  • WildcatMarketControllerFactory.sol ( #L283-L285 ):
283:     if (!archController.isRegisteredBorrower(msg.sender)) {
284:       revert NotRegisteredBorrower();
285:     }
100:     if (!IWildcatArchController(archController).isRegisteredMarket(msg.sender)) {
101:       revert NotRegisteredMarket();
102:     }

[N‑02] Custom errors has no error details

Consider adding parameters to the error to indicate which user or values caused the failure.

There are 21 instances (click to show):
  • ReentrancyGuard.sol ( #L17 ):
17:   error NoReentrantCalls();
16:   error NotControllerFactory();

17:   error NotController();

19:   error BorrowerAlreadyExists();

20:   error ControllerFactoryAlreadyExists();

21:   error ControllerAlreadyExists();

22:   error MarketAlreadyExists();

24:   error BorrowerDoesNotExist();

25:   error ControllerFactoryDoesNotExist();

26:   error ControllerDoesNotExist();

27:   error MarketDoesNotExist();
24:   error NotRegisteredBorrower();

25:   error InvalidProtocolFeeConfiguration();

26:   error CallerNotArchControllerOwner();

27:   error InvalidConstraints();

28:   error ControllerAlreadyDeployed();
  • FIFOQueue.sol ( #L17 ):
17:   error FIFOQueueOutOfBounds();
  • LibStoredInitCode.sol ( #L5 ):
5:   error InitCodeDeploymentFailed();
  • MathUtils.sol ( #L19 ):
19:   error MulDivFailed();
16: error InvalidReturnDataString();

17: error InvalidCompactString();

[N‑03] Import declarations should import specific identifiers, rather than the whole file

Using import declarations of the form import {<identifier_name>} from "some/file.sol" avoids polluting the symbol namespace making flattened files smaller, and speeds up compilation (but does not save any gas).

There are 50 instances (click to show):
  • WildcatArchController.sol ( #L5, #L6 ):
5: import 'solady/auth/Ownable.sol';

6: import './libraries/MathUtils.sol';
5: import 'solady/utils/SafeTransferLib.sol';

6: import './market/WildcatMarket.sol';

7: import './interfaces/IWildcatArchController.sol';

8: import './interfaces/IWildcatMarketControllerEventsAndErrors.sol';

9: import './interfaces/IWildcatMarketControllerFactory.sol';

10: import './libraries/LibStoredInitCode.sol';

11: import './libraries/MathUtils.sol';
5: import './interfaces/WildcatStructsAndEnums.sol';

6: import './interfaces/IWildcatMarketController.sol';

7: import './interfaces/IWildcatArchController.sol';

8: import './libraries/LibStoredInitCode.sol';

9: import './libraries/MathUtils.sol';

10: import './market/WildcatMarket.sol';

11: import './WildcatMarketController.sol';
  • Chainalysis.sol ( #L4 ):
4: import '../interfaces/IChainalysisSanctionsList.sol';
4: import './MathUtils.sol';

5: import './SafeCastLib.sol';

6: import './MarketState.sol';
5: import './MathUtils.sol';

6: import './SafeCastLib.sol';

7: import './FeeMath.sol';
  • MathUtils.sol ( #L4 ):
4: import './Errors.sol';
  • SafeCastLib.sol ( #L4 ):
4: import './Errors.sol';
  • Withdrawal.sol ( #L4, #L5 ):
4: import './MarketState.sol';

5: import './FIFOQueue.sol';
4: import '../libraries/FeeMath.sol';

5: import './WildcatMarketBase.sol';

6: import './WildcatMarketConfig.sol';

7: import './WildcatMarketToken.sol';

8: import './WildcatMarketWithdrawals.sol';
4: import '../libraries/FeeMath.sol';

5: import '../libraries/Withdrawal.sol';

7: import '../interfaces/IMarketEventsAndErrors.sol';

8: import '../interfaces/IWildcatMarketController.sol';

9: import '../interfaces/IWildcatSanctionsSentinel.sol';

11: import '../ReentrancyGuard.sol';

12: import '../libraries/BoolUtils.sol';
4: import '../interfaces/IWildcatSanctionsSentinel.sol';

5: import '../libraries/FeeMath.sol';

6: import '../libraries/SafeCastLib.sol';

7: import './WildcatMarketBase.sol';
  • WildcatMarketToken.sol ( #L4 ):
4: import './WildcatMarketBase.sol';
4: import './WildcatMarketBase.sol';

5: import '../libraries/MarketState.sol';

6: import '../libraries/FeeMath.sol';

7: import '../libraries/FIFOQueue.sol';

8: import '../interfaces/IWildcatSanctionsSentinel.sol';

9: import 'solady/utils/SafeTransferLib.sol';

[N‑04] Too long functions should be refactored

Functions with too many lines are difficult to understand. It is recommended to refactor complex functions into multiple shorter and easier to understand functions.

There are 4 instances:

  • WildcatMarketController.sol ( #L291 ):
/// @audit 70 lines
291:   function deployMarket(
  • StringQuery.sol ( #L33 ):
/// @audit 62 lines
33: function queryStringOrBytes32AsString(
  • WildcatMarketBase.sol ( #L76 ):
/// @audit 50 lines
76:   constructor() {
  • WildcatMarketWithdrawals.sol ( #L137 ):
/// @audit 52 lines
137:   function executeWithdrawal(

[N‑05] There is no need to initialize variables with 0

Since the variables are automatically set to 0 when created, it is redundant to initialize it with 0 again.

There are 12 instances (click to show):
93:     for (uint256 i = 0; i < count; i++) {

136:     for (uint256 i = 0; i < count; i++) {

179:     for (uint256 i = 0; i < count; i++) {

222:     for (uint256 i = 0; i < count; i++) {
133:     for (uint256 i = 0; i < count; i++) {

154:     for (uint256 i = 0; i < lenders.length; i++) {

170:     for (uint256 i = 0; i < lenders.length; i++) {

212:     for (uint256 i = 0; i < count; i++) {
  • WildcatMarketControllerFactory.sol ( #L146 ):
146:     for (uint256 i = 0; i < count; i++) {
  • Errors.sol ( #L4 ):
4: uint256 constant Panic_CompilerPanic = 0x00;
48:     for (uint256 i = 0; i < len; i++) {

75:     for (uint256 i = 0; i < n; i++) {

[N‑06] NatSpec documentation for contract is missing

e.g. @dev or @notice, and it must appear above the contract definition braces in order to be identified by the compiler as NatSpec.

There are 18 instances (click to show):
  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
13: contract WildcatMarketControllerFactory {
  • WildcatSanctionsEscrow.sol ( #L10 ):
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {
  • WildcatSanctionsSentinel.sol ( #L10 ):
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {
  • BoolUtils.sol ( #L4 ):
4: library BoolUtils {
  • FIFOQueue.sol ( #L16 ):
16: library FIFOQueueLib {
  • FeeMath.sol ( #L11 ):
11: library FeeMath {
  • LibStoredInitCode.sol ( #L4 ):
4: library LibStoredInitCode {
  • MarketState.sol ( #L44 ):
44: library MarketStateLib {
  • MathUtils.sol ( #L16 ):
16: library MathUtils {
  • SafeCastLib.sol ( #L6 ):
6: library SafeCastLib {
  • Withdrawal.sol ( #L37 ):
37: library WithdrawalLib {
  • WildcatMarket.sol ( #L10 ):
10: contract WildcatMarket is
  • WildcatMarketBase.sol ( #L14 ):
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketToken.sol ( #L6 ):
6: contract WildcatMarketToken is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[N‑07] Names of constants should use the UPPER_CASE_WITH_UNDERSCORES style

It is recommended by the Solidity Style Guide

There are 25 instances (click to show):
  • WildcatSanctionsSentinel.sol ( #L11-L12 ):
11:   bytes32 public constant override WildcatSanctionsEscrowInitcodeHash =
12:     keccak256(type(WildcatSanctionsEscrow).creationCode);
6: IChainalysisSanctionsList constant SanctionsList = IChainalysisSanctionsList(
7:   0x40C57923924B5c5c5455c48D93317139ADDaC8fb
8: );
4: uint256 constant Panic_CompilerPanic = 0x00;

5: uint256 constant Panic_AssertFalse = 0x01;

6: uint256 constant Panic_Arithmetic = 0x11;

7: uint256 constant Panic_DivideByZero = 0x12;

8: uint256 constant Panic_InvalidEnumValue = 0x21;

9: uint256 constant Panic_InvalidStorageByteArray = 0x22;

10: uint256 constant Panic_EmptyArrayPop = 0x31;

11: uint256 constant Panic_ArrayOutOfBounds = 0x32;

12: uint256 constant Panic_MemoryTooLarge = 0x41;

13: uint256 constant Panic_UninitializedFunctionPointer = 0x51;

15: uint256 constant Panic_ErrorSelector = 0x4e487b71;

16: uint256 constant Panic_ErrorCodePointer = 0x20;

17: uint256 constant Panic_ErrorLength = 0x24;

18: uint256 constant Error_SelectorPointer = 0x1c;
8: uint256 constant InvalidReturnDataString_selector = (
9:   0x4cb9c00000000000000000000000000000000000000000000000000000000000
10: );

12: uint256 constant SixtyThreeBytes = 0x3f;

13: uint256 constant ThirtyOneBytes = 0x1f;

14: uint256 constant OnlyFullWordMask = 0xffffffe0;

106: uint256 constant UnknownNameQueryError_selector = (
107:   0xed3df7ad00000000000000000000000000000000000000000000000000000000
108: );

109: uint256 constant UnknownSymbolQueryError_selector = (
110:   0x89ff815700000000000000000000000000000000000000000000000000000000
111: );

112: uint256 constant NameFunction_selector = (
113:   0x06fdde0300000000000000000000000000000000000000000000000000000000
114: );

115: uint256 constant SymbolFunction_selector = (
116:   0x95d89b4100000000000000000000000000000000000000000000000000000000
117: );
  • WildcatMarketBase.sol ( #L24 ):
24:   string public constant version = '1.0';

[N‑08] Names of private/internal functions should be prefixed with an underscore

It is recommended by the Solidity Style Guide.

There are 80 instances (click to show):
394:   function enforceParameterConstraints(
395:     string memory namePrefix,
396:     string memory symbolPrefix,
397:     uint16 annualInterestBips,
398:     uint16 delinquencyFeeBips,
399:     uint32 withdrawalBatchDuration,
400:     uint16 reserveRatioBips,
401:     uint32 delinquencyGracePeriod
402:   ) internal view virtual {

503:   function assertValueInRange(
504:     uint256 value,
505:     uint256 min,
506:     uint256 max,
507:     bytes4 errorSelector
508:   ) internal pure {
5:   function and(bool a, bool b) internal pure returns (bool c) {

11:   function or(bool a, bool b) internal pure returns (bool c) {

17:   function xor(bool a, bool b) internal pure returns (bool c) {
19:   function empty(FIFOQueue storage arr) internal view returns (bool) {

23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

55:   function push(FIFOQueue storage arr, uint32 value) internal {

61:   function shift(FIFOQueue storage arr) internal {

70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
19:   function calculateLinearInterestFromBips(
20:     uint256 rateBip,
21:     uint256 timeDelta
22:   ) internal pure returns (uint256 result) {

30:   function calculateBaseInterest(
31:     MarketState memory state,
32:     uint256 timestamp
33:   ) internal pure returns (uint256 baseInterestRay) {

40:   function applyProtocolFee(
41:     MarketState memory state,
42:     uint256 baseInterestRay,
43:     uint256 protocolFeeBips
44:   ) internal pure returns (uint256 protocolFee) {

53:   function updateDelinquency(
54:     MarketState memory state,
55:     uint256 timestamp,
56:     uint256 delinquencyFeeBips,
57:     uint256 delinquencyGracePeriod
58:   ) internal pure returns (uint256 delinquencyFeeRay) {

89:   function updateTimeDelinquentAndGetPenaltyTime(
90:     MarketState memory state,
91:     uint256 delinquencyGracePeriod,
92:     uint256 timeDelta
93:   ) internal pure returns (uint256 /* timeWithPenalty */) {

142:   function updateScaleFactorAndFees(
143:     MarketState memory state,
144:     uint256 protocolFeeBips,
145:     uint256 delinquencyFeeBips,
146:     uint256 delinquencyGracePeriod,
147:     uint256 timestamp
148:   )
149:     internal
150:     pure
151:     returns (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee)
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

48:   function getCreate2Prefix(address deployer) internal pure returns (uint256 create2Prefix) {

54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {

83:   function createWithStoredInitCode(address initCodeStorage) internal returns (address deployment) {

87:   function createWithStoredInitCode(
88:     address initCodeStorage,
89:     uint256 value
90:   ) internal returns (address deployment) {

99:   function create2WithStoredInitCode(
100:     address initCodeStorage,
101:     bytes32 salt
102:   ) internal returns (address deployment) {

106:   function create2WithStoredInitCode(
107:     address initCodeStorage,
108:     bytes32 salt,
109:     uint256 value
110:   ) internal returns (address deployment) {
51:   function totalSupply(MarketState memory state) internal pure returns (uint256) {

59:   function maximumDeposit(MarketState memory state) internal pure returns (uint256) {

66:   function normalizeAmount(
67:     MarketState memory state,
68:     uint256 amount
69:   ) internal pure returns (uint256) {

76:   function scaleAmount(MarketState memory state, uint256 amount) internal pure returns (uint256) {

87:   function liquidityRequired(
88:     MarketState memory state
89:   ) internal pure returns (uint256 _liquidityRequired) {

105:   function withdrawableProtocolFees(
106:     MarketState memory state,
107:     uint256 totalAssets
108:   ) internal pure returns (uint128) {

123:   function borrowableAssets(
124:     MarketState memory state,
125:     uint256 totalAssets
126:   ) internal pure returns (uint256) {

130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
30:   function calculateLinearInterestFromBips(
31:     uint256 rateBip,
32:     uint256 timeDelta
33:   ) internal pure returns (uint256 result) {

44:   function min(uint256 a, uint256 b) internal pure returns (uint256 c) {

51:   function max(uint256 a, uint256 b) internal pure returns (uint256 c) {

59:   function satSub(uint256 a, uint256 b) internal pure returns (uint256 c) {

71:   function ternary(
72:     bool condition,
73:     uint256 valueIfTrue,
74:     uint256 valueIfFalse
75:   ) internal pure returns (uint256 c) {

85:   function bipMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

105:   function bipDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

121:   function bipToRay(uint256 a) internal pure returns (uint256 b) {

138:   function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

155:   function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

173:   function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {

191:   function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {

47:   function availableLiquidityForPendingBatch(
48:     WithdrawalBatch memory batch,
49:     MarketState memory state,
50:     uint256 totalAssets
51:   ) internal pure returns (uint256) {

[N‑09] Names of private/internal state variables should be prefixed with an underscore

It is recommended by the Solidity Style Guide.

There are 23 instances (click to show):
53:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

55:   uint32 internal immutable MinimumDelinquencyGracePeriod;

56:   uint32 internal immutable MaximumDelinquencyGracePeriod;

58:   uint16 internal immutable MinimumReserveRatioBips;

59:   uint16 internal immutable MaximumReserveRatioBips;

61:   uint16 internal immutable MinimumDelinquencyFeeBips;

62:   uint16 internal immutable MaximumDelinquencyFeeBips;

64:   uint32 internal immutable MinimumWithdrawalBatchDuration;

65:   uint32 internal immutable MaximumWithdrawalBatchDuration;

67:   uint16 internal immutable MinimumAnnualInterestBips;

68:   uint16 internal immutable MaximumAnnualInterestBips;
44:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

46:   uint32 internal immutable MinimumDelinquencyGracePeriod;

47:   uint32 internal immutable MaximumDelinquencyGracePeriod;

49:   uint16 internal immutable MinimumReserveRatioBips;

50:   uint16 internal immutable MaximumReserveRatioBips;

52:   uint16 internal immutable MinimumDelinquencyFeeBips;

53:   uint16 internal immutable MaximumDelinquencyFeeBips;

55:   uint32 internal immutable MinimumWithdrawalBatchDuration;

56:   uint32 internal immutable MaximumWithdrawalBatchDuration;

58:   uint16 internal immutable MinimumAnnualInterestBips;

59:   uint16 internal immutable MaximumAnnualInterestBips;
  • WildcatSanctionsEscrow.sol ( #L14 ):
14:   address internal immutable asset;

[N‑10] Order of functions does not follow the Solidity Style Guide

According to the Solidity Style Guide, functions should be grouped according to their visibility and ordered: constructor, receive, fallback, external, public, internal, private. Within a grouping, place the view and pure functions last.

There are 43 instances (click to show):
/// @audit ↓↓ Out of order with `registerControllerFactory()`
98:   function getRegisteredBorrowersCount() external view returns (uint256) {

/// @audit ↑↑ Out of order with `getRegisteredBorrowersCount()`
106:   function registerControllerFactory(address factory) external onlyOwner {

/// @audit ↑↑ Out of order with `registerControllerFactory()`
113:   function removeControllerFactory(address factory) external onlyOwner {

/// @audit ↓↓ Out of order with `registerController()`
141:   function getRegisteredControllerFactoriesCount() external view returns (uint256) {

/// @audit ↑↑ Out of order with `getRegisteredControllerFactoriesCount()`
149:   function registerController(address controller) external onlyControllerFactory {

/// @audit ↑↑ Out of order with `registerController()`
156:   function removeController(address controller) external onlyOwner {

/// @audit ↓↓ Out of order with `registerMarket()`
184:   function getRegisteredControllersCount() external view returns (uint256) {

/// @audit ↑↑ Out of order with `getRegisteredControllersCount()`
192:   function registerMarket(address market) external onlyController {

/// @audit ↑↑ Out of order with `registerMarket()`
199:   function removeMarket(address market) external onlyOwner {
/// @audit ↓↓ Out of order with `authorizeLenders()`
142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

/// @audit ↑↑ Out of order with `isAuthorizedLender()`
153:   function authorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit ↑↑ Out of order with `authorizeLenders()`
169:   function deauthorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit ↑↑ Out of order with `deauthorizeLenders()`
182:   function updateLenderAuthorization(address lender, address[] memory markets) external {

/// @audit ↓↓ Out of order with `deployMarket()`
255:   function _resetTmpMarketParameters() internal {

/// @audit ↑↑ Out of order with `_resetTmpMarketParameters()`
291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {

/// @audit ↓↓ Out of order with `getParameterConstraints()`
394:   function enforceParameterConstraints(
395:     string memory namePrefix,
396:     string memory symbolPrefix,
397:     uint16 annualInterestBips,
398:     uint16 delinquencyFeeBips,
399:     uint32 withdrawalBatchDuration,
400:     uint16 reserveRatioBips,
401:     uint32 delinquencyGracePeriod
402:   ) internal view virtual {

/// @audit ↑↑ Out of order with `enforceParameterConstraints()`
446:   function getParameterConstraints()
447:     external
448:     view
449:     returns (MarketParameterConstraints memory constraints)

/// @audit ↑↑ Out of order with `getParameterConstraints()`
468:   function setAnnualInterestBips(
469:     address market,
470:     uint16 annualInterestBips
471:   ) external virtual onlyBorrower onlyControlledMarket(market) {

/// @audit ↑↑ Out of order with `setAnnualInterestBips()`
490:   function resetReserveRatio(address market) external virtual {
/// @audit ↓↓ Out of order with `isDeployedController()`
116:   function _storeMarketInitCode()
117:     internal
118:     virtual
119:     returns (address initCodeStorage, uint256 initCodeHash)

/// @audit ↑↑ Out of order with `_storeMarketInitCode()`
126:   function isDeployedController(address controller) external view returns (bool) {

/// @audit ↓↓ Out of order with `setProtocolFeeConfiguration()`
165:   function getProtocolFeeConfiguration()
166:     external
167:     view
168:     returns (

/// @audit ↑↑ Out of order with `getProtocolFeeConfiguration()`
195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {

/// @audit ↓↓ Out of order with `deployControllerAndMarket()`
282:   function deployController() public returns (address controller) {

/// @audit ↑↑ Out of order with `deployController()`
317:   function deployControllerAndMarket(
318:     string memory namePrefix,
319:     string memory symbolPrefix,
320:     address asset,
321:     uint128 maxTotalSupply,
322:     uint16 annualInterestBips,
323:     uint16 delinquencyFeeBips,
324:     uint32 withdrawalBatchDuration,
325:     uint16 reserveRatioBips,
326:     uint32 delinquencyGracePeriod
327:   ) external returns (address controller, address market) {
  • WildcatSanctionsEscrow.sol ( #L29, #L33 ):
/// @audit ↓↓ Out of order with `releaseEscrow()`
29:   function escrowedAsset() public view override returns (address, uint256) {

/// @audit ↑↑ Out of order with `escrowedAsset()`
33:   function releaseEscrow() public override {
/// @audit ↓↓ Out of order with `isSanctioned()`
30:   function _resetTmpEscrowParams() internal {

/// @audit ↑↑ Out of order with `_resetTmpEscrowParams()`
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

/// @audit ↓↓ Out of order with `createEscrow()`
65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {

/// @audit ↑↑ Out of order with `getEscrowAddress()`
95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {
/// @audit ↓↓ Out of order with `push()`
42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

/// @audit ↑↑ Out of order with `values()`
55:   function push(FIFOQueue storage arr, uint32 value) internal {

/// @audit ↑↑ Out of order with `push()`
61:   function shift(FIFOQueue storage arr) internal {

/// @audit ↑↑ Out of order with `shift()`
70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
/// @audit ↓↓ Out of order with `createWithStoredInitCode()`
54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {

/// @audit ↑↑ Out of order with `calculateCreate2Address()`
83:   function createWithStoredInitCode(address initCodeStorage) internal returns (address deployment) {

/// @audit ↑↑ Out of order with `createWithStoredInitCode()`
87:   function createWithStoredInitCode(
88:     address initCodeStorage,
89:     uint256 value
90:   ) internal returns (address deployment) {

/// @audit ↑↑ Out of order with `createWithStoredInitCode()`
99:   function create2WithStoredInitCode(
100:     address initCodeStorage,
101:     bytes32 salt
102:   ) internal returns (address deployment) {

/// @audit ↑↑ Out of order with `create2WithStoredInitCode()`
106:   function create2WithStoredInitCode(
107:     address initCodeStorage,
108:     bytes32 salt,
109:     uint256 value
110:   ) internal returns (address deployment) {
/// @audit ↓↓ Out of order with `toUint8()`
7:   function _assertNonOverflow(bool didNotOverflow) private pure {

/// @audit ↑↑ Out of order with `_assertNonOverflow()`
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {
/// @audit ↓↓ Out of order with `deposit()`
42:   function depositUpTo(
43:     uint256 amount
44:   ) public virtual nonReentrant returns (uint256 /* actualAmount */) {

/// @audit ↑↑ Out of order with `depositUpTo()`
86:   function deposit(uint256 amount) external virtual {
/// @audit ↓↓ Out of order with `coverageLiquidity()`
197:   function _getAccountWithRole(
198:     address accountAddress,
199:     AuthRole requiredRole
200:   ) internal returns (Account memory account) {

/// @audit ↑↑ Out of order with `_getAccountWithRole()`
223:   function coverageLiquidity() external view nonReentrantView returns (uint256) {

/// @audit ↓↓ Out of order with `borrowableAssets()`
238:   function totalAssets() public view returns (uint256) {

/// @audit ↑↑ Out of order with `totalAssets()`
252:   function borrowableAssets() external view nonReentrantView returns (uint256) {

/// @audit ↓↓ Out of order with `scaledTotalSupply()`
276:   function currentState() public view nonReentrantView returns (MarketState memory state) {

/// @audit ↑↑ Out of order with `currentState()`
285:   function scaledTotalSupply() external view nonReentrantView returns (uint256) {

/// @audit ↓↓ Out of order with `_writeState()`
399:   function _calculateCurrentState()
400:     internal
401:     view
402:     returns (

/// @audit ↑↑ Out of order with `_calculateCurrentState()`
448:   function _writeState(MarketState memory state) internal {

/// @audit ↑↑ Out of order with `_writeState()`
466:   function _processExpiredWithdrawalBatch(MarketState memory state) internal {

/// @audit ↑↑ Out of order with `_processExpiredWithdrawalBatch()`
498:   function _applyWithdrawalBatchPayment(
499:     WithdrawalBatch memory batch,
500:     MarketState memory state,
501:     uint32 expiry,
502:     uint256 availableLiquidity
503:   ) internal {
/// @audit ↓↓ Out of order with `nukeFromOrbit()`
42:   function reserveRatioBips() external view returns (uint256) {

/// @audit ↑↑ Out of order with `reserveRatioBips()`
74:   function nukeFromOrbit(address accountAddress) external nonReentrant {

/// @audit ↑↑ Out of order with `nukeFromOrbit()`
88:   function stunningReversal(address accountAddress) external nonReentrant {

/// @audit ↑↑ Out of order with `stunningReversal()`
112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {

/// @audit ↑↑ Out of order with `updateAccountAuthorization()`
134:   function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyController nonReentrant {

/// @audit ↓↓ Out of order with `setReserveRatioBips()`
149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

/// @audit ↑↑ Out of order with `setAnnualInterestBips()`
171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {
  • WildcatMarketToken.sol ( #L16, #L22 ):
/// @audit ↓↓ Out of order with `totalSupply()`
16:   function balanceOf(address account) public view virtual nonReentrantView returns (uint256) {

/// @audit ↑↑ Out of order with `balanceOf()`
22:   function totalSupply() external view virtual nonReentrantView returns (uint256) {
/// @audit ↓↓ Out of order with `queueWithdrawal()`
45:   function getAvailableWithdrawalAmount(
46:     address accountAddress,
47:     uint32 expiry
48:   ) external view nonReentrantView returns (uint256) {

/// @audit ↑↑ Out of order with `getAvailableWithdrawalAmount()`
77:   function queueWithdrawal(uint256 amount) external nonReentrant {

/// @audit ↑↑ Out of order with `queueWithdrawal()`
137:   function executeWithdrawal(
138:     address accountAddress,
139:     uint32 expiry
140:   ) external nonReentrant returns (uint256) {

/// @audit ↑↑ Out of order with `executeWithdrawal()`
190:   function processUnpaidWithdrawalBatch() external nonReentrant {

[N‑11] The nonReentrant modifier should occur before all other modifiers

This is a best-practice to protect against reentrancy in other modifiers

There are 6 instances:

119:   function borrow(uint256 amount) external onlyBorrower nonReentrant {

142:   function closeMarket() external onlyController nonReentrant {
112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {

134:   function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyController nonReentrant {

149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {

[N‑12] Redundant inheritance specifier

The contracts below already extend the specified contract, so there is no need to list it in the inheritance list again.

There is 1 instance:

/// @audit WildcatMarketConfig already extends WildcatMarketBase
10: contract WildcatMarket is
11:   WildcatMarketBase,
12:   WildcatMarketConfig,
13:   WildcatMarketToken,
14:   WildcatMarketWithdrawals

[N‑13] Strings should use double quotes rather than single quotes

It is recommended by the Solidity Style Guide

There are 3 instances:

257:     _tmpMarketParameters.namePrefix = '_';

258:     _tmpMarketParameters.symbolPrefix = '_';
  • WildcatMarketBase.sol ( #L24 ):
24:   string public constant version = '1.0';

[N‑14] Use of override is unnecessary

Starting from Solidity 0.8.8, the override keyword is not required when overriding an interface function, except for the case where the function is defined in multiple bases.

There are 9 instances (click to show):
21:   function balance() public view override returns (uint256) {

25:   function canReleaseEscrow() public view override returns (bool) {

29:   function escrowedAsset() public view override returns (address, uint256) {

33:   function releaseEscrow() public override {
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

48:   function overrideSanction(address account) public override {

56:   function removeSanctionOverride(address account) public override {

65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {

95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {

[N‑15] Unused errors

The following errors are defined but not used. It is recommended to check the code for logical omissions that cause them not to be used. If it's determined that they are not needed anywhere, it's best to remove them from the codebase to improve code clarity and minimize confusion. Note that there may be cases where an error appears to be used because it has multiple definitions in different files. In such cases, the definitions should be moved to a separate file.

There are 4 instances:

  • LibStoredInitCode.sol ( #L5 ):
5:   error InitCodeDeploymentFailed();
  • MathUtils.sol ( #L19 ):
19:   error MulDivFailed();
16: error InvalidReturnDataString();

17: error InvalidCompactString();

[N‑16] Unused events

The following events are defined but not used. It is recommended to check the code for logical omissions that cause them not to be used. If it's determined that they are not needed anywhere, it's best to remove them from the codebase to improve code clarity and minimize confusion. Note that there may be cases where an event appears to be used because it has multiple definitions in different files. In such cases, the definitions should be moved to a separate file.

There are 2 instances:

16:   event NewController(address borrower, address controller, string namePrefix, string symbolPrefix);

17:   event UpdateProtocolFeeConfiguration(
18:     address feeRecipient,
19:     uint16 protocolFeeBips,
20:     address originationFeeAsset,
21:     uint256 originationFeeAmount
22:   );

[N‑17] Large numeric literals should use underscores for readability

Large hardcoded numbers in code can be difficult to read. Consider using underscores for number literals to improve readability (e.g 1234567 -> 1_234_567). Consider using an underscore for every third digit from the right.

There are 4 instances:

  • WildcatMarketController.sol ( #L481 ):
481:         WildcatMarket(market).setReserveRatioBips(9000);
81:       constraints.maximumAnnualInterestBips > 10000 ||

83:       constraints.maximumDelinquencyFeeBips > 10000 ||

85:       constraints.maximumReserveRatioBips > 10000 ||

[N‑18] Assembly blocks should have extensive comments

Assembly blocks take a lot more time to audit than normal Solidity code, and often have gotchas and side-effects that the Solidity versions of the same code do not. Consider adding more comments explaining what is being done in every step of the assembly code, and describe why assembly is being used instead of Solidity.

There are 21 instances (click to show):
403:     assembly {
404:       if or(iszero(mload(namePrefix)), iszero(mload(symbolPrefix))) {
405:         // revert EmptyString();
406:         mstore(0x00, 0xecd7b0d1)
407:         revert(0x1c, 0x04)
408:       }
409:     }

509:     assembly {
510:       if or(lt(value, min), gt(value, max)) {
511:         mstore(0, errorSelector)
512:         revert(0, 4)
513:       }
514:     }
6:     assembly {
7:       c := and(a, b)
8:     }

12:     assembly {
13:       c := or(a, b)
14:     }

18:     assembly {
19:       c := xor(a, b)
20:     }
25:   assembly {
26:     mstore(0, errorSelector)
27:     revert(0, 4)
28:   }

36:   assembly {
37:     mstore(0, errorSelector)
38:     revert(Error_SelectorPointer, 4)
39:   }

48:   assembly {
49:     mstore(0, errorSelector)
50:     mstore(4, argument)
51:     revert(0, 0x24)
52:   }

61:   assembly {
62:     mstore(0, errorSelector)
63:     mstore(0x20, argument)
64:     revert(Error_SelectorPointer, 0x24)
65:   }
49:     assembly {
50:       create2Prefix := or(deployer, 0xff0000000000000000000000000000000000000000)
51:     }

91:     assembly {
92:       let initCodePointer := mload(0x40)
93:       let initCodeSize := sub(extcodesize(initCodeStorage), 1)
94:       extcodecopy(initCodeStorage, initCodePointer, 1, initCodeSize)
95:       deployment := create(value, initCodePointer, initCodeSize)
96:     }

111:     assembly {
112:       let initCodePointer := mload(0x40)
113:       let initCodeSize := sub(extcodesize(initCodeStorage), 1)
114:       extcodecopy(initCodeStorage, initCodePointer, 1, initCodeSize)
115:       deployment := create2(value, initCodePointer, initCodeSize, salt)
116:     }
76:     assembly {
77:       c := add(valueIfFalse, mul(condition, sub(valueIfTrue, valueIfFalse)))
78:     }

106:     assembly {
107:       // equivalent to `require(b != 0 && a <= (type(uint256).max - b/2) / BIP)`
108:       if or(iszero(b), gt(a, div(sub(not(0), div(b, 2)), BIP))) {
109:         mstore(0, Panic_ErrorSelector)
110:         mstore(Panic_ErrorCodePointer, Panic_Arithmetic)
111:         revert(Error_SelectorPointer, Panic_ErrorLength)
112:       }
113: 
114:       c := div(add(mul(a, BIP), div(b, 2)), b)
115:     }

123:     assembly {
124:       b := mul(a, BIP_RAY_RATIO)
125:       // equivalent to `require((b = a * BIP_RAY_RATIO) / BIP_RAY_RATIO == a )
126:       if iszero(eq(div(b, BIP_RAY_RATIO), a)) {
127:         mstore(0, Panic_ErrorSelector)
128:         mstore(Panic_ErrorCodePointer, Panic_Arithmetic)
129:         revert(Error_SelectorPointer, Panic_ErrorLength)
130:       }
131:     }

139:     assembly {
140:       // equivalent to `require(b == 0 || a <= (type(uint256).max - HALF_RAY) / b)`
141:       if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_RAY), b))))) {
142:         mstore(0, Panic_ErrorSelector)
143:         mstore(Panic_ErrorCodePointer, Panic_Arithmetic)
144:         revert(Error_SelectorPointer, Panic_ErrorLength)
145:       }
146: 
147:       c := div(add(mul(a, b), HALF_RAY), RAY)
148:     }

156:     assembly {
157:       // equivalent to `require(b != 0 && a <= (type(uint256).max - halfB) / RAY)`
158:       if or(iszero(b), gt(a, div(sub(not(0), div(b, 2)), RAY))) {
159:         mstore(0, Panic_ErrorSelector)
160:         mstore(Panic_ErrorCodePointer, Panic_Arithmetic)
161:         revert(Error_SelectorPointer, Panic_ErrorLength)
162:       }
163: 
164:       c := div(add(mul(a, RAY), div(b, 2)), b)
165:     }
8:     assembly {
9:       if iszero(didNotOverflow) {
10:         mstore(0, Panic_ErrorSelector)
11:         mstore(Panic_ErrorCodePointer, Panic_Arithmetic)
12:         revert(Error_SelectorPointer, Panic_ErrorLength)
13:       }
14:     }
25:   assembly {
26:     str := mload(0x40)
27:     mstore(0x40, add(str, 0x40))
28:     mstore(str, size)
29:     mstore(add(str, 0x20), value)
30:   }

66:     assembly {
67:       returndatacopy(0x00, 0x00, 0x20)
68:       value := mload(0)
69:     }

75:     assembly {
76:       str := mload(0x40)
77:       mstore(0x40, add(str, 0x40))
78:       mstore(str, size)
79:       mstore(add(str, 0x20), value)
80:     }

[N‑19] Complex casting

Consider whether the number of casts is really necessary, or whether using a different type would be more appropriate. Alternatively, add comments to explain in detail why the casts are necessary, and any implicit reasons why the cast does not introduce an overflow.

There are 6 instances (click to show):
288:     bytes32 salt = bytes32(uint256(uint160(msg.sender)));

288:     bytes32 salt = bytes32(uint256(uint160(msg.sender)));

344:     bytes32 salt = bytes32(uint256(uint160(borrower)));

344:     bytes32 salt = bytes32(uint256(uint160(borrower)));
71:       address(
72:         uint160(
73:           uint256(
74:             keccak256(
75:               abi.encodePacked(
76:                 bytes1(0xff),
77:                 address(this),
78:                 keccak256(abi.encode(borrower, account, asset)),
79:                 WildcatSanctionsEscrowInitcodeHash
80:               )
81:             )
82:           )
83:         )

72:         uint160(
73:           uint256(
74:             keccak256(
75:               abi.encodePacked(
76:                 bytes1(0xff),
77:                 address(this),
78:                 keccak256(abi.encode(borrower, account, asset)),
79:                 WildcatSanctionsEscrowInitcodeHash
80:               )
81:             )
82:           )

[N‑20] Constants/Immutables redefined elsewhere

Consider defining in only one contract so that values cannot become out of sync when only one location is updated. A cheap way to store constants/immutables in a single location is to create an internal constant in a library. If the variable is a local cache of another contract's value, consider making the cache variable internal or private, which will require external users to query the contract with the source of truth, so that callers don't get out of sync.

There are 38 instances (click to show):
/// @audit Seen in src/WildcatMarketControllerFactory.sol#31
41:   IWildcatArchController public immutable archController;

/// @audit Seen in src/market/WildcatMarketBase.sol#30
45:   address public immutable borrower;

/// @audit Seen in src/market/WildcatMarketBase.sol#27
47:   address public immutable sentinel;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#36
49:   address public immutable marketInitCodeStorage;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#38
51:   uint256 public immutable marketInitCodeHash;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#44
53:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

/// @audit Seen in src/WildcatMarketControllerFactory.sol#46
55:   uint32 internal immutable MinimumDelinquencyGracePeriod;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#47
56:   uint32 internal immutable MaximumDelinquencyGracePeriod;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#49
58:   uint16 internal immutable MinimumReserveRatioBips;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#50
59:   uint16 internal immutable MaximumReserveRatioBips;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#52
61:   uint16 internal immutable MinimumDelinquencyFeeBips;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#53
62:   uint16 internal immutable MaximumDelinquencyFeeBips;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#55
64:   uint32 internal immutable MinimumWithdrawalBatchDuration;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#56
65:   uint32 internal immutable MaximumWithdrawalBatchDuration;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#58
67:   uint16 internal immutable MinimumAnnualInterestBips;

/// @audit Seen in src/WildcatMarketControllerFactory.sol#59
68:   uint16 internal immutable MaximumAnnualInterestBips;
/// @audit Seen in src/WildcatMarketController.sol#41
31:   IWildcatArchController public immutable archController;

/// @audit Seen in src/market/WildcatMarketBase.sol#27
34:   address public immutable sentinel;

/// @audit Seen in src/WildcatMarketController.sol#49
36:   address public immutable marketInitCodeStorage;

/// @audit Seen in src/WildcatMarketController.sol#51
38:   uint256 public immutable marketInitCodeHash;

/// @audit Seen in src/WildcatMarketController.sol#53
44:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

/// @audit Seen in src/WildcatMarketController.sol#55
46:   uint32 internal immutable MinimumDelinquencyGracePeriod;

/// @audit Seen in src/WildcatMarketController.sol#56
47:   uint32 internal immutable MaximumDelinquencyGracePeriod;

/// @audit Seen in src/WildcatMarketController.sol#58
49:   uint16 internal immutable MinimumReserveRatioBips;

/// @audit Seen in src/WildcatMarketController.sol#59
50:   uint16 internal immutable MaximumReserveRatioBips;

/// @audit Seen in src/WildcatMarketController.sol#61
52:   uint16 internal immutable MinimumDelinquencyFeeBips;

/// @audit Seen in src/WildcatMarketController.sol#62
53:   uint16 internal immutable MaximumDelinquencyFeeBips;

/// @audit Seen in src/WildcatMarketController.sol#64
55:   uint32 internal immutable MinimumWithdrawalBatchDuration;

/// @audit Seen in src/WildcatMarketController.sol#65
56:   uint32 internal immutable MaximumWithdrawalBatchDuration;

/// @audit Seen in src/WildcatMarketController.sol#67
58:   uint16 internal immutable MinimumAnnualInterestBips;

/// @audit Seen in src/WildcatMarketController.sol#68
59:   uint16 internal immutable MaximumAnnualInterestBips;
/// @audit Seen in src/WildcatMarketControllerFactory.sol#34
11:   address public immutable override sentinel;

/// @audit Seen in src/market/WildcatMarketBase.sol#30
12:   address public immutable override borrower;

/// @audit Seen in src/market/WildcatMarketBase.sol#48
14:   address internal immutable asset;
  • WildcatSanctionsSentinel.sol ( #L16 ):
/// @audit Seen in src/WildcatMarketControllerFactory.sol#31
16:   address public immutable override archController;
/// @audit Seen in src/WildcatMarketController.sol#47
27:   address public immutable sentinel;

/// @audit Seen in src/WildcatMarketController.sol#45
30:   address public immutable borrower;

/// @audit Seen in src/WildcatSanctionsEscrow.sol#14
48:   address public immutable asset;

[N‑21] Convert simple if-statements to ternary expressions

Converting some if statements to ternaries (such as z = (a < b) ? x : y) can make the code more concise and easier to read.

There are 2 instances:

118:     if (_isAuthorized) {
119:       account.approval = AuthRole.DepositAndWithdraw;
120:     } else {
121:       account.approval = AuthRole.WithdrawOnly;
122:     }
  • WildcatMarketWithdrawals.sol ( #L54-L58 ):
54:     if (expiry == expiredBatchExpiry) {
55:       batch = expiredBatch;
56:     } else {
57:       batch = _withdrawalData.batches[expiry];
58:     }

[N‑22] Events should be emitted before external calls

Ensure that events follow the best practice of check-effects-interaction, and are emitted before external calls.

There are 11 instances (click to show):
  • WildcatSanctionsEscrow.sol ( #L40 ):
/// @audit `transfer()` is called on line 38
40:     emit EscrowReleased(account, asset, amount);
/// @audit `()` is called on line 110
112:     emit NewSanctionsEscrow(borrower, account, asset);

/// @audit `()` is called on line 110
116:     emit SanctionOverride(borrower, escrowContract);
/// @audit `safeTransferFrom()` is called on line 60
67:     emit Transfer(address(0), msg.sender, amount);

/// @audit `safeTransferFrom()` is called on line 60
68:     emit Deposit(msg.sender, amount, scaledAmount);

/// @audit `safeTransfer()` is called on line 107
108:     emit FeesCollected(withdrawableFees);

/// @audit `safeTransfer()` is called on line 129
130:     emit Borrow(amount);

/// @audit `safeTransferFrom()` is called on line 154
160:     emit MarketClosed(block.timestamp);
/// @audit `createEscrow()` is called on line 172
177:         emit Transfer(accountAddress, escrow, state.normalizeAmount(scaledBalance));

/// @audit `createEscrow()` is called on line 172
179:         emit SanctionedAccountAssetsSentToEscrow(
180:           accountAddress,
181:           escrow,
182:           state.normalizeAmount(scaledBalance)
183:         );
  • WildcatMarketWithdrawals.sol ( #L182 ):
/// @audit `safeTransfer()` is called on line 179
182:     emit WithdrawalExecuted(expiry, accountAddress, normalizedAmountWithdrawn);

[N‑23] Events are emitted without the sender information

When an action is triggered based on a user's action, not being able to filter based on who triggered the action makes event processing a lot more cumbersome. Including the msg.sender the events of these types of action will make events much more useful to end users, especially when msg.sender is not tx.origin.

There are 12 instances (click to show):
157:         emit LenderAuthorized(lender);

173:         emit LenderDeauthorized(lender);
  • WildcatSanctionsEscrow.sol ( #L40 ):
40:     emit EscrowReleased(account, asset, amount);
112:     emit NewSanctionsEscrow(borrower, account, asset);

116:     emit SanctionOverride(borrower, escrowContract);
108:     emit FeesCollected(withdrawableFees);

130:     emit Borrow(amount);
  • WildcatMarketConfig.sol ( #L99 ):
99:     emit AuthorizationStatusUpdated(accountAddress, account.approval);
96:       emit WithdrawalBatchCreated(state.pendingWithdrawalExpiry);

172:       emit SanctionedAccountWithdrawalSentToEscrow(
173:         accountAddress,
174:         escrow,
175:         expiry,
176:         normalizedAmountWithdrawn
177:       );

182:     emit WithdrawalExecuted(expiry, accountAddress, normalizedAmountWithdrawn);

208:       emit WithdrawalBatchClosed(expiry);

[N‑24] Event is missing indexed fields

Index event fields makes the field more quickly accessible to off-chain tools that parse events. However, note that each indexed field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.

There are 10 instances (click to show):
29:   event MarketAdded(address indexed controller, address market);

30:   event MarketRemoved(address market);

32:   event ControllerFactoryAdded(address controllerFactory);

33:   event ControllerFactoryRemoved(address controllerFactory);

35:   event BorrowerAdded(address borrower);

36:   event BorrowerRemoved(address borrower);

38:   event ControllerAdded(address indexed controllerFactory, address controller);

39:   event ControllerRemoved(address controller);
16:   event NewController(address borrower, address controller, string namePrefix, string symbolPrefix);

17:   event UpdateProtocolFeeConfiguration(
18:     address feeRecipient,
19:     uint16 protocolFeeBips,
20:     address originationFeeAsset,
21:     uint256 originationFeeAmount
22:   );

[N‑25] Inconsistent floating version pragma

Source files are using different floating version syntax, this is prone to compilation errors, and is not conducive to the code reliability and maintainability.

There are 2 instances:

  • WildcatMarketController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Chainalysis.sol ( #L2 ):
2: pragma solidity ^0.8.20;

[N‑26] Imports could be organized more systematically

The contract's interface should be imported first, followed by each of the interfaces it uses, followed by all other files. The examples below do not follow this layout.

There are 5 instances (click to show):
  • WildcatMarketController.sol ( #L7 ):
/// @audit Out of order with the prev import️ ⬆
7: import './interfaces/IWildcatArchController.sol';
  • WildcatMarketControllerFactory.sol ( #L6 ):
/// @audit Out of order with the prev import️ ⬆
6: import './interfaces/IWildcatMarketController.sol';
  • WildcatSanctionsEscrow.sol ( #L8 ):
/// @audit Out of order with the prev import️ ⬆
8: import { IWildcatSanctionsEscrow } from './interfaces/IWildcatSanctionsEscrow.sol';
  • WildcatMarketBase.sol ( #L7 ):
/// @audit Out of order with the prev import️ ⬆
7: import '../interfaces/IMarketEventsAndErrors.sol';
  • WildcatMarketWithdrawals.sol ( #L8 ):
/// @audit Out of order with the prev import️ ⬆
8: import '../interfaces/IWildcatSanctionsSentinel.sol';

[N‑27] Imports should use double quotes rather than single quotes

There are 67 instances (click to show):
  • WildcatArchController.sol ( #L4, #L5, #L6 ):
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';

5: import 'solady/auth/Ownable.sol';

6: import './libraries/MathUtils.sol';
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';

5: import 'solady/utils/SafeTransferLib.sol';

6: import './market/WildcatMarket.sol';

7: import './interfaces/IWildcatArchController.sol';

8: import './interfaces/IWildcatMarketControllerEventsAndErrors.sol';

9: import './interfaces/IWildcatMarketControllerFactory.sol';

10: import './libraries/LibStoredInitCode.sol';

11: import './libraries/MathUtils.sol';
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';

5: import './interfaces/WildcatStructsAndEnums.sol';

6: import './interfaces/IWildcatMarketController.sol';

7: import './interfaces/IWildcatArchController.sol';

8: import './libraries/LibStoredInitCode.sol';

9: import './libraries/MathUtils.sol';

10: import './market/WildcatMarket.sol';

11: import './WildcatMarketController.sol';
4: import { IERC20 } from './interfaces/IERC20.sol';

5: import { IChainalysisSanctionsList } from './interfaces/IChainalysisSanctionsList.sol';

6: import { SanctionsList } from './libraries/Chainalysis.sol';

7: import { WildcatSanctionsSentinel } from './WildcatSanctionsSentinel.sol';

8: import { IWildcatSanctionsEscrow } from './interfaces/IWildcatSanctionsEscrow.sol';
4: import { IChainalysisSanctionsList } from './interfaces/IChainalysisSanctionsList.sol';

5: import { IWildcatArchController } from './interfaces/IWildcatArchController.sol';

6: import { IWildcatSanctionsSentinel } from './interfaces/IWildcatSanctionsSentinel.sol';

7: import { SanctionsList } from './libraries/Chainalysis.sol';

8: import { WildcatSanctionsEscrow } from './WildcatSanctionsEscrow.sol';
  • Chainalysis.sol ( #L4 ):
4: import '../interfaces/IChainalysisSanctionsList.sol';
4: import './MathUtils.sol';

5: import './SafeCastLib.sol';

6: import './MarketState.sol';
4: import { AuthRole } from '../interfaces/WildcatStructsAndEnums.sol';

5: import './MathUtils.sol';

6: import './SafeCastLib.sol';

7: import './FeeMath.sol';
  • MathUtils.sol ( #L4 ):
4: import './Errors.sol';
  • SafeCastLib.sol ( #L4 ):
4: import './Errors.sol';
  • StringQuery.sol ( #L4 ):
4: import { LibBit } from 'solady/utils/LibBit.sol';
  • Withdrawal.sol ( #L4, #L5 ):
4: import './MarketState.sol';

5: import './FIFOQueue.sol';
4: import '../libraries/FeeMath.sol';

5: import './WildcatMarketBase.sol';

6: import './WildcatMarketConfig.sol';

7: import './WildcatMarketToken.sol';

8: import './WildcatMarketWithdrawals.sol';
4: import '../libraries/FeeMath.sol';

5: import '../libraries/Withdrawal.sol';

6: import { queryName, querySymbol } from '../libraries/StringQuery.sol';

7: import '../interfaces/IMarketEventsAndErrors.sol';

8: import '../interfaces/IWildcatMarketController.sol';

9: import '../interfaces/IWildcatSanctionsSentinel.sol';

10: import { IERC20, IERC20Metadata } from '../interfaces/IERC20Metadata.sol';

11: import '../ReentrancyGuard.sol';

12: import '../libraries/BoolUtils.sol';
4: import '../interfaces/IWildcatSanctionsSentinel.sol';

5: import '../libraries/FeeMath.sol';

6: import '../libraries/SafeCastLib.sol';

7: import './WildcatMarketBase.sol';
  • WildcatMarketToken.sol ( #L4 ):
4: import './WildcatMarketBase.sol';
4: import './WildcatMarketBase.sol';

5: import '../libraries/MarketState.sol';

6: import '../libraries/FeeMath.sol';

7: import '../libraries/FIFOQueue.sol';

8: import '../interfaces/IWildcatSanctionsSentinel.sol';

9: import 'solady/utils/SafeTransferLib.sol';

[N‑28] @openzeppelin/contracts should be upgraded to a newer version

These contracts import contracts from @openzeppelin/contracts, but they are not using the latest version.

There are 3 instances:

  • WildcatArchController.sol ( #L4 ):
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';
  • WildcatMarketController.sol ( #L4 ):
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';
  • WildcatMarketControllerFactory.sol ( #L4 ):
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';

[N‑29] Magic numbers should be replaced with constants

Magic numbers are hard-coded values in code that can make it difficult for developers and maintainers to understand the code, and can also cause confusion or errors. To improve the readability and maintainability of code, it is recommended to replace magic numbers with constants that have good readability.

There are 5 instances:

  • WildcatMarketController.sol ( #L481 ):
/// @audit 9000
481:         WildcatMarket(market).setReserveRatioBips(9000);
/// @audit 10000
81:       constraints.maximumAnnualInterestBips > 10000 ||

/// @audit 10000
83:       constraints.maximumDelinquencyFeeBips > 10000 ||

/// @audit 10000
85:       constraints.maximumReserveRatioBips > 10000 ||
  • WildcatSanctionsSentinel.sol ( #L76 ):
/// @audit 0xff
76:                 bytes1(0xff),

[N‑30] Expressions for constant values should use immutable rather than constant

While it doesn't save any gas because the compiler knows that developers often make this mistake, it's still best to use the right tool for the task at hand. There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts. constants should be used for literal values written into the code, and immutable variables should be used for expressions, or values calculated in, or passed into the constructor.

There are 7 instances (click to show):
  • WildcatSanctionsSentinel.sol ( #L11-L12 ):
11:   bytes32 public constant override WildcatSanctionsEscrowInitcodeHash =
12:     keccak256(type(WildcatSanctionsEscrow).creationCode);
6: IChainalysisSanctionsList constant SanctionsList = IChainalysisSanctionsList(
7:   0x40C57923924B5c5c5455c48D93317139ADDaC8fb
8: );
8: uint256 constant InvalidReturnDataString_selector = (
9:   0x4cb9c00000000000000000000000000000000000000000000000000000000000
10: );

106: uint256 constant UnknownNameQueryError_selector = (
107:   0xed3df7ad00000000000000000000000000000000000000000000000000000000
108: );

109: uint256 constant UnknownSymbolQueryError_selector = (
110:   0x89ff815700000000000000000000000000000000000000000000000000000000
111: );

112: uint256 constant NameFunction_selector = (
113:   0x06fdde0300000000000000000000000000000000000000000000000000000000
114: );

115: uint256 constant SymbolFunction_selector = (
116:   0x95d89b4100000000000000000000000000000000000000000000000000000000
117: );

[N‑31] Functions not used internally could be marked external

If desired, sub contracts can change the visibility from external to public by overriding the functions of their parents.

There are 9 instances (click to show):
  • WildcatSanctionsEscrow.sol ( #L29, #L33 ):
29:   function escrowedAsset() public view override returns (address, uint256) {

33:   function releaseEscrow() public override {
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

48:   function overrideSanction(address account) public override {

56:   function removeSanctionOverride(address account) public override {

95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {
149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {
  • WildcatMarketToken.sol ( #L16 ):
16:   function balanceOf(address account) public view virtual nonReentrantView returns (uint256) {

[N‑32] Use @inheritdoc for overridden functions

There are 9 instances (click to show):
21:   function balance() public view override returns (uint256) {

25:   function canReleaseEscrow() public view override returns (bool) {

29:   function escrowedAsset() public view override returns (address, uint256) {

33:   function releaseEscrow() public override {
34:   /**
35:    * @dev Returns boolean indicating whether `account` is sanctioned
36:    *      on Chainalysis and that status has not been overridden by
37:    *      `borrower`.
38:    */
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

45:   /**
46:    * @dev Overrides the sanction status of `account` for `borrower`.
47:    */
48:   function overrideSanction(address account) public override {

53:   /**
54:    * @dev Removes the sanction override of `account` for `borrower`.
55:    */
56:   function removeSanctionOverride(address account) public override {

61:   /**
62:    * @dev Calculate the create2 escrow address for the combination
63:    *      of `borrower`, `account`, and `asset`.
64:    */
65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {

87:   /**
88:    * @dev Creates a new WildcatSanctionsEscrow contract for `borrower`,
89:    *      `account`, and `asset` or returns the existing escrow contract
90:    *      if one already exists.
91:    *
92:    *      The escrow contract is added to the set of sanction override
93:    *      addresses for `borrower` so that it can not be blocked.
94:    */
95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {

[N‑33] Contracts should have NatSpec @author tags

There are 18 instances (click to show):
  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
13: contract WildcatMarketControllerFactory {
  • WildcatSanctionsEscrow.sol ( #L10 ):
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {
  • WildcatSanctionsSentinel.sol ( #L10 ):
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {
  • BoolUtils.sol ( #L4 ):
4: library BoolUtils {
  • FIFOQueue.sol ( #L16 ):
16: library FIFOQueueLib {
  • FeeMath.sol ( #L11 ):
11: library FeeMath {
  • LibStoredInitCode.sol ( #L4 ):
4: library LibStoredInitCode {
  • MarketState.sol ( #L44 ):
44: library MarketStateLib {
  • MathUtils.sol ( #L16 ):
16: library MathUtils {
  • SafeCastLib.sol ( #L6 ):
6: library SafeCastLib {
  • Withdrawal.sol ( #L37 ):
37: library WithdrawalLib {
  • WildcatMarket.sol ( #L10 ):
10: contract WildcatMarket is
  • WildcatMarketBase.sol ( #L14 ):
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketToken.sol ( #L6 ):
6: contract WildcatMarketToken is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[N‑34] Contracts should have @notice tags

The @notice is used to explain to users what the contract does. The compiler interprets /// or /** comments as this tag if one wasn't explicitly provided.

There are 18 instances (click to show):
  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
13: contract WildcatMarketControllerFactory {
  • WildcatSanctionsEscrow.sol ( #L10 ):
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {
  • WildcatSanctionsSentinel.sol ( #L10 ):
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {
  • BoolUtils.sol ( #L4 ):
4: library BoolUtils {
  • FIFOQueue.sol ( #L16 ):
16: library FIFOQueueLib {
  • FeeMath.sol ( #L11 ):
11: library FeeMath {
  • LibStoredInitCode.sol ( #L4 ):
4: library LibStoredInitCode {
  • MarketState.sol ( #L44 ):
44: library MarketStateLib {
  • MathUtils.sol ( #L16 ):
16: library MathUtils {
  • SafeCastLib.sol ( #L6 ):
6: library SafeCastLib {
  • Withdrawal.sol ( #L37 ):
37: library WithdrawalLib {
  • WildcatMarket.sol ( #L10 ):
10: contract WildcatMarket is
  • WildcatMarketBase.sol ( #L14 ):
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketToken.sol ( #L6 ):
6: contract WildcatMarketToken is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[N‑35] Contracts should have NatSpec @title tags

Some contract definitions have an incomplete NatSpec: add a @title notation to describe the contract to improve the code documentation.

There are 18 instances (click to show):
  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
13: contract WildcatMarketControllerFactory {
  • WildcatSanctionsEscrow.sol ( #L10 ):
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {
  • WildcatSanctionsSentinel.sol ( #L10 ):
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {
  • BoolUtils.sol ( #L4 ):
4: library BoolUtils {
  • FIFOQueue.sol ( #L16 ):
16: library FIFOQueueLib {
  • FeeMath.sol ( #L11 ):
11: library FeeMath {
  • LibStoredInitCode.sol ( #L4 ):
4: library LibStoredInitCode {
  • MarketState.sol ( #L44 ):
44: library MarketStateLib {
  • MathUtils.sol ( #L16 ):
16: library MathUtils {
  • SafeCastLib.sol ( #L6 ):
6: library SafeCastLib {
  • Withdrawal.sol ( #L37 ):
37: library WithdrawalLib {
  • WildcatMarket.sol ( #L10 ):
10: contract WildcatMarket is
  • WildcatMarketBase.sol ( #L14 ):
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketToken.sol ( #L6 ):
6: contract WildcatMarketToken is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[N‑36] Event declarations should have NatSpec descriptions

There are 10 instances (click to show):
29:   event MarketAdded(address indexed controller, address market);

30:   event MarketRemoved(address market);

32:   event ControllerFactoryAdded(address controllerFactory);

33:   event ControllerFactoryRemoved(address controllerFactory);

35:   event BorrowerAdded(address borrower);

36:   event BorrowerRemoved(address borrower);

38:   event ControllerAdded(address indexed controllerFactory, address controller);

39:   event ControllerRemoved(address controller);
16:   event NewController(address borrower, address controller, string namePrefix, string symbolPrefix);

17:   event UpdateProtocolFeeConfiguration(
18:     address feeRecipient,
19:     uint16 protocolFeeBips,
20:     address originationFeeAsset,
21:     uint256 originationFeeAmount
22:   );

[N‑37] NatSpec documentation for function is missing

It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as DeFi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.

There are 124 instances (click to show):
55:   constructor() {

63:   function registerBorrower(address borrower) external onlyOwner {

70:   function removeBorrower(address borrower) external onlyOwner {

77:   function isRegisteredBorrower(address borrower) external view returns (bool) {

81:   function getRegisteredBorrowers() external view returns (address[] memory) {

85:   function getRegisteredBorrowers(

98:   function getRegisteredBorrowersCount() external view returns (uint256) {

106:   function registerControllerFactory(address factory) external onlyOwner {

113:   function removeControllerFactory(address factory) external onlyOwner {

120:   function isRegisteredControllerFactory(address factory) external view returns (bool) {

124:   function getRegisteredControllerFactories() external view returns (address[] memory) {

128:   function getRegisteredControllerFactories(

141:   function getRegisteredControllerFactoriesCount() external view returns (uint256) {

149:   function registerController(address controller) external onlyControllerFactory {

156:   function removeController(address controller) external onlyOwner {

163:   function isRegisteredController(address controller) external view returns (bool) {

167:   function getRegisteredControllers() external view returns (address[] memory) {

171:   function getRegisteredControllers(

184:   function getRegisteredControllersCount() external view returns (uint256) {

192:   function registerMarket(address market) external onlyController {

199:   function removeMarket(address market) external onlyOwner {

206:   function isRegisteredMarket(address market) external view returns (bool) {

210:   function getRegisteredMarkets() external view returns (address[] memory) {

214:   function getRegisteredMarkets(

227:   function getRegisteredMarketsCount() external view returns (uint256) {
94:   constructor() {

125:   function getAuthorizedLenders(

138:   function getAuthorizedLendersCount() external view returns (uint256) {

142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

196:   function isControlledMarket(address market) external view returns (bool) {

200:   function getControlledMarkets() external view returns (address[] memory) {

204:   function getControlledMarkets(

217:   function getControlledMarketsCount() external view returns (uint256) {

221:   function computeMarketAddress(

255:   function _resetTmpMarketParameters() internal {

490:   function resetReserveRatio(address market) external virtual {

503:   function assertValueInRange(
72:   constructor(

106:   function _storeControllerInitCode()

116:   function _storeMarketInitCode()

126:   function isDeployedController(address controller) external view returns (bool) {

130:   function getDeployedControllersCount() external view returns (uint256) {

134:   function getDeployedControllers() external view returns (address[] memory) {

138:   function getDeployedControllers(

246:   function getMarketControllerParameters()

342:   function computeControllerAddress(address borrower) external view returns (address) {
16:   constructor() {

21:   function balance() public view override returns (uint256) {

25:   function canReleaseEscrow() public view override returns (bool) {

29:   function escrowedAsset() public view override returns (address, uint256) {

33:   function releaseEscrow() public override {
  • WildcatSanctionsSentinel.sol ( #L24, #L30 ):
24:   constructor(address _archController, address _chainalysisSanctionsList) {

30:   function _resetTmpEscrowParams() internal {
5:   function and(bool a, bool b) internal pure returns (bool c) {

11:   function or(bool a, bool b) internal pure returns (bool c) {

17:   function xor(bool a, bool b) internal pure returns (bool c) {
19:   function empty(FIFOQueue storage arr) internal view returns (bool) {

23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

55:   function push(FIFOQueue storage arr, uint32 value) internal {

61:   function shift(FIFOQueue storage arr) internal {

70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
30:   function calculateBaseInterest(

40:   function applyProtocolFee(

53:   function updateDelinquency(
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

54:   function calculateCreate2Address(

83:   function createWithStoredInitCode(address initCodeStorage) internal returns (address deployment) {

87:   function createWithStoredInitCode(

99:   function create2WithStoredInitCode(

106:   function create2WithStoredInitCode(
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
7:   function _assertNonOverflow(bool didNotOverflow) private pure {

17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
19: function bytes32ToString(bytes32 value) pure returns (string memory str) {

33: function queryStringOrBytes32AsString(

96: function queryName(address target) view returns (string memory) {

101: function querySymbol(address target) view returns (string memory) {
  • Withdrawal.sol ( #L38 ):
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {
76:   constructor() {

528:   function _applyWithdrawalBatchPaymentView(
  • WildcatMarketConfig.sol ( #L42 ):
42:   function reserveRatioBips() external view returns (uint256) {
31:   function approve(address spender, uint256 amount) external virtual nonReentrant returns (bool) {

36:   function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {

41:   function transferFrom(

59:   function _approve(address approver, address spender, uint256 amount) internal virtual {

64:   function _transfer(address from, address to, uint256 amount) internal virtual {
28:   function getWithdrawalBatch(

38:   function getAccountWithdrawalStatus(

45:   function getAvailableWithdrawalAmount(

190:   function processUnpaidWithdrawalBatch() external nonReentrant {

[N‑38] Functions missing NatSpec @param tag

There are 257 instances (click to show):
/// @audit Missing @param for `borrower`
63:   function registerBorrower(address borrower) external onlyOwner {

/// @audit Missing @param for `borrower`
70:   function removeBorrower(address borrower) external onlyOwner {

/// @audit Missing @param for `borrower`
77:   function isRegisteredBorrower(address borrower) external view returns (bool) {

/// @audit Missing @param for `start`, `end`
85:   function getRegisteredBorrowers(
86:     uint256 start,
87:     uint256 end
88:   ) external view returns (address[] memory arr) {

/// @audit Missing @param for `factory`
106:   function registerControllerFactory(address factory) external onlyOwner {

/// @audit Missing @param for `factory`
113:   function removeControllerFactory(address factory) external onlyOwner {

/// @audit Missing @param for `factory`
120:   function isRegisteredControllerFactory(address factory) external view returns (bool) {

/// @audit Missing @param for `start`, `end`
128:   function getRegisteredControllerFactories(
129:     uint256 start,
130:     uint256 end
131:   ) external view returns (address[] memory arr) {

/// @audit Missing @param for `controller`
149:   function registerController(address controller) external onlyControllerFactory {

/// @audit Missing @param for `controller`
156:   function removeController(address controller) external onlyOwner {

/// @audit Missing @param for `controller`
163:   function isRegisteredController(address controller) external view returns (bool) {

/// @audit Missing @param for `start`, `end`
171:   function getRegisteredControllers(
172:     uint256 start,
173:     uint256 end
174:   ) external view returns (address[] memory arr) {

/// @audit Missing @param for `market`
192:   function registerMarket(address market) external onlyController {

/// @audit Missing @param for `market`
199:   function removeMarket(address market) external onlyOwner {

/// @audit Missing @param for `market`
206:   function isRegisteredMarket(address market) external view returns (bool) {

/// @audit Missing @param for `start`, `end`
214:   function getRegisteredMarkets(
215:     uint256 start,
216:     uint256 end
217:   ) external view returns (address[] memory arr) {
/// @audit Missing @param for `start`, `end`
125:   function getAuthorizedLenders(
126:     uint256 start,
127:     uint256 end
128:   ) external view returns (address[] memory arr) {

/// @audit Missing @param for `lender`
142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

/// @audit Missing @param for `lenders`
153:   function authorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit Missing @param for `lenders`
169:   function deauthorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit Missing @param for `lender`, `markets`
182:   function updateLenderAuthorization(address lender, address[] memory markets) external {

/// @audit Missing @param for `market`
196:   function isControlledMarket(address market) external view returns (bool) {

/// @audit Missing @param for `start`, `end`
204:   function getControlledMarkets(
205:     uint256 start,
206:     uint256 end
207:   ) external view returns (address[] memory arr) {

/// @audit Missing @param for `asset`, `namePrefix`, `symbolPrefix`
221:   function computeMarketAddress(
222:     address asset,
223:     string memory namePrefix,
224:     string memory symbolPrefix
225:   ) external view returns (address) {

/// @audit Missing @param for `asset`, `namePrefix`, `symbolPrefix`, `maxTotalSupply`, `annualInterestBips`, `delinquencyFeeBips`, `withdrawalBatchDuration`, `reserveRatioBips`, `delinquencyGracePeriod`
291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {

/// @audit Missing @param for `asset`, `namePrefix`, `symbolPrefix`
370:   function _deriveSalt(
371:     address asset,
372:     string memory namePrefix,
373:     string memory symbolPrefix
374:   ) internal pure returns (bytes32 salt) {

/// @audit Missing @param for `namePrefix`, `symbolPrefix`, `annualInterestBips`, `delinquencyFeeBips`, `withdrawalBatchDuration`, `reserveRatioBips`, `delinquencyGracePeriod`
394:   function enforceParameterConstraints(
395:     string memory namePrefix,
396:     string memory symbolPrefix,
397:     uint16 annualInterestBips,
398:     uint16 delinquencyFeeBips,
399:     uint32 withdrawalBatchDuration,
400:     uint16 reserveRatioBips,
401:     uint32 delinquencyGracePeriod
402:   ) internal view virtual {

/// @audit Missing @param for `market`, `annualInterestBips`
468:   function setAnnualInterestBips(
469:     address market,
470:     uint16 annualInterestBips
471:   ) external virtual onlyBorrower onlyControlledMarket(market) {

/// @audit Missing @param for `market`
490:   function resetReserveRatio(address market) external virtual {

/// @audit Missing @param for `value`, `min`, `max`, `errorSelector`
503:   function assertValueInRange(
504:     uint256 value,
505:     uint256 min,
506:     uint256 max,
507:     bytes4 errorSelector
508:   ) internal pure {
/// @audit Missing @param for `_archController`, `_sentinel`, `constraints`
72:   constructor(
73:     address _archController,
74:     address _sentinel,
75:     MarketParameterConstraints memory constraints
76:   ) {

/// @audit Missing @param for `controller`
126:   function isDeployedController(address controller) external view returns (bool) {

/// @audit Missing @param for `start`, `end`
138:   function getDeployedControllers(
139:     uint256 start,
140:     uint256 end
141:   ) external view returns (address[] memory arr) {

/// @audit Missing @param for `feeRecipient`, `originationFeeAsset`, `originationFeeAmount`, `protocolFeeBips`
195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {

/// @audit Missing @param for `namePrefix`, `symbolPrefix`, `asset`, `maxTotalSupply`, `annualInterestBips`, `delinquencyFeeBips`, `withdrawalBatchDuration`, `reserveRatioBips`, `delinquencyGracePeriod`
317:   function deployControllerAndMarket(
318:     string memory namePrefix,
319:     string memory symbolPrefix,
320:     address asset,
321:     uint128 maxTotalSupply,
322:     uint16 annualInterestBips,
323:     uint16 delinquencyFeeBips,
324:     uint32 withdrawalBatchDuration,
325:     uint16 reserveRatioBips,
326:     uint32 delinquencyGracePeriod
327:   ) external returns (address controller, address market) {

/// @audit Missing @param for `borrower`
342:   function computeControllerAddress(address borrower) external view returns (address) {
/// @audit Missing @param for `_archController`, `_chainalysisSanctionsList`
24:   constructor(address _archController, address _chainalysisSanctionsList) {

/// @audit Missing @param for `borrower`, `account`
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

/// @audit Missing @param for `account`
48:   function overrideSanction(address account) public override {

/// @audit Missing @param for `account`
56:   function removeSanctionOverride(address account) public override {

/// @audit Missing @param for `borrower`, `account`, `asset`
65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {

/// @audit Missing @param for `borrower`, `account`, `asset`
95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {
/// @audit Missing @param for `a`, `b`
5:   function and(bool a, bool b) internal pure returns (bool c) {

/// @audit Missing @param for `a`, `b`
11:   function or(bool a, bool b) internal pure returns (bool c) {

/// @audit Missing @param for `a`, `b`
17:   function xor(bool a, bool b) internal pure returns (bool c) {
/// @audit Missing @param for `arr`
19:   function empty(FIFOQueue storage arr) internal view returns (bool) {

/// @audit Missing @param for `arr`
23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

/// @audit Missing @param for `arr`, `index`
30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

/// @audit Missing @param for `arr`
38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

/// @audit Missing @param for `arr`
42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

/// @audit Missing @param for `arr`, `value`
55:   function push(FIFOQueue storage arr, uint32 value) internal {

/// @audit Missing @param for `arr`
61:   function shift(FIFOQueue storage arr) internal {

/// @audit Missing @param for `arr`, `n`
70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
/// @audit Missing @param for `state`, `timestamp`
30:   function calculateBaseInterest(
31:     MarketState memory state,
32:     uint256 timestamp
33:   ) internal pure returns (uint256 baseInterestRay) {

/// @audit Missing @param for `state`, `baseInterestRay`, `protocolFeeBips`
40:   function applyProtocolFee(
41:     MarketState memory state,
42:     uint256 baseInterestRay,
43:     uint256 protocolFeeBips
44:   ) internal pure returns (uint256 protocolFee) {

/// @audit Missing @param for `state`, `timestamp`, `delinquencyFeeBips`, `delinquencyGracePeriod`
53:   function updateDelinquency(
54:     MarketState memory state,
55:     uint256 timestamp,
56:     uint256 delinquencyFeeBips,
57:     uint256 delinquencyGracePeriod
58:   ) internal pure returns (uint256 delinquencyFeeRay) {
/// @audit Missing @param for `data`
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

/// @audit Missing @param for `deployer`
48:   function getCreate2Prefix(address deployer) internal pure returns (uint256 create2Prefix) {

/// @audit Missing @param for `create2Prefix`, `salt`, `initCodeHash`
54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {

/// @audit Missing @param for `initCodeStorage`
83:   function createWithStoredInitCode(address initCodeStorage) internal returns (address deployment) {

/// @audit Missing @param for `initCodeStorage`, `value`
87:   function createWithStoredInitCode(
88:     address initCodeStorage,
89:     uint256 value
90:   ) internal returns (address deployment) {

/// @audit Missing @param for `initCodeStorage`, `salt`
99:   function create2WithStoredInitCode(
100:     address initCodeStorage,
101:     bytes32 salt
102:   ) internal returns (address deployment) {

/// @audit Missing @param for `initCodeStorage`, `salt`, `value`
106:   function create2WithStoredInitCode(
107:     address initCodeStorage,
108:     bytes32 salt,
109:     uint256 value
110:   ) internal returns (address deployment) {
/// @audit Missing @param for `state`
51:   function totalSupply(MarketState memory state) internal pure returns (uint256) {

/// @audit Missing @param for `state`
59:   function maximumDeposit(MarketState memory state) internal pure returns (uint256) {

/// @audit Missing @param for `state`, `amount`
66:   function normalizeAmount(
67:     MarketState memory state,
68:     uint256 amount
69:   ) internal pure returns (uint256) {

/// @audit Missing @param for `state`, `amount`
76:   function scaleAmount(MarketState memory state, uint256 amount) internal pure returns (uint256) {

/// @audit Missing @param for `state`
87:   function liquidityRequired(
88:     MarketState memory state
89:   ) internal pure returns (uint256 _liquidityRequired) {

/// @audit Missing @param for `state`, `totalAssets`
105:   function withdrawableProtocolFees(
106:     MarketState memory state,
107:     uint256 totalAssets
108:   ) internal pure returns (uint128) {

/// @audit Missing @param for `state`, `totalAssets`
123:   function borrowableAssets(
124:     MarketState memory state,
125:     uint256 totalAssets
126:   ) internal pure returns (uint256) {

/// @audit Missing @param for `state`
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

/// @audit Missing @param for `state`
138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
/// @audit Missing @param for `a`, `b`
44:   function min(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Missing @param for `a`, `b`
51:   function max(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Missing @param for `a`, `b`
59:   function satSub(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Missing @param for `condition`, `valueIfTrue`, `valueIfFalse`
71:   function ternary(
72:     bool condition,
73:     uint256 valueIfTrue,
74:     uint256 valueIfFalse
75:   ) internal pure returns (uint256 c) {

/// @audit Missing @param for `a`, `b`
85:   function bipMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Missing @param for `a`, `b`
105:   function bipDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Missing @param for `a`
121:   function bipToRay(uint256 a) internal pure returns (uint256 b) {

/// @audit Missing @param for `a`, `b`
138:   function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Missing @param for `a`, `b`
155:   function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Missing @param for `x`, `y`, `d`
173:   function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {

/// @audit Missing @param for `x`, `y`, `d`
191:   function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
/// @audit Missing @param for `didNotOverflow`
7:   function _assertNonOverflow(bool didNotOverflow) private pure {

/// @audit Missing @param for `x`
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

/// @audit Missing @param for `x`
21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

/// @audit Missing @param for `x`
25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

/// @audit Missing @param for `x`
29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

/// @audit Missing @param for `x`
33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

/// @audit Missing @param for `x`
37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

/// @audit Missing @param for `x`
41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

/// @audit Missing @param for `x`
45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

/// @audit Missing @param for `x`
49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

/// @audit Missing @param for `x`
53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

/// @audit Missing @param for `x`
57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

/// @audit Missing @param for `x`
61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

/// @audit Missing @param for `x`
65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

/// @audit Missing @param for `x`
69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

/// @audit Missing @param for `x`
73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

/// @audit Missing @param for `x`
77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

/// @audit Missing @param for `x`
81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

/// @audit Missing @param for `x`
85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

/// @audit Missing @param for `x`
89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

/// @audit Missing @param for `x`
93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

/// @audit Missing @param for `x`
97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

/// @audit Missing @param for `x`
101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

/// @audit Missing @param for `x`
105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

/// @audit Missing @param for `x`
109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

/// @audit Missing @param for `x`
113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

/// @audit Missing @param for `x`
117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

/// @audit Missing @param for `x`
121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

/// @audit Missing @param for `x`
125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

/// @audit Missing @param for `x`
129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

/// @audit Missing @param for `x`
133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

/// @audit Missing @param for `x`
137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
/// @audit Missing @param for `value`
19: function bytes32ToString(bytes32 value) pure returns (string memory str) {

/// @audit Missing @param for `target`, `rightPaddedFunctionSelector`, `rightPaddedGenericErrorSelector`
33: function queryStringOrBytes32AsString(
34:   address target,
35:   uint256 rightPaddedFunctionSelector,
36:   uint256 rightPaddedGenericErrorSelector
37: ) view returns (string memory str) {

/// @audit Missing @param for `target`
96: function queryName(address target) view returns (string memory) {

/// @audit Missing @param for `target`
101: function querySymbol(address target) view returns (string memory) {
/// @audit Missing @param for `batch`
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {

/// @audit Missing @param for `batch`, `state`, `totalAssets`
47:   function availableLiquidityForPendingBatch(
48:     WithdrawalBatch memory batch,
49:     MarketState memory state,
50:     uint256 totalAssets
51:   ) internal pure returns (uint256) {
/// @audit Missing @param for `amount`
42:   function depositUpTo(
43:     uint256 amount
44:   ) public virtual nonReentrant returns (uint256 /* actualAmount */) {

/// @audit Missing @param for `amount`
86:   function deposit(uint256 amount) external virtual {

/// @audit Missing @param for `amount`
119:   function borrow(uint256 amount) external onlyBorrower nonReentrant {
/// @audit Missing @param for `accountAddress`
150:   function _getAccount(address accountAddress) internal view returns (Account memory account) {

/// @audit Missing @param for `state`, `accountAddress`
163:   function _blockAccount(MarketState memory state, address accountAddress) internal {

/// @audit Missing @param for `accountAddress`, `requiredRole`
197:   function _getAccountWithRole(
198:     address accountAddress,
199:     AuthRole requiredRole
200:   ) internal returns (Account memory account) {

/// @audit Missing @param for `account`
292:   function scaledBalanceOf(address account) external view nonReentrantView returns (uint256) {

/// @audit Missing @param for `account`
299:   function getAccountRole(address account) external view nonReentrantView returns (AuthRole) {

/// @audit Missing @param for `state`
448:   function _writeState(MarketState memory state) internal {

/// @audit Missing @param for `state`
466:   function _processExpiredWithdrawalBatch(MarketState memory state) internal {

/// @audit Missing @param for `batch`, `state`, `expiry`, `availableLiquidity`
498:   function _applyWithdrawalBatchPayment(
499:     WithdrawalBatch memory batch,
500:     MarketState memory state,
501:     uint32 expiry,
502:     uint256 availableLiquidity
503:   ) internal {

/// @audit Missing @param for `batch`, `state`, `availableLiquidity`
528:   function _applyWithdrawalBatchPaymentView(
529:     WithdrawalBatch memory batch,
530:     MarketState memory state,
531:     uint256 availableLiquidity
532:   ) internal pure {
/// @audit Missing @param for `accountAddress`
74:   function nukeFromOrbit(address accountAddress) external nonReentrant {

/// @audit Missing @param for `accountAddress`
88:   function stunningReversal(address accountAddress) external nonReentrant {

/// @audit Missing @param for `_account`, `_isAuthorized`
112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {

/// @audit Missing @param for `_maxTotalSupply`
134:   function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyController nonReentrant {

/// @audit Missing @param for `_annualInterestBips`
149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

/// @audit Missing @param for `_reserveRatioBips`
171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {
/// @audit Missing @param for `account`
16:   function balanceOf(address account) public view virtual nonReentrantView returns (uint256) {

/// @audit Missing @param for `spender`, `amount`
31:   function approve(address spender, uint256 amount) external virtual nonReentrant returns (bool) {

/// @audit Missing @param for `to`, `amount`
36:   function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {

/// @audit Missing @param for `from`, `to`, `amount`
41:   function transferFrom(
42:     address from,
43:     address to,
44:     uint256 amount
45:   ) external virtual nonReentrant returns (bool) {

/// @audit Missing @param for `approver`, `spender`, `amount`
59:   function _approve(address approver, address spender, uint256 amount) internal virtual {

/// @audit Missing @param for `from`, `to`, `amount`
64:   function _transfer(address from, address to, uint256 amount) internal virtual {
/// @audit Missing @param for `expiry`
28:   function getWithdrawalBatch(
29:     uint32 expiry
30:   ) external view nonReentrantView returns (WithdrawalBatch memory) {

/// @audit Missing @param for `accountAddress`, `expiry`
38:   function getAccountWithdrawalStatus(
39:     address accountAddress,
40:     uint32 expiry
41:   ) external view nonReentrantView returns (AccountWithdrawalStatus memory) {

/// @audit Missing @param for `accountAddress`, `expiry`
45:   function getAvailableWithdrawalAmount(
46:     address accountAddress,
47:     uint32 expiry
48:   ) external view nonReentrantView returns (uint256) {

/// @audit Missing @param for `amount`
77:   function queueWithdrawal(uint256 amount) external nonReentrant {

/// @audit Missing @param for `accountAddress`, `expiry`
137:   function executeWithdrawal(
138:     address accountAddress,
139:     uint32 expiry
140:   ) external nonReentrant returns (uint256) {

[N‑39] Modifiers missing NatSpec @param tag

There is 1 instance:

  • WildcatMarketController.sol ( #L87 ):
/// @audit Missing @param for `market`
87:   modifier onlyControlledMarket(address market) {

[N‑40] Public variable declarations should have NatSpec descriptions

It is recommended to use the NatSpec tags @notice, @dev, @return, @inheritdoc for public state variables.

There are 23 instances (click to show):
41:   IWildcatArchController public immutable archController;

43:   IWildcatMarketControllerFactory public immutable controllerFactory;

45:   address public immutable borrower;

47:   address public immutable sentinel;

49:   address public immutable marketInitCodeStorage;

51:   uint256 public immutable marketInitCodeHash;

76:   mapping(address => TemporaryReserveRatio) public temporaryExcessReserveRatio;
31:   IWildcatArchController public immutable archController;

34:   address public immutable sentinel;

36:   address public immutable marketInitCodeStorage;

38:   uint256 public immutable marketInitCodeHash;

40:   address public immutable controllerInitCodeStorage;

42:   uint256 public immutable controllerInitCodeHash;
11:   address public immutable override sentinel;

12:   address public immutable override borrower;

13:   address public immutable override account;
11:   bytes32 public constant override WildcatSanctionsEscrowInitcodeHash =

14:   address public immutable override chainalysisSanctionsList;

16:   address public immutable override archController;

18:   TmpEscrowParams public override tmpEscrowParams;

20:   mapping(address borrower => mapping(address account => bool sanctionOverride))
  • WildcatMarketBase.sol ( #L24 ):
24:   string public constant version = '1.0';
  • WildcatMarketToken.sol ( #L13 ):
13:   mapping(address => mapping(address => uint256)) public allowance;

[N‑41] Functions missing NatSpec @return tag

There are 151 instances (click to show):
77:   function isRegisteredBorrower(address borrower) external view returns (bool) {

81:   function getRegisteredBorrowers() external view returns (address[] memory) {

85:   function getRegisteredBorrowers(
86:     uint256 start,
87:     uint256 end
88:   ) external view returns (address[] memory arr) {

98:   function getRegisteredBorrowersCount() external view returns (uint256) {

120:   function isRegisteredControllerFactory(address factory) external view returns (bool) {

124:   function getRegisteredControllerFactories() external view returns (address[] memory) {

128:   function getRegisteredControllerFactories(
129:     uint256 start,
130:     uint256 end
131:   ) external view returns (address[] memory arr) {

141:   function getRegisteredControllerFactoriesCount() external view returns (uint256) {

163:   function isRegisteredController(address controller) external view returns (bool) {

167:   function getRegisteredControllers() external view returns (address[] memory) {

171:   function getRegisteredControllers(
172:     uint256 start,
173:     uint256 end
174:   ) external view returns (address[] memory arr) {

184:   function getRegisteredControllersCount() external view returns (uint256) {

206:   function isRegisteredMarket(address market) external view returns (bool) {

210:   function getRegisteredMarkets() external view returns (address[] memory) {

214:   function getRegisteredMarkets(
215:     uint256 start,
216:     uint256 end
217:   ) external view returns (address[] memory arr) {

227:   function getRegisteredMarketsCount() external view returns (uint256) {
121:   function getAuthorizedLenders() external view returns (address[] memory) {

125:   function getAuthorizedLenders(
126:     uint256 start,
127:     uint256 end
128:   ) external view returns (address[] memory arr) {

138:   function getAuthorizedLendersCount() external view returns (uint256) {

142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

196:   function isControlledMarket(address market) external view returns (bool) {

200:   function getControlledMarkets() external view returns (address[] memory) {

204:   function getControlledMarkets(
205:     uint256 start,
206:     uint256 end
207:   ) external view returns (address[] memory arr) {

217:   function getControlledMarketsCount() external view returns (uint256) {

221:   function computeMarketAddress(
222:     address asset,
223:     string memory namePrefix,
224:     string memory symbolPrefix
225:   ) external view returns (address) {

238:   function getMarketParameters() external view returns (MarketParameters memory parameters) {

291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {

370:   function _deriveSalt(
371:     address asset,
372:     string memory namePrefix,
373:     string memory symbolPrefix
374:   ) internal pure returns (bytes32 salt) {

446:   function getParameterConstraints()
447:     external
448:     view
449:     returns (MarketParameterConstraints memory constraints)
106:   function _storeControllerInitCode()
107:     internal
108:     virtual
109:     returns (address initCodeStorage, uint256 initCodeHash)

116:   function _storeMarketInitCode()
117:     internal
118:     virtual
119:     returns (address initCodeStorage, uint256 initCodeHash)

126:   function isDeployedController(address controller) external view returns (bool) {

130:   function getDeployedControllersCount() external view returns (uint256) {

134:   function getDeployedControllers() external view returns (address[] memory) {

138:   function getDeployedControllers(
139:     uint256 start,
140:     uint256 end
141:   ) external view returns (address[] memory arr) {

223:   function getParameterConstraints()
224:     external
225:     view
226:     returns (MarketParameterConstraints memory constraints)

246:   function getMarketControllerParameters()
247:     external
248:     view
249:     virtual
250:     returns (MarketControllerParameters memory parameters)

282:   function deployController() public returns (address controller) {

317:   function deployControllerAndMarket(
318:     string memory namePrefix,
319:     string memory symbolPrefix,
320:     address asset,
321:     uint128 maxTotalSupply,
322:     uint16 annualInterestBips,
323:     uint16 delinquencyFeeBips,
324:     uint32 withdrawalBatchDuration,
325:     uint16 reserveRatioBips,
326:     uint32 delinquencyGracePeriod
327:   ) external returns (address controller, address market) {

342:   function computeControllerAddress(address borrower) external view returns (address) {
21:   function balance() public view override returns (uint256) {

25:   function canReleaseEscrow() public view override returns (bool) {

29:   function escrowedAsset() public view override returns (address, uint256) {
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {

95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {
5:   function and(bool a, bool b) internal pure returns (bool c) {

11:   function or(bool a, bool b) internal pure returns (bool c) {

17:   function xor(bool a, bool b) internal pure returns (bool c) {
19:   function empty(FIFOQueue storage arr) internal view returns (bool) {

23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {
30:   function calculateBaseInterest(
31:     MarketState memory state,
32:     uint256 timestamp
33:   ) internal pure returns (uint256 baseInterestRay) {

40:   function applyProtocolFee(
41:     MarketState memory state,
42:     uint256 baseInterestRay,
43:     uint256 protocolFeeBips
44:   ) internal pure returns (uint256 protocolFee) {

53:   function updateDelinquency(
54:     MarketState memory state,
55:     uint256 timestamp,
56:     uint256 delinquencyFeeBips,
57:     uint256 delinquencyGracePeriod
58:   ) internal pure returns (uint256 delinquencyFeeRay) {

89:   function updateTimeDelinquentAndGetPenaltyTime(
90:     MarketState memory state,
91:     uint256 delinquencyGracePeriod,
92:     uint256 timeDelta
93:   ) internal pure returns (uint256 /* timeWithPenalty */) {
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

48:   function getCreate2Prefix(address deployer) internal pure returns (uint256 create2Prefix) {

54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {

83:   function createWithStoredInitCode(address initCodeStorage) internal returns (address deployment) {

87:   function createWithStoredInitCode(
88:     address initCodeStorage,
89:     uint256 value
90:   ) internal returns (address deployment) {

99:   function create2WithStoredInitCode(
100:     address initCodeStorage,
101:     bytes32 salt
102:   ) internal returns (address deployment) {

106:   function create2WithStoredInitCode(
107:     address initCodeStorage,
108:     bytes32 salt,
109:     uint256 value
110:   ) internal returns (address deployment) {
51:   function totalSupply(MarketState memory state) internal pure returns (uint256) {

59:   function maximumDeposit(MarketState memory state) internal pure returns (uint256) {

66:   function normalizeAmount(
67:     MarketState memory state,
68:     uint256 amount
69:   ) internal pure returns (uint256) {

76:   function scaleAmount(MarketState memory state, uint256 amount) internal pure returns (uint256) {

87:   function liquidityRequired(
88:     MarketState memory state
89:   ) internal pure returns (uint256 _liquidityRequired) {

105:   function withdrawableProtocolFees(
106:     MarketState memory state,
107:     uint256 totalAssets
108:   ) internal pure returns (uint128) {

123:   function borrowableAssets(
124:     MarketState memory state,
125:     uint256 totalAssets
126:   ) internal pure returns (uint256) {

130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
44:   function min(uint256 a, uint256 b) internal pure returns (uint256 c) {

51:   function max(uint256 a, uint256 b) internal pure returns (uint256 c) {

59:   function satSub(uint256 a, uint256 b) internal pure returns (uint256 c) {

71:   function ternary(
72:     bool condition,
73:     uint256 valueIfTrue,
74:     uint256 valueIfFalse
75:   ) internal pure returns (uint256 c) {

85:   function bipMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

105:   function bipDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

121:   function bipToRay(uint256 a) internal pure returns (uint256 b) {

138:   function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

155:   function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

173:   function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {

191:   function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
19: function bytes32ToString(bytes32 value) pure returns (string memory str) {

33: function queryStringOrBytes32AsString(
34:   address target,
35:   uint256 rightPaddedFunctionSelector,
36:   uint256 rightPaddedGenericErrorSelector
37: ) view returns (string memory str) {

96: function queryName(address target) view returns (string memory) {

101: function querySymbol(address target) view returns (string memory) {
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {

47:   function availableLiquidityForPendingBatch(
48:     WithdrawalBatch memory batch,
49:     MarketState memory state,
50:     uint256 totalAssets
51:   ) internal pure returns (uint256) {
42:   function depositUpTo(
43:     uint256 amount
44:   ) public virtual nonReentrant returns (uint256 /* actualAmount */) {
150:   function _getAccount(address accountAddress) internal view returns (Account memory account) {

197:   function _getAccountWithRole(
198:     address accountAddress,
199:     AuthRole requiredRole
200:   ) internal returns (Account memory account) {

223:   function coverageLiquidity() external view nonReentrantView returns (uint256) {

231:   function scaleFactor() external view nonReentrantView returns (uint256) {

238:   function totalAssets() public view returns (uint256) {

252:   function borrowableAssets() external view nonReentrantView returns (uint256) {

260:   function accruedProtocolFees() external view nonReentrantView returns (uint256) {

267:   function previousState() external view returns (MarketState memory) {

276:   function currentState() public view nonReentrantView returns (MarketState memory state) {

285:   function scaledTotalSupply() external view nonReentrantView returns (uint256) {

292:   function scaledBalanceOf(address account) external view nonReentrantView returns (uint256) {

299:   function getAccountRole(address account) external view nonReentrantView returns (AuthRole) {

307:   function withdrawableProtocolFees() external view returns (uint128) {

399:   function _calculateCurrentState()
400:     internal
401:     view
402:     returns (
403:       MarketState memory state,
404:       uint32 expiredBatchExpiry,
405:       WithdrawalBatch memory expiredBatch
406:     )
21:   function maximumDeposit() external view returns (uint256) {

30:   function maxTotalSupply() external view returns (uint256) {

38:   function annualInterestBips() external view returns (uint256) {

42:   function reserveRatioBips() external view returns (uint256) {
16:   function balanceOf(address account) public view virtual nonReentrantView returns (uint256) {

22:   function totalSupply() external view virtual nonReentrantView returns (uint256) {

31:   function approve(address spender, uint256 amount) external virtual nonReentrant returns (bool) {

36:   function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {

41:   function transferFrom(
42:     address from,
43:     address to,
44:     uint256 amount
45:   ) external virtual nonReentrant returns (bool) {
24:   function getUnpaidBatchExpiries() external view nonReentrantView returns (uint32[] memory) {

28:   function getWithdrawalBatch(
29:     uint32 expiry
30:   ) external view nonReentrantView returns (WithdrawalBatch memory) {

38:   function getAccountWithdrawalStatus(
39:     address accountAddress,
40:     uint32 expiry
41:   ) external view nonReentrantView returns (AccountWithdrawalStatus memory) {

45:   function getAvailableWithdrawalAmount(
46:     address accountAddress,
47:     uint32 expiry
48:   ) external view nonReentrantView returns (uint256) {

137:   function executeWithdrawal(
138:     address accountAddress,
139:     uint32 expiry
140:   ) external nonReentrant returns (uint256) {

[N‑42] Contract name does not follow the Solidity Style Guide

According to the Solidity Style Guide, contracts and libraries should be named using the CapWords style and match their filenames.

There are 3 instances:

  • FIFOQueue.sol ( #L16 ):
/// @audit File name does not match contract name
16: library FIFOQueueLib {
  • MarketState.sol ( #L44 ):
/// @audit File name does not match contract name
44: library MarketStateLib {
  • Withdrawal.sol ( #L37 ):
/// @audit File name does not match contract name
37: library WithdrawalLib {

[N‑43] Functions and modifiers should be named in mixedCase style

As the Solidity Style Guide suggests: functions and modifiers should be named in mixedCase style.

There are 3 instances:

  • FIFOQueue.sol ( #L70 ):
70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
318:   function effectiveBorrowerAPR() external view returns (uint256) {

334:   function effectiveLenderAPR() external view returns (uint256) {

[N‑44] Variable names for immutables should use UPPER_CASE_WITH_UNDERSCORES

For immutable variable names, each word should use all capital letters, with underscores separating each word (UPPER_CASE_WITH_UNDERSCORES)

There are 50 instances (click to show):
41:   IWildcatArchController public immutable archController;

43:   IWildcatMarketControllerFactory public immutable controllerFactory;

45:   address public immutable borrower;

47:   address public immutable sentinel;

49:   address public immutable marketInitCodeStorage;

51:   uint256 public immutable marketInitCodeHash;

53:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

55:   uint32 internal immutable MinimumDelinquencyGracePeriod;

56:   uint32 internal immutable MaximumDelinquencyGracePeriod;

58:   uint16 internal immutable MinimumReserveRatioBips;

59:   uint16 internal immutable MaximumReserveRatioBips;

61:   uint16 internal immutable MinimumDelinquencyFeeBips;

62:   uint16 internal immutable MaximumDelinquencyFeeBips;

64:   uint32 internal immutable MinimumWithdrawalBatchDuration;

65:   uint32 internal immutable MaximumWithdrawalBatchDuration;

67:   uint16 internal immutable MinimumAnnualInterestBips;

68:   uint16 internal immutable MaximumAnnualInterestBips;
31:   IWildcatArchController public immutable archController;

34:   address public immutable sentinel;

36:   address public immutable marketInitCodeStorage;

38:   uint256 public immutable marketInitCodeHash;

40:   address public immutable controllerInitCodeStorage;

42:   uint256 public immutable controllerInitCodeHash;

44:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

46:   uint32 internal immutable MinimumDelinquencyGracePeriod;

47:   uint32 internal immutable MaximumDelinquencyGracePeriod;

49:   uint16 internal immutable MinimumReserveRatioBips;

50:   uint16 internal immutable MaximumReserveRatioBips;

52:   uint16 internal immutable MinimumDelinquencyFeeBips;

53:   uint16 internal immutable MaximumDelinquencyFeeBips;

55:   uint32 internal immutable MinimumWithdrawalBatchDuration;

56:   uint32 internal immutable MaximumWithdrawalBatchDuration;

58:   uint16 internal immutable MinimumAnnualInterestBips;

59:   uint16 internal immutable MaximumAnnualInterestBips;
11:   address public immutable override sentinel;

12:   address public immutable override borrower;

13:   address public immutable override account;

14:   address internal immutable asset;
  • WildcatSanctionsSentinel.sol ( #L14, #L16 ):
14:   address public immutable override chainalysisSanctionsList;

16:   address public immutable override archController;
27:   address public immutable sentinel;

30:   address public immutable borrower;

33:   address public immutable feeRecipient;

36:   uint256 public immutable protocolFeeBips;

39:   uint256 public immutable delinquencyFeeBips;

42:   uint256 public immutable delinquencyGracePeriod;

45:   address public immutable controller;

48:   address public immutable asset;

51:   uint256 public immutable withdrawalBatchDuration;

54:   uint8 public immutable decimals;

[N‑45] Non-assembly method available

There are some automated tools that will flag a project as having higher complexity if there is inline-assembly, so it's best to avoid using it where it's not necessary. In addition, most assembly methods can be replaced by non-assembly methods, for example:

  • assembly{ g := gas() } => uint256 g = gasleft()
  • assembly{ id := chainid() } => uint256 id = block.chainid
  • assembly { r := mulmod(a, b, d) } => uint256 m = mulmod(x, y, k)
  • assembly { size := extcodesize() } => uint256 size = address(a).code.length
  • etc.
There are 29 instances (click to show):
380:       mstore(0x20, keccak256(add(namePrefix, 32), mload(namePrefix)))

381:       mstore(0x40, keccak256(add(symbolPrefix, 32), mload(symbolPrefix)))

382:       salt := keccak256(0, 0x60)

407:         revert(0x1c, 0x04)

512:         revert(0, 4)
27:     revert(0, 4)

38:     revert(Error_SelectorPointer, 4)

51:     revert(0, 0x24)

64:     revert(Error_SelectorPointer, 0x24)
33:       initCodeStorage := create(0, add(data, 21), createSize)

37:         revert(0x1c, 0x04)

76:       create2Address := keccak256(0x0b, 0x55)

93:       let initCodeSize := sub(extcodesize(initCodeStorage), 1)

95:       deployment := create(value, initCodePointer, initCodeSize)

113:       let initCodeSize := sub(extcodesize(initCodeStorage), 1)

115:       deployment := create2(value, initCodePointer, initCodeSize, salt)
  • MarketState.sol ( #L134 ):
134:       result := gt(timestamp(), sub(expiry, 1))
94:         revert(Error_SelectorPointer, Panic_ErrorLength)

111:         revert(Error_SelectorPointer, Panic_ErrorLength)

129:         revert(Error_SelectorPointer, Panic_ErrorLength)

144:         revert(Error_SelectorPointer, Panic_ErrorLength)

161:         revert(Error_SelectorPointer, Panic_ErrorLength)

180:         revert(0x1c, 0x04)

198:         revert(0x1c, 0x04)
  • SafeCastLib.sol ( #L12 ):
12:         revert(Error_SelectorPointer, Panic_ErrorLength)
41:     let status := staticcall(gas(), target, 0, 0x04, 0, 0)

53:           revert(0, returndatasize())

57:         revert(0, 0x04)

61:       revert(0, 0x04)

[N‑46] Order of contract layout does not follow the Solidity Style Guide

As suggested by the Solidity Style Guide:

  • Layout contract elements in the following order: pragma statements, import statements, interfaces, libraries, contracts.
  • Inside each contract, library or interface, use the following order: type declarations, state variables, events, errors, modifiers, functions.
There are 6 instances (click to show):
  • ReentrancyGuard.sol ( #L20 ):
/// @audit ↑ Out of order with error NoReentrantCalls
20:   uint256 private _reentrancyGuard;
  • WildcatArchController.sol ( #L29 ):
/// @audit ↑ Out of order with error MarketDoesNotExist
29:   event MarketAdded(address indexed controller, address market);
  • WildcatMarketControllerFactory.sol ( #L31, #L244 ):
/// @audit ↑ Out of order with error ControllerAlreadyDeployed
31:   IWildcatArchController public immutable archController;

/// @audit ↑ Out of order with function getParameterConstraints()
244:   address internal _tmpMarketBorrowerParameter = address(1);
  • MathUtils.sol ( #L21 ):
/// @audit ↑ Out of order with error MulDivFailed
21:   using MathUtils for uint256;
  • WildcatMarketBase.sol ( #L131 ):
/// @audit ↑ Out of order with constructor ()
131:   modifier onlyBorrower() {

[N‑47] Missing zero address check in functions with address parameters

Adding a zero address check for each address type parameter can prevent errors.

There are 47 instances (click to show):
/// @audit `borrower not checked`
63:   function registerBorrower(address borrower) external onlyOwner {

/// @audit `borrower not checked`
70:   function removeBorrower(address borrower) external onlyOwner {

/// @audit `borrower not checked`
77:   function isRegisteredBorrower(address borrower) external view returns (bool) {

/// @audit `factory not checked`
106:   function registerControllerFactory(address factory) external onlyOwner {

/// @audit `factory not checked`
113:   function removeControllerFactory(address factory) external onlyOwner {

/// @audit `factory not checked`
120:   function isRegisteredControllerFactory(address factory) external view returns (bool) {

/// @audit `controller not checked`
149:   function registerController(address controller) external onlyControllerFactory {

/// @audit `controller not checked`
156:   function removeController(address controller) external onlyOwner {

/// @audit `controller not checked`
163:   function isRegisteredController(address controller) external view returns (bool) {

/// @audit `market not checked`
192:   function registerMarket(address market) external onlyController {

/// @audit `market not checked`
199:   function removeMarket(address market) external onlyOwner {

/// @audit `market not checked`
206:   function isRegisteredMarket(address market) external view returns (bool) {
/// @audit `lender not checked`
142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

/// @audit `lenders not checked`
153:   function authorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit `lenders not checked`
169:   function deauthorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit `lender not checked`
/// @audit `markets not checked`
182:   function updateLenderAuthorization(address lender, address[] memory markets) external {

/// @audit `market not checked`
196:   function isControlledMarket(address market) external view returns (bool) {

/// @audit `asset not checked`
221:   function computeMarketAddress(
222:     address asset,
223:     string memory namePrefix,
224:     string memory symbolPrefix
225:   ) external view returns (address) {

/// @audit `asset not checked`
291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {

/// @audit `market not checked`
490:   function resetReserveRatio(address market) external virtual {
/// @audit `controller not checked`
126:   function isDeployedController(address controller) external view returns (bool) {

/// @audit `asset not checked`
317:   function deployControllerAndMarket(
318:     string memory namePrefix,
319:     string memory symbolPrefix,
320:     address asset,
321:     uint128 maxTotalSupply,
322:     uint16 annualInterestBips,
323:     uint16 delinquencyFeeBips,
324:     uint32 withdrawalBatchDuration,
325:     uint16 reserveRatioBips,
326:     uint32 delinquencyGracePeriod
327:   ) external returns (address controller, address market) {

/// @audit `borrower not checked`
342:   function computeControllerAddress(address borrower) external view returns (address) {
/// @audit `borrower not checked`
/// @audit `account not checked`
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

/// @audit `account not checked`
48:   function overrideSanction(address account) public override {

/// @audit `account not checked`
56:   function removeSanctionOverride(address account) public override {

/// @audit `borrower not checked`
/// @audit `account not checked`
/// @audit `asset not checked`
65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {

/// @audit `borrower not checked`
/// @audit `account not checked`
/// @audit `asset not checked`
95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {
/// @audit `account not checked`
292:   function scaledBalanceOf(address account) external view nonReentrantView returns (uint256) {

/// @audit `account not checked`
299:   function getAccountRole(address account) external view nonReentrantView returns (AuthRole) {
/// @audit `accountAddress not checked`
74:   function nukeFromOrbit(address accountAddress) external nonReentrant {

/// @audit `accountAddress not checked`
88:   function stunningReversal(address accountAddress) external nonReentrant {

/// @audit `_account not checked`
112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {
/// @audit `account not checked`
16:   function balanceOf(address account) public view virtual nonReentrantView returns (uint256) {

/// @audit `spender not checked`
31:   function approve(address spender, uint256 amount) external virtual nonReentrant returns (bool) {

/// @audit `to not checked`
36:   function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {

/// @audit `from not checked`
/// @audit `to not checked`
41:   function transferFrom(
42:     address from,
43:     address to,
44:     uint256 amount
45:   ) external virtual nonReentrant returns (bool) {
/// @audit `accountAddress not checked`
38:   function getAccountWithdrawalStatus(
39:     address accountAddress,
40:     uint32 expiry
41:   ) external view nonReentrantView returns (AccountWithdrawalStatus memory) {

/// @audit `accountAddress not checked`
45:   function getAvailableWithdrawalAmount(
46:     address accountAddress,
47:     uint32 expiry
48:   ) external view nonReentrantView returns (uint256) {

/// @audit `accountAddress not checked`
137:   function executeWithdrawal(
138:     address accountAddress,
139:     uint32 expiry
140:   ) external nonReentrant returns (uint256) {

[N‑48] Named imports of parent contracts are missing

There are 11 instances (click to show):
  • WildcatArchController.sol ( #L8 ):
/// @audit Ownable
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
/// @audit IWildcatMarketControllerEventsAndErrors
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
/// @audit WildcatMarketBase
/// @audit WildcatMarketConfig
/// @audit WildcatMarketToken
/// @audit WildcatMarketWithdrawals
10: contract WildcatMarket is
11:   WildcatMarketBase,
12:   WildcatMarketConfig,
13:   WildcatMarketToken,
14:   WildcatMarketWithdrawals
  • WildcatMarketBase.sol ( #L14 ):
/// @audit ReentrancyGuard
/// @audit IMarketEventsAndErrors
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
/// @audit WildcatMarketBase
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketToken.sol ( #L6 ):
/// @audit WildcatMarketBase
6: contract WildcatMarketToken is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
/// @audit WildcatMarketBase
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[N‑49] Constants should be put on the left side of comparisons

Putting constants on the left side of comparison statements is a best practice known as Yoda conditions. Although solidity's static typing system prevents accidental assignments within conditionals, adopting this practice can improve code readability and consistency, especially when working across multiple languages.

There are 19 instances (click to show):
  • ReentrancyGuard.sol ( #L83 ):
/// @audit put `_NOT_ENTERED` on the left
83:     if (_reentrancyGuard != _NOT_ENTERED) {
/// @audit put `address(0)` on the left
345:     if (originationFeeAsset != address(0)) {

/// @audit put `bytes32(0)` on the left
351:     if (market.codehash != bytes32(0)) {

/// @audit put `0` on the left
477:       if (tmp.expiry == 0) {

/// @audit put `0` on the left
492:     if (tmp.expiry == 0) {
/// @audit put `address(0)` on the left
202:     bool nullFeeRecipient = feeRecipient == address(0);

/// @audit put `address(0)` on the left
203:     bool nullOriginationFeeAsset = originationFeeAsset == address(0);

/// @audit put `bytes32(0)` on the left
294:     if (controller.codehash != bytes32(0)) {
  • WildcatSanctionsSentinel.sol ( #L106 ):
/// @audit put `bytes32(0)` on the left
106:     if (escrowContract.codehash != bytes32(0)) return escrowContract;
/// @audit put `0` on the left
57:     if (scaledAmount == 0) revert NullMintAmount();

/// @audit put `0` on the left
98:     if (state.accruedProtocolFees == 0) {

/// @audit put `0` on the left
102:     if (withdrawableFees == 0) {
/// @audit put `address(0)` on the left
79:     if ((parameters.protocolFeeBips > 0).and(parameters.feeRecipient == address(0))) {

/// @audit put `0` on the left
507:     if (scaledAmountOwed == 0) {

/// @audit put `0` on the left
536:     if (scaledAmountOwed == 0) {
  • WildcatMarketToken.sol ( #L68 ):
/// @audit put `0` on the left
68:     if (scaledAmount == 0) {
/// @audit put `0` on the left
84:     if (scaledAmount == 0) {

/// @audit put `0` on the left
94:     if (state.pendingWithdrawalExpiry == 0) {

/// @audit put `0` on the left
160:     if (normalizedAmountWithdrawn == 0) {

[N‑50] Put all system-wide constants in one file

Putting all the system-wide constants in a single file improves code readability, makes it easier to understand the basic configuration and limitations of the system, and makes maintenance easier.

There are 2 instances:

  • WildcatSanctionsSentinel.sol ( #L11-L12 ):
11:   bytes32 public constant override WildcatSanctionsEscrowInitcodeHash =
12:     keccak256(type(WildcatSanctionsEscrow).creationCode);
  • WildcatMarketBase.sol ( #L24 ):
24:   string public constant version = '1.0';

[N‑51] Redundant return statement in a function with named return variables

Because the return variable (or its default value) has been assigned, explicit return at the end of the function is unnecessary, as it is returned automatically.

There is 1 instance:

42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {
43:     uint256 startIndex = arr.startIndex;
44:     uint256 nextIndex = arr.nextIndex;
45:     uint256 len = nextIndex - startIndex;
46:     _values = new uint32[](len);
47: 
48:     for (uint256 i = 0; i < len; i++) {
49:       _values[i] = arr.data[startIndex + i];
50:     }
51: 
52:     return _values;
53:   }

[N‑52] Duplicated require()/revert() checks should be refactored

Refactoring duplicate require()/revert() checks into a modifier or function can make the code more concise, readable and maintainable, and less likely to make errors or omissions when modifying the require() or revert().

There is 1 instance:

  • WildcatMarketWithdrawals.sol ( #L50 ):
/// @audit Duplicated on line 142
50:       revert WithdrawalBatchNotExpired();

[N‑53] Large multiples of ten should use scientific notation

Use a scientific notation rather than decimal literals (e.g. 1e6 instead of 1000000), for better code readability.

There are 4 instances:

  • WildcatMarketController.sol ( #L481 ):
/// @audit 9000 -> 9e3
481:         WildcatMarket(market).setReserveRatioBips(9000);
/// @audit 10000 -> 1e4
81:       constraints.maximumAnnualInterestBips > 10000 ||

/// @audit 10000 -> 1e4
83:       constraints.maximumDelinquencyFeeBips > 10000 ||

/// @audit 10000 -> 1e4
85:       constraints.maximumReserveRatioBips > 10000 ||

[N‑54] Non-interface files should use fixed compiler versions

To prevent the actual contracts being deployed from behaving differently depending on the compiler version, it is recommended to use fixed solidity versions for contracts and libraries.

Although we can configure a specific version through config (like hardhat, forge config files), it is recommended to set the fixed version in the solidity pragma directly before deploying to the mainnet.

There are 19 instances (click to show):
  • ReentrancyGuard.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatArchController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketControllerFactory.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsEscrow.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsSentinel.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • BoolUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FIFOQueue.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FeeMath.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • LibStoredInitCode.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MarketState.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MathUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • SafeCastLib.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Withdrawal.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarket.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketBase.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketConfig.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketToken.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketWithdrawals.sol ( #L2 ):
2: pragma solidity >=0.8.20;

[N‑55] Consider bounding input array length

The functions below take in an unbounded array, and make function calls for entries in the array. While the function will revert if it eventually runs out of gas, it may be a nicer user experience to require() that the length of the array is below some reasonable maximum, so that the user doesn't have to use up a full transaction's gas only to see that the transaction reverts.

There are 3 instances:

154:     for (uint256 i = 0; i < lenders.length; i++) {

170:     for (uint256 i = 0; i < lenders.length; i++) {

183:     for (uint256 i; i < markets.length; i++) {

[N‑56] Unused import

The identifier is imported but never used within the file.

There are 3 instances:

  • WildcatSanctionsEscrow.sol ( #L5, #L6 ):
/// @audit IChainalysisSanctionsList
5: import { IChainalysisSanctionsList } from './interfaces/IChainalysisSanctionsList.sol';

/// @audit SanctionsList
6: import { SanctionsList } from './libraries/Chainalysis.sol';
  • WildcatSanctionsSentinel.sol ( #L7 ):
/// @audit SanctionsList
7: import { SanctionsList } from './libraries/Chainalysis.sol';

[N‑57] Unused named return

Declaring named returns, but not using them, is confusing to the reader. Consider either completely removing them (by declaring just the type without a name), or remove the return statement and do a variable assignment. This would improve the readability of the code, and it may also help reduce regressions during future code refactors.

There are 8 instances (click to show):
  • WildcatMarketControllerFactory.sol ( #L165-L173 ):
/// @audit feeRecipient
/// @audit originationFeeAsset
/// @audit originationFeeAmount
/// @audit protocolFeeBips
165:   function getProtocolFeeConfiguration()
166:     external
167:     view
168:     returns (
169:       address feeRecipient,
170:       address originationFeeAsset,
171:       uint80 originationFeeAmount,
172:       uint16 protocolFeeBips
173:     )
  • WildcatSanctionsSentinel.sol ( #L65-L69 ):
/// @audit escrowAddress
65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {
/// @audit result
19:   function calculateLinearInterestFromBips(
20:     uint256 rateBip,
21:     uint256 timeDelta
22:   ) internal pure returns (uint256 result) {
/// @audit _liquidityRequired
87:   function liquidityRequired(
88:     MarketState memory state
89:   ) internal pure returns (uint256 _liquidityRequired) {
/// @audit result
30:   function calculateLinearInterestFromBips(
31:     uint256 rateBip,
32:     uint256 timeDelta
33:   ) internal pure returns (uint256 result) {

[N‑58] Use delete instead of assigning values to false

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L57 ):
57:     sanctionOverrides[msg.sender][account] = false;

[N‑59] Consider using delete rather than assigning zero to clear values

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

There are 5 instances:

144:     state.annualInterestBips = 0;

146:     state.reserveRatioBips = 0;
171:         account.scaledBalance = 0;

431:       state.pendingWithdrawalExpiry = 0;

489:     state.pendingWithdrawalExpiry = 0;

[N‑60] Use the latest Solidity version (0.8.19 for L2s)

Upgrading to the latest solidity version can optimize gas usage, take advantage of new features and improve overall contract efficiency. Where possible, based on compatibility requirements, it is recommended to use newer/latest solidity version to take advantage of the latest optimizations and features.

There are 19 instances (click to show):
  • ReentrancyGuard.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatArchController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketControllerFactory.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsEscrow.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsSentinel.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • BoolUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FIFOQueue.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FeeMath.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • LibStoredInitCode.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MarketState.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MathUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • SafeCastLib.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Withdrawal.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarket.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketBase.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketConfig.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketToken.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketWithdrawals.sol ( #L2 ):
2: pragma solidity >=0.8.20;

[N‑61] Named mappings are recommended

Named mappings (with syntax mapping(KeyType KeyName? => ValueType ValueName?)) are recommended.It can make the mapping variables clearer, more readable and easier to maintain.

There are 5 instances:

  • WildcatMarketController.sol ( #L76 ):
76:   mapping(address => TemporaryReserveRatio) public temporaryExcessReserveRatio;
  • WildcatSanctionsSentinel.sol ( #L20 ):
20:   mapping(address borrower => mapping(address account => bool sanctionOverride))
  • WildcatMarketBase.sol ( #L68 ):
68:   mapping(address => Account) internal _accounts;
  • WildcatMarketToken.sol ( #L13, #L13 ):
13:   mapping(address => mapping(address => uint256)) public allowance;

13:   mapping(address => mapping(address => uint256)) public allowance;

[N‑62] Whitespace in Expressions

See the Whitespace in Expressions section of the Solidity Style Guide.

There are 6 instances:

/// @audit Whitespace inside parenthesis
277:     (state, , ) = _calculateCurrentState();

/// @audit Whitespace before a comma
277:     (state, , ) = _calculateCurrentState();
/// @audit Whitespace inside parenthesis
17:     (MarketState memory state, , ) = _calculateCurrentState();

/// @audit Whitespace before a comma
17:     (MarketState memory state, , ) = _calculateCurrentState();

/// @audit Whitespace inside parenthesis
23:     (MarketState memory state, , ) = _calculateCurrentState();

/// @audit Whitespace before a comma
23:     (MarketState memory state, , ) = _calculateCurrentState();

[N‑63] Use a struct to encapsulate multiple function parameters

If a function has too many parameters, replacing them with a struct can improve code readability and maintainability, increase reusability, and reduce the likelihood of errors when passing the parameters.

There are 3 instances (click to show):
291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {

394:   function enforceParameterConstraints(
395:     string memory namePrefix,
396:     string memory symbolPrefix,
397:     uint16 annualInterestBips,
398:     uint16 delinquencyFeeBips,
399:     uint32 withdrawalBatchDuration,
400:     uint16 reserveRatioBips,
401:     uint32 delinquencyGracePeriod
402:   ) internal view virtual {
  • WildcatMarketControllerFactory.sol ( #L317-L327 ):
317:   function deployControllerAndMarket(
318:     string memory namePrefix,
319:     string memory symbolPrefix,
320:     address asset,
321:     uint128 maxTotalSupply,
322:     uint16 annualInterestBips,
323:     uint16 delinquencyFeeBips,
324:     uint32 withdrawalBatchDuration,
325:     uint16 reserveRatioBips,
326:     uint32 delinquencyGracePeriod
327:   ) external returns (address controller, address market) {

[N‑64] Returning a struct instead of a bunch of variables is better

If a function returns too many variables, replacing them with a struct can improve code readability, maintainability and reusability.

There is 1 instance:

  • WildcatMarketControllerFactory.sol ( #L165-L173 ):
165:   function getProtocolFeeConfiguration()
166:     external
167:     view
168:     returns (
169:       address feeRecipient,
170:       address originationFeeAsset,
171:       uint80 originationFeeAmount,
172:       uint16 protocolFeeBips
173:     )

[N‑65] Addresses shouldn't be hard-coded

It is often better to declare addresses as constant or immutable which can be assigned in constructor. This allows the code to remain the same across deployments on different networks, and avoids recompilation when addresses need to change.

There is 1 instance:

  • Chainalysis.sol ( #L7 ):
7:   0x40C57923924B5c5c5455c48D93317139ADDaC8fb

[N‑66] Events that mark critical parameter changes should contain both the old and the new value

This should especially be done if the new value is not required to be different from the old value.

There are 4 instances:

125:     emit AuthorizationStatusUpdated(_account, account.approval);

143:     emit MaxTotalSupplyUpdated(_maxTotalSupply);

158:     emit AnnualInterestBipsUpdated(_annualInterestBips);

192:     emit ReserveRatioBipsUpdated(_reserveRatioBips);

[N‑67] Non-public state variables should include comments

Consider adding some comments on non-public state variables to explain what they are supposed to do. This will help for future code reviews.

There are 35 instances (click to show):
11:   EnumerableSet.AddressSet internal _markets;

12:   EnumerableSet.AddressSet internal _controllerFactories;

13:   EnumerableSet.AddressSet internal _borrowers;

14:   EnumerableSet.AddressSet internal _controllers;
53:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

55:   uint32 internal immutable MinimumDelinquencyGracePeriod;

56:   uint32 internal immutable MaximumDelinquencyGracePeriod;

58:   uint16 internal immutable MinimumReserveRatioBips;

59:   uint16 internal immutable MaximumReserveRatioBips;

61:   uint16 internal immutable MinimumDelinquencyFeeBips;

62:   uint16 internal immutable MaximumDelinquencyFeeBips;

64:   uint32 internal immutable MinimumWithdrawalBatchDuration;

65:   uint32 internal immutable MaximumWithdrawalBatchDuration;

67:   uint16 internal immutable MinimumAnnualInterestBips;

68:   uint16 internal immutable MaximumAnnualInterestBips;

70:   EnumerableSet.AddressSet internal _authorizedLenders;

71:   EnumerableSet.AddressSet internal _controlledMarkets;
44:   uint256 internal immutable ownCreate2Prefix = LibStoredInitCode.getCreate2Prefix(address(this));

46:   uint32 internal immutable MinimumDelinquencyGracePeriod;

47:   uint32 internal immutable MaximumDelinquencyGracePeriod;

49:   uint16 internal immutable MinimumReserveRatioBips;

50:   uint16 internal immutable MaximumReserveRatioBips;

52:   uint16 internal immutable MinimumDelinquencyFeeBips;

53:   uint16 internal immutable MaximumDelinquencyFeeBips;

55:   uint32 internal immutable MinimumWithdrawalBatchDuration;

56:   uint32 internal immutable MaximumWithdrawalBatchDuration;

58:   uint16 internal immutable MinimumAnnualInterestBips;

59:   uint16 internal immutable MaximumAnnualInterestBips;

61:   ProtocolFeeConfiguration internal _protocolFeeConfiguration;

63:   EnumerableSet.AddressSet internal _deployedControllers;

244:   address internal _tmpMarketBorrowerParameter = address(1);
  • WildcatSanctionsEscrow.sol ( #L14 ):
14:   address internal immutable asset;
66:   MarketState internal _state;

68:   mapping(address => Account) internal _accounts;

70:   WithdrawalData internal _withdrawalData;

[N‑68] File is missing NatSpec

It is recommended that Solidity contracts are fully annotated using NatSpec

There are 7 instances:

[N‑69] Modifier declarations should have NatSpec descriptions

There are 7 instances:

  • WildcatArchController.sol ( #L41, #L48 ):
41:   modifier onlyControllerFactory() {

48:   modifier onlyController() {
  • WildcatMarketController.sol ( #L80, #L87 ):
80:   modifier onlyBorrower() {

87:   modifier onlyControlledMarket(address market) {
  • WildcatMarketControllerFactory.sol ( #L65 ):
65:   modifier onlyArchControllerOwner() {
131:   modifier onlyBorrower() {

136:   modifier onlyController() {

[N‑70] Empty bytes check is missing

Passing empty bytes to a function can cause unexpected behavior, such as certain operations failing, producing incorrect results, or wasting gas. It is recommended to check that all byte parameters are not empty.

There is 1 instance:

  • LibStoredInitCode.sol ( #L7 ):
/// @audit data
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

[N‑71] Contract functions should use an interface

All external/public functions should extend an interface. This is useful to ensure that the whole API is extracted and can be more easily integrated by other projects.

There are 93 instances (click to show):
63:   function registerBorrower(address borrower) external onlyOwner {

70:   function removeBorrower(address borrower) external onlyOwner {

77:   function isRegisteredBorrower(address borrower) external view returns (bool) {

81:   function getRegisteredBorrowers() external view returns (address[] memory) {

85:   function getRegisteredBorrowers(
86:     uint256 start,
87:     uint256 end
88:   ) external view returns (address[] memory arr) {

98:   function getRegisteredBorrowersCount() external view returns (uint256) {

106:   function registerControllerFactory(address factory) external onlyOwner {

113:   function removeControllerFactory(address factory) external onlyOwner {

120:   function isRegisteredControllerFactory(address factory) external view returns (bool) {

124:   function getRegisteredControllerFactories() external view returns (address[] memory) {

128:   function getRegisteredControllerFactories(
129:     uint256 start,
130:     uint256 end
131:   ) external view returns (address[] memory arr) {

141:   function getRegisteredControllerFactoriesCount() external view returns (uint256) {

149:   function registerController(address controller) external onlyControllerFactory {

156:   function removeController(address controller) external onlyOwner {

163:   function isRegisteredController(address controller) external view returns (bool) {

167:   function getRegisteredControllers() external view returns (address[] memory) {

171:   function getRegisteredControllers(
172:     uint256 start,
173:     uint256 end
174:   ) external view returns (address[] memory arr) {

184:   function getRegisteredControllersCount() external view returns (uint256) {

192:   function registerMarket(address market) external onlyController {

199:   function removeMarket(address market) external onlyOwner {

206:   function isRegisteredMarket(address market) external view returns (bool) {

210:   function getRegisteredMarkets() external view returns (address[] memory) {

214:   function getRegisteredMarkets(
215:     uint256 start,
216:     uint256 end
217:   ) external view returns (address[] memory arr) {

227:   function getRegisteredMarketsCount() external view returns (uint256) {
121:   function getAuthorizedLenders() external view returns (address[] memory) {

125:   function getAuthorizedLenders(
126:     uint256 start,
127:     uint256 end
128:   ) external view returns (address[] memory arr) {

138:   function getAuthorizedLendersCount() external view returns (uint256) {

142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

153:   function authorizeLenders(address[] memory lenders) external onlyBorrower {

169:   function deauthorizeLenders(address[] memory lenders) external onlyBorrower {

182:   function updateLenderAuthorization(address lender, address[] memory markets) external {

196:   function isControlledMarket(address market) external view returns (bool) {

200:   function getControlledMarkets() external view returns (address[] memory) {

204:   function getControlledMarkets(
205:     uint256 start,
206:     uint256 end
207:   ) external view returns (address[] memory arr) {

217:   function getControlledMarketsCount() external view returns (uint256) {

221:   function computeMarketAddress(
222:     address asset,
223:     string memory namePrefix,
224:     string memory symbolPrefix
225:   ) external view returns (address) {

238:   function getMarketParameters() external view returns (MarketParameters memory parameters) {

291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {

446:   function getParameterConstraints()
447:     external
448:     view
449:     returns (MarketParameterConstraints memory constraints)

468:   function setAnnualInterestBips(
469:     address market,
470:     uint16 annualInterestBips
471:   ) external virtual onlyBorrower onlyControlledMarket(market) {

490:   function resetReserveRatio(address market) external virtual {
126:   function isDeployedController(address controller) external view returns (bool) {

130:   function getDeployedControllersCount() external view returns (uint256) {

134:   function getDeployedControllers() external view returns (address[] memory) {

138:   function getDeployedControllers(
139:     uint256 start,
140:     uint256 end
141:   ) external view returns (address[] memory arr) {

165:   function getProtocolFeeConfiguration()
166:     external
167:     view
168:     returns (

195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {

223:   function getParameterConstraints()
224:     external
225:     view
226:     returns (MarketParameterConstraints memory constraints)

246:   function getMarketControllerParameters()
247:     external
248:     view
249:     virtual
250:     returns (MarketControllerParameters memory parameters)

282:   function deployController() public returns (address controller) {

317:   function deployControllerAndMarket(
318:     string memory namePrefix,
319:     string memory symbolPrefix,
320:     address asset,
321:     uint128 maxTotalSupply,
322:     uint16 annualInterestBips,
323:     uint16 delinquencyFeeBips,
324:     uint32 withdrawalBatchDuration,
325:     uint16 reserveRatioBips,
326:     uint32 delinquencyGracePeriod
327:   ) external returns (address controller, address market) {

342:   function computeControllerAddress(address borrower) external view returns (address) {
26:   function updateState() external nonReentrant {

42:   function depositUpTo(
43:     uint256 amount
44:   ) public virtual nonReentrant returns (uint256 /* actualAmount */) {

86:   function deposit(uint256 amount) external virtual {

96:   function collectFees() external nonReentrant {

119:   function borrow(uint256 amount) external onlyBorrower nonReentrant {

142:   function closeMarket() external onlyController nonReentrant {
223:   function coverageLiquidity() external view nonReentrantView returns (uint256) {

231:   function scaleFactor() external view nonReentrantView returns (uint256) {

238:   function totalAssets() public view returns (uint256) {

252:   function borrowableAssets() external view nonReentrantView returns (uint256) {

260:   function accruedProtocolFees() external view nonReentrantView returns (uint256) {

267:   function previousState() external view returns (MarketState memory) {

276:   function currentState() public view nonReentrantView returns (MarketState memory state) {

285:   function scaledTotalSupply() external view nonReentrantView returns (uint256) {

292:   function scaledBalanceOf(address account) external view nonReentrantView returns (uint256) {

299:   function getAccountRole(address account) external view nonReentrantView returns (AuthRole) {

307:   function withdrawableProtocolFees() external view returns (uint128) {

318:   function effectiveBorrowerAPR() external view returns (uint256) {

334:   function effectiveLenderAPR() external view returns (uint256) {
21:   function maximumDeposit() external view returns (uint256) {

30:   function maxTotalSupply() external view returns (uint256) {

38:   function annualInterestBips() external view returns (uint256) {

42:   function reserveRatioBips() external view returns (uint256) {

74:   function nukeFromOrbit(address accountAddress) external nonReentrant {

88:   function stunningReversal(address accountAddress) external nonReentrant {

112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {

134:   function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyController nonReentrant {

149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {
16:   function balanceOf(address account) public view virtual nonReentrantView returns (uint256) {

22:   function totalSupply() external view virtual nonReentrantView returns (uint256) {

31:   function approve(address spender, uint256 amount) external virtual nonReentrant returns (bool) {

36:   function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {

41:   function transferFrom(
42:     address from,
43:     address to,
44:     uint256 amount
45:   ) external virtual nonReentrant returns (bool) {
24:   function getUnpaidBatchExpiries() external view nonReentrantView returns (uint32[] memory) {

28:   function getWithdrawalBatch(
29:     uint32 expiry
30:   ) external view nonReentrantView returns (WithdrawalBatch memory) {

38:   function getAccountWithdrawalStatus(
39:     address accountAddress,
40:     uint32 expiry
41:   ) external view nonReentrantView returns (AccountWithdrawalStatus memory) {

45:   function getAvailableWithdrawalAmount(
46:     address accountAddress,
47:     uint32 expiry
48:   ) external view nonReentrantView returns (uint256) {

77:   function queueWithdrawal(uint256 amount) external nonReentrant {

137:   function executeWithdrawal(
138:     address accountAddress,
139:     uint32 expiry
140:   ) external nonReentrant returns (uint256) {

190:   function processUnpaidWithdrawalBatch() external nonReentrant {

[N‑72] Don't define functions with the same name in a contract

In Solidity, while function overriding allows for functions with the same name to coexist, it is advisable to avoid this practice to enhance code readability and maintainability. Having multiple functions with the same name, even with different parameters or in inherited contracts, can cause confusion and increase the likelihood of errors during development, testing, and debugging. Using distinct and descriptive function names not only clarifies the purpose and behavior of each function, but also helps prevent unintended function calls or incorrect overriding. By adopting a clear and consistent naming convention, developers can create more comprehensible and maintainable smart contracts.

There are 9 instances (click to show):
/// @audit Different function with same name found on line 81
85:   function getRegisteredBorrowers(

/// @audit Different function with same name found on line 124
128:   function getRegisteredControllerFactories(

/// @audit Different function with same name found on line 167
171:   function getRegisteredControllers(

/// @audit Different function with same name found on line 210
214:   function getRegisteredMarkets(
/// @audit Different function with same name found on line 121
125:   function getAuthorizedLenders(

/// @audit Different function with same name found on line 200
204:   function getControlledMarkets(
  • WildcatMarketControllerFactory.sol ( #L138 ):
/// @audit Different function with same name found on line 134
138:   function getDeployedControllers(
/// @audit Different function with same name found on line 83
87:   function createWithStoredInitCode(

/// @audit Different function with same name found on line 99
106:   function create2WithStoredInitCode(

[N‑73] Assembly block creates dirty bits

Writing data to the free memory pointer without later updating the free memory pointer will cause there to be dirty bits at that memory location. Not updating the free memory pointer will make it harder for the optimizer to reason about whether the memory needs to be cleaned before use, which will lead to worse optimizations. Update the free memory pointer and annotate the block (assembly ("memory-safe") { ... }) to avoid this issue.

There are 7 instances (click to show):
375:     assembly {
376:       // Cache free memory pointer
377:       let freeMemoryPointer := mload(0x40)
378:       // `keccak256(abi.encode(asset, keccak256(namePrefix), keccak256(symbolPrefix)))`
379:       mstore(0x00, asset)
380:       mstore(0x20, keccak256(add(namePrefix, 32), mload(namePrefix)))
381:       mstore(0x40, keccak256(add(symbolPrefix, 32), mload(symbolPrefix)))
382:       salt := keccak256(0, 0x60)
383:       // Restore free memory pointer
384:       mstore(0x40, freeMemoryPointer)
385:     }
59:     assembly {
60:       // Cache the free memory pointer so it can be restored
61:       // at the end
62:       let ptr := mload(0x40)
63: 
64:       // Write 0xff + address to bytes 11:32
65:       mstore(0x00, create2Prefix)
66: 
67:       // Write salt to bytes 32:64
68:       mstore(0x20, salt)
69: 
70:       // Write initcode hash to bytes 64:96
71:       mstore(0x40, initCodeHash)
72: 
73:       // Calculate create2 hash for token0, token1
74:       // The EVM only looks at the last 20 bytes, so the dirty
75:       // bits at the beginning do not need to be cleaned
76:       create2Address := keccak256(0x0b, 0x55)
77: 
78:       // Restore the free memory pointer
79:       mstore(0x40, ptr)
80:     }

91:     assembly {
92:       let initCodePointer := mload(0x40)
93:       let initCodeSize := sub(extcodesize(initCodeStorage), 1)
94:       extcodecopy(initCodeStorage, initCodePointer, 1, initCodeSize)
95:       deployment := create(value, initCodePointer, initCodeSize)
96:     }

111:     assembly {
112:       let initCodePointer := mload(0x40)
113:       let initCodeSize := sub(extcodesize(initCodeStorage), 1)
114:       extcodecopy(initCodeStorage, initCodePointer, 1, initCodeSize)
115:       deployment := create2(value, initCodePointer, initCodeSize, salt)
116:     }
25:   assembly {
26:     str := mload(0x40)
27:     mstore(0x40, add(str, 0x40))
28:     mstore(str, size)
29:     mstore(add(str, 0x20), value)
30:   }

75:     assembly {
76:       str := mload(0x40)
77:       mstore(0x40, add(str, 0x40))
78:       mstore(str, size)
79:       mstore(add(str, 0x20), value)
80:     }

83:     assembly {
84:       str := mload(0x40)
85:       // Get allocation size for the string including the length and data.
86:       // Rounding down returndatasize to nearest word because the returndata
87:       // has an extra offset word.
88:       let allocSize := and(sub(returndatasize(), 1), OnlyFullWordMask)
89:       mstore(0x40, add(str, allocSize))
90:       // Copy returndata after the offset
91:       returndatacopy(str, 0x20, sub(returndatasize(), 0x20))
92:     }

[N‑74] Control structures do not follow the Solidity Style Guide

Refer to the Solidity style guide - Control Structures.

There are 16 instances (click to show):
446:   function getParameterConstraints()
447:     external
448:     view
449:     returns (MarketParameterConstraints memory constraints)
450:   {
79:     if (

106:   function _storeControllerInitCode()
107:     internal
108:     virtual
109:     returns (address initCodeStorage, uint256 initCodeHash)
110:   {

116:   function _storeMarketInitCode()
117:     internal
118:     virtual
119:     returns (address initCodeStorage, uint256 initCodeHash)
120:   {

165:   function getProtocolFeeConfiguration()
166:     external
167:     view
168:     returns (
169:       address feeRecipient,
170:       address originationFeeAsset,
171:       uint80 originationFeeAmount,
172:       uint16 protocolFeeBips
173:     )
174:   {

204:     if (

223:   function getParameterConstraints()
224:     external
225:     view
226:     returns (MarketParameterConstraints memory constraints)
227:   {

246:   function getMarketControllerParameters()
247:     external
248:     view
249:     virtual
250:     returns (MarketControllerParameters memory parameters)
251:   {
  • WildcatSanctionsEscrow.sol ( #L34 ):
34:     if (!canReleaseEscrow()) revert CanNotReleaseEscrow();
  • WildcatSanctionsSentinel.sol ( #L106 ):
106:     if (escrowContract.codehash != bytes32(0)) return escrowContract;
142:   function updateScaleFactorAndFees(
143:     MarketState memory state,
144:     uint256 protocolFeeBips,
145:     uint256 delinquencyFeeBips,
146:     uint256 delinquencyGracePeriod,
147:     uint256 timestamp
148:   )
149:     internal
150:     pure
151:     returns (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee)
152:   {
10: contract WildcatMarket is
11:   WildcatMarketBase,
12:   WildcatMarketConfig,
13:   WildcatMarketToken,
14:   WildcatMarketWithdrawals
15: {

57:     if (scaledAmount == 0) revert NullMintAmount();
132:     if (msg.sender != borrower) revert NotApprovedBorrower();

137:     if (msg.sender != controller) revert NotController();

399:   function _calculateCurrentState()
400:     internal
401:     view
402:     returns (
403:       MarketState memory state,
404:       uint32 expiredBatchExpiry,
405:       WithdrawalBatch memory expiredBatch
406:     )
407:   {

[N‑75] Functions contain the same code

The functions below have the same implementation as is seen in other files. The functions should be refactored into functions of a common base contract.

There are 4 instances (click to show):
/// @audit Seen on line 223 of src/WildcatMarketControllerFactory.sol
446:   function getParameterConstraints()
447:     external
448:     view
449:     returns (MarketParameterConstraints memory constraints)
450:   {
451:     constraints.minimumDelinquencyGracePeriod = MinimumDelinquencyGracePeriod;
452:     constraints.maximumDelinquencyGracePeriod = MaximumDelinquencyGracePeriod;
453:     constraints.minimumReserveRatioBips = MinimumReserveRatioBips;
454:     constraints.maximumReserveRatioBips = MaximumReserveRatioBips;
455:     constraints.minimumDelinquencyFeeBips = MinimumDelinquencyFeeBips;
456:     constraints.maximumDelinquencyFeeBips = MaximumDelinquencyFeeBips;
457:     constraints.minimumWithdrawalBatchDuration = MinimumWithdrawalBatchDuration;
458:     constraints.maximumWithdrawalBatchDuration = MaximumWithdrawalBatchDuration;
459:     constraints.minimumAnnualInterestBips = MinimumAnnualInterestBips;
460:     constraints.maximumAnnualInterestBips = MaximumAnnualInterestBips;
461:   }
  • WildcatMarketControllerFactory.sol ( #L223-L238 ):
/// @audit Seen on line 446 of src/WildcatMarketController.sol
223:   function getParameterConstraints()
224:     external
225:     view
226:     returns (MarketParameterConstraints memory constraints)
227:   {
228:     constraints.minimumDelinquencyGracePeriod = MinimumDelinquencyGracePeriod;
229:     constraints.maximumDelinquencyGracePeriod = MaximumDelinquencyGracePeriod;
230:     constraints.minimumReserveRatioBips = MinimumReserveRatioBips;
231:     constraints.maximumReserveRatioBips = MaximumReserveRatioBips;
232:     constraints.minimumDelinquencyFeeBips = MinimumDelinquencyFeeBips;
233:     constraints.maximumDelinquencyFeeBips = MaximumDelinquencyFeeBips;
234:     constraints.minimumWithdrawalBatchDuration = MinimumWithdrawalBatchDuration;
235:     constraints.maximumWithdrawalBatchDuration = MaximumWithdrawalBatchDuration;
236:     constraints.minimumAnnualInterestBips = MinimumAnnualInterestBips;
237:     constraints.maximumAnnualInterestBips = MaximumAnnualInterestBips;
238:   }
/// @audit Seen on line 30 of src/libraries/MathUtils.sol
19:   function calculateLinearInterestFromBips(
20:     uint256 rateBip,
21:     uint256 timeDelta
22:   ) internal pure returns (uint256 result) {
23:     uint256 rate = rateBip.bipToRay();
24:     uint256 accumulatedInterestRay = rate * timeDelta;
25:     unchecked {
26:       return accumulatedInterestRay / SECONDS_IN_365_DAYS;
27:     }
28:   }
/// @audit Seen on line 19 of src/libraries/FeeMath.sol
30:   function calculateLinearInterestFromBips(
31:     uint256 rateBip,
32:     uint256 timeDelta
33:   ) internal pure returns (uint256 result) {
34:     uint256 rate = rateBip.bipToRay();
35:     uint256 accumulatedInterestRay = rate * timeDelta;
36:     unchecked {
37:       return accumulatedInterestRay / SECONDS_IN_365_DAYS;
38:     }
39:   }

[N‑76] Missing event for critical changes

Events should be emitted when critical changes are made to the contracts.

There are 2 instances:

468:   function setAnnualInterestBips(
469:     address market,
470:     uint16 annualInterestBips
471:   ) external virtual onlyBorrower onlyControlledMarket(market) {
  • WildcatMarketControllerFactory.sol ( #L195-L200 ):
195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {

[N‑77] Consider adding emergency-stop functionality

Adding a way to quickly halt protocol functionality in an emergency, rather than having to pause individual contracts one-by-one, will make in-progress hack mitigation faster and much less stressful.

There are 3 instances:

  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
13: contract WildcatMarketControllerFactory {

[N‑78] Avoid the use of sensitive terms

Use alternative variants, e.g. allowlist/denylist instead of whitelist/blacklist.

There are 2 instances:

26:   /// @dev Account with blacklist control, used for blocking sanctioned addresses.

153:       revert AccountBlacklisted();

[N‑79] Consider adding a block/deny-list

Doing so will significantly increase centralization, but will help to prevent hackers from using stolen tokens

There are 11 instances (click to show):
  • ReentrancyGuard.sol ( #L12 ):
12: contract ReentrancyGuard {
  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
13: contract WildcatMarketControllerFactory {
  • WildcatSanctionsEscrow.sol ( #L10 ):
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {
  • WildcatSanctionsSentinel.sol ( #L10 ):
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {
10: contract WildcatMarket is
11:   WildcatMarketBase,
12:   WildcatMarketConfig,
13:   WildcatMarketToken,
14:   WildcatMarketWithdrawals
15: {
  • WildcatMarketBase.sol ( #L14 ):
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketToken.sol ( #L6 ):
6: contract WildcatMarketToken is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[N‑80] Enable IR-based code generation

The IR-based code generator was introduced with an aim to not only allow code generation to be more transparent and auditable but also to enable more powerful optimization passes that span across functions. You can enable it on the command line using --via-ir or with the option {"viaIR": true}. This will take longer to compile, but you can just simple test it before deploying and if you got a better benchmark then you can add --via-ir to your deploy command More on: https://docs.soliditylang.org/en/v0.8.17/ir-breaking-changes.html

There is 1 instance:

  • Global finding

[N‑81] Contracts should have NatSpec @dev tags

The @dev tag is used to explain extra details to developers.

There are 19 instances (click to show):
  • ReentrancyGuard.sol ( #L12 ):
12: contract ReentrancyGuard {
  • WildcatArchController.sol ( #L8 ):
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
13: contract WildcatMarketControllerFactory {
  • WildcatSanctionsEscrow.sol ( #L10 ):
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {
  • WildcatSanctionsSentinel.sol ( #L10 ):
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {
  • BoolUtils.sol ( #L4 ):
4: library BoolUtils {
  • FIFOQueue.sol ( #L16 ):
16: library FIFOQueueLib {
  • FeeMath.sol ( #L11 ):
11: library FeeMath {
  • LibStoredInitCode.sol ( #L4 ):
4: library LibStoredInitCode {
  • MarketState.sol ( #L44 ):
44: library MarketStateLib {
  • MathUtils.sol ( #L16 ):
16: library MathUtils {
  • SafeCastLib.sol ( #L6 ):
6: library SafeCastLib {
  • Withdrawal.sol ( #L37 ):
37: library WithdrawalLib {
  • WildcatMarket.sol ( #L10 ):
10: contract WildcatMarket is
  • WildcatMarketBase.sol ( #L14 ):
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketToken.sol ( #L6 ):
6: contract WildcatMarketToken is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[N‑82] Error declarations should have NatSpec descriptions

There are 19 instances (click to show):
16:   error NotControllerFactory();

17:   error NotController();

19:   error BorrowerAlreadyExists();

20:   error ControllerFactoryAlreadyExists();

21:   error ControllerAlreadyExists();

22:   error MarketAlreadyExists();

24:   error BorrowerDoesNotExist();

25:   error ControllerFactoryDoesNotExist();

26:   error ControllerDoesNotExist();

27:   error MarketDoesNotExist();
24:   error NotRegisteredBorrower();

25:   error InvalidProtocolFeeConfiguration();

26:   error CallerNotArchControllerOwner();

27:   error InvalidConstraints();

28:   error ControllerAlreadyDeployed();
  • FIFOQueue.sol ( #L17 ):
17:   error FIFOQueueOutOfBounds();
  • LibStoredInitCode.sol ( #L5 ):
5:   error InitCodeDeploymentFailed();
16: error InvalidReturnDataString();

17: error InvalidCompactString();

[N‑83] Functions should have @notice tags

The @notice is used to explain to users what the function does. The compiler interprets /// or /** comments as this tag if one wasn't explicitly provided.

There are 124 instances (click to show):
55:   constructor() {

63:   function registerBorrower(address borrower) external onlyOwner {

70:   function removeBorrower(address borrower) external onlyOwner {

77:   function isRegisteredBorrower(address borrower) external view returns (bool) {

81:   function getRegisteredBorrowers() external view returns (address[] memory) {

85:   function getRegisteredBorrowers(

98:   function getRegisteredBorrowersCount() external view returns (uint256) {

106:   function registerControllerFactory(address factory) external onlyOwner {

113:   function removeControllerFactory(address factory) external onlyOwner {

120:   function isRegisteredControllerFactory(address factory) external view returns (bool) {

124:   function getRegisteredControllerFactories() external view returns (address[] memory) {

128:   function getRegisteredControllerFactories(

141:   function getRegisteredControllerFactoriesCount() external view returns (uint256) {

149:   function registerController(address controller) external onlyControllerFactory {

156:   function removeController(address controller) external onlyOwner {

163:   function isRegisteredController(address controller) external view returns (bool) {

167:   function getRegisteredControllers() external view returns (address[] memory) {

171:   function getRegisteredControllers(

184:   function getRegisteredControllersCount() external view returns (uint256) {

192:   function registerMarket(address market) external onlyController {

199:   function removeMarket(address market) external onlyOwner {

206:   function isRegisteredMarket(address market) external view returns (bool) {

210:   function getRegisteredMarkets() external view returns (address[] memory) {

214:   function getRegisteredMarkets(

227:   function getRegisteredMarketsCount() external view returns (uint256) {
94:   constructor() {

125:   function getAuthorizedLenders(

138:   function getAuthorizedLendersCount() external view returns (uint256) {

142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

196:   function isControlledMarket(address market) external view returns (bool) {

200:   function getControlledMarkets() external view returns (address[] memory) {

204:   function getControlledMarkets(

217:   function getControlledMarketsCount() external view returns (uint256) {

221:   function computeMarketAddress(

255:   function _resetTmpMarketParameters() internal {

490:   function resetReserveRatio(address market) external virtual {

503:   function assertValueInRange(
72:   constructor(

106:   function _storeControllerInitCode()

116:   function _storeMarketInitCode()

126:   function isDeployedController(address controller) external view returns (bool) {

130:   function getDeployedControllersCount() external view returns (uint256) {

134:   function getDeployedControllers() external view returns (address[] memory) {

138:   function getDeployedControllers(

246:   function getMarketControllerParameters()

342:   function computeControllerAddress(address borrower) external view returns (address) {
16:   constructor() {

21:   function balance() public view override returns (uint256) {

25:   function canReleaseEscrow() public view override returns (bool) {

29:   function escrowedAsset() public view override returns (address, uint256) {

33:   function releaseEscrow() public override {
  • WildcatSanctionsSentinel.sol ( #L24, #L30 ):
24:   constructor(address _archController, address _chainalysisSanctionsList) {

30:   function _resetTmpEscrowParams() internal {
5:   function and(bool a, bool b) internal pure returns (bool c) {

11:   function or(bool a, bool b) internal pure returns (bool c) {

17:   function xor(bool a, bool b) internal pure returns (bool c) {
19:   function empty(FIFOQueue storage arr) internal view returns (bool) {

23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

55:   function push(FIFOQueue storage arr, uint32 value) internal {

61:   function shift(FIFOQueue storage arr) internal {

70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
30:   function calculateBaseInterest(

40:   function applyProtocolFee(

53:   function updateDelinquency(
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

54:   function calculateCreate2Address(

83:   function createWithStoredInitCode(address initCodeStorage) internal returns (address deployment) {

87:   function createWithStoredInitCode(

99:   function create2WithStoredInitCode(

106:   function create2WithStoredInitCode(
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
7:   function _assertNonOverflow(bool didNotOverflow) private pure {

17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
19: function bytes32ToString(bytes32 value) pure returns (string memory str) {

33: function queryStringOrBytes32AsString(

96: function queryName(address target) view returns (string memory) {

101: function querySymbol(address target) view returns (string memory) {
  • Withdrawal.sol ( #L38 ):
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {
76:   constructor() {

528:   function _applyWithdrawalBatchPaymentView(
  • WildcatMarketConfig.sol ( #L42 ):
42:   function reserveRatioBips() external view returns (uint256) {
31:   function approve(address spender, uint256 amount) external virtual nonReentrant returns (bool) {

36:   function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {

41:   function transferFrom(

59:   function _approve(address approver, address spender, uint256 amount) internal virtual {

64:   function _transfer(address from, address to, uint256 amount) internal virtual {
28:   function getWithdrawalBatch(

38:   function getAccountWithdrawalStatus(

45:   function getAvailableWithdrawalAmount(

190:   function processUnpaidWithdrawalBatch() external nonReentrant {

[N‑84] Contracts should have full test coverage

While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.

There is 1 instance:

  • Global finding

[N‑85] Large or complicated code bases should implement invariant tests

This includes: large code bases, or code with lots of inline-assembly, complicated math, or complicated interactions between multiple contracts. Invariant fuzzers such as Echidna require the test writer to come up with invariants which should not be violated under any circumstances, and the fuzzer tests various inputs and function calls to ensure that the invariants always hold. Even code with 100% code coverage can still have bugs due to the order of the operations a user performs, and invariant fuzzers may help significantly.

There is 1 instance:

  • Global finding

[N‑86] Top-level declarations should be separated by at least two lines

There are 145 instances (click to show):
2: pragma solidity >=0.8.20;
3: 
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';

6: import './libraries/MathUtils.sol';
7: 
8: contract WildcatArchController is Ownable {

46:   }
47: 
48:   modifier onlyController() {

68:   }
69: 
70:   function removeBorrower(address borrower) external onlyOwner {

75:   }
76: 
77:   function isRegisteredBorrower(address borrower) external view returns (bool) {

79:   }
80: 
81:   function getRegisteredBorrowers() external view returns (address[] memory) {

83:   }
84: 
85:   function getRegisteredBorrowers(

96:   }
97: 
98:   function getRegisteredBorrowersCount() external view returns (uint256) {

111:   }
112: 
113:   function removeControllerFactory(address factory) external onlyOwner {

118:   }
119: 
120:   function isRegisteredControllerFactory(address factory) external view returns (bool) {

122:   }
123: 
124:   function getRegisteredControllerFactories() external view returns (address[] memory) {

126:   }
127: 
128:   function getRegisteredControllerFactories(

139:   }
140: 
141:   function getRegisteredControllerFactoriesCount() external view returns (uint256) {

154:   }
155: 
156:   function removeController(address controller) external onlyOwner {

161:   }
162: 
163:   function isRegisteredController(address controller) external view returns (bool) {

165:   }
166: 
167:   function getRegisteredControllers() external view returns (address[] memory) {

169:   }
170: 
171:   function getRegisteredControllers(

182:   }
183: 
184:   function getRegisteredControllersCount() external view returns (uint256) {

197:   }
198: 
199:   function removeMarket(address market) external onlyOwner {

204:   }
205: 
206:   function isRegisteredMarket(address market) external view returns (bool) {

208:   }
209: 
210:   function getRegisteredMarkets() external view returns (address[] memory) {

212:   }
213: 
214:   function getRegisteredMarkets(

225:   }
226: 
227:   function getRegisteredMarketsCount() external view returns (uint256) {
2: pragma solidity >=0.8.20;
3: 
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';

30: }
31: 
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {

85:   }
86: 
87:   modifier onlyControlledMarket(address market) {

123:   }
124: 
125:   function getAuthorizedLenders(

136:   }
137: 
138:   function getAuthorizedLendersCount() external view returns (uint256) {

140:   }
141: 
142:   function isAuthorizedLender(address lender) external view virtual returns (bool) {

198:   }
199: 
200:   function getControlledMarkets() external view returns (address[] memory) {

202:   }
203: 
204:   function getControlledMarkets(

215:   }
216: 
217:   function getControlledMarketsCount() external view returns (uint256) {

219:   }
220: 
221:   function computeMarketAddress(

253:   }
254: 
255:   function _resetTmpMarketParameters() internal {

488:   }
489: 
490:   function resetReserveRatio(address market) external virtual {

501:   }
502: 
503:   function assertValueInRange(
2: pragma solidity >=0.8.20;
3: 
4: import { EnumerableSet } from 'openzeppelin/contracts/utils/structs/EnumerableSet.sol';

11: import './WildcatMarketController.sol';
12: 
13: contract WildcatMarketControllerFactory {

104:   }
105: 
106:   function _storeControllerInitCode()

114:   }
115: 
116:   function _storeMarketInitCode()

124:   }
125: 
126:   function isDeployedController(address controller) external view returns (bool) {

128:   }
129: 
130:   function getDeployedControllersCount() external view returns (uint256) {

132:   }
133: 
134:   function getDeployedControllers() external view returns (address[] memory) {

136:   }
137: 
138:   function getDeployedControllers(

340:   }
341: 
342:   function computeControllerAddress(address borrower) external view returns (address) {
2: pragma solidity >=0.8.20;
3: 
4: import { IERC20 } from './interfaces/IERC20.sol';

8: import { IWildcatSanctionsEscrow } from './interfaces/IWildcatSanctionsEscrow.sol';
9: 
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {

19:   }
20: 
21:   function balance() public view override returns (uint256) {

23:   }
24: 
25:   function canReleaseEscrow() public view override returns (bool) {

27:   }
28: 
29:   function escrowedAsset() public view override returns (address, uint256) {

31:   }
32: 
33:   function releaseEscrow() public override {
2: pragma solidity >=0.8.20;
3: 
4: import { IChainalysisSanctionsList } from './interfaces/IChainalysisSanctionsList.sol';

8: import { WildcatSanctionsEscrow } from './WildcatSanctionsEscrow.sol';
9: 
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {

28:   }
29: 
30:   function _resetTmpEscrowParams() internal {
2: pragma solidity >=0.8.20;
3: 
4: library BoolUtils {

2: pragma solidity >=0.8.20;
3: 
4: library BoolUtils {

9:   }
10: 
11:   function or(bool a, bool b) internal pure returns (bool c) {

15:   }
16: 
17:   function xor(bool a, bool b) internal pure returns (bool c) {
2: pragma solidity ^0.8.20;
3: 
4: import '../interfaces/IChainalysisSanctionsList.sol';
14: using FIFOQueueLib for FIFOQueue global;
15: 
16: library FIFOQueueLib {

21:   }
22: 
23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

28:   }
29: 
30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

36:   }
37: 
38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

40:   }
41: 
42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

53:   }
54: 
55:   function push(FIFOQueue storage arr, uint32 value) internal {

59:   }
60: 
61:   function shift(FIFOQueue storage arr) internal {

68:   }
69: 
70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
2: pragma solidity >=0.8.20;
3: 
4: import './MathUtils.sol';

9: using MathUtils for uint256;
10: 
11: library FeeMath {

28:   }
29: 
30:   function calculateBaseInterest(

38:   }
39: 
40:   function applyProtocolFee(

51:   }
52: 
53:   function updateDelinquency(
2: pragma solidity >=0.8.20;
3: 
4: library LibStoredInitCode {

2: pragma solidity >=0.8.20;
3: 
4: library LibStoredInitCode {

52:   }
53: 
54:   function calculateCreate2Address(

81:   }
82: 
83:   function createWithStoredInitCode(address initCodeStorage) internal returns (address deployment) {

85:   }
86: 
87:   function createWithStoredInitCode(

97:   }
98: 
99:   function create2WithStoredInitCode(

104:   }
105: 
106:   function create2WithStoredInitCode(
2: pragma solidity >=0.8.20;
3: 
4: import { AuthRole } from '../interfaces/WildcatStructsAndEnums.sol';

42: }
43: 
44: library MarketStateLib {

128:   }
129: 
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

136:   }
137: 
138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
2: pragma solidity >=0.8.20;
3: 
4: import './Errors.sol';

14: uint256 constant SECONDS_IN_365_DAYS = 365 days;
15: 
16: library MathUtils {
2: pragma solidity >=0.8.20;
3: 
4: import './Errors.sol';

4: import './Errors.sol';
5: 
6: library SafeCastLib {

15:   }
16: 
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

19:   }
20: 
21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

23:   }
24: 
25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

27:   }
28: 
29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

31:   }
32: 
33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

35:   }
36: 
37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

39:   }
40: 
41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

43:   }
44: 
45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

47:   }
48: 
49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

51:   }
52: 
53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

55:   }
56: 
57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

59:   }
60: 
61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

63:   }
64: 
65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

67:   }
68: 
69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

71:   }
72: 
73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

75:   }
76: 
77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

79:   }
80: 
81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

83:   }
84: 
85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

87:   }
88: 
89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

91:   }
92: 
93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

95:   }
96: 
97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

99:   }
100: 
101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

103:   }
104: 
105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

107:   }
108: 
109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

111:   }
112: 
113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

115:   }
116: 
117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

119:   }
120: 
121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

123:   }
124: 
125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

127:   }
128: 
129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

131:   }
132: 
133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

135:   }
136: 
137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
2: pragma solidity >=0.8.20;
3: 
4: import { LibBit } from 'solady/utils/LibBit.sol';

31: }
32: 
33: function queryStringOrBytes32AsString(

94: }
95: 
96: function queryName(address target) view returns (string memory) {

99: }
100: 
101: function querySymbol(address target) view returns (string memory) {
2: pragma solidity >=0.8.20;
3: 
4: import './MarketState.sol';

35: }
36: 
37: library WithdrawalLib {
2: pragma solidity >=0.8.20;
3: 
4: import '../libraries/FeeMath.sol';

8: import './WildcatMarketWithdrawals.sol';
9: 
10: contract WildcatMarket is
2: pragma solidity >=0.8.20;
3: 
4: import '../libraries/FeeMath.sol';

12: import '../libraries/BoolUtils.sol';
13: 
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {

134:   }
135: 
136:   modifier onlyController() {

526:   }
527: 
528:   function _applyWithdrawalBatchPaymentView(
2: pragma solidity >=0.8.20;
3: 
4: import '../interfaces/IWildcatSanctionsSentinel.sol';

7: import './WildcatMarketBase.sol';
8: 
9: contract WildcatMarketConfig is WildcatMarketBase {

40:   }
41: 
42:   function reserveRatioBips() external view returns (uint256) {
2: pragma solidity >=0.8.20;
3: 
4: import './WildcatMarketBase.sol';

4: import './WildcatMarketBase.sol';
5: 
6: contract WildcatMarketToken is WildcatMarketBase {

34:   }
35: 
36:   function transfer(address to, uint256 amount) external virtual nonReentrant returns (bool) {

39:   }
40: 
41:   function transferFrom(

57:   }
58: 
59:   function _approve(address approver, address spender, uint256 amount) internal virtual {

62:   }
63: 
64:   function _transfer(address from, address to, uint256 amount) internal virtual {
2: pragma solidity >=0.8.20;
3: 
4: import './WildcatMarketBase.sol';

9: import 'solady/utils/SafeTransferLib.sol';
10: 
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

26:   }
27: 
28:   function getWithdrawalBatch(

36:   }
37: 
38:   function getAccountWithdrawalStatus(

43:   }
44: 
45:   function getAvailableWithdrawalAmount(

188:   }
189: 
190:   function processUnpaidWithdrawalBatch() external nonReentrant {

[N‑87] Consider adding formal verification proofs

Formal verification is the act of proving or disproving the correctness of intended algorithms underlying a system with respect to a certain formal specification/property/invariant, using formal methods of mathematics.

Some tools that are currently available to perform these tests on smart contracts are SMTChecker and Certora Prover.

There are 22 instances (click to show):

Gas Optimizations

[G‑01] The <array>.length should be cached outside of the for-loop

The overheads outlined below are PER LOOP, excluding the first loop:

  • storage arrays incur a Gwarmaccess (100 gas)
  • memory arrays use MLOAD (3 gas)
  • calldata arrays use CALLDATALOAD (3 gas) Caching the length changes each of these to a DUP<N> (3 gas), and gets rid of the extra DUP<N> needed to store the stack offset.

There are 3 instances:

154:     for (uint256 i = 0; i < lenders.length; i++) {

170:     for (uint256 i = 0; i < lenders.length; i++) {

183:     for (uint256 i; i < markets.length; i++) {

[G‑02] Use shift right instead of division if possible

Shifting right by n is like dividing by 2^n, while the shifting cost less gas because it does not require checks and jumps.

There are 2 instances:

/// @audit div 8
23:     size = (sizeInBits + 7) / 8;

/// @audit div 8
73:       size = (sizeInBits + 7) / 8;

[G‑03] Use storage instead of memory for structs/arrays

When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declaring the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incurring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct.

There are 8 instances (click to show):
  • WildcatMarketController.sol ( #L491 ):
491:     TemporaryReserveRatio memory tmp = temporaryExcessReserveRatio[market];
164:     Account memory account = _accounts[accountAddress];

468:     WithdrawalBatch memory batch = _withdrawalData.batches[expiry];
  • WildcatMarketConfig.sol ( #L89 ):
89:     Account memory account = _accounts[accountAddress];
59:     AccountWithdrawalStatus memory status = _withdrawalData.accountStatuses[expiry][accountAddress];

101:     WithdrawalBatch memory batch = _withdrawalData.batches[expiry];

146:     WithdrawalBatch memory batch = _withdrawalData.batches[expiry];

197:     WithdrawalBatch memory batch = _withdrawalData.batches[expiry];

[G‑04] Using private for constants saves gas

If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table

There are 2 instances:

  • WildcatSanctionsSentinel.sol ( #L11-L12 ):
11:   bytes32 public constant override WildcatSanctionsEscrowInitcodeHash =
12:     keccak256(type(WildcatSanctionsEscrow).creationCode);
  • WildcatMarketBase.sol ( #L24 ):
24:   string public constant version = '1.0';

[G‑05] Constructors can be marked as payable to save deployment gas

Payable functions cost less gas to execute, because the compiler does not have to add extra checks to ensure that no payment is provided. A constructor can be safely marked as payable, because only the deployer would be able to pass funds, and the project itself would not pass any funds.

There are 7 instances (click to show):
  • ReentrancyGuard.sol ( #L49 ):
49:   constructor() {
  • WildcatArchController.sol ( #L55 ):
55:   constructor() {
  • WildcatMarketController.sol ( #L94 ):
94:   constructor() {
  • WildcatMarketControllerFactory.sol ( #L72-L76 ):
72:   constructor(
73:     address _archController,
74:     address _sentinel,
75:     MarketParameterConstraints memory constraints
76:   ) {
  • WildcatSanctionsEscrow.sol ( #L16 ):
16:   constructor() {
  • WildcatSanctionsSentinel.sol ( #L24 ):
24:   constructor(address _archController, address _chainalysisSanctionsList) {
  • WildcatMarketBase.sol ( #L76 ):
76:   constructor() {

[G‑06] State variables only set in the constructor should be declared immutable

This can avoid a Gsset (20000 gas) on deployment (in constructor), and replaces the first access in each transaction (Gcoldsload - 2100 gas) and each access thereafter (Gwarmacces - 100 gas) with a PUSH32 (3 gas). While strings are not value types, and therefore cannot be immutable/constant if not hard-coded outside of the constructor, the same behavior can be achieved by making the current contract abstract with virtual functions for the string accessors, and having a child contract override the functions with the hard-coded implementation-specific values.

There are 2 instances:

97:     name = string.concat(parameters.namePrefix, queryName(parameters.asset));

98:     symbol = string.concat(parameters.symbolPrefix, querySymbol(parameters.asset));

[G‑07] Using ++X/--X instead of X++/X-- can save gas

It can save 5 gas for each execution / per iteration.

There are 12 instances (click to show):
93:     for (uint256 i = 0; i < count; i++) {

136:     for (uint256 i = 0; i < count; i++) {

179:     for (uint256 i = 0; i < count; i++) {

222:     for (uint256 i = 0; i < count; i++) {
133:     for (uint256 i = 0; i < count; i++) {

154:     for (uint256 i = 0; i < lenders.length; i++) {

170:     for (uint256 i = 0; i < lenders.length; i++) {

183:     for (uint256 i; i < markets.length; i++) {

212:     for (uint256 i = 0; i < count; i++) {
  • WildcatMarketControllerFactory.sol ( #L146 ):
146:     for (uint256 i = 0; i < count; i++) {
48:     for (uint256 i = 0; i < len; i++) {

75:     for (uint256 i = 0; i < n; i++) {

[G‑08] Unnecessary event parameters should be removed

Data that can be obtained from off-chain data and event log details should not be added as event parameters to save gas.

There is 1 instance:

  • WildcatMarket.sol ( #L160 ):
/// @audit `block.timestamp`
160:     emit MarketClosed(block.timestamp);

[G‑09] Unused named return variables without optimizer waste gas

Consider changing the variable to be an unnamed one, since the variable is never assigned, nor is it returned by name. If the optimizer is not turned on, leaving the code as it is will also waste gas for the stack variable.

There are 8 instances (click to show):
  • WildcatMarketControllerFactory.sol ( #L165-L173 ):
/// @audit feeRecipient
/// @audit originationFeeAsset
/// @audit originationFeeAmount
/// @audit protocolFeeBips
165:   function getProtocolFeeConfiguration()
166:     external
167:     view
168:     returns (
169:       address feeRecipient,
170:       address originationFeeAsset,
171:       uint80 originationFeeAmount,
172:       uint16 protocolFeeBips
173:     )
  • WildcatSanctionsSentinel.sol ( #L65-L69 ):
/// @audit escrowAddress
65:   function getEscrowAddress(
66:     address borrower,
67:     address account,
68:     address asset
69:   ) public view override returns (address escrowAddress) {
/// @audit result
19:   function calculateLinearInterestFromBips(
20:     uint256 rateBip,
21:     uint256 timeDelta
22:   ) internal pure returns (uint256 result) {
/// @audit _liquidityRequired
87:   function liquidityRequired(
88:     MarketState memory state
89:   ) internal pure returns (uint256 _liquidityRequired) {
/// @audit result
30:   function calculateLinearInterestFromBips(
31:     uint256 rateBip,
32:     uint256 timeDelta
33:   ) internal pure returns (uint256 result) {

[G‑10] Use unchecked block for safe subtractions

If it can be confirmed that the subtraction operation will not overflow, using an unchecked block can save gas. For example, require(x <= y); z = y - x; can be optimized to require(x <= y); unchecked { z = y - x; }.

There are 2 instances:

/// @audit checked on line 152
154:       asset.safeTransferFrom(borrower, address(this), totalDebts - currentlyHeld);

/// @audit checked on line 152
157:       asset.safeTransfer(borrower, currentlyHeld - totalDebts);

[G‑11] internal functions only called once can be inlined to save gas

If an internal function is only used once, there is no need to modularize it, unless the function calling it would otherwise be too long and complex. Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.

There are 5 instances:

59:   function _setReentrancyGuard() internal {

72:   function _clearReentrancyGuard() internal {
  • WildcatMarketController.sol ( #L255 ):
255:   function _resetTmpMarketParameters() internal {
466:   function _processExpiredWithdrawalBatch(MarketState memory state) internal {

528:   function _applyWithdrawalBatchPaymentView(
529:     WithdrawalBatch memory batch,
530:     MarketState memory state,
531:     uint256 availableLiquidity
532:   ) internal pure {

[G‑12] Functions that revert when called by normal users can be marked payable

If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are: CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which cost an average of about 21 gas per call to the function, in addition to the extra deployment cost.

There are 19 instances (click to show):
63:   function registerBorrower(address borrower) external onlyOwner {

70:   function removeBorrower(address borrower) external onlyOwner {

106:   function registerControllerFactory(address factory) external onlyOwner {

113:   function removeControllerFactory(address factory) external onlyOwner {

149:   function registerController(address controller) external onlyControllerFactory {

156:   function removeController(address controller) external onlyOwner {

192:   function registerMarket(address market) external onlyController {

199:   function removeMarket(address market) external onlyOwner {
153:   function authorizeLenders(address[] memory lenders) external onlyBorrower {

169:   function deauthorizeLenders(address[] memory lenders) external onlyBorrower {

468:   function setAnnualInterestBips(
469:     address market,
470:     uint16 annualInterestBips
471:   ) external virtual onlyBorrower onlyControlledMarket(market) {
195:   function setProtocolFeeConfiguration(
196:     address feeRecipient,
197:     address originationFeeAsset,
198:     uint80 originationFeeAmount,
199:     uint16 protocolFeeBips
200:   ) external onlyArchControllerOwner {

282:   function deployController() public returns (address controller) {
119:   function borrow(uint256 amount) external onlyBorrower nonReentrant {

142:   function closeMarket() external onlyController nonReentrant {
112:   function updateAccountAuthorization(
113:     address _account,
114:     bool _isAuthorized
115:   ) external onlyController nonReentrant {

134:   function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyController nonReentrant {

149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {

[G‑13] Use s.x = s.x + y instead of s.x += y for memory structs

Using the s.x = s.x + y instead of s.x += y for memory structs can save 100 gas. The same applies to -=, *=, etc.

There are 19 instances (click to show):
64:     account.scaledBalance += scaledAmount;

71:     state.scaledTotalSupply += scaledAmount;

105:     state.accruedProtocolFees -= withdrawableFees;
513:     batch.scaledAmountBurned += scaledAmountBurned;

514:     batch.normalizedAmountPaid += normalizedAmountPaid;

515:     state.scaledPendingWithdrawals -= scaledAmountBurned;

518:     state.normalizedUnclaimedWithdrawals += normalizedAmountPaid;

521:     state.scaledTotalSupply -= scaledAmountBurned;

542:     batch.scaledAmountBurned += scaledAmountBurned;

543:     batch.normalizedAmountPaid += normalizedAmountPaid;

544:     state.scaledPendingWithdrawals -= scaledAmountBurned;

547:     state.normalizedUnclaimedWithdrawals += normalizedAmountPaid;

550:     state.scaledTotalSupply -= scaledAmountBurned;
  • WildcatMarketToken.sol ( #L73, #L77 ):
73:     fromAccount.scaledBalance -= scaledAmount;

77:     toAccount.scaledBalance += scaledAmount;
89:     account.scaledBalance -= scaledAmount;

105:     batch.scaledTotalAmount += scaledAmount;

106:     state.scaledPendingWithdrawals += scaledAmount;

158:     state.normalizedUnclaimedWithdrawals -= normalizedAmountWithdrawn;

[G‑14] Operator >=/<= costs less gas than operator >/<

The compiler uses opcodes GT and ISZERO for code that uses >, but only requires LT for >=. A similar behavior applies for >, which uses opcodes LT and ISZERO, but only requires GT for <=. It can save 3 gas for each. It should be converted to the <=/>= equivalent when comparing against integer literals.

There are 21 instances (click to show):
81:       constraints.maximumAnnualInterestBips > 10000 ||

83:       constraints.maximumDelinquencyFeeBips > 10000 ||

85:       constraints.maximumReserveRatioBips > 10000 ||

201:     bool hasOriginationFee = originationFeeAmount > 0;

205:       (protocolFeeBips > 0 && nullFeeRecipient) ||
67:     if (timeWithPenalty > 0) {

155:     if (protocolFeeBips > 0) {

159:     if (delinquencyFeeBips > 0) {
  • WildcatMarket.sol ( #L147 ):
147:     if (_withdrawalData.unpaidBatches.length() > 0) {
79:     if ((parameters.protocolFeeBips > 0).and(parameters.feeRecipient == address(0))) {

82:     if (parameters.annualInterestBips > BIP) {

85:     if (parameters.reserveRatioBips > BIP) {

88:     if (parameters.protocolFeeBips > BIP) {

91:     if (parameters.delinquencyFeeBips > BIP) {

170:       if (scaledBalance > 0) {

428:       if (availableLiquidity > 0) {

472:     if (availableLiquidity > 0) {
152:     if (_annualInterestBips > BIP) {

172:     if (_reserveRatioBips > BIP) {
  • WildcatMarketWithdrawals.sol ( #L32, #L112 ):
32:     if ((expiry == expiredBatchExpiry).and(expiry > 0)) {

112:     if (availableLiquidity > 0) {

[G‑15] Usage of ints/uints smaller than 32 bytes incurs overhead

Using ints/uints smaller than 32 bytes may cost more gas. This is because the EVM operates on 32 bytes at a time, so if an element is smaller than 32 bytes, the EVM must perform more operations to reduce the size of the element from 32 bytes to the desired size.

There are 143 instances (click to show):
/// @audit uint128
14:   uint128 reserveRatioBips;

/// @audit uint128
15:   uint128 expiry;

/// @audit uint16
23:   uint16 protocolFeeBips;

/// @audit uint128
24:   uint128 maxTotalSupply;

/// @audit uint16
25:   uint16 annualInterestBips;

/// @audit uint16
26:   uint16 delinquencyFeeBips;

/// @audit uint32
27:   uint32 withdrawalBatchDuration;

/// @audit uint16
28:   uint16 reserveRatioBips;

/// @audit uint32
29:   uint32 delinquencyGracePeriod;

/// @audit uint32
55:   uint32 internal immutable MinimumDelinquencyGracePeriod;

/// @audit uint32
56:   uint32 internal immutable MaximumDelinquencyGracePeriod;

/// @audit uint16
58:   uint16 internal immutable MinimumReserveRatioBips;

/// @audit uint16
59:   uint16 internal immutable MaximumReserveRatioBips;

/// @audit uint16
61:   uint16 internal immutable MinimumDelinquencyFeeBips;

/// @audit uint16
62:   uint16 internal immutable MaximumDelinquencyFeeBips;

/// @audit uint32
64:   uint32 internal immutable MinimumWithdrawalBatchDuration;

/// @audit uint32
65:   uint32 internal immutable MaximumWithdrawalBatchDuration;

/// @audit uint16
67:   uint16 internal immutable MinimumAnnualInterestBips;

/// @audit uint16
68:   uint16 internal immutable MaximumAnnualInterestBips;

/// @audit uint128
295:     uint128 maxTotalSupply,

/// @audit uint16
296:     uint16 annualInterestBips,

/// @audit uint16
297:     uint16 delinquencyFeeBips,

/// @audit uint32
298:     uint32 withdrawalBatchDuration,

/// @audit uint16
299:     uint16 reserveRatioBips,

/// @audit uint32
300:     uint32 delinquencyGracePeriod

/// @audit uint80
335:     uint80 originationFeeAmount;

/// @audit uint16
397:     uint16 annualInterestBips,

/// @audit uint16
398:     uint16 delinquencyFeeBips,

/// @audit uint32
399:     uint32 withdrawalBatchDuration,

/// @audit uint16
400:     uint16 reserveRatioBips,

/// @audit uint32
401:     uint32 delinquencyGracePeriod

/// @audit uint16
470:     uint16 annualInterestBips
/// @audit uint16
19:     uint16 protocolFeeBips,

/// @audit uint32
46:   uint32 internal immutable MinimumDelinquencyGracePeriod;

/// @audit uint32
47:   uint32 internal immutable MaximumDelinquencyGracePeriod;

/// @audit uint16
49:   uint16 internal immutable MinimumReserveRatioBips;

/// @audit uint16
50:   uint16 internal immutable MaximumReserveRatioBips;

/// @audit uint16
52:   uint16 internal immutable MinimumDelinquencyFeeBips;

/// @audit uint16
53:   uint16 internal immutable MaximumDelinquencyFeeBips;

/// @audit uint32
55:   uint32 internal immutable MinimumWithdrawalBatchDuration;

/// @audit uint32
56:   uint32 internal immutable MaximumWithdrawalBatchDuration;

/// @audit uint16
58:   uint16 internal immutable MinimumAnnualInterestBips;

/// @audit uint16
59:   uint16 internal immutable MaximumAnnualInterestBips;

/// @audit uint80
171:       uint80 originationFeeAmount,

/// @audit uint16
172:       uint16 protocolFeeBips

/// @audit uint80
198:     uint80 originationFeeAmount,

/// @audit uint16
199:     uint16 protocolFeeBips

/// @audit uint128
321:     uint128 maxTotalSupply,

/// @audit uint16
322:     uint16 annualInterestBips,

/// @audit uint16
323:     uint16 delinquencyFeeBips,

/// @audit uint32
324:     uint32 withdrawalBatchDuration,

/// @audit uint16
325:     uint16 reserveRatioBips,

/// @audit uint32
326:     uint32 delinquencyGracePeriod
/// @audit uint128
5:   uint128 startIndex;

/// @audit uint128
6:   uint128 nextIndex;

/// @audit uint32
23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

/// @audit uint32
30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

/// @audit uint128
38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

/// @audit uint32
55:   function push(FIFOQueue storage arr, uint32 value) internal {

/// @audit uint128
56:     uint128 nextIndex = arr.nextIndex;

/// @audit uint128
62:     uint128 startIndex = arr.startIndex;

/// @audit uint128
70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {

/// @audit uint128
71:     uint128 startIndex = arr.startIndex;
/// @audit uint128
15:   uint128 maxTotalSupply;

/// @audit uint128
16:   uint128 accruedProtocolFees;

/// @audit uint128
19:   uint128 normalizedUnclaimedWithdrawals;

/// @audit uint104
21:   uint104 scaledTotalSupply;

/// @audit uint104
24:   uint104 scaledPendingWithdrawals;

/// @audit uint32
25:   uint32 pendingWithdrawalExpiry;

/// @audit uint32
29:   uint32 timeDelinquent;

/// @audit uint16
31:   uint16 annualInterestBips;

/// @audit uint16
33:   uint16 reserveRatioBips;

/// @audit uint112
35:   uint112 scaleFactor;

/// @audit uint32
36:   uint32 lastInterestAccruedTimestamp;

/// @audit uint104
41:   uint104 scaledBalance;

/// @audit uint128
108:   ) internal pure returns (uint128) {
/// @audit uint8
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

/// @audit uint16
21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

/// @audit uint24
25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

/// @audit uint32
29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

/// @audit uint40
33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

/// @audit uint48
37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

/// @audit uint56
41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

/// @audit uint64
45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

/// @audit uint72
49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

/// @audit uint80
53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

/// @audit uint88
57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

/// @audit uint96
61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

/// @audit uint104
65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

/// @audit uint112
69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

/// @audit uint120
73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

/// @audit uint128
77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

/// @audit uint136
81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

/// @audit uint144
85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

/// @audit uint152
89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

/// @audit uint160
93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

/// @audit uint168
97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

/// @audit uint176
101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

/// @audit uint184
105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

/// @audit uint192
109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

/// @audit uint200
113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

/// @audit uint208
117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

/// @audit uint216
121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

/// @audit uint224
125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

/// @audit uint232
129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

/// @audit uint240
133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

/// @audit uint248
137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
/// @audit uint104
19:   uint104 scaledTotalAmount;

/// @audit uint104
21:   uint104 scaledAmountBurned;

/// @audit uint128
23:   uint128 normalizedAmountPaid;

/// @audit uint104
27:   uint104 scaledAmount;

/// @audit uint128
28:   uint128 normalizedAmountWithdrawn;

/// @audit uint104
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {
/// @audit uint104
56:     uint104 scaledAmount = state.scaleAmount(amount).toUint104();

/// @audit uint128
101:     uint128 withdrawableFees = state.withdrawableProtocolFees(totalAssets());
/// @audit uint8
54:   uint8 public immutable decimals;

/// @audit uint104
166:       uint104 scaledBalance = account.scaledBalance;

/// @audit uint128
307:   function withdrawableProtocolFees() external view returns (uint128) {

/// @audit uint32
404:       uint32 expiredBatchExpiry,

/// @audit uint32
467:     uint32 expiry = state.pendingWithdrawalExpiry;

/// @audit uint32
501:     uint32 expiry,

/// @audit uint104
504:     uint104 scaledAvailableLiquidity = state.scaleAmount(availableLiquidity).toUint104();

/// @audit uint104
505:     uint104 scaledAmountOwed = batch.scaledTotalAmount - batch.scaledAmountBurned;

/// @audit uint104
510:     uint104 scaledAmountBurned = uint104(MathUtils.min(scaledAvailableLiquidity, scaledAmountOwed));

/// @audit uint128
511:     uint128 normalizedAmountPaid = state.normalizeAmount(scaledAmountBurned).toUint128();

/// @audit uint104
533:     uint104 scaledAvailableLiquidity = state.scaleAmount(availableLiquidity).toUint104();

/// @audit uint104
534:     uint104 scaledAmountOwed = batch.scaledTotalAmount - batch.scaledAmountBurned;

/// @audit uint104
539:     uint104 scaledAmountBurned = uint104(MathUtils.min(scaledAvailableLiquidity, scaledAmountOwed));

/// @audit uint128
540:     uint128 normalizedAmountPaid = state.normalizeAmount(scaledAmountBurned).toUint128();
/// @audit uint16
149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

/// @audit uint16
171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {
  • WildcatMarketToken.sol ( #L66 ):
/// @audit uint104
66:     uint104 scaledAmount = state.scaleAmount(amount).toUint104();
/// @audit uint32
29:     uint32 expiry

/// @audit uint32
31:     (, uint32 expiredBatchExpiry, WithdrawalBatch memory expiredBatch) = _calculateCurrentState();

/// @audit uint32
40:     uint32 expiry

/// @audit uint32
47:     uint32 expiry

/// @audit uint32
52:     (, uint32 expiredBatchExpiry, WithdrawalBatch memory expiredBatch) = _calculateCurrentState();

/// @audit uint104
83:     uint104 scaledAmount = state.scaleAmount(amount).toUint104();

/// @audit uint32
99:     uint32 expiry = state.pendingWithdrawalExpiry;

/// @audit uint32
139:     uint32 expiry

/// @audit uint128
151:     uint128 newTotalWithdrawn = uint128(

/// @audit uint128
155:     uint128 normalizedAmountWithdrawn = newTotalWithdrawn - status.normalizedAmountWithdrawn;

/// @audit uint32
194:     uint32 expiry = _withdrawalData.unpaidBatches.first();

[G‑16] Divisions can be unchecked to save gas

The expression type(int).min/(-1) is the only case where division causes an overflow. Therefore, uncheck can be used to save gas in scenarios where it is certain that such an overflow will not occur.

There are 4 instances:

  • FeeMath.sol ( #L26 ):
26:       return accumulatedInterestRay / SECONDS_IN_365_DAYS;
  • MathUtils.sol ( #L37 ):
37:       return accumulatedInterestRay / SECONDS_IN_365_DAYS;
23:     size = (sizeInBits + 7) / 8;

73:       size = (sizeInBits + 7) / 8;

[G‑17] Increments can be unchecked to save gas

Using unchecked increments can save gas by bypassing the built-in overflow checks. This can save 30-40 gas per iteration. So it is recommended to use unchecked increments when overflow is not possible.

There are 12 instances (click to show):
93:     for (uint256 i = 0; i < count; i++) {

136:     for (uint256 i = 0; i < count; i++) {

179:     for (uint256 i = 0; i < count; i++) {

222:     for (uint256 i = 0; i < count; i++) {
133:     for (uint256 i = 0; i < count; i++) {

154:     for (uint256 i = 0; i < lenders.length; i++) {

170:     for (uint256 i = 0; i < lenders.length; i++) {

183:     for (uint256 i; i < markets.length; i++) {

212:     for (uint256 i = 0; i < count; i++) {
  • WildcatMarketControllerFactory.sol ( #L146 ):
146:     for (uint256 i = 0; i < count; i++) {
48:     for (uint256 i = 0; i < len; i++) {

75:     for (uint256 i = 0; i < n; i++) {

[G‑18] Unused non-constant state variables waste gas

Saves a storage slot. If the variable is assigned a non-zero value, saves Gsset (20000 gas). If it's assigned a zero value, saves Gsreset (2900 gas). If the variable remains unassigned, there is no gas savings unless the variable is public, in which case the compiler-generated non-payable getter deployment cost is saved. If the state variable is overriding an interface's public function, mark the variable as constant or immutable so that it does not use a storage slot.

There are 2 instances:

57:   string public name;

60:   string public symbol;

[G‑19] Using assembly to check for zero can save gas

Using assembly to check for zero can save gas by allowing more direct access to the evm and reducing some of the overhead associated with high-level operations in solidity.

There are 15 instances (click to show):
345:     if (originationFeeAsset != address(0)) {

351:     if (market.codehash != bytes32(0)) {

477:       if (tmp.expiry == 0) {

492:     if (tmp.expiry == 0) {
  • WildcatMarketControllerFactory.sol ( #L294 ):
294:     if (controller.codehash != bytes32(0)) {
  • WildcatSanctionsSentinel.sol ( #L106 ):
106:     if (escrowContract.codehash != bytes32(0)) return escrowContract;
57:     if (scaledAmount == 0) revert NullMintAmount();

98:     if (state.accruedProtocolFees == 0) {

102:     if (withdrawableFees == 0) {
507:     if (scaledAmountOwed == 0) {

536:     if (scaledAmountOwed == 0) {
  • WildcatMarketToken.sol ( #L68 ):
68:     if (scaledAmount == 0) {
84:     if (scaledAmount == 0) {

94:     if (state.pendingWithdrawalExpiry == 0) {

160:     if (normalizedAmountWithdrawn == 0) {

[G‑20] Use assembly to write address/contract type storage values

Using assembly { sstore(state.slot, addr) instead of state = addr can save gas.

There are 2 instances:

  • WildcatMarketControllerFactory.sol ( #L286, #L298 ):
286:     _tmpMarketBorrowerParameter = msg.sender;

298:     _tmpMarketBorrowerParameter = address(1);

[G‑21] Use uint256(1)/uint256(2) instead of true/false to save gas for changes

Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from ‘false’ to ‘true’, after having been ‘true’ in the past. Refer to the source.

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L20-L22 ):
20:   mapping(address borrower => mapping(address account => bool sanctionOverride))
21:     public
22:     override sanctionOverrides;

[G‑22] Avoid zero transfer to save gas

In Solidity, unnecessary operations can waste gas. For example, a transfer function without a zero amount check uses gas even if called with a zero amount, since the contract state remains unchanged. Implementing a zero amount check avoids these unnecessary function calls, saving gas and improving efficiency.

There are 4 instances:

  • WildcatMarketController.sol ( #L346 ):
346:       originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);
  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);
60:     asset.safeTransferFrom(msg.sender, address(this), amount);

157:       asset.safeTransfer(borrower, currentlyHeld - totalDebts);

[G‑23] Don't emit events inside a loop

Emitting an event has an overhead of 375 gas, which will be incurred on every iteration of the loop. It is cheaper to emit only once after the loop has finished.

There are 2 instances:

157:         emit LenderAuthorized(lender);

173:         emit LenderDeauthorized(lender);

[G‑24] Optimize names to save gas

public/external function names and public member variable names can be optimized to save gas. Below are the interfaces/abstract contracts that can be optimized so that the most frequently-called functions use the least amount of gas possible during method lookup. Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted.

There are 9 instances (click to show):
  • WildcatArchController.sol ( #L8 ):
/// @audit registerBorrower(), removeBorrower(), isRegisteredBorrower(), getRegisteredBorrowers(), getRegisteredBorrowers(), getRegisteredBorrowersCount(), registerControllerFactory(), removeControllerFactory(), isRegisteredControllerFactory(), getRegisteredControllerFactories(), getRegisteredControllerFactories(), getRegisteredControllerFactoriesCount(), registerController(), removeController(), isRegisteredController(), getRegisteredControllers(), getRegisteredControllers(), getRegisteredControllersCount(), registerMarket(), removeMarket(), isRegisteredMarket(), getRegisteredMarkets(), getRegisteredMarkets(), getRegisteredMarketsCount()
8: contract WildcatArchController is Ownable {
  • WildcatMarketController.sol ( #L32 ):
/// @audit getAuthorizedLenders(), getAuthorizedLenders(), getAuthorizedLendersCount(), isAuthorizedLender(), authorizeLenders(), deauthorizeLenders(), updateLenderAuthorization(), isControlledMarket(), getControlledMarkets(), getControlledMarkets(), getControlledMarketsCount(), computeMarketAddress(), getMarketParameters(), deployMarket(), getParameterConstraints(), setAnnualInterestBips(), resetReserveRatio(), archController, controllerFactory, borrower, sentinel, marketInitCodeStorage, marketInitCodeHash, temporaryExcessReserveRatio
32: contract WildcatMarketController is IWildcatMarketControllerEventsAndErrors {
  • WildcatMarketControllerFactory.sol ( #L13 ):
/// @audit isDeployedController(), getDeployedControllersCount(), getDeployedControllers(), getDeployedControllers(), getProtocolFeeConfiguration(), setProtocolFeeConfiguration(), getParameterConstraints(), getMarketControllerParameters(), deployController(), deployControllerAndMarket(), computeControllerAddress(), archController, sentinel, marketInitCodeStorage, marketInitCodeHash, controllerInitCodeStorage, controllerInitCodeHash
13: contract WildcatMarketControllerFactory {
  • WildcatSanctionsEscrow.sol ( #L10 ):
/// @audit balance(), canReleaseEscrow(), escrowedAsset(), releaseEscrow(), sentinel, borrower, account
10: contract WildcatSanctionsEscrow is IWildcatSanctionsEscrow {
  • WildcatSanctionsSentinel.sol ( #L10 ):
/// @audit isSanctioned(), overrideSanction(), removeSanctionOverride(), getEscrowAddress(), createEscrow(), WildcatSanctionsEscrowInitcodeHash, chainalysisSanctionsList, archController, tmpEscrowParams, sanctionOverrides
10: contract WildcatSanctionsSentinel is IWildcatSanctionsSentinel {
  • WildcatMarket.sol ( #L10 ):
/// @audit updateState(), depositUpTo(), deposit(), collectFees(), borrow(), closeMarket()
10: contract WildcatMarket is
  • WildcatMarketBase.sol ( #L14 ):
/// @audit coverageLiquidity(), scaleFactor(), totalAssets(), borrowableAssets(), accruedProtocolFees(), previousState(), currentState(), scaledTotalSupply(), scaledBalanceOf(), getAccountRole(), withdrawableProtocolFees(), effectiveBorrowerAPR(), effectiveLenderAPR(), version, sentinel, borrower, feeRecipient, protocolFeeBips, delinquencyFeeBips, delinquencyGracePeriod, controller, asset, withdrawalBatchDuration
14: contract WildcatMarketBase is ReentrancyGuard, IMarketEventsAndErrors {
  • WildcatMarketConfig.sol ( #L9 ):
/// @audit maximumDeposit(), maxTotalSupply(), annualInterestBips(), reserveRatioBips(), nukeFromOrbit(), stunningReversal(), updateAccountAuthorization(), setMaxTotalSupply(), setAnnualInterestBips(), setReserveRatioBips()
9: contract WildcatMarketConfig is WildcatMarketBase {
  • WildcatMarketWithdrawals.sol ( #L11 ):
/// @audit getUnpaidBatchExpiries(), getWithdrawalBatch(), getAccountWithdrawalStatus(), getAvailableWithdrawalAmount(), queueWithdrawal(), executeWithdrawal(), processUnpaidWithdrawalBatch()
11: contract WildcatMarketWithdrawals is WildcatMarketBase {

[G‑25] Duplicated require()/revert() checks should be refactored to a modifier Or function to save gas

Saves deployment costs.

There is 1 instance:

  • WildcatMarketWithdrawals.sol ( #L50 ):
/// @audit Duplicated on line 142
50:       revert WithdrawalBatchNotExpired();

[G‑26] Reduce gas usage by moving to Solidity 0.8.19 or later

Solidity version 0.8.19 introduced a number of gas optimizations, refer to the Solidity 0.8.19 Release Announcement for details.

There are 3 instances:

  • Chainalysis.sol ( #L2 ):
2: pragma solidity ^0.8.20;
  • Errors.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • StringQuery.sol ( #L2 ):
2: pragma solidity >=0.8.20;

[G‑27] Newer versions of solidity are more gas efficient

The solidity language continues to pursue more efficient gas optimization schemes. Adopting a newer version of solidity can be more gas efficient.

There are 19 instances (click to show):
  • ReentrancyGuard.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatArchController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketControllerFactory.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsEscrow.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsSentinel.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • BoolUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FIFOQueue.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FeeMath.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • LibStoredInitCode.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MarketState.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MathUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • SafeCastLib.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Withdrawal.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarket.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketBase.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketConfig.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketToken.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketWithdrawals.sol ( #L2 ):
2: pragma solidity >=0.8.20;

[G‑28] The result of a function call should be cached rather than re-calling the function

The function calls in solidity are expensive. If the same result of the same function calls are to be used several times, the result should be cached to reduce the gas consumption of repeated calls.

There are 3 instances:

  • WildcatMarketBase.sol ( #L163 ):
/// @audit `state.normalizeAmount(scaledBalance)` called on lines: 177, 182
163:   function _blockAccount(MarketState memory state, address accountAddress) internal {
  • WildcatMarketConfig.sol ( #L171 ):
/// @audit `state.liquidityRequired()` called on lines: 181, 187
/// @audit `totalAssets()` called on lines: 181, 187
171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {

[G‑29] State variables that are used multiple times in a function should be cached in stack variables

When performing multiple operations on a state variable in a function, it is recommended to cache it first. Either multiple reads or multiple writes to a state variable can save gas by caching it on the stack. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses. Saves 100 gas per instance.

There are 10 instances:

  • WildcatMarketControllerFactory.sol ( #L282 ):
/// @audit _tmpMarketBorrowerParameter: 2 writes
282:   function deployController() public returns (address controller) {
  • WildcatMarketBase.sol ( #L466 ):
/// @audit _withdrawalData.batches: 2 reads
466:   function _processExpiredWithdrawalBatch(MarketState memory state) internal {
  • WildcatMarketWithdrawals.sol ( #L77, #L190 ):
/// @audit _withdrawalData.batches: 2 reads
77:   function queueWithdrawal(uint256 amount) external nonReentrant {

/// @audit _withdrawalData.batches: 2 reads
/// @audit _withdrawalData.unpaidBatches: 2 reads
190:   function processUnpaidWithdrawalBatch() external nonReentrant {

[G‑30] Use assembly to emit events

To efficiently emit events, it's possible to utilize assembly by making use of scratch space and the free memory pointer. This approach has the advantage of potentially avoiding the costs associated with memory expansion.

However, it's important to note that in order to safely optimize this process, it is preferable to cache and restore the free memory pointer.

A good example of such practice can be seen in Solady's codebase.

There are 36 instances (click to show):
67:     emit BorrowerAdded(borrower);

74:     emit BorrowerRemoved(borrower);

110:     emit ControllerFactoryAdded(factory);

117:     emit ControllerFactoryRemoved(factory);

153:     emit ControllerAdded(msg.sender, controller);

160:     emit ControllerRemoved(controller);

196:     emit MarketAdded(msg.sender, market);

203:     emit MarketRemoved(market);
157:         emit LenderAuthorized(lender);

173:         emit LenderDeauthorized(lender);
  • WildcatSanctionsEscrow.sol ( #L40 ):
40:     emit EscrowReleased(account, asset, amount);
50:     emit SanctionOverride(msg.sender, account);

58:     emit SanctionOverrideRemoved(msg.sender, account);

112:     emit NewSanctionsEscrow(borrower, account, asset);

116:     emit SanctionOverride(borrower, escrowContract);
67:     emit Transfer(address(0), msg.sender, amount);

68:     emit Deposit(msg.sender, amount, scaledAmount);

108:     emit FeesCollected(withdrawableFees);

130:     emit Borrow(amount);

160:     emit MarketClosed(block.timestamp);
168:       emit AuthorizationStatusUpdated(accountAddress, AuthRole.Blocked);

177:         emit Transfer(accountAddress, escrow, state.normalizeAmount(scaledBalance));

206:         emit AuthorizationStatusUpdated(accountAddress, AuthRole.DepositAndWithdraw);

452:     emit StateUpdated(state.scaleFactor, isDelinquent);

486:       emit WithdrawalBatchClosed(expiry);

524:     emit Transfer(address(this), address(0), normalizedAmountPaid);
99:     emit AuthorizationStatusUpdated(accountAddress, account.approval);

125:     emit AuthorizationStatusUpdated(_account, account.approval);

143:     emit MaxTotalSupplyUpdated(_maxTotalSupply);

158:     emit AnnualInterestBipsUpdated(_annualInterestBips);

192:     emit ReserveRatioBipsUpdated(_reserveRatioBips);
  • WildcatMarketToken.sol ( #L61, #L81 ):
61:     emit Approval(approver, spender, amount);

81:     emit Transfer(from, to, amount);
91:     emit Transfer(msg.sender, address(this), amount);

96:       emit WithdrawalBatchCreated(state.pendingWithdrawalExpiry);

208:       emit WithdrawalBatchClosed(expiry);

[G‑31] Use assembly to compute hashes to save gas

If the arguments to the encode call can fit into the scratch space (two words or fewer), then it's more efficient to use assembly to generate the hash (80 gas):

keccak256(abi.encodePacked(x, y)) -> assembly {mstore(0x00, a); mstore(0x20, b); let hash := keccak256(0x00, 0x40); }

There are 4 instances:

  • WildcatMarketControllerFactory.sol ( #L112, #L122 ):
112:     initCodeHash = uint256(keccak256(controllerInitCode));

122:     initCodeHash = uint256(keccak256(marketInitCode));
  • WildcatSanctionsSentinel.sol ( #L78, #L110 ):
78:                 keccak256(abi.encode(borrower, account, asset)),

110:     new WildcatSanctionsEscrow{ salt: keccak256(abi.encode(borrower, account, asset)) }();

[G‑32] Use calldata instead of memory for immutable arguments

Mark data types as calldata instead of memory where possible. This makes it so that the data is not automatically loaded into memory. If the data passed into the function does not need to be changed (like updating values in an array), it can be passed in as calldata. The one exception to this is if the argument must later be passed into another function that takes an argument that specifies memory storage.

There are 9 instances (click to show):
/// @audit lenders
153:   function authorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit lenders
169:   function deauthorizeLenders(address[] memory lenders) external onlyBorrower {

/// @audit markets
182:   function updateLenderAuthorization(address lender, address[] memory markets) external {

/// @audit namePrefix
/// @audit symbolPrefix
221:   function computeMarketAddress(
222:     address asset,
223:     string memory namePrefix,
224:     string memory symbolPrefix
225:   ) external view returns (address) {

/// @audit namePrefix
/// @audit symbolPrefix
291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {
  • WildcatMarketControllerFactory.sol ( #L317-L327 ):
/// @audit namePrefix
/// @audit symbolPrefix
317:   function deployControllerAndMarket(
318:     string memory namePrefix,
319:     string memory symbolPrefix,
320:     address asset,
321:     uint128 maxTotalSupply,
322:     uint16 annualInterestBips,
323:     uint16 delinquencyFeeBips,
324:     uint32 withdrawalBatchDuration,
325:     uint16 reserveRatioBips,
326:     uint32 delinquencyGracePeriod
327:   ) external returns (address controller, address market) {

[G‑33] Consider activating via-ir for deploying

By using --via-ir or {"viaIR": true}, the compiler is able to use more advanced multi-function optimizations, for extra gas savings.

There are 22 instances (click to show):

[G‑34] Using bools for storage incurs overhead

Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled. Use uint256(0) and uint256(1) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD.

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L20-L22 ):
20:   mapping(address borrower => mapping(address account => bool sanctionOverride))
21:     public
22:     override sanctionOverrides;

[G‑35] Multiple accesses of the same mapping/array key/index should be cached

The instances below point to the second+ access of a value inside a mapping/array, within a function. Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times, saves ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations. Caching an array's struct avoids recalculating the array offsets into memory/calldata

There are 3 instances:

  • WildcatMarketController.sol ( #L500 ):
/// @audit `temporaryExcessReserveRatio[market]` is also accessed on line 491
500:     delete temporaryExcessReserveRatio[market];
  • WildcatMarketBase.sol ( #L185 ):
/// @audit `_accounts[accountAddress]` is also accessed on line 164
185:       _accounts[accountAddress] = account;
  • WildcatMarketConfig.sol ( #L101 ):
/// @audit `_accounts[accountAddress]` is also accessed on line 89
101:     _accounts[accountAddress] = account;

[G‑36] State variable access within a loop

State variable reads and writes are more expensive than local variable reads and writes. Therefore, it is recommended to replace state variable reads and writes within loops with a local variable. Gas savings should be multiplied by the average loop length.

There are 6 instances:

/// @audit Access `_authorizedLenders` within for loop.
134:       arr[i] = _authorizedLenders.at(start + i);

/// @audit Access `_authorizedLenders` within for loop.
156:       if (_authorizedLenders.add(lender)) {

/// @audit Access `_authorizedLenders` within for loop.
172:       if (_authorizedLenders.remove(lender)) {

/// @audit Access `_controlledMarkets` within for loop.
185:       if (!_controlledMarkets.contains(market)) {

/// @audit Access `_authorizedLenders` within for loop.
188:       WildcatMarket(market).updateAccountAuthorization(lender, _authorizedLenders.contains(lender));

/// @audit Access `_controlledMarkets` within for loop.
213:       arr[i] = _controlledMarkets.at(start + i);

Disputed Issues

[D‑01] abi.encodePacked() should be replaced with bytes.concat()

Solidity version 0.8.4 introduces bytes.concat(), which can be used to replace abi.encodePacked() on bytes/strings. It can make the intended operation clearer, leading to less reviewer confusion.

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L75-L80 ):
/// @audit abi.encodePacked() is clearer for non-bytes parameters.
75:               abi.encodePacked(
76:                 bytes1(0xff),
77:                 address(this),
78:                 keccak256(abi.encode(borrower, account, asset)),
79:                 WildcatSanctionsEscrowInitcodeHash
80:               )

[D‑02] Visibility should be set explicitly rather than defaulting to internal

The rule is valid, but the following findings are invalid.

There are 2 instances:

11:   bytes32 public constant override WildcatSanctionsEscrowInitcodeHash =
12:     keccak256(type(WildcatSanctionsEscrow).creationCode);

20:   mapping(address borrower => mapping(address account => bool sanctionOverride))
21:     public
22:     override sanctionOverrides;

[D‑03] Using private rather than public for constants, saves gas

If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table

There are 27 instances (click to show):
41:   IWildcatArchController public immutable archController;

43:   IWildcatMarketControllerFactory public immutable controllerFactory;

45:   address public immutable borrower;

47:   address public immutable sentinel;

49:   address public immutable marketInitCodeStorage;

51:   uint256 public immutable marketInitCodeHash;
31:   IWildcatArchController public immutable archController;

34:   address public immutable sentinel;

36:   address public immutable marketInitCodeStorage;

38:   uint256 public immutable marketInitCodeHash;

40:   address public immutable controllerInitCodeStorage;

42:   uint256 public immutable controllerInitCodeHash;
11:   address public immutable override sentinel;

12:   address public immutable override borrower;

13:   address public immutable override account;
  • WildcatSanctionsSentinel.sol ( #L14, #L16 ):
14:   address public immutable override chainalysisSanctionsList;

16:   address public immutable override archController;
27:   address public immutable sentinel;

30:   address public immutable borrower;

33:   address public immutable feeRecipient;

36:   uint256 public immutable protocolFeeBips;

39:   uint256 public immutable delinquencyFeeBips;

42:   uint256 public immutable delinquencyGracePeriod;

45:   address public immutable controller;

48:   address public immutable asset;

51:   uint256 public immutable withdrawalBatchDuration;

54:   uint8 public immutable decimals;

[D‑04] Event names should use CamelCase

The instances below are already CamelCase (events are supposed to use CamelCase, not lowerCamelCase).

There are 10 instances:

29:   event MarketAdded(address indexed controller, address market);

30:   event MarketRemoved(address market);

32:   event ControllerFactoryAdded(address controllerFactory);

33:   event ControllerFactoryRemoved(address controllerFactory);

35:   event BorrowerAdded(address borrower);

36:   event BorrowerRemoved(address borrower);

38:   event ControllerAdded(address indexed controllerFactory, address controller);

39:   event ControllerRemoved(address controller);
  • WildcatMarketControllerFactory.sol ( #L16, #L17 ):
16:   event NewController(address borrower, address controller, string namePrefix, string symbolPrefix);

17:   event UpdateProtocolFeeConfiguration(

[D‑05] internal functions not called by the contract should be removed

All unused code should be removed or commented out. If the functions are required by an interface, the contract should inherit from that interface and use the override keyword.

There are 71 instances (click to show):
/// @audit Invalid for library function
5:   function and(bool a, bool b) internal pure returns (bool c) {

/// @audit Invalid for library function
11:   function or(bool a, bool b) internal pure returns (bool c) {

/// @audit Invalid for library function
17:   function xor(bool a, bool b) internal pure returns (bool c) {
/// @audit Invalid for library function
19:   function empty(FIFOQueue storage arr) internal view returns (bool) {

/// @audit Invalid for library function
23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

/// @audit Invalid for library function
30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

/// @audit Invalid for library function
38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

/// @audit Invalid for library function
42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

/// @audit Invalid for library function
55:   function push(FIFOQueue storage arr, uint32 value) internal {

/// @audit Invalid for library function
61:   function shift(FIFOQueue storage arr) internal {

/// @audit Invalid for library function
70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
/// @audit Invalid for library function
30:   function calculateBaseInterest(
31:     MarketState memory state,
32:     uint256 timestamp
33:   ) internal pure returns (uint256 baseInterestRay) {

/// @audit Invalid for library function
40:   function applyProtocolFee(
41:     MarketState memory state,
42:     uint256 baseInterestRay,
43:     uint256 protocolFeeBips
44:   ) internal pure returns (uint256 protocolFee) {

/// @audit Invalid for library function
53:   function updateDelinquency(
54:     MarketState memory state,
55:     uint256 timestamp,
56:     uint256 delinquencyFeeBips,
57:     uint256 delinquencyGracePeriod
58:   ) internal pure returns (uint256 delinquencyFeeRay) {

/// @audit Invalid for library function
142:   function updateScaleFactorAndFees(
143:     MarketState memory state,
144:     uint256 protocolFeeBips,
145:     uint256 delinquencyFeeBips,
146:     uint256 delinquencyGracePeriod,
147:     uint256 timestamp
148:   )
149:     internal
150:     pure
151:     returns (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee)
/// @audit Invalid for library function
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

/// @audit Invalid for library function
48:   function getCreate2Prefix(address deployer) internal pure returns (uint256 create2Prefix) {

/// @audit Invalid for library function
54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {
/// @audit Invalid for library function
51:   function totalSupply(MarketState memory state) internal pure returns (uint256) {

/// @audit Invalid for library function
59:   function maximumDeposit(MarketState memory state) internal pure returns (uint256) {

/// @audit Invalid for library function
66:   function normalizeAmount(
67:     MarketState memory state,
68:     uint256 amount
69:   ) internal pure returns (uint256) {

/// @audit Invalid for library function
76:   function scaleAmount(MarketState memory state, uint256 amount) internal pure returns (uint256) {

/// @audit Invalid for library function
87:   function liquidityRequired(
88:     MarketState memory state
89:   ) internal pure returns (uint256 _liquidityRequired) {

/// @audit Invalid for library function
105:   function withdrawableProtocolFees(
106:     MarketState memory state,
107:     uint256 totalAssets
108:   ) internal pure returns (uint128) {

/// @audit Invalid for library function
123:   function borrowableAssets(
124:     MarketState memory state,
125:     uint256 totalAssets
126:   ) internal pure returns (uint256) {

/// @audit Invalid for library function
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

/// @audit Invalid for library function
138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
/// @audit Invalid for library function
30:   function calculateLinearInterestFromBips(
31:     uint256 rateBip,
32:     uint256 timeDelta
33:   ) internal pure returns (uint256 result) {

/// @audit Invalid for library function
44:   function min(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
51:   function max(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
59:   function satSub(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
85:   function bipMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
105:   function bipDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
121:   function bipToRay(uint256 a) internal pure returns (uint256 b) {

/// @audit Invalid for library function
138:   function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
155:   function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
173:   function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {

/// @audit Invalid for library function
191:   function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
/// @audit Invalid for library function
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

/// @audit Invalid for library function
21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

/// @audit Invalid for library function
25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

/// @audit Invalid for library function
29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

/// @audit Invalid for library function
33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

/// @audit Invalid for library function
37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

/// @audit Invalid for library function
41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

/// @audit Invalid for library function
45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

/// @audit Invalid for library function
49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

/// @audit Invalid for library function
53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

/// @audit Invalid for library function
57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

/// @audit Invalid for library function
61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

/// @audit Invalid for library function
65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

/// @audit Invalid for library function
69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

/// @audit Invalid for library function
73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

/// @audit Invalid for library function
77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

/// @audit Invalid for library function
81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

/// @audit Invalid for library function
85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

/// @audit Invalid for library function
89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

/// @audit Invalid for library function
93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

/// @audit Invalid for library function
97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

/// @audit Invalid for library function
101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

/// @audit Invalid for library function
105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

/// @audit Invalid for library function
109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

/// @audit Invalid for library function
113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

/// @audit Invalid for library function
117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

/// @audit Invalid for library function
121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

/// @audit Invalid for library function
125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

/// @audit Invalid for library function
129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

/// @audit Invalid for library function
133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

/// @audit Invalid for library function
137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
/// @audit Invalid for library function
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {

/// @audit Invalid for library function
47:   function availableLiquidityForPendingBatch(
48:     WithdrawalBatch memory batch,
49:     MarketState memory state,
50:     uint256 totalAssets
51:   ) internal pure returns (uint256) {

[D‑06] Unused named return variables without optimizer waste gas

Consider changing the variable to be an unnamed one, since the variable is never assigned, nor is it returned by name. If the optimizer is not turned on, leaving the code as it is will also waste gas for the stack variable.

There are 21 instances (click to show):
/// @audit salt is used in assembly
370:   function _deriveSalt(
371:     address asset,
372:     string memory namePrefix,
373:     string memory symbolPrefix
374:   ) internal pure returns (bytes32 salt) {
/// @audit c is used in assembly
5:   function and(bool a, bool b) internal pure returns (bool c) {

/// @audit c is used in assembly
11:   function or(bool a, bool b) internal pure returns (bool c) {

/// @audit c is used in assembly
17:   function xor(bool a, bool b) internal pure returns (bool c) {
/// @audit initCodeStorage is used in assembly
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

/// @audit create2Prefix is used in assembly
48:   function getCreate2Prefix(address deployer) internal pure returns (uint256 create2Prefix) {

/// @audit create2Address is used in assembly
54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {

/// @audit deployment is used in assembly
87:   function createWithStoredInitCode(
88:     address initCodeStorage,
89:     uint256 value
90:   ) internal returns (address deployment) {

/// @audit deployment is used in assembly
106:   function create2WithStoredInitCode(
107:     address initCodeStorage,
108:     bytes32 salt,
109:     uint256 value
110:   ) internal returns (address deployment) {
  • MarketState.sol ( #L130 ):
/// @audit result is used in assembly
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {
/// @audit c is used in assembly
59:   function satSub(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit c is used in assembly
71:   function ternary(
72:     bool condition,
73:     uint256 valueIfTrue,
74:     uint256 valueIfFalse
75:   ) internal pure returns (uint256 c) {

/// @audit c is used in assembly
85:   function bipMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit c is used in assembly
105:   function bipDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit b is used in assembly
121:   function bipToRay(uint256 a) internal pure returns (uint256 b) {

/// @audit c is used in assembly
138:   function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit c is used in assembly
155:   function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit z is used in assembly
173:   function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {

/// @audit z is used in assembly
191:   function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
/// @audit str is used in assembly
19: function bytes32ToString(bytes32 value) pure returns (string memory str) {

/// @audit str is used in assembly
33: function queryStringOrBytes32AsString(
34:   address target,
35:   uint256 rightPaddedFunctionSelector,
36:   uint256 rightPaddedGenericErrorSelector
37: ) view returns (string memory str) {

[D‑07] Assembly blocks should have extensive comments

Assembly blocks take a lot more time to audit than normal Solidity code, and often have gotchas and side-effects that the Solidity versions of the same code do not. Consider adding more comments explaining what is being done in every step of the assembly code, and describe why assembly is being used instead of Solidity.

There are 10 instances (click to show):
  • WildcatMarketController.sol ( #L375 ):
375:     assembly {
  • LibStoredInitCode.sol ( #L8, #L59 ):
8:     assembly {

59:     assembly {
132:     assembly {
133:       // Equivalent to expiry > 0 && expiry <= block.timestamp
134:       result := gt(timestamp(), sub(expiry, 1))
135:     }
60:     assembly {
61:       // (a > b) * (a - b)
62:       // If a-b underflows, the product will be zero
63:       c := mul(gt(a, b), sub(a, b))
64:     }

86:     assembly {

174:     assembly {
175:       // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y))
176:       if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {
177:         // Store the function selector of `MulDivFailed()`.
178:         mstore(0x00, 0xad251c27)
179:         // Revert with (offset, size).
180:         revert(0x1c, 0x04)
181:       }
182:       z := div(mul(x, y), d)
183:     }

192:     assembly {
193:       // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y))
194:       if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {
195:         // Store the function selector of `MulDivFailed()`.
196:         mstore(0x00, 0xad251c27)
197:         // Revert with (offset, size).
198:         revert(0x1c, 0x04)
199:       }
200:       z := add(iszero(iszero(mod(mul(x, y), d))), div(mul(x, y), d))
201:     }
39:   assembly {

83:     assembly {
84:       str := mload(0x40)
85:       // Get allocation size for the string including the length and data.
86:       // Rounding down returndatasize to nearest word because the returndata
87:       // has an extra offset word.
88:       let allocSize := and(sub(returndatasize(), 1), OnlyFullWordMask)
89:       mstore(0x40, add(str, allocSize))
90:       // Copy returndata after the offset
91:       returndatacopy(str, 0x20, sub(returndatasize(), 0x20))
92:     }

[D‑08] Cast to bytes or bytes32 for clearer semantic meaning

The rule is valid, but the following findings are invalid.

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L75-L80 ):
75:               abi.encodePacked(
76:                 bytes1(0xff),
77:                 address(this),
78:                 keccak256(abi.encode(borrower, account, asset)),
79:                 WildcatSanctionsEscrowInitcodeHash
80:               )

[D‑09] Passing abi.encodePacked() with dynamic arguments to a hash can cause collisions

The cases below do not have multiple bytes/string arguments

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L74-L81 ):
74:             keccak256(
75:               abi.encodePacked(
76:                 bytes1(0xff),
77:                 address(this),
78:                 keccak256(abi.encode(borrower, account, asset)),
79:                 WildcatSanctionsEscrowInitcodeHash
80:               )
81:             )

[D‑10] Change public function visibility to external to save gas

After Solidity version 0.6.9 both public and external functions save the same amount of gas, and since these files are >0.6.9, these findings are invalid.

There are 9 instances (click to show):
  • WildcatSanctionsEscrow.sol ( #L29, #L33 ):
29:   function escrowedAsset() public view override returns (address, uint256) {

33:   function releaseEscrow() public override {
39:   function isSanctioned(address borrower, address account) public view override returns (bool) {

48:   function overrideSanction(address account) public override {

56:   function removeSanctionOverride(address account) public override {

95:   function createEscrow(
96:     address borrower,
97:     address account,
98:     address asset
99:   ) public override returns (address escrowContract) {
149:   function setAnnualInterestBips(uint16 _annualInterestBips) public onlyController nonReentrant {

171:   function setReserveRatioBips(uint16 _reserveRatioBips) public onlyController nonReentrant {
  • WildcatMarketToken.sol ( #L16 ):
16:   function balanceOf(address account) public view virtual nonReentrantView returns (uint256) {

[D‑11] NatSpec: Contract declarations should have @notice tags

The @notice is used to explain to users what the contract does. The compiler interprets /// or /** comments as this tag if one wasn't explicitly provided.

There is 1 instance:

  • ReentrancyGuard.sol ( #L12 ):
12: contract ReentrancyGuard {

[D‑12] Not initializing local variables to zero saves gas

This is only true for state variables, and does not save gas for local variables. The examples below are for local variables and therefore do not save gas, and are invalid.

There are 11 instances (click to show):
93:     for (uint256 i = 0; i < count; i++) {

136:     for (uint256 i = 0; i < count; i++) {

179:     for (uint256 i = 0; i < count; i++) {

222:     for (uint256 i = 0; i < count; i++) {
133:     for (uint256 i = 0; i < count; i++) {

154:     for (uint256 i = 0; i < lenders.length; i++) {

170:     for (uint256 i = 0; i < lenders.length; i++) {

212:     for (uint256 i = 0; i < count; i++) {
  • WildcatMarketControllerFactory.sol ( #L146 ):
146:     for (uint256 i = 0; i < count; i++) {
48:     for (uint256 i = 0; i < len; i++) {

75:     for (uint256 i = 0; i < n; i++) {

[D‑13] x += y is more expensive than x = x + y for state variables

It is not applicable to complex state variables.

There are 2 instances:

  • WildcatMarketBase.sol ( #L178 ):
178:         _accounts[escrow].scaledBalance += scaledBalance;
  • WildcatMarketWithdrawals.sol ( #L104 ):
104:     _withdrawalData.accountStatuses[expiry][msg.sender].scaledAmount += scaledAmount;

[D‑14] Revert on transfer to the zero address

The rule is valid, but the following findings are invalid.

There are 3 instances:

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

129:     asset.safeTransfer(msg.sender, amount);

154:       asset.safeTransferFrom(borrower, address(this), totalDebts - currentlyHeld);

[D‑15] The SafeTransferLib does not ensure that the token contract exists

safeTransfer()/safeTransferFrom() aren't called from this file, so the vulnerability doesn't exist here

There are 5 instances:

  • WildcatMarketController.sol ( #L5, #L35 ):
5: import 'solady/utils/SafeTransferLib.sol';

35:   using SafeTransferLib for address;
  • WildcatMarket.sol ( #L18 ):
18:   using SafeTransferLib for address;
  • WildcatMarketWithdrawals.sol ( #L9, #L12 ):
9: import 'solady/utils/SafeTransferLib.sol';

12:   using SafeTransferLib for address;

[D‑16] SafeTransferLib does not ensure that the token contract exists

There is a subtle difference between the implementation of solady/solmate's SafeTransferLib and OZ's SafeERC20. OZ's SafeERC20 checks if the token is a contract or not, while SafeTransferLib does not:

@dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.

There are 7 instances:

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

107:     asset.safeTransfer(feeRecipient, withdrawableFees);

129:     asset.safeTransfer(msg.sender, amount);

154:       asset.safeTransferFrom(borrower, address(this), totalDebts - currentlyHeld);

157:       asset.safeTransfer(borrower, currentlyHeld - totalDebts);
171:       asset.safeTransfer(escrow, normalizedAmountWithdrawn);

179:       asset.safeTransfer(accountAddress, normalizedAmountWithdrawn);

[D‑17] Solidity version 0.8.20 or above may not work on other chains due to PUSH0

The rule is invalid for this project

There are 22 instances (click to show):
  • ReentrancyGuard.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatArchController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketController.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketControllerFactory.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsEscrow.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatSanctionsSentinel.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • BoolUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Chainalysis.sol ( #L2 ):
2: pragma solidity ^0.8.20;
  • Errors.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FIFOQueue.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • FeeMath.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • LibStoredInitCode.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MarketState.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • MathUtils.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • SafeCastLib.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • StringQuery.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • Withdrawal.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarket.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketBase.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketConfig.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketToken.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • WildcatMarketWithdrawals.sol ( #L2 ):
2: pragma solidity >=0.8.20;

[D‑18] Floating pragma should be avoided

Interfaces should not use fixed compiler versions, since they may be used by projects using a different version.

There are 3 instances:

  • Chainalysis.sol ( #L2 ):
2: pragma solidity ^0.8.20;
  • Errors.sol ( #L2 ):
2: pragma solidity >=0.8.20;
  • StringQuery.sol ( #L2 ):
2: pragma solidity >=0.8.20;

[D‑19] SPDX identifier should be the in the first line of a solidity file

There are 22 instances (click to show):
  • ReentrancyGuard.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatArchController.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatMarketController.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatMarketControllerFactory.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatSanctionsEscrow.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatSanctionsSentinel.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • BoolUtils.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • Chainalysis.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • Errors.sol ( #L1 ):
1: // SPDX-License-Identifier: BUSL-1.1
  • FIFOQueue.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • FeeMath.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • LibStoredInitCode.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • MarketState.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • MathUtils.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • SafeCastLib.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • StringQuery.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • Withdrawal.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatMarket.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatMarketBase.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatMarketConfig.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatMarketToken.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT
  • WildcatMarketWithdrawals.sol ( #L1 ):
1: // SPDX-License-Identifier: MIT

[D‑20] Timestamp may be manipulation

Use of block.timestamp, in and of itself, is not evidence of an issue; there must be an incorrect usage in the code in order for there to be a vulnerability. There should also be a corresponding suggested fix.

There are 11 instances (click to show):
484:       tmp.expiry = uint128(block.timestamp + 2 weeks);

495:     if (block.timestamp < tmp.expiry) {
  • WildcatMarket.sol ( #L160 ):
160:     emit MarketClosed(block.timestamp);
114:       lastInterestAccruedTimestamp: uint32(block.timestamp)

378:     if (block.timestamp != state.lastInterestAccruedTimestamp) {

384:           block.timestamp

434:     if (state.lastInterestAccruedTimestamp != block.timestamp) {

439:         block.timestamp
49:     if (expiry > block.timestamp) {

95:       state.pendingWithdrawalExpiry = uint32(block.timestamp + withdrawalBatchDuration);

141:     if (expiry > block.timestamp) {

[D‑21] Unused internal functions should be removed to save deployment gas

All internal functions that are never used can be safely removed or commented out to save gas. If the functions are required by an interface, the contract should inherit from that interface and use the override keyword.

There are 71 instances (click to show):
/// @audit Invalid for library function
5:   function and(bool a, bool b) internal pure returns (bool c) {

/// @audit Invalid for library function
11:   function or(bool a, bool b) internal pure returns (bool c) {

/// @audit Invalid for library function
17:   function xor(bool a, bool b) internal pure returns (bool c) {
/// @audit Invalid for library function
19:   function empty(FIFOQueue storage arr) internal view returns (bool) {

/// @audit Invalid for library function
23:   function first(FIFOQueue storage arr) internal view returns (uint32) {

/// @audit Invalid for library function
30:   function at(FIFOQueue storage arr, uint256 index) internal view returns (uint32) {

/// @audit Invalid for library function
38:   function length(FIFOQueue storage arr) internal view returns (uint128) {

/// @audit Invalid for library function
42:   function values(FIFOQueue storage arr) internal view returns (uint32[] memory _values) {

/// @audit Invalid for library function
55:   function push(FIFOQueue storage arr, uint32 value) internal {

/// @audit Invalid for library function
61:   function shift(FIFOQueue storage arr) internal {

/// @audit Invalid for library function
70:   function shiftN(FIFOQueue storage arr, uint128 n) internal {
/// @audit Invalid for library function
30:   function calculateBaseInterest(
31:     MarketState memory state,
32:     uint256 timestamp
33:   ) internal pure returns (uint256 baseInterestRay) {

/// @audit Invalid for library function
40:   function applyProtocolFee(
41:     MarketState memory state,
42:     uint256 baseInterestRay,
43:     uint256 protocolFeeBips
44:   ) internal pure returns (uint256 protocolFee) {

/// @audit Invalid for library function
53:   function updateDelinquency(
54:     MarketState memory state,
55:     uint256 timestamp,
56:     uint256 delinquencyFeeBips,
57:     uint256 delinquencyGracePeriod
58:   ) internal pure returns (uint256 delinquencyFeeRay) {

/// @audit Invalid for library function
142:   function updateScaleFactorAndFees(
143:     MarketState memory state,
144:     uint256 protocolFeeBips,
145:     uint256 delinquencyFeeBips,
146:     uint256 delinquencyGracePeriod,
147:     uint256 timestamp
148:   )
149:     internal
150:     pure
151:     returns (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee)
/// @audit Invalid for library function
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

/// @audit Invalid for library function
48:   function getCreate2Prefix(address deployer) internal pure returns (uint256 create2Prefix) {

/// @audit Invalid for library function
54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {
/// @audit Invalid for library function
51:   function totalSupply(MarketState memory state) internal pure returns (uint256) {

/// @audit Invalid for library function
59:   function maximumDeposit(MarketState memory state) internal pure returns (uint256) {

/// @audit Invalid for library function
66:   function normalizeAmount(
67:     MarketState memory state,
68:     uint256 amount
69:   ) internal pure returns (uint256) {

/// @audit Invalid for library function
76:   function scaleAmount(MarketState memory state, uint256 amount) internal pure returns (uint256) {

/// @audit Invalid for library function
87:   function liquidityRequired(
88:     MarketState memory state
89:   ) internal pure returns (uint256 _liquidityRequired) {

/// @audit Invalid for library function
105:   function withdrawableProtocolFees(
106:     MarketState memory state,
107:     uint256 totalAssets
108:   ) internal pure returns (uint128) {

/// @audit Invalid for library function
123:   function borrowableAssets(
124:     MarketState memory state,
125:     uint256 totalAssets
126:   ) internal pure returns (uint256) {

/// @audit Invalid for library function
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {

/// @audit Invalid for library function
138:   function totalDebts(MarketState memory state) internal pure returns (uint256) {
/// @audit Invalid for library function
30:   function calculateLinearInterestFromBips(
31:     uint256 rateBip,
32:     uint256 timeDelta
33:   ) internal pure returns (uint256 result) {

/// @audit Invalid for library function
44:   function min(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
51:   function max(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
59:   function satSub(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
85:   function bipMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
105:   function bipDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
121:   function bipToRay(uint256 a) internal pure returns (uint256 b) {

/// @audit Invalid for library function
138:   function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
155:   function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit Invalid for library function
173:   function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {

/// @audit Invalid for library function
191:   function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
/// @audit Invalid for library function
17:   function toUint8(uint256 x) internal pure returns (uint8 y) {

/// @audit Invalid for library function
21:   function toUint16(uint256 x) internal pure returns (uint16 y) {

/// @audit Invalid for library function
25:   function toUint24(uint256 x) internal pure returns (uint24 y) {

/// @audit Invalid for library function
29:   function toUint32(uint256 x) internal pure returns (uint32 y) {

/// @audit Invalid for library function
33:   function toUint40(uint256 x) internal pure returns (uint40 y) {

/// @audit Invalid for library function
37:   function toUint48(uint256 x) internal pure returns (uint48 y) {

/// @audit Invalid for library function
41:   function toUint56(uint256 x) internal pure returns (uint56 y) {

/// @audit Invalid for library function
45:   function toUint64(uint256 x) internal pure returns (uint64 y) {

/// @audit Invalid for library function
49:   function toUint72(uint256 x) internal pure returns (uint72 y) {

/// @audit Invalid for library function
53:   function toUint80(uint256 x) internal pure returns (uint80 y) {

/// @audit Invalid for library function
57:   function toUint88(uint256 x) internal pure returns (uint88 y) {

/// @audit Invalid for library function
61:   function toUint96(uint256 x) internal pure returns (uint96 y) {

/// @audit Invalid for library function
65:   function toUint104(uint256 x) internal pure returns (uint104 y) {

/// @audit Invalid for library function
69:   function toUint112(uint256 x) internal pure returns (uint112 y) {

/// @audit Invalid for library function
73:   function toUint120(uint256 x) internal pure returns (uint120 y) {

/// @audit Invalid for library function
77:   function toUint128(uint256 x) internal pure returns (uint128 y) {

/// @audit Invalid for library function
81:   function toUint136(uint256 x) internal pure returns (uint136 y) {

/// @audit Invalid for library function
85:   function toUint144(uint256 x) internal pure returns (uint144 y) {

/// @audit Invalid for library function
89:   function toUint152(uint256 x) internal pure returns (uint152 y) {

/// @audit Invalid for library function
93:   function toUint160(uint256 x) internal pure returns (uint160 y) {

/// @audit Invalid for library function
97:   function toUint168(uint256 x) internal pure returns (uint168 y) {

/// @audit Invalid for library function
101:   function toUint176(uint256 x) internal pure returns (uint176 y) {

/// @audit Invalid for library function
105:   function toUint184(uint256 x) internal pure returns (uint184 y) {

/// @audit Invalid for library function
109:   function toUint192(uint256 x) internal pure returns (uint192 y) {

/// @audit Invalid for library function
113:   function toUint200(uint256 x) internal pure returns (uint200 y) {

/// @audit Invalid for library function
117:   function toUint208(uint256 x) internal pure returns (uint208 y) {

/// @audit Invalid for library function
121:   function toUint216(uint256 x) internal pure returns (uint216 y) {

/// @audit Invalid for library function
125:   function toUint224(uint256 x) internal pure returns (uint224 y) {

/// @audit Invalid for library function
129:   function toUint232(uint256 x) internal pure returns (uint232 y) {

/// @audit Invalid for library function
133:   function toUint240(uint256 x) internal pure returns (uint240 y) {

/// @audit Invalid for library function
137:   function toUint248(uint256 x) internal pure returns (uint248 y) {
/// @audit Invalid for library function
38:   function scaledOwedAmount(WithdrawalBatch memory batch) internal pure returns (uint104) {

/// @audit Invalid for library function
47:   function availableLiquidityForPendingBatch(
48:     WithdrawalBatch memory batch,
49:     MarketState memory state,
50:     uint256 totalAssets
51:   ) internal pure returns (uint256) {

[D‑22] Unused named return

The rule is valid, but the following findings are invalid.

There are 21 instances (click to show):
/// @audit salt is used in assembly
370:   function _deriveSalt(
371:     address asset,
372:     string memory namePrefix,
373:     string memory symbolPrefix
374:   ) internal pure returns (bytes32 salt) {
/// @audit c is used in assembly
5:   function and(bool a, bool b) internal pure returns (bool c) {

/// @audit c is used in assembly
11:   function or(bool a, bool b) internal pure returns (bool c) {

/// @audit c is used in assembly
17:   function xor(bool a, bool b) internal pure returns (bool c) {
/// @audit initCodeStorage is used in assembly
7:   function deployInitCode(bytes memory data) internal returns (address initCodeStorage) {

/// @audit create2Prefix is used in assembly
48:   function getCreate2Prefix(address deployer) internal pure returns (uint256 create2Prefix) {

/// @audit create2Address is used in assembly
54:   function calculateCreate2Address(
55:     uint256 create2Prefix,
56:     bytes32 salt,
57:     uint256 initCodeHash
58:   ) internal pure returns (address create2Address) {

/// @audit deployment is used in assembly
87:   function createWithStoredInitCode(
88:     address initCodeStorage,
89:     uint256 value
90:   ) internal returns (address deployment) {

/// @audit deployment is used in assembly
106:   function create2WithStoredInitCode(
107:     address initCodeStorage,
108:     bytes32 salt,
109:     uint256 value
110:   ) internal returns (address deployment) {
  • MarketState.sol ( #L130 ):
/// @audit result is used in assembly
130:   function hasPendingExpiredBatch(MarketState memory state) internal view returns (bool result) {
/// @audit c is used in assembly
59:   function satSub(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit c is used in assembly
71:   function ternary(
72:     bool condition,
73:     uint256 valueIfTrue,
74:     uint256 valueIfFalse
75:   ) internal pure returns (uint256 c) {

/// @audit c is used in assembly
85:   function bipMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit c is used in assembly
105:   function bipDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit b is used in assembly
121:   function bipToRay(uint256 a) internal pure returns (uint256 b) {

/// @audit c is used in assembly
138:   function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit c is used in assembly
155:   function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {

/// @audit z is used in assembly
173:   function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {

/// @audit z is used in assembly
191:   function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
/// @audit str is used in assembly
19: function bytes32ToString(bytes32 value) pure returns (string memory str) {

/// @audit str is used in assembly
33: function queryStringOrBytes32AsString(
34:   address target,
35:   uint256 rightPaddedFunctionSelector,
36:   uint256 rightPaddedGenericErrorSelector
37: ) view returns (string memory str) {

[D‑23] Using delete statement can save gas

Using delete instead of assigning zero to state variables does not save any extra gas with the optimizer on (saves 5-8 gas with optimizer completely off), so this finding is invalid, especially since if they were interested in gas savings, they'd have the optimizer enabled.

There are 6 instances:

  • WildcatSanctionsSentinel.sol ( #L57 ):
57:     sanctionOverrides[msg.sender][account] = false;
144:     state.annualInterestBips = 0;

146:     state.reserveRatioBips = 0;
171:         account.scaledBalance = 0;

431:       state.pendingWithdrawalExpiry = 0;

489:     state.pendingWithdrawalExpiry = 0;

[D‑24] Use != 0 or == 0 for unsigned integer comparison

Only valid prior to solidity version 0.8.13, and only for require() statements, and at least one of those is not true for the examples below.

There are 12 instances (click to show):
  • WildcatMarketControllerFactory.sol ( #L201, #L205 ):
201:     bool hasOriginationFee = originationFeeAmount > 0;

205:       (protocolFeeBips > 0 && nullFeeRecipient) ||
67:     if (timeWithPenalty > 0) {

155:     if (protocolFeeBips > 0) {

159:     if (delinquencyFeeBips > 0) {
  • WildcatMarket.sol ( #L147 ):
147:     if (_withdrawalData.unpaidBatches.length() > 0) {
79:     if ((parameters.protocolFeeBips > 0).and(parameters.feeRecipient == address(0))) {

170:       if (scaledBalance > 0) {

428:       if (availableLiquidity > 0) {

472:     if (availableLiquidity > 0) {
  • WildcatMarketWithdrawals.sol ( #L32, #L112 ):
32:     if ((expiry == expiredBatchExpiry).and(expiry > 0)) {

112:     if (availableLiquidity > 0) {

[D‑25] Avoid contract existence checks by using low level calls

Prior to 0.8.10 the compiler inserted extra code, including EXTCODESIZE (100 gas), to check for contract existence for external function calls. In more recent solidity versions, the compiler will not insert these checks if the external call has a return value. Similar behavior can be achieved in earlier versions by using low-level calls, since low level calls never check for contract existence.

There are 29 instances (click to show):
/// @audit Invalid for solidity version >= 0.8.10
96:     MarketControllerParameters memory parameters = controllerFactory.getMarketControllerParameters();

/// @audit Return value not used.
188:       WildcatMarket(market).updateAccountAuthorization(lender, _authorizedLenders.contains(lender));

/// @audit Invalid for solidity version >= 0.8.10
303:       if (!archController.isRegisteredBorrower(msg.sender)) {

/// @audit Invalid for solidity version >= 0.8.10
341:     ) = controllerFactory.getProtocolFeeConfiguration();

/// @audit Return value not used.
356:     archController.registerMarket(market);

/// @audit Invalid for solidity version >= 0.8.10
474:     if (annualInterestBips < WildcatMarket(market).annualInterestBips()) {

/// @audit Invalid for solidity version >= 0.8.10
478:         tmp.reserveRatioBips = uint128(WildcatMarket(market).reserveRatioBips());

/// @audit Return value not used.
481:         WildcatMarket(market).setReserveRatioBips(9000);

/// @audit Return value not used.
487:     WildcatMarket(market).setAnnualInterestBips(annualInterestBips);

/// @audit Return value not used.
499:     WildcatMarket(market).setReserveRatioBips(uint256(tmp.reserveRatioBips).toUint16());
/// @audit Invalid for solidity version >= 0.8.10
283:     if (!archController.isRegisteredBorrower(msg.sender)) {

/// @audit Return value not used.
299:     archController.registerController(controller);

/// @audit Invalid for solidity version >= 0.8.10
329:     market = IWildcatMarketController(controller).deployMarket(
330:       asset,
331:       namePrefix,
332:       symbolPrefix,
333:       maxTotalSupply,
334:       annualInterestBips,
335:       delinquencyFeeBips,
336:       withdrawalBatchDuration,
337:       reserveRatioBips,
338:       delinquencyGracePeriod
339:     );
/// @audit Invalid for solidity version >= 0.8.10
18:     (borrower, account, asset) = WildcatSanctionsSentinel(sentinel).tmpEscrowParams();

/// @audit Invalid for solidity version >= 0.8.10
22:     return IERC20(asset).balanceOf(address(this));

/// @audit Invalid for solidity version >= 0.8.10
26:     return !WildcatSanctionsSentinel(sentinel).isSanctioned(borrower, account);

/// @audit Return value not used.
38:     IERC20(asset).transfer(account, amount);
/// @audit Invalid for solidity version >= 0.8.10
42:       IChainalysisSanctionsList(chainalysisSanctionsList).isSanctioned(account);

/// @audit Invalid for solidity version >= 0.8.10
100:     if (!IWildcatArchController(archController).isRegisteredMarket(msg.sender)) {

/// @audit Return value not used.
110:     new WildcatSanctionsEscrow{ salt: keccak256(abi.encode(borrower, account, asset)) }();
/// @audit Invalid for solidity version >= 0.8.10
77:     MarketParameters memory parameters = IWildcatMarketController(msg.sender).getMarketParameters();

/// @audit Invalid for solidity version >= 0.8.10
99:     decimals = IERC20Metadata(parameters.asset).decimals();

/// @audit Invalid for solidity version >= 0.8.10
172:         address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow(
173:           accountAddress,
174:           borrower,
175:           address(this)
176:         );

/// @audit Invalid for solidity version >= 0.8.10
204:       if (IWildcatMarketController(controller).isAuthorizedLender(accountAddress)) {

/// @audit Invalid for solidity version >= 0.8.10
239:     return IERC20(asset).balanceOf(address(this));
  • WildcatMarketConfig.sol ( #L75, #L94 ):
/// @audit Invalid for solidity version >= 0.8.10
75:     if (!IWildcatSanctionsSentinel(sentinel).isSanctioned(borrower, accountAddress)) {

/// @audit Invalid for solidity version >= 0.8.10
94:     if (IWildcatSanctionsSentinel(sentinel).isSanctioned(borrower, accountAddress)) {
/// @audit Invalid for solidity version >= 0.8.10
164:     if (IWildcatSanctionsSentinel(sentinel).isSanctioned(borrower, accountAddress)) {

/// @audit Invalid for solidity version >= 0.8.10
166:       address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow(
167:         accountAddress,
168:         borrower,
169:         address(asset)
170:       );

[D‑26] Consider using named mappings

The rule is valid, but the following findings are invalid.

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L20 ):
20:   mapping(address borrower => mapping(address account => bool sanctionOverride))

[D‑27] Events that mark critical parameter changes should contain both the old and the new value

This should especially be done if the new value is not required to be different from the old value.

There are 6 instances:

  • WildcatMarket.sol ( #L67 ):
67:     emit Transfer(address(0), msg.sender, amount);
177:         emit Transfer(accountAddress, escrow, state.normalizeAmount(scaledBalance));

524:     emit Transfer(address(this), address(0), normalizedAmountPaid);
  • WildcatMarketToken.sol ( #L61, #L81 ):
61:     emit Approval(approver, spender, amount);

81:     emit Transfer(from, to, amount);
  • WildcatMarketWithdrawals.sol ( #L91 ):
91:     emit Transfer(msg.sender, address(this), amount);

[D‑28] State variables should include comments

There are comments for the following state variables.

There is 1 instance:

  • ReentrancyGuard.sol ( #L20 ):
20:   uint256 private _reentrancyGuard;

[D‑29] safeTransfer function does not check for contract existence

The examples below are either not token transfers, or are making high-level transfer()/transferFrom() calls (which check for contract existence), or are from a library that checks for contract existence.

There are 7 instances (click to show):
  • ReentrancyGuard.sol ( #L49 ):
49:   constructor() {
  • WildcatArchController.sol ( #L55 ):
55:   constructor() {
  • WildcatMarketController.sol ( #L94 ):
94:   constructor() {
  • WildcatMarketControllerFactory.sol ( #L72 ):
72:   constructor(
  • WildcatSanctionsEscrow.sol ( #L16 ):
16:   constructor() {
  • WildcatSanctionsSentinel.sol ( #L24 ):
24:   constructor(address _archController, address _chainalysisSanctionsList) {
  • WildcatMarketBase.sol ( #L76 ):
76:   constructor() {

[D‑30] Critical functions should use two-step procedure

The rule is invalid for this project

There are 2 instances:

182:   function updateLenderAuthorization(address lender, address[] memory markets) external {

468:   function setAnnualInterestBips(
469:     address market,
470:     uint16 annualInterestBips
471:   ) external virtual onlyBorrower onlyControlledMarket(market) {

[D‑31] Assembly block creates dirty bits

Writing data to the free memory pointer without later updating the free memory pointer will cause there to be dirty bits at that memory location. Not updating the free memory pointer will make it harder for the optimizer to reason about whether the memory needs to be cleaned before use, which will lead to worse optimizations. Update the free memory pointer and annotate the block (assembly ("memory-safe") { ... }) to avoid this issue.

There are 38 instances (click to show):
403:     assembly {

404:       if or(iszero(mload(namePrefix)), iszero(mload(symbolPrefix))) {

509:     assembly {

510:       if or(lt(value, min), gt(value, max)) {
6:     assembly {

12:     assembly {

18:     assembly {
25:   assembly {

36:   assembly {

48:   assembly {

61:   assembly {
8:     assembly {

35:       if iszero(initCodeStorage) {

49:     assembly {
  • MarketState.sol ( #L132 ):
132:     assembly {
60:     assembly {

76:     assembly {

86:     assembly {

88:       if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_BIP), b))))) {

106:     assembly {

108:       if or(iszero(b), gt(a, div(sub(not(0), div(b, 2)), BIP))) {

123:     assembly {

126:       if iszero(eq(div(b, BIP_RAY_RATIO), a)) {

139:     assembly {

141:       if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_RAY), b))))) {

156:     assembly {

158:       if or(iszero(b), gt(a, div(sub(not(0), div(b, 2)), RAY))) {

174:     assembly {

176:       if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {

192:     assembly {

194:       if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {
  • SafeCastLib.sol ( #L8, #L9 ):
8:     assembly {

9:       if iszero(didNotOverflow) {
39:   assembly {

47:     if or(iszero(status), iszero(or(isBytes32, eq(returndatasize(), 0x60)))) {

49:       if iszero(status) {

51:         if returndatasize() {

66:     assembly {

[D‑32] Avoid updating storage when the value hasn't changed

Manipulating storage in solidity is gas-intensive. It can be optimized by avoiding unnecessary storage updates when the new value equals the existing value. If the old value is equal to the new value, not re-storing the value will avoid a Gsreset (2900 gas), potentially at the expense of a Gcoldsload (2100 gas) or a Gwarmaccess (100 gas).

There are 2 instances:

  • WildcatMarketControllerFactory.sol ( #L286 ):
286:     _tmpMarketBorrowerParameter = msg.sender;
  • WildcatMarketToken.sol ( #L60 ):
60:     allowance[approver][spender] = amount;

[D‑33] Calculations should be memoized rather than re-calculating them

The function calls in solidity are expensive. If the same result of the same function calls are to be used several times, the result should be cached to reduce the gas consumption of repeated calls.

There are 21 instances (click to show):
/// @audit assertValueInRange(annualInterestBips,MinimumAnnualInterestBips,MaximumAnnualInterestBips,AnnualInterestBipsOutOfBounds.selector)
410:     assertValueInRange(

/// @audit assertValueInRange(delinquencyFeeBips,MinimumDelinquencyFeeBips,MaximumDelinquencyFeeBips,DelinquencyFeeBipsOutOfBounds.selector)
416:     assertValueInRange(

/// @audit assertValueInRange(withdrawalBatchDuration,MinimumWithdrawalBatchDuration,MaximumWithdrawalBatchDuration,WithdrawalBatchDurationOutOfBounds.selector)
422:     assertValueInRange(

/// @audit assertValueInRange(reserveRatioBips,MinimumReserveRatioBips,MaximumReserveRatioBips,ReserveRatioBipsOutOfBounds.selector)
428:     assertValueInRange(

/// @audit assertValueInRange(delinquencyGracePeriod,MinimumDelinquencyGracePeriod,MaximumDelinquencyGracePeriod,DelinquencyGracePeriodOutOfBounds.selector)
434:     assertValueInRange(

/// @audit revertWithSelector(AprChangeNotPending.selector)
493:       revertWithSelector(AprChangeNotPending.selector);

/// @audit revertWithSelector(ExcessReserveRatioStillActive.selector)
496:       revertWithSelector(ExcessReserveRatioStillActive.selector);
/// @audit previousTimeDelinquent.satSub(timeDelta)
115:     state.timeDelinquent = previousTimeDelinquent.satSub(timeDelta).toUint32();

/// @audit previousTimeDelinquent.satSub(delinquencyGracePeriod)
119:     uint256 secondsRemainingWithPenalty = previousTimeDelinquent.satSub(delinquencyGracePeriod);
/// @audit string.concat(parameters.namePrefix,queryName(parameters.asset))
97:     name = string.concat(parameters.namePrefix, queryName(parameters.asset));

/// @audit string.concat(parameters.symbolPrefix,querySymbol(parameters.asset))
98:     symbol = string.concat(parameters.symbolPrefix, querySymbol(parameters.asset));

/// @audit MathUtils.bipToRay(state.annualInterestBips)
321:     uint256 apr = MathUtils.bipToRay(state.annualInterestBips).bipMul(BIP + protocolFeeBips);

/// @audit MathUtils.bipToRay(delinquencyFeeBips)
323:       apr += MathUtils.bipToRay(delinquencyFeeBips);

/// @audit state.updateScaleFactorAndFees(protocolFeeBips,delinquencyFeeBips,delinquencyGracePeriod,expiry)
366:         (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee) = state

/// @audit state.updateScaleFactorAndFees(protocolFeeBips,delinquencyFeeBips,delinquencyGracePeriod,block.timestamp)
379:       (uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee) = state

/// @audit state.updateScaleFactorAndFees(protocolFeeBips,delinquencyFeeBips,delinquencyGracePeriod,expiredBatchExpiry)
415:         state.updateScaleFactorAndFees(

/// @audit state.updateScaleFactorAndFees(protocolFeeBips,delinquencyFeeBips,delinquencyGracePeriod,block.timestamp)
435:       state.updateScaleFactorAndFees(
  • WildcatMarketToken.sol ( #L72, #L76 ):
/// @audit _getAccount(from)
72:     Account memory fromAccount = _getAccount(from);

/// @audit _getAccount(to)
76:     Account memory toAccount = _getAccount(to);
/// @audit asset.safeTransfer(escrow,normalizedAmountWithdrawn)
171:       asset.safeTransfer(escrow, normalizedAmountWithdrawn);

/// @audit asset.safeTransfer(accountAddress,normalizedAmountWithdrawn)
179:       asset.safeTransfer(accountAddress, normalizedAmountWithdrawn);

[D‑34] Inconsistent spacing in comments

The comments below are in the //x format, which differs from the commonly used idiomatic comment syntax of //<space>x. It is recommended to use a consistent comment syntax throughout.

There are 5 instances:

  • ReentrancyGuard.sol ( #L7 ):
/// @audit It is an URL
7:  *         https://github.com/ProjectOpenSea/seaport/blob/main/contracts/lib/ReentrancyGuard.sol
/// @audit It is an URL
83:    *      see https://twitter.com/transmissions11/status/1451131036377571328

/// @audit It is an URL
103:    *      see https://twitter.com/transmissions11/status/1451131036377571328

/// @audit It is an URL
136:    *      see https://twitter.com/transmissions11/status/1451131036377571328

/// @audit It is an URL
153:    *      see https://twitter.com/transmissions11/status/1451131036377571328

[D‑35] Redundant state variable getters

The rule is valid, but the following findings are invalid.

There is 1 instance:

/// @audit `_state` is not public
267:   function previousState() external view returns (MarketState memory) {
268:     return _state;
269:   }

[D‑36] keccak256() should only need to be called on a specific string literal once

The hashes of literals should be stored in immutable variables, and the immutable variables should be used instead. If the hash is being used as a part of a function selector, the cast to bytes4 should also only be done once.

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L12 ):
/// @audit Assigned to constant
12:     keccak256(type(WildcatSanctionsEscrow).creationCode);

[D‑37] Use SafeCast to downcast safely

The rule is valid, but the following findings are invalid.

There are 2 instances:

/// @audit uint256(account.approval)
210:     if (uint256(account.approval) < uint256(requiredRole)) {

/// @audit uint256(requiredRole)
210:     if (uint256(account.approval) < uint256(requiredRole)) {

[D‑38] Use assembly to compute hashes to save gas

If the arguments to the encode call can fit into the scratch space (two words or fewer), then it's more efficient to use assembly to generate the hash (80 gas):

keccak256(abi.encodePacked(x, y)) -> assembly {mstore(0x00, a); mstore(0x20, b); let hash := keccak256(0x00, 0x40); }

There is 1 instance:

  • WildcatSanctionsSentinel.sol ( #L74-L81 ):
74:             keccak256(
75:               abi.encodePacked(
76:                 bytes1(0xff),
77:                 address(this),
78:                 keccak256(abi.encode(borrower, account, asset)),
79:                 WildcatSanctionsEscrowInitcodeHash
80:               )
81:             )

[D‑39] Use calldata instead of memory for immutable arguments

Mark data types as calldata instead of memory where possible. This makes it so that the data is not automatically loaded into memory. If the data passed into the function does not need to be changed (like updating values in an array), it can be passed in as calldata. The one exception to this is if the argument must later be passed into another function that takes an argument that specifies memory storage.

There are 9 instances (click to show):
/// @audit state
40:   function applyProtocolFee(
41:     MarketState memory state,
42:     uint256 baseInterestRay,
43:     uint256 protocolFeeBips
44:   ) internal pure returns (uint256 protocolFee) {

/// @audit state
89:   function updateTimeDelinquentAndGetPenaltyTime(
90:     MarketState memory state,
91:     uint256 delinquencyGracePeriod,
92:     uint256 timeDelta
93:   ) internal pure returns (uint256 /* timeWithPenalty */) {

/// @audit state
142:   function updateScaleFactorAndFees(
143:     MarketState memory state,
144:     uint256 protocolFeeBips,
145:     uint256 delinquencyFeeBips,
146:     uint256 delinquencyGracePeriod,
147:     uint256 timestamp
148:   )
/// @audit state
448:   function _writeState(MarketState memory state) internal {

/// @audit state
466:   function _processExpiredWithdrawalBatch(MarketState memory state) internal {

/// @audit batch
/// @audit state
498:   function _applyWithdrawalBatchPayment(
499:     WithdrawalBatch memory batch,
500:     MarketState memory state,
501:     uint32 expiry,
502:     uint256 availableLiquidity
503:   ) internal {

/// @audit batch
/// @audit state
528:   function _applyWithdrawalBatchPaymentView(
529:     WithdrawalBatch memory batch,
530:     MarketState memory state,
531:     uint256 availableLiquidity
532:   ) internal pure {

[D‑40] Consider adding emergency-stop functionality

The rule is valid, but the following findings are invalid.

There are 8 instances (click to show):
  • BoolUtils.sol ( #L4 ):
4: library BoolUtils {
  • FIFOQueue.sol ( #L16 ):
16: library FIFOQueueLib {
  • FeeMath.sol ( #L11 ):
11: library FeeMath {
  • LibStoredInitCode.sol ( #L4 ):
4: library LibStoredInitCode {
  • MarketState.sol ( #L44 ):
44: library MarketStateLib {
  • MathUtils.sol ( #L16 ):
16: library MathUtils {
  • SafeCastLib.sol ( #L6 ):
6: library SafeCastLib {
  • Withdrawal.sol ( #L37 ):
37: library WithdrawalLib {

[D‑41] Use descriptive constant instead of 0 as a parameter

Passing 0 or 0x0 as a function argument can sometimes result in a security issue(e.g. passing zero as the slippage parameter). A historical example is the infamous 0x0 address bug where numerous tokens were lost. This happens because 0 can be interpreted as an uninitialized address, leading to transfers to the 0x0 address, effectively burning tokens. Moreover, 0 as a denominator in division operations would cause a runtime exception. It's also often indicative of a logical error in the caller's code.

Consider using a constant variable with a descriptive name, so it's clear that the argument is intentionally being used, and for the right reasons.

There are 2 instances:

  • WildcatMarket.sol ( #L67 ):
67:     emit Transfer(address(0), msg.sender, amount);
  • WildcatMarketBase.sol ( #L524 ):
524:     emit Transfer(address(this), address(0), normalizedAmountPaid);

[D‑42] Prevent re-setting a state variable with the same value

This especially problematic when the setter also emits the same value, which may be confusing to offline parsers.

There is 1 instance:

  • WildcatMarketControllerFactory.sol ( #L286 ):
286:     _tmpMarketBorrowerParameter = msg.sender;

[D‑43] The deadline timestamp should be considered valid

According to EIP-2612, signatures used on exactly the deadline timestamp are supposed to be allowed. While the signature may or may not be used for the exact EIP-2612 use case (transfer approvals). For consistency's sake, all deadlines should follow this semantic. If the timestamp is an expiration rather than a deadline, consider whether it makes more sense to include the expiration timestamp as a valid timestamp, as is done for deadlines.

There are 3 instances:

  • WildcatMarketController.sol ( #L495 ):
495:     if (block.timestamp < tmp.expiry) {
  • WildcatMarketWithdrawals.sol ( #L49, #L141 ):
49:     if (expiry > block.timestamp) {

141:     if (expiry > block.timestamp) {

[D‑44] Loss of precision

Division by large numbers may result in the result being zero, due to solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator.

There are 2 instances:

23:     size = (sizeInBits + 7) / 8;

73:       size = (sizeInBits + 7) / 8;

[D‑45] Consider using bytes32 rather than a string

Using the bytes types for fixed-length strings is more efficient than having the EVM have to incur the overhead of string processing. Consider whether the value needs to be a string. A good reason to keep it as a string would be if the variable is defined in an interface that this project does not own.

There are 3 instances:

24:   string public constant version = '1.0';

57:   string public name;

60:   string public symbol;

[D‑46] Some tokens may revert when large transfers are made

Tokens such as COMP or UNI will revert when an address' balance reaches type(uint96).max. Ensure that the calls below can be broken up into smaller batches if necessary.

There are 8 instances (click to show):
  • WildcatMarketController.sol ( #L346 ):
346:       originationFeeAsset.safeTransferFrom(borrower, parameters.feeRecipient, originationFeeAmount);
  • WildcatSanctionsEscrow.sol ( #L38 ):
38:     IERC20(asset).transfer(account, amount);
60:     asset.safeTransferFrom(msg.sender, address(this), amount);

107:     asset.safeTransfer(feeRecipient, withdrawableFees);

129:     asset.safeTransfer(msg.sender, amount);

157:       asset.safeTransfer(borrower, currentlyHeld - totalDebts);
171:       asset.safeTransfer(escrow, normalizedAmountWithdrawn);

179:       asset.safeTransfer(accountAddress, normalizedAmountWithdrawn);

[D‑47] unchecked blocks with additions/multiplications may overflow

There aren't any checks to avoid an overflow which can happen inside an unchecked block, so the following subtractions may overflow silently.

There are 2 instances:

23:     size = (sizeInBits + 7) / 8;

73:       size = (sizeInBits + 7) / 8;

[D‑48] unchecked blocks with subtractions may underflow

There aren't any checks to avoid an underflow which can happen inside an unchecked block, so the following subtractions may underflow silently.

There are 2 instances:

22:     uint256 sizeInBits = 255 - uint256(value).ffs();

72:       uint256 sizeInBits = 255 - value.ffs();

[D‑49] Numeric values having to do with time should use time units for readability

The rule is valid, but the following findings are invalid.

There is 1 instance:

  • WildcatMarketController.sol ( #L484 ):
/// @audit 2 weeks
484:       tmp.expiry = uint128(block.timestamp + 2 weeks);

[D‑50] Contracts are vulnerable to rebasing accounting-related issues

Rebasing tokens are tokens that have each holder's balanceof() increase over time. Aave aTokens are an example of such tokens. If rebasing tokens are used, rewards accrue to the contract holding the tokens, and cannot be withdrawn by the original depositor. To address the issue, track 'shares' deposited on a pro-rata basis, and let shares be redeemed for their proportion of the current balance at the time of the withdrawal.

There are 7 instances (click to show):
291:   function deployMarket(
292:     address asset,
293:     string memory namePrefix,
294:     string memory symbolPrefix,
295:     uint128 maxTotalSupply,
296:     uint16 annualInterestBips,
297:     uint16 delinquencyFeeBips,
298:     uint32 withdrawalBatchDuration,
299:     uint16 reserveRatioBips,
300:     uint32 delinquencyGracePeriod
301:   ) external returns (address market) {
  • WildcatSanctionsEscrow.sol ( #L33 ):
33:   function releaseEscrow() public override {
42:   function depositUpTo(
43:     uint256 amount
44:   ) public virtual nonReentrant returns (uint256 /* actualAmount */) {

96:   function collectFees() external nonReentrant {

119:   function borrow(uint256 amount) external onlyBorrower nonReentrant {

142:   function closeMarket() external onlyController nonReentrant {
137:   function executeWithdrawal(
138:     address accountAddress,
139:     uint32 expiry
140:   ) external nonReentrant returns (uint256) {
@MarioPoneder
Copy link

Was asked by C4 staff to provide my thoughts about this bot report's Medium findings.
Tagging @laurenceday for visibility.

M-01: Centralization risk for privileged functions

It's always justified to point out centralization risks, i.e. methods with onlyOwner modifier in this particular case.
However, in the vast majority of cases the severity is Low/Informational since it's part of a protocol's design.
Furthermore, it always depends on who is the owner, could be just an EOA or better a multisig wallet or even a DAO.
All those factors affect the resulting severity.

In case of Wildcat, I consider the risk Low, so the bot report finding would not qualify as Medium severity finding if I had to judge it.

M-02: Return values of transfer()/transferFrom() not checked

[also M-03: Unsafe use of ERC20 transfer()/transferFrom() (duplicate of M-02)]

Even checking the return values of ERC-20 transfers is problematic and insufficient due to popular tokens like USDT deviating from the spec.
One always has to use SafeERC20 (as already mentioned by the sponsor), otherwise this is a valid Medium severity finding.

However, there is an exception in case the transferred tokens cannot be chosen freely by the users but need to be whitelisted. Also there are occasions where only a protocol's own correctly implemented tokens can be used (Wildcat market tokens for example). In these cases, the severity is only Informational, since no malfunction can occur.

In case of Wildact, if I remember correctly, the WildcatSanctionsEscrow contract could hold user-defined assets and not only their own market tokens. Therefore, Medium severity is justified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment