本文最后更新于 2024-05-14,本文发布时间距今超过 90 天, 文章内容可能已经过时。最新内容请以官方内容为准

Call VRF By Direct Funding

Static Badge
Static Badge

简介

Chainlink VRF (Verifiable Random Function) 的直接资助(Direct Funding)工作流程允许智能合约直接用 LINK 或者 native tokens 来支付随机数请求的费用。以下是详细的工作流程,包括合约之间的交互和资金流动:

  1. 部署 VRF 相关合约

    • VRFCoordinatorV2:负责管理 VRF 请求和响应。
    • VRFV2Wrapper:提供了一个接口,允许智能合约通过直接资助方式请求随机数。
  2. 配置 VRFWrapper

    • VRFWrapper 需要被配置,包括设置 gas 费用上限、wrapper 和 coordinator 的 gas overhead 等参数。
  3. 资助合约

    • 合约所有者需要向 VRFV2Wrapper 合约中转入足够的 LINK 或 native tokens,以支付将来的 VRF 请求费用。
  4. 请求随机数

    • 智能合约(消费者)通过调用 VRFV2Wrapper 的 requestRandomness 函数来请求随机数。这会触发一个 VRF 请求,并将请求者的地址和请求详情存储在 VRFCoordinatorV2 中。
  5. 计算价格

    • VRFV2Wrapper 会估算请求随机数所需的费用,包括 gas 成本和 VRF 服务费。
  6. 支付费用

    • 请求随机数的智能合约必须有足够的资金来支付这个费用。资金会从智能合约的账户中直接转移到 VRFV2Wrapper。
  7. VRF 值生成

    • VRFCoordinatorV2 会选择一个合适的区块,使用区块的哈希值和预定义的密钥来生成一个可验证的随机数。
  8. 履行请求

    • VRFCoordinatorV2 会在预定义的区块确认数后调用 VRFV2Wrapper 的 fulfillRandomWords 函数来提供随机数。
  9. 智能合约接收随机数

    • VRFV2Wrapper 接收随机数后,会调用智能合约的回调函数,传递随机数作为参数。
  10. 智能合约使用随机数

    • 智能合约可以使用接收到的随机数来执行预定的逻辑,例如随机分配资源或生成游戏结果。
  11. 费用结算

    • 一旦随机数请求被履行,VRFCoordinatorV2 会从 VRFV2Wrapper 中扣除相应的费用。

在整个流程中,资金流动如下:

  • 资助:合约所有者向 VRFV2Wrapper 转入 LINK 或 native tokens。
  • 支付:智能合约请求随机数时,资金从 VRFV2Wrapper 转移到 VRFCoordinatorV2,用于支付生成随机数的费用。

这个流程确保了随机数的生成是可验证的、公平的,并且不能被任何单个实体操纵。同时,它也保证了资金的流向清晰,智能合约可以准确地为服务支付费用。

VRF 请求成本计算步骤

  1. 确定 Gas 相关参数

    • Gas Price: 网络当前的 Gas 价格(以 wei 计)。
    • Callback Gas: 回调请求所使用的 Gas 数量。
    • Verification Gas: 链上验证随机数所使用的 Gas 数量。
  2. 计算总 Gas 成本
    使用以下公式计算总 Gas 成本(以 wei 计):

    Total Gas Cost (wei)=Gas Price×(Verification Gas+Callback Gas)
    
  3. 转换 Gas 成本为 LINK
    使用 ETH/LINK 汇率将 Gas 成本转换为 LINK:

    Total Gas Cost (LINK) = Total Gas Cost (wei) / ETH/LINK Conversion Rate (wei per LINK)
    
  4. 添加 LINK Premium
    根据协调合约中定义的 fulfillmentFlatFeeLinkPPMTier1 参数,将 LINK Premium 添加到总 Gas 成本中:

    Total Request Cost (LINK) = Total Gas Cost (LINK)+LINK Premium
    
  5. 计算最小订阅余额
    为了处理 Gas 价格波动,需要在订阅余额中维持足够的 LINK,以覆盖最大 Gas 成本和 Premium:

     Minimum Subscription Balance = Maximum Request Cost (LINK)
    

