ReownGameDualFeeV2

Description:

ERC20 token contract with Pausable, Factory capabilities. Standard implementation for fungible tokens on Ethereum.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/*
 * ReownGameDualFeeV2 — single-file (flattened-style) version
 * - No external imports. Minimal inlined interfaces/libraries.
 * - Keeps original behavior: fee is pulled from player and sent directly to treasury on startGame.
 * - Adds owner-only rescue functions to sweep accidentally sent tokens/ETH.
 * - Adds Pausable (owner) for emergency stops.
 * - Maintains signature-based score verification.
 *
 * NOTE: Redeploying this contract creates a NEW address with empty balances.
 *       If your previous deployment holds payout tokens, withdraw them from
 *       the old contract first and fund this new one.
 */

// ------------------------------------------------------------
// Minimal Interfaces
// ------------------------------------------------------------
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IERC20Metadata is IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
}

// ------------------------------------------------------------
// Ownable (minimal)
// ------------------------------------------------------------
abstract contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address initialOwner) {
        require(initialOwner != address(0), "Owner=0");
        _owner = initialOwner;
        emit OwnershipTransferred(address(0), initialOwner);
    }

    modifier onlyOwner() {
        require(msg.sender == _owner, "Ownable: caller is not the owner");
        _;
    }

    function owner() public view returns (address) { return _owner; }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "newOwner=0");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

// ------------------------------------------------------------
// ReentrancyGuard (minimal)
// ------------------------------------------------------------
abstract contract ReentrancyGuard {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
}

// ------------------------------------------------------------
// Pausable (minimal, owner-controlled)
// ------------------------------------------------------------
abstract contract Pausable is Ownable {
    bool private _paused;
    event Paused(address account);
    event Unpaused(address account);

    constructor(address initialOwner) Ownable(initialOwner) {
        _paused = false;
    }

    modifier whenNotPaused() {
        require(!_paused, "Pausable: paused");
        _;
    }

    modifier whenPaused() {
        require(_paused, "Pausable: not paused");
        _;
    }

    function paused() public view returns (bool) { return _paused; }

    function pause() public onlyOwner whenNotPaused { _paused = true; emit Paused(msg.sender); }
    function unpause() public onlyOwner whenPaused { _paused = false; emit Unpaused(msg.sender); }
}

// ------------------------------------------------------------
// Address utils (subset for SafeERC20)
// ------------------------------------------------------------
library Address {
    function isContract(address account) internal view returns (bool) {
        return account.code.length > 0;
    }

    function functionCall(address target, bytes memory data, string memory errorMessage)
        internal returns (bytes memory)
    {
        require(isContract(target), "Address: call to non-contract");
        (bool success, bytes memory returndata) = target.call(data);
        require(success, errorMessage);
        return returndata;
    }
}

// ------------------------------------------------------------
// SafeERC20 (minimal; handles optional return)
// ------------------------------------------------------------
library SafeERC20 {
    using Address for address;

    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        bytes memory data = abi.encodeWithSelector(token.transfer.selector, to, value);
        bytes memory ret = address(token).functionCall(data, "SafeERC20: transfer failed");
        if (ret.length > 0) {
            require(abi.decode(ret, (bool)), "SafeERC20: ERC20 transfer false");
        }
    }

    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        bytes memory data = abi.encodeWithSelector(token.transferFrom.selector, from, to, value);
        bytes memory ret = address(token).functionCall(data, "SafeERC20: transferFrom failed");
        if (ret.length > 0) {
            require(abi.decode(ret, (bool)), "SafeERC20: ERC20 transferFrom false");
        }
    }

    function safeApprove(IERC20 token, address spender, uint256 value) internal {
        bytes memory data = abi.encodeWithSelector(token.approve.selector, spender, value);
        bytes memory ret = address(token).functionCall(data, "SafeERC20: approve failed");
        if (ret.length > 0) {
            require(abi.decode(ret, (bool)), "SafeERC20: ERC20 approve false");
        }
    }
}

// ------------------------------------------------------------
// ECDSA & MessageHash (minimal)
// ------------------------------------------------------------
library ECDSA {
    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        if (signature.length != 65) revert("ECDSA: invalid signature length");
        bytes32 r; bytes32 s; uint8 v;
        assembly {
            r := mload(add(signature, 0x20))
            s := mload(add(signature, 0x40))
            v := byte(0, mload(add(signature, 0x60)))
        }
        // EIP-2 malleability check
        require(uint256(s) <= 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, "ECDSA: bad s");
        require(v == 27 || v == 28, "ECDSA: bad v");
        address signer = ecrecover(hash, v, r, s);
        require(signer != address(0), "ECDSA: ecrecover failed");
        return signer;
    }
}

library MessageHashUtils {
    function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
        // 32 is the length in bytes of hash
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:
32", hash));
    }
}

