Revolution Protocol - SpicyMeatball'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: 13/110

Findings: 8

Award: $776.51

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

44.0266 USDC - $44.03

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
duplicate-210

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/ERC20TokenEmitter.sol#L191-L198

Vulnerability details

Impact

ERC20TokenEmitter leaves some amount of ETH undistributed after a buyToken transaction. As a result, a significant sum of tokens will be left in the contract without a possibility to retrieve it.

Proof of Concept

During a buyToken transaction after we take some fees

uint256 msgValueRemaining = _handleRewardsAndGetValueToSend( msg.value, protocolRewardsRecipients.builder, protocolRewardsRecipients.purchaseReferral, protocolRewardsRecipients.deployer );

msgValueRemaining is split between treasury and creator addresses

uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000;

then creator's share is divided again

uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000;

creatorDirectPayment and toPayTreasury are distributed between the creator and the treasury accordingly, however the other part of creator's share will remain undistributed and therefore will be locked in the contract forever, because there are no other means to retrieve it.

Check this test case for ERC20TokenEmitter.t.sol

function testETHStuck() public { vm.startPrank(address(dao)); erc20TokenEmitter.setCreatorRateBps(1000); erc20TokenEmitter.setEntropyRateBps(5000); vm.startPrank(address(0)); address[] memory recipients = new address[](1); recipients[0] = address(1); uint256[] memory bps = new uint256[](1); bps[0] = 10_000; vm.deal(address(0), 100000 ether); console.log("BAL BEFORE: ", address(erc20TokenEmitter).balance); // try buy with - 1 ETH, to creator 10%, entropy 50% // after paying fees - 0.975 ETH // to treasury - 0.975 - 0.975 * 10 / 100 = 0.8775 ETH // to creator - (0.975 * 10 / 100) / 2 = 0.04875 ETH // stuck - 0.04875 ETH erc20TokenEmitter.buyToken{ value: 1e18 }( recipients, bps, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); console.log("BAL AFTER: ", address(erc20TokenEmitter).balance); assertEq(address(erc20TokenEmitter).balance, 0.04875 ether); }

Tools Used

Foundry

Assuming this part belongs to the treasury, we need to send it too

Assessed type

ETH-Transfer

#0 - c4-pre-sort

2023-12-22T03:05:36Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T03:05:44Z

raymondfam marked the issue as duplicate of #13

#2 - c4-pre-sort

2023-12-24T02:55:10Z

raymondfam marked the issue as duplicate of #406

#3 - c4-judge

2024-01-05T23:08:44Z

MarioPoneder marked the issue as satisfactory

Findings Information

🌟 Selected for report: osmanozdemir1

Also found by: 0xG0P1, King_, SpicyMeatball, hals, ktg, rvierdiiev

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
duplicate-168

Awards

494.8735 USDC - $494.87

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L313 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L355-L359 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/CultureIndex.sol#L226-L234

Vulnerability details

Impact

When an auction is created in the AuctionHouse contract, we mint a Verb token, thus increasing the totalSupply, but we don't know if the auction will be successful or not. Art pieces that were created during the auction may have their quorumVotes unfairly increased, if the auction ends later without bidders and the token is burned (totalSupply decreased).

create auction | mint token, supply increased | create piece and use current supply for quorumVotes calculation | auction failed | token burned, supply decreases

This issue may be especially harmful at the early stages, when there are not much tokens minted.

Proof of Concept

Check this test case for AuctionSettling.t.sol

function testInflateQuorum() public { uint256 pieceId0 = createDefaultArtPiece(); auction.unpause(); // art piece created during this auction will have increased quorumVotes compared to other // unfair because totalSupply will be decreased after settlement uint256 pieceId1 = createDefaultArtPiece(); vm.warp(block.timestamp + auction.duration()); // Fast forward time to end the auction auction.settleCurrentAndCreateNewAuction(); uint256 pieceId2 = createDefaultArtPiece(); ICultureIndex.ArtPiece memory piece0 = cultureIndex.getPieceById(pieceId0); ICultureIndex.ArtPiece memory piece1 = cultureIndex.getPieceById(pieceId1); ICultureIndex.ArtPiece memory piece2 = cultureIndex.getPieceById(pieceId2); console.log("Q1: ", piece0.quorumVotes); console.log("Q2: ", piece1.quorumVotes); console.log("Q3: ", piece2.quorumVotes); }

Tools Used

Forge

Consider minting tokens at the auction settlement phase rather than at auction creation. This way we'll be certain that Verb totalSupply won't decrease in the end.

Assessed type

ERC721

#0 - c4-pre-sort

2023-12-22T02:28:54Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T02:29:14Z

raymondfam marked the issue as duplicate of #16

#2 - c4-pre-sort

2023-12-24T05:13:59Z

raymondfam marked the issue as not a duplicate

#3 - c4-pre-sort

2023-12-24T05:14:08Z

raymondfam marked the issue as duplicate of #18

#4 - c4-judge

2024-01-08T21:43:00Z

MarioPoneder marked the issue as duplicate of #409

#5 - c4-judge

2024-01-08T21:49:27Z

MarioPoneder marked the issue as satisfactory

#6 - c4-judge

2024-01-10T18:30:35Z

MarioPoneder marked the issue as not a duplicate

#7 - c4-judge

2024-01-10T18:30:42Z

MarioPoneder marked the issue as duplicate of #168

#8 - c4-judge

2024-01-10T18:32:36Z

MarioPoneder changed the severity to 3 (High Risk)

Awards

7.2215 USDC - $7.22

Labels

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

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/CultureIndex.sol#L498-L503 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/CultureIndex.sol#L234

Vulnerability details

Impact

If the admin decides to reduce the quorum votes bps, the pieces created before this transaction will have to gather more votes than the new ones and thus will be at disadvantage.

Proof of Concept

When a creator creates a piece of art in CultureIndex.sol, we get the total supply of ERC20 and ERC721 tokens that are used in voting and calculate the number of votes needed to mint that piece.

newPiece.pieceId = pieceId; newPiece.totalVotesSupply = _calculateVoteWeight( erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply() ); newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); newPiece.metadata = metadata; newPiece.sponsor = msg.sender; newPiece.creationBlock = block.number; newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000;

Assume that we have a totalVotesSupply = 100_000 * 1e18 and quorumVotesBps = 3000 (30%), hence we need at least 30_000 * 1e18 votes for the piece to be eglible for minting. https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/CultureIndex.sol#L523

require(totalVoteWeights[piece.pieceId] >= piece.quorumVotes, "Does not meet quorum votes to be dropped.");

After some time the admin sets quorumVotesBps = 1000 (10%), making quorumVotes = 10_000 * 1e18 for new pieces. While older ones still need to gather 30_000 * 1e18 because we save this value at the storage when an art piece is created.

Tools Used

Manual review

Consider using current quorumVotesBps with stored piece.totalVotesSupply in dropTopVotedPiece https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/CultureIndex.sol#L519

ICultureIndex.ArtPiece memory piece = getTopVotedPiece(); - require(totalVoteWeights[piece.pieceId] >= piece.quorumVotes, "Does not meet quorum votes to be dropped."); + require(totalVoteWeights[piece.pieceId] >= quorumVotesBPS * piece.totalVotesSupply / 10_000, "Does not meet quorum votes to be dropped.");

Assessed type

Context

#0 - c4-pre-sort

2023-12-22T01:16:55Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T01:17:05Z

raymondfam marked the issue as duplicate of #16

#2 - c4-pre-sort

2023-12-24T15:11:01Z

raymondfam marked the issue as duplicate of #449

#3 - c4-judge

2024-01-06T15:57:27Z

MarioPoneder marked the issue as satisfactory

Awards

25.1638 USDC - $25.16

Labels

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

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/ERC20TokenEmitter.sol#L152

Vulnerability details

Impact

ERC20TokenEmitter uses VRGDA model to get the current price of the ERC20 token issued in exchange for ETH. Buyers are vulnerable to the price slippage due to price adjustments incurred by the VRGDA algorithm (price increases if amount sold is ahead of schedule and vice versa).

Proof of Concept

When user calls payable buyToken function, the contract calculates the amount it will mint to the user based on msg.value and the amount of emitted tokens https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/ERC20TokenEmitter.sol#L184

return vrgdac.yToX({ timeSinceStart: toDaysWadUnsafe(block.timestamp - startTime), sold: emittedTokenWad, amount: int(etherAmount) }); }

