Caver.js 에서 Kip17 토큰 소각

안녕하세요, 클레이튼 IDE를 이용해 KIP17 컨트랙트를 발행했고

컨트랙트로 만들어진 NFT들을 소각하려고 합니다.

아래와 같이 caver.js로 소각을 시도했습니다만,

ret = caver.klay.accounts.createWithAccountKey(minterAddress, minterPrivateKey);
ret = await caver.klay.accounts.wallet.add(ret);
const kip17Instance = await new caver.klay.KIP17({컨트랙트주소});
  ret = await kip17Instance.burn({토큰ID},{
    from : minterAddress,
    gas: '850000'
  }).then(console.log());

계속 revert가 발생중입니다. 토큰의 전송이나 다른 api들은 정상적으로 처리가 되는데, 유독 burn만 작동이 안되고 있습니다. 특별한 이유가 있을까요?
혹시 몰라서 setApprovalForAll을 다른 주소에 넘겨주고 해당 주소로도 burn을 해봤는데 작동이 안됩니다.

@eklee808

안녕하세요.
burn은 내부적으로 _isApprovedOwner 를 사용해서 Burn할 자격이 있는지를 검증하고 있습니다.

링크로 드린 코드를 참고해보셔서 정말 자격이 있는지를 검증해보는 게 우선일 거 같습니다.

감사합니다.

2 Likes

답변 감사합니다!

링크해주신 컨트랙트를 보면 _isApprovedOwner는 burn 함수에는 없고 transferFrom에 들어가 있는데, burn에서도 사용하는 부분이 또 있는 건가요?

말씀해주신대로 isApprovedOrOwner를 참고해보면 아래처럼 자격을 체크하는 부분이 있는데,

return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));

일단 owner 주소를 넣어도 소각이 안되서 isApprovedForAll()에 (owner주소,owner주소)로 파라미터를 넘겨보니 false가 떨어지더군요. 해서 setApprovalForAll() 함수로 다른 주소에 권한을 주고, 권한을 받은 주소를 넣고 burn을 시도해봐도 똑같이 revert가 뜨더군요;;

일단 _burn에 보면

require(ownerOf(tokenId) == owner, “KIP17: burn of token that is not own”);

이렇게 토큰의 소유자만 최종적으로 권한을 주는 것으로 보여서 다른 계정으로 토큰을 전송하고 burn을 해보려고 합니다.

현재 caver를 사용한 소각기능이 전체적으로 적용이 되지 않습니다. 위에 서술한 것처럼 다른 계정으로 KIP17 토큰을 전송하고 burn을 해도 여전히 되지 않네요.

@eklee808

혹시 배포하신 컨트랙트 코드가 어떻게 되시나요?
배포하신 컨트랙트 코드와 컨트랙트 주소 공유 부탁드립니다.

컨트랙트 주소는 다음과 같습니다. 바오밥 네트워크입니다.
0x0F94F275AC8E7f108794bCE4490cd4F5e320FE05 - 팩토리 컨트랙트
0xb1b488632179e46ff0431b2af0dcd67dd5ddfc2e - 팩토리에서 생성된 KIP17 컨트랙트

팩토리 컨트랙트에서 문제가 발생할 것으로 추정되는 위치는 다음과 같습니다.

pragma solidity ^0.5.6;
import './NFTMinter.sol';


