使用随机数预编译创建一个彩票合约¶
作者:Erin Shaben
概览¶
Moonbeam使用可验证随机函数(Verifiable Random Functions,VRF)生成可以在链上验证的随机数。VRF是一种加密函数,它接受一些输入并产生随机值以及这些随机值是由提交者生成的真实性证明。此证明可以由任何人验证,以确保生成的随机数值计算正确。
目前有两种可用随机数来源,他们基于区块生产者的VRF密钥以及过去的随机数结果提供随机输入:本地VRF和BABE Epoch随机数。本地VRF直接在Moonbeam中使用区块收集人的VRF密钥以及最新区块的VRF输出值决定。而BABE Epoch随机数是基于由中继链验证人在一个完整Epoch期间生产的所有VRF。
获取关于这两种随机数的更多信息,包括请求和履行流程如何工作,以及安全考量,请参考Moonbeam上的随机数页面。
Moonbeam提供随机数预编译,这是一个Solidity接口,使智能合约开发者能够使用以太坊API通过本地VRF或BABE epoch随机数生成随机数。Moonbeam也提供一个随机数消费者Solidity合约,您的合约必须继承自该合约才能使用已履行的随机数请求。
本教程将向您展示如何使用随机数预编译和随机数消费者合约创建一个随机挑选获胜者的彩票合约。
查看先决条件¶
在开始操作之前,您需要准备以下内容:
- 在Moonbase Alpha上创建/拥有三个账户,用于测试彩票合约
- 所有账户必须拥有一些
DEV
Token 您可以每24小时一次从Moonbase Alpha水龙头上获取DEV代币以在Moonbase Alpha上进行测试 - 一个已配置Moonbase Alpha测试网的空白Hardhat项目。要获取分步操作教程,请参考创建一个Hardhat项目和我们Hardhat文档页面的Hardhat配置文件部分
-
安装Hardhat Ethers插件。这将为您提供更简便的方式以使用Ethers.js库与Hardhat项目中的网络交互:
npm install @nomicfoundation/hardhat-ethers ethers@6
注意事项
要在Moonbeam或Moonriver网络上测试本指南中的示例,您可以从受支持的网络端点提供商之一获取您自己的端点和API密钥。
合约设置¶
以下为我们本次操作指南中创建彩票合约会用到的合约:
Randomness.sol
- 随机数预编译,这是一个Solidity接口,允许您请求随机数、获取关于随机数请求的信息、并履行请求等RandomnessConsumer.sol
- 随机数消费者,是一个抽象的Solidity合约,用于与随机数预编译交互。此合约负责验证随机数请求的origin(来源),确保随机数预编译始终是origin,并履行请求Lottery.sol
- 一个示例彩票合约,我们将在本教程中构建此合约。它将依靠随机数预编译和随机数消费者来请求用于挑选彩票获胜者的随机词
如果您尚未在Hardhat项目中创建contracts
目录,您需要创建一个新目录:
mkdir contracts && cd contracts
然后您可以创建以下三个文件,每个文件对应上述合约:
touch Randomness.sol RandomnessConsumer.sol Lottery.sol
在Randomness.sol
文件中,您可以粘贴随机数预编译合约。
Randomness.sol
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
/// @dev The Randomness contract's address.
address constant RANDOMNESS_ADDRESS = 0x0000000000000000000000000000000000000809;
/// @dev The Randomness contract's instance.
Randomness constant RANDOMNESS_CONTRACT = Randomness(RANDOMNESS_ADDRESS);
/// @dev Maximum number of random words being requested
uint32 constant MAX_RANDOM_WORDS = 100;
/// @dev Minimum number of blocks before a request can be fulfilled for Local VRF Request
uint32 constant MIN_VRF_BLOCKS_DELAY = 2;
/// @dev Maximum number of blocks before a request can be fulfilled for Local VRF Request
uint32 constant MAX_VRF_BLOCKS_DELAY = 2000;
/// @dev The deposit amount needed to request random words. There is 1 deposit per request
uint256 constant REQUEST_DEPOSIT_AMOUNT = 1000000000000000000;
/// @author The Moonbeam Team
/// @title Pallet Randomness Interface
/// @dev The interface through which solidity contracts will interact with Randomness
/// @custom:address 0x0000000000000000000000000000000000000809
interface Randomness {
/// @notice Event emitted when the request has been successfully executed
event FulFillmentSucceeded();
/// @notice Event emitted when the request has failed to execute fulfillment
event FulFillmentFailed();
/// @notice The status of the request
/// @param DoesNotExist The request doesn't exist
/// @param Pending The request cannot be fulfilled yet
/// @param Ready The request is ready to be fulfilled
/// @param Expired The request has expired
enum RequestStatus {
DoesNotExist,
Pending,
Ready,
Expired
}
/// @notice The type of randomness source
/// @param LocalVRF Randomness VRF using the parachain material as seed
/// @param RelayBabeEpoch Randomness VRF using relay material from previous epoch
enum RandomnessSource {
LocalVRF,
RelayBabeEpoch
}
/// @notice The request details
/// @param id The id of the request (is always < 2**64)
/// @param refundAddress The address receiving the left-over fees after the fulfillment
/// @param contractAddress The address of the contract being called back during fulfillment
/// @param fee The amount to set aside to pay for the fulfillment
/// @param gasLimit The gas limit to use for the fulfillment
/// @param salt A string being mixed with the randomness seed to obtain different random words. This should be as unique as possible; using the same salt will lead to same randomness result.
/// @param numWords The number of random words requested (from 1 to MAX_RANDOM_WORDS)
/// @param randomnessSource The type of randomness source used to generate the random words
/// @param fulfillmentBlock The parachain block number at which the request can be fulfilled (for LocalVRF only)
/// @param fulfillmentEpochIndex The relay epoch index at which the request can be fulfilled (for RelayBabeEpoch)
/// @param expirationBlock The parachain block number at which the request expires (for LocalVRF only)
/// @param expirationEpochIndex The relay epoch index at which the request expires (for RelayBabeEpoch)
/// @param status The current status of the request
struct Request {
uint256 id;
address refundAddress;
address contractAddress;
uint256 fee;
uint256 gasLimit;
bytes32 salt;
uint32 numWords;
RandomnessSource randomnessSource;
uint32 fulfillmentBlock;
uint64 fulfillmentEpochIndex;
uint32 expirationBlock;
uint64 expirationEpochIndex;
RequestStatus status;
}
/// Return the current relay epoch index
/// @dev An epoch represents real time and not a block number
/// @dev Currently, time between epoch changes cannot be longer than:
/// @dev - Kusama/Westend/Rococo: 600 relay blocks (1 hour)
/// @dev - Polkadot: 2400 relay blocks (4 hours)
/// @custom:selector 81797566
function relayEpochIndex() external view returns (uint64);
/// Return the deposit required to perform a request
/// @dev Each request will need a deposit.
/// @custom:selector fb7cfdd7
function requiredDeposit() external view returns (uint256);
/// @notice Returns the request status
/// @param requestId The id of the request to check (must be < 2**64)
/// @return status Status of the request
/// @custom:selector d8a4676f
function getRequestStatus(uint256 requestId)
external
view
returns (RequestStatus status);
/// @notice Returns the request or revert
/// @param requestId The id of the request to check (must be < 2**64)
/// @return request The request
/// @custom:selector c58343ef
function getRequest(uint256 requestId)
external
view
returns (Request memory request);
/// @notice Request random words generated from the parachain VRF
/// @dev This is using pseudo-random VRF executed by the collator at the fulfillment
/// @dev Warning:
/// @dev The collator in charge of producing the block at fulfillment can decide to skip
/// @dev producing the block in order to have a different random word generated by the next
/// @dev collator, at the cost of a block reward. It is therefore economically viable to use
/// @dev this randomness source only if the financial reward at stake is lower than the block
/// @dev reward.
/// @dev In order to reduce the risk of a collator being able to predict the random words
/// @dev when the request is performed, it is possible to increase the delay to multiple blocks
/// @dev The higher the delay is, the less likely the collator will be able to know which
/// @dev collator will be in charge of fulfilling the request.
/// @dev Fulfillment is manual and can be executed by anyone (for free) after the given delay
/// @param refundAddress The address receiving the left-over fees after the fulfillment
/// @param fee The amount to set aside to pay for the fulfillment
/// @param gasLimit The gas limit to use for the fulfillment
/// @param salt A string being mixed with the randomness seed to obtain different random words
/// @param numWords The number of random words requested (from 1 to MAX_RANDOM_WORDS)
/// @param delay The number of blocks until the request can be fulfilled (between MIN_DELAY_BLOCKS and MAX_DELAY_BLOCKS)
/// @return requestId The id of the request requestLocalVRFRandomWords
/// @custom:selector 9478430c
function requestLocalVRFRandomWords(
address refundAddress,
uint256 fee,
uint64 gasLimit,
bytes32 salt,
uint8 numWords,
uint64 delay
) external returns (uint256);
/// @notice Request random words generated from the relaychain Babe consensus
/// @dev The random words are generated from the hash of the all the VRF provided by the
/// @dev relaychain validator during 1 epoch.
/// @dev It requires a delay of at least 1 epoch after the current epoch to be unpredictable
/// @dev at the time the request is performed.
/// @dev Warning:
/// @dev The validator (on the relaychain) of the last block of an epoch can decide to skip
/// @dev producing the block in order to choose the previous generated epoch random number
/// @dev at the cost of a relaychain block rewards. It is therefore economically viable to use
/// @dev this randomness source only if the financial reward at stake is lower than the relaychain
/// @dev block reward.
/// @dev (see https://crates.parity.io/pallet_babe/struct.RandomnessFromOneEpochAgo.html)
/// @dev Fulfillment is manual and can be executed by anyone (for free) at
/// @dev the beginning of the 2nd relay epoch following the current one
/// @param refundAddress The address receiving the left-over fees after the fulfillment
/// @param fee Amount to set aside to pay for the fulfillment. Those fees are taken from the contract
/// @param gasLimit Gas limit for the fulfillment
/// @param salt Salt to be mixed with raw randomness to get output
/// @param numWords Number of random words to be returned (limited to MAX_RANDOM_WORDS)
/// @return requestId The id of the request
/// @custom:selector 33c14a63
function requestRelayBabeEpochRandomWords(
address refundAddress,
uint256 fee,
uint64 gasLimit,
bytes32 salt,
uint8 numWords
) external returns (uint256);
/// @dev fulFill the request which will call the contract method "fulfillRandomWords"
/// @dev Fees of the caller are refunded if the request is fulfillable
/// @param requestId Request to be fulfilled (must be < 2**64)
/// @custom:selector 9a91eb0d
function fulfillRequest(uint256 requestId) external;
/// @param requestId Request receiving the additional fees (must be < 2**64)
/// @param feeIncrease Amount to increase
/// @custom:selector d0408a7f
function increaseRequestFee(uint256 requestId, uint256 feeIncrease)
external;
/// @param requestId Request to be purged (must be < 2**64)
/// @custom:selector 1d26cbab
function purgeExpiredRequest(uint256 requestId) external;
}
同样的,在RandomnessConsumer.sol
文件中,您可以粘贴随机数消费者合约。
RandomnessConsumer.sol
// Inspired by: https://raw.githubusercontent.com/smartcontractkit/chainlink/8e8a996fd882c0861bdc9824c1ca27c857c87d03/contracts/src/v0.8/VRFConsumerBaseV2.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.3;
/// @dev The Randomness contract's address.
address constant RANDOMNESS_ADDRESS = 0x0000000000000000000000000000000000000809;
/** ****************************************************************************
* @notice Interface for contracts using VRF randomness
* *****************************************************************************
* @dev PURPOSE
*
* @dev The purpose of this contract is to make it easy for contracts to talk to
* @dev the Randomness Precompile. It ensures 2 things:
* @dev 1. The fulfillment came from the Randomness Precompile
* @dev 2. The consumer contract implements fulfillRandomWords.
* *****************************************************************************
* @dev USAGE
*
* @dev Calling contracts must inherit from RandomnessConsumer
*
* @dev Call one of the randomness request functions:
* @dev 1. requestLocalVRFRandomWords(refundAddress, fee, gasLimit, salt
* @dev numWords, delay),
* @dev 2. requestRelayBabeEpochRandomWords(refundAddress, fee, gasLimit, salt
* @dev numWords),
* @dev see (Randomness.sol for a description of each function and their arguments).
*
* @dev Once the request has been registered and the minimum delay is passed, the
* @dev request then can be executed (for 0 fee) by anyone. it will call your
* @dev contract's fulfillRandomWords method.
*
* @dev The randomness argument to fulfillRandomWords is a set of random words
* @dev generated from your requestId.
*
* @dev If your contract could have concurrent requests open, you can use the
* @dev requestId returned from requestRandomWords to track which response is associated
* @dev with which randomness request.
* @dev See "SECURITY CONSIDERATIONS" for principles to keep in mind,
* @dev if your contract could have multiple requests in flight simultaneously.
*
* @dev Colliding `requestId`s are cryptographically impossible as long as seeds
* @dev differ.
*
* *****************************************************************************
* @dev SECURITY CONSIDERATIONS
*
* @dev A method with the ability to call your fulfillRandomness method directly
* @dev could spoof a VRF response with any random value, so it's critical that
* @dev it cannot be directly called by anything other than this base contract
* @dev (specifically, by the RandomnessConsumer.rawFulfillRandomness method).
*
* @dev For your users to trust that your contract's random behavior is free
* @dev from malicious interference, it's best if you can write it so that all
* @dev behaviors implied by a VRF response are executed *during* your
* @dev fulfillRandomness method. If your contract must store the response (or
* @dev anything derived from it) and use it later, you must ensure that any
* @dev user-significant behavior which depends on that stored value cannot be
* @dev manipulated by a subsequent VRF request.
*
* @dev Similarly, the collators have some influence over the order in which
* @dev VRF responses appear on the blockchain, so if your contract could have
* @dev multiple VRF requests in flight simultaneously, you must ensure that
* @dev the order in which the VRF responses arrive cannot be used to manipulate
* @dev your contract's user-significant behavior.
*
* @dev Since the output of the random words generated for
* @dev *requestLocalVRFRandomWords* is dependant of the collator producing the
* @dev block at fulfillment, the collator could skip its block forcing the
* @dev fulfillment to be executed by a different collator, and therefore
* @dev generating a different VRF.
* @dev However, such an attack would incur the cost of losing the block reward to
* @dev the collator.
* @dev It is also possible for a collator to be able to predict some of the
* @dev possible outcome of the VRF if the delay between the request and the
* @dev fulfillment is too short. It is for this reason that we allow to provide
* @dev a higher delay
*
* @dev Since the output of the random words generated for
* @dev *requestRelayBabeEpochRandomWords* is dependant of the relaychain
* @dev validator producing the blocks during an epoch, it is possible for
* @dev the last validator of an epoch to choose between 2 possible VRF
* @dev outputs by skipping the production of a block.
* @dev However, such an attack would incur the cost of losing the block reward to
* @dev the validator
* @dev It is not possible for a parachain collator to predict nor influence
* @dev the output of the relaychain VRF, not to censor the fulfillment as long as
* @dev there is one honest collator.
*/
abstract contract RandomnessConsumer {
error OnlyRandomnessPrecompileCanFulfill(address have, address want);
/**
* @notice fulfillRandomness handles the VRF response. Your contract must
* @notice implement it. See "SECURITY CONSIDERATIONS" above for important
* @notice principles to keep in mind when implementing your fulfillRandomness
* @notice method.
*
* @dev RandomnessConsumer expects its subcontracts to have a method with this
* @dev signature, and will call it once it has verified the proof
* @dev associated with the randomness. (It is triggered via a call to
* @dev rawFulfillRandomness, below.)
*
* @param requestId The Id initially returned by requestLocalVRFRandomWords or requestRelayBabeEpochRandomWords
* @param randomWords The VRF output expanded to the requested number of words
*/
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
internal
virtual;
// rawFulfillRandomness is called by Randomness Precompile when the executeFulfillement
// is called. rawFulfillRandomness then calls fulfillRandomness, after validating
// the origin of the call
function rawFulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) external {
if (msg.sender != RANDOMNESS_ADDRESS) {
revert OnlyRandomnessPrecompileCanFulfill(
msg.sender,
RANDOMNESS_ADDRESS
);
}
fulfillRandomWords(requestId, randomWords);
}
}
我们将在以下部分开始添加功能至Lottery.sol
合约。
创建彩票智能合约¶
从更高层面来说,我们正在创建的彩票合约将定义彩票规则,允许参与并使用随机生成的词来公平挑选获胜者。我们将通过随机数预编译请求随机词。然后,我们将使用随机数消费者接口消费已完成请求的结果,以便我们的合约可以使用随机生成的词挑选获胜者并支付奖励。我们将会在构建彩票合约时演示分步流程。但是现在,您可以查看下图了解整个流程。
此合约仅用于演示目的,不可用于生产环境。
开始之前,请先设置彩票合约。为此,您需要:
- 导入
Randomness.sol
预编译和RandomnessConsumer.sol
接口 - 继承RandomnessConsumer接口
- 创建一个随机数预编译变量randomness,以便我们后续轻松访问其函数
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "./Randomness.sol";
import {RandomnessConsumer} from "./RandomnessConsumer.sol";
contract Lottery is RandomnessConsumer {
// Randomness Precompile interface
Randomness public randomness =
Randomness(0x0000000000000000000000000000000000000809);
}
为Lottery和随机数请求定义参数¶
接下来,我们需要为彩票定义规则,例如:
- 参与费
- 最低/最高参与人数
- 彩票的最短长度
- 获胜者人数
在Lottery
合约中,您可以添加以下参数:
// 中奖人数。此数字对应多少个随机词将被请求。不能超过MAX_RANDOM_WORDS(来自随机数预编译)
uint8 public NUM_WINNERS = 2;
// 可以履行请求之前的区块数(针对本地VRF随机数)。MIN_VRF_BLOCKS_DELAY(来自随机数预编译)
// 为经济价值低的游戏提供足够安全的最小数量。稍微增加延迟会降低收集人能够预测伪随机数的本已很低的概率
uint32 public VRF_BLOCKS_DELAY = MIN_VRF_BLOCKS_DELAY;
// 开始抽奖的最低参与人数
uint256 public MIN_PARTICIPANTS = 3;
// 允许的最高参与人数。 重要的是限制总累积奖池(通过限制参与者数量)以保证收集人的经济激励
// 以避免试图影响伪随机数。(有关详细信息,请参阅 Randomness.sol)
uint256 public MAX_PARTICIPANTS = 20;
// 参与抽奖所需的费用。将进入累积奖池
uint256 public PARTICIPATION_FEE = 100000 gwei;
我们也需要定义一些与请求随机数特别相关的参数:
- 履行随机数请求的交易的gas限制
- 开始抽奖并请求随机词所需的最低费用。每个随机数请求都需要支付执行费用。此费用的目的是支付随机数请求的执行费用,这样就可以允许任何人履行请求,因为该请求的费用已支付。当提交随机数请求时,需要指定退款账户,用于接收多余的退款。设置合约时需要设置彩票合约的所有者将收到退款
- salt前缀和全局请求计数都将用于生成唯一的随机数请求
接下来,您可以添加以下参数:
// 用于执行的gas限制,其取决于执行的代码和请求的字词个数。
// 根据请求的大小和fulfillRandomWords()函数中回调请求的处理来测试和调整这个限制
uint64 public FULFILLMENT_GAS_LIMIT = 100000;
// 开始抽奖所需的最低费用。这并不能保证有足够的费用来支付履行所使用的gas。
// 理想情况下,考虑到可能的gas价格波动,应该设一个较大的值。额外费用将退还给调用者
uint256 public MIN_FEE = FULFILLMENT_GAS_LIMIT * 150 gwei;
// 一个字符串,用于允许使用与其他合约不同的salt
bytes32 public SALT_PREFIX = "my_demo_salt_change_me";
// 存储提交的请求的全局数量。这个数字被用作使每个请求唯一的salt
uint256 public globalRequestCount;
除了这些参数,我们需要创建一些变量,用于追踪当前的彩票:
- 当前的请求ID
- 当前的参与者列表
- 奖池设置
- 彩票合约的所有者。这是必不可少的,因为只有合约所有者才有权限开启抽奖
- 所使用的随机数来源(本地VRF或BABE epoch)
// The current request id
uint256 public requestId;
// The list of current participants
address[] public participants;
// The current amount of token at stake in the lottery
uint256 public jackpot;
// the owner of the contract
address owner;
// Which randomness source to use. This correlates to the values in the
// RandomnessSource enum in the Randomness Precompile
Randomness.RandomnessSource randomnessSource;
创建构造函数¶
现在,我们已经完成了彩票所需的所有变量的初始设置,接下来我们可以开始编写函数以设置彩票。首先,我们将从创建构造函数开始。
构造函数接受一个uint8参数作为随机数来源,这对应位于随机数预编译中的RandomnessSource
enum中定义的随机数类型的索引。因此,我们需要为本地VRF传入0
或者为BABE epoch随机数传入1
。此函数将是payable
,因为我们需要在部署时提交保证金并在后续用于执行随机数请求
保证金在随机数预编译中定义,这是和执行费用一样必不可少的。在完成请求后,保证金将退还给初始请求者,在本示例中为合约的所有者。如果未完成请求,则该请求会过期且需要被清除。请求清除后,保证金将被退还。
constructor(
Randomness.RandomnessSource source
) payable RandomnessConsumer() {
// Because this contract can only perform one randomness request at a time,
// we only need to have one required deposit
uint256 requiredDeposit = randomness.requiredDeposit();
if (msg.value < requiredDeposit) {
revert("Deposit too Low");
}
// Update parameters
randomnessSource = source;
owner = msg.sender;
globalRequestCount = 0;
jackpot = 0;
// Set the requestId to the maximum allowed value by the precompile (64 bits)
requestId = 2 ** 64 - 1;
}
为彩票中的参与者添加逻辑¶
接下来,我们可以创建函数,以允许用户参与到彩票抽奖中。participate
函数将是payable
,因为每个参与者将需要提交一个参与费。
participate
函数将包含以下逻辑:
- 使用随机数预编译的
getRequestStatus
函数检查彩票是否尚未开始。此函数将返回通过RequestStatus
enum定义的状态。如果状态不是DoesNotExist
,则表示彩票已开始 - 检查参与费是否满足要求
- 如果上述两项都符合要求,则参与者将被添加至参与者列表当中,他们的参与费将被加入到奖池中
function participate() external payable {
// We check that the lottery hasn't started yet
if (
randomness.getRequestStatus(requestId) !=
Randomness.RequestStatus.DoesNotExist
) {
revert("Request already initiated");
}
// Each player must submit a fee to participate, which is added to
// the jackpot
if (msg.value != PARTICIPATION_FEE) {
revert("Invalid participation fee");
}
participants.push(msg.sender);
jackpot += msg.value;
}
挑战
在上述函数中,我们检查了彩票尚未开始,但是如果我们想要了解彩票的明确状态呢?请创建一个函数来解决此问题并返回彩票状态。
添加逻辑以启动彩票和请求随机数¶
启动彩票的逻辑包含一个重要的组件:请求随机数。如上所述,只有彩票合约的所有者才有权限开启彩票抽奖。这样一来,所有者需要为请求提交执行费用。
startLottery
函数将包含以下逻辑:
- 检查彩票是否尚未开始,操作方式如
participate
函数所示 - 检查是否达到要求的参与者数量
- 检查执行费用是否满足最低要求
- 检查合约余额是否足够支付保证金。还记得构造函数是如何接受请求保证金的吗?保证金将存储于合约中直到此函数被调用
- 如果上述条件均返回true,我们将通过随机数预编译连同履行费用一起提交随机数请求。根据随机数来源,随机数预编译的
requestLocalVRFRandomWords
或requestRelayBabeEpochRandomWords
函数将和以下参数一起被调用:- 接收多余费用退款的地址
- 履行费用
- 履行请求的gas上限
- salt,这是一个字符串,与随机数种子(randomness seed)共同使用以获取不同的随机词。
globalRequestCount
用于确保独特性 - 请求随机词的数量,基于挑选的获胜者数量设定
- (仅支持本地VRF)延迟时间,在履行请求之前必须通过的区块数量
由于彩票功能仅限所有者调用,因此我们也需要添加onlyOwner
修饰符来要求msg.sender
为owner
。
function startLottery() external payable onlyOwner {
// Check we haven't started the randomness request yet
if (
randomness.getRequestStatus(requestId) !=
Randomness.RequestStatus.DoesNotExist
) {
revert("Request already initiated");
}
// Check that the number of participants is acceptable
if (participants.length < MIN_PARTICIPANTS) {
revert("Not enough participants");
}
if (participants.length >= MAX_PARTICIPANTS) {
revert("Too many participants");
}
// Check the fulfillment fee is enough
uint256 fee = msg.value;
if (fee < MIN_FEE) {
revert("Not enough fee");
}
// Check there is enough balance on the contract to pay for the deposit.
// This would fail only if the deposit amount required is changed in the
// Randomness Precompile.
uint256 requiredDeposit = randomness.requiredDeposit();
if (address(this).balance < jackpot + requiredDeposit) {
revert("Deposit too low");
}
if (randomnessSource == Randomness.RandomnessSource.LocalVRF) {
// Request random words using local VRF randomness
requestId = randomness.requestLocalVRFRandomWords(
msg.sender,
fee,
FULFILLMENT_GAS_LIMIT,
SALT_PREFIX ^ bytes32(globalRequestCount++),
NUM_WINNERS,
VRF_BLOCKS_DELAY
);
} else {
// Requesting random words using BABE Epoch randomness
requestId = randomness.requestRelayBabeEpochRandomWords(
msg.sender,
fee,
FULFILLMENT_GAS_LIMIT,
SALT_PREFIX ^ bytes32(globalRequestCount++),
NUM_WINNERS
);
}
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
添加逻辑以履行随机数请求¶
在这一部分,我们将添加履行请求和处理请求结果的两个函数:fulfillRequest
和fulfillRandomWords
。
fulfillRequest
函数将调用随机数预编译的fulfillRequest
函数。在调用此函数时,会在下面调用随机数消费者的rawFulfillRandomWords
函数,这将验证调用来自随机数预编译。从那里,调用随机数消费者合约的fulfillRandomWords
函数,并使用区块的随机数结果和给定的salt计算请求的随机词,然后将其返回。如果请求成功完成,将发出FulfillmentSucceeded
事件;反之,将发出FulfillmentFailed
事件。
对于已完成的请求,执行费用将从请求费用中退还给fulfillRequest
的调用者。然后,任何多余的费用和请求保证金将转移给指定退款地址。
fulfillRandomWords
函数定义回调函数pickWinners
,其负责处理完成请求。在本示例中,回调函数将使用随机词挑选获胜者并支付奖励。fulfillRandomWords
函数的签名必须与随机数消费者的fulfillRandomWords
函数的签名一致。
function fulfillRequest() public {
randomness.fulfillRequest(requestId);
}
function fulfillRandomWords(
uint256 /* requestId */,
uint256[] memory randomWords
) internal override {
pickWinners(randomWords);
}
我们将在下一部分为pickWinners
函数创建逻辑。
挑战
如果在请求履行之前,因为gas价格变化很大而导致此函数失败?目前我们无法增加履行费用,请创建一个函数来解决此问题并允许我们增加履行费用。
添加逻辑以挑选彩票获胜者¶
彩票合约的最后一步是创建pickWinners
函数,如上所述,该函数负责使用随机词为彩票抽奖挑选获胜者。
pickWinners
函数包含以下逻辑:
- 确定获胜者数量。如果您修改了
NUM_WINNERS
或MIN_PARTICIPANTS
的数量时必须设置此值,因为NUM_WINNERS
需大于MIN_PARTICIPANTS
- 根据奖池中的数量和获胜者总人数计算获胜者的奖金数量
- 使用随机词确定获胜者
- 为每位获胜者分发奖励,确保在分发之前将奖励从奖池中移除
// This function is called only by the fulfillment callback
function pickWinners(uint256[] memory randomWords) internal {
// Get the total number of winners to select
uint256 totalWinners = NUM_WINNERS < participants.length
? NUM_WINNERS
: participants.length;
// The amount distributed to each winner
uint256 amountAwarded = jackpot / totalWinners;
for (uint32 i = 0; i < totalWinners; i++) {
// This is safe to index randomWords with i because we requested
// NUM_WINNERS random words
uint256 randomWord = randomWords[i];
// Using modulo is not totally fair, but fair enough for this demo
uint256 index = randomWord % participants.length;
address payable winner = payable(participants[index]);
delete participants[index];
jackpot -= amountAwarded;
winner.transfer(amountAwarded);
}
}
恭喜您!您已经完成了创建Lottery.sol
合约的整个过程了!您可以在下面找到Lottery.sol
合约的完整版本。请注意,此合约仅用于演示目的,不可用于生产环境。
Lottery.sol
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "./Randomness.sol";
import {RandomnessConsumer} from "./RandomnessConsumer.sol";
contract Lottery is RandomnessConsumer {
// Randomness Precompile interface
Randomness public randomness =
Randomness(0x0000000000000000000000000000000000000809);
// The number of winners. This number corresponds to how many random words
// will be requested. Cannot exceed MAX_RANDOM_WORDS (from the Randomness
// Precompile)
uint8 public NUM_WINNERS = 2;
// The number of blocks before the request can be fulfilled (for Local VRF
// randomness). The MIN_VRF_BLOCKS_DELAY (from the Randomness Precompile)
// provides a minimum number that is safe enough for games with low economical
// value at stake. Increasing the delay slightly reduces the probability
// (already very low) of a collator being able to predict the pseudo-random number
uint32 public VRF_BLOCKS_DELAY = MIN_VRF_BLOCKS_DELAY;
// The minimum number of participants to start the lottery
uint256 public MIN_PARTICIPANTS = 3;
// The maximum number of participants allowed to participate. It is important
// to limit the total jackpot (by limiting the number of participants) to
// guarantee the economic incentive of a collator to avoid trying to influence
// the pseudo-random. (See Randomness.sol for more details)
uint256 public MAX_PARTICIPANTS = 20;
// The fee needed to participate in the lottery. Will go into the jackpot
uint256 public PARTICIPATION_FEE = 100000 gwei;
// The gas limit allowed to be used for the fulfillment, which depends on the
// code that is executed and the number of words requested. Test and adjust
// this limit based on the size of the request and the processing of the
// callback request in the fulfillRandomWords() function
uint64 public FULFILLMENT_GAS_LIMIT = 100000;
// The minimum fee needed to start the lottery. This does not guarantee that
// there will be enough fee to pay for the gas used by the fulfillment.
// Ideally it should be over-estimated considering possible fluctuation of
// the gas price. Additional fee will be refunded to the caller
uint256 public MIN_FEE = FULFILLMENT_GAS_LIMIT * 150 gwei;
// A string used to allow having different salt than other contracts
bytes32 public SALT_PREFIX = "INSERT_ANY_STRING_FOR_SALT";
// Stores the global number of requests submitted. This number is used as a
// salt to make each request unique
uint256 public globalRequestCount;
// The current request id
uint256 public requestId;
// The list of current participants
address[] public participants;
// The current amount of token at stake in the lottery
uint256 public jackpot;
// the owner of the contract
address owner;
// Which randomness source to use. This correlates to the values in the
// RandomnessSource enum in the Randomness Precompile
Randomness.RandomnessSource randomnessSource;
constructor(
Randomness.RandomnessSource source
) payable RandomnessConsumer() {
// Because this contract can only perform one randomness request at a time,
// we only need to have one required deposit
uint256 requiredDeposit = randomness.requiredDeposit();
if (msg.value < requiredDeposit) {
revert("Deposit too Low");
}
// Update parameters
randomnessSource = source;
owner = msg.sender;
globalRequestCount = 0;
jackpot = 0;
// Set the requestId to the maximum allowed value by the precompile (64 bits)
requestId = 2 ** 64 - 1;
}
function participate() external payable {
// We check that the lottery hasn't started yet
if (
randomness.getRequestStatus(requestId) !=
Randomness.RequestStatus.DoesNotExist
) {
revert("Request already initiated");
}
// Each player must submit a fee to participate, which is added to
// the jackpot
if (msg.value != PARTICIPATION_FEE) {
revert("Invalid participation fee");
}
participants.push(msg.sender);
jackpot += msg.value;
}
function startLottery() external payable onlyOwner {
// Check we haven't started the randomness request yet
if (
randomness.getRequestStatus(requestId) !=
Randomness.RequestStatus.DoesNotExist
) {
revert("Request already initiated");
}
// Check that the number of participants is acceptable
if (participants.length < MIN_PARTICIPANTS) {
revert("Not enough participants");
}
if (participants.length >= MAX_PARTICIPANTS) {
revert("Too many participants");
}
// Check the fulfillment fee is enough
uint256 fee = msg.value;
if (fee < MIN_FEE) {
revert("Not enough fee");
}
// Check there is enough balance on the contract to pay for the deposit.
// This would fail only if the deposit amount required is changed in the
// Randomness Precompile.
uint256 requiredDeposit = randomness.requiredDeposit();
if (address(this).balance < jackpot + requiredDeposit) {
revert("Deposit too low");
}
if (randomnessSource == Randomness.RandomnessSource.LocalVRF) {
// Request random words using local VRF randomness
requestId = randomness.requestLocalVRFRandomWords(
msg.sender,
fee,
FULFILLMENT_GAS_LIMIT,
SALT_PREFIX ^ bytes32(globalRequestCount++),
NUM_WINNERS,
VRF_BLOCKS_DELAY
);
} else {
// Requesting random words using BABE Epoch randomness
requestId = randomness.requestRelayBabeEpochRandomWords(
msg.sender,
fee,
FULFILLMENT_GAS_LIMIT,
SALT_PREFIX ^ bytes32(globalRequestCount++),
NUM_WINNERS
);
}
}
function fulfillRequest() public {
randomness.fulfillRequest(requestId);
}
function fulfillRandomWords(
uint256 /* requestId */,
uint256[] memory randomWords
) internal override {
pickWinners(randomWords);
}
// This function is called only by the fulfillment callback
function pickWinners(uint256[] memory randomWords) internal {
// Get the total number of winners to select
uint256 totalWinners = NUM_WINNERS < participants.length
? NUM_WINNERS
: participants.length;
// The amount distributed to each winner
uint256 amountAwarded = jackpot / totalWinners;
for (uint32 i = 0; i < totalWinners; i++) {
// This is safe to index randomWords with i because we requested
// NUM_WINNERS random words
uint256 randomWord = randomWords[i];
// Using modulo is not totally fair, but fair enough for this demo
uint256 index = randomWord % participants.length;
address payable winner = payable(participants[index]);
delete participants[index];
jackpot -= amountAwarded;
winner.transfer(amountAwarded);
}
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
}
挑战
您可以在开始创建彩票、选择获胜者或分配奖励给获胜者时添加一些事件,以便让合约更易于使用。
与彩票合约交互¶
现在,我们已经了解并创建了彩票合约,接下来可以开始部署并启动彩票抽奖。
编译和部署彩票合约¶
要编译合约,您可以简单运行:
npx hardhat compile
编译后,将会创建artifacts
目录:这将存放合约的字节码和元数据,即.json
文件。建议您将此目录添加至.gitignore
。
在开始部署Lottery.sol
合约之前,我们需要创建一个部署脚本。
您可以为脚本创建一个新目录并命名为scripts
,然后向其添加名为deploy.js
的新文件:
mkdir scripts &&
touch scripts/deploy.js
我们可以使用ethers
编写部署脚本。我们将使用Hardhat运行此脚本,因此无需导入任何其他库,只需简单执行以下步骤:
- 使用
getContractFactory
函数创建一个彩票合约的本地示例 - 使用随机数预编译的
requiredDeposit
函数获取随机数请求所需的保证金 - 使用存在于本实例中的
deploy
函数以实例化智能合约。您可以传入0
以使用本地VRF随机数或者传入1
以使用BABE epoch随机数。在本示例中,我们使用的是本地VRF随机数。我们也需要在部署时提交保证金 - 使用
waitForDeployment
等待部署 - 部署好后,我们可以使用合约实例获取合约地址
async function main() {
// 1. Get the contract to deploy
const Lottery = await ethers.getContractFactory('Lottery');
// 2. Get the required deposit amount from the Randomness Precompile
const Randomness = await ethers.getContractAt(
'Randomness',
'0x0000000000000000000000000000000000000809'
);
const deposit = await Randomness.requiredDeposit();
// 3. Instantiate a new Lottery smart contract that uses local VRF
// randomness and pass in the required deposit
const lottery = await Lottery.deploy(0, { value: deposit });
console.log('Deploying Lottery...');
// 4. Waiting for the deployment to resolve
await lottery.waitForDeployment();
// 5. Use the contract instance to get the contract address
console.log('Lottery deployed to:', lottery.target);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
要部署彩票合约,我们将使用run
命令并将moonbase
指定为网络:
npx hardhat run --network moonbase scripts/deploy.js
您也可以使用其他Moonbeam网络,请确保指向正确的网络。网络名称需要与hardhat.config.js
中定义的保持一致。
几秒钟后,合约成功部署,您将在终端看到合约地址。保存合约地址,我们将在下一部分中用于合约实例交互。
创建脚本以与彩票合约交互¶
我们可以继续使用我们的Hardhat项目,另外创建脚本来与彩票合约交互并调用它的一些功能。例如,我们可以在scripts
目录中创建另一个脚本来参与彩票抽奖:
touch participate.js
然后,我们可以添加以下代码,这将使用合约名称和合约地址创建彩票合约的实例。接下来,我们可以直接从合约获取参与费用和调用合约的participate
函数:
async function participate() {
const lottery = await ethers.getContractAt(
'Lottery',
'INSERT_CONTRACT_ADDRESS'
);
const participationFee = await lottery.PARTICIPATION_FEE();
const tx = await lottery.participate({ value: participationFee });
console.log('Participation transaction hash:', tx.hash);
}
participate()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
要运行此脚本,您可以使用以下命令:
npx hardhat run --network moonbase scripts/participate.js
交易哈希将在后台显示。您可以使用哈希在Moonscan查看交易。
这样就可以了!您可以继续创建另外的脚本来执行彩票的后续步骤,例如启动彩票抽奖和挑选获胜者。
本教程仅用于教育目的。 因此,不应在生产环境中使用本教程中创建的任何合约或代码。
| Created: April 20, 2023