Revolution Protocol - mojito_auditor's results

A protocol to empower communities to raise funds, fairly distribute governance, and maximize their impact in the world.

General Information

Platform: Code4rena

Start Date: 13/12/2023

Pot Size: $36,500 USDC

Total HM: 18

Participants: 110

Period: 8 days

Judge: 0xTheC0der

Id: 311

League: ETH

Collective

Findings Distribution

Researcher Performance

Rank: 38/110

Findings: 3

Award: $170.72

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Awards

2.6741 USDC - $2.67

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
duplicate-515

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/AuctionHouse.sol#L348

Vulnerability details

Impact

In the AuctionHouse contract, a reservePrice limit is set for the final price of any auction. If the last bid does not meet this reservePrice, the auction is considered unsuccessful and the Verb token is burnt. This behavior is checked and executed in the function _settleAuction().

// Check if contract balance is greater than reserve price
// @audit this could by bypass by transferring ETH to this contract directly using self-destruct
if (address(this).balance < reservePrice) { 
    // If contract balance is less than reserve price, refund to the last bidder
    if (_auction.bidder != address(0)) {
        _safeTransferETHWithFallback(_auction.bidder, _auction.amount);
    }

    // And then burn the Noun
    verbs.burn(_auction.verbId);
}

However, the check uses the current contract balance, which could be easily bypassed by transferring ETH directly to the contract using self-destruct.

Proof of Concept

Even though the AuctionHouse does not have an external receive function, an attacker can still use self-destruct on another contract and refund the ETH to the AuctionHouse contract. More information about self-destruct can be found at:

https://solidity-by-example.org/hacks/self-destruct/

Tools Used

Manual Review

Consider checking the reservePrice against the last bid instead of the current contract balance.

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-12-22T19:21:38Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2023-12-22T19:21:49Z

raymondfam marked the issue as duplicate of #72

#2 - c4-pre-sort

2023-12-22T23:20:06Z

raymondfam marked the issue as not a duplicate

#3 - c4-pre-sort

2023-12-22T23:20:11Z

raymondfam marked the issue as sufficient quality report

#4 - c4-pre-sort

2023-12-22T23:20:19Z

raymondfam marked the issue as duplicate of #515

#5 - c4-judge

2024-01-05T22:06:41Z

MarioPoneder changed the severity to 3 (High Risk)

#6 - c4-judge

2024-01-05T22:07:56Z

MarioPoneder marked the issue as satisfactory

#7 - c4-judge

2024-01-11T18:03:12Z

MarioPoneder changed the severity to 2 (Med Risk)

Findings Information

🌟 Selected for report: ast3ros

Also found by: 0xG0P1, KupiaSec, Pechenite, cccz, deth, dimulski, mojito_auditor, osmanozdemir1, peanuts, rvierdiiev, zhaojie

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
duplicate-409

Awards

51.1381 USDC - $51.14

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L313

Vulnerability details

Impact

In CultureIndex, the createPiece() function records the current vote and ERC20 supplies. It also sets newPiece.creationBlock = block.number.

newPiece.totalVotesSupply = _calculateVoteWeight( 
    erc20VotingToken.totalSupply(),
    erc721VotingToken.totalSupply()
);
newPiece.totalERC20Supply = erc20VotingToken.totalSupply();
newPiece.metadata = metadata;
newPiece.sponsor = msg.sender;
newPiece.creationBlock = block.number;

When voting for a piece, the voting power is obtained at the creationBlock. However, this may cause a mismatch between totalVotesSupply and totalERC20Supply. This is because additional transactions within the same block as createPiece() could alter the ERC20Supply.

Proof of Concept

Consider this scenario:

  1. At block 100, createPiece() is called with erc20VotingToken.totalSupply() = 1000. This results in totalERC20Supply = 1000.
  2. In the same block 100, after the createPiece() transaction, another transaction buyToken() is made to buy a large amount of voting token, which will increase erc20VotingToken.totalSupply() = 5000.
  3. Even though these tokens were bought after the piece was created, they still have voting power because the block number is still 100. This also makes totalERC20Supply and totalVotesSupply incorrect. Although voting supply and ERC20 supply are calculated with erc20VotingToken.totalSupply() = 1000, the actual ERC20 supply that is allowed to vote will be 5000.

Tools Used

Manual Review

Consider getting the voting power at creationBlock - 1 instead when casting a vote for an art piece.

Assessed type

Other

#0 - c4-pre-sort

2023-12-22T19:25:50Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T19:26:26Z

raymondfam marked the issue as duplicate of #260

#2 - c4-pre-sort

2023-12-24T05:40:01Z

raymondfam marked the issue as duplicate of #409

#3 - c4-judge

2024-01-05T22:39:10Z

MarioPoneder marked the issue as satisfactory

Findings Information

Labels

bug
2 (Med Risk)
disagree with severity
grade-a
satisfactory
sponsor confirmed
sufficient quality report
duplicate-266

Awards

116.9139 USDC - $116.91

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/MaxHeap.sol#L104

Vulnerability details

Impact

In the MaxHeap contract, the maxHeapify() function is used to reheapify the heap to maintain the heap property. Starting from a given position in the heap, it gets the values of the left and right children. If a child's value is greater than the current position's value, they are swapped to maintain the heap's structure.

