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: 38/110
Findings: 3
Award: $170.72
π Selected for report: 0
π Solo Findings: 0
π Selected for report: jnforja
Also found by: 0x175, 0xCiphky, 0xDING99YA, 0xmystery, ArmedGoose, Aymen0909, Franklin, KupiaSec, McToady, MrPotatoMagic, Ocean_Sky, PNS, Pechenite, TermoHash, Topmark, _eperezok, alexbabits, deth, hals, imare, jeff, ktg, leegh, mahdirostami, marqymarq10, mojito_auditor, neocrao, ptsanev, twcctop, zraxx
2.6741 USDC - $2.67
In the AuctionHouse contract, a reservePrice
limit is set for the final price of any auction. If the last bid does not meet this reservePrice
, the auction is considered unsuccessful and the Verb token is burnt. This behavior is checked and executed in the function _settleAuction()
.
// Check if contract balance is greater than reserve price // @audit this could by bypass by transferring ETH to this contract directly using self-destruct if (address(this).balance < reservePrice) { // If contract balance is less than reserve price, refund to the last bidder if (_auction.bidder != address(0)) { _safeTransferETHWithFallback(_auction.bidder, _auction.amount); } // And then burn the Noun verbs.burn(_auction.verbId); }
However, the check uses the current contract balance, which could be easily bypassed by transferring ETH directly to the contract using self-destruct.
Even though the AuctionHouse does not have an external receive function, an attacker can still use self-destruct on another contract and refund the ETH to the AuctionHouse contract. More information about self-destruct can be found at:
https://solidity-by-example.org/hacks/self-destruct/
Manual Review
Consider checking the reservePrice
against the last bid instead of the current contract balance.
Invalid Validation
#0 - c4-pre-sort
2023-12-22T19:21:38Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-12-22T19:21:49Z
raymondfam marked the issue as duplicate of #72
#2 - c4-pre-sort
2023-12-22T23:20:06Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-12-22T23:20:11Z
raymondfam marked the issue as sufficient quality report
#4 - c4-pre-sort
2023-12-22T23:20:19Z
raymondfam marked the issue as duplicate of #515
#5 - c4-judge
2024-01-05T22:06:41Z
MarioPoneder changed the severity to 3 (High Risk)
#6 - c4-judge
2024-01-05T22:07:56Z
MarioPoneder marked the issue as satisfactory
#7 - c4-judge
2024-01-11T18:03:12Z
MarioPoneder changed the severity to 2 (Med Risk)
π Selected for report: ast3ros
Also found by: 0xG0P1, KupiaSec, Pechenite, cccz, deth, dimulski, mojito_auditor, osmanozdemir1, peanuts, rvierdiiev, zhaojie
51.1381 USDC - $51.14
In CultureIndex, the createPiece()
function records the current vote and ERC20 supplies. It also sets newPiece.creationBlock = block.number
.
newPiece.totalVotesSupply = _calculateVoteWeight( erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply() ); newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); newPiece.metadata = metadata; newPiece.sponsor = msg.sender; newPiece.creationBlock = block.number;
When voting for a piece, the voting power is obtained at the creationBlock
. However, this may cause a mismatch between totalVotesSupply
and totalERC20Supply
. This is because additional transactions within the same block as createPiece()
could alter the ERC20Supply.
Consider this scenario:
createPiece()
is called with erc20VotingToken.totalSupply() = 1000
. This results in totalERC20Supply = 1000
.createPiece()
transaction, another transaction buyToken()
is made to buy a large amount of voting token, which will increase erc20VotingToken.totalSupply() = 5000
.totalERC20Supply
and totalVotesSupply
incorrect. Although voting supply and ERC20 supply are calculated with erc20VotingToken.totalSupply() = 1000
, the actual ERC20 supply that is allowed to vote will be 5000
.Manual Review
Consider getting the voting power at creationBlock - 1
instead when casting a vote for an art piece.
Other
#0 - c4-pre-sort
2023-12-22T19:25:50Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T19:26:26Z
raymondfam marked the issue as duplicate of #260
#2 - c4-pre-sort
2023-12-24T05:40:01Z
raymondfam marked the issue as duplicate of #409
#3 - c4-judge
2024-01-05T22:39:10Z
MarioPoneder marked the issue as satisfactory
π Selected for report: DanielArmstrong
Also found by: 0xDING99YA, MrPotatoMagic, SpicyMeatball, bart1e, mojito_auditor, nmirchev8, pep7siup
116.9139 USDC - $116.91
In the MaxHeap contract, the maxHeapify()
function is used to reheapify the heap to maintain the heap property. Starting from a given position in the heap, it gets the values of the left and right children. If a child's value is greater than the current position's value, they are swapped to maintain the heap's structure.
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; // @audit not check if `right` node is exist or not (could be a node has been removed earlier) if (posValue < leftValue || posValue < rightValue) { if (leftValue > rightValue) { swap(pos, left); maxHeapify(left); } else { swap(pos, right); maxHeapify(right); } } }
The problem is that there may be cases where the right child does not exist but contains the value of a previously removed node. If the removed node has a larger value than the current node, the removed node will be swapped back into the heap. This breaks the heap's structure and could return the incorrect maximum value when queried.
Please copy this test to Updates.t.sol
and also import console.sol
.
function testMaxHeapify() public { // Modified from testUpdateValueDecrease() maxHeapTester.insert(1, 20); maxHeapTester.insert(2, 20); maxHeapTester.insert(3, 20); maxHeapTester.extractMax(); maxHeapTester.extractMax(); maxHeapTester.extractMax(); // At this moment, the heap is empty. maxHeapTester.insert(4, 10); maxHeapTester.insert(5, 10); // Trigger maxHeapify() maxHeapTester.updateValue(4, 7); (uint256 itemId, uint256 value) = maxHeapTester.getMax(); // Output // Item ID: 3 // Value: 20 console.log("Item ID: ", itemId); console.log("Value: ", value); }
Manual Review
Check if the right node exists before making comparisons.
Other
#0 - c4-pre-sort
2023-12-22T19:25:01Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T19:25:04Z
raymondfam marked the issue as primary issue
#2 - c4-sponsor
2024-01-04T23:36:57Z
rocketman-21 (sponsor) confirmed
#3 - c4-sponsor
2024-01-04T23:37:03Z
rocketman-21 marked the issue as disagree with severity
#4 - rocketman-21
2024-01-04T23:37:28Z
as stated elsewhere, we never allowed updating values of the heap downwards, so this wouldn't have been an issue in the current protocol
nonetheless valid
#5 - MarioPoneder
2024-01-05T22:30:54Z
@rocketman-21 Could you please elaborate further on that?
It seems to me after a first review that downwards heapifying is possible in the protocol.
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 }
#6 - rocketman-21
2024-01-06T19:15:11Z
I meant that in all cases where we used the updateValue function (in the CultureIndex) in the protocol, it only allowed updateValue
with a newValue greater than the previous.
The maxHeap in isolation was definitely broken, but we were not using it in the CultureIndex in a way it would've been exploited.
I'm fine with whatever severity you want to make it
#7 - MarioPoneder
2024-01-06T22:32:51Z
This is also a tough one, as all other issues around MaxHeap
. First of all I genuinely appreciate the efforts put into this report and many of its duplicates, clearly an underlying bug is shown.
However, no impacts on the function of protocol as a whole are shown. Therefore, it's hard to justify High/Medium
severity for this group of findings.
I have to downgrade to QA for now, but invite the author of this submission or its duplicates to provide a runnable PoC during PJQA which shows impacts through the main entry points of the protocol, i.e. not purely interacting with the MaxHeap
contract.
Thank you for your understanding!
#8 - c4-judge
2024-01-06T22:33:03Z
MarioPoneder changed the severity to QA (Quality Assurance)
#9 - c4-judge
2024-01-07T16:10:59Z
MarioPoneder marked the issue as grade-a
#10 - mojito-auditor
2024-01-11T13:13:15Z
Hi @MarioPoneder, could you please take a second look at this issue?
This issue has clearly demonstrated a way to break one of the main invariants in the MaxHeap. Since this contract is within the scope and the sponsor has explicitly specified the main invariant, breaking the invariant should already classify the issue as having at least Medium severity. This contract stores important data, and the contract that interacts with it (CultureIndex) is upgradeable. Therefore, if this issue is not fixed, it could be easily exploited in the future.
#11 - c4-judge
2024-01-11T14:37:12Z
This previously downgraded issue has been upgraded by MarioPoneder
#12 - c4-judge
2024-01-11T14:37:36Z
MarioPoneder marked the issue as duplicate of #266
#13 - c4-judge
2024-01-11T14:37:45Z
MarioPoneder marked the issue as satisfactory