Solidity实现后端授权的中心化游戏用户代币申领功能咨询
带后端授权的代币申领功能实现方案
核心逻辑
你的需求可以通过ECDSA签名验证方案实现,既能保证只有后端授权的请求才能完成代币转账,又可以让用户自行发起交易承担Gas成本,后端不需要上链操作:
- 后端生成唯一的ECDSA密钥对,公钥存入合约,私钥仅后端持有不对外泄露
- 用户发起申领请求时先向后端提交任务完成凭证、自己的钱包地址
- 后端校验用户任务完成状态合法后,用私钥对「用户地址、申领金额、签名过期时间、全局唯一流水号」四个参数签名,将参数和签名一并返回给用户
- 用户拿到参数和签名后调用合约申领函数,合约校验签名合法性,校验通过后直接将对应金额的代币从游戏主钱包转账到用户地址
合约代码示例
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract GameTokenClaim { using ECDSA for bytes32; using SafeERC20 for IERC20; // 后端签名公钥,部署合约时传入 address public immutable signer; // 游戏代币合约地址 IERC20 public immutable gameToken; // 记录已使用的流水号,避免同一签名重复申领 mapping(bytes32 => bool) public usedNonce; event ClaimSuccess(address indexed user, uint256 amount, bytes32 nonce); constructor(address _signer, address _gameToken) { signer = _signer; gameToken = IERC20(_gameToken); } function claimTokens( uint256 amount, uint256 expireAt, bytes32 nonce, bytes calldata signature ) external { // 校验签名是否过期 require(block.timestamp <= expireAt, "Claim signature expired"); // 校验流水号是否已被使用 require(!usedNonce[nonce], "Nonce already used"); // 拼接签名原文,逻辑必须和后端签名时完全一致 bytes32 msgHash = keccak256(abi.encodePacked(msg.sender, amount, expireAt, nonce)); // 转换为以太坊签名标准哈希 bytes32 ethSignedMsgHash = msgHash.toEthSignedMessageHash(); // 校验签名是否为后端私钥生成 require(ethSignedMsgHash.recover(signature) == signer, "Invalid signature"); // 标记流水号已使用 usedNonce[nonce] = true; // 从游戏主钱包转账代币到用户地址,需提前给本合约授权足够的代币额度 gameToken.safeTransferFrom(gameToken.owner(), msg.sender, amount); emit ClaimSuccess(msg.sender, amount, nonce); } }
后端签名示例(Node.js环境)
const { ethers } = require("ethers"); // 后端持有私钥,严禁泄露 const signerPrivateKey = "YOUR_BACKEND_PRIVATE_KEY"; const wallet = new ethers.Wallet(signerPrivateKey); async function generateClaimSignature(userAddress, amount, expireAt, nonce) { // 参数拼接顺序、类型必须和合约完全一致 const msgHash = ethers.utils.solidityKeccak256( ["address", "uint256", "uint256", "bytes32"], [userAddress, amount, expireAt, nonce] ); const signature = await wallet.signMessage(ethers.utils.arrayify(msgHash)); return { amount, expireAt, nonce, signature }; }
注意事项
- 合约部署完成后,需要用游戏主钱包在代币合约中给申领合约授权足够的代币额度,否则转账会失败
- 签名过期时间建议设置为5-10分钟,避免签名泄露后被恶意使用
- 全局唯一流水号可以用用户ID+任务ID拼接后做哈希生成,确保同一个任务对应唯一的流水号,防止重复申领
- 若你使用的是原生ETH作为奖励,只需把合约中的ERC20转账逻辑替换为payable转账即可,提前将需要发放的ETH转入申领合约中
- 整个流程中只有用户调用
claimTokens函数需要支付Gas,后端仅做离线签名无任何链上成本
内容的提问来源于stack exchange,提问作者Matheus Benedet
相关产品推荐
相关产品推荐

