Inverse Finance contest - immeas's results

Rethink the way you borrow.

General Information

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

Inverse Finance

Findings Distribution

Researcher Performance

Rank: 1/127

Findings: 5

Award: $4,809.57

QA:
grade-b

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: djxploit

Also found by: immeas

Labels

bug
2 (Med Risk)
satisfactory
duplicate-252

Awards

1176.1776 USDC - $1,176.18

External Links

Lines of code

https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L533

Vulnerability details

Impact

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.

Proof of Concept

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

Tools Used

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

Findings Information

🌟 Selected for report: immeas

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
selected for report
M-13

Awards

3397.8465 USDC - $3,397.85

External Links

Lines of code

https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L562

Vulnerability details

Impact

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.

Proof of Concept

    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.

Tools Used

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

Findings Information

🌟 Selected for report: gs8nrv

Also found by: Holmgren, idkwhatimdoing, immeas, kaden, rvierdiiev, yamapyblack

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sponsor acknowledged
judge review requested
duplicate-469

Awards

198.4346 USDC - $198.43

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools 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:

  1. The user needs to have not been liquidated when the initial twoDayLow was logged, which is already unlikely. Liquidations are highly profitable operations, and it's unlikely that we'll see a situation where a liquidation is unprofitable but another action that updates the oracle price is.
  2. The dampened price of the twoDayLow must be high enough to be in liquidation territory, meaning that the twoDayLow would have to fall significantly short of the liquidation requirements.

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

Lines of code

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

Vulnerability details

Impact

The api doesn't provide the necessary data points to validate the feed. Also, Chainlink can stop providing this deprecated api.

Proof of Concept

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

Tools Used

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

Low

L-1 consider two step ownership transfer for 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:    }

L-2 consider using safeTransfer/From or a safeTransfer/From library

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

Non-crit

NC-1 use 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;

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

NC-3 open todos

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

NC-4 very long row

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

NC-5 misspelled comment

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

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