Revolution Protocol - 0xG0P1'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: 15/110

Findings: 2

Award: $546.01

🌟 Selected for report: 0

🚀 Solo Findings: 0

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/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L228 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/AuctionHouse.sol#L313

Vulnerability details

Impact

The calculation for quorum votes will be incorrect in this case.

Proof of Concept

Assuming the following scenario:

  1. An ongoing auction where bidders are actively bidding for an NFT with 2 days remaining.
  2. Simultaneously, an art piece is created. Let's consider ERC20 totalSupply as 1000 tokens and ERC721 totalSupply as 10.

Assume erc721VotingTokenWeight is set to 20. The voting weight is calculated as erc20Balance + (erc721Balance * erc721VotingTokenWeight * 1e18), resulting in a value 'X' (e.g., 1000 + (10 * 20 * 1e18)).

Now, this 'X' is used to calculate quorum votes with the formula (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000. However, this approach is flawed. The ongoing auction holds a VerbToken each time, and these tokens are owned by the auction contract. Since the auction contract cannot execute the vote function in the CultureIndex contract, it disrupts the voting process. In the worst-case scenario, quorum votes might exceed the votes held by the voters. The actual voting weight should be calculated as erc20Balance + ((erc721Balance - 1) * erc721VotingTokenWeight * 1e18),

To address this issue, if an auction is in progress, the VerbTokens held by the Auction contract should be excluded when calculating quorum votes. This adjustment ensures a more accurate representation of voting power, preventing potential issues where quorum votes surpass the actual votes held by voters.

Tools Used

Manual review

When the auction is ongoing, exclude the VerbTokens held by the Auction contract from the calculation of the voting weight (_calculateVoteWeight( erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply() -1 );).

Assessed type

Timing

#0 - c4-pre-sort

2023-12-22T17:30:39Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T17:30:48Z

raymondfam marked the issue as duplicate of #18

#2 - c4-judge

2024-01-08T21:43:04Z

MarioPoneder marked the issue as duplicate of #409

#3 - c4-judge

2024-01-08T21:45:46Z

MarioPoneder marked the issue as unsatisfactory: Insufficient quality

#4 - c4-judge

2024-01-08T21:46:46Z

MarioPoneder marked the issue as satisfactory

#5 - c4-judge

2024-01-10T18:29:12Z

MarioPoneder marked the issue as not a duplicate

#6 - c4-judge

2024-01-10T18:29:22Z

MarioPoneder marked the issue as duplicate of #168

#7 - c4-judge

2024-01-10T18:32:36Z

MarioPoneder changed the severity to 3 (High 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
edited-by-warden
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#L234 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L284-L286 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L313

Vulnerability details

Impact

If someone mints tokens after an art piece is created, they can still vote for that piece.

Proof of Concept

Imagine the following scenario:

At block number X:

  1. Bob mints 1000 tokens (ERC20 only, no ERC721 for simplicity).

At block number X + 1:

  1. Bob mints an additional 1000 tokens. Now, Bob's total votes are 2000, and ERC20's total supply is 2000.
  2. An art piece is created. Piece.quorumVotes are calculated as follows: (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000, where totalVotesSupply = _calculateVoteWeight(erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply()). The quorum votes are calculated from the token total supply at the time of piece creation.
  3. Bob mints 1000 more tokens in the same block that the piece is created.

In this scenario, although Bob's voting weight should be 2000 (up to piece creation), Bob's votes will be 3000. The vulnerability lies in the _getPastVotes() function called when the vote function is executed. The internal _vote function calculates the voting weight as follows: uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock);. The issue is that _getPastVotes() returns the value at the end of the corresponding block (X + 1). Consequently, the quorum votes are calculated corresponding to Bob's 2000 votes (total supply at the time of piece creation), while the actual votes eligible for voting are 3000.

Test:

function testCreatePiece() public {
    address bob = address(5);
    
    vm.stopPrank();
    vm.startPrank(address(erc20TokenEmitter)); 
    erc20Token.mint(bob, 1000); // Minting 1000 tokens at X block
    vm.stopPrank();
    vm.roll(block.number + 1); // X + 1 block
    vm.startPrank(address(erc20TokenEmitter)); 
    erc20Token.mint(bob, 1000); // Minting another 1000 tokens at X + 1 block
    vm.stopPrank();
    uint256 newPieceId = createArtPiece( // Art piece created at X + 1 block
        "Mona Lisa",
        "A masterpiece",
        ICultureIndex.MediaType.IMAGE,
        "ipfs://legends",
        "",
        "",
        address(0x1),
        10000
    );
    vm.startPrank(address(erc20TokenEmitter)); // Minting another 1000 tokens at X + 1 block
    erc20Token.mint(bob, 1000);
    vm.stopPrank();
    vm.roll(block.number + 2); // X + 2 block for voting
    vm.startPrank(bob);
    cultureIndex.vote(newPieceId); // Bob voting for the Piece
    vm.stopPrank();

    // Validate that the piece was created with correct data
    ICultureIndex.ArtPiece memory createdPiece = cultureIndex.getPieceById(newPieceId);

    assertEq(createdPiece.pieceId, newPieceId);
    assertEq(createdPiece.metadata.name, "Mona Lisa");
    assertEq(createdPiece.metadata.description, "A masterpiece");
    assertEq(createdPiece.metadata.image, "ipfs://legends");
    assertEq(createdPiece.creators[0].creator, address(0x1));
    assertEq(createdPiece.creators[0].bps, 10000);
    uint Total = cultureIndex.totalVoteWeights(newPieceId); // Calculating the Total votes weight of the Piece
    assertEq(Total, 3000); // Asserting with Bob's Votes
}

Results:

Running 14 tests for test/culture-index/ArtPiece.t.sol:CultureIndexArtPieceTest [PASS] testArtPieceCreationAndVoting(uint256,uint256) (runs: 256, μ: 1703438, ~: 1718588) [PASS] testCreatePiece() (gas: 777916) //Passing Test [PASS] testCreatePieceWithMultipleCreators() (gas: 420749) [PASS] testCreatePieceWithMultipleCreatorsInvalidBPS() (gas: 20428) [PASS] testCreatorArrayLengthConstraint() (gas: 101011) [PASS] testExcessiveTotalBasisPoints() (gas: 19611) [PASS] testFirstPieceId() (gas: 347058) [PASS] testInvalidCreatorAddress() (gas: 18839) [PASS] testInvalidMediaType() (gas: 19193) [PASS] testMissingMediaDataAnimation() (gas: 20955) [PASS] testMissingMediaDataImage() (gas: 20731) [PASS] testMissingMediaDataText() (gas: 21069) [PASS] testPieceIDIncrement() (gas: 664986) [PASS] testTooFewTotalBasisPoints() (gas: 19083) Test result: ok. 14 passed; 0 failed; 0 skipped; finished in 1.32s

Tools Used

Manual Review and Foundry

Replace block.number with block.timestamp to record the creation time of an art piece instead of its block number. Then, retrieve past votes that occurred around the timestamp when the art piece was created .

Assessed type

Timing

#0 - c4-pre-sort

2023-12-22T15:40:47Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T15:42:00Z

raymondfam marked the issue as duplicate of #260

#2 - c4-pre-sort

2023-12-24T05:39:59Z

raymondfam marked the issue as duplicate of #409

#3 - c4-judge

2024-01-05T22:38:47Z

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