Solidity

ERC-5192 실습 : SBT (Soul Bound Token)

mmalmmizal 2025. 2. 25. 22:45

ERC-5192 이란?

https://eips.ethereum.org/EIPS/eip-5192#copyright

https://it-timehacker.tistory.com/455

 

ERC-5192: Minimal Soulbound NFTs

Minimal interface for soulbinding EIP-721 NFTs

eips.ethereum.org

 

ERC-5192는 ERC-721을 기반으로 만들어졌으며 ERC-721의 기능을 상속한다.

여기에 ERC-5192는 전송이 불가능하다는 특성이 존재한다.

 

 생성시에 잠금 여부를 한번 정하고 SBT면 잠김 상태로 모든 컨트렉트가 그 이후에는 작동을 하고 잠금 상태가 아니라면 일반적인 NFT처럼 작동을 하는 모델

 

  • Locked 여부
  • Locked와 Unlocked 이벤트

 

 

ERC-5192 실습-1 (vscode와 Sepolia 테스트넷)

NTT : Non-Transferable Token
  • attestate/ERC5192 설치

https://github.com/attestate/ERC5192/tree/main

npm install erc5192
  • openzeppelin 환경 설정

이때, openzeppelin의 v5 버전을 사용하면 아래와 같은 오류가 발생한다.

https://ethereum.stackexchange.com/questions/164883/getting-undeclared-identifier-error-because-existstokenid-function-doesnt-ex

기존 버전 확인

npm list @openzeppelin/contracts

기존 버전 삭제

npm uninstall @openzeppelin/contracts

 

특정 버전으로 설치

npm install @openzeppelin/contracts@4.9
 
  • 컨트랙트 작성

Contract NTT is ERC5192 :  ERC5192를 상속받아 기능을 확장한 스마트 컨트랙트

ERC5192 Instance를 생성할 때(contract를 배포할 때) 이름, 토큰의 심볼, Locked 초기 잠금여부를 인수로 받는다.

 

constructor(string memory _name, string memory _symbol, bool _isLocked) ERC5192(_name, _symbol, _isLocked) {}

상속 관계일 때 생성자 

부모 컨트랙트가 생성자를 가지고 있을 때, 자식 컨트랙트 생성자 안에서 부모 생성자를 명시적으로 호출해야 한다.

contract B{
	constructor (){
	}
}

컨트랙트 A는 B를 상속 받는다. 

contract A is B {
	constructor() B() {
	}
}

 

  • 코드 컴파일
truffle compile
  • 컨트랙트 sepolia에 배포

deployer.deploy()는 첫 번째 매개변수로 계약 이름을 받고, 그 이후로 그 계약 생성자에 전달될 매개변수들을 받는다.

//migrations/2_deploy_contract.js

const NTT = artifacts.require("NTT");
const UseNTT = artifacts.require("UseNTT");

module.exports = async function (deployer, network, accounts) {
  await deployer.deploy(UseNTT);
  const useNTTInstance = await UseNTT.deployed();

  console.log("UseNTT deployed at:", useNTTInstance.address);
};
//truffle-config.js 
    sepolia: {
      provider: () =>
        new HDWalletProvider(
          [process.env.MNEMONIC], // Private Key
          `https://eth-sepolia.g.alchemy.com/v2/${process.env.AICHEMY_PROJECT_ID}`
        ),
      network_id: 11155111,
      gas: //트랜잭션의 가스한도 
      maxFeePerGas: //(baseFee + maxPriorityFeePerGas)의 상한선
      maxPriorityFeePerGas: //가스 단위당 지불할 최대 우선순위 수수료

 

 

EIP-1559

EIP-1559는 이더리움 네트워크에서 트랜잭션 비용을 예측 가능하게 만들기 위해 도입된 모델

gasPrice는 EIP-1559 이전의 방식이라 maxFeePerGas와 maxPriorityFeePerGas 로 대체되었다.

실제 가스 비용 = 사용된 가스 × (baseFee + 실제 지불된 priorityFee)

 

가스의 단위는 Gwei

1 ETH = 10^9 Gwei (1,000,000,000 Gwei)

1 ETH = 10^18Wei

 

  • maxFeePerGas : 트랜잭션당 지불할 의향이 있는 총 최대 가스 가격(baseFee + priorityFee), baseFee(네트워크가 정하는 기본 수수료) + maxPriorityFeePerGas를 합한 값보다 커야 함. (baseFee + maxPriorityFeePerGas)의 상한선 역할을 한다.
  • maxPriorityFeePerGas : 가스 단위당 지불할 최대 우선순위 수수료

 

 

truffle migrate --network sepolia

 

 

 

  • useNTT 함수 호출하기
instance = await UseNTT.deployed();

instance.useNTT("NTT 받을 주소");

 

⚠️ Fail with error 'ERC721: transfer to non ERC721Receiver implementer'

더보기
⚠️ Fail with error 'ERC721: transfer to non ERC721Receiver implementer'

 

openzeppelin ERC721 구현 내용

  • _safeMint
function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
    _mint(to, tokenId);
    ERC721Utils.checkOnERC721Received(_msgSender(), address(0), to, tokenId, data);
}
  • _mint 함수
 function _mint(address to, uint256 tokenId) internal {
      if (to == address(0)) {
          revert ERC721InvalidReceiver(address(0));
      }
      address previousOwner = _update(to, tokenId, address(0));
      if (previousOwner != address(0)) {
          revert ERC721InvalidSender(address(0));
      }
  }

 

현재 NTT 계약에서 safeMint를 사용하고 있어서 발생하는 문제

_mint를 사용하는 방식으로 변경하였다

 

SBT 동작 검증

전송하려고 하니 예상된 오류 발생

SBT 토큰이 lock 되어있기 때문에 전송이 불가능 하다.


ERC-5192 실습-2 (Remix와 ganache)

 

NTT 계약 발급

 
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

import "./ERC5192.sol";


contract NTT is ERC5192 {
  bool private isLocked;
  constructor(string memory _name, string memory _symbol, bool _isLocked)
    ERC5192(_name, _symbol, _isLocked)
  {
    isLocked = _isLocked;
  }
  function Mint(address to, uint256 tokenId) external {
    _mint(to, tokenId);
    if (isLocked) emit Locked(tokenId);
  }

  function safeTransfer(address from, address to, uint256 tokenId) external {
    require(!isLocked, "NTT: Token is locked and cannot be transferred.");
    transferFrom(from, to, tokenId); // Calls ERC721's transferFrom
}
}

 

UseNTT 계약

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

import "./NTT.sol";

contract UseNTT {
    NTT public ntt;

    constructor(address _nttAddress) {
        ntt = NTT(_nttAddress);
    }

    function useNTT(address receiver) public {
        uint256 tokenId = 0; // Define token ID
        address minter = address(this);
        ntt.Mint(minter, tokenId);

        ntt.safeTransfer(minter, receiver, tokenId);
    }
}​

 

 

SBT 동작 검증

전송하려고 하니 예상된 오류 발생

SBT 토큰이 lock 되어있기 때문에 전송이 불가능 하다.



'Solidity' 카테고리의 다른 글

업그레이드 가능한 contract (Proxy)  (0) 2025.04.03
Solidity의 event와 로그 저장  (0) 2024.12.18