在 Solidity 智能合约中调用 Chainlink 预言机获取外部数据及生成随机数

  • Blue
  • 更新于 2023-12-20 18:02
  • 阅读 4424

在我们做去中心化应用开发时,其中有两个头疼的问题,一是想要获取去中心化的一些数据,如代币价格等,还有一个就是安全的创建随机数,这篇文章带大家了解一下这两块的实际应用。

在我们做去中心化应用开发时,其中有两个头疼的问题,一是想要获取去中心化的一些数据,如代币价格等,还有一个就是安全的创建随机数,这篇文章带大家了解一下这两块的实际应用。

一、调用 Chainklink 获取外部数据

1. Chainlink 预言机介绍

由于智能合约无法调用外部 API 这一特性,所以诞生了预言机这个机制用来帮助智能合约获取外部数据,除了应用最广泛的价格数据以外,还包括一些天气数据,体育比赛数据,股票市场数据,交通数据,甚至包括总统选结果等数据。

除了提供数据,预言机广义上的功能也包括提供随机数和作为触发器实现智能合约执行,它们都算是链下的工具来和链上的合约进行交互。

Chainlink 是目前预言机数据的最大提供平台。

2. Chainlink 获取数据示例

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

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

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED
 * VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

/**
 * If you are reading data feeds on L2 networks, you must
 * check the latest answer from the L2 Sequencer Uptime
 * Feed to ensure that the data is accurate in the event
 * of an L2 sequencer outage. See the
 * https://docs.chain.link/data-feeds/l2-sequencer-feeds
 * page for details.
 */

contract DataConsumerV3 {
    AggregatorV3Interface internal dataFeed;

    /**
     * Network: Sepolia
     * Aggregator: BTC/USD
     * Address: 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
     */
    constructor() {
        dataFeed = AggregatorV3Interface(
            0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
        );
    }

    /**
     * Returns the latest answer.
     */
    function getPrice() public view returns (int) {
        // prettier-ignore
        (
            /* uint80 roundID */,
            int answer,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        ) = dataFeed.latestRoundData();
        return answer / (10 ** 8);
    }
}

上面是一个获取BTC/USD价格的智能合约示例。

  • 首先 import名为AggregatorV3Interface的接口,这个接口由chainlink官方的包提供。
  • constructor构造函数中,初始化一个名为dataFeedAggregatorV3Interface接口对象,在初始化的接口函数中传入Chainklink部署的对应BTC/USD价格的合约地址,这里因为是用的Sepolia测试网,具体每个网络可以获取哪些价格数据的合约地址,点此查看
  • 最后通过一个getPrice函数调用dataFeedlatestRoundData方法来获取价格,返回的answer参数就是具体的价格,但每个交易对的金额精度不一样,一般要除以金额精度才是真正的USD单位,如这里的精度是8,最终answer需要除以10 ** 8才是USD

3. 通过 Remix 部署合约测试

我们的智能合约需要调用Sepolia其他智能合约,所以没办法直接在本地测试,需要部署到Sepolia测试网上,建议通过Remix来部署

image.png

首先在MetaMask钱包中添加Sepolia网络,然后在Remix部署界面选择使用MetaMask钱包部署,最后点击Deploy按钮部署。

image.png

部署成功后就可以看到我们已经部署的智能合约,执行getPrice方法来获取价格。

更详细的获取外部数据文档,点此查看官方文档

二、调用 Chainlink 生成随机数

1. Solidity 直接生成随机数

/** 
* 链上伪随机数生成
* 利用keccak256()打包一些链上的全局变量/自定义变量
* 返回时转换成uint256类型
*/
function getRandomOnchain() public view returns(uint256){
    // remix运行blockhash会报错
    bytes32 randomBytes = keccak256(abi.encodePacked(block.number, msg.sender, blockhash(block.timestamp-1)));
    return uint256(randomBytes);
}

