Solo Contract

The SoloChallenge contract, is a comprehensive smart contract for managing solo player challenges. It integrates Chainlink's VRF for randomness, utilizes OpenZeppelin's security features, and coordinates with ChallengeBase for roles and templates. Key functionalities include creating, joining, leaving, canceling, and paying out challenges, as well as handling commission and refunds. The contract also supports custom challenges and tournament brackets, offering a versatile platform for various gaming scenarios.

 // SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import './ChallengeBase.sol';
import './util/TimeLock.sol';

/// @title Manages Solo player gaming Challenges
/// @author Challenge.GG
/// @dev The SoloChallenge contract, is a comprehensive smart contract for managing solo player challenges. 
/// It integrates Chainlink's VRF for randomness, utilizes OpenZeppelin's security features, and coordinates with ChallengeBase for roles and templates. 
/// Key functionalities include creating, joining, leaving, canceling, and paying out challenges, as well as handling commission and refunds. 
/// The contract also supports custom challenges and tournament brackets, offering a versatile platform for various gaming scenarios.
contract SoloChallenge is Pausable, ReentrancyGuard, VRFConsumerBaseV2, ConfirmedOwner {
     using SafeERC20 for IERC20;

    ChallengeBase private _baseContract; // Address to base contract that handles roles, templates and other common data
    mapping (uint256 => Challenge) private _challenges; //challengeId => Challenge
    address private _splitContract; //Address to contract that handles split of commission. 
    address private _foundingWallet; //Address to wallet that sponsored payout will be drawn from
    bool public softPaused;  // If true no new challenges can be created but existing can be completed   
    mapping (IERC20 => uint256) public commission; //Current commission per token    
    TimeLock public timeLock;
    
    //ChainLink VRF variables
    uint64 public immutable vrfSubscriptionId; // Chainlink VRF subscription ID.
    VRFCoordinatorV2Interface vrfCoordinatorInf; // Chainlink VRF coordinator contract.
    mapping(uint256 => RequestStatus) public vrfRequests; // requestId --> requestStatus mapping.
    bytes32 public vrfKeyHash; // Chainlink VRF key hash. The gas lane to use, which specifies the maximum gas price to bump to. For Etherum 200, 500,1000 gwei
    uint16 public  vrfRequestConfirmations = 3; // Number of required Chainlink VRF node confirmations. Recommended value is 3-6
    uint32 public vrfCallbackGasLimit = 100000; // Depends on the number of requested values that you want sent to the fulfillRandomWords() function. 
                                                //Storing each word costs about 20,000 gas, so 100,000 is a safe default.
    // Chainlink VRF request status.
    struct RequestStatus {
        bool exists;
        bool fulfilled;
        uint256 challengeId;        
    }


    // Challenge data
    struct Challenge {
        ChallengeBase.ChallengeStatus status;
        mapping (address => Player) players;
        address[] playerAddresses;
        uint256 fee;
        IERC20 token;
        uint16 maxPlayersCount;
        uint16 minPlayersCount; //only for tournaments
        uint16 playersCount;

        uint256 prizeFund;      //Sponsored payout
        IERC20 prizeFundToken;  //Token to use for sponsored payout

        uint256 amountPaidOut;
        uint256 amountRefunded;
        uint256 comission;

        bool isCustom; //Flag to indicate custom (not template based) challenge
        
        uint256[] randomSeeds; //Random seeds used for tournament brackets
        
        uint16 index; //We save an index to make sure events can be retrieved in order
    }

    // Player data
    struct Player {
        ChallengeBase.PlayerStatus status;
        uint64 score;
        uint16 placement;
        
        uint256 amountPaidOut;
        uint256 amountRefunded;

        bool refunded;
        bool paidOut;

        uint16 index; //To keep track of order players joined, this is per challenge
    }

    //Used to return data from payout function event
    struct PlayerResult {
        address player;
        uint16 placement;
        uint256 payout;
        uint8 status;
    }

    //Used to return data from refund function event
    struct PlayerRefund {
        address player;
        bool refunded;
    }

     //Events

    //Challenges based on templates    
    event ChallengeJoined(uint256 challengeId, uint16 gameTemplateId, uint256 fee, IERC20 token, uint16 maxPlayersCount, address indexed participant, uint16 playersCount, bool created, bool started, uint16 eventIndex, uint256 date);  
    
    //Challenges based on templates or custom challenges
    event ChallengeLeft(uint256 challengeId, address indexed participant, uint16 eventIndex, uint256 date);  
    event ChallengePayout(uint256 challengeId, uint256 payout, IERC20 payoutToken, uint256 commision, IERC20 commissionToken, PlayerResult[] playerResults);
    event ChallengeCanceled(uint256 challengeId, uint8 status, PlayerRefund[] playerRefunds);
    event BatchRefund(uint256 challengeId, PlayerRefund[] playerRefunds);

    //Challenges related to custom challenges
    event ChallengeCreated(uint256 internalChallengeId, uint256 challengeId, uint256 date); 
    event ChallengeStarted(uint256 challengeId, uint256 date);
    
    //Only tournaments
    event BracketSeedsRequested(uint256 challengeId, uint256 vrfRequestId);
    event BracketSeedsCreated(uint256 challengeId, uint256[] randomSeeds);
 
    //Payout to split contract
    event TransferCommissionSucceeded(uint256 amount, IERC20 token);
     
    // Modifiers

    /// @dev Restricted to members of the admin role.
    modifier onlyAdmin()
    {
        require(_baseContract.isAdmin(msg.sender), "Restricted to admin");
        _;
    }  

    /// @dev Restricted to members of the Manager role.
    modifier onlyManager()
    {
        require(_baseContract.isManager(msg.sender), "Restricted to managers");
        _;
    }
    
    /// @dev Restricted to members of the Payer role.
    modifier onlyPayer()
    {
        require(_baseContract.isPayer(msg.sender), "Restricted to payers");
        _;
    }

    /// @dev Constructor
    /// @param multisigAdmin Multisig admin of contract
    /// @param baseContract Address to base contract that handles roles, templates and other common data
    /// @param splitContract Address to contract that handles split of commission
    /// @param subscriptionId Chainlink VRF subscription ID.
    /// @param coordinator Chainlink VRF coordinator contract.
    /// @param keyHash Chainlink VRF key hash.
    constructor(address multisigAdmin, address baseContract, address splitContract, address timeLockContract, address foundingWallet, uint64 subscriptionId, address coordinator, bytes32 keyHash) 
        VRFConsumerBaseV2(coordinator) 
        ConfirmedOwner(multisigAdmin){
        
        require(baseContract != address(0), "Base contract address is the zero address");
        require(splitContract != address(0), "Split contract address is the zero address");
        require(foundingWallet != address(0), "Founding wallet is the zero address");
        require(coordinator != address(0), "Coordinator address is the zero address");
        require(timeLockContract != address(0), "TimeLock address is the zero address");

        _baseContract = ChallengeBase(baseContract);     
        timeLock = TimeLock(timeLockContract); 
        _splitContract = splitContract;
        _foundingWallet = foundingWallet;

        vrfSubscriptionId = subscriptionId;
        vrfKeyHash = keyHash;
        vrfCoordinatorInf = VRFCoordinatorV2Interface(
            coordinator
        );

        softPaused=false;
    }

    receive() external payable {}
    
    /// @dev Sets the split contract address
    /// Split contract is setup on ThirdWeb (https://thirdweb.com/thirdweb.eth/Split)
    /// We need to be able to change split contract since split should be sent to staking contracts and they are not deployed yet
    /// For extra security all changes to split contract is done through a timelock contract.
    /// @param splitContract Address to contract that handles split of commission
    /// @param actionId Action id generated by TimeLock contract
    function setSplitContract(address splitContract,bytes32 actionId) external onlyAdmin {

        require(timeLock.getExecutionTime(actionId) != 0, "Action not scheduled");
        require(block.timestamp >= timeLock.getExecutionTime(actionId), "Action not ready");
        require(actionId == timeLock.generateActionId("setSplitContract", abi.encode(splitContract), address(this)), "Invalid actionId");

        require(splitContract != address(0), "Split contract address is the zero address");
        _splitContract = splitContract;
        // Clear the scheduled action
        timeLock.clearAction(actionId);
    }

    /// @dev Sets the base contract address
    /// @param baseContract Address to base contract that handles roles, templates and other common data
    function setBaseContract(address baseContract) external onlyAdmin {
         require(baseContract != address(0), "Base contract address is the zero address");
        _baseContract = ChallengeBase(baseContract);
    }

    /// @dev Sets the founding wallet address
    /// Make sure this contract has enought allowance to transfer tokens from founding wallet
    /// @param foundingWallet Address to founding wallet
    function setFoundingWallet(address foundingWallet) external onlyAdmin {
        require(foundingWallet != address(0), "Founding wallet is the zero address");
        _foundingWallet = foundingWallet;
    }

    /// @dev Distributes all commission of the specified type to the split contract
    /// This would be usefull if we need to do a contract upgrade and we want to distribute all commission that are below threshold before we do the upgrade 
    /// @param token ERC20 token to distribute
    function distributeTokensToSplitContract(address token) external nonReentrant onlyAdmin {
       _transferCommission(IERC20(token));
    }

    /// @dev Withdraws ETH sent to contract by mistake
    /// @param to Address to send ETH to
    /// @param amount Amount to send
    function withdrawETH(address payable to, uint256 amount) external nonReentrant onlyAdmin { 
        require(to != address(0), "To wallet is the zero address");
        require(address(this).balance >= amount, "Insufficient ETH balance");
        Address.sendValue(to, amount);
    }

    /// @dev Pause contract
    function pause() external onlyManager {
        _pause();
    }

    /// @dev Unpause contract
    function unpause() external onlyManager {
        _unpause();
    }

    /// @dev Soft pause contract. No new challenges can be created but existing can be completed
    function softPause() external onlyManager {
        softPaused=true;
    }

    /// @dev Unsoft pause contract
    function unSoftPause() external onlyManager {
        softPaused=false;
    }

    /// @dev Sets the Chainlink VRF coordinator contract address.
    /// @param coordinator Chainlink VRF coordinator contract.
    function setVrfCoordinator(address coordinator) external onlyAdmin {
        require(coordinator != address(0), "Coordinator address is the zero address");
        vrfCoordinatorInf = VRFCoordinatorV2Interface(
            coordinator
        );
    }

    /// @dev Sets the Chainlink VRF key hash.
    /// @param keyHash Chainlink VRF key hash.
    function setVrfKeyHash(bytes32 keyHash) external onlyManager {
        vrfKeyHash = keyHash;
    }

    /// @dev Sets the Chainlink VRF request confirmations.
    /// @param requestConfirmations Chainlink VRF request confirmations.
    function setVrfRequestConfirmations(uint16 requestConfirmations) external onlyManager {
        vrfRequestConfirmations = requestConfirmations;
    }

    /// @dev Sets the Chainlink VRF callback gas limit.
    /// @param callbackGasLimit Chainlink VRF callback gas limit.
    function setVrfCallbackGasLimit(uint32 callbackGasLimit) external onlyManager {
        vrfCallbackGasLimit = callbackGasLimit;
    }
    
    /// @dev Creates a custom challenge
    /// A custom challenge is not created from a template.
    /// A custom challenge is normally started at a set time by startChallenge or createBrackets(if tournament)
    /// @param internalChallengeId Internal challenge id used by backend system
    /// @param fee Fee to join challenge    
    /// @param token ERC20 token to use for challenge
    /// @param prizeFund Sponsored payout for challenge
    /// @param prizeFundToken ERC20 token to use for sponsored payout
    /// @param maxPlayersCount Max number of players in challenge    
    function createChallenge(uint256 internalChallengeId, uint256 fee,  address token, uint256 prizeFund, address prizeFundToken, uint16 minPlayersCount, uint16 maxPlayersCount) external nonReentrant onlyManager {     
        require(_baseContract.isValidToken(token),"Invalid ERC20 token"); 
        require(maxPlayersCount>1,"Max players must be greater than 1");
        require(minPlayersCount>1 && minPlayersCount<=maxPlayersCount,"Min players must be greater than 1 and less or equal to max players");
        require(fee > 0 || prizeFund> 0,"Fee or prize fund must be greater than 0");
        require(prizeFund>=0,"Prize fund must be greater than or equal to 0");

        uint256 challengeId = _baseContract.incrementAndGetChallengeId();
        Challenge storage challenge = _challenges[challengeId];
        challenge.fee=fee;
        challenge.prizeFund=prizeFund;
        challenge.prizeFundToken=IERC20(prizeFundToken);
        challenge.token=IERC20(token);
        challenge.minPlayersCount=minPlayersCount;
        challenge.maxPlayersCount=maxPlayersCount;
        challenge.status=ChallengeBase.ChallengeStatus.Created;
        challenge.isCustom=true;
        
        if(prizeFund>0){
            require(_baseContract.isValidToken(prizeFundToken),"Invalid ERC20 token"); 
            _transferFromFoundingWallet(prizeFund, IERC20(prizeFundToken), address(this));
        }

        emit ChallengeCreated(internalChallengeId, challengeId, block.timestamp);

    }

    /// @dev Starts a custom challenge
    /// A custom challenge is not created from a template, needs to be created by createChallenge
    /// A custom challenge is normally started at a set time by this method or callback fulfillRandomWords from calling requestBracketSeeds(if tournament)
    /// @param challengeId Challenge id
    function startChallenge(uint256 challengeId) external nonReentrant onlyManager {     
        Challenge storage challenge = _challenges[challengeId];
        require(challenge.status==ChallengeBase.ChallengeStatus.Created,"Incorrect Challenge status");
        require(challenge.playersCount >= challenge.minPlayersCount, "Number of players must be >= min players");
        require(challenge.isCustom,"Challenge is not custom");
        challenge.status=ChallengeBase.ChallengeStatus.Running;        
        emit ChallengeStarted(challengeId, block.timestamp);
    }

    /// @dev Request bracket seeds for challenge tournament. 
    /// This is used for tournaments. Each player is randomly assigned to a bracket. To generate brackets we need a verifiable random seed.
    /// This can be done for both custom and template based challenges.
    /// Custom challenges will have the status Created and Challenges from templates will have the status Running when this is called.
    /// @param challengeId Challenge id
    /// @param numSeeds Number of random seeds to generate
    function requestBracketSeeds(uint256 challengeId, uint32  numSeeds) external nonReentrant onlyManager {
        require(numSeeds > 0, "Number of seeds must be greater than zero.");
        Challenge storage challenge = _challenges[challengeId];
        require(challenge.status==ChallengeBase.ChallengeStatus.Created 
                || challenge.status==ChallengeBase.ChallengeStatus.Running,"Incorrect Challenge status");
        require(challenge.playersCount >= challenge.minPlayersCount, "Number of players must be >= min players");   
        require(challenge.randomSeeds.length==0,"Brackets already created");

        // Will revert if subscription is not set and funded.
        uint256 requestId = vrfCoordinatorInf.requestRandomWords(
            vrfKeyHash,
            vrfSubscriptionId,
            vrfRequestConfirmations,
            vrfCallbackGasLimit,
            numSeeds
        );
        vrfRequests[requestId] = RequestStatus({
            exists: true,
            fulfilled: false,
            challengeId: challengeId
        });

       
        emit BracketSeedsRequested(challengeId, requestId);
    }

    /// @dev Batch refund. For challenges with  many players we refund in batches if needed.
    /// @param challengeId Challenge id
    /// @param playerAddresses Addresses of players to refund
    function refundBatch(uint256 challengeId, address[] memory playerAddresses) external nonReentrant onlyPayer {
        Challenge storage challenge = _challenges[challengeId];
        require(challenge.status == ChallengeBase.ChallengeStatus.Canceled || challenge.status == ChallengeBase.ChallengeStatus.Error, "Challenge not in refundable state");
        require(challenge.fee>0,"Challenge does not have a fee");

        PlayerRefund[] memory playerRefunds = new PlayerRefund[](playerAddresses.length);
        for (uint i = 0; i < playerAddresses.length; i++) {
            playerRefunds[i] = PlayerRefund({
                    player: playerAddresses[i],
                    refunded: false
            });                
            playerRefunds[i].refunded=_processRefund(challenge, playerAddresses[i]);  
        }

        emit BatchRefund(challengeId, playerRefunds);
    }

    /// @dev Does a payout to winners in challenge based on their score/placement. Scores is a ordered list. Amount payed out is based on payout distribution and fee.
    /// @param challengeId Challenge id
    /// @param winners Addresses of players to payout
    /// @param scores Scores of players to payout
    function payout(uint256 challengeId, address[] memory winners, uint64[] memory scores) external nonReentrant onlyPayer {
        Challenge storage challenge = _challenges[challengeId];
        require(challenge.status==ChallengeBase.ChallengeStatus.Running,"Challenge is not running");

        bool isSponsoredPayout = challenge.prizeFund>0;
        IERC20 prizeToken = isSponsoredPayout ? challenge.prizeFundToken : challenge.token;

        uint256 _possiblePayout= isSponsoredPayout ? challenge.prizeFund : (challenge.fee*challenge.playersCount);
        uint256 _actualPayout=0;

        (,,uint8[] memory payoutDistribution,)  = _baseContract.getPayoutDistribution(challenge.playersCount,isSponsoredPayout);

        require(payoutDistribution.length>0,"Distribuition not set");

        //To send event
        // Array of structs to hold player details for the event
        PlayerResult[] memory playerReults = new PlayerResult[](winners.length);        
        
        uint16 placement=1;
        for (uint16 i = 0; i < winners.length; i++) {
             Player storage player = challenge.players[winners[i]];    
             
             if(player.status==ChallengeBase.PlayerStatus.Participating){
                 
                  player.status=ChallengeBase.PlayerStatus.Completed;
                  player.score=scores[i];
                  player.placement=placement;
                  
                  if(payoutDistribution.length>=placement){
                    player.amountPaidOut=(payoutDistribution[placement-1]*_possiblePayout)/100;

                    if(player.amountPaidOut>0){               
                        player.paidOut=true;
                        _actualPayout = _actualPayout+player.amountPaidOut;
                        prizeToken.safeTransfer(winners[i], player.amountPaidOut);                 
                    }
                  }
                
                  placement++;
             }
             else{
                    player.status=ChallengeBase.PlayerStatus.Failed;
             }

            playerReults[i] = PlayerResult({
                player: winners[i],
                placement: player.placement,
                payout: player.amountPaidOut,
                status: uint8(player.status)
            });
        }

        challenge.amountPaidOut=_actualPayout;
        challenge.comission= isSponsoredPayout ? (challenge.fee*challenge.playersCount) : _possiblePayout-_actualPayout;
        challenge.status=ChallengeBase.ChallengeStatus.Finished;

        // Handle commission and send it to split contract when amount exceeds threshold
        
        commission[challenge.token] += challenge.comission;    
        
     
        if(commission[challenge.token] >= _baseContract.getValidTokenThreshold(address(challenge.token))) {        
           _transferCommission(challenge.token); // Call to the private function to transfer commission           
        }

        emit ChallengePayout(challengeId, challenge.amountPaidOut, prizeToken, challenge.comission, challenge.token, playerReults);                
    }

   

    /// @dev Cancels a challenge and refunds up to maxrefunds. If more players then players will be refunded in batches.
    /// @param challengeId Challenge id
    /// @param status Status to set challenge to
    function cancel(uint256 challengeId, uint8 status, uint16 maxRefunds) external nonReentrant onlyPayer  {
        Challenge storage challenge = _challenges[challengeId];
        require(challenge.status==ChallengeBase.ChallengeStatus.Running || challenge.status==ChallengeBase.ChallengeStatus.Created,"Challenge is not running");

        // Array of structs to hold player details for the event
        PlayerRefund[] memory playerRefunds;
     
        if(challenge.fee>0 && challenge.playerAddresses.length<=maxRefunds){
            
            playerRefunds = new PlayerRefund[](challenge.playerAddresses.length);
            for (uint16 i = 0; i < challenge.playerAddresses.length; i++) {
                
                playerRefunds[i] = PlayerRefund({
                    player: challenge.playerAddresses[i],
                    refunded: false
                });                
                playerRefunds[i].refunded=_processRefund(challenge, challenge.playerAddresses[i]);              
            }
        }
        else{
            playerRefunds = new PlayerRefund[](0);
        }      
        
        if(status==uint8(ChallengeBase.ChallengeStatus.Error)){
            challenge.status=ChallengeBase.ChallengeStatus.Error;
        }
        else{
            challenge.status=ChallengeBase.ChallengeStatus.Canceled;
        }

        if(challenge.prizeFund>0){
            uint256 amountToRefund = challenge.prizeFund;
            challenge.prizeFund=0;     
            challenge.prizeFundToken.safeTransfer(_foundingWallet, amountToRefund);                   
        }

        emit ChallengeCanceled(challengeId, uint8(challenge.status), playerRefunds);

    }     
   
    /// @dev Called by players to join a challenge of the specified template type. 
    /// If challenge does not exist or is full a new one is created.
    /// Full challenges will be started
    /// Player must not be blocked or template paused.    
    /// Fee is withdrawn from player and stored in contract until challenge is completed or canceled. Then payout or refund is done.
    /// @param gameTemplateId Id of game template to join
    function joinChallenge(uint16 gameTemplateId ) external nonReentrant whenNotPaused { 
           require(!_baseContract.isBlocked(msg.sender),"User is blocked");          
           
           ChallengeBase.GameTemplate memory gameTemplate = _baseContract.getGameTemplate(gameTemplateId);           
           require(gameTemplate.maxPlayersCount!=0,"Unknown game");
           require(!gameTemplate.paused,"This challenge type is currently not available");

           bool created=false;
           bool started=false;
           
           uint256 currentChallengeId = _baseContract.getCurrentId(gameTemplateId); 

           //If this is an exising challenge it could have been canceled or in error state, in that case we need to create a new challenge.
           if(currentChallengeId==0 
                || _challenges[currentChallengeId].status==ChallengeBase.ChallengeStatus.Canceled
                || _challenges[currentChallengeId].status==ChallengeBase.ChallengeStatus.Error){

               //Check if we allow new game, could be that we disable this to allow people to finish already created
               require(!gameTemplate.softPaused,"This challenge type is currently disabled");   
               //Created challenges can be compled but no new ones can be created
               require(!softPaused,"Joining a challenge is currently paused");   

               (currentChallengeId,created) = _createChallenge(gameTemplateId, gameTemplate);      
              
           }

           Challenge storage challenge = _challenges[currentChallengeId];                                   
          
           // Call the private function to join the challenge
           (started) = _joinChallenge(challenge,gameTemplateId,true);          

           emit ChallengeJoined(currentChallengeId, gameTemplateId, challenge.fee, challenge.token, challenge.maxPlayersCount, msg.sender, challenge.playersCount, created, started, challenge.index, block.timestamp);  
           //We increase event index after event is emitted so create have index 0
           challenge.index++; 
    }

    /// @dev Join a custom challenge. 
    /// A custom challenge is not created from a template.
    /// A custom challenge is normally started at a set time by startChallenge or createBrackets(if tournament)
    /// @param challengeId Id of challenge to join
    function joinChallengeById(uint256 challengeId) external nonReentrant whenNotPaused {
        require(!_baseContract.isBlocked(msg.sender),"User is blocked"); 

        Challenge storage challenge = _challenges[challengeId];
        require(challenge.status==ChallengeBase.ChallengeStatus.Created,"Challenge does not exist");
        require(challenge.isCustom,"Challenge is not custom");
       
        bool started = false;        
        // Call the private function to join the challenge
        (started) = _joinChallenge(challenge,0,false);
        
        //We increase event index before event is emitted since create is in separate function
        challenge.index++; 
        emit ChallengeJoined(challengeId, 0, challenge.fee, challenge.token, challenge.maxPlayersCount, msg.sender, challenge.playersCount, false, started, challenge.index, block.timestamp);  
    }    

    /// @dev Called by players to leave a challenge.
    /// @dev Players can only leave if not challenge is started.
    /// @dev Player will be refunded if allowed to leave.
    /// @param challengeId Id of challenge to leave
    function leaveChallenge(uint256 challengeId) external nonReentrant whenNotPaused {
        Challenge storage challenge = _challenges[challengeId];
        require(challenge.status==ChallengeBase.ChallengeStatus.Created,"Challenge is started or does not exist");
        
        Player storage player = challenge.players[msg.sender];
        require(player.status==ChallengeBase.PlayerStatus.Participating,"Not participating in challenge");

        challenge.playersCount--;
        //Remove from keys. Will remove element but will not keep order. No need now to handle address(0)
        for(uint16 i=0; i<challenge.playerAddresses.length;i++){
              if(challenge.playerAddresses[i]==msg.sender){
                  challenge.playerAddresses[i]=challenge.playerAddresses[challenge.playerAddresses.length-1];
                  challenge.playerAddresses.pop();
              }  
        }

        player.status=ChallengeBase.PlayerStatus.NotParticipating;
        player.refunded=true;
        player.amountRefunded=challenge.fee;

        if(challenge.fee>0){
            challenge.token.safeTransfer(msg.sender, challenge.fee);
        }
        
        emit ChallengeLeft(challengeId, msg.sender, challenge.index, block.timestamp);  

        challenge.index++; 
    }  
    
    /// @dev Returns balance of specified token
    /// @param token Token to check balance of
    /// @return balance Balance of token
    function tokenBalance(address token) external view returns (uint256 balance) {
      return IERC20(token).balanceOf(address(this));
    }   

    /// @dev Returns some of the challenge data (not players)
    /// @param challengeId Challenge id
    /// @return status  Challenge status
    /// @return playersCount Players in challenge 
    /// @return maxPlayersCount Max players in challenge
    /// @return minPlayersCount Min players in challenge
    /// @return fee Fee to join challenge
    /// @return token Token used for challenge
    /// @return prizeFund Sponsored payout for challenge
    /// @return prizeFundToken Token used for sponsored payout
    /// @return amountPaidOut Amount paid out to players 
    /// @return amountRefunded Amount refunded to players
    /// @return comission Commission earned by challenge
    /// @return isCustom If challenge is custom
    function getChallenge(uint256 challengeId) external view returns (uint8 status, uint16 playersCount,uint16 maxPlayersCount, uint16 minPlayersCount, uint256 fee, IERC20 token, uint256 prizeFund, IERC20 prizeFundToken, uint256 amountPaidOut, uint256 amountRefunded, uint256 comission, bool isCustom){
       Challenge storage challenge = _challenges[challengeId];
       return (uint8(challenge.status), challenge.playersCount, challenge.maxPlayersCount, challenge.minPlayersCount, challenge.fee, challenge.token, challenge.prizeFund, challenge.prizeFundToken, challenge.amountPaidOut, challenge.amountRefunded, challenge.comission, challenge.isCustom);
    }

    /// @dev Returns a specific challenge seed used to create tournament brackets
    /// Only applicable for tournaments
    /// @param challengeId Challenge id
    /// @param randomSeeds Seeds created by Chainlink VRF
    function  getChallengeSeeds(uint256 challengeId) external view returns (uint256[] memory randomSeeds){
        Challenge storage challenge = _challenges[challengeId];
        return challenge.randomSeeds;
    }

    /// @dev Returns specified player data
    /// @param challengeId Challenge id 
    /// @param participant Address of player 
    /// @return status Player status 
    /// @return score Player score
    /// @return placement Player placement
    /// @return amountPaidOut Amount paid out to player 
    /// @return amountRefunded Amount refunded to player 
    /// @return paidOut If player is paid out 
    /// @return refunded If player is refunded
    /// @return index Index of player in challenge, if players leave there will be a gap in index
    function getParticipant(uint256 challengeId, address participant) external view returns(uint8 status, uint64 score, uint16 placement, uint256 amountPaidOut, uint256 amountRefunded,  bool paidOut, bool refunded, uint16 index ) {
          Player memory player = _challenges[challengeId].players[participant];
          return (uint8(player.status), player.score, player.placement, player.amountPaidOut, player.amountRefunded, player.paidOut, player.refunded, player.index);
    }

    /// @dev Returns the split contract address
    function getSplitContract() external view returns(address) {
        return _splitContract;
    }

    /// @dev Callback function used by the Chainlink VRF Coordinator to return the random number to the contract.
    /// This function is invoked by the VRF Coordinator once the requested random number is ready.
    /// It must only be callable by the VRF Coordinator to ensure the integrity of the randomness provided.
    /// The function processes the random number(s) and typically triggers contract logic that depends on randomness.
    /// 
    /// @param requestId The unique identifier of the random number request, correlating to the original request made.
    /// @param randomSeeds An array of random numbers generated and returned by the VRF Coordinator. Each element
    /// in the array is a random number. The number of elements depends on the request parameters when randomness was requested.
    function fulfillRandomWords(uint256 requestId, uint256[] memory randomSeeds) internal override {
        require(vrfRequests[requestId].exists, "request not found");
        vrfRequests[requestId].fulfilled = true;

        Challenge storage challenge = _challenges[vrfRequests[requestId].challengeId];
        challenge.status=ChallengeBase.ChallengeStatus.Running;
        challenge.randomSeeds=randomSeeds;

        emit BracketSeedsCreated(vrfRequests[requestId].challengeId, randomSeeds);
    }

    /// @dev Helper function to join a challenge based on a template or custom challenge
    /// @param challenge Challenge to join
    /// @param gameTemplateId Id of game template to join, only if template based otherwise 0
    /// @param canStart If challenge is ready to start, only if template based otherwise false
    /// @return started If challenge was started
    function _joinChallenge(Challenge storage challenge, uint16 gameTemplateId, bool canStart) private returns (bool) {
        Player storage player = challenge.players[msg.sender];

        require(player.status==ChallengeBase.PlayerStatus.NotParticipating,"Player is already participating");
        require(challenge.playersCount<challenge.maxPlayersCount,"Challenge is already full");
        challenge.playersCount++;
        challenge.playerAddresses.push(msg.sender); // Add to array of keys
        player.status=ChallengeBase.PlayerStatus.Participating;
        player.refunded=false;
        player.amountRefunded=0;
        player.index=challenge.index; //Set current index to keep track of order players joined

        bool started = false;
        if(canStart && challenge.playersCount==challenge.maxPlayersCount){
            challenge.status=ChallengeBase.ChallengeStatus.Running;                    
            _baseContract.resetCurrentId(gameTemplateId);
            started=true;
        }
        if(challenge.fee>0){
            _withdrawFee(challenge.fee,challenge.token);
        }          

        return (started);
    }

    /// @dev Helper function to create a challenge from a game template
    /// @param gameTemplateId Id of game template to join
    /// @param gameTemplate Game template to base challenge on
    /// @return newChallengeId Id of new challenge
    /// @return created If challenge was created
    function _createChallenge(uint16 gameTemplateId, ChallengeBase.GameTemplate memory gameTemplate) private returns (uint256, bool) {           
        uint256 newChallengeId = _baseContract.incrementAndSetCurrentId(gameTemplateId);

        Challenge storage challenge = _challenges[newChallengeId];
        challenge.fee=gameTemplate.fee;
        challenge.token=gameTemplate.token;
        challenge.prizeFund=gameTemplate.prizeFund;
        challenge.prizeFundToken=gameTemplate.prizeFundToken;
        challenge.minPlayersCount=gameTemplate.maxPlayersCount;
        challenge.maxPlayersCount=gameTemplate.maxPlayersCount;
        challenge.status=ChallengeBase.ChallengeStatus.Created;

        if(challenge.prizeFund>0){
            _transferFromFoundingWallet(challenge.prizeFund, challenge.prizeFundToken, address(this));
        }

        return (newChallengeId,true);
        
    }

    /// @dev Helper function to withdraw fee from player
    /// @param fee Fee to withdraw
    /// @param token Token to withdraw    
    function _withdrawFee(uint256 fee, IERC20 token) private {
        require(token.allowance(msg.sender, address(this)) >= fee, "Check allowance");
        require(token.balanceOf(msg.sender)>=fee, "Not enough coins");
        token.safeTransferFrom(msg.sender, address(this), fee);
    }

    /// @dev Helper function to withdraw fee from founding wallet
    /// @param fee Fee to withdraw
    /// @param token Token to withdraw
    function _transferFromFoundingWallet(uint256 fee, IERC20 token, address to) private {
        require(token.allowance(_foundingWallet, address(this)) >= fee, "Check founding wallet allowance");
        require(token.balanceOf(_foundingWallet)>=fee, "Not enough coins in founding wallet");
        token.safeTransferFrom(_foundingWallet, to, fee);
    }

     // @dev Function to process refund for a single player
    // @param challenge Challenge
    // @param playerAddress Address of player to refund
    // @return refunded If player was refunded
    function _processRefund(Challenge storage challenge, address playerAddress) private returns (bool refunded) {
        Player storage player = challenge.players[playerAddress];
        if (player.status == ChallengeBase.PlayerStatus.Participating && !player.refunded) {
            player.refunded = true;
            player.amountRefunded = challenge.fee;
            challenge.amountRefunded += challenge.fee;
            challenge.token.safeTransfer(playerAddress, challenge.fee); 
            return true;
           
        }
        return false;
    }

    /// @dev Helper function to transfer commission to split contract
    /// @param token Token to transfer
    function _transferCommission(IERC20 token) private {
        uint256 amountToTransfer = commission[token];
        require(amountToTransfer > 0, "No commission to distribute");
        uint256 balance = token.balanceOf(address(this));
        require(balance >= amountToTransfer, "Not enough balance to transfer commission");

        commission[token] = 0; // Reset the totalCommission
        token.safeTransfer(_splitContract, amountToTransfer); // Transfer commission
        emit TransferCommissionSucceeded(amountToTransfer, token); // Emit event
    }

}

Last updated