Beta Finance Invitational - 0xStalin's results

Personalized lending and borrowing with zero liquidity fragmentation and maximal capital efficiency on the EVM.

General Information

Platform: Code4rena

Start Date: 01/11/2023

Pot Size: $24,150 USDC

Total HM: 5

Participants: 5

Period: 5 days

Judge: cccz

Total Solo HM: 4

Id: 303

League: ETH

Beta Finance

Findings Distribution

Researcher Performance

Rank: 2/5

Findings: 1

Award: $0.00

QA:
grade-a

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: 0xStalin

Also found by: ladboy233

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
M-02

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-11-betafinance/blob/main/Omni_Protocol/src/OmniPool.sol#L303

Vulnerability details

Impact

  • Users can't repay their debts if the OmniPool contract is paused which can cause users to fall into liquidation and lose their collateral

Proof of Concept

  • The OmniPool::repay() function has implemented the whenNotPaused modifier, which will prevent the function from being used if the contract is paused. The problem is that the usage of this function should not be prevented because if users are unnable to repay their debts, their accounts can fall into liquidation status while the OmniPool contract is paused, and once the contract is unpaused, and liquidations are enabled too, if the account felt into liquidation status, now the users and liquidators will be in a mev run to either repay the debt or liquidate the collateral.
    • This presents an unnecesary risk to users by preventing them to repay their debts.

OmniPool contract

//@audit-issue -> If contract is paused, this function can't be called if the contract is paused because of the whenNotPaused modifier!
function repay(uint96 _subId, address _market, uint256 _amount) external whenNotPaused {
    ...
}

Tools Used

Manual Audit

  • The mitigation is very straight forward, don't disable the borrower's repayments, and doesn't interrupt the repayments. Remove the whenNotPaused modifier

OmniPool contract

