Platform: Code4rena
Start Date: 24/07/2023
Pot Size: $100,000 USDC
Total HM: 18
Participants: 73
Period: 7 days
Judge: alcueca
Total Solo HM: 8
Id: 267
League: ETH
Rank: 7/73
Findings: 2
Award: $5,235.33
π Selected for report: 1
π Solo Findings: 1
π Selected for report: ravikiranweb3
5190.4455 USDC - $5,190.45
Comptroller's admin user can replace the rewards distributor contract with a new MultiRewardDistributor using _setRewardDistributor() function.
At that time, if the supplier and borrowers had accured rewards that are not distributed. The _setRewardDistributor() function replaces the old reward distributor with the new one without disbursing the borrower and supplier rewards accured.
The new logic of computation and distribution could be different in the newer version and hence before replacement, the accured rewards should be distributed using the existing logic.
The new accruals could take effect with nil accruals providing a clean slate.
The setRewardDistributor updates the mutiRewardDistributor contract with a new instance with out transferring the accured rewards to borrowers and lenders.
function _setRewardDistributor(MultiRewardDistributor newRewardDistributor) public { require(msg.sender == admin, "Unauthorized"); MultiRewardDistributor oldRewardDistributor = rewardDistributor; rewardDistributor = newRewardDistributor; emit NewRewardDistributor(oldRewardDistributor, newRewardDistributor); }
The above set function should be added with a logic to look at all the markets and for each market, the borrower and supplier rewards should be disbursed before the new instance is updated. As long as there are accured rewards, the old instance should not be replaced.
Manual review
step 1: Maintain a list of all accounts that participate in borrowing and supplying. getAllParticipants() returns a list of participants.
Step 2: Add an internal function disburseAllRewards() which can be called by adminOnly. This function looks for all partitipant accounts and for all markets, it claims rewards before updating the rewards distributor with a new instance.
function disburseAllRewards() internal adminOnly { address[] memory holders = getAllParticipants(); MToken[] memory mTokens = getAllMarkets(); claimReward(holders, mTokens, true, true); } // @audit use the existing claimReward function to disburse the rewards to accounts that had accruals.
As the number of participants and markets grow, the onchain approach may result in DOS as the size of computations may not fit in a single block due to gas limit.
As an alternative, the claimReward() could be called from the outside for all the holders and markets in smaller chunks.
Use getOutstandingRewardsForUser() function to check if there are any un-disbursed rewards in the MultiRewarddistributor contract. Only if there are no un-disbursed rewards, then only allow the admin to replace the reward distributor contract with a new instance.
Taking these steps will prevent the potential case where borrowers and suppliers could loose accured rewards.
Key drive away with this approach is, unless all the accured are distributed and they is nothing to distribute, dont update the multi reward distributor contract.
Token-Transfer
#0 - c4-pre-sort
2023-08-03T14:10:20Z
0xSorryNotSorry marked the issue as primary issue
#1 - ElliotFriedman
2023-08-03T20:56:24Z
This is valid, but disagree with severity since admin is trusted. Otherwise, if admin isn't trusted, then they could rug all the funds in the contract using the _rescueFunds
method.
#2 - c4-sponsor
2023-08-03T20:56:29Z
ElliotFriedman marked the issue as sponsor disputed
#3 - c4-sponsor
2023-08-03T20:56:33Z
ElliotFriedman marked the issue as disagree with severity
#4 - ElliotFriedman
2023-08-03T20:56:44Z
didn't mean to dispute, just meant to disagree with severity
#5 - alcueca
2023-08-12T21:33:22Z
@ElliotFriedman, note that the admin doesn't need to be malicious. You could be a few months down the line, and receive a bug report that forces you to pause and replace the rewards distributor. It would be really uncomfortable to move the not claimed rewards to the new contract.
#6 - c4-judge
2023-08-12T21:33:41Z
alcueca marked the issue as satisfactory
#7 - c4-judge
2023-08-21T21:41:12Z
alcueca marked the issue as selected for report
π Selected for report: immeas
Also found by: 0x70C9, 0xAnah, 0xArcturus, 0xComfyCat, 0xWaitress, 0xackermann, 0xkazim, 2997ms, 33audits, Arz, Aymen0909, ChrisTina, JP_Courses, John_Femi, Jorgect, Kaysoft, LosPollosHermanos, MohammedRizwan, Nyx, Rolezn, Sathish9098, Stormreckson, T1MOH, Tendency, Topmark, Udsen, Vagner, albertwh1te, ast3ros, banpaleo5, berlin-101, catellatech, cats, codetilda, cryptonue, eeshenggoh, fatherOfBlocks, hals, jamshed, jaraxxus, josephdara, kankodu, kodyvim, kutugu, lanrebayode77, mert_eren, nadin, naman1778, niki, petrichor, ravikiranweb3, said, solsaver, souilos, twcctop, wahedtalash77
44.8793 USDC - $44.88
Comptroller::setMarketBorrowCaps() Set Borrow caps for markets that are listed. require(markets[address(mToken)].isListed,"Invalid token");
Comptroller::_setMarketSupplyCaps()
Set Supply caps for markets that are listed.
require(markets[address(mToken)].isListed,"Invalid token");
chainlinkCompositeOracle is using a floating pragma and also older version of compiler different from other contract files.
Comptroller::nonReentrant is an unused modifier and so is the _locked storage variable.
ChainlinkOracle::setAdmin should check for zero address for the parameter before updating the state variable.
ChainlinkCompositeOracle::getDerivedPrice() There may be tokens with 0 decimals for which the oracle will not work.
Comptroller::mintAllowed should be until the supply cap. require(nextTotalSupplies < supplyCap, "market supply cap reached"); should be require(nextTotalSupplies <= supplyCap, "market supply cap reached");
#0 - c4-judge
2023-08-12T18:19:10Z
alcueca marked the issue as grade-a
#1 - c4-sponsor
2023-08-15T17:40:35Z
ElliotFriedman marked the issue as sponsor confirmed