Skip to content

NFTfi는 무엇일까?(ERC 4907)

2022년 NFT의 등장 이후, 수많은 NFT 프로젝트가 등장하였고 엄청난 양의 NFT가 거래되었습니다. 하지만 이러한 인기와는 반대로 NFT의 실효성에 대한 의문은 끊임없이 제기되었습니다. 많은 돈을 내고 NFT를 구매해 소유하게 되었지만 이 것을 사용할 수 있는 방법이 없었기 때문입니다. 이러한 문제를 해결하기 위해 NFT Service Provider는 자신의 NFT를 소유한 유저에게 서비스 할인, 이벤트 참여 등 다양한 혜택을 부여하는 것으로 NFT에 대한 사용성을 넓혔으며 이제는 NFT를 금융적으로 이용할 수 있는 생태계를 구축하기 시작했습니다. 이러한 생태계를 NFTfi라고 하는데요. 과연 NFTfi가 무엇인지, 이것과 관련된 컨트랙트 중 하나인 ERC-4907은 어떻게 작성되어 있는지 확인해 보도록 하겠습니다.

NFTfi

NFTfi란 NFT와 탈중앙화 금융(DeFi)를 합친 말로 NFT를 담보로 Token을 대출하거나 혹은 NFT를 대여한 뒤 구매자가 필요에 따라 사용할 수 있도록 하여 NFT의 가치와 유동성을 확보할 수 있는 생태계를 의미합니다. 기존 NFT의 경우, 소유자가 NFT를 사용할 수 있는 방법이 크게 없었으나 NFTfi가 활성화 되면서 NFT의 사용성을 넓혀 가치를 높히고 이를 바탕으로 유동성을 공급하면서 NFT 시장의 새로운 길을 열어주었습니다. NFTfi에서 NFT를 이용할 수 있는 방법은 1. NFT를 이용한 대출(Loan), 2. NFT 스테이킹(Staking), 3. NFT 대여(Rental)가 있습니다. 3가지 방법에 대한 대표적인 프로젝트와 프로세스는 다음과 같습니다.

NFT를 이용한 대출 (loan)

  • 차용자가 담보로 잡힐 NFT를 제시합니다.
  • 채권자가 해당 NFT에 대한 가치를 평가한 후 대출금, 상환금, 상환일을 제안합니다. 대출금은 차용자가 채권자에게 빌리는 금액이며 상환금은 차용자가 채권자에게 갚아야 하는 금액입니다.
  • 계약 체결되면 NFT는 Lock이 되며 차용자는 대출금을 받습니다. 이후, 2가지 옵션이 존재합니다.

  • option 1) 상환일 내에 상환금 상환 시, NFT는 Unlock되고 차용자에게 다시 소유권이 돌아갑니다.
  • option 2) 상환일 내에 미상환 시, NFT는 Unlock되고 채권자가 NFT를 소유하게 됩니다.

NFT 스테이킹 (Staking)

  • The owner locks up their NFT using a staking service.
  • The NFT is deposited and the owner obtains the right to participate in staking. By participating in staking, the owner can earn profits.

NFT 대여 (Rental)

  • Owner는 자신의 NFT를 대여할 수 있도록 금액과 만료 시간을 제시합니다.
  • User는 Owner가 제시한 금액을 전송하여 해당 NFT에 대한 사용권을 받습니다.(소유권은 변경되지 않습니다.)
  • 만료 시간이 지나면 해당 유저의 사용권은 만료되어 회수됩니다.

이러한 서비스를 위해 이더리움에서는 몇 가지 표준 컨트랙트를 지원하고 있습니다. 그 중 대표적으로 알려진 ERC-4907, Rental NFT 컨트랙트에 대해서 분석해 보도록 하겠습니다.

ERC4907 – Rental NFT, an Extension of EIP-721

ERC-4907은 Rental NFT라는 이름에서 알 수 있듯, NFT 대여와 관련된 대표적인 ERC 표준 입니다. ERC-721의 extension으로 NFT Rental 프로토콜을 서비스하고 있는 더블프로토콜에서 EIP-4907 제안하였고 커뮤니티 투표를 통해 표준으로 확정되면서 ERC-4907이 되었습니다. 그들은 ERC-4907을 제안하게 된 이유에 대해서 다음과 같이 설명하였습니다.

