The Wildcat Protocol - ast3ros'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: 39/131

Findings: 4

Award: $167.63

QA:
grade-b

๐ŸŒŸ Selected for report: 0

๐Ÿš€ Solo Findings: 0

Awards

13.1205 USDC - $13.12

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-266

External Links

Lines of code

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

Vulnerability details

Impact

Sanctioned accounts can exploit a loophole to evade asset lock, essentially bypassing the system's sanctions

Proof of Concept

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 {

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketConfig.sol#L74

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); }

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketToken.sol#L64-L82

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.

Tools Used

Manual

When market token is transferred, check from and to addresses to see if they are sanctioned or not.

Assessed type

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

Awards

6.6715 USDC - $6.67

Labels

bug
3 (High Risk)
satisfactory
sponsor confirmed
sufficient quality report
upgraded by judge
duplicate-68

External Links

Lines of code

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

Vulnerability details

Impact

Due to the incorrect order of arguments when setting up the escrow, funds meant to be locked away are erroneously transferred to the borrower.

Proof of Concept

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) );

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketBase.sol#L172-L176

address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( accountAddress, borrower, address(asset) );

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/market/WildcatMarketWithdrawals.sol#L166-L170

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) {

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatSanctionsSentinel.sol#L95-L99

As a result, the temporary escrow parameters (tmpEscrowParams) are set up incorrectly:

tmpEscrowParams = TmpEscrowParams(borrower, account, asset);

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatSanctionsSentinel.sol#L108

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); }

https://github.com/code-423n4/2023-10-wildcat/blob/dfbeba095582b30491708e0be1f87a4974fcf10d/src/WildcatSanctionsEscrow.sol#L33-L41

Tools Used

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)
        );

Assessed type

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

Findings Information

๐ŸŒŸ Selected for report: MiloTruck

Also found by: CaeraDenoir, T1MOH, ast3ros, elprofesor, joaovwfreire, rvierdiiev, t0x1c, trachev

Labels

bug
2 (Med Risk)
satisfactory
duplicate-497

Awards

137.6749 USDC - $137.67

External Links

Lines of code

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatMarketController.sol#L477-L482

Vulnerability details

Impact

By exploiting the setAnnualInterestBips function, a borrower can effectively lower the reserve ratio, gaining access to additional funds without incurring any penalty fees.

Proof of Concept

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); }

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatMarketController.sol#L477-L482

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.

Tools Used

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.

Assessed type

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

Awards

10.1663 USDC - $10.17

Labels

bug
downgraded by judge
grade-b
low quality report
QA (Quality Assurance)
duplicate-46
Q-21

External Links

Lines of code

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

Vulnerability details

Impact

Lenders have the ability to front-run the process of being deauthorized, thereby bypassing the borrower's intent to revoke their lending privileges.

Proof of Concept

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); } } }

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatMarketController.sol#L169-L176

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)); } }

https://github.com/code-423n4/2023-10-wildcat/blob/c0e8f1b785659fe58ac43fb61e859b4da0b643d5/src/WildcatMarketController.sol#L182-L190

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.

Tools Used

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.

Assessed type

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

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