以太坊上的 VRF 请求成本示例

假设以下参数:

  • Gas Lane: 500 gwei
  • Callback Gas Limit: 100,000
  • Max Verification Gas: 200,000
  • LINK Premium: 0.25 LINK
  • ETH/LINK Conversion Rate: 1 LINK = 0.004 ETH
  1. 计算总 Gas 成本

    Total Gas Cost (wei)= 500 × 10^−9 ETH/wei × (200,000 + 100,000) = 150,000,000 wei
    
  2. 转换为 LINK

    Total Gas Cost (LINK)= 150,000,000 wei / 1,000,000,000,000 wei/ETH × 0.004 ETH/LINK = 0.6LINK
    
  3. 加上 LINK Premium

    Total Request Cost (LINK) = 0.6 LINK + 0.25 LINK = 0.85 LINK
    

    因此,为了处理这个 VRF 请求,你需要至少 0.85 LINK 的余额。

  4. Arbitrum 特殊说明

    对于 Arbitrum,交易成本包括 L2 Gas 成本和 L1 成本。Arbitrum 交易以批次的形式发布到 L1 Ethereum,这会产生 L1 成本。对于个别交易,总成本包括包含该交易的批次发布到 L1 的部分 L1 成本。

注意事项

  • 以上计算是一个简化示例,实际的计算可能需要考虑额外的因素,如 fallbackWeiPerUnitLink 值。
  • 所有数值都是假设的,实际的计算需要根据实时的 Gas 价格和 ETH/LINK 汇率进行。
  • 确保在订阅账户中有足够的 LINK 余额,以处理 Gas 价格波动和 Premium。

通过上述步骤,你可以估算出进行一次 VRF 请求所需的成本,并确保你的订阅账户中有足够的资金来支付这些成本。

实战

🤔 想要实现的效果

  1. 🤝 用户通过 LINK 或原生代币直接与智能合约互动,支付 VRF 请求费用。
  2. 🎲 智能合约发出随机数请求,并成功接收。
  3. 🎯 利用接收到的随机数,智能合约内部执行特定逻辑,比如将其转换为骰子的点数。

🛠️ 实现逻辑

  1. 直接支付逻辑

    1. 💰 两种策略:
      1. 先充值后消费 – 用户预先向合约充值 LINK 或原生代币,随后合约利用这部分资金调用 VRF 服务。 (本文采用该逻辑)
      2. 即付即用 – 用户直接调用 VRF 请求,合约根据费用即时从用户处收取 LINK 或原生代币。
    2. 🔐 不论哪种方式,事先都需要用户通过Approve操作授权智能合约,以便从其账户划转资金。
  2. 请求&接收随机数

    1. 📡 合约集成VRFV2WrapperConsumerBase,个性化定制requestRandomWords函数,处理随机数请求流程。
  3. 逻辑处理

    1. 🧮 重写fulfillRandomWords函数,当 VRF 服务回调时,根据随机数结果执行内部逻辑,比如转换成骰子点数。

📃 概念图示以及实际代码

graph LR
    A[用户] --> |Approve| B(VRF合约)
    B --> |Fund LINK| C(预存入LINK)
    C --> |Request Randomness| D(VRF服务)
    D --> |generate/verify/callback| E(合约处理)
    E --> |映射为骰子点数| F(结果)
  • A(用户)通过Approve操作连接到B(VRF 合约)。
  • B通过Fund (LINK)操作连接到C(预存入 LINK)。
  • C通过Request Randomness操作连接到D(VRF 服务)。
  • D通过generate/verify/callback操作连接到E(合约处理)。
  • E通过映射为骰子点数操作连接到F(结果)。
// VRFDirectFundingV2ForDicing.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {VRFV2WrapperConsumerBase} from "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol";
import {VRFRequestMapping} from "./VRFRequestMapping.sol";

