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: 39/131
Findings: 4
Award: $167.63
๐ Selected for report: 0
๐ Solo Findings: 0
๐ 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/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketConfig.sol#L74 https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketToken.sol#L64-L82
Sanctioned accounts can exploit a loophole to evade asset lock, essentially bypassing the system's sanctions
When an account is flagged for sanctions by Chainalysis, it isn't immediately blocked. The account is actually blocked only when someone calls the nukeFromOrbit
function.
function nukeFromOrbit(address accountAddress) external nonReentrant {
In the WildcatMarketToken
contract, any accountโeven if it's been flagged for sanctionsโcan freely transfer tokens until it is officially blocked. Here's the _transfer
function that allows this:
function _transfer(address from, address to, uint256 amount) internal virtual { MarketState memory state = _getUpdatedState(); uint104 scaledAmount = state.scaleAmount(amount).toUint104(); if (scaledAmount == 0) { revert NullTransferAmount(); } Account memory fromAccount = _getAccount(from); fromAccount.scaledBalance -= scaledAmount; _accounts[from] = fromAccount; Account memory toAccount = _getAccount(to); toAccount.scaledBalance += scaledAmount; _accounts[to] = toAccount; _writeState(state); emit Transfer(from, to, amount); }
This creates a window of opportunity for a sanctioned account. If the account realizes it's been flagged by Chainalysis, it could transfer its tokens to a different account before nukeFromOrbit
is called. Subsequently, it can withdraw assets using a new account or trade the market tokens with other whitelist lenders.
Manual
When market token is transferred, check from
and to
addresses to see if they are sanctioned or not.
Context
#0 - c4-pre-sort
2023-10-27T03:17:34Z
minhquanym marked the issue as duplicate of #54
#1 - c4-judge
2023-11-07T14:36:22Z
MarioPoneder changed the severity to 3 (High Risk)
#2 - c4-judge
2023-11-07T14:37:57Z
MarioPoneder marked the issue as satisfactory
๐ 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/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketBase.sol#L172-L176 https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketWithdrawals.sol#L166-L170 https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatSanctionsSentinel.sol#L95-L99
Due to the incorrect order of arguments when setting up the escrow, funds meant to be locked away are erroneously transferred to the borrower.
When an account is blocked, the market calls WildcatSanctionsSentinel
to create an escrow and transfer assets, specifying accountAddress
, borrower
, and asset
address as arguments:
address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( accountAddress, borrower, address(this) );
address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( accountAddress, borrower, address(asset) );
However, the createEscrow
function expects the arguments in the order of borrower
, account
, and asset address
, which is misaligned with the call:
function createEscrow( address borrower, address account, address asset ) public override returns (address escrowContract) {
As a result, the temporary escrow parameters (tmpEscrowParams
) are set up incorrectly:
tmpEscrowParams = TmpEscrowParams(borrower, account, asset);
Subsequently, the escrow release logic releases the funds to the wrong account:
function releaseEscrow() public override { if (!canReleaseEscrow()) revert CanNotReleaseEscrow(); uint256 amount = balance(); IERC20(asset).transfer(account, amount); emit EscrowReleased(account, asset, amount); }
Manual
Fix the argument order when calling createEscrow
to align with its expected parameters. Specifically, swap the positions of accountAddress
and borrower
:
address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( - accountAddress, - borrower, + borrower, + accountAddress, address(this) );
Other
#0 - c4-pre-sort
2023-10-27T02:23:03Z
minhquanym marked the issue as primary issue
#1 - c4-pre-sort
2023-10-27T02:59:44Z
minhquanym marked the issue as sufficient quality report
#2 - laurenceday
2023-10-31T23:42:06Z
Valid finding, well done everyone!
#3 - c4-sponsor
2023-10-31T23:42:13Z
laurenceday (sponsor) confirmed
#4 - c4-judge
2023-11-07T11:43:31Z
MarioPoneder marked issue #68 as primary and marked this issue as a duplicate of 68
#5 - c4-judge
2023-11-07T11:46:38Z
MarioPoneder changed the severity to 3 (High Risk)
#6 - c4-judge
2023-11-07T11:47:03Z
MarioPoneder marked the issue as partial-50
#7 - c4-judge
2023-11-07T11:48:29Z
MarioPoneder marked the issue as satisfactory
๐ Selected for report: MiloTruck
Also found by: CaeraDenoir, T1MOH, ast3ros, elprofesor, joaovwfreire, rvierdiiev, t0x1c, trachev
137.6749 USDC - $137.67
By exploiting the setAnnualInterestBips
function, a borrower can effectively lower the reserve ratio, gaining access to additional funds without incurring any penalty fees.
The function setAnnualInterestBips
allows the borrower to manipulate the reserve ratio. When reducing the interest rate, the code sets the reserve ratio to 9000 (or 90%) for the next two weeks, with the assumption that the current reserve ratio bips is much less than 90%.
function setAnnualInterestBips( address market, uint16 annualInterestBips ) external virtual onlyBorrower onlyControlledMarket(market) { // If borrower is reducing the interest rate, increase the reserve // ratio for the next two weeks. if (annualInterestBips < WildcatMarket(market).annualInterestBips()) { TemporaryReserveRatio storage tmp = temporaryExcessReserveRatio[market]; if (tmp.expiry == 0) { tmp.reserveRatioBips = uint128(WildcatMarket(market).reserveRatioBips()); // Require 90% liquidity coverage for the next 2 weeks WildcatMarket(market).setReserveRatioBips(9000); } tmp.expiry = uint128(block.timestamp + 2 weeks); } WildcatMarket(market).setAnnualInterestBips(annualInterestBips); }
However, in an edge case when the market is risky and the current setup is for the reserve ratio is more than 90% so the lender can become comfortable with the risk. The reserve ratio is assumed to be remain the same since there is no function for borrower to update.
After all the lenders deposit assets to the market, the borrower call setAnnualInterestBips
and reduce the reserve ratio to 90% enjoy additional funds without paying any penalty fee.
The borrower can continuously keep the ratio at 90% by reduce interest rate and increase interest rate immediately. So the expiry is reset for additional two weeks and interest the same so the lenders not withdraw.
Manual
Implement a check that compares the new reserve ratio with the current one when changing the interest rate. Ensure that the new reserve ratio is not lower than the current one, thereby preventing borrowers from reducing it to gain undue advantages.
Context
#0 - c4-pre-sort
2023-10-27T17:05:21Z
minhquanym marked the issue as duplicate of #75
#1 - c4-judge
2023-11-07T18:35:37Z
MarioPoneder marked the issue as satisfactory
๐ 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/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatMarketController.sol#L169-L176 https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatMarketController.sol#L182-L190
Lenders have the ability to front-run the process of being deauthorized, thereby bypassing the borrower's intent to revoke their lending privileges.
When a borrower seeks to deauthorize a lender, the process involves two separate functions: deauthorizeLenders
to remove the lender from the _authorizedLenders
list, and updateLenderAuthorization
to update the lender's status in each market.
function deauthorizeLenders(address[] memory lenders) external onlyBorrower { for (uint256 i = 0; i < lenders.length; i++) { address lender = lenders[i]; if (_authorizedLenders.remove(lender)) { emit LenderDeauthorized(lender); } } }
function updateLenderAuthorization(address lender, address[] memory markets) external { for (uint256 i; i < markets.length; i++) { address market = markets[i]; if (!_controlledMarkets.contains(market)) { revert NotControlledMarket(); } WildcatMarket(market).updateAccountAuthorization(lender, _authorizedLenders.contains(lender)); } }
The problem arises because these are separate transactions. A lender, who is aware of being deauthorized, can front-run the updateLenderAuthorization
transaction (2nd transaction) and deposit assets into the market, effectively circumventing the deauthorization.
Additionally, if a borrower fails to update all relevant markets during the deauthorization, an unauthorized lender could continue operating in those markets undetected.
Manual
To prevent this vulnerability, combine the deauthorization and market status update into a single atomic function. This ensures that once a lender is deauthorized, the change is reflected across all markets immediately, eliminating the potential for front-running.
Other
#0 - c4-pre-sort
2023-10-27T09:25:27Z
minhquanym marked the issue as low quality report
#1 - c4-pre-sort
2023-10-27T09:28:47Z
minhquanym marked the issue as duplicate of #46
#2 - c4-judge
2023-11-07T18:41:11Z
MarioPoneder changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-09T15:06:57Z
MarioPoneder marked the issue as grade-b