“일부 유틸리티 NFT는 경우에 따라 소유자와 사용자가 항상 같지 않을 수 있다. 이러할 경우, 소유자와 사용자를 식별하고 그에 따라 작업을 수행할 권한을 별도로 관리하는 것을 필요로 한다. 일부 프로젝트에서는 Operator, Controler와 같은 이름으로 역할을 구분하여 사용하고 있지만 이러한 구성이 점점 보편화 됨에 따라 모든 애플리케이션간 협업을 용이하게 하기 위해 통합된 표준이 필요하다.”

“NFT 사용 권한 부여를 위해서는 2개의 트랜잭션이 필요하다. (1. 새 사용자 역할 부여를 위한 주소 입력, 2. 사용자 역할 회수) 2개의 트랜잭션을 발생시키는 것은 효율성, 경제성이 떨어지므로 1개의 트랜잭션만 이용하여 사용 기간을 자동으로 종료하게 하였다.”

또한 ERC-4907을 도입하게 되면 다음과 같은 효과가 있을 것이라고 예상하였습니다.

💡 도입 효과

권리 할당 편리 – 역할 지정으로 인해 소유주와 사용자의 권한(권리)를 쉽게 관리할 수 있다.

간단한 대여 관리Expires 기능을 이용하여 자동으로 사용자의 권한을 만료하여 별도의 트랜잭션 없이 권한을 회수할 수 있다.

손쉬운 타사 플랫폼 연동 – NFT 발급자, 애플리케이션의 허가 없이 다른 프로젝트와 상호 작용이 가능하다.

이전 버전과의 호환성 – ERC721과 완전히 호환된다.

 

그렇다면 이러한 도입 효과를 얻기 위해 컨트랙트 코드를 어떻게 작성하였는지 분석해 보도록 하겠습니다.

 

IERC4907.sol

  • IERC4907.sol를 보면 setUser, userOf, userExpires 3가지 Function이 있으며 이 외에 UpdateUser라는 Event가 있는 것을 확인할 수 있습니다.
				
					interface IERC4907 {

    // Logged when the user of an NFT is changed or expires is changed
    /// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed
    /// The zero address for user indicates that there is no user address
    event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

    /// @notice set the user and expires of an NFT
    /// @dev The zero address indicates there is no user
    /// Throws if `tokenId` is not valid NFT
    /// @param user  The new user of the NFT
    /// @param expires  UNIX timestamp, The new user could use the NFT before expires
    function setUser(uint256 tokenId, address user, uint64 expires) external;

    /// @notice Get the user address of an NFT
    /// @dev The zero address indicates that there is no user or the user is expired
    /// @param tokenId The NFT to get the user address for
    /// @return The user address for this NFT
    function userOf(uint256 tokenId) external view returns(address);

    /// @notice Get the user expires of an NFT
    /// @dev The zero value indicates that there is no user
    /// @param tokenId The NFT to get the user expires for
    /// @return The user expires for this NFT
    function userExpires(uint256 tokenId) external view returns(uint256);
				
			

IERC4907을 상속받아 작성된 ERC4907.sol 코드는 다음과 같습니다.

ERC4907.sol

				
					// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC4907.sol";

contract ERC4907 is ERC721, IERC4907 {
    struct UserInfo 
    {
        address user;   // address of user role
        uint64 expires; // unix timestamp, user expires
    }

    mapping (uint256  => UserInfo) internal _users;

    constructor(string memory name_, string memory symbol_)
     ERC721(name_, symbol_)
     {
     }
    
    /// @notice set the user and expires of an NFT
    /// @dev The zero address indicates there is no user
    /// Throws if `tokenId` is not valid NFT
    /// @param user  The new user of the NFT
    /// @param expires  UNIX timestamp, The new user could use the NFT before expires
    function setUser(uint256 tokenId, address user, uint64 expires) public virtual{
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC4907: transfer caller is not owner nor approved");
        UserInfo storage info =  _users[tokenId];
        info.user = user;
        info.expires = expires;
        emit UpdateUser(tokenId, user, expires);
    }

    /// @notice Get the user address of an NFT
    /// @dev The zero address indicates that there is no user or the user is expired
    /// @param tokenId The NFT to get the user address for
    /// @return The user address for this NFT
    function userOf(uint256 tokenId) public view virtual returns(address){
        if( uint256(_users[tokenId].expires) >=  block.timestamp){
            return  _users[tokenId].user;
        }
        else{
            return address(0);
        }
    }

    /// @notice Get the user expires of an NFT
    /// @dev The zero value indicates that there is no user
    /// @param tokenId The NFT to get the user expires for
    /// @return The user expires for this NFT
    function userExpires(uint256 tokenId) public view virtual returns(uint256){
        return _users[tokenId].expires;
    }

    /// @dev See {IERC165-supportsInterface}.
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IERC4907).interfaceId || super.supportsInterface(interfaceId);
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override{
        super._beforeTokenTransfer(from, to, tokenId);

        if (from != to && _users[tokenId].user != address(0)) {
            delete _users[tokenId];
            emit UpdateUser(tokenId, address(0), 0);
        }
    }
}
				
			