/**
 * @title VRFDirectFundingV2ForDicing
 * @dev 本智能合约旨在为基于区块链的骰子游戏或其他需要随机数功能的应用提供直接资金支持和可验证随机数(VRF)服务。
 * 它集成了 Chainlink VRF v2 直接资金功能,允许用户通过智能合约直接请求随机数并支付费用,无需预先订阅服务。
 * 此外,该合约支持资金管理,允许用户作为资助者存入 LINK 代币,用于支付 VRF 请求的费用,并能够提取剩余资金。
 *
 * 关键特性:
 * - **随机数请求与管理:** 合约继承自 VRFRequestMapping,用于追踪 VRF 请求的状态,包括请求者、是否已付款以及是否已完成等信息。
 * - **资金管理:** 实现了一套机制,允许用户存入和提取 LINK 代币,为 VRF 请求预先提供资金,并确保费用支付流程透明高效。
 * - **事件通知:** 合约通过事件(如 FundingReceived, FundingWithdrawn, RequestSent, RequestFulfilled 等)通知链上操作的结果。
 * - **权限控制:** 引入了访问控制逻辑,确保只有合约所有者或指定的资助者才能执行某些敏感操作。
 * - **错误处理:** 定义了多个自定义错误类型,以精确地捕获并响应不同类型的异常情况。
 *
 * 注意:
 * - 本合约是为特定场景定制的,并依赖于 Chainlink VRF 服务,使用前需配置正确的 VRF Wrapper 地址和 LINK 代币合约地址。
 * - 合约的经济模型和权限管理需要根据实际应用场景做适当调整。
 */
