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: 12/127
Findings: 3
Award: $785.51
š Selected for report: 1
š Solo Findings: 0
š Selected for report: jayphbee
Also found by: catchup, cccz, corerouter, trustindistrust
342.9734 USDC - $342.97
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.
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.
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()); }
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
#2 - 08xmt
2022-11-09T04:23:37Z
#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
š Selected for report: trustindistrust
Also found by: 0xbepresent, Jujic, Lambda, RaoulSchaffranek, c7e7eff, catchup, codexploder, cryptonue, d3e4, eierina, jwood, pashov, peanuts, pedroais, simon135
43.7242 USDC - $43.72
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
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.
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.
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.
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.
š 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
398.8207 USDC - $398.82
Locations 1 2 3 4 5 6 7 8 9 10 11 12 13 14
Sensitive actions like access control changes and ownership on contracts should emit events for easy tracking and notification.
Implement new events for functions that should have them.
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.
setFeed()
, the normalized price could be off by one or more orders of magnitude. The gain compared to the operational risk is unclear.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.
ā
ā
ā
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.
ā
ā
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