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: 79/131
Findings: 1
Award: $16.66
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 3docSec
Also found by: 0xCiphky, 0xbepresent, 0xbrett8571, Eigenvectors, MiloTruck, Toshii, Tricko, TrungOre, ZdravkoHr, b0g0, caventa, cu5t0mpeo, deth, ggg_ttt_hhh, gizzy, joaovwfreire, josephdara, serial-coder, smiling_heretic, stackachu
16.6643 USDC - $16.66
https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarket.sol#L42-L77 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketConfig.sol#L149-L159
The borrower can frontrun lenders deposits and preemptively decrease the interest rate, potentially deceiving lenders into engaging in unfavourable loans they would typically avoid. Because the lack of on-chain check for minimum interest rate during deposits there is no way for lenders to safeguard against this form of manipulation. Even if they employ off-chain monitoring tools, this might go unnoticed, leading to prolonged exposure of their funds in the market with minimal to no interest earned.
Let's examine the typical borrower-lender process. A borrower creates a market, and lenders search for a suitable market meeting their criteria (interest rates, reserve ratios, etc). They decide to deposit their funds into this market by utilizing the deposit
or depositUpTo
methods. However, it's important to note that these methods only verify the amount
parameter and do not inspect any other market parameters. This is because it is assumed that the lender has already conducted this validation prior to calling the method. The issue arises because this verification is not done on-chain during the execution of the deposit
call. Consequently, this enables a malicious borrower to frontrun these deposit calls and alter the parameters before they are executed, ultimately leading to lenders entering unfavourable loan agreements.
Consider the following example scenario for a first deposit in a newly create market, which is simpler since we do not need to consider the reserveRatio
. The same attack is possible in a market with existing debt, but it necessitates the borrower managing the required reserveRatio
due to the reduction in annualInterestBips
. Nonetheless, it can still be executed. In this scenario, Bob plays the role of the malicious borrower, and Alice is the lender. A Proof of Concept (POC) for this scenario is provided at the end of this report.
annualInterestBips
set to 1000 (other parameters not relevant to this example will be omitted).depositUpTo(1e18)
function to deposit 1e18
tokens into the market.setAnnualInterestBips(0)
function to reduce interest rates to zero (as the market is new and has no debt, there is no penalty to reserve ratios).Should Bob's frontrunning attempt succeed, Alice gets into a loan with 0% APR. If the lender remains unaware of being deceived, the borrower can utilize an interest-free loan for an extended duration. Even if the lenders employs off-chain monitoring to track AnnualInterestBipsUpdated
events and respond to potential interest rate adjustments by the borrower, there is still a possibility of them being deceived, because in this attack the interest rate is modified prior to their deposit, meaning that if the lender only checks for rate changes after their deposit confirmation block, they may remain unaware of the borrower's manipulation.
Manual Review
Consider adding a new argument minAnnualInterestBips
to deposit
and depositUpTo
, so lenders can ensure that their deposit will only be executed if the current annualInterestBips
of the market meets or exceeds their specified requirement. Below is an example implementation for reference:
diff --git a/WildcatMarket.sol b/WildcatMarket.mod.sol index 1723465..1790954 100644 --- a/WildcatMarket.sol +++ b/WildcatMarket.mod.sol @@ -38,20 +38,25 @@ contract WildcatMarket is * * Reverts if the market is closed or if the scaled token amount * that would be minted for the deposit is zero. */ function depositUpTo( uint256 amount + uint256 minAnnualInterestBips ) public virtual nonReentrant returns (uint256 /* actualAmount */) { // Get current state MarketState memory state = _getUpdatedState(); if (state.isClosed) { revert DepositToClosedMarket(); } + if (state.annualInterestBips < minAnnualInterestBips) { + revert AnnualInterestBipsBelowMin(); + } + // Reduce amount if it would exceed totalSupply amount = MathUtils.min(amount, state.maximumDeposit()); // Scale the mint amount uint104 scaledAmount = state.scaleAmount(amount).toUint104(); if (scaledAmount == 0) revert NullMintAmount();
Run the following POC in /test folder of the contest repository.
// SPDX-License-Identifier: MIT pragma solidity >=0.8.20; import './BaseMarketTest.sol'; import 'src/interfaces/IMarketEventsAndErrors.sol'; import 'src/libraries/MathUtils.sol'; import 'src/libraries/SafeCastLib.sol'; import 'src/libraries/MarketState.sol'; import 'solady/utils/SafeTransferLib.sol'; contract WildcatMarketTest is BaseMarketTest { using stdStorage for StdStorage; using MathUtils for int256; using MathUtils for uint256; function test_poc() external { //Setup address alice = makeAddr("alice"); address bob = parameters.controller; uint256 amount = 1e18; _authorizeLender(alice); asset.mint(alice, amount); vm.prank(alice); asset.approve(address(market), amount); //Initial annualInterestBips set to 1000 assertEq(market.annualInterestBips(), 1000); //Bob frontruns alice's tx and sets annualInterestBips to 0 vm.prank(bob); market.setAnnualInterestBips(0); assertEq(market.annualInterestBips(), 0); //Alice deposits to the market with annualInterestBips = 0 vm.expectEmit(address(market)); emit Deposit(alice, 1e18, 1e18); vm.prank(alice); market.depositUpTo(amount); } }
Other
#0 - c4-pre-sort
2023-10-28T14:31:57Z
minhquanym marked the issue as duplicate of #443
#1 - c4-judge
2023-11-07T12:33:54Z
MarioPoneder marked the issue as satisfactory