contract VRFDirectFundingV2ForDicing is VRFRequestMapping, VRFV2WrapperConsumerBase {
    // 定义自定义错误,用于在特定条件下抛出异常
    error VRFDirectFundingV2ForDicing__RequestNotFound(uint256 _requestId);
    error VRFDirectFundingV2ForDicing__OnlyOwnerOrFunder(address sender, address owner);
    error VRFDirectFundingV2ForDicing__InsufficientFunds(address funder, int256 balance, uint256 paid);
    error VRFDirectFundingV2ForDicing__LinkTransferError(address sender, address receiver, int256 amount);

    // 事件声明,用于记录资金提取、接收和请求状态变化。
    event FundingWithdrawn(address indexed funder, uint256 amount);
    event FundingReceived(address indexed funder, uint256 amount);
    event RequestSent(address indexed requester, uint256 indexed requestId, uint256 paid);
    event RequestFulfilled(
        address indexed requester, uint256 indexed requestId, uint8 indexed dice, uint256 randomWord, uint256 payment
    );

    // 存储资助者的 LINK 余额
    mapping(address => int256) private s_funders; /* funder => Link amount */

    /**
     * @notice onlyOwnerOrFunder 修饰符,限制只有合约所有者或资助者可以执行修饰的函数。
     */
    modifier onlyOwnerOrFunder() {
        // not owner or funder with non-positive balance will revert
        if (msg.sender != owner() && s_funders[msg.sender] <= 0) {
            revert VRFDirectFundingV2ForDicing__OnlyOwnerOrFunder(msg.sender, owner());
        }
        _;
    }

    // configuration: https://docs.chain.link/vrf/v2/direct-funding/supported-networks#configurations
    // _linkAddress: 0x779877A7B0D9E8603169DdbD7836e478b4624789
    // _wrapperAddress: 0xab18414CD93297B0d12ac29E63Ca20f515b3DB46
    /**
     * @notice 构造函数,初始化合约并设置 LINK 代币合约和 VRF v2 Wrapper 合约的地址。
     * @param _linkAddress LINK 代币合约的地址。
     * @param _wrapperAddress VRF v2 Wrapper 合约的地址。
     */
    constructor(address _linkAddress, address _wrapperAddress)
        VRFRequestMapping(msg.sender)
        VRFV2WrapperConsumerBase(_linkAddress, _wrapperAddress)
    {}

    /**
     * @notice requestRandomWords 函数,允许合约所有者或资助者请求 VRF 随机数。
     * @param _callbackGasLimit 用于计算请求价格的最大 gas 限制。
     * @param _requestConfirmations VRF 请求的最小确认数。
     * @return requestId VRF 请求的唯一标识符。
     */
    function requestRandomWords(uint32 _callbackGasLimit, uint16 _requestConfirmations)
        external
        onlyOwnerOrFunder
        returns (uint256 requestId)
    {
        requestId = requestRandomness(_callbackGasLimit, _requestConfirmations, 1);
        uint256 paid = VRF_V2_WRAPPER.calculateRequestPrice(_callbackGasLimit);
        // uint256 balance = LINK.balanceOf(address(this));
        uint256 balance = uint256(s_funders[msg.sender]);
        if (balance < paid) {
            revert VRFDirectFundingV2ForDicing__InsufficientFunds(msg.sender, int256(balance), paid);
        }
        mappingRequestStatus(msg.sender, requestId, paid);
        s_funders[msg.sender] -= int256(paid);
        emit RequestSent(msg.sender, requestId, paid);
        return requestId;
    }

    /**
     * @notice fulfillRandomWords 函数,用于在 VRF 服务响应时由 VRF Coordinator 调用,提供随机数。
     * @param _requestId VRF 请求的唯一标识符。
     * @param _randomWords 随机数数组。
     */
    function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override {
        RequestStatus storage requestStatus = s_requestStatus[_requestId];
        if (requestStatus.paid == 0) {
            revert VRFDirectFundingV2ForDicing__RequestNotFound(_requestId);
        }
        requestStatus.fulfilled = true;
        requestStatus.randomWord = _randomWords[0];
        requestStatus.dice = uint8(_randomWords[0] % 6) + 1;
        emit RequestFulfilled(
            requestStatus.requester, _requestId, requestStatus.dice, _randomWords[0], requestStatus.paid
        );
    }

    /**
     * @notice getRequestStatus 函数,允许外部查询特定 VRF 请求的状态。
     * @param _requestId VRF 请求的唯一标识符。
     * return 请求状态信息,包括请求者、是否已履行、随机数、骰子号码和已支付金额。
     */
    function getRequestStatus(uint256 _requestId)
        external
        view
        returns (address requester, bool fulfilled, uint256 randomWord, uint8 dice, uint256 paid)
    {
        RequestStatus memory requestStatus = s_requestStatus[_requestId];
        if (requestStatus.paid == 0) revert VRFDirectFundingV2ForDicing__RequestNotFound(_requestId);
        return (
            requestStatus.requester,
            requestStatus.fulfilled,
            requestStatus.randomWord,
            requestStatus.dice,
            requestStatus.paid
        );
    }

    // 资助者相关函数,包括存入 LINK 代币、提取 LINK 代币和合约所有者退还资助者 LINK 代币

    /**
     * @notice fundLink 函数,允许资助者向合约存入 LINK 代币。
     * @notice 注意:实际应用场景中,资助者需要先向合约授权足够的 LINK 代币,然后调用 fundLink 函数来存入。
     * 用户需要先向合约授权足够的 LINK 代币,然后调用 fundLink 函数来存入。
     * - LINK on Sepolia testnet: https://sepolia.etherscan.io/token/0x779877A7B0D9E8603169DdbD7836e478b4624789#writeContract
     * 1. 打开上面的 LINK 链接, 用户找到 Write Contract/approve 按钮,输入要授权的 LINK 代币数量 (10 LINK = 10 * 1e18)
     * 2. 设置 spender 为该合约地址, 例如: 0x4AFeD5d23322F21e31cEcF300cB43E714Ef8eA31
     * 3. 点击 Approve 按钮, 并且 wallet 确认.
     * 3. 授权完成后,用户就可以调用 fundLink 函数来存入 LINK 代币, 数量需要 <= approved 数量 (10 LINK = 10 * 1e18).
     * @param _amount 要存入的 LINK 代币数量。
     */
    function fundLink(uint256 _amount) external {
        // 检查是否已经授权了足够的 LINK 代币
        if (LINK.allowance(msg.sender, address(this)) < _amount) {
            // 如果没有授权或授权不足
            // owner, spender, amount
            revert VRFDirectFundingV2ForDicing__LinkTransferError(msg.sender, address(this), int256(_amount));
        }

        // 执行 transferFrom 函数,将 LINK 代币从用户账户转移到合约账户
        bool transferSuccess = LINK.transferFrom(msg.sender, address(this), _amount);
        if (!transferSuccess) {
            revert VRFDirectFundingV2ForDicing__LinkTransferError(msg.sender, address(this), int256(_amount));
        }

        // 更新资助者的 LINK 余额
        s_funders[msg.sender] += int256(_amount);

        // 发出资金接收事件
        emit FundingReceived(msg.sender, _amount);
    }

    /**
     * @notice withdrawLINK 函数,允许资助者提取他们的 LINK 代币。
     * @param _amount 要提取的 LINK 代币数量。
     */
    function withdrawLINK(uint256 _amount) external onlyOwnerOrFunder {
        if (s_funders[msg.sender] < int256(_amount)) {
            revert VRFDirectFundingV2ForDicing__InsufficientFunds(msg.sender, s_funders[msg.sender], _amount);
        }
        s_funders[msg.sender] -= int256(_amount);
        if (!LINK.transfer(msg.sender, _amount)) {
            revert VRFDirectFundingV2ForDicing__LinkTransferError(address(this), msg.sender, int256(_amount));
        }
        emit FundingWithdrawn(msg.sender, _amount);
    }

    /**
     * @notice refundFunder 函数,允许合约所有者退还资助者的 LINK 代币。
     * @param _funder 资助者的地址。
     * @param _amount 要退还的 LINK 代币数量。
     */
    function refundFunder(address _funder, uint256 _amount) external OnlyOwnerOrCollaborators {
        if (s_funders[_funder] < int256(_amount)) {
            revert VRFDirectFundingV2ForDicing__InsufficientFunds(_funder, s_funders[_funder], _amount);
        }
        s_funders[_funder] -= int256(_amount);
        if (!LINK.transfer(_funder, _amount)) {
            revert VRFDirectFundingV2ForDicing__LinkTransferError(address(this), _funder, int256(_amount));
        }
        emit FundingWithdrawn(_funder, _amount);
    }

    // 获取资助者 LINK 余额和合约 LINK 余额的函数

    /**
     * @notice getLinkBalance 函数,获取资助者的 LINK 余额。
     * @param _funder 资助者的地址。
     * @return 资助者的 LINK 余额。
     */
    function getLinkBalance(address _funder) external view returns (uint256) {
        return uint256(s_funders[_funder]);
    }

    /**
     * @notice getLinkBalanceOfContract 函数,获取合约的 LINK 余额。
     * @return 合约的 LINK 余额。
     */
    function getLinkBalanceOfContract() external view returns (uint256) {
        return LINK.balanceOf(address(this));
    }

    // 仅用于测试目的的函数

    /**
     * @notice withdrawAllOfContract 函数,允许合约所有者提取合约中的所有 LINK 代币和以太币。
     * 部署到测试网络后,该测试函数会导致的问题: owner 取出所有 balance时, s_funders 这个 mapping 里余额信息未被处理清空
     * 进而会导致后续已经存在的 requester fundLink 之后的数额 >= 真实数额.
     * 因此,该函数仅用于测试目的,不建议在生产环境中使用。
     */
    function withdrawAllOfContract() public onlyOwner {
        require(LINK.transfer(msg.sender, LINK.balanceOf(address(this))), "Unable to transfer");
        address payable owner = payable(owner());
        require(owner.send(address(this).balance), "Unable to send ether");
    }
}

