The WildcatMarket contract has the mechanism where the delinquency status of the market is not immediately updated when the market becomes delinquent due to accrued interest. This occurs because the delinquency check is performed in the _writeState function, which is called after the state has been updated in _getUpdatedState and pending withdrawals processed using the updated fees.
In WildcatMarketBase.sol, the _getUpdatedState function updates the market state, including accruing interest and fees:
function _getUpdatedState() internal returns (MarketState memory state) {
state = _state;
// ... (code for handling expired withdrawal batch)
if (block.timestamp != lastInterestAccruedTimestamp) {
(uint256 baseInterestRay, uint256 delinquencyFeeRay, uint256 protocolFee) = state
.updateScaleFactorAndFees(
delinquencyFeeBips,
delinquencyGracePeriod,
block.timestamp
);
// ... (emit event)
}
// ... (rest of the function)
}
However, the delinquency status is only checked and updated in the _writeState function typically at the end of methods:
function _writeState(MarketState memory state) internal {
bool isDelinquent = state.liquidityRequired() > totalAssets();
state.isDelinquent = isDelinquent;
// ... (rest of the function)
}
This means that if a market becomes delinquent due to accrued interest and then _getUpdatedState is called, this delinquency status won't be reflected in the isDelinquent flag and less interest will be paid to the lenders.
The impact of this vulnerability includes:
No delinquency fee earned by lenders: Lenders may loose out on delinquency fee for the period between updates.
Potential for exploitation: The borrower could potentially take advantage of this delay to perform direct transfer of just enough assets to the contracts in the hooks to keep the state non delinquent. This would save the borrower delinquency penalty fee which is expected to be higher than base interest APR while not fully meeting the obligation.
Illustration of exploitation scenario:
The market is about to become delinquent.
The borrower, monitoring the situation off-chain, notices this.
A normal queueWithdrawal transaction is about to be processed pushing the market to delinquency.
The borrower can exploit this in two ways:
a) Front-running:
The borrower front-runs the queueWithdrawal transaction with their own transaction to transfer just enough assets to keep the market above the delinquency threshold.
b) Hook manipulation (lower probability):
The borrower implements a malicious onQueueWithdrawal hook that transfers assets to the market just before the withdrawal is processed.
Here's the relevant code in queueWithdrawal:
function queueWithdrawal(
uint256 amount
) external nonReentrant sphereXGuardExternal returns (uint32 expiry) {
MarketState memory state = _getUpdatedState();
// ... other code ...
return _queueWithdrawal(state, account, msg.sender, scaledAmount, amount, _runtimeConstant(0x24));
}
function _queueWithdrawal(
MarketState memory state,
Account memory account,
address accountAddress,
uint104 scaledAmount,
uint normalizedAmount,
uint baseCalldataSize
) internal returns (uint32 expiry) {
// ... other code ...
hooks.onQueueWithdrawal(accountAddress, expiry, scaledAmount, state, baseCalldataSize);
// ... rest of the function
}
In both cases:
The _getUpdatedState() function is called, but it doesn't update the delinquency status immediately.
The withdrawal is processed without marking the market as delinquent, because the borrower's last-minute transfer (either via front-running or the hook) kept the liquidity just above the threshold.
The borrower hopes that the next state update (which would potentially mark the market as delinquent) will occur much later, giving them more time to operate in a non-delinquent state.
Recommended Mitigation Steps
Delinquency status should be updated before withdrawal processing in _getUpdatedState
Lines of code
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketBase.sol#L540-L542
Vulnerability details
Proof of Concept
The WildcatMarket contract has the mechanism where the delinquency status of the market is not immediately updated when the market becomes delinquent due to accrued interest. This occurs because the delinquency check is performed in the _writeState function, which is called after the state has been updated in _getUpdatedState and pending withdrawals processed using the updated fees. In WildcatMarketBase.sol, the _getUpdatedState function updates the market state, including accruing interest and fees:
However, the delinquency status is only checked and updated in the _writeState function typically at the end of methods:
This means that if a market becomes delinquent due to accrued interest and then _getUpdatedState is called, this delinquency status won't be reflected in the isDelinquent flag and less interest will be paid to the lenders.
The impact of this vulnerability includes:
Illustration of exploitation scenario:
The borrower can exploit this in two ways: a) Front-running:
The borrower front-runs the queueWithdrawal transaction with their own transaction to transfer just enough assets to keep the market above the delinquency threshold.
b) Hook manipulation (lower probability):
The borrower implements a malicious onQueueWithdrawal hook that transfers assets to the market just before the withdrawal is processed.
Here's the relevant code in queueWithdrawal:
In both cases:
Recommended Mitigation Steps
Delinquency status should be updated before withdrawal processing in _getUpdatedState
Assessed type
Other