이 코드를 분석하면 다음과 같습니다.

Variables

*struct UserInfo**

UserInfo라는 구조체를 선언합니다. UserInfo는 차용자의 주소를 저장하는 address type의 user와 NFT 대여 만료 기간을 저장하는 unit64 type의 expires를 변수로 갖습니다. expires는 만료 시간을unixTimestamp로 저장합니다.

				
					   struct UserInfo
    {
        address user;   // address of user role
        uint64 expires; // unix timestamp, user expires
    }

				
			

mapping (uint256 => UserInfo) internal **_users;**

TokenId와 UserInfo를 매핑하는 _users 를 선언합니다. TokenId를 Key 값으로 이용하여 매핑된 UserInfo 구조체에 접근할 수 있습니다.

TokenId(user, expires)
(uint256)({UserAddress} , {UnixTimestamp})

Method

function setUser(uint256 tokenId, address user, uint64 expires)

NFT의 tokenId, 사용자(Owner 아님)의 주소, 유효기간을 인자로 받아 msg.sender가 해당 NFT의 Owner인지 확인한 후, UserInfo와 동일한 구조를 가진 info라는 구조체를 선언하여 사용자 주소와 유효기간을 저장한 뒤, UpdateUser event를 실행하여 데이터를 기록합니다.

				
					 function setUser(uint256 tokenId, address user, uint64 expires) public virtual{
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC4907: transfer caller is not owner nor approved");
        UserInfo storage info =  _users[tokenId];
        info.user = user;
        info.expires = expires;
        emit UpdateUser(tokenId, user, expires);
    }
				
			

function userOf(uint256 tokenId)

NFT의 tokenId를 인자로 받아 해당 값과 매핑된 구조체의 데이터를 불러옵니다. 구조체에 저장된 expires의 값이 block.timestamp보다 클 경우, 매핑된 구조체에 저장된 Address를, block.timestamp보다 작을 경우, zeroAddress를 반환하는 함수 입니다.

				
							function userOf(uint256 tokenId) public view virtual returns(address){
        if( uint256(_users[tokenId].expires) >=  block.timestamp){
            return  _users[tokenId].user;
        }
        else{
            return address(0);
        }
    }
				
			

function userExpires(uint256 tokenId)

NFT의 tokenId를 인자로 받아 해당 tokenId와 매핑된 구조체에 저장된 expires 변수의 값을 반환하는 함수입니다.

				
					function userExpires(uint256 tokenId) public view virtual returns(uint256){
        return _users[tokenId].expires;
    }

				
			

Event

Event가 실행되며 indexed로 지정한 uint256 indexed tokenId, address indexed user 두 가지 값이 Log에 저장됩니다. indexed되어있지 않은 uint64 expires 는 Data Field에 기록됩니다.

				
					event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

				
			

Test & Result

ERC-4907을 테스트하기 위해 컨트랙트를 Ethereum Sepolia Testnet에 배포한 후, Method를 실행하였습니다.

  • Owner : 0x99b1CB2591578A5ceF5F7003CB6c5561B87A3122
  • User : 0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC
  • Owner가 User에게 NFT를 대여해주며 expires를 5분 뒤로 설정하였습니다. 실제 실행된 트랜잭션은 아래 링크를 통해 확인할 수 있습니다.
  • Sepolia Testnet Transaction 확인하기

Owner가 To 주소를 컨트랙트 주소로 설정하여 트랜잭션을 전송합니다. 트랜잭션이 실행되면서 UpdateUser Event가 실행되며 Log가 기록됩니다. Log에는 TokenId와 AddressIndexed되어 기록되며 expires는 Data Field에 저장된 것을 확인할 수 있습니다.

대여자의 주소와 만료기간을 확인할 수 있는 userOfuserExpires 를 실행하여 반환받은 다음과 같습니다. ownerOf Method를 추가하여 실제 소유자와 대여자를 명확하게 비교할 수 있도록 설정하였습니다. expires 가 경과하자 대여자의 주소가 zeroAddress로 변경된 것을 확인할 수 있습니다.

				
					//info.expires >= block.timestamp
userOf : 0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC
user Expires : 1687514172
ownerOf : 0x99b1CB2591578A5ceF5F7003CB6c5561B87A3122