// VRFRequestMapping.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

/**
 * @title VRFRequestMapping
 * @dev 这个抽象合约用作实现一个服务的基础,该服务管理对可验证随机函数(VRF)提供者的请求,通常是 Chainlink 的 VRF 服务。
 * 它扩展了 `ConfirmedOwner` 合约以确保操作由合约所有者或指定的合作伙伴授权。
 * 该合约的主要目的是促进请求随机数的过程,跟踪它们的履行状态,并将它们与请求者关联。
 *
 * 关键特性:
 * - **请求管理:** 允许请求者发起 VRF 请求并跟踪每个请求的状态,包括是否已付款和已履行。
 * - **合作伙伴系统:** 允许合约所有者指定其他地址作为合作伙伴,他们可以执行删除请求等操作。
 * - **事件发射:** 发射事件以信号何时删除请求者,促进链下监控。
 * - **数据检索:** 提供函数以获取有关请求者及其关联的请求 ID 的信息。
 *
 * 函数包括:
 * - `mappingRequestStatus`:内部记录请求的详细信息,如请求者、付款和履行状态。
 * - `addCollaborator` & `removeCollaborator`:管理列表,授权地址与合约所有者一起执行操作。
 * - `deleteRequest(uint256)`:给定其 ID,删除特定的已履行请求。
 * - `deleteRequest(address)`:删除与给定请求者关联的所有请求。
 * - `getRequesterByRequestId`:根据请求 ID 检索请求者的地址。
 * - `getRequestIdsByRequester`:返回由特定请求者发起的请求 ID 数组。
 *
 * 错误处理:
 * - 定义了自定义错误以处理以下场景:尝试删除未完成的请求,未经授权的地址尝试操作,或通过 ID 查找不存在的请求者。
 *
 * 注意:此合约是抽象的,旨在被具体实现继承,后者定义了如何发起和履行 VRF 请求,可直接与 Chainlink 的 VRF 相关合约集成。
 */
