Inverse Finance contest - trustindistrust'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: 12/127

Findings: 3

Award: $785.51

QA:
grade-a

🌟 Selected for report: 1

šŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: jayphbee

Also found by: catchup, cccz, corerouter, trustindistrust

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
duplicate-275

Awards

342.9734 USDC - $342.97

External Links

Lines of code

https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Market.sol#L607-L608

Vulnerability details

Description

The Market.liquidate() function handles liquidations of unhealthy user debt. Anyone can call the function and repay a quantity of the undercollateralized debt position and receive a fee for doing so, paid out of the user's collateral.

When this occurs an additional fee is transferred from the user's collateral to the protocol.

The calculation to do so handles the case when the user's escrow can fully cover the fee. However, it fails to handle the case where the escrow does not contain the required tokens. As such, the fee will not be assessed against the user.

While this may be an intentional design choice, it isn't noted or explained in the documentation.

Severity Rationalization

This would seem to be a value leak at the protocol level. There is no other method by which the protocol could forcefully recover the fee.

If this is an intentional decision to avoid spending gas on transfering (what are anticipated to be) dust amounts of ERC20, it must be pointed out that the liquidator pays this gas cost, not the protocol. Therefore there's no particular reason to not allow the protocol to fully benefit from the liquidation.

Mitigation

Handle the else case in order to capture the fee. Something similar to the following would be sufficient:

if(escrow.balance() >= liquidationFee) { escrow.pay(gov, liquidationFee); } else { escrow.pay(gov, escrow.balance()); }
Tooling

Manual review

#0 - c4-judge

2022-11-05T19:50:27Z

0xean marked the issue as primary issue

#1 - 0xean

2022-11-05T19:51:08Z

leaving for sponsor review, but this is ultimately leads to dust amounts so probably will downgrade to QA

#3 - c4-sponsor

2022-11-09T04:23:49Z

08xmt marked the issue as sponsor confirmed

#4 - c4-judge

2022-11-28T18:26:00Z

0xean marked the issue as duplicate of #275

#5 - Simon-Busch

2022-12-05T15:34:56Z

Issue marked as satisfactory as requested by 0xean

Findings Information

Awards

43.7242 USDC - $43.72

Labels

bug
2 (Med Risk)
primary issue
sponsor disputed
selected for report
M-08

External Links

Lines of code

https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Market.sol#L359 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Market.sol#L376

Vulnerability details

Description

The FiRM Marketplace contract contains multiple governance functions for setting important values for a given debt market. Many of these are numeric values that affect ratios/levels for debt positions, fees, incentives, etc.

In particular, Market.setCollateralFactorBps() sets the ratio for how much collateral is required for loans vs the debt taken on by the user. The lower the value, the less debt a user can take on. See Market.getCreditLimitInternal() for that implementation.

The function Market.getWithdrawalLimitInternal() calculates how much collateral a user can withdraw from the protocol, factoring in their current level of debt. It contains the following check:

if(collateralFactorBps == 0) return 0;

This would cause the user to not be able to withdraw any tokens, so long as they had any non-0 amount of debt and the collateralFactorBps was 0.

Severity Rationalization

It is the warden's estimation that all semantics for locking functionality of the protocol should be explicit rather than implicit. While it is very unlikely that governance would intentionally set this value to 0, if it were to do so it would disproportionately affect users whose debt values were low compared to their deposited collateral.

