code-423n4 / 2023-12-revolutionprotocol-findings

3 stars 2 forks source link

`MaxHeap.extractMax` function doesn't return the element that reached maximum value first in some cases #512

Open c4-bot-8 opened 11 months ago

c4-bot-8 commented 11 months ago

Lines of code

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

Vulnerability details

Impact

Proof of Concept

Code Instances:

MaxHeap.updateValue function

    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
    }

MaxHeap.extractMax function

    function extractMax() external onlyAdmin returns (uint256, uint256) {
        require(size > 0, "Heap is empty");

        uint256 popped = heap[0];
        heap[0] = heap[--size];
        maxHeapify(0);

        return (popped, valueMapping[popped]);
    }

Foundry PoC:

  1. Add this testDoesItWorkCorrectly test inside the MaxHeapTestSuite test contract that resides in the MaxHeap.t.sol test file which is located in the following directory packages/revolution/test/max-heap/MaxHeap.t.sol :
   ├── MaxHeap.t.sol
   │   ├── MaxHeapTestSuite
   │       ├── testDoesItWorkCorrectly
    function testDoesItWorkCorrectly() public {
        uint256 highestVotes = 10;

        //1. two art pieces are added:
        vm.stopPrank();
        vm.startPrank(address(cultureIndex));
        maxHeap.insert(1, 0);
        maxHeap.insert(2, 0);
        vm.warp(block.timestamp + 2 hours);

        //2. the first art piece got highestVotes:
        maxHeap.updateValue(1, 10);

        //3. then another 3 art pieces are added:
        maxHeap.insert(3, 0);
        maxHeap.insert(4, 0);
        maxHeap.insert(5, 0);
        vm.warp(block.timestamp + 2 hours);

        //4. the fourth and the fifth art piece got highestVotes with 2-hours apart:
        maxHeap.updateValue(4, 10);
        vm.warp(block.timestamp + 2 hours);
        maxHeap.updateValue(5, 10);

        //5. extracting the highest voted piece that's going to be called by the `CulturIndex.dropTopVotedPiece`, and it will be the first piece:
        (uint256 pieceId, uint256 pieceVotes) = maxHeap.extractMax();
        assertEq(pieceId, 1);

        //6. then extracting the second highest voted piece that's going to be called by the `CulturIndex.dropTopVotedPiece`:
        (pieceId, pieceVotes) = maxHeap.extractMax();

        //7. so as can be noticed: it extracted the fifth piece instead of the fourth piece, while it should extract the fourth piece because it reached the highest votes before the fifth piece:
        assertFalse(pieceId == 4);
        assertEq(pieceId, 5);
        console.log("extracted pieceId is : ", pieceId);
        console.log("extracted pieceVotes is : ", pieceVotes);
        vm.stopPrank();
    }
  1. Explained scenario:

    1. two pieces are added: 1 and 2.
    2. piece#1 got the highest votes.
    3. then another three pieces are added.
    4. piece#4 got the highest votes.
    5. then after some time; piece#5 got the highest votes.
    6. maxHeap.extractMax function is called, where it extracted the maximum piece which is piece#1.
    7. then maxHeap.extractMax function is called again to extract the next maximum piece: it's supposed to extract piece#4 as it was the first piece to reach the highest votes before piece#5; but it extracted piece#5 instead.
  2. Test result:

$ FOUNDRY_PROFILE=dev forge test --match-test testDoesItWorkCorrectly -vvv
Running 1 test for test/max-heap/Updates.t.sol:MaxHeapUpdateTestSuite
[PASS] testDoesItWorkCorrectly() (gas: 349055)
Logs:
  pieceId is :  5
  pieceVotes is :  10

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.58ms

Tools Used

Manual Review.

Recommended Mitigation Steps

Update MaxHeap.extractMax function to handle this case.

Assessed type

Context

c4-pre-sort commented 11 months ago

raymondfam marked the issue as sufficient quality report

c4-pre-sort commented 11 months ago

raymondfam marked the issue as duplicate of #266

c4-pre-sort commented 11 months ago

raymondfam marked the issue as not a duplicate

c4-pre-sort commented 11 months ago

raymondfam marked the issue as primary issue

c4-pre-sort commented 11 months ago

raymondfam marked the issue as high quality report

rocketman-21 commented 10 months ago

same issue as other maxheap issues

c4-sponsor commented 10 months ago

rocketman-21 (sponsor) confirmed

c4-sponsor commented 10 months ago

rocketman-21 (sponsor) disputed

rocketman-21 commented 10 months ago

actually upon further inspection this is how it should function

time that the pieces were inserted has no weight to the order in which they are extracted

just because pieceId 4 and 5 both have 10 votes, the MaxHeap does not guarantee that the 4th will be extracted first even if it reached 10 votes first.

c4-judge commented 10 months ago

MarioPoneder changed the severity to QA (Quality Assurance)

MarioPoneder commented 10 months ago

See comment on previous primary issue: https://github.com/code-423n4/2023-12-revolutionprotocol-findings/issues/266#issuecomment-1879666356

c4-judge commented 10 months ago

MarioPoneder marked the issue as grade-b

DevHals commented 10 months ago

Hi @MarioPoneder,

as per your comment on issue 266; this one has a runnable PoC to clearly identify what is wrong with the MaxHeap.extractMax function and how it introduces unfair auctions for art-pieces as some might reach top-voted before other pieces but will not be auctioned first (and might never get the chance to be auctioned)!

I kindly ask you to take a second look and re-evaluate this issue,

Thanks!

MarioPoneder commented 10 months ago

Thank you for your comment!

This is an edge-case of 2 or more art prices getting the same votes, see also #582. Furthermore, MaxHeap is not designed to respect insertion order, only the votes.
Nevertheless this is a valid feature suggestion and therefore QA seems appropriate.