abstract contract VRFRequestMapping is ConfirmedOwner {
    // 定义自定义错误,用于在特定条件下抛出异常
    error VRFRequestMapping__RequesterNotFoundById(uint256 _requestId);
    error VRFRequestMapping__RequestHasNotBeenFulfilled(address _requester, uint256 _requestId);
    error OnlyOwnerOrCollateralOwner(address _opeartor);
    error VRFRequestMapping__OnlyRequester(address _opeartor);

    // 事件声明,用于在删除请求者时触发事件
    event VRFRequestMapping__RequesterDeleted(address _requester);

    constructor(address _owner) ConfirmedOwner(_owner) {}

    /**
     * @notice OnlyOwnerOrCollaborators 修饰符,限制只有合约所有者或合作伙伴可以执行修饰的函数。
     */
    modifier OnlyOwnerOrCollaborators() {
        if (msg.sender != owner() && !s_collateralOwners[msg.sender]) {
            revert OnlyOwnerOrCollateralOwner(msg.sender);
        }
        _;
    }

    // 合作伙伴地址到布尔值的映射,表示是否为合作伙伴
    mapping(address => bool) internal s_collateralOwners;

    /**
     * @notice mappingRequestStatus 内部函数,用于初始化请求状态并将其与请求者关联。
     * @param _requester 请求的发起者地址。
     * @param _requestId 请求的 ID。
     * @param _paid 已支付的 LINK 数量。
     */
    struct RequestStatus {
        address requester;
        uint256 paid; // amount paid in link
        bool fulfilled; // whether the request has been successfully fulfilled
        uint256 randomWord; // the randomWord got from Chainlink VRF
        uint8 dice; // mod the randomWord to get the dice number
    }

    mapping(uint256 => RequestStatus) internal s_requestStatus; /* requestId => RequestStatus */
    mapping(address => uint256[]) internal s_VRFRequestMappings; /* requester => requestId[] */

    /**
     * @notice mappingRequestStatus 内部函数,用于初始化请求状态并将其与请求者关联。
     * @param _requester 请求的发起者地址。
     * @param _requestId 请求的 ID。
     * @param _paid 已支付的 LINK 数量。
     */
    function mappingRequestStatus(address _requester, uint256 _requestId, uint256 _paid) internal {
        s_requestStatus[_requestId] =
            RequestStatus({requester: _requester, paid: _paid, fulfilled: false, randomWord: 0, dice: 0});
        s_VRFRequestMappings[_requester].push(_requestId);
    }

    // 允许合约所有者添加合作伙伴
    function addCollaborator(address _collaborator) external onlyOwner {
        s_collateralOwners[_collaborator] = true;
    }

    // 允许合约所有者移除合作伙伴
    function removeCollaborator(address _collaborator) external onlyOwner {
        s_collateralOwners[_collaborator] = false;
    }

    /**
     * @notice deleteRequest 公共函数,用于删除特定请求及其相关信息。
     * @param _requestId 要删除的请求的 ID。
     */
    function deleteRequest(uint256 _requestId) public OnlyOwnerOrCollaborators {
        // Retrieve the request information
        RequestStatus storage request = s_requestStatus[_requestId];
        // If the request has not been fulfilled yet, revert
        if (!request.fulfilled) {
            revert VRFRequestMapping__RequestHasNotBeenFulfilled(request.requester, _requestId);
        }

        address requester = request.requester;
        uint256[] storage requesterIds = s_VRFRequestMappings[requester];

        // Traverse the array of request IDs for the requester, find and delete the specified request ID
        for (uint256 i = 0; i < requesterIds.length; i++) {
            if (requesterIds[i] == _requestId) {
                requesterIds[i] = requesterIds[requesterIds.length - 1];
                requesterIds.pop();
                break;
            }
        }

        // Delete the request information
        delete s_requestStatus[_requestId];
    }

    /**
     * @notice deleteRequest 公共函数,用于删除特定请求者的所有请求。
     * @param _requester 要删除其请求的请求者的地址。
     */
    function deleteRequest(address _requester) public OnlyOwnerOrCollaborators {
        uint256[] memory requestIds = s_VRFRequestMappings[_requester];
        for (uint256 i = 0; i < requestIds.length; i++) {
            delete s_requestStatus[requestIds[i]];
        }
        emit VRFRequestMapping__RequesterDeleted(_requester);
        delete s_VRFRequestMappings[_requester];
    }

    /**
     * @notice getRequesterByRequestId 公共函数,根据请求 ID 获取请求者地址。
     * @param _requestId 请求的 ID。
     * @return 请求者的地址。
     */
    function getRequesterByRequestId(uint256 _requestId) public view returns (address) {
        address requester = s_requestStatus[_requestId].requester;
        if (requester == address(0)) {
            revert VRFRequestMapping__RequesterNotFoundById(_requestId);
        }
        return requester;
    }

    /**
     * @notice getRequestIdsByRequester 公共函数,根据请求者地址获取其所有请求 ID。
     * @param _requester 请求者的地址。
     * @return 请求者所有请求的 ID 数组。
     */
    function getRequestIdsByRequester(address _requester) public view returns (uint256[] memory) {
        return s_VRFRequestMappings[_requester];
    }
}

