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
Rank: 2/5
Findings: 1
Award: $0.00
π Selected for report: 1
π Solo Findings: 0
Data not available
https://github.com/code-423n4/2023-11-betafinance/blob/main/Omni_Protocol/src/OmniPool.sol#L303
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.
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 { ... }
Manual Audit
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 { ... }
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%.
Data not available
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!
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:
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; ... }
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:
+ 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; }
Fix:
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