Gaming SDK - Play-to-earn
Implementation design of the Gaming SDK play to earn feature that prevents fraudulent transactions when players are gaming onchain.
Requirements
For the scenario: "I would like to have a player be able to claim a max amount of tokens after reaching a certain level and that would be maxed out per day. So after the level is reached it would restart each day."
- Players should not be able to submit fraudulent transactions
- Players should not be able to submit the same transaction twice
Solutions already looked into
- Steph's meta transaction proposal: https://hackmd.io/BffeJGjmQminTwyohNz0eA
- Drawback: Probably not the best UX if the player has to sign transactions several times.
Few thoughts
- Can we use commit/reveal style process on-chain here? Game server publishes a list of hash for each checkpoint and show the secret value on-screen on reaching a checkpoint, players can submit the corresponding hash to verify and claim their reward. Optimization -> use merkle tree.
- If centralization is okay as mentioned in the ticket, can we not simply have the game server transfer assets to players once they reach a checkpoint, thus by eliminating all UX problems and having single point for achieving it? (In this case, the gas fees needs to be paid by the central party, whoever that is.)
Commit - Reveal Scheme
The problems that we are trying to address:
- For a better UX, we do not want players to keep signing every now & then, while playing the game.
- Players should not be able to replay the transaction and get the same rewards more than once.
- Players should not be able to claim rewards unless they have played the game and cleared a level. In other words, no one other than the intended player should be able to claim the reward.
The assumptions:
- There is a game server responsible for holding the game logic (at least till it has not transferred all game elements and logic to the player side) and game state.
- The owner of the game server is probably the owner of the smart contract minting rewards or at least provide an end-point to check a game state for a player in case the game server owner is different from the owner of the smart contract.
- Smart contract owner does not want to pay for the gas fees for all players and their rewards as it can get very expensive. (Alternatively, if they are willing to pay, simple transfer of the rewards is as simple a solution can get.)
The logic
Game server keeps track of the sign ups. Game server knows the progress of each player. Before the game play begins, and after a player has signed up using their address that they want to use for receiving the rewards, the game server can publish a Merkle Root mapped to the player's corresponding address.
[Note: The code examples are only for explanatory purposes and are not necessarily the most efficient.]
mapping(string => address) rootToAddress;
The game developers can set a certain minimum level of rewards that they want to pre-authorize. For example, if the game has 25 checkpoints where there is a possibility to earn a reward, the game developer can choose to pre-authorize rewards for 10 checkpoints.
What pre-authorize means is that, game-developer has generated 10 secrets (unique strings) for a particular player, created a Merkle Tree using these secrets and published the corresponding Merkle Root to the smart contract. When a player reaches a certain checkpoint, the corresponding secret will be made visible to the player (along with the Merkle Proof).The player can then use this proof to mint the reward.
We also need a mechanism that restricts a player to mint a reward multiple times using the same secret. For this, we can have another mapping from the secret to player's address OR from the secret to tokenID. Once a secret is used to mint a token, the corresponding mapping will be added. When minting, a check should be performed to see if the mapping for the secret already exists or not and if it does, it should not be minted.
An example code for the logic:
pragma solidity >=0.8.0 <0.9.0;
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract RandomGameContract {
mapping(string => address) rootToAddress;
mapping(string => address) secretToAddress;
function verifyMerkleProof(
bytes32[] calldata _merkleProof,
string merkleRoot,
string secret,
string tokenId
) public returns (bool){
// Allow only the player for whom this root is intended
require(rootToAddress[merkleRoot] == msg.sender, "Not authorised");
// Check if the proof is valid
require(MerkleProof.verify(_merkleProof, merkleRoot, secret), "Incorrect proof");
// Check if the token for corresponding secret has been minted or not
require(secretToAddress[secret] == address(0), "Already minted");
/** Minting call goes here with param 'tokenId' to mint/redeem specific token **/
secretToAddress[secret] = msg.sender; //Replace msg.sender with tokenId if mapping corresponds to tokenId
return true;
}
}
The minting logic can be derived from the lazy minting technique.
To save the gas fees for minting each rewards, another function can be provided for bulk minting which would accept an array of Merkle Proofs, verify each of them (or simply create the tree if all leaves are used) and mint all rewards together. This depends on the game and what the game developers want.
Once a player has reached a certain checkpoint, game developer can publish new Merkle Root for next set of checkpoints (and remove the old one, if required). There is no advantage of doing so other than the game developer wanting to have some custom logic with their rewards token and/or to track some stuff for the players. Putting a single Merkle Root for all checkpoints in the game in the beginning also works and game developers need to pay for gas only once for each player.
Game developer can also publish a single merkle root for certain set of players to save on the gas cost.
Notes
The terminology of commit-reveal corresponds from the fact that before beginning the game play, game server commits to some secret values by putting the corresponding hash on chain. The minting is made possible only if the corresponding secrets for the corresponding player are revealed off-chain and verified on-chain.
Even though the game server acts as a central entity here, the players get an assurance that their rewards are locked on-chain before the game begins, hence offering a certain level of confidence. Players of-course have to rely on game server for getting the secrets once reaching a certain checkpoint and relying on them to not act maliciously for faking or giving the wrong secrets.