🤖 部署署合约

💎 本文采用的测试链为:Sepolia Testnet

  1. 🏃‍💻 打开 Remix
  2. 📄 根据上面代码,创建两个对应的合约.sol文件。
    1. VRFDirectFundingV2ForDicing.sol
    2. VRFRequestMapping.sol
  3. 🔨 编译合约,将编译后的合约 VRFDirectFundingV2ForDicing.sol 🚀部署到 Sepolia 链上。
    1. 记下部署成功的合约地址,本文合约地址为:0x4AFeD5d23322F21e31cEcF30cB43E714Ef8eA31 📝
  4. 🔎 打开 LINK on Sepolia
    1. 👤 用户找到 Write Contract/approve 按钮,输入要授权的 LINK 代币数量。
      1. 以 4 LINK 为例,(4 LINK = 4 * 1e18), 则应该输入 4000000000000000000000 💰
    2. 设置 spender 为该合约地址,例如:0x4AFeD5d2332F21e31cB43E78b462478eA31 🔌
    3. 点击 Approve 按钮,并且 wallet 确认本次 approve tx。✅
    4. 授权完成后,用户就可以调用 fundLink 函数来存入 LINK 代币,数量需要 <= approved 数量 (4 LINK = 4 * 1e18). ✅
  5. 🔙 回到 Remix, 找到合约的 fundLink 函数,输入 40000000000000000000 作为参数,点击 fundLink 按钮,确认交易。
  6. 💳 交易成功之后,LINK 则从 📤 transfer 💸 to Contract. 从而可以使用 requestRandomWords 函数来请求随机数。
  7. 🔎 找到 requestRandomWords 函数,
    1. 输入 10000 作为 callbackGasLimit 参数 🔢
    2. 输入 3 作为 _requestConfirmations 参数 🔢
    3. 点击 requestRandomWords 按钮,确认交易。✅
  8. 💳 成功之后,找到 getRequestIdsByRequester 函数,输入钱包地址查询 requestId 列表。
  9. 📋 复制最新的requestIdgetRequestStatus函数里,即可查询当前 requestStatus
