基于区块链的抽奖模拟器

231 阅读9分钟

首先介绍一些概念,这些概念在我们的开发过程中至关重要,是合约上线的基础

forge框架

在本项目中,我将使用forge框架实现项目的部署;这里简单介绍下forge框架

Forge 框架,即 Forge,是一个区块链应用开发框架,它为开发者提供了按需创建区块链和开发去中心化应用(DApps)的最简单方法。Forge 为开发者处理底层复杂的区块链技术,让他们专注于自己更熟悉的产品和业务开发。因此,Forge 大大降低了开发部署区块链应用的难度,并且该框架使个人甚至小型团队能够独自开发足够复杂的去中心化应用。

Forge 为开发者提供了一系列在区块链上开发应用的工具。首先,开发者将拥有许多用于创建和部署自定义区块链的工具,比如 Forge CLI、Forge Patron 和 Forge Deploy。确切地说,Forge CLI 和 Forge Patron 帮助开发者建立一个工作环境,并在他们自己的计算机上创建用于编程和测试的链。最后一个,顾名思义,用于部署区块链。

我们先安装rust,再安装Foundry,最后安装forge;Foundry会安装三个框架,除了forge之外还有cast,anvil,他们的作用如下

  • forge(构建 & 测试工具)
  • cast(链上交互工具)
  • anvil(本地模拟链)

首先我们安装rust,安装地址:win.rustup.rs/x86_64

rustc --version

cargo --version

如果可以看到版本输出,说明我们安装正确;

按下 win + R 键输入 cmd,并输入下面的代码

cargo install --git https://github.com/foundry-rs/foundry foundry-cli anvil chisel --bins --locked

最后验证我们安装的框架是否正确:

forge --version
cast --version
anvil --version

alchemy平台

Alchemy 是一个专注于区块链应用开发的基础设施平台。它不仅提供节点服务,还增加了许多开发工具和高级功能,使开发者能够更快、更高效地构建和调试 DApps。

主要功能:

  • 提供对以太坊、Polygon、Optimism、Arbitrum 等区块链的访问。
  • 高级调试工具(如用于追踪交易状态的工具)。
  • 性能优化:提供更快的查询速度和更高的可靠性。

在项目中我们需要到下面的网站中去创建自己的dapp,并且复制自己在sepolia的dapp链接,步骤如下:

  1. 进入到如下地址,并且点击创建app:dashboard.alchemy.com/ 在这里插入图片描述
  2. 一步步创建即可
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  3. 最后我们可以看到自己创建的dapp已经存在,并且在页面上显示了apikey;我们选择ethereum sepolia,然后复制我们的Network URL,保存到文末项目中的.env中的RPC_URL中,后续我们部署我们的合约到测试网络时会使用到这个dapp.

demo:

https://eth-sepolia.g.alchemy.com/v2/你的API密钥

在这里插入图片描述

etherscan

在本项目中,我们还将使用到etherscan来访问区块链上的公共数据,比如账户余额、交易记录、合约信息等,下面是etherscan的介绍

Etherscan由一支由区块链爱好者组成的专业团队于 2015 年推出,是以太坊网络的基石区块浏览器和分析平台。它在设计上以用户为中心,是了解以太坊区块链的全面窗口,方便用户轻松浏览、验证和探索交易、地址和智能合约。

该平台超越了区块浏览器的基本功能,提供对以太坊各种活动的洞察。从追踪 ERC-20 代币交易和 NFT 铸币,到深入研究智能合约详情和监控钱包余额,Etherscan 让这些复杂的任务变得简单易用。

就像谷歌或必应等搜索引擎索引并呈现互联网海量数据一样,Etherscan 在以太坊区块链中扮演着类似的角色。它揭秘并翻译区块链的技术数据,并通过用户友好的界面呈现。这不仅简化了区块链信息的排序和筛选过程,还为普通用户和开发者开辟了丰富的可能性,增强了他们对以太坊生态系统的理解和互动。

在我们的项目中,我们使用到etherscan去验证合约,让其他人看到我们的源码,所以这里需要使用 etherscan-api-key

读者朋友可以通过如下的链接创建etherscan:
info.etherscan.com/api-keys/

钱包私钥

你可以使用Metamask创建一个你自己的钱包,然后创建自己的账户,在账户中你可以找到自己的私钥,这个私钥将在后续部署区块链应用的过程中被使用到,对应 private-keys

代码逻辑介绍

现在,我们已经拥有了开发合约的基础工具,我们来介绍下合约具体做了些什么

  1. 用户支付费用参与抽奖。
  2. 定时检查是否可以开奖(使用 Chainlink Keepers)。
  3. 自动调用 Chainlink VRF 获取随机数选择赢家。
  4. 将所有奖池资金发送给随机选中的赢家。

