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: 75/131
Findings: 3
Award: $16.79
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xpiken
Also found by: 0xCiphky, 0xComfyCat, 0xStalin, 0xhegel, 0xkazim, 3docSec, AM, Aymen0909, CaeraDenoir, DeFiHackLabs, Drynooo, Eigenvectors, Fulum, HALITUS, HChang26, Jiamin, Juntao, LokiThe5th, Mike_Bello90, MiloTruck, QiuhaoLi, Silvermist, SovaSlava, SpicyMeatball, T1MOH, Toshii, TrungOre, TuringConsulting, Vagner, Yanchuan, ZdravkoHr, _nd_koo, almurhasan, audityourcontracts, ayden, cartlex_, circlelooper, crunch, cu5t0mpeo, deth, erictee, ggg_ttt_hhh, gizzy, gumgumzum, hash, jasonxiale, josephdara, ke1caM, kodyvim, lanrebayode77, marqymarq10, max10afternoon, nirlin, nonseodion, osmanozdemir1, peter, radev_sw, rvierdiiev, said, serial-coder, sl1, smiling_heretic, squeaky_cactus, stackachu, tallo, trachev, zaevlad
0.0606 USDC - $0.06
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
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.
Search through the code repository and no such function exist.
Manual review
Implement close market functionality on market controller contract
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)
🌟 Selected for report: 0xpiken
Also found by: 0xCiphky, 0xComfyCat, 0xStalin, 0xhegel, 0xkazim, 3docSec, AM, Aymen0909, CaeraDenoir, DeFiHackLabs, Drynooo, Eigenvectors, Fulum, HALITUS, HChang26, Jiamin, Juntao, LokiThe5th, Mike_Bello90, MiloTruck, QiuhaoLi, Silvermist, SovaSlava, SpicyMeatball, T1MOH, Toshii, TrungOre, TuringConsulting, Vagner, Yanchuan, ZdravkoHr, _nd_koo, almurhasan, audityourcontracts, ayden, cartlex_, circlelooper, crunch, cu5t0mpeo, deth, erictee, ggg_ttt_hhh, gizzy, gumgumzum, hash, jasonxiale, josephdara, ke1caM, kodyvim, lanrebayode77, marqymarq10, max10afternoon, nirlin, nonseodion, osmanozdemir1, peter, radev_sw, rvierdiiev, said, serial-coder, sl1, smiling_heretic, squeaky_cactus, stackachu, tallo, trachev, zaevlad
0.0606 USDC - $0.06
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
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.
Search through the code repository and no such function exist.
Manual review
Implement such functionality on market controller contract
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)
🌟 Selected for report: 0xStalin
Also found by: 0xCiphky, 0xComfyCat, 0xbepresent, 3docSec, Eigenvectors, Fulum, HChang26, Infect3d, QiuhaoLi, SandNallani, SovaSlava, TrungOre, YusSecurity, ZdravkoHr, ast3ros, ayden, bdmcbri, cu5t0mpeo, elprofesor, gizzy, jasonxiale, kodyvim, marqymarq10, max10afternoon, nisedo, nobody2018, radev_sw, rvierdiiev, serial-coder, xeros
6.5602 USDC - $6.56
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
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.
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); }
On updateAccountAuthorization
grant role Null
by default if lender is not authorized.
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
🌟 Selected for report: J4X
Also found by: 0x3b, 0xCiphky, 0xComfyCat, 0xStalin, 0xbepresent, 3docSec, DavidGiladi, DeFiHackLabs, Drynooo, Fulum, GREY-HAWK-REACH, InAllHonesty, MatricksDeCoder, Mike_Bello90, MiloTruck, Phantom, SHA_256, T1MOH, Udsen, VAD37, YusSecurity, ZdravkoHr, ast3ros, caventa, deepkin, deth, devival, ggg_ttt_hhh, inzinko, jasonxiale, josieg_74497, karanctf, ke1caM, nisedo, nobody2018, nonseodion, osmanozdemir1, radev_sw, rvierdiiev, serial-coder, t0x1c
10.1663 USDC - $10.17
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
setAnnualInterestBips
with lower interestsetAnnualInterestBips
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.
annualInterestBips
is too restrictiveannualInterestBips
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