안녕하세요, 이중혁입니다

배우고 경험한 기술들을 하나씩 정리하는 공간

Chainlink Oracle

Chainlink Price Feed 오라클

Chainlink Data Feed

역할

Chainlink Data Feed는 블록체인 외부의 실시간 데이터를 스마트 컨트랙트에 안전하게 제공하는 탈중앙화된 오라클 네트워크입니다. 여러 독립적인 노드가 데이터를 수집하고 집계하여 조작이 불가능한 신뢰할 수 있는 가격 정보를 제공합니다.

핵심 특징

  • 탈중앙화: 여러 노드의 합의 기반
  • 실시간 업데이트: 약 1분 간격
  • 고정밀도: 8자리 소수점 지원
  • 검증된 데이터: 신뢰할 수 있는 데이터 소스

지원 Price Feed (Polygon Mainnet)

MATIC/USD
0xd0D5e3DB44DE05E9F294BB0a3bEEaF030DE24Ada
ETH/USD
0x0715A7794a1dc8e42615F059dD6e406A6594651A

AggregatorV3Interface

• latestRoundData(): 최신 가격 조회
• 반환값: (roundId, price, startedAt, updatedAt, answeredInRound)
• price: 8자리 소수점 (예: 80000000 = $0.80)

AdminCoinToUsd

역할

Chainlink Price Feed를 활용하여 MATIC과 ETH의 실시간 USD 가격을 조회하는 컨트랙트입니다. Chainlink의 8자리 소수점을 Solidity 표준인 18자리로 변환하여 제공하며, 다른 컨트랙트에서 쉽게 가격 정보를 활용할 수 있도록 합니다.

핵심 책임

  • Chainlink 오라클 연동
  • MATIC/USD 가격 조회
  • ETH/USD 가격 조회
  • 정밀도 변환 (8자리 → 18자리)

주요 기능

getPrice
MATIC, ETH 가격을 동시에 조회
getMaticPrice
MATIC/USD 가격 조회
getEtherPrice
ETH/USD 가격 조회

정밀도 변환

• Chainlink: 8자리 (80000000)
• Solidity: 18자리 (800000000000000000)
• 변환: price × 10¹⁰
• 이유: ERC20 토큰 표준과 일관성

데이터 플로우

1. 데이터 수집

여러 Chainlink 노드가 거래소에서 가격 데이터를 수집합니다.

2. 데이터 집계

수집된 가격 데이터를 중앙값으로 집계하여 이상값을 제거합니다.

3. 온체인 저장

집계된 가격을 Aggregator 컨트랙트에 저장합니다.

4. 가격 조회

AdminCoinToUsd가 latestRoundData()로 최신 가격을 조회합니다.

활용 사례

  • 동적 가격 책정: NFT/토큰 가격을 USD 기준으로 설정
  • 담보 비율 계산: 대출/스테이킹 시 담보 가치 계산
  • 수수료 계산: USD 기준 수수료를 암호화폐로 변환
  • 환율 기반 거래: 실시간 환율로 스왑/교환

시스템 장점

  • 탈중앙화: 단일 실패 지점 없음
  • 신뢰성: 검증된 노드의 합의 기반
  • 조작 방지: 중앙값 집계로 이상값 제거
  • 가스 효율: view 함수로 조회 비용 없음
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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

contract AdminUsdToCoin {
    address constant usdToMatic = 0xd0D5e3DB44DE05E9F294BB0a3bEEaF030DE24Ada;
    address constant usdToEth = 0x0715A7794a1dc8e42615F059dD6e406A6594651A;

    function getPrice() public view returns (int, int) {
        return (getMaticPrice(), getEtherPrice());
    }

    function getMaticPrice() public view returns (int) {
        ( , int matic, , , ) = AggregatorV3Interface(usdToMatic).latestRoundData();
        return matic * 10 ** 10;
    }

    function getEtherPrice() public view returns (int) {
        ( , int eth, , , ) = AggregatorV3Interface(usdToEth).latestRoundData();
        return eth * 10 ** 10;
    }
}

Chainlink VRF 랜덤 생성 시스템

Chainlink VRF

역할

Chainlink VRF(Verifiable Random Function)는 블록체인에서 검증 가능한 난수를 생성하는 탈중앙화된 오라클 서비스입니다. 암호학적으로 안전하고 조작이 불가능한 난수를 제공하여 가챠, 추첨, 게임 등 공정성이 중요한 서비스에서 필수적으로 사용됩니다.

핵심 특징

  • 검증 가능성: 암호학적 증명으로 난수 검증
  • 조작 불가능: 누구도 결과를 예측하거나 조작할 수 없음
  • 탈중앙화: 여러 노드가 난수 생성 참여
  • 온체인 검증: 블록체인에서 직접 검증

VRF 작동 방식

1. 요청 (Request)
컨트랙트가 VRF Coordinator에 난수 요청
2. 생성 (Generate)
Chainlink 노드가 암호학적 난수 생성
3. 검증 (Verify)
VRF Coordinator가 증명 검증
4. 콜백 (Callback)
fulfillRandomWords로 난수 전달

RandomSeedNumber

역할

Chainlink VRF를 활용하여 검증 가능한 난수를 생성하고 관리하는 컨트랙트입니다. 가챠 시스템에서 NFT Shape의 속성을 랜덤하게 결정하기 위해 사용되며, VRFConsumerBaseV2를 상속받아 Chainlink VRF Coordinator와 통신합니다.

