[Ethereum] 비트코인 시세에 따라 변하는 NFT 만들기(feat. Chainlink)

0xDave·2022년 9월 12일
0

Ethereum

목록 보기
10/112
post-thumbnail

🚨 오라클 문제


블록체인은 체인 바깥의 정보를 실시간으로 업데이트하기가 어렵다. 예를 들어, 월드컵에서 실시간으로 바뀌는 우리나라 순위를 블록체인 위에 기록한다고 해보자. 매 경기마다 순위는 계속해서 바뀌기 때문에 외부에서 누군가가 체인에 정보를 넣어줘야 한다. 이 때 또 다른 문제가 발생한다. 첫 번째는 누가 정보를 넣어줄 것이며, 두 번째는 넣어준 정보가 정확한 정보인지 어떻게 아느냐다.

체인링크에서는 이러한 문제들을 해결하기 위해 탈중앙화된 노드를 사용한다. 노드를 통해 체인 바깥의 정보를 서로 검증하고 컨트랙트에 데이터를 제공한다. 모든 요청 및 응답은 온체인에 기록되며 이는 네트워크의 신뢰도 및 정확도를 판단하는 데 사용된다. 이렇게 온체인과 오프체인을 연결해주는 것을 오라클이라 한다. 현재 다양한 곳에서 사용되며, 주로 디파이에서 가격 데이터를 제공하는데 사용된다.


🤖 준비물


먼저 몇 가지 준비물이 필요하다.

1. 체인링크 Faucet

https://faucets.chain.link/rinkeby 에서 테스트넷에서 사용할 LINK 토큰과 ETH를 받는다.

2. IPFS url 만들기

가격에 맞춰서 변할 NFT 사진을 filebase를 통해 저장하고, ipfs url를 다른 곳에 잘 보관해놓는다. ipfs url 만드는 방법은 Alchemy에서 올려준 영상에 잘 나와있다.


📚 사전 지식


1. Trigger

예제 코드를 보기에 앞서 알아두면 좋은 것들이 있다. 첫 번째는 Trigger다. 체인링크를 통해 우리의 컨트랙트가 동작하려면 Trigger가 필요하다. Trigger의 종류에는 두 가지가 있다.

1) Time-based trigger
2) Custom logic trigger

Time-based trigger는 일정 시간마다 trigger가 발동한다. 알람을 설정해서 일정 시간이 되면 컨트랙트의 함수가 동작하는 경우다. Custom logic trigger는 원하는 조건을 만족할 경우 함수가 돌아가도록 할 수 있다. 이 때 Custom logic trigger는 Keepers-compatible 해야한다.

체인링크를 통해 외부에서 데이터를 가져오는 과정은 다음과 같다.

1) 사용자는 스마트컨트랙트를 만든 후 trigger가 담긴 Upkeep job 을 만들어 Chainlink Keeper App에 등록한다.(LINK 토큰 필요)

2) 이후 조건을 충족한 trigger가 발생하면 요청을 받은 체인링크의 노드로 부터 데이터를 가져온다.

3) 가져온 데이터를 통해 컨트랙트의 함수가 실행되고 트랜잭션이 블록에 포함된다.


🧑‍💻 예제 코드 (Data Feed 가져오기)


먼저 Openzeppelin의 Contracts Wizard를 통해 간단한 ERC721 컨트랙트를 만든다. 이후 Custom logic trigger를 사용할 수 있도록 컨트랙트를 Keepers-compatible로 만들어줘야 한다.

KeeperCompatibleAggregatorV3Interface 를 임포트 해오고 KeeperCompatibleInterface를 컨트랙트에 상속한다.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts@4.7.3/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@4.7.3/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts@4.7.3/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts@4.7.3/access/Ownable.sol";
import "@openzeppelin/contracts@4.7.3/utils/Counters.sol";