If this transaction was frontrunned or executed later, the user may receive fewer tokens than he expected (e.g. the transaction was executed when the amount sold is ahead of schedule).

Tools Used

Manual review

Consider adding deadline and minAmountOut checks

function buyToken( address[] calldata addresses, uint[] calldata basisPointSplits, ProtocolRewardAddresses calldata protocolRewardsRecipients uint256 deadline, uint256 minAmountOut ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { require(deadline > block.timestamp, "expired"); ... require(uint256(totalTokensForBuyers) >= minAmountOut), "amount too small")

Assessed type

Token-Transfer

#0 - c4-pre-sort

2023-12-22T02:30:26Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T02:30:45Z

raymondfam marked the issue as duplicate of #26

#2 - c4-pre-sort

2023-12-24T06:00:43Z

raymondfam marked the issue as duplicate of #397

#3 - c4-judge

2024-01-06T16:29:23Z

MarioPoneder marked the issue as satisfactory

Findings Information

Labels

bug
2 (Med Risk)
grade-b
satisfactory
sufficient quality report
edited-by-warden
duplicate-266

Awards

116.9139 USDC - $116.91

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/MaxHeap.sol#L160-L161 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/MaxHeap.sol#L105-L111 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/MaxHeap.sol#L150

Vulnerability details

Impact

When we extract the maximum element, last element in the heap is placed on top without updating it's positionMapping and previous heap values

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]); }

