EasyBet 是一个基于以太坊区块链的去中心化竞猜平台,旨在解决传统体育彩票系统缺乏交易功能的问题。通过智能合约技术,本系统实现了公平、透明、可交易的彩票竞猜机制。
核心创新点:
- 支持彩票 NFT 化(ERC721),使彩票成为可交易资产
- 链上订单簿系统,实现彩票二级市场交易
- 双币种支付系统(ETH + ERC20 代币)
- 自动化奖金分配机制,确保公平性
| 技术 | 版本 | 用途 |
|---|---|---|
| Solidity | ^0.8.20 | 智能合约编程语言 |
| Hardhat | 最新版 | 开发框架、测试、部署 |
| OpenZeppelin | 5.x | ERC20/ERC721 标准库 |
| Ethers.js | v6 | 合约交互库 |
| 技术 | 版本 | 用途 |
|---|---|---|
| React | 19.x | 前端框架 |
| TypeScript | 最新版 | 类型安全 |
| Ethers.js | v6 | Web3 连接 |
| MetaMask | - | 钱包集成 |
# 安装 Node.js (v18+)
# 访问 https://nodejs.org/ 下载安装
# 安装 Ganache
# 方式1: 下载 Ganache GUI
# 访问 https://trufflesuite.com/ganache/
# 下载并安装桌面应用
# 方式2: 安装 ganache-cli
npm install -g ganache- 访问 https://metamask.io/
- 添加浏览器扩展(Chrome/Firefox/Brave)
- 创建或导入钱包
cd contracts
npx hardhat node# 进入合约目录
cd contracts
# 安装依赖
npm install
# 编译合约
npx hardhat compile
px hardhat run scripts/deploy.ts --network localhostcd ../frontend编辑 src/contracts/addresses.ts,粘贴刚才部署的合约地址
# 在 contracts 目录下
cd ../contracts
# 复制 ABI 文件到前端
cp artifacts/contracts/EasyBet.sol/EasyBet.json ../frontend/src/contracts/
cp artifacts/contracts/BettingTicket.sol/BettingTicket.json ../frontend/src/contracts/
cp artifacts/contracts/BettingToken.sol/BettingToken.json ../frontend/src/contracts/cd ../frontend
# 安装依赖(首次运行)
npm install
# 启动开发服务器
npm run start预期输出:
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.1.100:3000
- 点击 MetaMask 图标
- 点击顶部网络下拉菜单
- 点击"添加网络" → "手动添加网络"
- 输入以下信息:
Hardhat Network 配置:
- 网络名称:
Hardhat Local - RPC URL:
http://localhost:8545 - 链 ID:
1337 - 货币符号:
ETH
显示填写好的网络配置信息,包括网络名称、RPC URL、链ID等字段
随后会显示基本的界面
. 在"Create New Activity"区域填写表单:
- Title: 输入活动标题(如"NBA 2025 MVP")
- Choices: 输入选项,用逗号分隔(如"LeBron James, Stephen Curry, Kevin Durant")
- Duration: 选择持续时间(默认 3600 秒 = 1 小时)
- Prize Pool: 输入初始奖池金额(如 0.1 ETH)
下注区域
交易区域
- 在活动列表中找到想要下注的活动
- 在"Place Bet"区域选择:
- Activity ID: 选择活动编号(如 0)
- Choice: 选择竞猜选项(如 "Stephen Curry",索引为 1)
- Amount: 输入下注金额(如 0.05 ETH)
- Payment: 确保"Use ETH"选中(默认)
下注两次之后的池子
提:** 需要先领取 BTK 代币(参见 7.5 节)
- 在下注表单中选择"Use BTK Token"
Payment区域显示两个单选框,"Use ETH"未选中,"Use BTK Token"选中(蓝色圆点),Amount输入框填写"50"(代表50 BTK)
可以从MetaMask中查看
也可以从HardHat终端查看
选择出售Ticket
另一个账户的视角
购买ticket
交易完成,获得ticket
只有庄家有权利选择result是哪个选项
结算完成之后,游戏马上结束
随后大家会获得相应的分数
┌─────────────────────────────────────────────────────────┐
│ 前端应用 (React) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 活动管理界面 │ │ 下注界面 │ │ 交易市场 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Web3Context (Ethers.js v6) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↕ MetaMask
┌─────────────────────────────────────────────────────────┐
│ 以太坊区块链 (Ganache/Hardhat) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ EasyBet │ │ BettingTicket│ │ BettingToken │ │
│ │ (主合约) │ │ (ERC721) │ │ (ERC20) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ 合约间调用关系 │
└─────────────────────────────────────────────────────────┘
- EasyBet: 核心业务逻辑合约,部署时自动创建 BettingTicket 和 BettingToken
- BettingTicket: 由 EasyBet 拥有,只有 EasyBet 可以铸造新彩票
- BettingToken: 独立的 ERC20 代币,用户可以领取并用于下注
核心数据结构:
struct Activity {
address creator; // 活动创建者
string title; // 活动标题
string[] choices; // 竞猜选项
uint256 totalPrizePool; // 总奖池
uint256 endTimestamp; // 结束时间
ActivityStatus status; // 活动状态 (Active/Finished/Cancelled)
uint256 winningChoice; // 获胜选项
bool resultSet; // 是否已设置结果
mapping(uint256 => uint256) choiceBetAmounts; // 每个选项的下注总额
mapping(uint256 => uint256[]) choiceTickets; // 每个选项的彩票列表
}
struct OrderBookEntry {
uint256 tokenId; // 彩票 ID
address seller; // 卖家地址
uint256 price; // 出售价格
bool active; // 订单是否有效
}关键功能实现:
-
创建活动 (
createActivity)- 输入:标题、选项数组、持续时间
- 要求:至少 2 个选项,最短 1 小时,必须附带 ETH 作为初始奖池
- 存储:自增活动 ID,记录创建者和配置信息
-
下注功能
placeBet(): 使用 ETH 下注placeBetWithTokens(): 使用 ERC20 代币下注- 流程:验证活动状态 → 增加奖池 → 铸造 ERC721 彩票 → 记录到对应选项
-
结果设置 (
setResult)- 权限:仅活动创建者或合约所有者
- 流程:标记活动为 Finished → 记录获胜选项 → 触发奖金分配
-
奖金分配 (
_distributePrizes)- 扣除 5% 平台手续费
- 按照下注金额比例分配剩余奖池给获胜者
- 奖金暂存在用户余额中,需手动提取
-
订单簿系统 (Bonus +3分)
placeOrder(): 卖家挂单出售彩票fillOrder(): 买家购买挂单彩票getActiveOrders(): 查询活跃订单
安全特性:
- 使用 OpenZeppelin 的
Ownable实现权限控制 - 防止活动结束后下注
- 防止自己购买自己的订单
- 余额提取前清零,防止重入攻击
核心数据结构:
struct TicketInfo {
uint256 activityId; // 所属活动 ID
uint256 choiceIndex; // 选择的选项
uint256 betAmount; // 下注金额
uint256 timestamp; // 购买时间
}关键功能:
mintTicket(): 仅合约所有者(EasyBet)可调用,铸造新彩票listForSale(): 彩票持有者挂单出售buyTicket(): 购买挂单彩票(直接交易,不走订单簿)cancelSale(): 取消挂单
特点:
- 每张彩票是唯一的 NFT,包含完整的下注信息
- 支持标准 ERC721 转账和授权
- 提供按活动查询彩票的功能
核心参数:
- 代币符号:
BTK(BettingToken) - 初始供应: 1,000,000 BTK (部署时铸造给所有者)
- 水龙头金额: 1,000 BTK
- 冷却时间: 24 小时
关键功能:
faucet(): 用户每 24 小时可领取 1000 BTKmint(): 所有者可铸造新代币- 标准 ERC20 转账和授权功能
实现细节:
- 使用
lastFaucetTimemapping 记录每个地址的上次领取时间 - 自动检查冷却时间,防止频繁领取
职责:
- 管理 Web3 连接状态(Provider, Signer, Account)
- 初始化三个智能合约实例
- 处理钱包连接和账户切换
- 验证合约部署状态
关键代码逻辑:
// 连接钱包
const connectWallet = async () => {
const provider = new ethers.BrowserProvider(window.ethereum);
const accounts = await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
// 验证合约是否部署
const code = await provider.getCode(CONTRACT_ADDRESSES.EasyBet);
if (code === '0x') {
alert('Contract not deployed!');
return;
}
// 初始化合约实例
const easyBet = new ethers.Contract(
CONTRACT_ADDRESSES.EasyBet,
EasyBetABI.abi,
signer
);
// 测试合约调用
const result = await easyBet.helloworld();
console.log('Contract test:', result);
}全局状态:
provider: BrowserProvider 实例signer: 当前账户的签名者account: 当前连接的账户地址easyBetContract,ticketContract,tokenContract: 合约实例
状态管理:
interface Activity {
id: number;
creator: string;
title: string;
choices: string[];
totalPrizePool: string;
endTimestamp: number;
status: number; // 0:Active, 1:Finished, 2:Cancelled
winningChoice: number;
resultSet: boolean;
}核心功能模块:
-
活动创建表单
const handleCreateActivity = async () => { const choices = newChoices.split(',').map(c => c.trim()); const tx = await easyBetContract.createActivity( newTitle, choices, parseInt(newDuration), { value: ethers.parseEther(newPrize) } ); await tx.wait(); loadActivities(); }
-
下注功能
const handlePlaceBet = async () => { if (useToken) { // 使用 ERC20 代币下注 const amount = ethers.parseEther(betAmount); // 先授权 await tokenContract.approve(easyBetContract.target, amount); // 再下注 await easyBetContract.placeBetWithTokens(selectedActivity, selectedChoice, amount); } else { // 使用 ETH 下注 await easyBetContract.placeBet(selectedActivity, selectedChoice, { value: ethers.parseEther(betAmount) }); } }
-
结果设置(仅创建者可见)
const handleSetResult = async (activityId: number, choice: number) => { await easyBetContract.setResult(activityId, choice); loadActivities(); }
-
余额管理
- 显示 ETH 奖金余额(从
userBalances读取) - 显示 BTK 代币余额(从
tokenContract.balanceOf读取) - 提现功能:
easyBetContract.withdraw()
- 显示 ETH 奖金余额(从
-
水龙头功能
const handleFaucet = async () => { await tokenContract.faucet(); loadBalances(); }
UI 组件结构:
- 顶部导航:钱包地址、余额显示、连接按钮
- 活动创建区:表单输入(标题、选项、时长、奖池)
- 活动列表区:卡片展示所有活动,包含状态和操作按钮
- 下注区:选择活动、选项、金额和支付方式
- 管理区:仅创建者可见的结果设置界面
实现内容:
-
代币合约 (BettingToken.sol)
- 符合 ERC20 标准
- 代币名称: BettingToken (BTK)
- 初始供应: 1,000,000 BTK
-
水龙头功能
- 每个地址每 24 小时可领取 1000 BTK
- 使用
lastFaucetTimemapping 跟踪冷却时间 - 前端提供一键领取按钮
-
完整支付流程
- 前端支持切换支付方式(ETH / BTK)
- 使用 BTK 下注时先调用
approve(),再调用placeBetWithTokens() - 按 1:1 比例转换 BTK 到 ETH 等值(简化处理)
技术亮点:
- 冷却机制防止恶意频繁领取
- 自动转换代币到 ETH 等值用于奖池计算
- 前端实时显示 BTK 余额
实现内容:
-
订单簿数据结构
struct OrderBookEntry { uint256 tokenId; // 彩票 NFT ID address seller; // 卖家地址 uint256 price; // 出售价格 (ETH) bool active; // 订单状态 } mapping(uint256 => OrderBookEntry[]) public orderBooks; // activityId => orders
-
挂单功能 (
placeOrder)- 卖家选择要出售的彩票和价格
- 自动授权合约转移彩票
- 订单添加到对应活动的订单簿
-
购买功能 (
fillOrder)- 买家选择订单索引并支付 ETH
- 合约转移彩票 NFT 给买家
- 转移支付金额给卖家
- 标记订单为不活跃
-
查询功能 (
getActiveOrders)- 返回指定活动的所有活跃订单
- 包含 tokenId、卖家地址、价格列表
- 前端可据此显示订单簿界面
技术亮点:
- 完全在链上存储订单,无需中心化服务器
- 支持多个订单同时挂单
- 自动处理 NFT 转移和支付
- 支持退还多余支付金额
订单簿显示示例:
活动 #1 订单簿
┌────────┬──────────────────┬─────────┐
│ Ticket │ Seller │ Price │
├────────┼──────────────────┼─────────┤
│ #001 │ 0x1234...5678 │ 0.05 ETH│
│ #003 │ 0xabcd...ef00 │ 0.03 ETH│
│ #007 │ 0x9876...5432 │ 0.08 ETH│
└────────┴──────────────────┴─────────┘




