import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract BullBear is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable, KeeperCompatibleInterface {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("Bull&Bear", "BBTK") {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }
  
  //...


constructor 수정

//변수 선언
uint public interval;
uint public lastTimeStamp;

//interface 타입 변수 선언
AggregatorV3Interface public priceFeed;
int256 public currentPrice;

//디플로이 할 때 업데이트 주기와 기준이 될 종목 설정
constructor(uint updateInterval, address _priceFeed) ERC721("Bull&Bear", "BBTK") {
    interval = updateInterval;
    lastTimeStamp = block.timestamp;

    //priiceFeed에 BTC/USD Feed 컨트랙트 주소를 넣어주면 가격 트래킹이 가능하다.
    priceFeed = AggregatorV3Interface(_priceFeed);
    currentPrice = getLatestPrice();
}

자신이 트래킹하고 싶은 토큰을 나중에 디플로이 할 때 설정이 가능하다. 토큰별 컨트랙트 주소는 Ethereum Data Feeds에 잘 나와있다. 이더리움 뿐만 아니라 다른 체인들도 트래킹 가능하다.

safeMint 수정

//각 NFT URI
string[] bullUrisIpfs = [
    "https://ipfs.io/ipfs/QmcEd1R1Qq7M6G7NnHxDXYdiYtJAn9A2FGjg94Ku75HgUD?filename=gamer_bull.png",
    "https://ipfs.io/ipfs/QmUveY3PgfLD9TahrAEH2WieCG1PfU9Ybdq9WbZe8wXwvQ?filename=party_bull.png",
    "https://ipfs.io/ipfs/QmXou1mty7AqhKzxrxC5C7Xqx8yVjVi8ZEqjacUpQQ89hD?filename=simple_bull.png"
];

string[] bearUrisIpfs = [
    "https://ipfs.io/ipfs/QmSHyHr3kHKza3YRMfNz5HFatotSvLSa17YHJPzXJow1gU?filename=beanie_bear.png",
    "https://ipfs.io/ipfs/QmVqKAMsQRbNcVffMUwyzVHhcx37sx46qnFX6nnYqu1rb9?filename=coolio_bear.png",
    "https://ipfs.io/ipfs/QmNeo34iQGsPLagnw1E6LjzfgmqVNLYftCymHKv11q9QsS?filename=simple_bear.png"
];

function safeMint(address to, string memory uri) public onlyOwner {
    uint256 tokenId = _tokenIdCounter.current();
    _tokenIdCounter.increment();
    _safeMint(to, tokenId);

    //uri 디폴드로 설정
    string memory defaultUri = bullUrisIpfs[0];
    _setTokenURI(tokenId, defaultUri);
}

bull, bear용 배열을 만들어서 가격이 변할 때마다 바뀔 NFT url을 넣어준다. 앞서 만들었던 url을 활용한다. 현재는 민팅할 때 bull NFT를 디폴트로 설정했다. 나중에 performUpkeep 함수를 통해 가격이 변하면 url을 수정하도록 하면 된다.


가격에 따라 url 변경

//event 변수 선언
event TokenUpdated(string marketTrend);

//bear 또는 bull 상황에 맞춰서 tokenURI 업데이트
function updateAlltokenUris(string memory trend) internal {
    if(compareStrings("bear", trend)) {
        for (uint i=0; i<_tokenIdCounter.current(); i++) {
            _setTokenURI(i, bearUrisIpfs[0]);
        }
    } else {
        for (uint i=0; i<_tokenIdCounter.current(); i++) {
            _setTokenURI(i, bullUrisIpfs[0]);
        }
    }
    emit TokenUpdated(trend);
    }

//글자 비교 함수
function compareStrings(string memory a, string memory b) internal pure returns (bool) {
    return (keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)));
}

for 문을 사용해서 지금까지 발행된 모든 NFT의 메타데이터를 바꾼다. 현재 trend가 bear라면 그에 맞게 bearUrisIpfs 배열에 있는 url을 적용시킨다. 반대 상황이라면 bullUrisIpfs 배열을 사용한다. 마지막에 변경 사항을 알 수 있도록 emit을 통해 event를 남겨놓는다.


checkUpkeep 함수와 performUpkeep 함수

추가적으로 Keepers-compatible contracts 가 되기 위해선 checkUpkeep 함수와 performUpkeep 함수가 컨트랙트 내에 있어야 한다.

checkUpkeep 함수는 체인 외부에서 performUpkeep 함수가 실행되어야 하는지 매 블록마다 체크한다. checkUpkeep 함수가 true를 리턴하면 체인 내부에 있는 performUpkeep 함수에 작성된 트리거가 발동한다. 코드는 다음과 같다.


