The Wildcat Protocol - 0xComfyCat's results

Banking, but worse - a protocol for fixed-rate, undercollateralised credit facilities.

General Information

Platform: Code4rena

Start Date: 16/10/2023

Pot Size: $60,500 USDC

Total HM: 16

Participants: 131

Period: 10 days

Judge: 0xTheC0der

Total Solo HM: 3

Id: 296

League: ETH

Wildcat Protocol

Findings Distribution

Researcher Performance

Rank: 75/131

Findings: 3

Award: $16.79

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarket.sol#L142 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatMarketController.sol#L32-L516

Vulnerability details

Impact

When borrower is done borrowing from market, they should be able to close the market to avoid paying interest further. The closeMarket function of market contract must be called by it's market controller. But in current market controller implementation, the function that suppose to perform a call to closeMarket doesn't exist. Therefore borrower will not be able to close the market.

Proof of Concept

Search through the code repository and no such function exist.

Tools Used

Manual review

Implement close market functionality on market controller contract

Assessed type

Other

#0 - c4-pre-sort

2023-10-27T07:31:54Z

minhquanym marked the issue as duplicate of #147

#1 - c4-judge

2023-11-07T14:09:15Z

MarioPoneder marked the issue as partial-50

#2 - c4-judge

2023-11-07T14:16:53Z

MarioPoneder changed the severity to 3 (High Risk)

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatMarketController.sol#L32-L516 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketConfig.sol#L134

Vulnerability details

Impact

Market's maxTotalSupply parameter is set during market creation and can be updated via market config setMaxTotalSupply function which is marked with onlyController modifier. But in current market controller implementation, the function that suppose to perform a call to market's setMaxTotalSupply doesn't exist. Therefore borrower will not be able to update market maxTotalSupply parameter after the market has been deployed.

Proof of Concept

Search through the code repository and no such function exist.

Tools Used

Manual review

Implement such functionality on market controller contract

Assessed type

Other

#0 - c4-pre-sort

2023-10-27T07:02:09Z

minhquanym marked the issue as duplicate of #147

#1 - c4-judge

2023-11-07T13:57:13Z

MarioPoneder marked the issue as partial-50

#2 - c4-judge

2023-11-07T14:16:53Z

MarioPoneder changed the severity to 3 (High Risk)

Awards

6.5602 USDC - $6.56

Labels

bug
3 (High Risk)
partial-50
sponsor confirmed
sufficient quality report
upgraded by judge
duplicate-266

External Links

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketConfig.sol#L118-L122 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatMarketController.sol#L182-L190

Vulnerability details

Impact

Per documentation "However, it is worth noting that withdrawal requests (and subsequent claims/redemptions) can only be generated by addresses that have been approved for the controller contract of a given market" and stated invariant "The assets of a market should never be able to be withdrawn by anyone that is not the borrower or a lender with either the WithdrawOnly or DepositAndWithdraw role."

Only address with WithdrawOnly role and above should be able to queue withdrawal. The role check is performed within queueWithdrawal correctly. However, arbitrary address with default AuthRole Null that is not an approved lender can obtain WithdrawOnly by calling controller updateLenderAuthorization. Thus, anyone can queue a withdrawal and violate the invariant.

Proof of Concept

Controller updateLenderAuthorization makes an updateAccountAuthorization call to the specified market. Sending a boolean flag which determine if the lender is authorized or not.

function updateLenderAuthorization(address lender, address[] memory markets) external { for (uint256 i; i < markets.length; i++) { ... WildcatMarket(market).updateAccountAuthorization(lender, _authorizedLenders.contains(lender)); } }

Market's updateAccountAuthorization then grant lender role based on the flag.

function updateAccountAuthorization( address _account, bool _isAuthorized ) external onlyController nonReentrant { ... if (_isAuthorized) { account.approval = AuthRole.DepositAndWithdraw; } else { account.approval = AuthRole.WithdrawOnly; } ... }

However, even the account is not authorized, the function will grant them WithdrawOnly role by default.

Following test case showed that bob with initial Null role can obtain WithdrawOnly role and queue a withdrawal. The test can be paste into WithdrawalsTest contract and run as-is.

