You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 12:06:03