Blurry Crepe Rabbit
Medium
The _removeBid
function in the Auction contract uses safeTransfer
for token transfers without proper failure handling. If a user is blacklisted (e.g., in USDC on Base chain) after placing a bid, any attempt to remove their bid will fail due to the transfer reverting. This causes the entire _removeBid
function to revert, effectively creating a Denial of Service (DOS) condition where the bid cannot be removed and the auction's functionality may be blocked.
https://github.com/sherlock-audit/2024-12-plaza-finance/blob/main/plaza-evm/src/Auction.sol#L298-L331
In the _removeBid
function:
function _removeBid(uint256 bidIndex) internal {
// ... bid removal logic ...
address bidder = bidToRemove.bidder;
uint256 buyReserveAmount = bidToRemove.buyReserveAmount;
uint256 sellCouponAmount = bidToRemove.sellCouponAmount;
currentCouponAmount -= sellCouponAmount;
totalSellReserveAmount -= buyReserveAmount;
// This transfer will revert if bidder is blacklisted
IERC20(buyCouponToken).safeTransfer(bidder, sellCouponAmount);
emit BidRemoved(bidIndex, bidder, buyReserveAmount, sellCouponAmount);
delete bids[bidIndex];
bidCount--;
}
The vulnerability exists because:
- The function relies on a direct
safeTransfer
call that will revert if the transfer fails - If a user is blacklisted after placing their bid, any attempt to remove their bid will fail
- This creates a permanent DOS condition as the bid cannot be removed
- The issue is particularly concerning for tokens like USDC on Base chain that maintain blacklists
- Denial of Service: If a bidder gets blacklisted, their bid becomes "stuck" in the system and cannot be removed
- Auction Disruption: Could block critical auction operations that depend on bid removal
- Manual code review
Implement a try-catch mechanism with pending refunds:
function _removeBid(uint256 bidIndex) internal {
Bid storage bidToRemove = bids[bidIndex];
uint256 nextIndex = bidToRemove.nextBidIndex;
uint256 prevIndex = bidToRemove.prevBidIndex;
// Update linked list pointers
if (prevIndex == 0) {
highestBidIndex = nextIndex;
} else {
bids[prevIndex].nextBidIndex = nextIndex;
}
if (nextIndex == 0) {
lowestBidIndex = prevIndex;
} else {
bids[nextIndex].prevBidIndex = prevIndex;
}
address bidder = bidToRemove.bidder;
uint256 buyReserveAmount = bidToRemove.buyReserveAmount;
uint256 sellCouponAmount = bidToRemove.sellCouponAmount;
currentCouponAmount -= sellCouponAmount;
totalSellReserveAmount -= buyReserveAmount;
try IERC20(buyCouponToken).safeTransfer(bidder, sellCouponAmount) {
emit BidRemoved(bidIndex, bidder, buyReserveAmount, sellCouponAmount);
} catch {
pendingRefunds[bidder] += sellCouponAmount;
emit BidRemovedPendingRefund(bidIndex, bidder, buyReserveAmount, sellCouponAmount);
}
delete bids[bidIndex];
bidCount--;
}
Additional recommendations:
- Add a
claimPendingRefund
function for users to claim their refunds later - Consider implementing a maximum cap on pending refunds per address
- Add events to track pending refund status
- Add documentation about the refund mechanism in the contract