Platform: Code4rena
Start Date: 16/10/2023
Pot Size: $60,500 USDC
Total HM: 16
Participants: 131
Period: 10 days
Judge: 0xTheC0der
Total Solo HM: 3
Id: 296
League: ETH
Rank: 25/131
Findings: 1
Award: $377.71
π Selected for report: 0
π Solo Findings: 0
377.709 USDC - $377.71
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarket.sol#L128
In the file WildcatMarket.sol
, function borrow
calls _writeState
before transferring token from contract to borrower. Thus, _writeState
cannot reflect the actual vault liquidity or asset, then calculate the wrong parameter isDelinquent
.
If isDelinquent
is wrong, delinquency fee cannot be calculated correctly, which may bring losses to lender to borrower.
function borrow(uint256 amount) external onlyBorrower nonReentrant { MarketState memory state = _getUpdatedState(); if (state.isClosed) { revert BorrowFromClosedMarket(); } uint256 borrowable = state.borrowableAssets(totalAssets()); if (amount > borrowable) { revert BorrowAmountTooHigh(); } _writeState(state); asset.safeTransfer(msg.sender, amount); emit Borrow(amount); } function _writeState(MarketState memory state) internal { bool isDelinquent = state.liquidityRequired() > totalAssets(); state.isDelinquent = isDelinquent; _state = state; emit StateUpdated(state.scaleFactor, isDelinquent); }
State parameter isDelinquent
compares liquidityRequired
and totalAssets
to determine if vault is under-collateralized. In the function borrow
, this parameter is calculated before token transfer, which means borrow amount has not been subtracted from totalAssets()
, leading to wrong calculation result.
Manual analysis.
Call _writeState(state); after token transfer.
function borrow(uint256 amount) external onlyBorrower nonReentrant { MarketState memory state = _getUpdatedState(); if (state.isClosed) { revert BorrowFromClosedMarket(); } uint256 borrowable = state.borrowableAssets(totalAssets()); if (amount > borrowable) { revert BorrowAmountTooHigh(); } asset.safeTransfer(msg.sender, amount); _writeState(state); emit Borrow(amount); }
Other
#0 - c4-pre-sort
2023-10-27T15:08:41Z
minhquanym marked the issue as duplicate of #36
#1 - c4-judge
2023-11-07T15:16:01Z
MarioPoneder marked the issue as satisfactory
377.709 USDC - $377.71
In the file WildcatMarket.sol
, function collectFees
calls _writeState
before transferring token from contract to borrower. Thus, _writeState
cannot reflect the actual vault liquidity or asset, then calculate the wrong parameter isDelinquent
.
If isDelinquent
is wrong, delinquency fee cannot be calculated correctly, which may bring losses to lender to borrower.
function collectFees() external nonReentrant { MarketState memory state = _getUpdatedState(); if (state.accruedProtocolFees == 0) { revert NullFeeAmount(); } uint128 withdrawableFees = state.withdrawableProtocolFees(totalAssets()); if (withdrawableFees == 0) { revert InsufficientReservesForFeeWithdrawal(); } state.accruedProtocolFees -= withdrawableFees; _writeState(state); asset.safeTransfer(feeRecipient, withdrawableFees); emit FeesCollected(withdrawableFees); } function _writeState(MarketState memory state) internal { bool isDelinquent = state.liquidityRequired() > totalAssets(); state.isDelinquent = isDelinquent; _state = state; emit StateUpdated(state.scaleFactor, isDelinquent); } function withdrawableProtocolFees( MarketState memory state, uint256 totalAssets ) internal pure returns (uint128) { uint256 totalAvailableAssets = totalAssets - state.normalizedUnclaimedWithdrawals; return uint128(MathUtils.min(totalAvailableAssets, state.accruedProtocolFees)); } function liquidityRequired( MarketState memory state ) internal pure returns (uint256 _liquidityRequired) { uint256 scaledWithdrawals = state.scaledPendingWithdrawals; uint256 scaledRequiredReserves = (state.scaledTotalSupply - scaledWithdrawals).bipMul( state.reserveRatioBips ) + scaledWithdrawals; return state.normalizeAmount(scaledRequiredReserves) + state.accruedProtocolFees + state.normalizedUnclaimedWithdrawals; }
State parameter isDelinquent
compares liquidityRequired
and totalAssets
to determine if vault is under-collateralized. In the function borrow
, this parameter is calculated before token transfer, which means borrow amount has not been subtracted from totalAssets()
, leading to wrong calculation result.
Letβs take a look at an example:
isDelinquent = state.liquidityRequired() > totalAssets()
, where liquidityRequired is 200 WETH and totalAssets() is still 200 WETH. Thus isDelinquent
is false, which is wrong. Since protocol fee has been withdrawn, totalAssets()
drops to 198 WETH, state.liquidityRequired()
is still 200 WETH, isDelinquent
should be true.isDelinquent
incorrectly, thus delinquency fee cannot be calculated correctly, causing losses to lenders.Manual analysis.
Call _writeState(state)
after transferring fee to fee recipient.
function collectFees() external nonReentrant { MarketState memory state = _getUpdatedState(); if (state.accruedProtocolFees == 0) { revert NullFeeAmount(); } uint128 withdrawableFees = state.withdrawableProtocolFees(totalAssets()); if (withdrawableFees == 0) { revert InsufficientReservesForFeeWithdrawal(); } state.accruedProtocolFees -= withdrawableFees; asset.safeTransfer(feeRecipient, withdrawableFees); _writeState(state); emit FeesCollected(withdrawableFees); }
Other
#0 - c4-pre-sort
2023-10-27T15:06:16Z
minhquanym marked the issue as duplicate of #36
#1 - c4-judge
2023-11-07T15:15:08Z
MarioPoneder marked the issue as satisfactory