// ------------------------------------------------------------
// Main Contract
// ------------------------------------------------------------
contract ReownGameDualFeeV2 is Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using MessageHashUtils for bytes32;

    IERC20 public immutable payoutToken;   // token for rewards (held by this contract)
    IERC20 public immutable feeToken;      // token used to pay the fee (e.g., USDC)

    address public treasury;               // fee recipient
    uint256 public feeAmount;              // in feeToken's decimals (USDC: 6 decimals)
    address public scoreSigner;            // backend signer that authorizes rewards
    uint256 public payoutPerPoint;         // reward rate: tokens per score point (payoutToken decimals)

    mapping(address => bool) public hasActiveGame;
    mapping(bytes32 => bool) public usedClaimIds;

    event GameStarted(address indexed player, uint256 feePaid, address feeToken, address treasury);
    event RewardClaimed(address indexed player, uint256 score, uint256 payoutAmount);
    event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
    event FeeUpdated(uint256 oldFee, uint256 newFee);
    event ScoreSignerUpdated(address indexed oldSigner, address indexed newSigner);
    event PayoutPerPointUpdated(uint256 oldRate, uint256 newRate);

    constructor(
        address _payoutToken,
        address _feeToken,
        address _treasury,
        uint256 _feeAmount,
        address _scoreSigner,
        uint256 _payoutPerPoint,
        address _owner
    ) Pausable(_owner) {
        require(_payoutToken != address(0) && _feeToken != address(0), "token=0");
        require(_treasury != address(0), "treasury=0");
        require(_scoreSigner != address(0), "signer=0");
        payoutToken = IERC20(_payoutToken);
        feeToken = IERC20(_feeToken);
        treasury = _treasury;
        feeAmount = _feeAmount;
        scoreSigner = _scoreSigner;
        payoutPerPoint = _payoutPerPoint;
    }

    // --------------------------------------------------------
    // Admin
    // --------------------------------------------------------
    function setTreasury(address newTreasury) external onlyOwner {
        require(newTreasury != address(0), "treasury=0");
        emit TreasuryUpdated(treasury, newTreasury);
        treasury = newTreasury;
    }

    function setFeeAmount(uint256 newFee) external onlyOwner {
        emit FeeUpdated(feeAmount, newFee);
        feeAmount = newFee;
    }

    function setScoreSigner(address newSigner) external onlyOwner {
        require(newSigner != address(0), "signer=0");
        emit ScoreSignerUpdated(scoreSigner, newSigner);
        scoreSigner = newSigner;
    }

    function setPayoutPerPoint(uint256 newRate) external onlyOwner {
        emit PayoutPerPointUpdated(payoutPerPoint, newRate);
        payoutPerPoint = newRate;
    }

    // --------------------------------------------------------
    // Core Flow
    // --------------------------------------------------------
    function startGame() external whenNotPaused nonReentrant {
        // Pull fee from player and immediately forward to treasury
        feeToken.safeTransferFrom(msg.sender, treasury, feeAmount);
        hasActiveGame[msg.sender] = true;
        emit GameStarted(msg.sender, feeAmount, address(feeToken), treasury);
    }

    function claimReward(uint256 score, bytes32 claimId, bytes calldata signature)
        external whenNotPaused nonReentrant
    {
        require(hasActiveGame[msg.sender], "no active game");
        require(!usedClaimIds[claimId], "claim used");
        require(_verifyScoreSignature(msg.sender, score, claimId, signature), "bad signature");

        usedClaimIds[claimId] = true;
        hasActiveGame[msg.sender] = false;

        uint256 amount = score * payoutPerPoint;
        payoutToken.safeTransfer(msg.sender, amount);
        emit RewardClaimed(msg.sender, score, amount);
    }

    function _verifyScoreSignature(
        address player,
        uint256 score,
        bytes32 claimId,
        bytes calldata signature
    ) internal view returns (bool) {
        // Domain-separate with contract address to avoid replay across deployments
        bytes32 digest = keccak256(abi.encodePacked(
            address(this),
            player,
            score,
            claimId
        ));
        bytes32 ethSigned = digest.toEthSignedMessageHash();
        return ECDSA.recover(ethSigned, signature) == scoreSigner;
    }

    // --------------------------------------------------------
    // Owner withdrawals / rescue
    // --------------------------------------------------------
    function withdrawPayoutTokens(address to, uint256 amount) external onlyOwner {
        require(to != address(0), "to=0");
        payoutToken.safeTransfer(to, amount);
    }

    function rescueERC20(address token, address to, uint256 amount) external onlyOwner {
        require(to != address(0), "to=0");
        IERC20(token).transfer(to, amount); // deliberately not using SafeERC20 to keep generic
    }

    function rescueETH(address payable to, uint256 amount) external onlyOwner {
        require(to != address(0), "to=0");
        (bool ok, ) = to.call{value: amount}("");
        require(ok, "ETH transfer failed");
    }

    receive() external payable {}
}

Tags:
ERC20, Token, Pausable, Factory|addr:0x9403f1e384211aa2441a519c2887c7cbec3c62ae|verified:true|block:23656116|tx:0x3e47794255ed4d2e18e2758a3cf72b8ef77fec26c5ab728a69b435ee42a18510|first_check:1761468055

Submitted on: 2025-10-26 09:40:55

Comments

Log in to comment.

No comments yet.