主要是利用keccak256函数根据区块高度当前调用人时间戳等生成随机数,但是这种方法并不安全,因为结果对于矿工是可预测的,如我们做一款Web3彩票项目,矿工可以选择根据这个生成结果是否中奖选择是否上报这个区块。

2. 调用 Chainklink VRF 生成随机数示例

// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFV2WrapperConsumerBase.sol";

/**
 * Request testnet LINK and ETH here: https://faucets.chain.link/
 * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
 */

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

contract VRFv2DirectFundingConsumer is
    VRFV2WrapperConsumerBase,
    ConfirmedOwner
{
    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(
        uint256 requestId,
        uint256[] randomWords,
        uint256 payment
    );

    struct RequestStatus {
        uint256 paid; // amount paid in link
        bool fulfilled; // whether the request has been successfully fulfilled
        uint256[] randomWords;
    }
    mapping(uint256 => RequestStatus)
        public s_requests; /* requestId --> requestStatus */

    // past requests Id.
    uint256[] public requestIds;
    uint256 public lastRequestId;

    // Depends on the number of requested values that you want sent to the
    // fulfillRandomWords() function. Test and adjust
    // this limit based on the network that you select, the size of the request,
    // and the processing of the callback request in the fulfillRandomWords()
    // function.
    uint32 callbackGasLimit = 100000;

    // The default is 3, but you can set this higher.
    uint16 requestConfirmations = 3;

    // For this example, retrieve 2 random values in one request.
    // Cannot exceed VRFV2Wrapper.getConfig().maxNumWords.
    uint32 numWords = 2;

    // Address LINK - hardcoded for Sepolia
    address linkAddress = 0x779877A7B0D9E8603169DdbD7836e478b4624789;

    // address WRAPPER - hardcoded for Sepolia
    address wrapperAddress = 0xab18414CD93297B0d12ac29E63Ca20f515b3DB46;

    constructor()
        ConfirmedOwner(msg.sender)
        VRFV2WrapperConsumerBase(linkAddress, wrapperAddress)
    {}

    function requestRandomWords()
        external
        onlyOwner
        returns (uint256 requestId)
    {
        requestId = requestRandomness(
            callbackGasLimit,
            requestConfirmations,
            numWords
        );
        s_requests[requestId] = RequestStatus({
            paid: VRF_V2_WRAPPER.calculateRequestPrice(callbackGasLimit),
            randomWords: new uint256[](0),
            fulfilled: false
        });
        requestIds.push(requestId);
        lastRequestId = requestId;
        emit RequestSent(requestId, numWords);
        return requestId;
    }

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

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

    /**
     * Allow withdraw of Link tokens from the contract
     */
    function withdrawLink() public onlyOwner {
        LinkTokenInterface link = LinkTokenInterface(linkAddress);
        require(
            link.transfer(msg.sender, link.balanceOf(address(this))),
            "Unable to transfer"
        );
    }
}

这是一个Sepolia测试网的示例,每个网络下linkAddresswrapperAddress地址不同, 每个网络具体可以 点此查看

3. 部署及测试

image.png

部署的时候同样选择MetaMask连接到Sepolia网络,部署好了后,我们还需要给部署好的合约转入LINK代币(很重要的)。

image.png

复制这个合约地址,用MetaMask钱包给这个地址转入LINK代币2-3个左右(测试用)。

image.png

转入成功后就可以调用requestRandomWords方法,它会生成一个requestId,这个requestId可以通过调用lastRequestId方法查看。

最后通过给getRequestStatus方法传入requestId参数并调用就可以查看Chainlink响应给我们的随机字符串了。

具体的获取随机数官方文档,点此查看

这样我们就最终实现了在Solidity 智能合约中调用 Chainklink 获取外部数据及生成随机数,如果各位小伙伴有什么问题,也欢迎留言哦!

点赞 2
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

3 条评论

请先 登录 后评论
Blue
Blue
0xA108...9999
积极主动,顺势而为!