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
Rank: 40/110
Findings: 5
Award: $154.91
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: osmanozdemir1
Also found by: 0xCiphky, 0xDING99YA, 0xlemon, 0xluckhu, AS, Abdessamed, BARW, KupiaSec, MrPotatoMagic, SovaSlava, SpicyMeatball, ast3ros, bart1e, hakymulla, ktg, n1punp, plasmablocks, shaka, twcctop
44.0266 USDC - $44.03
Due to a flaw in the ERC20TokenEmitter
contract, a portion of ETH used to buy governance tokens becomes irretrievably stuck in the contract. This represents a financial loss to the protocol and its participants.
When purchasing tokens from the ERC20TokenEmitter contract, the incoming ETH is allocated to protocol rewards, treasury, and creators
// Get value left after protocol rewards uint256 msgValueRemaining = _handleRewardsAndGetValueToSend( msg.value, protocolRewardsRecipients.builder, protocolRewardsRecipients.purchaseReferral, protocolRewardsRecipients.deployer ); //Share of purchase amount to send to treasury uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000; //Share of purchase amount to reserve for creators //Ether directly sent to creators uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000;
However, when entropyRateBps
is greater than zero, not all of the remaining ETH is transferred to creators. Only a fraction is sent according to the following formula, leaving the remainder trapped in the contract.
uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000;
POC:
cd packages/revolution
test/EthStuck.t.sol
forge test -vvvvv --match-path test/EthStuck.t.sol --match-test testEthStuckInContract
// SPDX-License-Identifier: MIT pragma solidity 0.8.22; import { Test } from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; import { AuctionHouseTest } from "./auction/AuctionHouse.t.sol"; import { IERC20TokenEmitter } from "../../src/interfaces/IERC20TokenEmitter.sol"; contract POCTest is AuctionHouseTest { function testEthStuckInContract() public { erc20TokenEmitter.setCreatorRateBps(1000); erc20TokenEmitter.setEntropyRateBps(3000); uint256 buyAmount = 10 ether; vm.deal(address(21), buyAmount); address[] memory recipients = new address[](1); recipients[0] = address(1); uint256[] memory bps = new uint256[](1); bps[0] = 10_000; vm.stopPrank(); vm.prank(address(21)); erc20TokenEmitter.buyToken{ value: buyAmount }( recipients, bps, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); console.log("Contract balance: ", address(erc20TokenEmitter).balance); // 682500000000000000 // Total input amount 10 ETH // Reward split: 2.5% total => 10 * 2.5% = 0.25 ETH (1) // Remainning: 10 - 0.25 = 9.75 ETH // toPayTreasury = 9.75 * (1 - creatorRateBps) = 9.75 * (1 - 10%) = 8.775 ETH (2) // creatorDirectPayment = (9.75 - 8.775) * entropyRateBps = (9.75 - 8.775) * 30% = 0.2925 ETH (3) // ETH left in the ERC20TokenEmitter contract: total - (1) - (2) - (3) = 10 - 0.25 - 8.775 - 0.2925 = 0.6825 ETH assertEq(address(erc20TokenEmitter).balance, 682500000000000000); } }
Manual
Adjust the contract logic to handle cases where entropyRateBps
is greater than zero. Specifically, the remaining ETH after creator payments should be transferred to the treasury
account. This ensures that no ETH is left stranded in the contract.
// Get value left after protocol rewards uint256 msgValueRemaining = _handleRewardsAndGetValueToSend( msg.value, protocolRewardsRecipients.builder, protocolRewardsRecipients.purchaseReferral, protocolRewardsRecipients.deployer ); //Share of purchase amount to send to treasury uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000; //Share of purchase amount to reserve for creators //Ether directly sent to creators uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000; + if (entropyRateBps > 0) { + toPayTreasury += (msgValueRemaining - toPayTreasury - creatorDirectPayment) + }
Error
#0 - c4-pre-sort
2023-12-22T16:40:01Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T16:40:16Z
raymondfam marked the issue as duplicate of #13
#2 - c4-pre-sort
2023-12-24T02:54:21Z
raymondfam marked the issue as high quality report
#3 - c4-pre-sort
2023-12-24T02:54:26Z
raymondfam marked the issue as not a duplicate
#4 - c4-pre-sort
2023-12-24T02:54:30Z
raymondfam marked the issue as primary issue
#5 - c4-sponsor
2024-01-04T23:28:44Z
rocketman-21 (sponsor) confirmed
#6 - c4-judge
2024-01-05T23:05:15Z
MarioPoneder marked the issue as satisfactory
#7 - c4-judge
2024-01-05T23:22:24Z
MarioPoneder marked issue #210 as primary and marked this issue as a duplicate of 210
🌟 Selected for report: deth
Also found by: 00xSEV, 0xCiphky, 0xHelium, ABAIKUNANBAEV, Aamir, AkshaySrivastav, King_, Pechenite, SpicyMeatball, Tricko, ast3ros, ayden, bart1e, deth, fnanni, ke1caM, mahdirostami, peanuts, pep7siup, pontifex, ptsanev, roland, rvierdiiev, y4y
7.2215 USDC - $7.22
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/08ff070da420e95d7c7ddf9d068cbf54433101c4/packages/revolution/src/CultureIndex.sol#L313-L319 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/08ff070da420e95d7c7ddf9d068cbf54433101c4/packages/revolution/src/CultureIndex.sol#L322
There's a systemic bias against art pieces created earlier in the protocol's lifecycle. Due to the voting mechanism's design, these pieces may never be selected for auction, representing an inherent unfairness to early creators.
The voting power for an art piece is fixed at its creation time, based on the total voting power available in the system at that moment. As the protocol evolves and more voting tokens are minted or bought, later-created pieces inherently have a larger pool of potential votes.
uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock); require(weight > minVoteWeight, "Weight must be greater than minVoteWeight"); votes[pieceId][voter] = Vote(voter, weight); totalVoteWeights[pieceId] += weight; uint256 totalWeight = totalVoteWeights[pieceId];
However, the maxHeap uses the absolute amount of vote weight to consider which pieceId has the highest vote.
maxHeap.updateValue(pieceId, totalWeight);
The maxHeap
structure uses the absolute amount of vote weight to determine the art piece with the highest votes. However, the total voting power in the system increases over time, allowing newer pieces to amass more votes than earlier ones.
A voter can easily forever block an old art piece from selected by voting for a new piece with the amount more than total potential received voting power of the old one.
Consider Scenario:
POC: In this POC, we have art piece id 1 blocked by art piece id 3. Art piece 1 is created at start then the total possible vote weight is 0. Art piece id 3 has 10e18
vote and block art piece 1 forever from being selected.
// SPDX-License-Identifier: MIT pragma solidity 0.8.22; import { Test } from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; import { AuctionHouseTest } from "./auction/AuctionHouse.t.sol"; import { ICultureIndex, ICultureIndexEvents } from "../../src/interfaces/ICultureIndex.sol"; contract POCTest is AuctionHouseTest { function testBlockArtPiece() public { // Setup for first auction with 2 art piece ID 0 and 1 and 2 uint256 verbId0 = createDefaultArtPiece(); uint256 verbId1 = createArtPiece( "Mona Lisa 1", "A real masterpiece", ICultureIndex.MediaType.IMAGE, "ipfs://legends", "", "", address(0x2), 10000 ); uint256 verbId2 = createArtPiece( "Mona Lisa 2", "A fake masterpiece", ICultureIndex.MediaType.IMAGE, "ipfs://legends", "", "", address(0x3), 10000 ); (,,,,,,,uint256 totalVotesSupply)= cultureIndex.pieces(0); console.log("total vote supply of piece 1: ", totalVotesSupply); (,,,,,,,uint256 totalVotesSupply2)= cultureIndex.pieces(2); console.log("total vote supply of piece 2: ", totalVotesSupply2); vm.roll(block.number + 1); auction.unpause(); // First auction start uint256 bidAmount = 100 ether; vm.deal(address(21), bidAmount + 2 ether); vm.stopPrank(); vm.startPrank(address(21)); auction.createBid{ value: bidAmount }(0, address(21)); vm.stopPrank(); (, , , uint256 endTime, , ) = auction.auction(); vm.warp(endTime); auction.settleCurrentAndCreateNewAuction(); vm.warp(endTime + 20); vm.roll(block.number + 20); // First auction end with settlement for id 0. ID 2 is selected to the next auction. ID 1 move to the root of the maxHeap as max value uint256 verbId3 = createArtPiece( "Mona Lisa 2", "A fake masterpiece", ICultureIndex.MediaType.IMAGE, "ipfs://legends", "", "", address(0x3), 10000 ); vm.roll(block.number + 1); vm.startPrank(address(21)); // Cannot vote for art piece 1 because the vote at block number 1 is 0 console.log("Past vote at block number 1 for address 21: ", cultureIndex.getPastVotes(address(21), 1)); // 0 vm.expectRevert("Weight must be greater than minVoteWeight"); cultureIndex.vote(1); // However, voter can vote for art piece 3 because art piece 3 is created at block 22, and the vote at block 22 of user is more than 0 cultureIndex.vote(3); vm.stopPrank(); uint256 totalVoteWeights1= cultureIndex.totalVoteWeights(1); console.log("totalVoteWeights1: ", totalVoteWeights1); // 0 Vote uint256 totalVoteWeights3= cultureIndex.totalVoteWeights(3); console.log("totalVoteWeights3: ", totalVoteWeights3); // 10e18 vote // Art piece id 3 is the max item max Heap. (uint256 maxItemId, uint256 maxValue) = maxHeap.getMax(); console.log("maxItemId: ", maxItemId); // maxItemId = 3 console.log("maxValue: ", maxValue); // maxValue = 10e18 } }
Manual
Consider allowing creators to "refresh" their art pieces in the maxHeap
. This would enable them to reinsert their pieces into the voting pool, giving them access to the increased voting power available later in the protocol's lifecycle.
DoS
#0 - c4-pre-sort
2023-12-22T16:50:15Z
raymondfam marked the issue as duplicate of #16
#1 - c4-pre-sort
2023-12-22T16:50:20Z
raymondfam marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-12-24T15:11:16Z
raymondfam marked the issue as duplicate of #449
#3 - c4-judge
2024-01-06T16:02:25Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: ast3ros
Also found by: 0xG0P1, KupiaSec, Pechenite, cccz, deth, dimulski, mojito_auditor, osmanozdemir1, peanuts, rvierdiiev, zhaojie
66.4796 USDC - $66.48
This vulnerability allows for the minting and auctioning of an art piece that has not met the required quorum. It enables malicious voters to influence outcomes with fewer votes than what is stipulated by the protocol. This undermines a key invariant of the protocol:
An art piece that has not met quorum cannot be dropped.
The quorumVotes
for an art piece are calculated at its creation as a fraction of the totalVotesSupply
, which depends on the total supply of erc20VotingToken
and erc721VotingToken
:
(quorumVotesBPS * newPiece.totalVotesSupply) / 10_000.
File: src/CultureIndex.sol 209: function createPiece( 210: ArtPieceMetadata calldata metadata, 211: CreatorBps[] calldata creatorArray 212: ) public returns (uint256) { 213: uint256 creatorArrayLength = validateCreatorsArray(creatorArray); 214: 215: // Validate the media type and associated data 216: validateMediaType(metadata); 217: 218: uint256 pieceId = _currentPieceId++; 219: 220: /// @dev Insert the new piece into the max heap 221: maxHeap.insert(pieceId, 0); 222: 223: ArtPiece storage newPiece = pieces[pieceId]; 224: 225: newPiece.pieceId = pieceId; 226: newPiece.totalVotesSupply = _calculateVoteWeight( 227: erc20VotingToken.totalSupply(), 228: erc721VotingToken.totalSupply() 229: ); 230: newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); 231: newPiece.metadata = metadata; 232: newPiece.sponsor = msg.sender; 233: newPiece.creationBlock = block.number; 234: newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000;
The totalVotesSupply
is calculated using total supply of erc20VotingToken
and erc721VotingToken
at the time the piece is created. It intends to calculate all the voting power that can vote for this art piece.
File: src/CultureIndex.sol 284: function _calculateVoteWeight(uint256 erc20Balance, uint256 erc721Balance) internal view returns (uint256) { 285: return erc20Balance + (erc721Balance * erc721VotingTokenWeight * 1e18); 286: }
The vulnerability arises because the totalVotesSupply
is computed based on the token supplies at the time of art piece creation. However, due to the block-based clock mode in the vote checkpoint, the total supplies of erc20VotingToken
and erc721VotingToken
can increase within the same block, resulting in an underestimation of totalVotesSupply
and consequently, quorumVotes
.
File: src/base/VotesUpgradeable.sol 85: /** 86: * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based 87: * checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match. 88: */ 89: function clock() public view virtual returns (uint48) { 90: return Time.blockNumber(); 91: }
Possible attack scenarios include:
POC: This POC demonstrates the first case when a voter back-run the createPiece
transaction to understate the totalVotesSupply
and quorumVotes
.
cd packages/revolution
test/BypassQuorum.t.sol
forge test -vvvvv --match-path test/BypassQuorum.t.sol --match-test testBypassquorumVotes
// SPDX-License-Identifier: MIT pragma solidity 0.8.22; import { Test } from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; import { AuctionHouseTest } from "./auction/AuctionHouse.t.sol"; import { IERC20TokenEmitter } from "../../src/interfaces/IERC20TokenEmitter.sol"; contract POCTest is AuctionHouseTest { function testBypassquorumVotes() public { uint256 verbId0 = createDefaultArtPiece(); (,,,,uint256 creationBlock ,uint256 quorumVotes,,uint256 totalVotesSupply)= cultureIndex.pieces(0); console.log("creationBlock: ", creationBlock); // creationBlock: 1 console.log("quorumVotes: ", quorumVotes); // quorumVotes: 0 console.log("totalVotesSupply: ", totalVotesSupply); // totalVotesSupply: 0 // Voter back-run the createPiece transaction and buy vote tokens in the same block, the supply is not reflected in the piece info and the quorum is understated at 0 uint256 buyAmount = 100 ether; vm.deal(address(21), buyAmount); address[] memory recipients = new address[](1); recipients[0] = address(1); uint256[] memory bps = new uint256[](1); bps[0] = 10_000; vm.stopPrank(); vm.prank(address(21)); erc20TokenEmitter.buyToken{ value: buyAmount }( recipients, bps, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); console.log("Should be quorum: ", cultureIndex.quorumVotes()); // Should be quorum: 1940052234587701020 } }
Manual
When the piece is created, only store the creationBlock
. The quorum should not be stored. It should be calculated directly using VotesUpgradeable.getPastTotalSupply
, this will return the value at the end of the corresponding block.
It ensures that quorumVotes
accurately reflects the voting power at the end of the block in which the art piece was created, thereby mitigating the risk of quorum bypass through token supply manipulation within the same block.
function dropTopVotedPiece() public nonReentrant returns (ArtPiece memory) { require(msg.sender == dropperAdmin, "Only dropper can drop pieces"); ICultureIndex.ArtPiece memory piece = getTopVotedPiece(); - require(totalVoteWeights[piece.pieceId] >= piece.quorumVotes, "Does not meet quorum votes to be dropped."); + uint256 totalVotesSupply = _calculateVoteWeight( + erc20VotingToken.getPastTotalSupply(piece.creationBlock), + erc721VotingToken.getPastTotalSupply(piece.creationBlock) + ); + uint256 quorumVotes = (quorumVotesBPS * totalVotesSupply) / 10_000; + require(totalVoteWeights[piece.pieceId] >= quorumVotes, "Does not meet quorum votes to be dropped.");
Governance
#0 - c4-pre-sort
2023-12-22T16:43:39Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T16:44:35Z
raymondfam marked the issue as duplicate of #260
#2 - c4-pre-sort
2023-12-24T05:39:10Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-12-24T05:39:22Z
raymondfam marked the issue as primary issue
#4 - c4-pre-sort
2023-12-24T05:39:31Z
raymondfam marked the issue as high quality report
#5 - c4-sponsor
2024-01-04T23:36:16Z
rocketman-21 (sponsor) confirmed
#6 - c4-judge
2024-01-05T22:35:35Z
MarioPoneder marked the issue as satisfactory
#7 - c4-judge
2024-01-05T22:57:44Z
MarioPoneder marked the issue as selected for report
🌟 Selected for report: ayden
Also found by: 0xCiphky, AS, Brenzee, Inference, KupiaSec, SpicyMeatball, ast3ros, ayden, fnanni, hals, ktg, mahdirostami, nmirchev8, rvierdiiev, wangxx2026
29.8238 USDC - $29.82
When entropyRateBps
is set to 0 in the AuctionHouse
contract, the _settleAuction
function will always revert. This results in the inability of the auction house to settle auctions, effectively causing a Denial of Service (DoS) for this critical functionality. Consequently, the bid winner is unable to receive the NFT.
In the auctionHouse
contract, entropyRateBps
determines the proportion of (auction proceeds * creatorRate) that is allocated to the creator in ether, expressed in basis points.
File: src/AuctionHouse.sol 248: /** 249: * @notice Set the split of (auction proceeds * creatorRate) that is sent to the creator as ether in basis points. 250: * @dev Only callable by the owner. 251: * @param _entropyRateBps New entropy rate in basis points. 252: */ 253: function setEntropyRateBps(uint256 _entropyRateBps) external onlyOwner { 254: require(_entropyRateBps <= 10_000, "Entropy rate must be less than or equal to 10_000"); 255: 256: entropyRateBps = _entropyRateBps; 257: emit EntropyRateBpsUpdated(_entropyRateBps); 258: }
The contract allows for entropyRateBps
to be zero, resulting in the entire auction proceeds being used to purchase governance tokens, with no direct ether transfer to the creators.
uint256 ethPaidToCreators = 0; //Transfer creator's share to the creator, for each creator, and build arrays for erc20TokenEmitter.buyToken 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; //Calculate paymentAmount for specific creator based on BPS splits - same as multiplying by creatorDirectPayment uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000); ethPaidToCreators += paymentAmount; //Transfer creator's share to the creator _safeTransferETHWithFallback(creator.creator, paymentAmount); } } //Buy token from ERC20TokenEmitter for all the creators if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); }
With entropyRateBps == 0
, the first conditional block (if (creatorsShare > 0 && entropyRateBps > 0
)) is bypassed, leading to the potential issue in the subsequent block (if (creatorsShare > ethPaidToCreators)
). Here, the vrgdaReceivers
and vrgdaSplits
arrays are not constructed, causing the erc20TokenEmitter.buyToken external call to revert.
POC:
cd packages/revolution
test/EntropyRateZero.t.sol
forge test -vvvvv --match-path test/EntropyRateZero.t.sol --match-test testAuctionHouseRevertWhenEntropyRateBpsZero
// SPDX-License-Identifier: MIT pragma solidity 0.8.22; import { Test } from "forge-std/Test.sol"; import { AuctionHouseTest } from "./auction/AuctionHouse.t.sol"; contract POCTest is AuctionHouseTest { function testAuctionHouseRevertWhenEntropyRateBpsZero() public { // Set entropy to 0 auction.setEntropyRateBps(0); uint256 verbId = createDefaultArtPiece(); auction.unpause(); uint256 bidAmount = 100 ether; vm.deal(address(1), bidAmount + 2 ether); vm.stopPrank(); vm.startPrank(address(1)); auction.createBid{ value: bidAmount }(0, address(21)); (, , , uint256 endTime, , ) = auction.auction(); vm.warp(endTime); // Function is reverted because receiver address is 0 vm.expectRevert(); // Error: ERC20InvalidReceiver(0x0000000000000000000000000000000000000000) auction.settleCurrentAndCreateNewAuction(); } }
Manual
Handle the case when EntropyRateBps is 0 explicitly in the contract logic to ensures that even when entropyRateBps
is 0, the vrgdaReceivers
and vrgdaSplits
arrays are properly populated:
//Transfer creator's share to the creator, for each creator, and build arrays for erc20TokenEmitter.buyToken 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; //Calculate paymentAmount for specific creator based on BPS splits - same as multiplying by creatorDirectPayment uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000); ethPaidToCreators += paymentAmount; //Transfer creator's share to the creator _safeTransferETHWithFallback(creator.creator, paymentAmount); } } + + if (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; + } + } + //Buy token from ERC20TokenEmitter for all the creators if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); }
Error
#0 - c4-pre-sort
2023-12-22T16:41:44Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T16:42:56Z
raymondfam marked the issue as duplicate of #335
#2 - c4-judge
2024-01-06T01:25:32Z
MarioPoneder changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-01-06T15:11:22Z
This previously downgraded issue has been upgraded by MarioPoneder
#4 - c4-judge
2024-01-06T15:12:25Z
MarioPoneder marked the issue as duplicate of #160
#5 - c4-judge
2024-01-06T15:13:38Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: 0xmystery
Also found by: 00xSEV, 0xCiphky, 0xDING99YA, 0xhitman, ABAIKUNANBAEV, Aamir, BARW, IllIllI, King_, MrPotatoMagic, Pechenite, SovaSlava, SpicyMeatball, Topmark, Ward, ZanyBonzy, ast3ros, bart1e, cheatc0d3, deth, developerjordy, hals, imare, kaveyjoe, ktg, leegh, osmanozdemir1, peanuts, roland, rvierdiiev, shaka, sivanesh_808, spacelord47
7.359 USDC - $7.36
The current implementation of maxHeap introduces a bias in art piece selection, as elements in the left branch are favored when two nodes have identical values. This issue arises during the maxHeapify process, where the left child is preferred over the right, regardless of their insertion order.
It means that the art piece in the left branch will always be priortize in selection if two art pieces have the same voting amount even though the left one is inserted later than the right one.
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; if (posValue < leftValue || posValue < rightValue) { if (leftValue > rightValue) { swap(pos, left); maxHeapify(left); } else { swap(pos, right); maxHeapify(right); } } }
POC: Put the test in the MaxHeap test: packages/revolution/test/max-heap/MaxHeap.t.sol
function testLeftPriorityOverRight() public { maxHeap.insert(0, 17); maxHeap.insert(1, 11); maxHeap.insert(2, 16); maxHeap.insert(3, 12); maxHeap.insert(4, 13); maxHeap.insert(5, 16); (uint256 maxItemId, uint256 maxValue) = maxHeap.extractMax(); console.log("maxItemId: ", maxItemId); console.log("maxValue: ", maxValue); // the item id 5 is the maximum over item id 2 even though item id 2 is inserted much earlier (maxItemId, maxValue) = maxHeap.getMax(); console.log("maxItemId: ", maxItemId); console.log("maxValue: ", maxValue); }
Document this unexpected behavior for users so they can aware and does not feel unfair.
The fixed gas limit of 50,000 in the _safeTransferETHWithFallback function lacks flexibility and may not align with future Ethereum network changes. A rigid gas limit can lead to failed transactions if the required gas exceeds this threshold due to change in Opcodes gas.
assembly { // Transfer ETH to the recipient // Limit the call to 50,000 gas success := call(50000, _to, _amount, 0, 0, 0, 0) }
Allow admin to update the gas limit.
When creating a new piece, the requirements for metadata
are they must include name, description, and image. However, there is no input validation to make sure that the name, description and image are provided.
This oversight could result in incomplete or incorrect art piece registrations, as it currently doesn't enforce the presence of essential fields.
function createPiece( ArtPieceMetadata calldata metadata, CreatorBps[] calldata creatorArray ) public returns (uint256) { uint256 creatorArrayLength = validateCreatorsArray(creatorArray); // Validate the media type and associated data validateMediaType(metadata);
Add name, description and image validation in createPiece
function.
The clockmode in voting calculation is defaulted to block.number. However, there can be potential issue when the block.number is determined differently in Optimism.
function CLOCK_MODE() public view virtual returns (string memory) { // Check that the clock was not modified if (clock() != Time.blockNumber()) { revert ERC6372InconsistentClock(); } return "mode=blocknumber&from=default"; }
Similar issue is reported: https://github.com/code-423n4/2023-06-lybra-findings/issues/114
The Natspec documentation for the getPastVotes
function is misleading and incorrect. It currently describes the functionality of a different function (getVotes
), which can cause confusion for developers and users of the code.
/** * @notice Returns the voting power of a voter at the current block. * @param account The address of the voter. * @return The voting power of the voter. */ function getPastVotes(address account, uint256 blockNumber) external view override returns (uint256) { return _getPastVotes(account, blockNumber); }
Update the Natspec comments to accurately reflect the purpose and behavior of the getPastVotes
function.
#0 - c4-pre-sort
2023-12-24T17:27:38Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-01-07T19:45:39Z
MarioPoneder marked the issue as grade-b