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 {}
}
Submitted on: 2025-10-26 09:40:55
Comments
Log in to comment.
No comments yet.