Base Contract
The ChallengeBase contract serves as a core component for managing gaming challenges on the blockchain. It integrates role-based access control, game template management, and prize distribution, ensuring secure and flexible challenge creation and participation. Additionally, it manages valid ERC20 tokens for transactions and maintains a system to increment and track challenge IDs effectively.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title Base contract for all Challenge types, includes common structs and functions
/// @author Challenge.GG
/// @dev The ChallengeBase contract serves as a core component for managing gaming challenges on the blockchain.
/// It integrates role-based access control, game template management, and prize distribution, ensuring secure and flexible challenge creation and participation.
/// Additionally, it manages valid ERC20 tokens for transactions and maintains a system to increment and track challenge IDs effectively.
contract ChallengeBase is AccessControl {
bytes32 public constant CONTRACT_ROLE = keccak256("CONTRACT"); //CONTRACT role is required to increment ids
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER"); //MANAGER role is required to handle gametemplates, payout structures and more
bytes32 public constant PAYER_ROLE = keccak256("PAYMENT"); //PAYER role is required to handle payments
uint256 private _challengeId = 0; // Current challenge id, incremented for each new challenge created
mapping (uint16 => uint256) private _lastestIds; //Latest ids for each game template
mapping (address => bool) public isBlocked; //Players blocked from participating in challenges
mapping (uint16 => GameTemplate) public gameTemplates; //Map of ids to game templates
mapping (bool => PrizeDistribution[]) public prizeDistributions; // Mapping for sponsored and unsponsored distributions of prizes, x number of players will have a certain distribution including commission for Challenge.gg if not sponsored
mapping(address => ValidERC20Token) public validTokens; //Valid tokens to play with and their commission threshold (When we send comission to split contract)
//Struct definitions
//Challenge status
enum ChallengeStatus {
Unvailable, //0 is default, when checking if challenge exists
Created,
Running,
Finished,
Error,
Canceled
}
//Player status
enum PlayerStatus {
NotParticipating, //0 is default, when checking if player exists
Participating,
Completed,
Failed
}
//A template to base created challenges on
struct GameTemplate {
uint256 fee;
IERC20 token;
uint256 prizeFund;
IERC20 prizeFundToken;
uint16 maxPlayersCount; //Do not need min players since min players is always equal to max players
bool paused; //If paused, players can not create or join challenges of this type
bool softPaused; //If soft paused, players can join existing challenges of this type but not create new ones
}
// Percentage distribution of prizes, [80,10] = 80% to 1:st place, 10% to 2:nd place, 10% to ChallengeGG as comission
struct PrizeDistribution {
uint16 minPlayers;
uint16 maxPlayers;
uint8[] distribution;
}
//Valid tokens to play with and their commission threshold (When we send comission to split contract)
struct ValidERC20Token {
bool valid;
uint256 commission_threshold;
}
//Modifiers
/// @dev Restricted to members of the admin role.
modifier onlyAdmin()
{
require(isAdmin(msg.sender), "Restricted to admins");
_;
}
/// @dev Restricted to members of the Contract role.
modifier onlyContract()
{
require(isContract(msg.sender), "Restricted to allowed contracts");
_;
}
/// @dev Restricted to members of the Manager role.
modifier onlyManager()
{
require(isManager(msg.sender), "Restricted to managers");
_;
}
/// @dev Constructor
/// @param multisigAdmin Mutisig owner
constructor(address multisigAdmin) {
_grantRole(DEFAULT_ADMIN_ROLE, multisigAdmin);
_setRoleAdmin(CONTRACT_ROLE, DEFAULT_ADMIN_ROLE);
_setRoleAdmin(MANAGER_ROLE, DEFAULT_ADMIN_ROLE);
_setRoleAdmin(PAYER_ROLE, DEFAULT_ADMIN_ROLE);
}
//Update functions
/// @dev Add an account to the Contract role. Restricted to admins.
/// @param account Address to add
function addContract(address account) external onlyAdmin
{
grantRole(CONTRACT_ROLE, account);
}
/// @dev Remove an account from the Contract role. Restricted to admins.
/// @param account Address to remove
function removeContract(address account) external onlyAdmin
{
revokeRole(CONTRACT_ROLE, account);
}
/// @dev Add an account to the Manager role. Restricted to admins.
/// @param account Address to add
function addManager(address account) external onlyAdmin
{
grantRole(MANAGER_ROLE, account);
}
/// @dev Remove an account from the Manager role. Restricted to admins.
/// @param account Address to remove
function removeManager(address account) external onlyAdmin
{
revokeRole(MANAGER_ROLE, account);
}
/// @dev Add an account to the Payer role. Restricted to admins.
/// @param account Address to add
function addPayer(address account) external onlyAdmin
{
grantRole(PAYER_ROLE, account);
}
/// @dev Remove an account from the Payer role. Restricted to admins.
/// @param account Address to remove
function removePayer(address account) external onlyAdmin
{
revokeRole(PAYER_ROLE, account);
}
/// @dev Set challenge id offset. If we replace contract we need to set the challenge id offset. Restricted to admins.
/// @param challengeId Challenge id to set
function setChallengeIdOffset(uint256 challengeId) external onlyAdmin {
_challengeId=challengeId;
}
/// @dev Set current id offset for a game template. Restricted to admins.
/// @param gameTemplateId Game template id
/// @param challengeId Challenge id to set
function setCurrentIdOffset(uint16 gameTemplateId, uint256 challengeId) external onlyAdmin {
_lastestIds[gameTemplateId] = challengeId;
}
/// @dev Increment challenge id by one and returns the id, restricted to contracts
/// @return challengeId Current challenge id
function incrementAndGetChallengeId() external onlyContract returns(uint256) {
_challengeId++;
return _challengeId;
}
/// @dev Increment the challenge id and set it for a specific game template id
/// @param gameTemplateId Game template id
/// @return challengeId Current challenge id
function incrementAndSetCurrentId(uint16 gameTemplateId) external onlyContract returns(uint256) {
_challengeId++;
_lastestIds[gameTemplateId] = _challengeId;
return _challengeId;
}
/// @dev Resets the current id for a game template, restricted to contracts
/// @param gameTemplateId Game template id
function resetCurrentId(uint16 gameTemplateId) external onlyContract {
_lastestIds[gameTemplateId] = 0;
}
/// @dev Add a ERC20 token to the valid tokens list, restricted to managers
/// @param tokenAddress Token address to add
/// @param commission_threshold Commission threshold to set
function addValidToken(address tokenAddress, uint256 commission_threshold) external onlyManager {
validTokens[tokenAddress] = ValidERC20Token({
valid: true,
commission_threshold: commission_threshold
});
}
/// @dev Remove a ERC20 token from the valid tokens list, restricted to managers
function removeValidToken(address tokenAddress) external onlyManager {
delete validTokens[tokenAddress];
}
/// @dev Add a new game template, restricted to managers
/// @param gameTemplateId Game template id
/// @param maxPlayersCount Max players for this template
/// @param fee Fee for this template
/// @param token ERC20 Token to use for this template
function addGameTemplate(uint16 gameTemplateId, uint16 maxPlayersCount, uint256 fee, address token, uint256 prizeFund, address prizeFundToken) external onlyManager {
require(gameTemplates[gameTemplateId].maxPlayersCount==0,"Template already exist");
_setGameTemplate(gameTemplateId, maxPlayersCount, fee, token, prizeFund, prizeFundToken);
}
/// @dev Update a game template, restricted to managers
/// @param gameTemplateId Game template id
/// @param maxPlayersCount Max players for this template
/// @param fee Fee for this template
/// @param token ERC20 Token to use for this template
function updateGameTemplate(uint16 gameTemplateId, uint16 maxPlayersCount, uint256 fee, address token, uint256 prizeFund, address prizeFundToken) external onlyManager {
require(gameTemplates[gameTemplateId].maxPlayersCount!=0,"Template does not exist");
_setGameTemplate(gameTemplateId, maxPlayersCount, fee, token, prizeFund, prizeFundToken);
}
/// @dev Delete a game template, restricted to managers
/// @param gameTemplateId Game template id
function deleteGameTemplate(uint16 gameTemplateId) external onlyManager {
require(gameTemplates[gameTemplateId].maxPlayersCount!=0,"Template does not exist");
delete gameTemplates[gameTemplateId];
}
/// @dev Set if template is paused (players can not create or join challenges of this type). Restricted to managers
/// @param gameTemplateId Game template id
/// @param paused True if template should be paused
function setTemplatePaused(uint16 gameTemplateId, bool paused) external onlyManager {
GameTemplate storage gameTemplate = gameTemplates[gameTemplateId];
gameTemplate.paused = paused;
}
/// @dev Set if template is soft paused (players can join existing challenges but not create new ones). Restricted to managers
/// @param gameTemplateId Game template id
/// @param softPaused True if template should be soft paused
function setTemplateSoftPaused(uint16 gameTemplateId, bool softPaused) external onlyManager {
GameTemplate storage gameTemplate = gameTemplates[gameTemplateId];
gameTemplate.softPaused = softPaused;
}
/// @dev Add or replace a payout distribution, restricted to managers
/// @param minPlayers Minimum number of players for this distribution
/// @param maxPlayers Maximum number of players for this distribution
/// @param percentage Percentage distribution, [80,10] = 80% to 1:st place, 10% to 2:nd place, 10% to ChallengeGG as comission
/// @param isSponsored True if sponsored distribution
function setPayoutDistribution(uint16 minPlayers, uint16 maxPlayers, uint8[] memory percentage, bool isSponsored) external onlyManager {
require(percentage.length>0, "Distribution is empty");
require(minPlayers>0, "Min players must be greater than 0");
require(maxPlayers>=minPlayers, "Max players must be greater than or equal to min players");
// Ensure valid distribution
uint256 totalPercentage;
for (uint256 i = 0; i < percentage.length; i++) {
totalPercentage += percentage[i];
}
require(totalPercentage <= 100, "Total percentage exceeds 100");
bool _replaced;
PrizeDistribution[] storage prizeDistribution = prizeDistributions[isSponsored];
for (uint8 i = 0; i < prizeDistribution.length; i++) {
if(prizeDistribution[i].minPlayers==minPlayers && prizeDistribution[i].maxPlayers==maxPlayers){
prizeDistribution[i].distribution=percentage;
_replaced=true;
}
}
if(!_replaced){
//uint8[] memory base;
prizeDistribution.push(PrizeDistribution(minPlayers, maxPlayers, percentage));
//prizeDistribution[prizeDistribution.length-1].distribution=percentage;
}
}
/// @dev Delete a payout distribution, restricted to managers
/// @param minPlayers Minimum number of players for this distribution
/// @param maxPlayers Maximum number of players for this distribution
/// @param isSponsored True if sponsored distribution should be deleted
function deletePayoutDistribution(uint16 minPlayers, uint16 maxPlayers, bool isSponsored) external onlyManager {
PrizeDistribution[] storage prizeDistribution = prizeDistributions[isSponsored];
for (uint8 i = 0; i < prizeDistribution.length; i++) {
if(prizeDistribution[i].minPlayers==minPlayers && prizeDistribution[i].maxPlayers==maxPlayers){
delete prizeDistribution[i];
}
}
}
/// @dev Set if player is blocked from participating in challenges
/// @param player Address of player
/// @param blocked True if player should be blocked
function setBlocked(address player, bool blocked) external onlyManager {
isBlocked[player]=blocked;
}
//View functions
/// @dev Checks if a ERC20 token is valid to use in Challenge.gg.
/// @param tokenAddress Token address to check
/// @return valid True if token is valid
function isValidToken(address tokenAddress) external view returns(bool){
return validTokens[tokenAddress].valid;
}
/// Returs the commission threshold for a token, as soon as commision for token is above theshold we send commission to split contract
/// @param tokenAddress Token address to check
/// @return commission_threshold Commission threshold
function getValidTokenThreshold(address tokenAddress) external view returns(uint256){
return validTokens[tokenAddress].commission_threshold;
}
/// @dev Returns the current challenge id
/// @return challengeId Current challenge id
function getChallengeId() external view returns(uint256) {
return _challengeId;
}
/// @dev Returns the current challenge id for a game template
/// @param gameTemplateId Game template id
/// @return challengeId Current challenge id
function getCurrentId(uint16 gameTemplateId) external view returns(uint256) {
return _lastestIds[gameTemplateId];
}
/// Returns the game template for a id
/// @param gameTemplateId Game template id
/// @return gameTemplate Game template
function getGameTemplate(uint16 gameTemplateId) external view returns(GameTemplate memory) {
return gameTemplates[gameTemplateId];
}
/// Checks if a template is paused (players can not create or join challenges of this type)
/// @param gameTemplateId Game template id
/// @return paused True if template is paused
function isTemplatePaused(uint16 gameTemplateId) external view returns (bool paused) {
GameTemplate storage gameTemplate = gameTemplates[gameTemplateId];
return gameTemplate.paused;
}
/// Checks if template is soft paused (players can join existing challenges but not create new ones)
/// @param gameTemplateId Game template id
/// @return softPaused True if template is soft paused
function isTemplateSoftPaused(uint16 gameTemplateId) external view returns (bool softPaused) {
GameTemplate storage gameTemplate = gameTemplates[gameTemplateId];
return gameTemplate.softPaused;
}
/// Returns the payout distribution for a number of players
/// @param players Number of players
/// @param isSponsored True if sponsored distribution should be returned
/// @return minPlayers Minimum number of players for this distribution
/// @return maxPlayers Maximum number of players for this distribution
/// @return percentage Percentage distribution, [80,10] = 80% to 1:st place, 10% to 2:nd place, 10% to ChallengeGG as comission
/// @return commission Commission for this distribution
function getPayoutDistribution(uint16 players, bool isSponsored) external view returns(uint16 minPlayers, uint16 maxPlayers, uint8[] memory percentage,uint8 commission) {
PrizeDistribution[] storage prizeDistribution = prizeDistributions[isSponsored];
for (uint8 i = 0; i < prizeDistribution.length; i++) {
if(prizeDistribution[i].minPlayers<=players && prizeDistribution[i].maxPlayers>=players){
uint8 sum=0;
for(uint j=0; j< prizeDistribution[i].distribution.length; j++){
sum=sum+prizeDistribution[i].distribution[j];
}
uint8 _commission= 100-sum;
return (prizeDistribution[i].minPlayers,prizeDistribution[i].maxPlayers, prizeDistribution[i].distribution,_commission);
}
}
}
/// @dev Checks if the account belongs to the admin role.
/// @param account Address to check
/// @return True if account belongs to 'ADMIN' role
function isAdmin(address account) public view returns (bool)
{
return hasRole(DEFAULT_ADMIN_ROLE, account);
}
/// @dev Checks if the account belongs to the contract role.
/// @param account Address to check
/// @return True if account belongs to 'CONTRACT' role
function isContract(address account) public view returns (bool)
{
return hasRole(CONTRACT_ROLE, account);
}
/// @dev Checks if the account belongs to the manager role.
/// @param account Address to check
/// @return True if account belongs to 'MANAGER' role
function isManager(address account) public view returns (bool)
{
return hasRole(MANAGER_ROLE, account);
}
/// @dev Checks if the account belongs to the payer role.
/// @param account Address to check
/// @return True if account belongs to 'PAYER' role
function isPayer(address account) public view returns (bool)
{
return hasRole(PAYER_ROLE, account);
}
function _setGameTemplate(uint16 gameTemplateId, uint16 maxPlayersCount, uint256 fee, address token, uint256 prizeFund, address prizeFundToken) private {
require(validTokens[token].valid, "Invalid ERC20 token");
if(prizeFund > 0) {
require(validTokens[prizeFundToken].valid, "Invalid ERC20 prize fund token");
}
GameTemplate storage gameTemplate = gameTemplates[gameTemplateId];
gameTemplate.maxPlayersCount = maxPlayersCount;
gameTemplate.fee = fee;
gameTemplate.token = IERC20(token);
gameTemplate.prizeFund = prizeFund;
gameTemplate.prizeFundToken = IERC20(prizeFundToken);
}
}
Last updated