This leads to several issues:

  • heap elements collision
  • break of the maxHeap invariant (parent > child)

Both of this issues arise if we decrease the value of the element, currently there is no way of it's happening because CultureIndex contract can only increase the number of votes, however there is a condition https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/MaxHeap.sol#L150 from which it follows that the developers have provided for such a possibility

Proof of Concept

Let's start with a collision. Consider initial heap

heap[0] = 50 id 1 heap[1] = 30 id 2 heap[2] = 20 id 3

After we extract the top element it will look like this

heap[0] = 30 id 2 heap[1] = 20 id 3 heap[2] = 20 id 3 // can be overwritten

Notice that although size = 2 we still have heap[2] = 20 in the storage. Next we update id 2 to 10, update(2, 10), this will give us

heap[0] = 20 id 3 heap[1] = 20 id 3 // 10 should be here heap[2] = 10 id 2 // can be overwritten

Instead of moving id 3 to the heap[1] we swapped it with a dead position, because of this condition https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/MaxHeap.sol#L105-L112 if leftValue = rightValue, protocol will always swap with a right position, which in our case can be overwritten with a newly inserted item leading to a collision.

Test case for MaxHeap.t.sol

function testExtractMaxBreak() public { // Fill the heap maxHeap.insert(1, 50); maxHeap.insert(2, 30); maxHeap.insert(3, 20); console.log(" ", maxHeap.valueMapping(maxHeap.heap(0))); console.log(" /", " \\ "); console.log( " ", maxHeap.valueMapping(maxHeap.heap(1)), " ", maxHeap.valueMapping(maxHeap.heap(2)) ); maxHeap.extractMax(); maxHeap.updateValue(2, 10); maxHeap.insert(5, 1); console.log("\nAFTER EXTRACTION AND UPDATE\n"); console.log(" ", maxHeap.valueMapping(maxHeap.heap(0))); console.log(" /", " \\ "); console.log( " ", maxHeap.valueMapping(maxHeap.heap(1)), " ", maxHeap.valueMapping(maxHeap.heap(2)) // Notice that id 3 was overwritten with a new element ); }

Now to the second issue, if the last element is the second greatest element in the heap

heap[0] = 50 id 1 heap[1] = 20 id 2 heap[2] = 30 id 3

After extraction

heap[0] = 30 id 3 heap[1] = 20 id 2

Notice that positionMapping[heap[0]] = 2 wasn't updated, this allows us to decrease the top element value without triggering maxHeapify, after update(3, 1) we are still at the top

heap[0] = 1 id 3 heap[1] = 20 id 2
function testExtractMaxBreak() public { // Fill the heap maxHeap.insert(1, 50); maxHeap.insert(2, 20); maxHeap.insert(3, 30); console.log(" ", maxHeap.valueMapping(maxHeap.heap(0))); console.log(" /", " \\ "); console.log( " ", maxHeap.valueMapping(maxHeap.heap(1)), " ", maxHeap.valueMapping(maxHeap.heap(2)) ); maxHeap.extractMax(); maxHeap.updateValue(3, 1); console.log("\nAFTER EXTRACTION AND UPDATE\n"); console.log(" ", maxHeap.valueMapping(maxHeap.heap(0))); console.log(" /", " \\ "); console.log( " ", maxHeap.valueMapping(maxHeap.heap(1)), " ", maxHeap.valueMapping(maxHeap.heap(2)) // DEAD ELEMENT ); }

Tools Used

Foundry

Clean and update values in extractMax,

function extractMax() external onlyAdmin returns (uint256, uint256) { require(size > 0, "Heap is empty"); uint256 popped = heap[0]; heap[0] = heap[--size]; heap[size] = 0; positionMapping[heap[0]] = 0; maxHeapify(0); return (popped, valueMapping[popped]); }

Assessed type

Context

#0 - c4-pre-sort

2023-12-22T00:40:25Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2023-12-22T00:41:28Z

raymondfam marked the issue as duplicate of #41

#2 - c4-pre-sort

2023-12-22T05:26:20Z

raymondfam marked the issue as sufficient quality report

#3 - c4-pre-sort

2023-12-22T05:26:38Z

raymondfam marked the issue as duplicate of #266

#4 - c4-pre-sort

2023-12-24T03:55:37Z

raymondfam marked the issue as not a duplicate

#5 - c4-pre-sort

2023-12-24T03:55:50Z

raymondfam marked the issue as duplicate of #363

#6 - c4-pre-sort

2023-12-24T04:03:49Z

raymondfam marked the issue as duplicate of #266

#7 - c4-judge

2024-01-06T12:28:12Z

MarioPoneder changed the severity to QA (Quality Assurance)

#8 - MarioPoneder

2024-01-06T12:29:04Z

#9 - c4-judge

2024-01-07T16:22:21Z

MarioPoneder marked the issue as grade-b

#10 - c4-judge

2024-01-10T23:49:24Z

This previously downgraded issue has been upgraded by MarioPoneder

#11 - c4-judge

2024-01-10T23:50:06Z

MarioPoneder marked the issue as satisfactory

#12 - k4zanmalay

2024-01-11T07:50:19Z

Hi @MarioPoneder ! I believe this issue covers both #266 and #363:

#266 addresses the problem with heap mapping not updated correctly (deleted item value is still in the storage), this issue is described in the first part of the report

Let's start with a collision. Consider initial heap

heap[0] = 50 id 1 heap[1] = 30 id 2 heap[2] = 20 id 3 After we extract the top element it will look like this

heap[0] = 30 id 2 heap[1] = 20 id 3 heap[2] = 20 id 3 // can be overwritten Notice that although size = 2 we still have heap[2] = 20 in the storage. Next we update id 2 to 10, update(2, 10), this will give us ...

#363 addresses the problem with not updating the positionMapping and breaking the max heap order invariant (parent > child), which is described in the second part of this report

Now to the second issue, if the last element is the second greatest element in the heap

heap[0] = 50 id 1 heap[1] = 20 id 2 heap[2] = 30 id 3 After extraction

heap[0] = 30 id 3 heap[1] = 20 id 2 Notice that positionMapping[heap[0]] = 2 wasn't updated, this allows us to decrease the top element value without triggering >maxHeapify, after update(3, 1) we are still at the top ...

Mitigation report also includes fixes for both of them

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

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

        return (popped, valueMapping[popped]);
    }

I think that we shouldn't split #363 and #266 and leave only one issue, because they have common root:

  • both come from extractMax function not correctly updating storage values

Thank you!

#13 - mcgrathcoutinho

2024-01-11T08:04:42Z

Hi @k4zanmalay, please check my comment here as to why these issues have different root causes.

#14 - k4zanmalay

2024-01-11T08:50:18Z

@mcgrathcoutinho after rereading your POC where you increase the value of the item, I agree with you and the judge that your finding is a separate issue. I still think that this report should be credited for it too (at least partially).

#15 - MarioPoneder

2024-01-11T16:00:21Z

Thank you for your comments!

If this issue was perfectly describing both core issues, I'd have to make it the primary one and duplicate #363 and #266 to it with partial credit. However, it's not quite there yet.

As I cannot duplicate to two issues (to one of them only partial credit), it seems appropriate to leave the duplication as it is since it is closer to #266 than to #363 from my point of view.

Thank you for your understanding.

Findings Information

Labels

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

Awards

51.1381 USDC - $51.14

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/ERC20TokenEmitter.sol#L179-L184

Vulnerability details

Impact

The issue here is that we quote amounts for creators and buyers separately, knowing that VRGDA algorithm adjusts the price based on tokens sold inside a day, resulting amount will be different, getTokenQuoteForEther(toCreator) + getTokenQuoteForEther(buyerAmount) != getTokenQuoteForEther(toCreator + buyerAmount)

Thus, we mint more tokens than we should.

Proof of Concept

Assume that targetPrice = 1 ether, perTimeUnit = 1000 ether and we try to buy tokens with 20000 ether (somewhat exaggerated amount to highlight the issue)

function testC4_1() public { vm.startPrank(address(0)); int quote0 = erc20TokenEmitter.getTokenQuoteForEther(20000e18); int quote1 = erc20TokenEmitter.getTokenQuoteForEther(10000e18); console.log("QUOTES: "); console.logInt(quote0); console.logInt(quote1); }

quoting full amount yields 10760 * 1e18 tokens, but if we split 20000 * 1e18 in two 10000 * 1e18 quotes in the same transaction we'll receive 13659 * 1e18.

This is exactly what happens in buyToken, we split the user's payment, and quote amounts for buyers and creators separately

int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) : int(0); // Tokens to emit to buyers int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0);