contract NFTManager{

    address payable owner;  
    mapping (address => NFTMinter) public nftManager;


    constructor() public {
        owner = msg.sender;
    }
    function mintNewToken(string memory _name, string memory _symbol) public returns (address){
        NFTMinter minter = new NFTMinter(owner, _name, _symbol); 
//여기서 일단 팩토리가 컨트랙트를 만드는데, 이 부분에서 Creator가 일단 팩토리 컨트랙트로 정해집니다.
        nftManager[msg.sender] = minter;        
        minter.transferOwnership(msg.sender);
//이 부분에서 오너십은 메소드를 호출한 주소로 전송됩니다. Creator와 Owner가 분리됩니다.
        return address(nftManager[msg.sender]);
    }

    function burnToken(uint _tokenNum, address minterAddress) public {
        nftManager[minterAddress].burnToken(_tokenNum);
//Kip17 컨트랙트의 소각을 연결합니다. 
    }

위 코드의 burnToken은 caver.klay.kip17.burn()이 revert가 나기 때문에 직접 kip17 컨트랙트와 연결해서 sendTransaction으로 시도해보기 위해서 만들었습니다.

KIP17의 _burn 메소드를 보면 ownerOf(tokenId) == owner를 require하는데, '토큰의 소유자’만 필터링해서 토큰을 삭제하는 것으로 보여집니다. 헌데 직접 배포하고 발행까지 한 주소에서는 소각이 안되고, 이를 다른 주소로 보내더라도 역시 소각이 되지 않습니다.

달리 소각이 안될만한 이유가 있을까요?

@eklee808

팩토리 컨트랙트 뿐만 아니라 문제가 되는 컨트랙트 자체도 공유해주시면 감사하겠습니다.

그리고 NFTManager.burnToken 메서드의 경우 내부적으로 NFTMinter.burnToken 을 호출하고 있는 것으로 보이는데요, Caver에서 제공하는 KIP17.burn 의 경우 아래와 같이 burn(uint256) 이라는 메서드 시그니쳐와 매핑되는 기능입니다.

이 부분을 헷갈리신 건 아닌지도 확인 부탁드려요.

pragma solidity 0.4.24;

/// @title KIP-17 Non-Fungible Token Standard, optional burning extension
///  Note: KIP-13 identifier for this interface is 0x42966c68.
interface IKIP17Burnable {
    /// @notice Destroy the specified token
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws  `_tokenId`
    ///  is not a valid NFT. 
    /// @param _tokenId The token ID to be burned
    function burn(uint256 _tokenId) public;
}

안녕하세요. @eklee808

코드를 확인해보니 NftMinter의 burnToToken을 호출하게 되어있는데, 이 부분은 함수가 어떻게 되어있는지요?

연결 및 사용하는 KIP17 contract의 code를 명확히 파악하신 후 burn에서 사용하는 권한 체크가 어떻게 되어있는지 확인하시어 코드를 작성 및 디버깅 해보시는게 빠를 것 같습니다.

제가 확인해본바로는 클레이튼에서 제공하는 KIP-17 contract의 burn함수는 권한체크하는 함수가 있습니다.
이 부분 확인해보셨는지요?

1 Like

답변 늦어진 점 죄송합니다.
아래는 팩토리에서 생성된 컨트랙트의 생성자와 소각부분입니다.

pragma solidity ^0.5.6;

import "@klaytn/contracts/token/KIP17/KIP17Full.sol";
import "./Ownable.sol";

contract NFTMinter is KIP17Full, Ownable{  
  address payable ownerAddr;  

  constructor(address payable _owner, string memory _name, string memory _symbol) KIP17Full(_name, _symbol) public { 
    ownerAddr = _owner;

  }


  function burnToken(uint _tokenNum) public{
    _burn(msg.sender, _tokenNum);

    }

말씀해주신 부분을 보니 caver에서 제공하는 burn의 경우 KIP17Burnable의 burn 메소드와 매칭되는 것 같은데, 그러면 KIP17Full을 받아서 생성되는 컨트랙트의 경우 KIP17Metadata와 KIP17Enumerable만 포함하고 있기 때문에 적용이 안될 수 있을까요?
KIP17.sol의 경우에도 역시 burn 메소드를 포함하고 있고, KIP17.sol의 burn 메소드의 경우는 다음과 같이 토큰ID의 소유권만 체크하고 있습니다.


    function _burn(address owner, uint256 tokenId) internal {
        require(ownerOf(tokenId) == owner, "KIP17: burn of token that is not own");

        _clearApproval(tokenId);

        _ownedTokensCount[owner].decrement();
        _tokenOwner[tokenId] = address(0);

        emit Transfer(owner, address(0), tokenId);
    }

안녕하세요, 답변 감사합니다!

말씀해주신 컨트랙트의 함수는 다음과 같습니다.

pragma solidity ^0.5.6;

import "@klaytn/contracts/token/KIP17/KIP17Full.sol";
import "./Ownable.sol";

contract NFTMinter is KIP17Full, Ownable{  
  address payable ownerAddr;  

  constructor(address payable _owner, string memory _name, string memory _symbol) KIP17Full(_name, _symbol) public { 
    ownerAddr = _owner;

  }


  function burnToken(uint _tokenNum) public{
    _burn(msg.sender, _tokenNum);

    }

링크해주신 KIP-17 contract의 burn 함수는 Burnable 컨트랙트를 is로 받아서 생성해야 적용이 될까요?
일단, 제 컨트랙트의 경우엔 KIP17Full만 받았기 때문에 Burnable의 메소드는 호출되지 않을 것이라고 추측됩니다만, Burnable의 메소드로 매칭될 경우에, isApprovedOrOwner의 경우에는 다음과 같이 호출주소가 Token의 Owner이면 Approved의 유무와 상관 없이 소각되어야 할 것 같은데, 역시 Burnable이 포함되지 않아서 매칭이 되지 않는 걸까요?
(참고로 컨트랙트는 Klaytn IDE로 배포했습니다.)


    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
        require(_exists(tokenId), "KIP17: operator query for nonexistent token");
        address owner = ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }

또, Burnable의 burn을 KIP17 instance로 호출하지 않고, 제가 작성한 burnToken으로 KIP17의 _burn 메소드를 호출하지는 않을지 궁금합니다.

추가적으로 방금 컨트랙트에 KIP17Burnable을 추가하고 테스트해보니 토큰의 소유주라면 소각이 가능했습니다. 정말 감사합니다!

앞으로의 문제는 해결되었습니다만, KIP17Burnable이 아닌 KIP17Full의 _burn() 메소드로는 소각이 안되는 걸까요? 아니면 다른 부분에서 제가 확인해볼 수 있는 부분이 있을까요?

@eklee808
_burn() 메서드는 internal로 선언되어 있어 외부에서 호출이 불가합니다. 해당 내용은 솔리디티 문서를 참고하시면 더 자세히 알아보실 수 있을 거예요.

추가적으로 internal 이 아니었다 해도 메서드 시그니쳐 중 이름과 데이터 타입이 서로 다르기 때문에(앞에 언더바가 있냐 없냐의 유무 + uint256이 아닌 uint로 선언되어 있음) Caver의 KIP17의 burn으로는 호출이 안됩니다.
스마트 컨트랙트의 메서드를 호출하는 행위는 로우레벨에서 보면
메서드의 시그니쳐를 해쉬한 값 중 앞 4바이트로 어떤 함수를 호출할지를 구분합니다.

해시라는 건 입력이 다르면 출력값도 다르겠죠. 즉
keccak256("burn(uint256)") 의 결과 중 앞 4바이트를 추출한 값과
keccak256("_burn(uint)")의 결과 중 앞 4바이트를 추출한 값은 당연히 다릅니다.

Caver에서 제공하는 기능은 KIP17 표준에 정의된 메서드를 편하게 호출할 수 있게 해주는 것인데 질문자분께서 사용하시는 컨트랙트는 KIP17의 표준에서 사용하고 있는 burn 이 아닌 다른 함수 시그니쳐로 사용을 하고 계시죠.
Caver SDK에서는 burn(uint256) 함수를 호출하려하는데, 질문자분의 컨트랙트에는 해당 함수가 없으니 에러가 발생하고 있는 것이구요.

internal로 선언되어 있어서 호출이 안되겠지만, 그게 아니었다고 해도 이미 함수 시그니쳐 자체가 다르기에 Caver의 KIP17.burn 을 활용하셔서 호출하실 수는 없습니다.

늦은 시간에 답변 정말 감사합니다. uint256과 uint를 혼용해서 사용했던 것은 제가 찾아내야 했던 이슈인데 정말 죄송합니다…

다만, caver.klay.kip17 api로 사용한 것은 kip17 instance, 그러니까 생성된 컨트랙트를 직접 인스턴스화한 후 burn api를 호출했으니 제가 팩토리 컨트랙트로부터 호출한 burnToken과는 별개로 테스트했던 것입니다. 이 경우 컨트랙트에 Burnable이 포함되어 있지 않아서 소각이 되지 않았던 것으로 보입니다.

말씀해주신대로 파라미터는 uint256으로 전달하고, 컨트랙트 전체에서 혼용하는 부분을 없앤 뒤 테스트했습니다. 팩토리 컨트랙트에서는 호출했더니 revert가 났고, 이 부분을 Klaytn IDE에서 직접 호출해보니 다음과 같은 메시지를 받았습니다.
(테스트 환경을 위해 KIP17Burnable은 다시 제외했습니다)

한가지 헷갈리는건, 이 경우 메소드 호출은 됐는데, require문 때문에 걸린 것인지, 아니면 말씀해주신대로 internal이기 때문에 호출이 되지 않은 것인데 IDE의 메시지만 저렇게 뜬 것인지 궁금합니다.

마지막으로, 팩토리에서 생성된 KIP17Full 컨트랙트의 ABI와 Address를 직접 가져와서, 다음과 같이 sendTransaction을 보내니 성공적으로 소각이 되었습니다.


          ret = await caver.klay.sendTransaction({
            type: 'SMART_CONTRACT_EXECUTION',
            from: minterAddress, //발행주소
            to: nftAddress, //배포된 컨트랙트 주소
            data: contract.methods.burnToken(tokenNum).encodeABI(),
            gas: '1000000'
          })

추측컨데 _burn(uint256) 메소드가 internal이기 때문에 revert가 나지 않은 것인가 생각이 됩니다. 헌데, 팩토리 컨트랙트에서는 burnToken()메소드를 호출하지, _burn(uint256)을 직접 호출하는 것이 아닌데 internal에 걸리는 것인지 궁금합니다.

답변 덕분에 문제되던 부분이 상당히 해결되었습니다. 늦은 시간까지 정말 감사하고 죄송합니다.

@eklee808

burnToken을 통해 internal 함수인 _burn(uint256) 을 호출하는 건 문제 없습니다.
제가 말씀드린 호출이 안된다는 부분은 직접 호출하려는 시도(data 필드에 _burn(uint256)의 ABI를 넣어서 트랜잭션을 보내는 행위)가 안된다고 말씀 드린 거예요.

IDE의 메시지는 아래 Returned error: execution reverted KIP17: burn of token that is not own 으로 미루어보았을 때, require 문 조건에 걸리기 때문인 거 같습니다.