function maxHeapify(uint256 pos) internal {
    uint256 left = 2 * pos + 1;
    uint256 right = 2 * pos + 2;

    uint256 posValue = valueMapping[heap[pos]];
    uint256 leftValue = valueMapping[heap[left]];
    uint256 rightValue = valueMapping[heap[right]];

    if (pos >= (size / 2) && pos <= size) return; 

    // @audit not check if `right` node is exist or not (could be a node has been removed earlier)
    if (posValue < leftValue || posValue < rightValue) { 
        if (leftValue > rightValue) {
            swap(pos, left);
            maxHeapify(left);
        } else {
            swap(pos, right);
            maxHeapify(right);
        }
    }
}

The problem is that there may be cases where the right child does not exist but contains the value of a previously removed node. If the removed node has a larger value than the current node, the removed node will be swapped back into the heap. This breaks the heap's structure and could return the incorrect maximum value when queried.

Proof of Concept

Please copy this test to Updates.t.sol and also import console.sol.

function testMaxHeapify() public {
    // Modified from testUpdateValueDecrease()
    maxHeapTester.insert(1, 20);
    maxHeapTester.insert(2, 20);
    maxHeapTester.insert(3, 20);

    maxHeapTester.extractMax();
    maxHeapTester.extractMax();
    maxHeapTester.extractMax();
  
    // At this moment, the heap is empty.

    maxHeapTester.insert(4, 10);
    maxHeapTester.insert(5, 10);

    // Trigger maxHeapify()
    maxHeapTester.updateValue(4, 7);

    (uint256 itemId, uint256 value) = maxHeapTester.getMax();

    // Output
    // Item ID:  3
    // Value:  20
    console.log("Item ID: ", itemId);
    console.log("Value: ", value);
}

Tools Used

Manual Review

Check if the right node exists before making comparisons.

Assessed type

Other

#0 - c4-pre-sort

2023-12-22T19:25:01Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T19:25:04Z

raymondfam marked the issue as primary issue

#2 - c4-sponsor

2024-01-04T23:36:57Z

rocketman-21 (sponsor) confirmed

#3 - c4-sponsor

2024-01-04T23:37:03Z

rocketman-21 marked the issue as disagree with severity

#4 - rocketman-21

2024-01-04T23:37:28Z

as stated elsewhere, we never allowed updating values of the heap downwards, so this wouldn't have been an issue in the current protocol

nonetheless valid

#5 - MarioPoneder

2024-01-05T22:30:54Z

@rocketman-21 Could you please elaborate further on that?
It seems to me after a first review that downwards heapifying is possible in the protocol.

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/MaxHeap.sol#L136-L151

    function updateValue(uint256 itemId, uint256 newValue) public onlyAdmin {
        uint256 position = positionMapping[itemId];
        uint256 oldValue = valueMapping[itemId];

        // Update the value in the valueMapping
        valueMapping[itemId] = newValue;

        // Decide whether to perform upwards or downwards heapify
        if (newValue > oldValue) {
            // Upwards heapify
            while (position != 0 && valueMapping[heap[position]] > valueMapping[heap[parent(position)]]) {
                swap(position, parent(position));
                position = parent(position);
            }
        } else if (newValue < oldValue) maxHeapify(position); // Downwards heapify
    }

#6 - rocketman-21

2024-01-06T19:15:11Z

I meant that in all cases where we used the updateValue function (in the CultureIndex) in the protocol, it only allowed updateValue with a newValue greater than the previous.

The maxHeap in isolation was definitely broken, but we were not using it in the CultureIndex in a way it would've been exploited.

I'm fine with whatever severity you want to make it

#7 - MarioPoneder

2024-01-06T22:32:51Z

This is also a tough one, as all other issues around MaxHeap. First of all I genuinely appreciate the efforts put into this report and many of its duplicates, clearly an underlying bug is shown.
However, no impacts on the function of protocol as a whole are shown. Therefore, it's hard to justify High/Medium severity for this group of findings.
I have to downgrade to QA for now, but invite the author of this submission or its duplicates to provide a runnable PoC during PJQA which shows impacts through the main entry points of the protocol, i.e. not purely interacting with the MaxHeap contract.

Thank you for your understanding!

#8 - c4-judge

2024-01-06T22:33:03Z

MarioPoneder changed the severity to QA (Quality Assurance)

#9 - c4-judge

2024-01-07T16:10:59Z

MarioPoneder marked the issue as grade-a

#10 - mojito-auditor

2024-01-11T13:13:15Z

Hi @MarioPoneder, could you please take a second look at this issue?

This issue has clearly demonstrated a way to break one of the main invariants in the MaxHeap. Since this contract is within the scope and the sponsor has explicitly specified the main invariant, breaking the invariant should already classify the issue as having at least Medium severity. This contract stores important data, and the contract that interacts with it (CultureIndex) is upgradeable. Therefore, if this issue is not fixed, it could be easily exploited in the future.

#11 - c4-judge

2024-01-11T14:37:12Z

This previously downgraded issue has been upgraded by MarioPoneder

#12 - c4-judge

2024-01-11T14:37:36Z

MarioPoneder marked the issue as duplicate of #266

#13 - c4-judge

2024-01-11T14:37:45Z

MarioPoneder marked the issue as satisfactory

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