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: 1/127
Findings: 5
Award: $4,809.57
🌟 Selected for report: 1
🚀 Solo Findings: 1
1176.1776 USDC - $1,176.18
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L533
An attacker can prevent someone from repaying his whole debt. Either just to grief or if the timing is right and the borrower is repaying just before the DBR goes into deficit it could incur an unwanted force replenish for the borrower.
There's actually already a test for this, just modified it a bit to split into one frontrunning tx:
function testRepay_Fails_WhenAmountGtDebtFrontRun() public { gibWeth(user, wethTestAmount); gibDBR(user, wethTestAmount); gibDOLA(user, 500e18); vm.startPrank(user); deposit(wethTestAmount); uint borrowAmount = getMaxBorrowAmount(wethTestAmount); market.borrow(borrowAmount); market.repay(user, 1); // front run DoS vm.expectRevert("Insufficient debt"); market.repay(user, borrowAmount); }
vscode, old reports
Allow overpay of the debt and just let the diff count as profit.
#0 - c4-judge
2022-11-05T22:20:10Z
0xean marked the issue as duplicate
#1 - Simon-Busch
2022-12-05T15:53:10Z
Issue marked as satisfactory as requested by 0xean
🌟 Selected for report: immeas
3397.8465 USDC - $3,397.85
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L562
If a user wants to completely forceReplenish a borrower with deficit, the borrower or any other malicious party can front run this with a dust amount to prevent the replenish.
function testForceReplenishFrontRun() public { gibWeth(user, wethTestAmount); gibDBR(user, wethTestAmount / 14); uint initialReplenisherDola = DOLA.balanceOf(replenisher); vm.startPrank(user); deposit(wethTestAmount); uint borrowAmount = getMaxBorrowAmount(wethTestAmount); market.borrow(borrowAmount); uint initialUserDebt = market.debts(user); uint initialMarketDola = DOLA.balanceOf(address(market)); vm.stopPrank(); vm.warp(block.timestamp + 5 days); uint deficitBefore = dbr.deficitOf(user); vm.startPrank(replenisher); market.forceReplenish(user,1); // front run DoS vm.expectRevert("Amount > deficit"); market.forceReplenish(user, deficitBefore); // fails due to amount being larger than deficit assertEq(DOLA.balanceOf(replenisher), initialReplenisherDola, "DOLA balance of replenisher changed"); assertEq(DOLA.balanceOf(address(market)), initialMarketDola, "DOLA balance of market changed"); assertEq(DOLA.balanceOf(replenisher) - initialReplenisherDola, initialMarketDola - DOLA.balanceOf(address(market)), "DOLA balance of market did not decrease by amount paid to replenisher"); assertEq(dbr.deficitOf(user), deficitBefore-1, "Deficit of borrower was not fully replenished"); // debt only increased by dust assertEq(market.debts(user) - initialUserDebt, 1 * replenishmentPriceBps / 10000, "Debt of borrower did not increase by replenishment price"); }
This requires that the two txs end up in the same block. If they end up in different blocks the front run transaction will need to account for the increase in deficit between blocks.
vscode, forge
Use min(deficit,amount)
as amount to replenish
#0 - 0xean
2022-11-05T22:23:28Z
very similar to #439 and unclear as the benefit the attacker is gaining here. They would be better off just front running the entire transaction and getting additional reward. Will leave open for sponsor review, but most likely QA or invalid.
#1 - c4-sponsor
2022-11-10T05:03:53Z
08xmt marked the issue as sponsor confirmed
#2 - 08xmt
2022-11-10T05:06:42Z
Fixed in https://github.com/InverseFinance/FrontierV2/pull/16 Possible to imagine a situation where an attacker has an underwater loan and keeps front running his own forced replenishments with single digit DBR forced replenishments.
#3 - c4-judge
2022-11-28T19:35:24Z
0xean marked the issue as satisfactory
#4 - c4-judge
2022-12-01T15:54:06Z
0xean marked the issue as selected for report
🌟 Selected for report: gs8nrv
Also found by: Holmgren, idkwhatimdoing, immeas, kaden, rvierdiiev, yamapyblack
198.4346 USDC - $198.43
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L127 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L135
A borrower could get an undue liquidation losing parts or almost all of their collateral, depending on how much an attacker manages to manipulate the price. If this is a governance token this could be beneficial, other than just getting tokens for a low price, since that could increase their influence over the organization in question.
Since the oracle takes the lowest seen value of two days, an attacker only need to manipulate an artificially low price once to liquidate one or multiple borrowers.
function testBorrow_oraclePriceManipulated() public { gibWeth(user, wethTestAmount); gibDBR(user, wethTestAmount); vm.startPrank(user); deposit(wethTestAmount); uint borrowAmount = getMaxBorrowAmount(wethTestAmount); market.borrow(borrowAmount*3/4); // only borrow 75% of credit amount leaving a healthy 25% cushion in the debt assertEq(market.getLiquidatableDebt(user),0); ethFeed.changeAnswer(ethFeed.latestAnswer()/2); // price mainpulated oracle.getPrice(address(market.collateral()),market.collateralFactorBps()); // record price ethFeed.changeAnswer(1600e18); // price returns assertGt(market.getLiquidatableDebt(user), 0); }
Perhaps not applicable when ETH
is used as collateral but for governance tokens. Since their trading volume is lower and the price could more easily be manipulated. A large holder of that governance token could drop the price and record it in the oracle. Force liquidate tokens used as collateral for loans, buy their initial tokens back, gaining more influence where the governance tokens are used.
vs code, forge
Do not take the low of two days but the high of the low/mid of two or three days or some other sort of low time weighted average.
#0 - c4-judge
2022-11-05T18:45:22Z
0xean marked the issue as change severity
#1 - c4-judge
2022-11-05T18:50:27Z
0xean marked the issue as primary issue
#2 - 08xmt
2022-11-09T04:36:36Z
Impossible to have an oracle that is completely insusceptible to manipulation.
The two day low issue is a duplicate of https://github.com/code-423n4/2022-10-inverse-findings/issues/150
#3 - c4-sponsor
2022-11-09T04:36:47Z
08xmt requested judge review
#4 - 08xmt
2022-11-25T11:23:44Z
For a liquidation to happen where a user is liquidated for more than they should have, some assumptions need to be made:
While we acknowledge the possibility of this, we find it extremely unlikely.
#5 - c4-sponsor
2022-11-25T11:23:56Z
08xmt marked the issue as sponsor acknowledged
#6 - c4-judge
2022-11-28T22:30:55Z
0xean marked the issue as satisfactory
#7 - 0xean
2022-11-28T23:47:56Z
I am going to dupe this with #220
While it isn't an exact dupe, the outcome is very much the same and the a solution is also something that might look identical (IE smoothing prices with some sort of reasonable filter)
#8 - c4-judge
2022-11-28T23:48:14Z
0xean marked the issue as duplicate of #220
#9 - c4-judge
2022-12-07T08:20:22Z
Simon-Busch marked the issue as duplicate of #469
🌟 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/main/src/Oracle.sol#L82 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L119
The api doesn't provide the necessary data points to validate the feed. Also, Chainlink can stop providing this deprecated api.
https://docs.chain.link/docs/data-feeds/price-feeds/api-reference/#latestanswer
the same issue was considered medium here: https://code4rena.com/reports/2022-01-yield/#m-01-oracle-data-feed-is-insufficiently-validated https://code4rena.com/reports/2021-09-wildcredit/#m-01-use-of-deprecated-chainlink-api https://code4rena.com/reports/2021-07-wildcredit/#m-01-chainlink---use-latestrounddata-instead-of-latestanswer-to-run-more-validations
vscode, previous reports and chainlink docs
use latest: https://docs.chain.link/docs/data-feeds/price-feeds/api-reference/#latestrounddata
then validate that the oracle prices are not stale:
function viewPrice(address token, uint collateralFactorBps) external view returns (uint) { if(fixedPrices[token] > 0) return fixedPrices[token]; if(feeds[token].feed != IChainlinkFeed(address(0))) { // get price from feed (uint80 roundID, int256 price, , uint256 timestamp, uint80 answeredInRound) = feeds[token].feed.latestRoundData(); require(price > 0, "Invalid feed price"); require(answeredInRound >= roundID, "Stale price"); require(timestamp > 0, "Round not complete"); // normalize price ... } revert("Price not found"); }
#0 - neumoxx
2022-10-31T08:40:05Z
Duplicate of #601
#1 - c4-judge
2022-11-05T17:48:59Z
0xean marked the issue as duplicate
#2 - Simon-Busch
2022-12-05T15:29:01Z
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
🌟 Selected for report: 0x1f8b
Also found by: 0xNazgul, 0xSmartContract, Aymen0909, B2, Bnke0x0, Deivitto, Diana, Dinesh11G, ElKu, JC, Josiah, Rahoz, RaymondFam, ReyAdmirado, Rolezn, Waze, __141345__, adriro, aphak5010, brgltd, c3phas, c7e7eff, carlitox477, cducrest, ch0bu, chrisdior4, cryptonue, cryptostellar5, cylzxje, d3e4, delfin454000, enckrish, evmwanderer, fatherOfBlocks, gogo, hansfriese, horsefacts, immeas, leosathya, lukris02, neumo, oyc_109, pedr02b2, rbserver, robee, rotcivegaf, rvierdiiev, sakshamguruji, shark, simon135, tnevler, trustindistrust, wagmi
36.7345 USDC - $36.73
Market
and Fed
As in DBR.sol
, to prevent accidental transfer to invalid addresses.
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L130
File: src/Market.sol 130: function setGov(address _gov) public onlyGov { gov = _gov; }
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Fed.sol#L48-L51
File: src/Fed.sol 48: function changeGov(address _gov) public { 49: require(msg.sender == gov, "ONLY GOV"); 50: gov = _gov; 51: }
safeTransfer/From
or a safeTransfer/From
libraryEven though there might be a strict vetting of what tokens can be used as collateral, there's little cost of using safeTransfer
. That will give you the option of using more types of collateral. Otherwise you would have to develop a new Market contract, which could impact trust.
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L280
File: src/Market.sol 280: collateral.transferFrom(msg.sender, address(escrow), amount);
https://github.com/code-423n4/2022-10-inverse/blob/main/src/escrows/SimpleERC20Escrow.sol#L38
File: src/escrows/SimpleERC20Escrow.sol 38: token.transfer(recipient, amount);
https://github.com/code-423n4/2022-10-inverse/blob/main/src/escrows/GovTokenEscrow.sol#L45
File: src/escrows/GovTokenEscrow.sol 45: token.transfer(recipient, amount);
1e18
instead of 1 ether
Please use scientific notation for better code readability. 1 ether
also implies that it has something to do with native ether which it doesn't.
There are 6 instances of this issue:
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L315 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L326 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L360 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L597 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L606
File: src/Market.sol: 315: return collateralBalance * oracle.viewPrice(address(collateral), collateralFactorBps) / 1 ether; 326: return collateralBalance * oracle.getPrice(address(collateral), collateralFactorBps) / 1 ether; 360: uint minimumCollateral = debt * 1 ether / oracle.getPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; 597: uint liquidatorReward = repaidDebt * 1 ether / price; 606: uint liquidationFee = repaidDebt * 1 ether / price * liquidationFeeBps / 10000;
https://github.com/code-423n4/2022-10-inverse/blob/main/src/escrows/INVEscrow.sol#L72
File: src/escrows/INVEscrow.sol: 72: uint invBalanceInXInv = xINV.balanceOf(address(this)) * xINV.exchangeRateStored() / 1 ether;
oracle.getPrice()
is better named updatePrice()
get
and view
are very easy to mix up. Also, get
implies no side effects while update/getAndUpdate would imply that there are changes as well.
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L112
File: src/Oracle.sol 78: function viewPrice(address token, uint collateralFactorBps) external view returns (uint) { 112: function getPrice(address token, uint collateralFactorBps) external returns (uint) {
https://github.com/code-423n4/2022-10-inverse/blob/main/src/escrows/INVEscrow.sol#L35
File: src/escrows/INVEscrow.sol 35: xINV = _xINV; // TODO: Test whether an immutable variable will persist across proxies
GitHub starts using a scroll bar when the length is over 164 characters, the lines below should be split when they reach that length.
There are 4 instances of this issue:
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L12
File: src/Oracle.sol 12:The Pessimistic Oracle introduces collateral factor into the pricing formula. It ensures that any given oracle price is dampened to prevent borrowers from borrowing more than the lowest recorded value of their collateral over the past 2 days.
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Fed.sol#L99
File: src/Fed.sol 99: @dev Markets can have more DOLA in them than they've been supplied, due to force replenishes. This call will revert if trying to contract more than have been supplied.
https://github.com/code-423n4/2022-10-inverse/blob/main/src/escrows/GovTokenEscrow.sol#L16
File: src/escrows/GovTokenEscrow.sol 16: This specific escrow is meant as an example of how an escrow can be implemented that allows depositors to delegate votes with their collateral, unlike pooled deposit protocols.
https://github.com/code-423n4/2022-10-inverse/blob/main/src/escrows/INVEscrow.sol#L25
File: src/escrows/INVEscrow.sol 25: This escrow allows user to deposit INV collateral directly into the xINV contract, earning APY and allowing them to delegate votes on behalf of the xINV collateral
https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L589
File: src/Market.sol 589: @param repaidDebt Th amount of user user debt to liquidate.
#0 - c4-judge
2022-11-07T21:46:03Z
0xean marked the issue as grade-b