如何用Solidity和Web3.js锁定用户MetaMask钱包中的游戏代币?
区块链井字棋押金锁定与结算实现方案
针对你开发区块链井字棋时的押金锁定需求,以下是基于Solidity和Web3.js的具体实现方案,涵盖合约核心逻辑、前端交互流程及关键注意事项:
一、Solidity合约核心实现
押金锁定的核心思路是将用户的代币(ETH或ERC20)暂存在合约中,待游戏结束后由合约自动完成分配或退还。
1. 原生ETH版本合约
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract TicTacToe { enum GameStatus { WaitingForOpponent, InProgress, Completed } struct Game { address player1; address player2; uint256 stakeAmount; GameStatus status; address winner; uint8[9] board; // 0=空,1=player1,2=player2 } mapping(uint256 => Game) public games; uint256 public nextGameId; event GameCreated(uint256 gameId, address creator, uint256 stake); event GameJoined(uint256 gameId, address player2); event GameEnded(uint256 gameId, address winner); // 创建游戏:用户存入ETH作为押金,直接锁定在合约 function createGame() external payable { require(msg.value > 0, "押金金额不能为0"); uint256 gameId = nextGameId++; games[gameId] = Game({ player1: msg.sender, player2: address(0), stakeAmount: msg.value, status: GameStatus.WaitingForOpponent, winner: address(0), board: [0,0,0,0,0,0,0,0,0] }); emit GameCreated(gameId, msg.sender, msg.value); } // 加入游戏:对手需存入等额ETH function joinGame(uint256 gameId) external payable { Game storage game = games[gameId]; require(game.status == GameStatus.WaitingForOpponent, "游戏不处于等待对手状态"); require(msg.sender != game.player1, "不能加入自己创建的游戏"); require(msg.value == game.stakeAmount, "押金金额不匹配"); game.player2 = msg.sender; game.status = GameStatus.InProgress; emit GameJoined(gameId, msg.sender); } // 落子逻辑 function makeMove(uint256 gameId, uint8 position) external { Game storage game = games[gameId]; require(game.status == GameStatus.InProgress, "游戏未进行中"); require(position < 9, "无效位置"); require(game.board[position] == 0, "位置已被占用"); require(msg.sender == game.player1 || msg.sender == game.player2, "你不是游戏玩家"); game.board[position] = (msg.sender == game.player1) ? 1 : 2; checkGameResult(gameId); } // 检查游戏结果并处理结算 function checkGameResult(uint256 gameId) internal { Game storage game = games[gameId]; uint8[9] memory board = game.board; // 胜负判定组合:行、列、对角线 uint8[8][3] memory winningCombos = [ [0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6] ]; for (uint i = 0; i < winningCombos.length; i++) { uint8 a = board[winningCombos[i][0]]; uint8 b = board[winningCombos[i][1]]; uint8 c = board[winningCombos[i][2]]; if (a != 0 && a == b && b == c) { game.winner = (a == 1) ? game.player1 : game.player2; game.status = GameStatus.Completed; payoutWinner(gameId); emit GameEnded(gameId, game.winner); return; } } // 平局判定:棋盘已满 bool isBoardFull = true; for (uint i = 0; i < board.length; i++) { if (board[i] == 0) { isBoardFull = false; break; } } if (isBoardFull) { game.status = GameStatus.Completed; refundBothPlayers(gameId); emit GameEnded(gameId, address(0)); // 0地址表示平局 } } // 向获胜者转账全部押金 function payoutWinner(uint256 gameId) internal { Game storage game = games[gameId]; uint256 totalStake = game.stakeAmount * 2; payable(game.winner).transfer(totalStake); } // 平局时退还双方押金 function refundBothPlayers(uint256 gameId) internal { Game storage game = games[gameId]; payable(game.player1).transfer(game.stakeAmount); payable(game.player2).transfer(game.stakeAmount); } // 对手超时未加入时,创建者可撤回押金 function withdrawStakeIfWaiting(uint256 gameId) external { Game storage game = games[gameId]; require(game.status == GameStatus.WaitingForOpponent, "游戏不处于等待状态"); require(msg.sender == game.player1, "仅创建者可撤回"); payable(msg.sender).transfer(game.stakeAmount); delete games[gameId]; } }
2. ERC20代币版本合约
如果使用自定义ERC20代币,需修改为代币授权+转移的模式:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract TicTacToeERC20 { IERC20 public immutable token; enum GameStatus { WaitingForOpponent, InProgress, Completed } struct Game { address player1; address player2; uint256 stakeAmount; GameStatus status; address winner; uint8[9] board; } mapping(uint256 => Game) public games; uint256 public nextGameId; event GameCreated(uint256 gameId, address creator, uint256 stake); event GameJoined(uint256 gameId, address player2); event GameEnded(uint256 gameId, address winner); constructor(address _tokenAddress) { token = IERC20(_tokenAddress); } // 创建游戏:用户需先授权合约,再转移代币到合约锁定 function createGame(uint256 stakeAmount) external { require(stakeAmount > 0, "押金金额不能为0"); require(token.transferFrom(msg.sender, address(this), stakeAmount), "代币转移失败"); uint256 gameId = nextGameId++; games[gameId] = Game({ player1: msg.sender, player2: address(0), stakeAmount: stakeAmount, status: GameStatus.WaitingForOpponent, winner: address(0), board: [0,0,0,0,0,0,0,0,0] }); emit GameCreated(gameId, msg.sender, stakeAmount); } // 加入游戏:对手需存入等额代币 function joinGame(uint256 gameId) external { Game storage game = games[gameId]; require(game.status == GameStatus.WaitingForOpponent, "游戏不处于等待对手状态"); require(msg.sender != game.player1, "不能加入自己创建的游戏"); require(token.transferFrom(msg.sender, address(this), game.stakeAmount), "代币转移失败"); game.player2 = msg.sender; game.status = GameStatus.InProgress; emit GameJoined(gameId, msg.sender); } // 获胜者结算(代币版) function payoutWinner(uint256 gameId) internal { Game storage game = games[gameId]; uint256 totalStake = game.stakeAmount * 2; require(token.transfer(game.winner, totalStake), "代币发放失败"); } // 平局退款(代币版) function refundBothPlayers(uint256 gameId) internal { Game storage game = games[gameId]; require(token.transfer(game.player1, game.stakeAmount), "退款失败"); require(token.transfer(game.player2, game.stakeAmount), "退款失败"); } // 其他逻辑(落子、胜负判定、超时撤回)和ETH版本一致,此处省略 }
二、Web3.js前端交互实现
1. 连接MetaMask钱包
async function connectWallet() { if (!window.ethereum) { alert("请安装MetaMask钱包"); return null; } try { const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); return accounts[0]; } catch (error) { console.error("钱包连接失败:", error); return null; } }
2. 创建ETH版本游戏
async function createETHGame(contractInstance, stakeEthAmount) { const account = await connectWallet(); if (!account) return; const stakeWei = web3.utils.toWei(stakeEthAmount.toString(), 'ether'); try { const tx = await contractInstance.methods.createGame().send({ from: account, value: stakeWei }); const gameId = tx.events.GameCreated.returnValues.gameId; console.log("游戏创建成功,ID:", gameId); // 可将gameId和合约地址分享给好友 } catch (error) { console.error("创建游戏失败:", error); } }
3. 加入ETH版本游戏
async function joinETHGame(contractInstance, gameId, stakeEthAmount) { const account = await connectWallet(); if (!account) return; const stakeWei = web3.utils.toWei(stakeEthAmount.toString(), 'ether'); try { await contractInstance.methods.joinGame(gameId).send({ from: account, value: stakeWei }); console.log("成功加入游戏"); } catch (error) { console.error("加入游戏失败:", error); } }
4. ERC20版本游戏交互(需先授权)
// 授权合约花费代币 async function approveERC20(tokenContract, gameContractAddress, stakeAmount) { const account = await connectWallet(); if (!account) return; const amount = web3.utils.toWei(stakeAmount.toString(), 'ether'); try { await tokenContract.methods.approve(gameContractAddress, amount).send({ from: account }); console.log("代币授权成功"); } catch (error) { console.error("授权失败:", error); } } // 创建ERC20版本游戏 async function createERC20Game(gameContract, tokenContract, stakeAmount) { await approveERC20(tokenContract, gameContract.options.address, stakeAmount); const account = await connectWallet(); try { const tx = await gameContract.methods.createGame(web3.utils.toWei(stakeAmount.toString(), 'ether')).send({ from: account }); const gameId = tx.events.GameCreated.returnValues.gameId; console.log("ERC20游戏创建成功,ID:", gameId); } catch (error) { console.error("创建游戏失败:", error); } }
三、关键注意事项
- 安全防护:Solidity 0.8+默认开启溢出检查,使用
transfer(ETH)或OpenZeppelin的安全转账函数(ERC20)避免重入攻击; - 状态同步:前端需监听合约的
GameCreated、GameJoined、GameEnded事件,实时更新UI状态; - 超时机制:建议添加时间限制,比如创建游戏后24小时对手未加入,允许创建者撤回押金;
- 本地验证:前端可做棋盘状态的本地校验,但最终结果以合约执行为准,防止用户作弊。
内容的提问来源于stack exchange,提问作者webflax developers
相关产品推荐
相关产品推荐

