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