# Building a Pay-to-Win Game Smart Contract on Conflux

Building a Pay-to-Win Game Smart Contract on Conflux

Players pay CFX to buy “power.” More payment = more power. The contract tracks it all on-chain.


What is “pay to win”?

In a pay-to-win game, players can spend real money (or crypto) to get an advantage: stronger stats, faster progress, or items that free players can’t easily get. The economic rule is simple: pay more → get more power.

We can put that rule on a blockchain so that:

  • Payments are transparent and trustless.
  • No one can fake their power level; it’s stored on-chain.
  • The game (or another contract) can read each player’s power for leaderboards or gameplay.

This post walks through a minimal PayToWinGame contract written in Solidity for Conflux eSpace (EVM-compatible). Players send CFX to buy power; the owner can change the price and withdraw collected funds.


Contract overview

The contract does three things:

  1. Accepts CFX — Players call buyPower() and send CFX.
  2. Converts payment to powerpower = msg.value / pricePerPower, capped per transaction.
  3. Tracks everythingpowerOf[player], totalPowerSold, totalCollected; owner can withdraw() and setPrice().

1. State and config

We store who owns the contract, the price per unit of power, a per-tx cap, and each player’s total power. We also keep global stats for transparency.

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

contract PayToWinGame {

    address public owner;

    /// Price per 1 power (in wei, e.g. 0.01 CFX = 1e16 wei per power)
    uint256 public pricePerPower;
    /// Max power a single purchase can add (cap per tx)
    uint256 public maxPowerPerPurchase;

    /// Player address => total power bought (cumulative)
    mapping(address => uint256) public powerOf;
    /// Total power ever sold (for stats)
    uint256 public totalPowerSold;
    /// Total CFX collected by the contract
    uint256 public totalCollected;

    event PowerPurchased(address indexed player, uint256 amountPaidWei, uint256 powerAdded);
    event Withdrawn(address indexed to, uint256 amount);
    event PriceUpdated(uint256 newPricePerPower, uint256 newMaxPowerPerPurchase);

    error OnlyOwner();
    error InvalidAmount();
    error ZeroPower();
  • owner — Can withdraw and update price.
  • pricePerPower — Wei required per 1 power (e.g. 1e16 = 0.01 CFX per power).
  • maxPowerPerPurchase — Max power granted in a single buyPower() call.
  • powerOf[player] — Total power ever bought by that address.
  • totalPowerSold / totalCollected — For stats and optional off-chain leaderboards.

2. Constructor and access control

The deployer becomes owner and sets the initial price and cap. Only the owner can call withdraw and setPrice.

    modifier onlyOwner() {
        if (msg.sender != owner) revert OnlyOwner();
        _;
    }

    constructor(uint256 _pricePerPower, uint256 _maxPowerPerPurchase) {
        owner = msg.sender;
        pricePerPower = _pricePerPower;   // e.g. 1e16 = 0.01 CFX per 1 power
        maxPowerPerPurchase = _maxPowerPerPurchase; // e.g. 1000
    }

Example: deploy with pricePerPower = 1e16 (0.01 CFX) and maxPowerPerPurchase = 1000 so each call gives at most 1000 power.


3. Core logic: buy power

Players send CFX to buyPower(). We compute how much power they get, cap it, then credit them and optionally refund excess.

    function buyPower() external payable {
        if (msg.value == 0) revert InvalidAmount();
        uint256 power = msg.value / pricePerPower;
        if (power == 0) revert ZeroPower();
        if (power > maxPowerPerPurchase) {
            power = maxPowerPerPurchase;
        }
        uint256 cost = power * pricePerPower;
        if (cost < msg.value) {
            // Refund excess (optional: could leave as donation)
            (bool ok,) = msg.sender.call{ value: msg.value - cost }("");
            require(ok, "Refund failed");
        }
        powerOf[msg.sender] += power;
        totalPowerSold += power;
        totalCollected += cost;
        emit PowerPurchased(msg.sender, cost, power);
    }
  • power = msg.value / pricePerPower — Integer division; remainder is refunded or could be kept as donation.
  • Cap — If power > maxPowerPerPurchase, we use the cap and refund the rest.
  • Refundmsg.value - cost is sent back so the player only pays for the power they get.
  • UpdatespowerOf[msg.sender] increases, totalPowerSold and totalCollected increase, and we emit an event for indexing.

4. Read functions

Anyone can read a player’s power or total power sold. Your game or a leaderboard can use these.

    function getPower(address player) external view returns (uint256) {
        return powerOf[player];
    }

    function getTotalPowerSold() external view returns (uint256) {
        return totalPowerSold;
    }

5. Owner: withdraw and set price

Only the owner can withdraw the contract balance and change the pricing parameters.

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        (bool ok,) = owner.call{ value: balance }("");
        require(ok, "Withdraw failed");
        emit Withdrawn(owner, balance);
    }

    function setPrice(uint256 _pricePerPower, uint256 _maxPowerPerPurchase) external onlyOwner {
        pricePerPower = _pricePerPower;
        maxPowerPerPurchase = _maxPowerPerPurchase;
        emit PriceUpdated(_pricePerPower, _maxPowerPerPurchase);
    }

    receive() external payable {}
}
  • withdraw() — Sends the full contract balance to owner.
  • setPrice() — Updates pricePerPower and maxPowerPerPurchase for future purchases.
  • receive() — Allows the contract to accept plain CFX transfers (e.g. for buyPower()).

