sherlock-2023-11-nouns-builder-h01

[H-01] when reservedUntilTokenId > 100 first funder loss 1% NFT

보고서

Summary

reservedUntilTokenId 가 100보다 클 때, 첫번째 baseTokenId를 잘못 설정하여 첫번째 창립자가 자신에게 할당된 NFT 중 1%를 받을 수 없게 된다.

Keyword

logic flaw, arithmetic error

Vulnerability

DAO 창립자에게 줄 NFT를 예약하기 위해 _addFounders 함수를 호출한다. 창립자에게 특정 끝번호(0~99)를 특정 창립자에게 민팅하도록 예약한다.

reservedUntilTokenId 파라미터는 별개의 민터로 민팅하기 위해 예약해둔 토큰 id가 끝나는 지점으로, 이 tokenId부터 창립자 또는 낙찰자에게 NFT 발행을 시작한다.

    function _addFounders(IManager.FounderParams[] calldata _founders, uint256 reservedUntilTokenId) internal {
      ...
@>              uint256 baseTokenId = reservedUntilTokenId;
 
                // For each token to vest:
                for (uint256 j; j < founderPct; ++j) {
                    // Get the available token id
@>                  baseTokenId = _getNextTokenId(baseTokenId);
 
                    // Store the founder as the recipient
                    tokenRecipient[baseTokenId] = newFounder;
 
                    emit MintScheduled(baseTokenId, founderId, newFounder);
 
                    // Update the base token id
                    baseTokenId = (baseTokenId + schedule) % 100;
                }
      ...
    }
    
    function _getNextTokenId(uint256 _tokenId) internal view returns (uint256) {
      unchecked {
@>      while (tokenRecipient[_tokenId].wallet != address(0)) {
          _tokenId = (++_tokenId) % 100;
        }
 
        return _tokenId;
      }
    }

reservedUntilTokenId > 100 일 때, 파라미터 _founders 배열의 첫 번째로 등록된 창립자(_founders[0])는 원래 받아야 하는 양의 1%의 NFT를 받을 수 없게 된다.

예를 들어 reservedUntilTokenId = 200 일 때, 첫 번째 호출된 _getNextTokenId(200) 의 결과는 200이며 tokenRecipient[200] = founder_0 로 설정된다.

founder[0].founderPct = 10 이라 가정하면 founder_0는 reservedUntilTokenId 이전까지의 NFT를 10% 할당받을 수 있다. founder_0는 다음과 같은 tokenId를 할당받는다.

  • tokenRecipient[200].wallet = founder_0 (첫번째 _getNextTokenId(200)가 200을 리턴하므로)
  • tokenRecipient[10].wallet = founder_0 (두번째 _getNextTokenId((200 + 10) %100 = 10) 로 10을 리턴)
  • tokenRecipient[20].wallet = founder_0
  • tokenRecipient[90].wallet = founder

하지만 첫번째에 설정된 200번 NFT는 founder_0 에게 발행될 수 없다. _isForFounder 함수에서 창립자에게 할당된 NFT를 확인하고 발행하는데, 모듈러 계산을 통해 끝자리만 확인하기 때문이다. 끝자리 200을 예약했지만 _tokenId % 100 의 결과가 200이 나올 수 없으므로 발행받을 수 없다.

    function _isForFounder(uint256 _tokenId) private returns (bool) {
        // Get the base token id
@>      uint256 baseTokenId = _tokenId % 100;
 
        // If there is no scheduled recipient:
@>      if (tokenRecipient[baseTokenId].wallet == address(0)) {
            return false;
 
            // Else if the founder is still vesting:
        } else if (block.timestamp < tokenRecipient[baseTokenId].vestExpiry) {
            // Mint the token to the founder
@>          _mint(tokenRecipient[baseTokenId].wallet, _tokenId);
 
            return true;
 
            // Else the founder has finished vesting:
        } else {
            // Remove them from future lookups
            delete tokenRecipient[baseTokenId];
 
            return false;
        }
    }

Impact

reservedUntilTokenId 가 100보다 클 때, 첫번째 창립자가 자신에게 할당된 NFT 중 1%를 받을 수 없게 된다.

Mitigation

_addFounders에서 등록하는 baseTokenId는 0부터 시작한다.

    function _addFounders(IManager.FounderParams[] calldata _founders, uint256 reservedUntilTokenId) internal {
      ...
 
                // Used to store the base token id the founder will recieve
-               uint256 baseTokenId = reservedUntilTokenId;
+               uint256 baseTokenId = 0;

또는 reservedUntilTokenId % 100 부터 시작한다.

    function _addFounders(IManager.FounderParams[] calldata _founders, uint256 reservedUntilTokenId) internal {
      ...
 
                // Used to store the base token id the founder will recieve
-               uint256 baseTokenId = reservedUntilTokenId;
+               uint256 baseTokenId = reservedUntilTokenId  % 100;

tags: bughunting, nouns dao, smart contract, solidity, logic flaw, arithmetic error, severity high