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: 17/110
Findings: 4
Award: $453.20
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: DanielArmstrong
Also found by: 0xDING99YA, MrPotatoMagic, SpicyMeatball, bart1e, mojito_auditor, nmirchev8, pep7siup
116.9139 USDC - $116.91
MaxHeap.sol
has a crucial role for the whole protocol as it stores and filters the voting for the pieces and it is very important to work correctlyextractMax()
we can notice something very suspicious:size
. We can notice that we actually haven't removed the last item from his previous place inside heap
and now we store the same piece id on two places, which is wrong.
Note that: major imapct that comes from this root would araise if we call updateValue
with new value, which is smaller than the previous, which would be the case if the protocol has functionality to downvote, which they play to implement in future. However the issue is regarding a data structure implementation, which is whole in the scope of the audit and it is confirmed and valuable for the sponsor:I have created detailed diagram showing the state of the heap after each operation:
Here is a coded PoC with detailed comments.
You can paste it inside /packages/revolution/test/max-heap/Updates.t.sol
, import import "forge-std/console.sol";
at the beginning of the file and run:
FOUNDRY_PROFILE=dev forge test --match-test "testUpdateValueExtractMaxDoesntEraseLastValue" -vv
at ./revolution
directory
Manual Review
function extractMax() external onlyAdmin returns (uint256, uint256) { require(size > 0, "Heap is empty"); uint256 popped = heap[0]; heap[0] = heap[--size]; + delete heap[size] + positionMapping[heap[0]] = 0 maxHeapify(0); return (popped, valueMapping[popped]); }
Context
#0 - c4-pre-sort
2023-12-22T16:06:19Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T16:06:42Z
raymondfam marked the issue as primary issue
#2 - c4-pre-sort
2023-12-24T04:16:26Z
raymondfam marked the issue as duplicate of #266
#3 - c4-judge
2024-01-06T12:28:13Z
MarioPoneder changed the severity to QA (Quality Assurance)
#4 - c4-judge
2024-01-07T16:45:13Z
MarioPoneder marked the issue as grade-b
#5 - c4-judge
2024-01-10T23:49:24Z
This previously downgraded issue has been upgraded by MarioPoneder
#6 - c4-judge
2024-01-10T23:54:06Z
MarioPoneder marked the issue as satisfactory
263.9929 USDC - $263.99
We can see that when a pice is created there is a validation for the provided mediaType
, but we should note that ArtPieceMetadata
has multiple string fields, which are never checked:
struct ArtPieceMetadata { string name; string description; MediaType mediaType; string image; string text; string animationUrl; }
On a later stage (if a piece has reached quorum and is most voted) all those values are copied inside VerbsToken
. This operation is crucial, because it is part of the flow to close an auction and start another, which is getting the most voted piece.
It is possible for a user maliciously or not to put a large string data when creating a piece, so later the fees are extremely high and unpayable. In the PoC section I have tested with a large text for description, which is 580 words (A normal amount if someone want to include a narrative or story to his art). In this case gas spent for only opening the auction is 3371079
, which for Ethereum Mainnet would cost:60 (gwei) * 3371079 = 202264740 ~ $400
, which is a lot and can be a lot larger, because currently there is no validation.
Note that also in testing environment we have an empty heap, which in real application with some time functionating would add more operations , as extracting and ordering heap is part of the flow
Here is a simple test, which you can run in any of files inside packages/revolution/test/auction
using FOUNDRY_PROFILE=dev forge test --match-test "testGasCostForLargeDescription" -vv
when you are inside ./revolution
Manual Review
uint256 public constant MAX_METADATA_FIELDS_L = 1000; function validateMetadata(ArtPieceMetadata calldata metadata) internal pure { uint256 allFieldsLength = metadata.name.length + metadata.description.length + metadata.image.length + metadata.text.length + metadata.animationUrl.length; require( allFieldsLength <= MAX_METADATA_FIELDS_L); }
DoS
#0 - c4-pre-sort
2023-12-22T03:13:17Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T03:13:26Z
raymondfam marked the issue as duplicate of #93
#2 - c4-pre-sort
2023-12-24T14:35:51Z
raymondfam marked the issue as duplicate of #195
#3 - MarioPoneder
2024-01-06T13:20:19Z
See comment on primary issue: https://github.com/code-423n4/2023-12-revolutionprotocol-findings/issues/195#issuecomment-1879684718
#4 - c4-judge
2024-01-06T13:20:24Z
MarioPoneder marked the issue as partial-25
#5 - c4-judge
2024-01-11T18:26:13Z
MarioPoneder marked the issue as not a duplicate
#6 - c4-judge
2024-01-11T18:52:08Z
MarioPoneder marked the issue as duplicate of #178
#7 - c4-judge
2024-01-11T18:52:12Z
MarioPoneder marked the issue as partial-50
🌟 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
reservePrice
and winning bid are close to zero and entropyRateBps
is high enough, settling an Auction can hit INVALID_ETH_AMOUNT
and block new auction from starting._settleAuction
=> buyToken
=> _handleRewardsAndGetValueToSend
=> computeTotalReward
Here is a gist with PoC. In a comment above the test I have provided the params for the configuration, which allow this revert to happen https://gist.github.com/NicolaMirchev/59e55575a166b89a78c0cc01b67c8aea
Manual Review
reservePrice
to 0.001 etherbuyTokens()
inside _settleAuction
:if (creatorsShare > ethPaidToCreators && (creatorShare - ethPaidToCreators) > 0.0000001 ether) { creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); }
DoS
#0 - c4-pre-sort
2023-12-22T15:45:37Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T15:45:47Z
raymondfam marked the issue as duplicate of #8
#2 - c4-pre-sort
2023-12-24T03:14:12Z
raymondfam marked the issue as duplicate of #160
#3 - c4-judge
2024-01-06T14:56:41Z
MarioPoneder changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-01-06T15:06:36Z
MarioPoneder marked the issue as satisfactory
42.484 USDC - $42.48
Observing the main and most important flow of revolution protocol (mint the winning NFT and start auction) we can immediately see the complexity of the nested functions and actions. Since we work in blockchain gas cost and execution is crucial as it costs money and in some situations it could result in DoS. Currently the flow consist of settling an auction, transferring rewards, minting erc20votes, extracting value from constantly increasing heap and minting new NFT with arbitrary string data. All this should happen in one transaction initiated inside settleCurrentAndCreateNewAuction
. But the worst part is the large tolerance of untrusted addresses, which are called, forwarding 50_000 gas.
Lets carefully observe the flow of settling an auction:
call
ERC20TokenEmitter::buyTokens
, which is also with great complexity, we again iterate over the creators array.Mainnet included
I have coded a PoC, where you can examine that the issue is actually bigger, as the whole flow could exceeded Mainnet gas limit, which would result in DoS. Even if admin pause
and call only settleAuction
, the gas used for the flow is > 30_000_000 which is mainned average gas limit.
MaliciousParticipant.sol
inside packages/revolution/test/auction/AuctionSettling.t.sol
FOUNDRY_PROFILE=dev forge test --match-test "testSpendGasAmountOnLargeCreatorsArray" -vv
for faster executionManual Review
DoS
#0 - c4-pre-sort
2023-12-22T01:16:07Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T01:16:28Z
raymondfam marked the issue as duplicate of #93
#2 - c4-pre-sort
2023-12-24T14:35:48Z
raymondfam marked the issue as duplicate of #195
#3 - c4-judge
2024-01-06T13:18:11Z
MarioPoneder changed the severity to 2 (Med Risk)
#4 - MarioPoneder
2024-01-06T13:18:37Z
See comment on primary issue: https://github.com/code-423n4/2023-12-revolutionprotocol-findings/issues/195#issuecomment-1879684718
#5 - c4-judge
2024-01-06T13:18:44Z
MarioPoneder marked the issue as partial-25
#6 - NicolaMirchev
2024-01-10T15:32:09Z
Hey, @MarioPoneder
#7 - c4-judge
2024-01-11T18:26:24Z
MarioPoneder marked the issue as not a duplicate
#8 - c4-judge
2024-01-11T18:46:56Z
MarioPoneder marked the issue as duplicate of #93
#9 - c4-judge
2024-01-11T18:47:17Z
MarioPoneder marked the issue as satisfactory