Skip to content

Latest commit

 

History

History
128 lines (96 loc) · 4.08 KB

File metadata and controls

128 lines (96 loc) · 4.08 KB

Blunt Plastic Raccoon

High

Auctions can't be ended due to logical error

Summary

When the distribution period is over, anyone can start the auction which auctions off reserve tokens to cover the coupons needed to payout bondETH holders. Auction::startAuction() deploys a new auction contract and stores the address in the auctions mapping using the currentPeriod as an index. Then bondToken.increaseIndexedAssetPeriod() increments currentPeriod:

function startAuction() external whenNotPaused() {
...

  auctions[currentPeriod] = Utils.deploy(
     address(new Auction()),
        abi.encodeWithSelector(
          Auction.initialize.selector,
          address(couponToken),
          address(reserveToken),
          couponAmountToDistribute,
          block.timestamp + auctionPeriod,
          1000,
          address(this),
          poolSaleLimit
        )
      );

    // Increase the bond token period
@>  bondToken.increaseIndexedAssetPeriod(sharesPerToken);

...
  }

function increaseIndexedAssetPeriod(uint256 sharesPerToken) public onlyRole(DISTRIBUTOR_ROLE) whenNotPaused() {
    globalPool.previousPoolAmounts.push(
      PoolAmount({
        period: globalPool.currentPeriod,
        amount: totalSupply(),
        sharesPerToken: globalPool.sharesPerToken
      })
    );
@>  globalPool.currentPeriod++;
...
  }

After the auction period is over (e.g. 10 days), endAuction() determines if the auction was successful for not. If it was successful, it calls Pool.transferReserveToAuction():

function endAuction() external auctionExpired whenNotPaused {
...

    if (currentCouponAmount < totalBuyCouponAmount) {
      state = State.FAILED_UNDERSOLD;
    } else if (totalSellReserveAmount >= (IERC20(sellReserveToken).balanceOf(pool) * poolSaleLimit) / 100) {
        state = State.FAILED_POOL_SALE_LIMIT;
    } else {
      state = State.SUCCEEDED;
@>    Pool(pool).transferReserveToAuction(totalSellReserveAmount);
...
  }

The problem is that Pool.transferReserveToAuction() uses the currentPeriod to get the address of the auction that is over. But currentPeriod() was incremented when the auction started so auctions[currentPeriod] will return address(0) causing endAuction to always revert:

function transferReserveToAuction(uint256 amount) external virtual {
    (uint256 currentPeriod, ) = bondToken.globalPool();
@>  address auctionAddress = auctions[currentPeriod];
@>  require(msg.sender == auctionAddress, CallerIsNotAuction());
    
    IERC20(reserveToken).safeTransfer(msg.sender, amount);
  }

This breaks the auction component of the protocol.

Root Cause

auctions[currentPeriod] will always return address(0) causing the next line to always revert.

function transferReserveToAuction(uint256 amount) external virtual {
    (uint256 currentPeriod, ) = bondToken.globalPool();
@>  address auctionAddress = auctions[currentPeriod];
@>  require(msg.sender == auctionAddress, CallerIsNotAuction());
    
    IERC20(reserveToken).safeTransfer(msg.sender, amount);
  }

https://github.com/sherlock-audit/2024-12-plaza-finance/blob/14a962c52a8f4731bbe4655a2f6d0d85e144c7c2/plaza-evm/src/Pool.sol#L579-L580

Internal Pre-conditions

  1. Someone calls endAuction() after can auction finishes successfully

External Pre-conditions

n/a

Attack Path

  1. The distribution period on a pool ends.
  2. startAuction() is called.
  3. The auction period ends.
  4. The auction was successful in obtaining the necessary amount of coupon tokens to payout bondETH holders.
  5. endAuction() is called, but reverts due to the coding mistake.

Impact

Auction participants funds get stuck in auction that can't be ended.

PoC

No response

Mitigation

function transferReserveToAuction(uint256 amount) external virtual {
    (uint256 currentPeriod, ) = bondToken.globalPool();
-     address auctionAddress = auctions[currentPeriod];
+     address auctionAddress = auctions[currentPeriod - 1];
    require(msg.sender == auctionAddress, CallerIsNotAuction());
    
    IERC20(reserveToken).safeTransfer(msg.sender, amount);
  }