Venus Prime - deadrxsezzz's results

Earn, borrow & lend on the #1 Decentralized Money Market on the BNB chain.

General Information

Platform: Code4rena

Start Date: 28/09/2023

Pot Size: $36,500 USDC

Total HM: 5

Participants: 115

Period: 6 days

Judge: 0xDjango

Total Solo HM: 1

Id: 290

League: ETH

Venus Protocol

Findings Distribution

Researcher Performance

Rank: 29/115

Findings: 1

Award: $198.48

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

198.4834 USDC - $198.48

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-555

External Links

Lines of code

https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L827

Vulnerability details

Impact

User will break internal accounting and will lead to key function revert

Proof of Concept

Whenever, _startScoreUpdateRound is invoked pendingScoreUpdates is set to the current totalIrrevocable + totalRevocable

    function _startScoreUpdateRound() internal {
        nextScoreUpdateRoundId++;
        totalScoreUpdatesRequired = totalIrrevocable + totalRevocable;
        pendingScoreUpdates = totalScoreUpdatesRequired;
    }

However, this accounting can easily be broken in the following scenario: After _startScoreUpdateRound is invoked, a user is minted their prime token

    function _mint(bool isIrrevocable, address user) internal {
        if (tokens[user].exists) revert IneligibleToClaim();

        tokens[user].exists = true;
        tokens[user].isIrrevocable = isIrrevocable;

        if (isIrrevocable) {
            totalIrrevocable++;
        } else {
            totalRevocable++;
        }

        if (totalIrrevocable > irrevocableLimit || totalRevocable > revocableLimit) revert InvalidLimit();

        emit Mint(user, isIrrevocable);
    }

Shortly after the user unstakes from the XVS vault and falls bellow the minimum stake threshold, therefore getting his Prime token burned. _burn at the end calls _updateRoundAfterTokenBurned

    function _updateRoundAfterTokenBurned(address user) internal {
        if (totalScoreUpdatesRequired > 0) totalScoreUpdatesRequired--;

        if (pendingScoreUpdates > 0 && !isScoreUpdated[nextScoreUpdateRoundId][user]) {
            pendingScoreUpdates--;
        }
    }

As it can be seen, it decreases pendingScoreUpdates. This would break internal accounting. This is especially a problem as then when the function updateScores is called with all users pending score updates, it will revert due to underflow:

    function updateScores(address[] memory users) external {
        if (pendingScoreUpdates == 0) revert NoScoreUpdatesRequired();
        if (nextScoreUpdateRoundId == 0) revert NoScoreUpdatesRequired();

        for (uint256 i = 0; i < users.length; ) {
            address user = users[i];

            if (!tokens[user].exists) revert UserHasNoPrimeToken();
            if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue;

            address[] storage _allMarkets = allMarkets;
            for (uint256 j = 0; j < _allMarkets.length; ) {
                address market = _allMarkets[j];
                _executeBoost(user, market);
                _updateScore(user, market);

                unchecked {
                    j++;
                }
            }

            pendingScoreUpdates--;  // @audit - will revert here due to underflow 
            isScoreUpdated[nextScoreUpdateRoundId][user] = true;

            unchecked {
                i++;
            }

            emit UserScoreUpdated(user);
        }
    }

Since pendingScoreUpdates initial value will be lower than the number of accounts updated, it will unexpectedly revert to underflow. Note that this issue could both happen intentionally and unintentionally, hence the medium severity of it.

Tools Used

Manual review

Add the following change to _mint:

    function _mint(bool isIrrevocable, address user) internal {
        if (tokens[user].exists) revert IneligibleToClaim();

        tokens[user].exists = true;
        tokens[user].isIrrevocable = isIrrevocable;
+       isScoreUpdated[nextScoreUpdateRoundId][user] = true;

        if (isIrrevocable) {
            totalIrrevocable++;
        } else {
            totalRevocable++;
        }

        if (totalIrrevocable > irrevocableLimit || totalRevocable > revocableLimit) revert InvalidLimit();

        emit Mint(user, isIrrevocable);
    }

Assessed type

Context

#0 - c4-pre-sort

2023-10-04T22:06:57Z

0xRobocop marked the issue as duplicate of #555

#1 - c4-judge

2023-11-02T00:58:46Z

fatherGoose1 marked the issue as satisfactory

#2 - c4-judge

2023-11-05T00:52:22Z

fatherGoose1 changed the severity to 3 (High Risk)

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