Flow in one picture

Player                    Contract
   |                         |
   |  send CFX               |
   |  buyPower()             |
   |------------------------>|
   |                         | power = value / pricePerPower
   |                         | powerOf[player] += power
   |                         | (refund excess if any)
   |<------------------------|
   |  (refund if any)        |
   |                         |
   |  getPower(player)       |
   |------------------------>|
   |<------------------------|  powerOf[player]

Example usage

  • Deploy:
    PayToWinGame(1e16, 1000) → 0.01 CFX per power, max 1000 power per tx.

  • Player:
    Sends 1 CFX to buyPower() → gets 100 power (1e18 / 1e16 = 100), rest refunded if any.
    Sends 0.005 CFX → gets 0 power (integer division), so design your pricePerPower so small payments still give at least 1 power, or they’ll revert with ZeroPower.

  • Game / frontend:
    Call getPower(playerAddress) to show power or build a leaderboard; use PowerPurchased events for history.

  • Owner:
    Call withdraw() to take collected CFX; call setPrice(newPrice, newCap) to change economics.


Security notes

  • Refund — We use call{ value } for the refund; in more complex contracts consider reentrancy guards (e.g. OpenZeppelin) and checks-effects-interactions.
  • Owner — Owner has full control over funds and pricing; use a multisig or timelock if the contract holds significant value.
  • Integer mathpower = msg.value / pricePerPower truncates; small msg.value can yield 0 and revert with ZeroPower. Choose pricePerPower accordingly.

Deploy on Conflux eSpace

Use Hardhat (or your existing Conflux eSpace config). Example deploy script:

// scripts/deploy-paytowin.js
const pricePerPower = ethers.parseEther("0.01");  // 0.01 CFX per 1 power
const maxPowerPerPurchase = 1000n;

const Game = await ethers.getContractFactory("PayToWinGame");
const game = await Game.deploy(pricePerPower, maxPowerPerPurchase);
await game.waitForDeployment();
console.log("PayToWinGame at:", await game.getAddress());

Run against Conflux eSpace Testnet (Chain ID 71) so players can pay with testnet CFX.


Summary

Piece Role
buyPower() Pay CFX → get power (capped, excess refunded).
powerOf[player] Cumulative power per address (for leaderboards / game logic).
getPower / getTotalPowerSold Read-only views for frontends.
withdraw() / setPrice() Owner collects CFX and adjusts economics.
Events PowerPurchased, Withdrawn, PriceUpdated for indexing and UX.

GitHub; https://github.com/Vikash-8090-Yadav/playtowinconflux
Demo Video; https://youtu.be/vENAC0I9pag

This gives you a minimal, on-chain pay-to-win economy: pay CFX → get power → use it in your game or leaderboard. Deploy it on Conflux eSpace and plug your frontend or game backend into getPower() and the events.