序号类型名称当前值描述
0addressrequesterwallet-address请求者
1boolfulfilledfalseChainlink 还未返回随机数
2uint256randomWord0默认值 0
3uint8dice0默认值 0
4uint256paid31752849107074264已支付的 LINK 数量 1 uint/10e18 = 1 LINK
  • ✅ 如果 fulfilledtrue,则 randomWord 字段会被更新为 Chainlink 返回的随机数。dice也会更新。可以多次重复利用getRequestStatus查询结果。
序号类型名称当前值描述
0addressrequesterwallet-address请求者
1boolfulfilledtrueChainlink 已成功返回随机数
2uint256randomWord112930790923327229834738651636523172975866465166633221344177435547134793609260默认值 0
3uint8dice3默认值 0
4uint256paid31752849107074264已支付的 LINK 数量 1 uint/10e18 = 1 LINK
  • ❌ 如果 fulfilled 长时间显示 false,则说明 _callbackGasLimit_ 设置过低,导致 Chainlink 无法返回随机数。可以尝试增加callbackGasLimit 参数,重新请求。

🧩 扩展

如何实现第二种即付即用的逻辑呢?

🔖 重点在于:将代码中的 requestRandomWords 函数修改为即付即用模式。

  • VRF_V2_WRAPPER.calculateRequestPrice 计算出本次 request 所需消耗的 LINK 时,
  • 从 user 那里 transferFrom 相应的 LINK 到合约即可。
具体实现

仅更改了关键函数 requestRandomWords,其他代码视需求更改。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {VRFV2WrapperConsumerBase} from "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol";
import {VRFRequestMapping} from "./VRFRequestMapping.sol";

contract VRFDirectFundingV2ForDicing is VRFRequestMapping, VRFV2WrapperConsumerBase {

    // configuration: https://docs.chain.link/vrf/v2/direct-funding/supported-networks#configurations
    // _linkAddress: 0x779877A7B0D9E8603169DdbD7836e478b4624789
    // _wrapperAddress: 0xab18414CD93297B0d12ac29E63Ca20f515b3DB46
    /**
     * @notice 构造函数,初始化合约并设置 LINK 代币合约和 VRF v2 Wrapper 合约的地址。
     * @param _linkAddress LINK 代币合约的地址。
     * @param _wrapperAddress VRF v2 Wrapper 合约的地址。
     */
    constructor(address _linkAddress, address _wrapperAddress)
        VRFRequestMapping(msg.sender)
        VRFV2WrapperConsumerBase(_linkAddress, _wrapperAddress)
    {}

   /**
     * @notice requestRandomWords 函数,允许合约所有者或资助者请求 VRF 随机数。
     * @param _callbackGasLimit 用于计算请求价格的最大 gas 限制。
     * @param _requestConfirmations VRF 请求的最小确认数。
     * @return requestId VRF 请求的唯一标识符。
     */
    function requestRandomWords(uint32 _callbackGasLimit, uint16 _requestConfirmations)
        external
        onlyOwnerOrFunder
        returns (uint256 requestId)
    {
        requestId = requestRandomness(_callbackGasLimit, _requestConfirmations, 1);
        uint256 paid = VRF_V2_WRAPPER.calculateRequestPrice(_callbackGasLimit);

        // 🥇 关键代码如下: 即付即用模式
        // 直接从用户那里 transferFrom 相应的 LINK 到合约即可
        require(LINK.transferFrom(msg.sender,address(this),paid),"Not enough LINK");

        uint256 balance = uint256(s_funders[msg.sender]);
        if (balance < paid) {
            revert VRFDirectFundingV2ForDicing__InsufficientFunds(msg.sender, int256(balance), paid);
        }
        mappingRequestStatus(msg.sender, requestId, paid);
        s_funders[msg.sender] -= int256(paid);
        emit RequestSent(msg.sender, requestId, paid);
        return requestId;
    }

    // snip....
}

💦 即使是即用即付模式,也需要注意:

  • 合约所有者需要预先 approve 合约地址,以便合约可以从用户那里 transferFrom LINK。
  • 合约所有者需要确保用户有足够的 LINK 代币来支付请求。

🔗 参考