Skip to content

Latest commit

 

History

History
103 lines (68 loc) · 3.74 KB

File metadata and controls

103 lines (68 loc) · 3.74 KB

Wide Pistachio Worm

High

Auction::endAuction result can be manipulated

Summary

the reliance of endAuction function to the current reserveToken balance of the pool makes the outcome of the auction can be manipulated.

Auction.sol#L336-L350

  function endAuction() external auctionExpired whenNotPaused {
    if (state != State.BIDDING) revert AuctionAlreadyEnded();

    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);
      IERC20(buyCouponToken).safeTransfer(beneficiary, IERC20(buyCouponToken).balanceOf(address(this)));
    }

    emit AuctionEnded(state, totalSellReserveAmount, totalBuyCouponAmount);
  }

anyone with enough reserveToken balance or derivative token (lev/bondETH) can manipulate to change the outcome of current auction status to SUCCEEDED or FAILED_POOL_SALE_LIMIT

Root Cause

(totalSellReserveAmount >= (IERC20(sellReserveToken).balanceOf(pool) * poolSaleLimit) / 100)

the condition above can be manipulated by Pool::create or Pool::redeem because these two function change the state of IERC20(sellReserveToken).balanceOf(pool) at the end of auction duration (typically 10 days) or when the amount of Auction::totalBuyCouponAmount is already fulfilled.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

consider a situation where: poolSaleLimit = 90

case if pool balance = 100 and totalSellReserveAmount = 85:

the auction would SUCCEEDED. but user with enough derivative token can redeem with the output of ~5.57 reserveToken and then call Auction::endAuction

the result would be

totalSellReserveAmount >= poolBalance * poolSaleLimit/100
85 >= (100 - 5.57) * 90/100
85 >= 94.43 * 90/100
85 >= 84.987 =>>> false =>>> status changed from SUCCEEDED to FAILED_POOL_SALE_LIMIT

case if pool balance = 100 and totalSellReserveAmount = 60:

the auction would SUCCEEDED. but user with enough derivative token can redeem with the output of ~33.34 reserveToken and then call Auction::endAuction

the result would be

totalSellReserveAmount >= poolBalance * poolSaleLimit/100
60 >= (100 - 33.34) * 90/100
60 >= 66.66 * 90/100
60 >= 59.994 =>>> false =>>> status changed from SUCCEEDED to FAILED_POOL_SALE_LIMIT

case if pool balance = 100 and totalSellReserveAmount = 105:

the auction would FAILED_POOL_SALE_LIMIT. but user with enough reserveToken can call create with the input of ~16.67 reserveToken and then call Auction::endAuction

the result would be

totalSellReserveAmount >= poolBalance * poolSaleLimit/100
105 >= (100 + 16.67) * 90/100
105 >= 116.67 * 90/100
105 >= 105.003 =>>> true=>>> status changed from  FAILED_POOL_SALE_LIMIT to SUCCEEDED

all the above manipulation require the user to have relative high amount of derivative/reserve token, but the fact that they can call create/redeem again to get whatever the original amount and token they have, this griefing attack can be considered to only require gas fee paid by attacker

Impact

auction result can be manipulated, possibly to prevent the auction from succeeding. making the user loss the potential amount of coupon token from the auction bidder.

PoC

No response

Mitigation

when the auction is active (typically 10 days), consider to lock the create and redeem function inside the corresponding Pool contract