- function repay(uint96 _subId, address _market, uint256 _amount) external whenNotPaused {
//@audit-ok => Allow repayments even if the contract is paused!
+ function repay(uint96 _subId, address _market, uint256 _amount) external {
    ...
}

Assessed type

Timing

#0 - c4-judge

2023-11-07T07:42:01Z

thereksfour marked the issue as satisfactory

#1 - c4-judge

2023-11-07T07:42:07Z

thereksfour marked the issue as primary issue

#2 - allenjlee

2023-11-12T18:45:35Z

We will remove the whenNotPaused modifier for repay

#3 - c4-sponsor

2023-11-12T18:45:39Z

allenjlee (sponsor) confirmed

#4 - stalinMacias

2023-11-12T19:48:38Z

@thereksfour I just want to confirm if issues #5 & #34 have been marked as a 50% each of those?

From a discussion we had in #5 each of those reports is individually reporting only one problem, in #32 I'm reporting the two problems that are individually reported in #5 & #34

#5 - JeffCX

2023-11-12T19:55:30Z

please read report #5 again sir,it is fully duplicate of this report

#6 - stalinMacias

2023-11-12T20:00:29Z

@JeffCX Right, so, #32 & #5 are dupes, #34 main focus is about repaying interest but it implicitly suggests the problem of not being able to repay, so, that one can be a 50%, or is your call @JeffCX if you want to mark it invalid and let only #32 & #5 as full dupes.

#7 - JeffCX

2023-11-12T20:47:44Z

I think we can leave that up to judge, either way, we share the point of this finding equally. πŸ‘

#8 - c4-judge

2023-11-13T14:57:59Z

thereksfour marked the issue as selected for report

#9 - thereksfour

2023-11-13T15:08:03Z

I think the root cause of this issue is that users are not able to repay when paused and the interest accrued may lead to the user's account being unhealthy. So I would consider all three of them 100%.

Findings Information

🌟 Selected for report: T1MOH

Also found by: 0xStalin, bin2chen, dirk_y, ladboy233

Labels

bug
grade-a
QA (Quality Assurance)
sponsor acknowledged
Q-01

Awards

Data not available

External Links

[L-01] When initializing the variables of an OmniToken, ot verifying if the borrow cap for the tranch #0 is > 0 will cause the OmniToken not to be borrowable until the admin sets again the borrowCap for tranch# > 0

When initializing the parameters of an OmniToken, the argument _borrowCaps is not checked to validate if the tranch# 0 will be assigned with a borrowCap > 0. If the tranch #0 borrow cap is 0, it means that it can't access any of the liquidity from the higher tranches!

  • Taking as referencethe setTrancheBorrowCaps() function, the tranch# 0 must have a borrowCap > 0
    function initialize(address _omniPool, address _underlying, address _irm, uint256[] memory _borrowCaps) external initializer {
        ...
        //@audit-issue => If the tranch #0 borrow cap is 0, it means that it can't access any of the liquidity from the higher tranches!
        trancheBorrowCaps = _borrowCaps;
        ...
    }

Fix:

  • Add a check to validate that tranch# 0 borrowCap is > 0
    function initialize(address _omniPool, address _underlying, address _irm, uint256[] memory _borrowCaps) external initializer {
        ...
+       require(_borrowCaps[0] > 0, "Invalid borrow caps, must always allow 0 to borrow.");
        trancheBorrowCaps = _borrowCaps;
        ...
    }

[L-02] Not fetching all the user subaccounts when using the balanceOf()

    function balanceOf(address _owner) external view returns (uint256) {
        uint256 totalDeposit = 0;
        for (uint96 i = 0; i < MAX_VIEW_ACCOUNTS; ++i) {
          //@audit-issue => The totalDeposit of the owner will be only the sum of the first 25 subaccounts!
            totalDeposit += balanceOfAccount[_owner.toAccount(i)];
        }
        return totalDeposit;
    }

Fix:

  • Allow the user to input the index from where it would like to start iterating, example, if user inputs index as 10, iterate from the subbaccs 10 to 35.
    • In this way, the account owner can get the balance of all his subaccounts, it might require a couple of calls, but it can get it done.
+   function balanceOf(address _owner, uint8 index) external view returns (uint256) {
        uint256 totalDeposit = 0;
+       for (uint96 i = index; i < MAX_VIEW_ACCOUNTS; ++i) {
            totalDeposit += balanceOfAccount[_owner.toAccount(i)];
        }
        return totalDeposit;
    }

[L-03] No need to use the outflowtokens, the on-fee transfer is deducted from the amount being transferred and taken away from the receiver, not from the sender

  • The outflowTokens does an extra of unnecesary steps, the intention is to account for any transfer fees that could've been charged, but, the transfer fees are applied to the receiver.
    • Example, User A sends 10 Tokens and the fee is 10%:
      • User A will get deducted the 10 Tokens that is sending from his balance
      • The contract will charge 1 token as fee (the 10%)
      • User B will only receive 9 tokens (because of the transfer fees)
  • So, the sender will only get deducted the amount that is sending, nothing more, nothing less.

Fix:

  • Remove the lines of code to take a balance snapshot and compute the difference after the transfer is completed
function _outflowTokens(address _to, uint256 _amount) internal returns (uint256) {
-   uint256 balanceBefore = IERC20(underlying).balanceOf(address(this));
    IERC20(underlying).safeTransfer(_to, _amount);
+   return _amount;    
-   uint256 balanceAfter = IERC20(underlying).balanceOf(address(this));
-   return balanceBefore - balanceAfter;
}

#0 - c4-judge

2023-11-07T17:19:06Z

thereksfour marked the issue as grade-b

#1 - allenjlee

2023-11-10T08:01:28Z

Agree, think the second issue is not applicable, as the purpose of including balanceOf(address) is so that wallets/explorers pick up the contract balance for users. If you need to pass an index in, then the interface becomes incorrect. Could add an additional function that is balanceOf(address, index) but I think it's unnecessary, as user's should be directly querying accounts to know where exactly the balances are

For the outflowTokens I see you're point for fee-on-transfer if it's from sender. However, there's always potential edge cases, as there is no "standard" way to check implementation, perhaps it does a fee on receiver after balance is credited to re-credit to sender. There's many different ways for the balance to potentially be wrong from what is reportedly "sent", so we prefer to err on the side of safety here.

#2 - c4-sponsor

2023-11-10T08:02:29Z

allenjlee (sponsor) acknowledged

#3 - c4-judge

2023-11-15T04:50:57Z

thereksfour 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