Notice that emittedTokensWad counter is updated later https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/ERC20TokenEmitter.sol#L187-L188

Tools Used

Foundry, ERC20TokenEmitter.t.sol

Perhaps we should use msgValueRemaining - creatorDirectPayment to quote amount of tokens and split it between buyers and creators afterwards.

Assessed type

Math

#0 - c4-pre-sort

2023-12-22T04:23:53Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T04:24:03Z

raymondfam marked the issue as duplicate of #194

#2 - c4-judge

2024-01-06T13:55:14Z

MarioPoneder marked the issue as satisfactory

Findings Information

Awards

29.8238 USDC - $29.82

Labels

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

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L383-L388 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L401-L402 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/NontransferableERC20Votes.sol#L128-L129

Vulnerability details

Impact

When the auction is successfully settled, the creators receive a share of the winning bid in two ways:

  • direct ETH payment
  • ERC20 tokens from ERC20TokenEmitter These amounts are configured using entropyRateBps, based on it's value:
  • if rate = 10_000 (100%), the whole share is paid in ETH
  • if rate = 0, the whole share is used to buy ERC20 tokens
  • we split the share if 0 < rate < 10_000

The problem is that we initialize vrgdaReceivers and vrgdaSplits arrays only if entropyRateBps > 0 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L383-L388