看起来我们的流程十分简单,但是这里涉及到两个关键点,如何设置定时任务和获取随机数?这里我们使用到了chainlink的两个工具, Chainlink Keepers用来设置定时任务,Chainlink VRF用来获取随机数,下面我们简单介绍下他们

Chainlink Keeper

它是一组 去中心化节点,会持续监控你的智能合约并在需要时调用它们指定的函数(如 performUpkeep()),从而 实现链上自动化。

换句话说,Keeper 就是 Web3 世界里的「定时器」+「触发器」。

这里是chainlink的官方文档:chainlink autimation

Chainlink Keeper/Automation 运行方式:

  1. 你在合约中实现 checkUpkeep() 和 performUpkeep() 两个函数。
  2. Keeper 节点定期调用 checkUpkeep(),看是否需要执行任务。
  3. 如果checkUpkeep()返回 true,Keeper 节点就会自动调用 performUpkeep() 来执行操作。

大家可以使用自己的区块链账号注册一个定时任务,下面是链接
chainlink automation

Chainlink VRF

Chainlink VRF(Verifiable Random Function,可验证随机函数)是 Chainlink 提供的一项服务,用于在智能合约中生成安全、不可预测、可验证的随机数。
具体工作流程:

  1. 用户调用 → requestRandomWords()
  2. Chainlink VRF 节点生成随机数 + 证明
  3. Chainlink VRF Coordinator 验证并回调合约
  4. 合约中的 fulfillRandomWords() 得到随机数,我们可以重写这个函数,实现拿到随机数之后的逻辑

大家可以使用自己的区块链账号在下面的连接中注册验证器
chainlink vrf

code

我们现在已经安装了forge,使用alchemy连接sepolia testnet,并且可以通过etherscan验证我们的合约,下面来介绍下我们真正要部署的代码

// Layout of Contract:
// version
// imports
// errors
// interfaces, libraries, contracts
// Type declarations
// State variables
// Events
// Modifiers
// Functions

// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// view & pure functions

// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/interfaces/AutomationCompatibleInterface.sol";

/**
 * @title A sample Raffle Contract
 * @author Patrick Collins
 * @notice This contract is for creating a sample raffle contract
 * @dev This implements the Chainlink VRF Version 2
 */
contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface {
    /* Errors */
    error Raffle__UpkeepNotNeeded(uint256 currentBalance, uint256 numPlayers, uint256 raffleState);
    error Raffle__TransferFailed();
    error Raffle__SendMoreToEnterRaffle();
    error Raffle__RaffleNotOpen();

    /* Type declarations */
    enum RaffleState {
        OPEN,
        CALCULATING
    }

    /* State variables */
    // Chainlink VRF Variables
    uint256 private immutable i_subscriptionId;
    bytes32 private immutable i_gasLane;
    uint32 private immutable i_callbackGasLimit;
    uint16 private constant REQUEST_CONFIRMATIONS = 3;
    uint32 private constant NUM_WORDS = 1;

    // Lottery Variables
    uint256 private immutable i_interval;
    uint256 private immutable i_entranceFee;
    uint256 private s_lastTimeStamp;
    address private s_recentWinner;
    address payable[] private s_players;
    RaffleState private s_raffleState;

    /* Events */
    event RequestedRaffleWinner(uint256 indexed requestId);
    event RaffleEnter(address indexed player);
    event WinnerPicked(address indexed player);

    /* Functions */
    constructor(
        uint256 subscriptionId,
        bytes32 gasLane, // keyHash
        uint256 interval,
        uint256 entranceFee,
        uint32 callbackGasLimit,
        address vrfCoordinatorV2
    ) VRFConsumerBaseV2Plus(vrfCoordinatorV2) {
        i_gasLane = gasLane;
        i_interval = interval;
        i_subscriptionId = subscriptionId;
        i_entranceFee = entranceFee;
        s_raffleState = RaffleState.OPEN;
        s_lastTimeStamp = block.timestamp;
        i_callbackGasLimit = callbackGasLimit;
        // uint256 balance = address(this).balance;
        // if (balance > 0) {
        //     payable(msg.sender).transfer(balance);
        // }
    }

    function enterRaffle() public payable {
        // require(msg.value >= i_entranceFee, "Not enough value sent");
        // require(s_raffleState == RaffleState.OPEN, "Raffle is not open");
        if (msg.value < i_entranceFee) {
            revert Raffle__SendMoreToEnterRaffle();
        }
        if (s_raffleState != RaffleState.OPEN) {
            revert Raffle__RaffleNotOpen();
        }
        s_players.push(payable(msg.sender));
        // Emit an event when we update a dynamic array or mapping
        // Named events with the function name reversed
        emit RaffleEnter(msg.sender);
    }

    /**
     * @dev This is the function that the Chainlink Keeper nodes call
     * they look for `upkeepNeeded` to return True.
     * the following should be true for this to return true:
     * 1. The time interval has passed between raffle runs.
     * 2. The lottery is open.
     * 3. The contract has ETH.
     * 4. Implicity, your subscription is funded with LINK.
     */
    function checkUpkeep(bytes memory /* checkData */ )
        public
        view
        override
        returns (bool upkeepNeeded, bytes memory /* performData */ )
    {
        bool isOpen = RaffleState.OPEN == s_raffleState;
        bool timePassed = ((block.timestamp - s_lastTimeStamp) > i_interval);
        bool hasPlayers = s_players.length > 0;
        bool hasBalance = address(this).balance > 0;
        upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers);
        return (upkeepNeeded, "0x0"); // can we comment this out?
    }

    /**
     * @dev Once `checkUpkeep` is returning `true`, this function is called
     * and it kicks off a Chainlink VRF call to get a random winner.
     */
    function performUpkeep(bytes calldata /* performData */ ) external override {
        (bool upkeepNeeded,) = checkUpkeep("");
        // require(upkeepNeeded, "Upkeep not needed");
        if (!upkeepNeeded) {
            revert Raffle__UpkeepNotNeeded(address(this).balance, s_players.length, uint256(s_raffleState));
        }

        s_raffleState = RaffleState.CALCULATING;

        // Will revert if subscription is not set and funded.
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: i_gasLane,
                subId: i_subscriptionId,
                requestConfirmations: REQUEST_CONFIRMATIONS,
                callbackGasLimit: i_callbackGasLimit,
                numWords: NUM_WORDS,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    // Set nativePayment to true to pay for VRF requests with Sepolia ETH instead of LINK
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
        // Quiz... is this redundant?
        emit RequestedRaffleWinner(requestId);
    }

    /**
     * @dev This is the function that Chainlink VRF node
     * calls to send the money to the random winner.
     */
    function fulfillRandomWords(uint256, /* requestId */ uint256[] calldata randomWords) internal override {
        // s_players size 10
        // randomNumber 202
        // 202 % 10 ? what's doesn't divide evenly into 202?
        // 20 * 10 = 200
        // 2
        // 202 % 10 = 2
        uint256 indexOfWinner = randomWords[0] % s_players.length;
        address payable recentWinner = s_players[indexOfWinner];
        s_recentWinner = recentWinner;
        s_players = new address payable[](0);
        s_raffleState = RaffleState.OPEN;
        s_lastTimeStamp = block.timestamp;
        emit WinnerPicked(recentWinner);
        (bool success,) = recentWinner.call{value: address(this).balance}("");
        // require(success, "Transfer failed");
        if (!success) {
            revert Raffle__TransferFailed();
        }
    }

    /**
     * Getter Functions
     */
    function getRaffleState() public view returns (RaffleState) {
        return s_raffleState;
    }

    function getNumWords() public pure returns (uint256) {
        return NUM_WORDS;
    }

    function getRequestConfirmations() public pure returns (uint256) {
        return REQUEST_CONFIRMATIONS;
    }

    function getRecentWinner() public view returns (address) {
        return s_recentWinner;
    }

    function getPlayer(uint256 index) public view returns (address) {
        return s_players[index];
    }

    function getLastTimeStamp() public view returns (uint256) {
        return s_lastTimeStamp;
    }

    function getInterval() public view returns (uint256) {
        return i_interval;
    }

    function getEntranceFee() public view returns (uint256) {
        return i_entranceFee;
    }

    function getNumberOfPlayers() public view returns (uint256) {
        return s_players.length;
    }
}

合约部署验证

大家可以clone我的项目,然后在项目中运行下面的指令,程序会自动部署代码到eth-sepolia链上;注意要将其中的key替换为我们上文提到的key

forge script script/DeployRaffle.s.sol:DeployRaffle \
  --broadcast \
  --verify \
  --rpc-url https://eth-sepolia.g.alchemy.com/v2/<ALCHEMY_API_KEY> \
  --etherscan-api-key <ETHERSCAN_API_KEY> \
  --private-keys <YOUR_PRIVATE_KEY>

这里是我部署到testnet上的合约地址,感兴趣的小伙伴可以通过etherscan参与到这个抽奖游戏当中,但是需要你的账户中提前有测试网络的eth

0x98e580d1dA541188EfA53CCC8aD1190807e086B1

直接点击enterRaffel即可参与

在这里插入图片描述
我就是最后一轮抽奖的赢家
在这里插入图片描述

代码仓库

ShangyiAlone/foundry-smart-contract-lottery: blockchain-based lottery simulator

欢迎大家star,下载,运行

引用

www.arcblock.io/blog/zh/pos…
learnblockchain.cn/article/645…
www.youtube.com/watch?v=sas…