function test_AnyoneCanWithdraw() public { // Bob failed to queue withdrawal // Copied from test_queueWithdrawal_NotApprovedLender _deposit(alice, 1e18); vm.prank(alice); market.transfer(bob, 1e18); vm.startPrank(bob); vm.expectRevert(IMarketEventsAndErrors.NotApprovedLender.selector); market.queueWithdrawal(1e18); // End copy // Bob can't queue a withdrawal because his role is null assertEq(uint8(market.getAccountRole(bob)), uint8(AuthRole.Null)); // So his withdrawal amount should be 0 AccountWithdrawalStatus memory status = market .getAccountWithdrawalStatus( bob, uint32(block.timestamp + parameters.withdrawalBatchDuration) ); assertEq(status.scaledAmount, 0); // We update bob role to WithdrawOnly via `updateLenderAuthorization` address[] memory markets = new address[](1); markets[0] = address(market); controller.updateLenderAuthorization(bob, markets); // Now bob has the WithdrawOnly role assertEq( uint8(market.getAccountRole(bob)), uint8(AuthRole.WithdrawOnly) ); // Bob tries to queue a withdrawal and succeeds vm.prank(bob); market.queueWithdrawal(1e18); // His withdrawal is queued status = market.getAccountWithdrawalStatus( bob, uint32(block.timestamp + parameters.withdrawalBatchDuration) ); assertEq(status.scaledAmount, 1e18); }

Tools Used

  • Manual review
  • Foundry

On updateAccountAuthorization grant role Null by default if lender is not authorized.

Assessed type

Access Control

#0 - c4-pre-sort

2023-10-27T08:50:48Z

minhquanym marked the issue as primary issue

#1 - c4-pre-sort

2023-10-27T17:14:49Z

minhquanym marked the issue as sufficient quality report

#2 - laurenceday

2023-10-29T15:01:36Z

This one was a really good find, thank you to everyone who discovered it.

Had this gone unchecked, any arbitrary wallet would have been able to make withdrawal requests to markets provided that they had market tokens - this doesn't break any of the actual math involved, but could lead to pollution of otherwise well-curated authorisation lists by borrowers on their controllers, and - more concerningly - sidestep any assurances that a borrower might have been able to build up that they weren't creditors to someone operating from a sanctioned jurisdiction.

We've fixed this by requiring that an account already hold the WithdrawAndDeposit role (so they can only be bumped down to WithdrawOnly) if the _isAuthorized parameter is false: see https://github.com/wildcat-finance/wildcat-protocol/issues/40.

Excellent work, we appreciate it!

#3 - laurenceday

2023-10-31T22:27:10Z

Confirmed as an issue with updateAccountAuthorization: https://github.com/code-423n4/2023-10-wildcat-findings/issues/155#issuecomment-1784137482

The easier way to fix this is to tweak updateAccountAuthorization in a slightly different way than proposed (see comment).

Great find, everyone!

#4 - c4-sponsor

2023-10-31T22:27:31Z

laurenceday (sponsor) confirmed

#5 - c4-judge

2023-11-07T12:50:49Z

MarioPoneder marked the issue as satisfactory

#6 - c4-judge

2023-11-07T12:59:26Z

MarioPoneder marked issue #236 as primary and marked this issue as a duplicate of 236

#7 - c4-judge

2023-11-14T13:59:01Z

MarioPoneder changed the severity to 3 (High Risk)

#8 - c4-judge

2023-11-14T14:00:33Z

MarioPoneder marked the issue as partial-50

#9 - c4-judge

2023-11-14T14:02:30Z

MarioPoneder marked the issue as duplicate of #266

Low

It is not trivial to withdraw 100%

It is not trivial to withdraw 100% because the input amount of queueWithdrawal will be less than actual market token balance of lender when the transaction is executed due to interest accrual. Consider add function queueWithdrawalAll

If borrower set reserve ratio higher than 90% they can reduce it to 90% consistently by calling setAnnualInterestBips with lower interest

setAnnualInterestBips always fixed reserveRatio to 90%. If somehow borrower deploy a market with 95% reserveRatio they can lower it to 90% consistently by calling setAnnualInterestBips every 2 weeks.

Non-critical

annualInterestBips is too restrictive

annualInterestBips is hard-capped at 100% in market config contract setReserveRatioBips. Borrower might want to deploy a market with higher interest rate to attract lenders.

#0 - c4-judge

2023-11-09T15:17:08Z

MarioPoneder marked the issue as grade-c

#1 - c4-judge

2023-11-09T15:17:16Z

MarioPoneder marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter