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: 12/131
Findings: 7
Award: $563.07
π Selected for report: 0
π Solo Findings: 0
π Selected for report: osmanozdemir1
Also found by: 0xCiphky, 0xbepresent, Aymen0909, InAllHonesty, QiuhaoLi, TrungOre, ggg_ttt_hhh, hash, nonseodion, rvierdiiev
304.1365 USDC - $304.14
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarket.sol#L142-L161
In case if market doesn't have enough balance then it's marked as delinquent. In this case if borrower will not transfer additional funds to cover needed funds, then after grace period, penalty will be applied, that borrower will have to pay.
FeeMath.updateTimeDelinquentAndGetPenaltyTime
function is responsible to calculate penalty time that user should pay. This function is called every action along with _getUpdatedState
function. So in case if user has any penalty time, then he should pay for it.
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/libraries/FeeMath.sol#L89-L123
function updateTimeDelinquentAndGetPenaltyTime( MarketState memory state, uint256 delinquencyGracePeriod, uint256 timeDelta ) internal pure returns (uint256 /* timeWithPenalty */) { // Seconds in delinquency at last update uint256 previousTimeDelinquent = state.timeDelinquent; if (state.isDelinquent) { // Since the borrower is still delinquent, increase the total // time in delinquency by the time elapsed. state.timeDelinquent = (previousTimeDelinquent + timeDelta).toUint32(); // Calculate the number of seconds the borrower had remaining // in the grace period. uint256 secondsRemainingWithoutPenalty = delinquencyGracePeriod.satSub( previousTimeDelinquent ); // Penalties apply for the number of seconds the market spent in // delinquency outside of the grace period since the last update. return timeDelta.satSub(secondsRemainingWithoutPenalty); } // Reduce the total time in delinquency by the time elapsed, stopping // when it reaches zero. state.timeDelinquent = previousTimeDelinquent.satSub(timeDelta).toUint32(); // Calculate the number of seconds the old timeDelinquent had remaining // outside the grace period, or zero if it was already in the grace period. uint256 secondsRemainingWithPenalty = previousTimeDelinquent.satSub(delinquencyGracePeriod); // Only apply penalties for the remaining time outside of the grace period. return MathUtils.min(secondsRemainingWithPenalty, timeDelta); }
Penalty time is calculated in 2 ways. It depends on market state: if it's delinquent or not. In case if yes, that means that user still didn't pay all funds and he should pay penalty for the time that is bigger than grace period.
But in case if market is not delinquent anymore and state.timeDelinquent
is not 0, it means that market was delinquent, but recently user has covered needed liquidity. So in this case user still should pay additional penalty until his state.timeDelinquent
is less than grace period. And each time state.timeDelinquent
is decreased with passed time.
As example consider next one. Market has 5 days grace period. Market became delinquent and after 7 days borrower recovered liquidity. So at that moment, when he called _getUpdatedState
function, penalty of 2 days were applied. Now market is delinquent, but user still have to pay penalty for next 2 days(as timer now goes back) until he reaches grace period. So if someone calls any action on day 8, that means that borrower still should pay penalty for 1 day, and he will do that later, not now.
Now when we know this let's talk about closing of market. https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarket.sol#L142-L161
function closeMarket() external onlyController nonReentrant { MarketState memory state = _getUpdatedState(); state.annualInterestBips = 0; state.isClosed = true; state.reserveRatioBips = 0; if (_withdrawalData.unpaidBatches.length() > 0) { revert CloseMarketWithUnpaidWithdrawals(); } uint256 currentlyHeld = totalAssets(); uint256 totalDebts = state.totalDebts(); if (currentlyHeld < totalDebts) { // Transfer remaining debts from borrower asset.safeTransferFrom(borrower, address(this), totalDebts - currentlyHeld); } else if (currentlyHeld > totalDebts) { // Transfer excess assets to borrower asset.safeTransfer(borrower, currentlyHeld - totalDebts); } _writeState(state); emit MarketClosed(block.timestamp); }
Market can be closed only when state.totalDebts()
amount of assets is present in the market.
state.totalDebts()
is normalized total supply + unclaimed assets + protocol fees.
This means that market should be in delinquent
state. However, there is not check that state.timeDelinquent <= delinquencyGracePeriod
, which means that not whole penalty is applied yet. So borrower can close market and avoid repaying all penalty.
Borrower can close market without repaying penalty
VsCode
Add check that state.timeDelinquent <= delinquencyGracePeriod
when close market.
Error
#0 - c4-pre-sort
2023-10-28T02:35:01Z
minhquanym marked the issue as duplicate of #592
#1 - c4-judge
2023-11-07T15:35:50Z
MarioPoneder marked the issue as satisfactory
#2 - c4-judge
2023-11-07T15:41:08Z
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/market/WildcatMarket.sol#L142
WildcatMarket.closeMarket
function allows borrower to close his market, so he repays everything and interests stop accruing.
As you can see currently it can be called by controller only. But the problem is that WildcatMarketController
doesn't have such functionality.
Borrower will not be able to close market and will continue pay interests.
VsCode
Implement such function inside WildcatMarketController
.
Error
#0 - c4-pre-sort
2023-10-27T07:17:36Z
minhquanym marked the issue as duplicate of #147
#1 - c4-judge
2023-11-07T14:04:33Z
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/market/WildcatMarketConfig.sol#L134
WildcatMarketConfig.setMaxTotalSupply
function allows borrower to change max total supply. New value can be only bigger than current total supply.
As you can see currently it can be called by controller only. But the problem is that WildcatMarketController
doesn't have such functionality.
Borrower will not be able to change total supply, so he will not be able to get additional liquidity into the market.
VsCode
Implement such function inside WildcatMarketController
.
Error
#0 - c4-pre-sort
2023-10-27T06:23:20Z
minhquanym marked the issue as duplicate of #162
#1 - c4-pre-sort
2023-10-27T06:58:15Z
minhquanym marked the issue as duplicate of #147
#2 - c4-judge
2023-11-07T13:49:41Z
MarioPoneder marked the issue as partial-50
#3 - 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
13.1205 USDC - $13.12
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketConfig.sol#L74-L81
In order if lender is malicious in any way, then WildcatMarketConfig.nukeFromOrbit
can be called.
This function will check that lender is sanctioned. And if so, then _blockAccount
function is called.
This function will send all tokens of sanctioned user to the escrow. But, it will do that only if address is not already blocked. Note, that address will be marked as blocked, once this check will pass. Then if user has balance it will be sent to the escrow balance.
This should be a good protection that should take funds out of sanctioned user. However, it's not good enough as user can just frontrun nukeFromOrbit
and transfer all his funds to another address(suppose he controls several approved addresses) or he can transfer it to any other authorised lender and then find a way to withdraw(maybe pay commission). This is possible, because transfer
function doesn't check that from
or to
is sanctioned and allows to do transfer to any address. Instead this function calls _getAccount
function to receive to
and from
accounts. But _getAccount
function, only check that account is already blocked and not sanctioned. So frontrun will work.
Sanctioned user can avoid blocking and get all funds.
VsCode
Do not allow to transfer market tokens from
sanctioned address and to
sanctioned address. Maybe make _getAccount
function check if address is sanctioned instead of blocked.
Error
#0 - c4-pre-sort
2023-10-27T03:18:12Z
minhquanym marked the issue as duplicate of #54
#1 - c4-judge
2023-11-07T14:39:03Z
MarioPoneder marked the issue as satisfactory
π 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
13.1205 USDC - $13.12
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketConfig.sol#L121 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketWithdrawals.sol#L81
Wildcat protocol would like only authorized lender to be able to work with market. So borrower has ability to add/remove lenders.
Using WildcatMarketController.updateLenderAuthorization
anyone can trigger from controller to market, which will update lender's status for the market. This will just send true/false depending on lender's authorization.
As result, function will update account.approval
to DepositAndWithdraw
if authorized, or WithdrawOnly
otherwise. The idea of updateAccountAuthorization
function was to add ability to remove someone, who previously was authorized. But also it can be used by anyone to grant WithdrawOnly
approval to any non-blocked address.
Once it's done, then that account has ability to withdraw from market and as result invariant that not allowed lender can't participate with market is broken.
Account that was not allowed by borrower to participate in market is able to do that.
VsCode
Maybe better authorizeLenders
and deauthorizeLenders
function call market.updateAccountAuthorization
for added/removed user.
Error
#0 - c4-pre-sort
2023-10-27T08:23:11Z
minhquanym marked the issue as low quality report
#1 - c4-pre-sort
2023-10-27T08:52:11Z
minhquanym marked the issue as duplicate of #155
#2 - c4-judge
2023-11-07T12:58:13Z
MarioPoneder marked the issue as satisfactory
#3 - c4-judge
2023-11-14T13:59:00Z
MarioPoneder changed the severity to 3 (High Risk)
#4 - c4-judge
2023-11-14T14:01:01Z
MarioPoneder marked the issue as partial-50
#5 - c4-judge
2023-11-14T14:02:23Z
MarioPoneder marked the issue as duplicate of #266
π Selected for report: YusSecurity
Also found by: 0xAsen, 0xCiphky, 0xDING99YA, 0xKbl, 0xSwahili, 0xbepresent, 3docSec, AS, Aymen0909, DeFiHackLabs, GREY-HAWK-REACH, KeyKiril, MiloTruck, QiuhaoLi, Silvermist, SovaSlava, TrungOre, VAD37, Vagner, Yanchuan, ZdravkoHr, ast3ros, cartlex_, d3e4, deth, ggg_ttt_hhh, gizzy, kodyvim, nirlin, nobody2018, rvierdiiev, serial-coder, sl1, tallo, xeros
6.6715 USDC - $6.67
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketBase.sol#L172-L176 https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatSanctionsEscrow.sol#L38
In case if lender is sanctioned, then he can be blocked and his funds should go to the special escrow account. In order to create escrow WildcatSanctionsSentinel.createEscrow
is called. It expects 3 params: borrower, account, asset, where borrower
should be market borrower and account
is sanctioned lender.
However, inside _blockAccount
function these params were added in wrong order, you can see that accountAddress, borrower, address(this)
is passed, which means that WildcatSanctionsEscrow
will have borrower
where lender will be stored and account
variable where will be borrower stored.
Because of that canReleaseEscrow
function will work incorrectly as it will actually be checking that borrower
is sanctioned instead of lender, which means that it will likely always return false
. And in the end, borrower will be able to call releaseEscrow
and receive all funds on his balance.
And in case if lender will be unsanctioned, then he will never get funds back, of course.
Exactly same behaviour currently inside WildcatMarketWithdrawals
contract.
Borrower can take all funds of sanctioned lenders.
VsCode
Provide params in correct order.
Error
#0 - c4-pre-sort
2023-10-27T02:54:10Z
minhquanym marked the issue as duplicate of #515
#1 - c4-judge
2023-11-07T12:10:52Z
MarioPoneder marked the issue as satisfactory
π Selected for report: osmanozdemir1
Also found by: 0xCiphky, 0xStalin, HChang26, Infect3d, Jiamin, Juntao, QiuhaoLi, circlelooper, crunch, rvierdiiev
91.2409 USDC - $91.24
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarketBase.sol#L163-L187
In case if lender is sanctioned, then it's blocked and his market tokens are sent to the escrow. Pls, note that his market token still continue earning interests.
As result, lender can't interact with market anymore until he is not sanctioned anymore. Once it will be done, he will be able to get funds out of escrow.
It's possible that user will be locked forever. In this case his tokens will continue earning interests from borrower. As lender is sanctioned it's realy possible that borrower would like to not have any deal with such lender. So he will unregister such lender, but as i said lender's tokens will still accruing interests. This is because currently borrower doesn't have any ability to return back funds of sanctioned lender, so he don't use his funds anymore.
I believe that borrower should have functionality that allows him to repay all market tokens of sanctioned lender. In this case all assets will also go to the escrow. And borrower will not pay any interests for lender anymore and don't use his funds.
Borrower continue paying interests to the lender and uses his funds.
VsCode
Escrow can have function that only borrower can trigger. This function will then ask market to burn escrow's market token and send them to the escrow. This will remove tokens from circulation then.
Error
#0 - c4-pre-sort
2023-10-27T14:46:22Z
minhquanym marked the issue as primary issue
#1 - c4-pre-sort
2023-10-27T17:14:25Z
minhquanym marked the issue as sufficient quality report
#2 - laurenceday
2023-10-31T22:11:36Z
This is expected behaviour, not a bug.
I believe that borrower should have functionality that allows him to repay all market tokens of sanctioned lender.
They already do. If a borrower wants to terminate the growth of a sanctioned account's market tokens while they're in an escrow contract, they're free to close the market and recreate another.
We're not freezing interest for a subset of tokens because of the way that the interest math works over the supply, and not willing to permit the borrower to start burning tokens on behalf of lenders, regardless of their status.
Interesting idea, but not a finding that breaks any invariant or leaks value in an unexpected way.
#3 - c4-sponsor
2023-10-31T22:11:42Z
laurenceday (sponsor) disputed
#4 - MarioPoneder
2023-11-07T18:13:50Z
I respectfully disagree with the sponsor:
According to the protocol's WhitePaper (page 16), blocked accounts should not earn interest. "The effect of this would be that an auxiliary escrow contract is deployed and the debt obligation of the borrower towards the lender is transferred to this new contract. Interest does not accrue within this contract from the time the debt is transferred, and the borrower is expected to immediately return the balance of funds due up to that time to the escrow contract."
#5 - c4-judge
2023-11-07T18:14:01Z
MarioPoneder marked the issue as satisfactory
#6 - c4-judge
2023-11-07T18:20:56Z
MarioPoneder marked issue #550 as primary and marked this issue as a duplicate of 550
π Selected for report: MiloTruck
Also found by: CaeraDenoir, T1MOH, ast3ros, elprofesor, joaovwfreire, rvierdiiev, t0x1c, trachev
137.6749 USDC - $137.67
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/WildcatMarketController.sol#L481
In case if borrower wants to change annual interests rate fro the market, then he can call WildcatMarketController.setAnnualInterestBips
. In case if he is increasing rate then this is considered as good operation for lenders, so then nothing more is done and rate is updated.
But if he decreases rate, then protocol decided that it's time for user's to think about that situation and everyone who wants should be able to quit market. For such reason, market reserve ratio is set to 90% for 2 weeks. So lenders have 2 weeks to withdraw and then anyone can call resetReserveRatio
function which will restore previous reserve ratio.
This is good thought, however it's possible that some markets will have bigger reserve ratio than 90%, for example 95% and really small interest rates. In this case, instead of increasing reserve ratio, protocol will decrease it, which will lead to the ability for borrower to borrow more funds without penalty.
Reserve ratio is decreased instead of increasing when annual interest rate is decreased.
VsCode
Maybe reserve ratio for next 2 weeks should not be hardcoded, but should depend on previous one somehow.
Error
#0 - c4-pre-sort
2023-10-27T16:58:29Z
minhquanym marked the issue as primary issue
#1 - c4-pre-sort
2023-10-27T17:14:05Z
minhquanym marked the issue as sufficient quality report
#2 - laurenceday
2023-10-31T21:56:36Z
This one is complicated - we presented 90% as the reserve ratio utilised by the protocol as we presented it for audit in order to illustrate it was a high amount effectively constituting a ragequit and to avoid bikeshedding over the definition, but it seems all it's done is generate a bunch of issues like this.
In practice, we'll be using an equation along the lines of tempReserve = maximum(|newRate - oldRate|/oldRate, currentRate)
which retains, for example, an existing 25% ratio in the event of a 10% decrease in APR.
It's very unlikely that the borrower would want to set a market with a reserve ratio over 90% in any event (there are some instances where 100% is 'reasonable', admittedly) - however no deployed part of the protocol actually defines the maximum bound since these are parameters determined by the market controller factories when deployed.
This one is hard, because you can make this argument for literally any value of the reserve ratio short of 100%, which isn't feasible because it would effectively block the borrower for being able to do anything with the funds for two weeks, and they'd probably just be better off closing the market and starting another one in that case.
I'm going to tentatively dispute the validity of this but only because I want to discuss this one further with the judges.
#3 - c4-sponsor
2023-10-31T21:56:48Z
laurenceday (sponsor) disputed
#4 - MarioPoneder
2023-11-07T18:29:48Z
Although this is an unlikely issue and per design choice, its impacts are real & valid.
See also hardcoded reset of reserve rate to 90%.
Therefore, I intend to maintain Medium severity.
#5 - c4-judge
2023-11-07T18:30:38Z
MarioPoneder marked the issue as satisfactory
#6 - c4-judge
2023-11-07T18:36:43Z
MarioPoneder marked issue #497 as primary and marked this issue as a duplicate of 497
π 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
https://github.com/code-423n4/2023-10-wildcat/blob/main/src/market/WildcatMarket.sol#L26-L29
Every time when any action with funds is executed on market, then _getUpdatedState
function is called. The purpose of this function is to accrue interests and increase scaleFactor according to it. Usually borrower pays only annual interest rate + protocol fee rate, but when market is in delinquency, then user pays penalty.
So also after almost each action with funds, _writeState
is called, which tracks if market isDelinquent
. This is needed to know moments when penalty should be accrued or not.
Currently if user would like to repay funds, then he should just transfer them directly to the market. Such action will not call _getUpdatedState
and not call _writeState
, which means that in case if after the transfer market became not delinquent, this will not be noticed and borrower will continue accruing penalty, until _writeState
is called.
User can pay more fees.
VsCode
Create separate function that allows anyone to pay on behalf of borrower and then call _writeState
and _getUpdatedState
.
Error
#0 - c4-pre-sort
2023-10-27T17:10:23Z
minhquanym marked the issue as duplicate of #123
#1 - c4-pre-sort
2023-10-27T17:10:30Z
minhquanym marked the issue as not a duplicate
#2 - c4-pre-sort
2023-10-27T17:13:30Z
minhquanym marked the issue as sufficient quality report
#3 - c4-pre-sort
2023-10-28T11:40:15Z
minhquanym marked the issue as primary issue
#4 - c4-sponsor
2023-11-01T16:14:22Z
d1ll0n marked the issue as disagree with severity
#5 - c4-sponsor
2023-11-01T16:15:16Z
d1ll0n (sponsor) acknowledged
#6 - d1ll0n
2023-11-01T16:15:17Z
Adding a repay
function is a good QA suggestion (we will be doing that) but this isn't a bug so much as a missing feature - borrowers can still call updateState
themselves.
#7 - MarioPoneder
2023-11-08T16:35:57Z
This is the complementary issue to #323.
The borrower is heavily incentivized to call updateState()
after repayment.
This is rather an UX issue, therefore QA seems appropriate.
#8 - c4-judge
2023-11-08T16:37:06Z
MarioPoneder changed the severity to QA (Quality Assurance)
#9 - c4-judge
2023-11-09T15:09:49Z
MarioPoneder marked the issue as grade-b