if (creatorsShare > 0 && entropyRateBps > 0) { for (uint256 i = 0; i < numCreators; i++) { ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i]; vrgdaReceivers[i] = creator.creator; vrgdaSplits[i] = creator.bps;

As a result this block may be executed with empty arrays if rate = 0 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L399-L409

if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); }

which later leads to a revert in this check https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/NontransferableERC20Votes.sol#L128-L129

Proof of Concept

function testC4_2() public { auction.setEntropyRateBps(0); uint256 pieceId = createDefaultArtPiece(); auction.unpause(); vm.stopPrank(); vm.deal(address(1), 10000 ether); vm.prank(address(1)); auction.createBid{value: 1 ether}(pieceId, address(this)); vm.warp(block.timestamp + auction.duration()); // Fast forward time to end the auction // will revert with ERC20InvalidReceiver(0x0000000000000000000000000000000000000000) auction.settleCurrentAndCreateNewAuction(); }

Tools Used

Foundry, AuctionSettling.t.sol

Initialize vrgdaReceivers and vrgdaSplits arrays outside of the if statement

Assessed type

Token-Transfer

#0 - c4-pre-sort

2023-12-22T04:37:27Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2023-12-22T04:38:27Z

raymondfam marked the issue as primary issue

#2 - c4-pre-sort

2023-12-22T07:02:16Z

raymondfam marked the issue as duplicate of #335

#3 - c4-pre-sort

2023-12-22T07:02:21Z

raymondfam marked the issue as sufficient quality report

#4 - c4-judge

2024-01-06T01:07:50Z

MarioPoneder marked the issue as unsatisfactory: Overinflated severity

#5 - c4-judge

2024-01-06T01:23:12Z

MarioPoneder marked the issue as not a duplicate

#6 - c4-judge

2024-01-06T01:25:32Z

MarioPoneder changed the severity to QA (Quality Assurance)

#7 - c4-judge

2024-01-06T01:27:56Z

MarioPoneder marked the issue as grade-c

#8 - c4-judge

2024-01-06T15:14:25Z

This previously downgraded issue has been upgraded by MarioPoneder

#9 - c4-judge

2024-01-06T15:14:42Z

MarioPoneder marked the issue as duplicate of #160

#10 - c4-judge

2024-01-06T15:14:50Z

MarioPoneder marked the issue as satisfactory

Findings Information

Awards

29.8238 USDC - $29.82

Labels

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

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L390-L391 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L399 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/protocol-rewards/src/abstract/RewardSplits.sol#L41

Vulnerability details

Impact

When the auction is successfully settled, the creators receive a share of the winning bid in two ways:

  • direct ETH payment
  • ERC20 tokens from ERC20TokenEmitter

These amounts are configured using entropyRateBps, based on it's value:

  • if rate = 10_000 (100%), the whole share is paid in ETH
  • if rate = 0, the whole share is used to buy ERC20 tokens
  • we split the share if 0 < rate < 10_000

If the auction admin decides to fully pay creatorsShare in ETH there are may be troubles as the contract assumes that in the case of entropyRateBps = 10_000 creatorsShare = ethPaidToCreators therefore skipping ERC20 tokens minting. https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L399

if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); }