핵심 책임

  • Chainlink VRF 연동
  • 난수 요청 및 저장
  • 난수 상태 관리 (fulfilled 여부)
  • 생성된 난수 조회 인터페이스 제공

주요 기능

requestRandomWords
Chainlink VRF에 난수 요청
fulfillRandomWords
VRF Coordinator가 난수 전달 (콜백)
getRandomNumber
생성된 난수 조회
getRequestStatus
난수 생성 완료 여부 확인

GachaMachine (통합 예시)

GachaMachine은 RandomSeedNumber를 활용하여 NFT 가챠 시스템을 구현한 컨트랙트입니다. 사용자가 가챠 티켓을 사용하면 Chainlink VRF를 통해 공정한 난수를 생성하고, 이를 기반으로 NFT Shape의 속성(능력치, 등급 등)을 랜덤하게 결정합니다.

Shape 가챠 (VRF 사용)

  • NFT Shape 발행
  • Chainlink VRF로 난수 요청
  • 난수 기반 속성 결정
  • 검증 가능한 공정성

Number 가챠 (온체인 난수)

  • NFT Number 발행
  • blockhash 기반 난수 생성
  • 빠른 처리 속도
  • 가스 비용 절감

VRF 데이터 플로우

1. 가챠 요청

사용자가 가챠 티켓으로 Shape NFT 발행 요청

2. VRF 요청

GachaMachine이 RandomSeedNumber를 통해 난수 요청

3. 난수 생성

Chainlink 노드가 VRF로 난수 생성 및 증명

4. 콜백 전달

VRF Coordinator가 난수를 fulfillRandomWords로 전달

5. 속성 결정

난수를 기반으로 NFT Shape의 속성 결정

활용 사례

  • NFT 가챠: 랜덤 속성의 NFT 발행
  • 추첨 시스템: 공정한 당첨자 선정
  • 게임 보상: 예측 불가능한 보상 결정
  • 랜덤 이벤트: 게임 내 랜덤 이벤트 발생

시스템 장점

  • 검증 가능: 암호학적 증명으로 공정성 검증
  • 조작 불가능: 개발자도 결과를 조작할 수 없음
  • 신뢰성: 탈중앙화 노드 네트워크
  • 투명성: 모든 난수 생성 과정이 온체인에 기록
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import "../utils/PortfolioSuperOperators.sol";

contract RandomSeedNumber is VRFConsumerBaseV2, PortfolioSuperOperators {
    event RequestSent(address indexed userAddress, uint256 indexed gachaTicketTokenId, uint256 indexed tokenId, uint256 requestId);
    event RequestFulfilled(address indexed userAddress, uint256 indexed requestId, uint256[] indexed randomWords);

    struct RequestStatus {
        bool fulfilled;
        bool exists;
        uint256[] randomWords;
    }

    mapping(uint256 => RequestStatus) public s_requests;
    VRFCoordinatorV2Interface COORDINATOR;
    uint64 s_subscriptionId;

    uint256[] public requestIds;
    uint256 public lastRequestId;
    
    bytes32 keyHash;
    uint32 callbackGasLimit = 100000;
    uint16 requestConfirmations = 3;
    uint32 numWords = 1;
    address vrfCoordinator;

    constructor(
        bytes32 _keyHash,
        address _vrfCoordinator,
        uint64 subscriptionId,
        string memory operator,
        address portfolioAdmin
    )
    VRFConsumerBaseV2(_vrfCoordinator) PortfolioSuperOperators(operator, portfolioAdmin)
    {
        keyHash = _keyHash;
        vrfCoordinator = _vrfCoordinator;
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        s_subscriptionId = subscriptionId;
    }

    function requestRandomWords(address userAddress, uint256 gachaTicketTokenId, uint256 tokenId)
    external onlySuperOperator returns (uint256)
    {
        uint256 requestId = COORDINATOR.requestRandomWords(
            keyHash,
            s_subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
        s_requests[requestId] = RequestStatus({
            randomWords : new uint256[](0),
            exists : true,
            fulfilled : false
        });
        requestIds.push(requestId);
        lastRequestId = requestId;
        emit RequestSent(userAddress, gachaTicketTokenId, tokenId, requestId);
        return requestId;
    }

    function fulfillRandomWords(
        uint256 _requestId,
        uint256[] memory _randomWords
    ) internal override {
        require(s_requests[_requestId].exists, "request not found");
        s_requests[_requestId].fulfilled = true;
        s_requests[_requestId].randomWords = _randomWords;
        emit RequestFulfilled(msg.sender, _requestId, _randomWords);
    }

    function getRequestStatus(
        uint256 _requestId
    ) external view returns (bool fulfilled) {
        require(s_requests[_requestId].exists, "request not found");
        RequestStatus memory request = s_requests[_requestId];
        return request.fulfilled;
    }

    function getRequestStatusMany(uint256[] memory _requestIdArray) external view returns (bool[] memory fulfilled) {
        bool[] memory fulfilledResults = new bool[](_requestIdArray.length);
        for (uint i = 0; i < _requestIdArray.length; i++) {
            require(s_requests[_requestIdArray[i]].exists, "request not found");
            RequestStatus memory request = s_requests[_requestIdArray[i]];
            fulfilledResults[i] = request.fulfilled;
        }
        return fulfilledResults;
    }

    function getRandomNumber(
        uint256 _requestId
    ) external view returns (uint256 randomWord) {
        require(s_requests[_requestId].exists, "request not found");
        RequestStatus memory request = s_requests[_requestId];
        return request.randomWords[0];
    }
}