It is also obvious that the same function that set the value to 0 could be used to revert the change. However, this would take time. Inverse Finance has mandatory minimums for the time required to process governance items in its workflow (https://docs.inverse.finance/inverse-finance/governance/creating-a-proposal)

The community has a social agreement to post all proposals on the forum and as a draft in GovMills at least 24 hours before the proposal is put up for an on-chain vote, and also to host a community call focusing on the proposal before the voting period.

Once a proposal has passed, it must be queued on-chain. This action can be triggered by anyone who is willing to pay the gas fee (usually done by a DAO member). The proposal then enters a holding period of 40 hours to allow users time to prepare for the consequences of the execution of the proposal.

As such, were the situation to occur it would cause at least 64 hours of lock.

Since the contract itself only overtly contains locking for new borrowing, this implicit lock on withdraws seems like an unnecessary risk.

Mitigation

Consider a minimum for this value, to go along with the maximum value check already present in the setter function. While this will still reduce the quantity of collateral that can be withdrawn by users, it would allow for some withdraws to occur.

An explicit withdrawal lock could be implemented, making the semantic clear. This function could have modified access controls to enable faster reactions vs governance alone.

Alternatively, if there was an intention for this value to accept 0, consider an 'escape hatch' function that could be enacted by users when a 'defaulted' state is set on the Market.

Tooling

Manual review

#0 - c4-judge

2022-11-05T21:18:51Z

0xean marked the issue as duplicate

#1 - c4-judge

2022-12-01T16:02:18Z

0xean marked the issue as selected for report

#2 - c4-judge

2022-12-07T08:21:26Z

Simon-Busch marked the issue as not a duplicate

#3 - c4-judge

2022-12-07T08:21:35Z

Simon-Busch marked the issue as primary issue

#4 - c4-sponsor

2022-12-14T15:33:50Z

08xmt marked the issue as sponsor disputed

#5 - 08xmt

2022-12-14T15:35:19Z

This is functioning as intended. Setting a low collateralFactor like this is essentially a way to force borrowers to repay their debt. It may be a necessary operation in an emergency.

Non-critical

Contract is missing events for certain actions

Locations 1 2 3 4 5 6 7 8 9 10 11 12 13 14

Description

Sensitive actions like access control changes and ownership on contracts should emit events for easy tracking and notification.

Suggested course of action

Implement new events for functions that should have them.

Low

Unorthodox token decimals normalization algorithm limits maximum token precision and introduces operational risk

Locations: 1 2

Description

The Oracle contract handles the recording of pricing data for the lending system. Other contracts utilize viewPrice() and getPrice() to fetch pricing data.

These functions include the following lines:

uint8 feedDecimals = feeds[token].feed.decimals(); uint8 tokenDecimals = feeds[token].tokenDecimals; uint8 decimals = 36 - feedDecimals - tokenDecimals; uint normalizedPrice = price * (10 ** decimals);

tokenDecimals comes from data supplied to the setter function setFeed() as an argument set by the operator. feedDecimals comes from data supplied by the Chainlink infrastructure.

As such there are a couple of potential issues.

  1. It's unclear from the code why two 'samples' of the token's precision are being taken during normalization. If the operator supplies an incorrect value via setFeed(), the normalized price could be off by one or more orders of magnitude. The gain compared to the operational risk is unclear.
  2. Due to the subtraction, assuming the two values are equal, the protocol is limiting itself to tokens that use at most 18 decimals of precision. While this is the overwhelming majority of tokens, this makes the contract somewhat inflexible against future developments.
Suggested course of action

Consider the business case for allowing the operator to supply a token precision that diverges from the Chainlink-supplied value. If the intention is to tacitly support other oracles that might not directly supply precision values, carefully weigh the risk of such oracles later adding such data to the operation of the protocol oracle.

Low

​

DBR token lacks address(0) guards on transfer functions while possessing public burn function

​

Description

​ The DolaBorrowingRights contract implements most ERC20 semantics, including transfer functions and mint/burn functions. ​ However, while burn() is public and emits an Event for burning, the transfer() and transferFrom() functions lack a check to block 'burning' of tokens by sending them to the 0 address. ​

Suggested course of action

​ Modify the transfer functions to that it's clear that burning tokens is handled via burn() and so that the correct intentioned events are emitted.

#0 - c4-judge

2022-11-07T21:24:21Z

0xean marked the issue as grade-b

#1 - c4-judge

2022-11-08T01:06:50Z

0xean marked the issue as grade-a

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