//info.expires < block.timestamp
userOf : 0x0000000000000000000000000000000000000000
user Expires : 1687514172
ownerOf : 0x99b1CB2591578A5ceF5F7003CB6c5561B87A3122
				
			

이러한 내용을 바탕으로 EIP-4907은 이더리움 커뮤니티의 투표를 통해 ERC-4907로 선정되었습니다. 하지만 ERC-4907을 실제로 사용하기 전, 유의해야 하는 사항이 있습니다.

  1. **setUser 에 예외 처리가 없다.**

setUser 는 단순히 Address와 Expires를 지정하는 함수지만 어떠한 예외 처리도 되어있지 않기 때문에 아무런 제한 없이 대여 중인 NFT를 다른 유저에게 대여할 수 있습니다. 이를 방지하기 위해서는 parameter로 입력한 tokenId에 해당하는 NFT가 현재 대여 중인지 확인한 후, 대여 중일 경우, 해당 트랜잭션을 revert 처리하는 코드가 추가되어야 합니다.

  1. **Expires 만료는 zeroAddress 반환이다.**

userOf 함수를 보면 Expires 기간이 만료되었을 경우, Struct에 저장된 Address와 expires를 변경하지 않고 zeroAddress를 반환하도록 작성되어 있습니다. expires 만료 시 Address, expires 초기화를 하게되면 트랜잭션이 발생하기 때문에 gas를 아끼게 하기 위한 것으로 추측됩니다. userOf, userExpires Method를 이용하면 이용 기간이 만료된 유저의 정보에 대해 zeroAddress로 확인 가능하지만 Struct의 데이터를 조회하였을 때, 여전히 user의 Address가 남아있기 때문에 착오가 발생할 수 있습니다.

  1. **사용권소유권이 아니다.**

해당 코드에서 대여(사용 권한 부여)란 Struct에 Address와 Expires를 저장하는 것 뿐, 실제 NFT가 전송되는 것이 아닙니다. 유저의 지갑에서 NFT를 확인할 수 없기 때문에 해당 유저가 NFT를 대여한 유저라는 것을 확인해 줄 수 있는 페이지 구현도 고려되어야 합니다.

위와 같이 여러 가지 유의 사항이 있지만 ERC-4907과 같이 NFT의 확장 기능이 나타나 NFT의 사용성이 늘어난다는 것은 분명 유의미한 일입니다. 소유와 조회에만 집중되었던 수준에서 벗어나 NFT에 대한 실제 필요성과 가치가 증명되는 것이기 때문입니다. 최근 ERC 표준이 된 컨트랙트를 살펴보면 NFT에 관련된 내용이 많습니다. 이러한 흐름을 따라 NFT의 발전을 지켜보고 체험하는 것도 재미있는 일이 될 것 입니다.

Share your blockchain-related digital insights with your friends

Facebook
Twitter
LinkedIn

Get more insights

댕크샤딩은 무엇일까? – #2 샤딩 vs. 댕크샤딩

Danksharding is an improved version of Ethereum’s sharding technology, which is one of the techniques that greatly increase transaction capacity and reduce gas fees in Ethreum 2.0 upgrade. To help you understand what Danksharding is, let’s first take a look at Ethereum's scalability strategy, which aims to increase network performance and ensure scalability.

이더리움 확장성 솔루션, 댕크샤딩 (Danksharding)은 무엇일까? #1

댕크샤딩(Danksharding)은 이더리움 네트워크의 샤딩 기술을 개선한 것으로, 이더리움 2.0 업그레이드에서 채택된 기술 중 하나입니다. 댕크샤딩은 이더리움의 트랜잭션 처리량을 크게 향상시키고, 수수료를 낮추는 데 중점을 두고 있습니다.

NFT 마케팅 전략: 고객 참여와 브랜드 가치 높이기

가상화폐 시장은 한 풀 죽었다는 여론과 달리, 시장을 선도하는 글로벌 기업들은 2021년을 기점으로 웹3(디지털 자산) 사업 진출에 속도를 내고 있습니다. 이 글에서는 스타벅스와 같은 글로벌 기업들이 디지털 자산을 어떻게 활용했는지

NFT 활용 마케팅을 통한 브랜드 경쟁력 강화

블록체인 리서치 기업 메사리(Messari)의 ‘How are Major Brands Tapping into the NFT Market’ 보고서에 따르면, ‘21년을 기점으로 주요 글로벌 기업들의 웹3 사업 진출이 가속화 되었고, NFT를 활용한 다양한 마케팅 사례가