Platform: Code4rena
Start Date: 25/10/2022
Pot Size: $50,000 USDC
Total HM: 18
Participants: 127
Period: 5 days
Judge: 0xean
Total Solo HM: 9
Id: 175
League: ETH
Rank: 4/127
Findings: 3
Award: $3,554.50
π Selected for report: 1
π Solo Findings: 1
π Selected for report: Jeiwan
3397.8465 USDC - $3,397.85
Oracle's viewPrice
function doesn't report a dampened price until getPrice
is called and today's price is updated. This will impact the public read-only functions that call it:
getCollateralValue
);getCreditLimit
);These functions are used to get on-chain state and prepare values for write calls (e.g. calculate withdrawal amount before withdrawing or calculate a user's debt that can be liquidated before liquidating it). Thus, wrong values returned by these functions can cause withdrawal of a wrong amount or liquidation of a wrong debt or cause reverts.
// src/test/Oracle.t.sol function test_viewPriceNoDampenedPrice_AUDIT() public { uint collateralFactor = market.collateralFactorBps(); uint day = block.timestamp / 1 days; uint feedPrice = ethFeed.latestAnswer(); //1600e18 price saved as daily low oracle.getPrice(address(WETH), collateralFactor); assertEq(oracle.dailyLows(address(WETH), day), feedPrice); vm.warp(block.timestamp + 1 days); uint newPrice = 1200e18; ethFeed.changeAnswer(newPrice); //1200e18 price saved as daily low oracle.getPrice(address(WETH), collateralFactor); assertEq(oracle.dailyLows(address(WETH), ++day), newPrice); vm.warp(block.timestamp + 1 days); newPrice = 3000e18; ethFeed.changeAnswer(newPrice); //1200e18 should be twoDayLow, 3000e18 is current price. We should receive dampened price here. // Notice that viewPrice is called before getPrice. uint viewPrice = oracle.viewPrice(address(WETH), collateralFactor); uint price = oracle.getPrice(address(WETH), collateralFactor); assertEq(oracle.dailyLows(address(WETH), ++day), newPrice); assertEq(price, 1200e18 * 10_000 / collateralFactor); // View price wasn't dampened. assertEq(viewPrice, 3000e18); }
Manual review
Consider this change:
--- a/src/Oracle.sol +++ b/src/Oracle.sol @@ -89,6 +89,9 @@ contract Oracle { uint day = block.timestamp / 1 days; // get today's low uint todaysLow = dailyLows[token][day]; + if(todaysLow == 0 || normalizedPrice < todaysLow) { + todaysLow = normalizedPrice; + } // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor
#0 - 0xean
2022-11-05T21:47:32Z
well written report that explains the impact of this unlike the others. will leave open for review.
#1 - c4-sponsor
2022-11-15T13:38:13Z
08xmt marked the issue as sponsor confirmed
#2 - 08xmt
2022-11-15T13:39:49Z
#3 - c4-judge
2022-11-28T18:21:00Z
0xean marked the issue as satisfactory
#4 - c4-judge
2022-12-01T15:54:37Z
0xean marked the issue as selected for report
156.2673 USDC - $156.27
A borrower can quit their position and withdraw full collateral before their DBR deficit is replenished. This leaves the protocol with a bad debt. The borrower can borrow again using a different address and an escrow to avoid replenishing their DBR deficit.
// src/test/Market.t.sol function testQuitWithDBRDeficit_AUDIT() public { gibWeth(user, wethTestAmount); gibDBR(user, wethTestAmount); vm.startPrank(user); deposit(wethTestAmount); uint borrowAmount = getMaxBorrowAmount(wethTestAmount); market.borrow(borrowAmount); // Simulating DBR deficit accruing. dbr.transfer(address(0), dbr.balanceOf(user)); vm.warp(block.timestamp + 100); uint256 deficit = dbr.deficitOf(user); assertEq(deficit, 4312531709791983); // User's DBR deficit hasn't been replenished. // User repays full debt. market.repay(user, market.debts(user)); // User withdraws full collateral. uint256 withdrawAmount = market.getWithdrawalLimit(user); market.withdraw(withdrawAmount); assertEq(DOLA.balanceOf(user), 0); assertEq(WETH.balanceOf(user), wethTestAmount); // User has 0 collateral. assertEq(market.getCollateralValue(user), 0); // User has 0 debt. assertEq(market.debts(user), 0); // However, user still has the deficit of DBR. assertEq(dbr.deficitOf(user), deficit); vm.stopPrank(); // The deficit cannot be replenished because user has 0 collateral. vm.expectRevert("Exceeded collateral value"); market.forceReplenish(user, deficit); }
Manual review
Consider this change:
--- a/src/Market.sol +++ b/src/Market.sol @@ -458,6 +458,7 @@ contract Market { @param amount The amount being withdrawn. */ function withdrawInternal(address from, address to, uint amount) internal { + require(dbr.deficitOf(from) == 0, "Unreplenished DBR deficit"); uint limit = getWithdrawalLimitInternal(from); require(limit >= amount, "Insufficient withdrawal limit"); IEscrow escrow = getEscrow(from);
#0 - 0xean
2022-11-05T18:43:15Z
I believe the intention is that someone would call forceReplenish on a user, but certainly isn't guaranteed. Will let sponsor comment before final judging.
#1 - c4-judge
2022-11-05T18:44:02Z
0xean marked the issue as primary issue
#2 - c4-sponsor
2022-11-14T11:13:41Z
08xmt marked the issue as disagree with severity
#3 - 08xmt
2022-11-14T12:08:55Z
This is a Low Severity risk, as it's expected force replenishers will be using private transaction relayers, making them impossible to front run. There is a risk of small amounts of DBR not being replenished (Amounts where incentive is below transaction fee costs).
#4 - c4-sponsor
2022-11-14T12:09:13Z
08xmt marked the issue as sponsor acknowledged
#5 - c4-sponsor
2022-11-25T11:13:29Z
08xmt marked the issue as sponsor confirmed
#6 - 08xmt
2022-11-25T11:15:35Z
#7 - 0xean
2022-11-28T16:19:28Z
@08xmt - while the front running issue is part of it, there is no guarantee that someone calls forceReplenish as you state. I think this issue is highlighting that problem in particular and suggest a simple way to handle the problem.
I am unable to see your PR (since its private), but based on the fact that a fix was implemented, this seems more like a M severity issue. Will circle back but welcome comments before a final ruling.
#8 - c4-judge
2022-11-28T17:55:52Z
0xean changed the severity to 2 (Med Risk)
#9 - c4-judge
2022-11-28T19:35:25Z
0xean marked the issue as satisfactory
#10 - 08xmt
2022-11-30T11:24:37Z
@0xean Fix makes it impossible to withdraw collateral when you have a DBR deficit, which should make shenanigans like this impossible.
#11 - c4-judge
2022-12-07T08:16:07Z
Simon-Busch marked the issue as duplicate of #583
π Selected for report: rbserver
Also found by: 0x1f8b, 0xNazgul, 0xc0ffEE, 8olidity, Aymen0909, Chom, Franfran, Jeiwan, Jujic, Lambda, M4TZ1P, Olivierdem, Rolezn, Ruhum, TomJ, Wawrdog, __141345__, bin2chen, c7e7eff, carlitox477, catchup, cccz, codexploder, cuteboiz, d3e4, dipp, djxploit, eierina, elprofesor, hansfriese, horsefacts, idkwhatimdoing, imare, immeas, joestakey, ladboy233, leosathya, martin, minhtrng, pashov, peanuts, pedroais, rokinot, rvierdiiev, saneryee, sorrynotsorry, tonisives
0.385 USDC - $0.38
https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Oracle.sol#L6 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Oracle.sol#L82 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Oracle.sol#L116
The Oracle
contract calls latestAnswer
function of a Chainlink oracle, which was deprecated. In case Chainling stops supporting the oracles with deprecated functions, the Oracle
contract will has to be patched and redeployed.
interface IChainlinkFeed { function decimals() external view returns (uint8); function latestAnswer() external view returns (uint); // @audit deprecated function }
latestAnswer in the API reference documentation of Chainlink
Manual review
Consider updating the interface and using latestRoundData
instead of latestAnswer
.
#0 - neumoxx
2022-10-31T08:42:12Z
Duplicate of #601
#1 - c4-judge
2022-11-05T17:49:09Z
0xean marked the issue as duplicate
#2 - Simon-Busch
2022-12-05T15:28:18Z
Issue marked as satisfactory as requested by 0xean
#3 - c4-judge
2022-12-07T08:14:13Z
Simon-Busch marked the issue as duplicate of #584