//리턴한 performData를 performUpkeep 함수에 넘긴다.
function checkUpkeep(bytes calldata /* checkData */) external view override returns (bool upkeepNeeded, bytes memory /* performData */) {
    //현재 시각과 최근 업데이트된 시각을 뺀 시간(그 사이의 텀)이 내가 컨트랙트를 시작할 때 설정한 업데이트 주기보다 길다면 업데이트 필요(true 리턴)
    //checkData는 Upkeep이 등록되면 정의된다.
    upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
}

function performUpkeep(bytes calldata /* performData */) external override {
    //checkUpkeep 함수에서 업데이트를 확인 했더라도 performUpkeep에서 한 번 더 확인하는 것이 좋다.
    if ((block.timestamp - lastTimeStamp) > interval ) {
        //시간을 업데이트 하고
        lastTimeStamp = block.timestamp;
            
        //최신가격을 가져온다.
        int latestPrice = getLatestPrice();
            
        if(latestPrice == currentPrice) {
            return;
        }
        //가격에 맞춰서 토큰Uri 수정
        if(latestPrice < currentPrice) {
            updateAlltokenUris("bear");
        } else {
            updateAlltokenUris("bull");
        }
        //가격 업데이트
        currentPrice = latestPrice;
    }
}

latestRoundData

최신 가격을 가져올 때 latestRoundData 가 사용된다. latestRoundDataAggregatorV3Interface 에 포함된 함수다. 여러 정보를 가져올 수 있지만 destruct해서 가격만 리턴시킨다.

 //최신가격 가져오기
 function getLatestPrice() public view returns (int256) {
     (
         /*uint80 roundID*/,
         int256 price,
         /*uint startedAt*/,
         /*uint timeStamp* 
         /*uint80 answeredInRound*/
     ) = priceFeed.latestRoundData();
     return price;
 }


🤷‍♂️ 의문점과 해결


AggregatorV3Interface는 어떻게 파라미터를 받는거지?


AggregatorV3Interface public priceFeed;
priceFeed = AggregatorV3Interface(_priceFeed);

AggregatorV3Interface 는 priceFeed 변수를 interface type으로 선언할 때 사용됐다. 그런데 파라미터를 받아서 가격을 가져와 변수에 할당할 때도 사용된다. interface type은 파라미터를 받을 수 있는건가? 이해가 잘 안 가서 소스코드를 살펴봤다.

그런데 파라미터를 받을 수 있는 곳은 없었다(???) 하지만 나와 같은 궁금증을 가진 사람이 또 있었다. ethereum stackexchange에서 의문을 해소할 수 있었다.

interface는 일종의 정해진 규격이라고 할 수 있다. interface로 선언된 변수는 interface에 포함된 함수를 호출할 수 있다. solidity에서는 interface와 address를 같이 사용하면 interface 내부의 함수를 호출할 수 있는 객체를 받는다.

위 사진에서 설명한 것처럼 컨트랙트 내부의 함수를 사용하고 싶다고 해서 컨트랙트 주소를 객체처럼 사용할 수는 없다. 메소드를 사용하려면 내부 함수를 정의한 interface에 컨트랙트 주소를 넣어서 메소드를 호출할 수 있는 객체로 만들어줘야 한다.

interface에서 주의할 점

  1. 내부 함수는 external로 선언해야 한다.
  2. 변수와 constructor는 선언할 수 없다.
  3. enum이나 struct는 선언할 수 있다.
  4. 컨트랙트 자체에 interface를 상속한다면, interface 내부에 선언된 함수는 반드시 사용되어야 한다.(이 때 override, public 상태로 사용 되어야 함)

👏 한계점


가격에 따라 NFT의 메타데이터를 바꿀 때 일괄적으로 배열의 첫 번째 항목을 적용시켰다. 발행된 모든 NFT가 한 가지로만 바뀌면 재미없으니 배열에 있는 항목들 중에서 랜덤하게 바뀌도록 변경하면 더 괜찮을 것 같다. 다음편에서는 체인링크의 VRF를 사용해서 검증된 랜덤 숫자에 따라 NFT를 바꿔보자.

전체코드는 여기서 볼 수 있다.


출처 및 참고자료


  1. 체인링크 노드 오퍼레이터란?

  2. How would we know that a parameter can be passed into the interface's constructor?

  3. Solidity 솔리디티 강좌 39강 - Interface 인터페이스

  4. Connect APIs to your Smart Contracts using Chainlink | Road to Web3

profile
Just BUIDL :)

0개의 댓글