However, this condition not always holds: depending on the bid amount and creators bps, a rounding error may occur making ethPaidToCreators slightly smaller than creatorsShare. As a result auction will try to mint this dust difference failing in process because of minPurchaseAmount violation https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/protocol-rewards/src/abstract/RewardSplits.sol#L41

Proof of Concept

function testC4_3() public { auction.setCreatorRateBps(1000); //10 % auction.setEntropyRateBps(10000); //100 % address[] memory creators = new address[](3); uint256[] memory bps = new uint256[](3); creators[0] = address(1); creators[1] = address(1); creators[2] = address(1); bps[0] = 3333; bps[1] = 3333; bps[2] = 3334; uint256 pieceId = createArtPieceMultiCreator( "Mona Lisa", "A masterpiece", ICultureIndex.MediaType.IMAGE, "ipfs://legends", "", "", creators, bps ); auction.unpause(); vm.stopPrank(); vm.deal(address(1), 10000 ether); vm.prank(address(1)); auction.createBid{value: 1e18 + 12345678}(pieceId, address(1)); vm.warp(block.timestamp + auction.duration()); // Fast forward time to end the auction vm.expectRevert(abi.encodeWithSignature("INVALID_ETH_AMOUNT()")); auction.settleCurrentAndCreateNewAuction(); }

Tools Used

Foundry, AuctionSettling.t.sol

Add entropyRateBps check in https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L399

if ((creatorsShare > ethPaidToCreators )&& (entropyRateBps != 10_000)) {

Assessed type

Math

#0 - c4-pre-sort

2023-12-22T16:52:50Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T16:53:27Z

raymondfam marked the issue as duplicate of #8

#2 - c4-pre-sort

2023-12-24T03:14:13Z

raymondfam marked the issue as duplicate of #160

#3 - c4-judge

2024-01-06T15:06:46Z

MarioPoneder marked the issue as satisfactory

Awards

7.359 USDC - $7.36

Labels

bug
downgraded by judge
grade-b
insufficient quality report
primary issue
QA (Quality Assurance)
edited-by-warden
Q-32

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/VerbsToken.sol#L130-L156 https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/cryptography/EIP712Upgradeable.sol#L68-L91

Vulnerability details

Impact

Attempts to delegate verb tokens with a signature may fail.

Proof of Concept

VerbsToken NFT inherits from VotesUpgradeable.sol and EIP712Upgradeable.sol contracts, which allows users to use the token in the voting process. User can delegate votes directly or with a signature https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/base/VotesUpgradeable.sol#L180-L199

function delegateBySig( address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) public virtual { if (block.timestamp > expiry) { revert VotesExpiredSignature(expiry); } address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry))), v, r, s ); _useCheckedNonce(signer, nonce); _delegate(signer, delegatee); }

In that case we use _hashTypedDataV4 function from EIP712Upgradeable.sol, unfortunately this contract isn't initialized, which means default empty name and version strings will be used to construct a domain separator.

function _buildDomainSeparator() private view returns (bytes32) { return keccak256(abi.encode(TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), block.chainid, address(this))); } function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); }

As a result decoded signer address will be different from the initial signer and won't have any tokens to delegate.

Tools Used

Manual review

Initialize EIP712Upgradeable https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/VerbsToken.sol#L130

function initialize( address _minter, address _initialOwner, address _descriptor, address _cultureIndex, IRevolutionBuilder.ERC721TokenParams memory _erc721TokenParams ) external initializer { require(msg.sender == address(manager), "Only manager can initialize"); require(_minter != address(0), "Minter cannot be zero address"); require(_initialOwner != address(0), "Initial owner cannot be zero address"); // Initialize the reentrancy guard __ReentrancyGuard_init(); // Setup ownable __Ownable_init(_initialOwner); // Initialize the ERC-721 token __ERC721_init(_erc721TokenParams.name, _erc721TokenParams.symbol); + __EIP712_init(_erc721TokenParams.name, "1"); _contractURIHash = _erc721TokenParams.contractURIHash; // Set the contracts minter = _minter; descriptor = IDescriptorMinimal(_descriptor); cultureIndex = ICultureIndex(_cultureIndex); }

Assessed type

en/de-code

#0 - c4-pre-sort

2023-12-21T23:59:41Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2023-12-21T23:59:45Z

raymondfam marked the issue as primary issue

#2 - raymondfam

2023-12-22T00:00:02Z

This should only concern CultureIndex.sol:

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

// Initialize EIP-712 support __EIP712_init(string.concat(_cultureIndexParams.name, " CultureIndex"), "1");

#3 - MarioPoneder

2024-01-06T22:56:16Z

Technically correct, but not impacting intended functionality of VerbsToken

#4 - c4-judge

2024-01-06T22:56:26Z

MarioPoneder changed the severity to QA (Quality Assurance)

#5 - c4-judge

2024-01-06T22:56:32Z

MarioPoneder marked the issue as grade-b

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