ETH Price: $3,337.04 (-1.12%)
Gas: 9 Gwei

Contract

0x22E3097647349B91766fe1085f1E1F05288fD201
 

Overview

ETH Balance

0 ETH

Eth Value

$0.00

Multichain Info

No addresses found
Transaction Hash
Method
Block
From
To
0x60c06040184057992023-10-22 12:20:23281 days ago1697977223IN
 Contract Creation
0 ETH0.006151976.45988324

View more zero value Internal Transactions in Advanced View mode

Advanced mode:
Loading...
Loading

Similar Match Source Code
This contract matches the deployed Bytecode of the Source Code for Contract 0x25c1E3FC...81F87dB49
The constructor portion of the code might be different and could alter the actual behaviour of the contract

Contract Name:
BiasCalculator

Compiler Version
v0.8.16+commit.07a7930e

Optimization Enabled:
Yes with 200 runs

Other Settings:
default evmVersion
File 1 of 17 : BiasCalculator.sol
//██████╗  █████╗ ██╗      █████╗ ██████╗ ██╗███╗   ██╗
//██╔══██╗██╔══██╗██║     ██╔══██╗██╔══██╗██║████╗  ██║
//██████╔╝███████║██║     ███████║██║  ██║██║██╔██╗ ██║
//██╔═══╝ ██╔══██║██║     ██╔══██║██║  ██║██║██║╚██╗██║
//██║     ██║  ██║███████╗██║  ██║██████╔╝██║██║ ╚████║
//╚═╝     ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═════╝ ╚═╝╚═╝  ╚═══╝
 

//SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.16;

import "../interfaces/IGaugeController.sol";
import "../libraries/QuestDataTypes.sol";
import "../QuestBoard.sol";
import "../libraries/Errors.sol";

/** @title Quest Bias Calculator Module  */
/// @author Paladin
/*
    Contract to calculate the bias of a given Quest based on the Gauge & Quest parameters
*/
contract BiasCalculator {

    /** @notice Seconds in a Week */
    uint256 private constant WEEK = 604800;
    /** @notice Max VoterList size */
    uint256 private constant MAX_VOTERLIST_SIZE = 10;

    /** @notice Address of the Curve Gauge Controller */
    address public immutable GAUGE_CONTROLLER;

    /** @notice Address of the QuestBoard contract */
    address public immutable questBoard;

    /** @notice Mapping of voter list (blacklist or whitelist) for each Quest */
    // ID => VoterList
    mapping(uint256 => address[]) private questVoterList;
    /** @notice Mapping of valid Quests */
    mapping(uint256 => bool) private validQuests;


    // Events

    /** @notice Event emitted when an address is added to a Quest voter list */
    event AddToVoterList(uint256 indexed questID, address indexed account);
    /** @notice Event emitted when an address is removed from a Quest voter list */
    event RemoveFromVoterList(uint256 indexed questID, address indexed account);


    // Modifier

    /** @notice Check the caller is the QuestBoard contract */
    modifier onlyBoard(){
        if(msg.sender != questBoard) revert Errors.CallerNotAllowed();
        _;
    }


    // Constructor
    constructor(address _gaugeController, address _questBoard) {
        if(_gaugeController == address(0) || _questBoard == address(0)) revert Errors.AddressZero();

        questBoard = _questBoard;
        GAUGE_CONTROLLER = _gaugeController;
    }

    /**
    * @notice Returns the current Period for the contract
    */
    function getCurrentPeriod() public view returns(uint256) {
        return (block.timestamp / WEEK) * WEEK;
    }

    /**
    * @notice Returns the voter list for a given Quest
    * @param questID ID of the Quest
    * @return address[] : vote list
    */
    function getQuestVoterList(uint256 questID) external view returns(address[] memory){
        return questVoterList[questID];
    }

    /**
    * @notice Returns the current reduced bias for a given gauge (based on a Quest's voter list)
    * @param questID ID of the Quest
    * @param gauge address of the gauge
    * @param questType Vote type of the Quest
    * @return uint256 : current reduced bias
    */
    function getCurrentReducedBias(uint256 questID, address gauge, QuestDataTypes.QuestVoteType questType) external view returns(uint256) {
        uint256 nextPeriod = getCurrentPeriod() + WEEK;

        return getReducedBias(nextPeriod, questID, gauge, questType);
    }

    /**
    * @notice Returns the reduced bias for a given gauge for a given period (based on a Quest's voter list)
    * @param period timestamp of the period
    * @param questID ID of the Quest
    * @param gauge address of the gauge
    * @param questType Vote type of the Quest
    * @return uint256 : current reduced bias
    */
    function getReducedBias(uint256 period, uint256 questID, address gauge, QuestDataTypes.QuestVoteType questType) public view returns(uint256) {
        address[] memory voterList = questVoterList[questID];

        IGaugeController gaugeController = IGaugeController(GAUGE_CONTROLLER);

        uint256 voterListSumBias;

        uint256 voterListLength = voterList.length;
            for(uint256 i; i < voterListLength;) {
                voterListSumBias += _getVoterBias(gauge, voterList[i], period);

                unchecked { i++; }
            }

        // For a WHITELIST type, simply return the sum of voters bias
        if(questType == QuestDataTypes.QuestVoteType.WHITELIST) return voterListSumBias;

        // Get the bias of the Gauge for the given period
        uint256 periodAdjustedBias = gaugeController.points_weight(gauge, period).bias;

        // If the Quest is a Blacklist, we need to remove the bias of the voters
        if(questType == QuestDataTypes.QuestVoteType.BLACKLIST) {
            periodAdjustedBias = voterListSumBias >= periodAdjustedBias ? 0 : periodAdjustedBias - voterListSumBias;
        }
        
        return periodAdjustedBias;
    }

    /**
    * @notice Returns the bias for a given voter for a given gauge, at a given period
    * @param gauge address of the gauge
    * @param voter address of the voter
    * @param period timestamp of the period
    * @return userBias (uint256) : voter bias
    */
    function _getVoterBias(address gauge, address voter, uint256 period) internal view returns(uint256 userBias) {
        IGaugeController gaugeController = IGaugeController(GAUGE_CONTROLLER);
        uint256 lastUserVote = gaugeController.last_user_vote(voter, gauge);
        IGaugeController.VotedSlope memory voteUserSlope = gaugeController.vote_user_slopes(voter, gauge);

        if(lastUserVote >= period) return 0;
        if(voteUserSlope.end <= period) return 0;
        if(voteUserSlope.slope == 0) return 0;

        userBias = voteUserSlope.slope * (voteUserSlope.end - period);
    }

    /**
    * @notice Adds a given address to a Quest's voter list
    * @dev Adds a given address to a Quest's voter list
    * @param questID ID of the Quest
    * @param account address of the voter
    */
    function _addToVoterList(uint256 questID, address account) internal {
        //We don't want to have 2x the same address in the list
        address[] memory _list = questVoterList[questID];
        uint256 length = _list.length;
        for(uint256 i; i < length;){
            if(_list[i] == account) revert Errors.AlreadyListed();
            unchecked {
                ++i;
            }
        }

        questVoterList[questID].push(account);

        emit AddToVoterList(questID, account);
    }

    /**
    * @notice Sets the initial voter list for a given Quest
    * @param questID ID of the Quest
    * @param accounts list of voters
    */
    function setQuestVoterList(uint256 questID, address[] calldata accounts) external onlyBoard {
        uint256 length = accounts.length;
        if(length > MAX_VOTERLIST_SIZE) revert Errors.MaxListSize();

        for(uint256 i; i < length;){
            if(accounts[i] == address(0)) revert Errors.AddressZero();

            _addToVoterList(questID, accounts[i]);

            unchecked {
                ++i;
            }
        }

        validQuests[questID] = true;
    }

    /**
    * @notice Adds a given list of addresses to a Quest's voter list
    * @param questID ID of the Quest
    * @param accounts list of voters
    */
    function addToVoterList(uint256 questID, address[] calldata accounts) external {
        uint256 length = accounts.length;
        if(length == 0) revert Errors.EmptyArray();
        if(!validQuests[questID]) revert Errors.InvalidQuestID();
        if(msg.sender != QuestBoard(questBoard).getQuestCreator(questID)) revert Errors.CallerNotAllowed();
        if(length + questVoterList[questID].length > MAX_VOTERLIST_SIZE) revert Errors.MaxListSize();


        for(uint256 i = 0; i < length;){
            if(accounts[i] == address(0)) revert Errors.AddressZero();

            _addToVoterList(questID, accounts[i]);

            unchecked {
                ++i;
            }
        }
    }

    /**
    * @notice Removes a given address from a Quest's voter list
    * @param questID ID of the Quest
    * @param account address of the voter
    */
    function removeFromVoterList(uint256 questID, address account) external {
        if(!validQuests[questID]) revert Errors.InvalidQuestID();
        if(msg.sender != QuestBoard(questBoard).getQuestCreator(questID)) revert Errors.CallerNotAllowed();
        if(account == address(0)) revert Errors.AddressZero();

        address[] memory _list = questVoterList[questID];
        uint256 length = _list.length;

        for(uint256 i; i < length;){
            if(_list[i] == account){
                if(i != length - 1){
                    questVoterList[questID][i] = _list[length - 1];
                }
                questVoterList[questID].pop();

                emit RemoveFromVoterList(questID, account);

                return;
            }

            unchecked {
                ++i;
            }
        }
    }

}

File 2 of 17 : IGauge.sol
// SPDX-License-Identifier: MIT

pragma solidity 0.8.16;

/**
 * @dev Interface made for the Curve's Gauge contract
 */
interface IGauge {

    struct Reward {
        address token;
        address distributor;
        uint256 period_finish;
        uint256 rate;
        uint256 last_update;
        uint256 integral;
    }

    function reward_data(address _reward_token) external view returns(Reward memory);

    function deposit_reward_token(address _reward_token, uint256 _amount) external;
    function set_reward_distributor(address _reward_token, address _distributor) external;
    function add_reward(address _reward_token, address _distributor) external;
    
}

File 3 of 17 : IGaugeController.sol
// SPDX-License-Identifier: MIT

pragma solidity 0.8.16;

/**
 * @dev Interface made for the Curve's GaugeController contract
 */
interface IGaugeController {

    struct VotedSlope {
        uint slope;
        uint power;
        uint end;
    }
    
    struct Point {
        uint bias;
        uint slope;
    }
    
    function vote_user_slopes(address, address) external view returns(VotedSlope memory);
    function last_user_vote(address, address) external view returns(uint);
    function points_weight(address, uint256) external view returns(Point memory);
    function checkpoint_gauge(address) external;
    function gauge_types(address _addr) external view returns(int128);
    
}

File 4 of 17 : IQuestBoard.sol
//SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.16;

import "../libraries/QuestDataTypes.sol";

/** @title Interface fo Quest Board V2  */
/// @author Paladin
interface IQuestBoard {

    // Structs

    /** @notice Struct for a Period of a Quest */
    struct QuestPeriod {
        // Total reward amount that can be distributed for that period
        uint256 rewardAmountPerPeriod;
        // Min Amount of reward for each vote (for 1 veToken)
        uint256 minRewardPerVote;
        // Max Amount of reward for each vote (for 1 veToken)
        uint256 maxRewardPerVote;
        // Min Target Bias for the Gauge
        uint256 minObjectiveVotes;
        // Max Target Bias for the Gauge
        uint256 maxObjectiveVotes;
        // Amount of reward to distribute, at period closing
        uint256 rewardAmountDistributed;
        // Timestamp of the Period start
        uint48 periodStart;
        // Current state of the Period
        QuestDataTypes.PeriodState currentState;
    }

    /** @notice Struct holding the parameters of the Quest common for all periods */
    struct Quest {
        // Address of the Quest creator (caller of createQuest() method)
        address creator;
        // Address of the ERC20 used for rewards
        address rewardToken;
        // Address of the target Gauge
        address gauge;
        // Total number of periods for the Quest
        uint48 duration;
        // Timestamp where the 1st QuestPeriod starts
        uint48 periodStart;
        // Total amount of rewards paid for this Quest
        // If changes were made to the parameters of this Quest, this will account
        // any added reward amounts
        uint256 totalRewardAmount;
        // Quest Types
        QuestTypes types;
    }

    /** @notice Struct with all the Quest types */
    struct QuestTypes {
        QuestDataTypes.QuestVoteType voteType;
        QuestDataTypes.QuestRewardsType rewardsType;
        QuestDataTypes.QuestCloseType closeType;
    }

    /** @notice Struct for the local variables in _createQuest() method */
    struct CreateVars {
        address creator;
        uint256 rewardPerPeriod;
        uint256 minObjective;
        uint256 startPeriod;
        uint256 periodIterator;
        uint256 maxObjective;
    }

    /** @notice Struct for the local variables in extendQuest() method */
    struct ExtendVars {
        uint256 lastPeriod;
        address gauge;
        address rewardToken;
        uint256 rewardPerPeriod;
        uint256 periodIterator;
        uint256 minObjective;
        uint256 maxObjective;
        uint256 minRewardPerVote;
        uint256 maxRewardPerVote;
    }

    /** @notice Struct for the local variables in updateQuestParameters() methods */
    struct UpdateVars {
        uint256 remainingDuration;
        uint256 currentPeriod;
        uint256 newRewardPerPeriod;
        uint256 newMaxObjective;
        uint256 newMinObjective;
        uint256 periodIterator;
        uint256 lastPeriod;
        address creator;
    }

    // Events

    /** @notice Event emitted when the Board is Initialized */
    event Init(address distributor, address biasCalculator);

    /** @notice Event emitted when a new Quest is created */
    event NewQuest(
        uint256 indexed questID,
        address indexed creator,
        address indexed gauge,
        address rewardToken,
        uint48 duration,
        uint256 startPeriod
    );

    /** @notice Event emitted when the Quest duration is extended */
    event ExtendQuestDuration(uint256 indexed questID, uint256 addedDuration, uint256 addedRewardAmount);

    /** @notice Event emitted when a Quest parameters are updated */
    event UpdateQuestParameters(
        uint256 indexed questID,
        uint256 indexed updatePeriod,
        uint256 newMinRewardPerVote,
        uint256 newMaxRewardPerVote,
        uint256 addedPeriodRewardAmount
    );

    /** @notice Event emitted when Quest creator withdraw undistributed rewards */
    event WithdrawUnusedRewards(uint256 indexed questID, address recipient, uint256 amount);

    /** @notice Event emitted when a Period is Closed */
    event PeriodClosed(uint256 indexed questID, uint256 indexed period);
    /** @notice Event emitted when a Quest Period rools over the undistributed rewards */
    event RewardsRollover(uint256 indexed questID, uint256 newRewardPeriod, uint256 newMinRewardPerVote, uint256 newMaxRewardPerVote);

    /** @notice Event emitted when a Period Bias is fixed */
    event PeriodBiasFixed(uint256 indexed questID, uint256 indexed period, uint256 newBias);

    /** @notice Event emitted when a new reward token is whitelisted */
    event WhitelistToken(address indexed token, uint256 minRewardPerVote);
    /** @notice Event emitted when a reward token parameter is updated */
    event UpdateRewardToken(address indexed token, uint256 newMinRewardPerVote);
    /** @notice Event emitted when the contract is killed */
    event Killed(uint256 killTime);
    /** @notice Event emitted when the contract is unkilled */
    event Unkilled(uint256 unkillTime);
    /** @notice Event emitted when the Quest creator withdraw all unused funds (if the contract was killed) */
    event EmergencyWithdraw(uint256 indexed questID, address recipient, uint256 amount);

    /** @notice Event emitted when a new manager is approved */
    event ApprovedManager(address indexed manager);
    /** @notice Event emitted when a manager is removed */
    event RemovedManager(address indexed manager);
    /** @notice Event emitted when the Chest address is updated */
    event ChestUpdated(address oldChest, address newChest);
    /** @notice Event emitted when a custom fee ratio is set for a given address */
    event SetCustomFeeRatio(address indexed creator, uint256 customFeeRatio);
    /** @notice Event emitted when the Distributor address is updated */
    event DistributorUpdated(address oldDistributor, address newDistributor);
    /** @notice Event emitted when the fee ratio is updated */
    event PlatformFeeRatioUpdated(uint256 oldFeeRatio, uint256 newFeeRatio);
    /** @notice Event emitted when the minimum objective of votes is updated */
    event MinObjectiveUpdated(uint256 oldMinObjective, uint256 newMinObjective);
    
}

File 5 of 17 : Errors.sol
pragma solidity 0.8.16;
//SPDX-License-Identifier: MIT

library Errors {

    // Common Errors
    error AddressZero();
    error NullAmount();
    error CallerNotAllowed();
    error IncorrectRewardToken();
    error SameAddress();
    error InequalArraySizes();
    error EmptyArray();
    error EmptyParameters();
    error AlreadyInitialized();
    error InvalidParameter();
    error CannotRecoverToken();
    error ForbiddenCall();

    error CannotBeOwner();
    error CallerNotPendingOwner();

    error Killed();
    error AlreadyKilled();
    error NotKilled();
    error KillDelayExpired();
    error KillDelayNotExpired();


    // Merkle Errors
    error MerkleRootNotUpdated();
    error AlreadyClaimed();
    error InvalidProof();
    error EmptyMerkleRoot();
    error IncorrectRewardAmount();
    error MerkleRootFrozen();
    error NotFrozen();
    error AlreadyFrozen();


    // Quest Errors
    error CallerNotQuestBoard();
    error IncorrectQuestID();
    error IncorrectPeriod();
    error TokenNotWhitelisted();
    error QuestAlreadyListed();
    error QuestNotListed();
    error PeriodAlreadyUpdated();
    error PeriodNotClosed();
    error PeriodStillActive();
    error PeriodNotListed();
    error EmptyQuest();
    error EmptyPeriod();
    error ExpiredQuest();
    error QuestNotStarted();

    error NotInitialized();
    error NoDistributorSet();
    error DisitributorFail();
    error InvalidGauge();
    error InvalidQuestID();
    error InvalidPeriod();
    error ObjectiveTooLow();
    error NewObjectiveTooLow();
    error RewardPerVoteTooLow();
    error MinValueOverMaxValue();
    error IncorrectDuration();
    error IncorrectAddDuration();
    error IncorrectTotalRewardAmount();
    error IncorrectAddedRewardAmount();
    error IncorrectFeeAmount();
    error InvalidQuestType();
    error QuestTypesIncompatible();
    error CalletNotQuestCreator();
    error LowerRewardPerVote();
    error LowerObjective();
    error CreatorNotAllowed();
    error AlreadyListed();
    error NotListed();
    error MaxListSize();
    error BoardIsNotAllowedDistributor();


    //Math
    error NumberExceed48Bits();

}

File 6 of 17 : QuestDataTypes.sol
//SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.16;

/** @title Data Types fo Quest Board V2  */
/// @author Paladin
library QuestDataTypes {

    // Enums

    /** @notice State of each Period for each Quest */
    enum PeriodState { ZERO, ACTIVE, CLOSED, DISTRIBUTED }
    // All Periods are ACTIVE at creation since the voters from past periods are also accounted for the future period

    /** @notice Types of Vote logic for Quests */
    enum QuestVoteType { NORMAL, BLACKLIST, WHITELIST }
    // NORMAL: basic vote logic
    // BLACKLIST: remove the blacklisted voters bias from the gauge biases
    // WHITELIST: only sum up the whitelisted voters biases

    /** @notice Types of Rewards logic for Quests */
    enum QuestRewardsType { FIXED, RANGE }
    // FIXED: reward per vote is fixed
    // RANGE: reward per vote is a range between min and max, based on the Quest completion between min objective and max objective

    /** @notice Types of logic for undistributed rewards when closing Quest periods */
    enum QuestCloseType { NORMAL, ROLLOVER, DISTRIBUTE }
    // NORMAL: undistributed rewards are available to be withdrawn by the creator
    // ROLLOVER: undistributed rewards are added to the next period, increasing the reward/vote parameter
    // DISTRIBUTE: undistributed rewards are sent to the gauge for direct distribution

}

File 7 of 17 : MultiMerkleDistributor.sol
//██████╗  █████╗ ██╗      █████╗ ██████╗ ██╗███╗   ██╗
//██╔══██╗██╔══██╗██║     ██╔══██╗██╔══██╗██║████╗  ██║
//██████╔╝███████║██║     ███████║██║  ██║██║██╔██╗ ██║
//██╔═══╝ ██╔══██║██║     ██╔══██║██║  ██║██║██║╚██╗██║
//██║     ██║  ██║███████╗██║  ██║██████╔╝██║██║ ╚████║
//╚═╝     ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═════╝ ╚═╝╚═╝  ╚═══╝
 

//SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.16;

import "./oz/interfaces/IERC20.sol";
import "./oz/libraries/SafeERC20.sol";
import "./oz/utils/MerkleProof.sol";
import "./utils/Owner.sol";
import "./oz/utils/ReentrancyGuard.sol";
import "./libraries/Errors.sol";

/** @title Quest Multi Merkle Distributor  */
/// @author Paladin
/*
    Contract holds ERC20 rewards from Quests
    Can handle multiple MerkleRoots
*/

contract MultiMerkleDistributor is Owner, ReentrancyGuard {
    using SafeERC20 for IERC20;

    /** @notice Seconds in a Week */
    uint256 private constant WEEK = 604800;

    /** @notice Mapping listing the reward token associated to each Quest ID */
    // QuestID => reward token
    mapping(uint256 => address) public questRewardToken;

    /** @notice Mapping of tokens this contract is or was distributing */
    // token address => boolean
    mapping(address => bool) public rewardTokens;

    // Periods: timestamp => start of a week, used as a voting period 
    // in the Curve GaugeController though the timestamp / WEEK *  WEEK logic.
    // Handled through the QuestManager contract.
    // Those can be fetched through this contract when they are closed, or through the QuestManager contract.

    /** @notice List of Closed QuestPeriods by Quest ID */
    // QuestID => array of periods
    mapping(uint256 => uint256[]) public questClosedPeriods;

    /** @notice Merkle Root for each period of a Quest (indexed by Quest ID) */
    // QuestID => period => merkleRoot
    mapping(uint256 => mapping(uint256 => bytes32)) public questMerkleRootPerPeriod;

    /** @notice Amount of rewards for each period of a Quest (indexed by Quest ID) */
    // QuestID => period => totalRewardsAmount
    mapping(uint256 => mapping(uint256 => uint256)) public questRewardsPerPeriod;

    /** @notice BitMap of claims for each period of a Quest */
    // QuestID => period => claimedBitMap
    // This is a packed array of booleans.
    mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) private questPeriodClaimedBitMap;

    /** @notice Address of the QuestBoard contract */
    address public immutable questBoard;


    // Events

    /** @notice Event emitted when a user Claims */
    event Claimed(
        uint256 indexed questID,
        uint256 indexed period,
        uint256 index,
        uint256 amount,
        address rewardToken,
        address indexed account
    );
    /** @notice Event emitted when a New Quest is added */
    event NewQuest(uint256 indexed questID, address rewardToken);
    /** @notice Event emitted when a Period of a Quest is updated (when the Merkle Root is added) */
    event QuestPeriodUpdated(uint256 indexed questID, uint256 indexed period, bytes32 merkleRoot);


    // Modifier

    /** @notice Check the caller is either the admin or the QuestBoard contract */
    modifier onlyAllowed(){
        if(msg.sender != questBoard && msg.sender != owner()) revert Errors.CallerNotAllowed();
        _;
    }


    // Constructor

    constructor(address _questBoard){
        if(_questBoard == address(0)) revert Errors.AddressZero();

        questBoard = _questBoard;
    }

    // Functions
   
    /**
    * @notice Checks if the rewards were claimed for a user on a given period
    * @dev Checks if the rewards were claimed for a user (based on the index) on a given period
    * @param questID ID of the Quest
    * @param period Amount of underlying to borrow
    * @param index Index of the claim
    * @return bool : true if already claimed
    */
    function isClaimed(uint256 questID, uint256 period, uint256 index) public view returns (bool) {
        uint256 claimedWordIndex = index >> 8;
        uint256 claimedBitIndex = index & 0xff;
        uint256 claimedWord = questPeriodClaimedBitMap[questID][period][claimedWordIndex];
        uint256 mask = (1 << claimedBitIndex);
        return claimedWord & mask != 0;
    }
   
    /**
    * @dev Sets the rewards as claimed for the index on the given period
    * @param questID ID of the Quest
    * @param period Timestamp of the period
    * @param index Index of the claim
    */
    function _setClaimed(uint256 questID, uint256 period, uint256 index) private {
        uint256 claimedWordIndex = index >> 8;
        uint256 claimedBitIndex = index & 0xff;
        questPeriodClaimedBitMap[questID][period][claimedWordIndex] |= (1 << claimedBitIndex);
    }

    //Basic Claim   
    /**
    * @notice Claims the reward for a user for a given period of a Quest
    * @dev Claims the reward for a user for a given period of a Quest if the correct proof was given
    * @param questID ID of the Quest
    * @param period Timestamp of the period
    * @param index Index in the Merkle Tree
    * @param account Address of the user claiming the rewards
    * @param amount Amount of rewards to claim
    * @param merkleProof Proof to claim the rewards
    */
    function claim(uint256 questID, uint256 period, uint256 index, address account, uint256 amount, bytes32[] calldata merkleProof) public nonReentrant {
        if(account == address(0)) revert Errors.AddressZero();
        if(questMerkleRootPerPeriod[questID][period] == 0) revert Errors.MerkleRootNotUpdated();
        if(isClaimed(questID, period, index)) revert Errors.AlreadyClaimed();

        // Check that the given parameters match the given Proof
        bytes32 node = keccak256(abi.encodePacked(questID, period, index, account, amount));
        if(!MerkleProof.verify(merkleProof, questMerkleRootPerPeriod[questID][period], node)) revert Errors.InvalidProof();

        // Set the rewards as claimed for that period
        // And transfer the rewards to the user
        address rewardToken = questRewardToken[questID];
        _setClaimed(questID, period, index);
        questRewardsPerPeriod[questID][period] -= amount;
        IERC20(rewardToken).safeTransfer(account, amount);

        emit Claimed(questID, period, index, amount, rewardToken, account);
    }


    //Struct ClaimParams
    struct ClaimParams {
        uint256 questID;
        uint256 period;
        uint256 index;
        uint256 amount;
        bytes32[] merkleProof;
    }


    //Multi Claim   
    /**
    * @notice Claims multiple rewards for a given list
    * @dev Calls the claim() method for each entry in the claims array
    * @param account Address of the user claiming the rewards
    * @param claims List of ClaimParams struct data to claim
    */
    function multiClaim(address account, ClaimParams[] calldata claims) external {
        uint256 length = claims.length;
        
        if(length == 0) revert Errors.EmptyParameters();

        for(uint256 i; i < length;){
            claim(claims[i].questID, claims[i].period, claims[i].index, account, claims[i].amount, claims[i].merkleProof);

            unchecked{ ++i; }
        }
    }


    //FullQuest Claim (form of Multi Claim but for only one Quest => only one ERC20 transfer)
    //Only works for the given periods (in ClaimParams) for the Quest. Any omitted period will be skipped   
    /**
    * @notice Claims the reward for all the given periods of a Quest, and transfer all the rewards at once
    * @dev Sums up all the rewards for given periods of a Quest, and executes only one transfer
    * @param account Address of the user claiming the rewards
    * @param questID ID of the Quest
    * @param claims List of ClaimParams struct data to claim
    */
    function claimQuest(address account, uint256 questID, ClaimParams[] calldata claims) external nonReentrant {
        if(account == address(0)) revert Errors.AddressZero();
        uint256 length = claims.length;

        if(length == 0) revert Errors.EmptyParameters();

        // Total amount claimable, to transfer at once
        uint256 totalClaimAmount;
        address rewardToken = questRewardToken[questID];

        for(uint256 i; i < length;){
            if(claims[i].questID != questID) revert Errors.IncorrectQuestID();
            if(questMerkleRootPerPeriod[questID][claims[i].period] == 0) revert Errors.MerkleRootNotUpdated();
            if(isClaimed(questID, claims[i].period, claims[i].index)) revert Errors.AlreadyClaimed();

            // For each period given, if the proof matches the given parameters, 
            // set as claimed and add to the to total to transfer
            bytes32 node = keccak256(abi.encodePacked(questID, claims[i].period, claims[i].index, account, claims[i].amount));
            if(!MerkleProof.verify(claims[i].merkleProof, questMerkleRootPerPeriod[questID][claims[i].period], node)) revert Errors.InvalidProof();

            _setClaimed(questID, claims[i].period, claims[i].index);
            questRewardsPerPeriod[questID][claims[i].period] -= claims[i].amount;
            totalClaimAmount += claims[i].amount;

            emit Claimed(questID, claims[i].period, claims[i].index, claims[i].amount, rewardToken, account);

            unchecked{ ++i; }
        }

        // Transfer the total claimed amount
        IERC20(rewardToken).safeTransfer(account, totalClaimAmount);
    }

   
    /**
    * @notice Returns all current Closed periods for the given Quest ID
    * @dev Returns all current Closed periods for the given Quest ID
    * @param questID ID of the Quest
    * @return uint256[] : List of closed periods
    */
    function getClosedPeriodsByQuests(uint256 questID) external view returns (uint256[] memory) {
        return questClosedPeriods[questID];
    }



    // Manager functions
   
    /**
    * @notice Adds a new Quest to the listing
    * @dev Adds a new Quest ID and the associated reward token
    * @param questID ID of the Quest
    * @param token Address of the ERC20 reward token
    * @return bool : success
    */
    function addQuest(uint256 questID, address token) external returns(bool) {
        if(msg.sender != questBoard) revert Errors.CallerNotAllowed();
        if(questRewardToken[questID] != address(0)) revert Errors.QuestAlreadyListed();
        if(token == address(0)) revert Errors.TokenNotWhitelisted();

        // Add a new Quest using the QuestID, and list the reward token for that Quest
        questRewardToken[questID] = token;

        if(!rewardTokens[token]) rewardTokens[token] = true;

        emit NewQuest(questID, token);

        return true;
    }

    /**
    * @notice Adds a new period & the rewards of this period for a Quest
    * @dev Adds a new period & the rewards of this period for a Quest
    * @param questID ID of the Quest
    * @param period Timestamp of the period
    * @param totalRewardAmount Total amount of rewards to distribute for the period
    * @return bool : success
    */
    function addQuestPeriod(uint256 questID, uint256 period, uint256 totalRewardAmount) external returns(bool) {
        period = (period / WEEK) * WEEK;
        if(msg.sender != questBoard) revert Errors.CallerNotAllowed();
        if(questRewardToken[questID] == address(0)) revert Errors.QuestNotListed();
        if(questRewardsPerPeriod[questID][period] != 0) revert Errors.PeriodAlreadyUpdated();
        if(period == 0) revert Errors.IncorrectPeriod();
        if(totalRewardAmount == 0) revert Errors.NullAmount();

        questRewardsPerPeriod[questID][period] = totalRewardAmount;

        return true;
    }


    function fixQuestPeriod(uint256 questID, uint256 period, uint256 newTotalRewardAmount) external returns(bool) {
        if(msg.sender != questBoard) revert Errors.CallerNotAllowed();
        period = (period / WEEK) * WEEK;
        if(questRewardToken[questID] == address(0)) revert Errors.QuestNotListed();
        if(period == 0) revert Errors.IncorrectPeriod();
        if(questRewardsPerPeriod[questID][period] == 0) revert Errors.PeriodNotListed();

        uint256 previousTotalRewardAmount = questRewardsPerPeriod[questID][period];

        questRewardsPerPeriod[questID][period] = newTotalRewardAmount;

        if(previousTotalRewardAmount > newTotalRewardAmount){
            // Send back the extra amount of reward token that was incorrectly sent
            // In the case of missing reward token, the Board will send them to this contract

            uint256 extraAmount = previousTotalRewardAmount - newTotalRewardAmount;
            IERC20(questRewardToken[questID]).safeTransfer(questBoard, extraAmount);
        }

        return true;
    }
   
    /**
    * @notice Updates the period of a Quest by adding the Merkle Root
    * @dev Add the Merkle Root for the eriod of the given Quest
    * @param questID ID of the Quest
    * @param period timestamp of the period
    * @param totalAmount sum of all rewards for the Merkle Tree
    * @param merkleRoot MerkleRoot to add
    * @return bool: success
    */
    function updateQuestPeriod(uint256 questID, uint256 period, uint256 totalAmount, bytes32 merkleRoot) external onlyAllowed returns(bool) {
        period = (period / WEEK) * WEEK;
        if(questRewardToken[questID] == address(0)) revert Errors.QuestNotListed();
        if(period == 0) revert Errors.IncorrectPeriod();
        if(questRewardsPerPeriod[questID][period] == 0) revert Errors.PeriodNotListed();
        if(questMerkleRootPerPeriod[questID][period] != 0) revert Errors.PeriodAlreadyUpdated();
        if(merkleRoot == 0) revert Errors.EmptyMerkleRoot();

        // Add a new Closed Period for the Quest
        questClosedPeriods[questID].push(period);

        if(totalAmount != questRewardsPerPeriod[questID][period]) revert Errors.IncorrectRewardAmount();

        // Add the new MerkleRoot for that Closed Period
        questMerkleRootPerPeriod[questID][period] = merkleRoot;

        emit QuestPeriodUpdated(questID, period, merkleRoot);

        return true;
    }


    //  Admin functions
   
    /**
    * @notice Recovers ERC2O tokens sent by mistake to the contract
    * @dev Recovers ERC2O tokens sent by mistake to the contract
    * @param token Address tof the EC2O token
    * @return bool: success
    */
    function recoverERC20(address token) external onlyOwner nonReentrant returns(bool) {
        if(rewardTokens[token]) revert Errors.CannotRecoverToken();
        uint256 amount = IERC20(token).balanceOf(address(this));
        if(amount == 0) revert Errors.NullAmount();
        IERC20(token).safeTransfer(owner(), amount);

        return true;
    }

    // 
    /**
    * @notice Allows to update the MerkleRoot for a given period of a Quest if the current Root is incorrect
    * @dev Updates the MerkleRoot for the period of the Quest
    * @param questID ID of the Quest
    * @param period Timestamp of the period
    * @param merkleRoot New MerkleRoot to add
    * @return bool : success
    */
    function emergencyUpdateQuestPeriod(uint256 questID, uint256 period, uint256 addedRewardAmount, bytes32 merkleRoot) external onlyOwner returns(bool) {
        // In case the given MerkleRoot was incorrect:
        // Process:
        // 1 - block claims for the Quest period by using this method to set an incorrect MerkleRoot, where no proof matches the root
        // 2 - prepare a new Merkle Tree, taking in account user previous claims on that period, and missing/overpaid rewards
        //      a - for all new claims to be added, set them after the last index of the previous Merkle Tree
        //      b - for users that did not claim, keep the same index, and adjust the amount to claim if needed
        //      c - for indexes that were claimed, place an empty node in the Merkle Tree (with an amount at 0 & the address 0xdead as the account)
        // 3 - update the Quest period with the correct MerkleRoot
        // (no need to change the Bitmap, as the new MerkleTree will account for the indexes already claimed)

        period = (period / WEEK) * WEEK;
        if(questRewardToken[questID] == address(0)) revert Errors.QuestNotListed();
        if(period == 0) revert Errors.IncorrectPeriod();
        if(questMerkleRootPerPeriod[questID][period] == 0) revert Errors.PeriodNotClosed();
        if(merkleRoot == 0) revert Errors.EmptyMerkleRoot();

        questMerkleRootPerPeriod[questID][period] = merkleRoot;

        questRewardsPerPeriod[questID][period] += addedRewardAmount;

        emit QuestPeriodUpdated(questID, period, merkleRoot);

        return true;
    }

}

File 8 of 17 : IERC20Permit.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Permit.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
 * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
 *
 * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
 * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
 * need to send a transaction, and thus is not required to hold Ether at all.
 */
interface IERC20Permit {
    /**
     * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
     * given ``owner``'s signed approval.
     *
     * IMPORTANT: The same issues {IERC20-approve} has related to transaction
     * ordering also apply here.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     * - `deadline` must be a timestamp in the future.
     * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
     * over the EIP712-formatted function arguments.
     * - the signature must use ``owner``'s current nonce (see {nonces}).
     *
     * For more information on the signature format, see the
     * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
     * section].
     */
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    /**
     * @dev Returns the current nonce for `owner`. This value must be
     * included whenever a signature is generated for {permit}.
     *
     * Every successful call to {permit} increases ``owner``'s nonce by one. This
     * prevents a signature from being used multiple times.
     */
    function nonces(address owner) external view returns (uint256);

    /**
     * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
     */
    // solhint-disable-next-line func-name-mixedcase
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

File 9 of 17 : IERC20.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

File 10 of 17 : SafeERC20.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.0;

import "../interfaces/IERC20.sol";
import "../extensions/IERC20Permit.sol";
import "../utils/Address.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    using Address for address;

    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }

    /**
     * @dev Deprecated. This function has issues similar to the ones found in
     * {IERC20-approve}, and its usage is discouraged.
     *
     * Whenever possible, use {safeIncreaseAllowance} and
     * {safeDecreaseAllowance} instead.
     */
    function safeApprove(IERC20 token, address spender, uint256 value) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 newAllowance = token.allowance(address(this), spender) + value;
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
    }

    function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        unchecked {
            uint256 oldAllowance = token.allowance(address(this), spender);
            require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
            uint256 newAllowance = oldAllowance - value;
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
        }
    }

    function safePermit(
        IERC20Permit token,
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        uint256 nonceBefore = token.nonces(owner);
        token.permit(owner, spender, value, deadline, v, r, s);
        uint256 nonceAfter = token.nonces(owner);
        require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
        if (returndata.length > 0) {
            // Return data is optional
            require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
        }
    }
}

File 11 of 17 : Address.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol)

pragma solidity ^0.8.1;

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     *
     * Furthermore, `isContract` will also return true if the target contract within
     * the same transaction is already scheduled for destruction by `SELFDESTRUCT`,
     * which only has an effect at the end of a transaction.
     * ====
     *
     * [IMPORTANT]
     * ====
     * You shouldn't rely on `isContract` to protect against flash loan attacks!
     *
     * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
     * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
     * constructor.
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize/address.code.length, which returns 0
        // for contracts in construction, since the code is only stored at the end
        // of the constructor execution.

        return account.code.length > 0;
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain `call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
     * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
     *
     * _Available since v4.8._
     */
    function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        if (success) {
            if (returndata.length == 0) {
                // only check isContract if the call was successful and the return data is empty
                // otherwise we already know that it was a contract
                require(isContract(target), "Address: call to non-contract");
            }
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    /**
     * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason or using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    function _revert(bytes memory returndata, string memory errorMessage) private pure {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            /// @solidity memory-safe-assembly
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }
}

File 12 of 17 : Context.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

File 13 of 17 : MerkleProof.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol)

pragma solidity ^0.8.0;

/**
 * @dev These functions deal with verification of Merkle Tree proofs.
 *
 * The tree and the proofs can be generated using our
 * https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
 * You will find a quickstart guide in the readme.
 *
 * WARNING: You should avoid using leaf values that are 64 bytes long prior to
 * hashing, or use a hash function other than keccak256 for hashing leaves.
 * This is because the concatenation of a sorted pair of internal nodes in
 * the merkle tree could be reinterpreted as a leaf value.
 * OpenZeppelin's JavaScript library generates merkle trees that are safe
 * against this attack out of the box.
 */
library MerkleProof {
    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     */
    function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        return processProof(proof, leaf) == root;
    }

    /**
     * @dev Calldata version of {verify}
     *
     * _Available since v4.7._
     */
    function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        return processProofCalldata(proof, leaf) == root;
    }

    /**
     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
     * hash matches the root of the tree. When processing the proof, the pairs
     * of leafs & pre-images are assumed to be sorted.
     *
     * _Available since v4.4._
     */
    function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = _hashPair(computedHash, proof[i]);
        }
        return computedHash;
    }

    /**
     * @dev Calldata version of {processProof}
     *
     * _Available since v4.7._
     */
    function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = _hashPair(computedHash, proof[i]);
        }
        return computedHash;
    }

    /**
     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a merkle tree defined by
     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
     *
     * CAUTION: Not all merkle trees admit multiproofs. See {processMultiProof} for details.
     *
     * _Available since v4.7._
     */
    function multiProofVerify(
        bytes32[] memory proof,
        bool[] memory proofFlags,
        bytes32 root,
        bytes32[] memory leaves
    ) internal pure returns (bool) {
        return processMultiProof(proof, proofFlags, leaves) == root;
    }

    /**
     * @dev Calldata version of {multiProofVerify}
     *
     * CAUTION: Not all merkle trees admit multiproofs. See {processMultiProof} for details.
     *
     * _Available since v4.7._
     */
    function multiProofVerifyCalldata(
        bytes32[] calldata proof,
        bool[] calldata proofFlags,
        bytes32 root,
        bytes32[] memory leaves
    ) internal pure returns (bool) {
        return processMultiProofCalldata(proof, proofFlags, leaves) == root;
    }

    /**
     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
     * respectively.
     *
     * CAUTION: Not all merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
     *
     * _Available since v4.7._
     */
    function processMultiProof(
        bytes32[] memory proof,
        bool[] memory proofFlags,
        bytes32[] memory leaves
    ) internal pure returns (bytes32 merkleRoot) {
        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
        // the merkle tree.
        uint256 leavesLen = leaves.length;
        uint256 totalHashes = proofFlags.length;

        // Check proof validity.
        require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");

        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
        bytes32[] memory hashes = new bytes32[](totalHashes);
        uint256 leafPos = 0;
        uint256 hashPos = 0;
        uint256 proofPos = 0;
        // At each step, we compute the next hash using two values:
        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
        //   get the next hash.
        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
        //   `proof` array.
        for (uint256 i = 0; i < totalHashes; i++) {
            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
            bytes32 b = proofFlags[i]
                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
                : proof[proofPos++];
            hashes[i] = _hashPair(a, b);
        }

        if (totalHashes > 0) {
            unchecked {
                return hashes[totalHashes - 1];
            }
        } else if (leavesLen > 0) {
            return leaves[0];
        } else {
            return proof[0];
        }
    }

    /**
     * @dev Calldata version of {processMultiProof}.
     *
     * CAUTION: Not all merkle trees admit multiproofs. See {processMultiProof} for details.
     *
     * _Available since v4.7._
     */
    function processMultiProofCalldata(
        bytes32[] calldata proof,
        bool[] calldata proofFlags,
        bytes32[] memory leaves
    ) internal pure returns (bytes32 merkleRoot) {
        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
        // the merkle tree.
        uint256 leavesLen = leaves.length;
        uint256 totalHashes = proofFlags.length;

        // Check proof validity.
        require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");

        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
        bytes32[] memory hashes = new bytes32[](totalHashes);
        uint256 leafPos = 0;
        uint256 hashPos = 0;
        uint256 proofPos = 0;
        // At each step, we compute the next hash using two values:
        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
        //   get the next hash.
        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
        //   `proof` array.
        for (uint256 i = 0; i < totalHashes; i++) {
            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
            bytes32 b = proofFlags[i]
                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
                : proof[proofPos++];
            hashes[i] = _hashPair(a, b);
        }

        if (totalHashes > 0) {
            unchecked {
                return hashes[totalHashes - 1];
            }
        } else if (leavesLen > 0) {
            return leaves[0];
        } else {
            return proof[0];
        }
    }

    function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
        return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
    }

    function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
        /// @solidity memory-safe-assembly
        assembly {
            mstore(0x00, a)
            mstore(0x20, b)
            value := keccak256(0x00, 0x40)
        }
    }
}

File 14 of 17 : Ownable.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "./Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

File 15 of 17 : ReentrancyGuard.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}

File 16 of 17 : QuestBoard.sol
//██████╗  █████╗ ██╗      █████╗ ██████╗ ██╗███╗   ██╗
//██╔══██╗██╔══██╗██║     ██╔══██╗██╔══██╗██║████╗  ██║
//██████╔╝███████║██║     ███████║██║  ██║██║██╔██╗ ██║
//██╔═══╝ ██╔══██║██║     ██╔══██║██║  ██║██║██║╚██╗██║
//██║     ██║  ██║███████╗██║  ██║██████╔╝██║██║ ╚████║
//╚═╝     ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═════╝ ╚═╝╚═╝  ╚═══╝
 

//SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.16;

import "./oz/interfaces/IERC20.sol";
import "./oz/libraries/SafeERC20.sol";
import "./oz/utils/ReentrancyGuard.sol";
import "./libraries/QuestDataTypes.sol";
import "./interfaces/IQuestBoard.sol";
import "./interfaces/IGaugeController.sol";
import "./interfaces/IGauge.sol";
import "./MultiMerkleDistributor.sol";
import "./modules/BiasCalculator.sol";
import "./utils/Owner.sol";
import "./libraries/Errors.sol";

/** @title Warden Quest Board V2  */
/// @author Paladin
/*
    V2 of Quest Board allowing to blacklist or whitelist veToken voters
    and chose between fixed or ranged rewards distribution
*/

contract QuestBoard is IQuestBoard, Owner, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // Storage

    /** @notice Address of the Curve Gauge Controller */
    address public immutable GAUGE_CONTROLLER;

    /** @notice Seconds in a Week */
    uint256 private constant WEEK = 604800;
    /** @notice 1e18 scale */
    uint256 private constant UNIT = 1e18;
    /** @notice Max BPS value (100%) */
    uint256 private constant MAX_BPS = 10000;
    /** @notice Delay where contract can be unkilled */
    uint256 private constant KILL_DELAY = 2 * 604800; //2 weeks

    /** @notice ID for the next Quest to be created */
    uint256 public nextID;

    /** @notice List of Quest (indexed by ID) */
    // ID => Quest
    mapping(uint256 => Quest) public quests;
    /** @notice List of timestamp periods the Quest is active in */
    // QuestID => Periods (timestamps)
    mapping(uint256 => uint48[]) private questPeriods;
    /** @notice Mapping of all QuestPeriod struct for each period of each Quest */
    // QuestID => period => QuestPeriod
    mapping(uint256 => mapping(uint256 => QuestPeriod)) public periodsByQuest;
    /** @notice All the Quests present in this period */
    // period => array of Quest
    mapping(uint256 => uint256[]) private questsByPeriod;
    /** @notice All the Quests present in this period for each gauge */
    // gauge => period => array of Quest
    mapping(address => mapping(uint256 => uint256[])) private questsByGaugeByPeriod;
    /** @notice Mapping of Distributors used by each Quest to send rewards */
    // ID => Distributor
    mapping(uint256 => address) public questDistributors;
    /** @notice Amount not distributed, for Quest creators to redeem */
    mapping(uint256 => uint256) public questWithdrawableAmount;


    /** @notice Platform fees ratio (in BPS) */
    uint256 public platformFeeRatio = 400;
    /** @notice Mapping of specific fee ratio for some Quest creators */
    // Creator => specific fee
    mapping(address => uint256) public customPlatformFeeRatio;

    /** @notice Minimum Objective required */
    uint256 public objectiveMinimalThreshold;

    /** @notice Address of the Chest to receive platform fees */
    address public questChest;
    /** @notice Address of the reward Distributor contract */
    address public distributor;
    /** @notice Address of the Bias Calculator Module */
    address public biasCalculator;

    /** @notice Mapping of addresses allowed to call manager methods */
    mapping(address => bool) private approvedManagers;
    /** @notice Whitelisted tokens that can be used as reward tokens */
    mapping(address => bool) public whitelistedTokens;
    /** @notice Min rewardPerVote per token (to avoid spam creation of useless Quest) */
    mapping(address => uint256) public minRewardPerVotePerToken;

    /** @notice Boolean, true if the cotnract was killed, stopping main user functions */
    bool public isKilled;
    /** @notice Timestamp when the contract was killed */
    uint256 public killTs;

    

    // Modifiers

    /** @notice Check the caller is either the admin or an approved manager */
    modifier onlyAllowed(){
        if(!approvedManagers[msg.sender] && msg.sender != owner()) revert Errors.CallerNotAllowed();
        _;
    }

    /** @notice Check that contract was not killed */
    modifier isAlive(){
        if(isKilled) revert Errors.Killed();
        _;
    }

    /** @notice Check that contract was initialized */
    modifier isInitialized(){
        if(distributor == address(0)) revert Errors.NotInitialized();
        _;
    }


    // Constructor
    constructor(address _gaugeController, address _chest){
        if(
            _gaugeController == address(0)
            || _chest == address(0)
        ) revert Errors.AddressZero();

        GAUGE_CONTROLLER = _gaugeController;

        questChest = _chest;

        objectiveMinimalThreshold = 1000 * UNIT;
    }

   
    /**
    * @notice Initialize the contract
    * @param _distributor Address of the Distributor
    * @param _biasCalculator Address of the Bias Calculator
    */
    function init(address _distributor, address _biasCalculator) external onlyOwner {
        if(distributor != address(0)) revert Errors.AlreadyInitialized();
        if(_distributor == address(0) || _biasCalculator == address(0)) revert Errors.AddressZero();

        distributor = _distributor;
        biasCalculator= _biasCalculator;

        emit Init(_distributor, _biasCalculator);
    }


    // View Functions
   
    /**
    * @notice Returns the current Period for the contract
    * @dev Returns the current Period for the contract
    */
    function getCurrentPeriod() public view returns(uint256) {
        return (block.timestamp / WEEK) * WEEK;
    }
   
    /**
    * @notice Returns the list of all Quest IDs active on a given period
    * @dev Returns the list of all Quest IDs active on a given period
    * @param period Timestamp of the period
    * @return uint256[] : Quest IDs for the period
    */
    function getQuestIdsForPeriod(uint256 period) external view returns(uint256[] memory) {
        period = (period / WEEK) * WEEK;
        return questsByPeriod[period];
    }
   
    /**
    * @notice Returns the list of all Quest IDs active on a given period
    * @dev Returns the list of all Quest IDs active on a given period
    * @param period Timestamp of the period
    * @return uint256[] : Quest IDs for the period
    */
    function getQuestIdsForPeriodForGauge(address gauge, uint256 period) external view returns(uint256[] memory) {
        period = (period / WEEK) * WEEK;
        return questsByGaugeByPeriod[gauge][period];
    }
   
    /**
    * @notice Returns all periods for a Quest
    * @dev Returns all period timestamps for a Quest ID
    * @param questId ID of the Quest
    * @return uint256[] : List of period timestamps
    */
    function getAllPeriodsForQuestId(uint256 questId) external view returns(uint48[] memory) {
        return questPeriods[questId];
    }
   
    /**
    * @notice Returns all QuestPeriod of a given Quest
    * @dev Returns all QuestPeriod of a given Quest ID
    * @param questId ID of the Quest
    * @return QuestPeriod[] : list of QuestPeriods
    */
    function getAllQuestPeriodsForQuestId(uint256 questId) external view returns(QuestPeriod[] memory) {
        uint256 nbPeriods = questPeriods[questId].length;
        QuestPeriod[] memory periods = new QuestPeriod[](nbPeriods);
        for(uint256 i; i < nbPeriods;){
            periods[i] = periodsByQuest[questId][questPeriods[questId][i]];
            unchecked{ ++i; }
        }
        return periods;
    }
   
    /**
    * @notice Returns the number of periods to come for a given Quest
    * @dev Returns the number of periods to come for a given Quest
    * @param questID ID of the Quest
    * @return uint : remaining duration (non active periods)
    */
    function _getRemainingDuration(uint256 questID) internal view returns(uint256) {
        // Since we have the current period, the start period for the Quest, and each period is 1 WEEK
        // We can find the number of remaining periods in the Quest simply by dividing the remaining time between
        // currentPeriod and the last QuestPeriod start, plus 1 WEEK, by a WEEK.
        // If the current period is the last period of the Quest, we want to return 1
        if(questPeriods[questID].length == 0) revert Errors.EmptyQuest();
        uint256 lastPeriod = questPeriods[questID][questPeriods[questID].length - 1];
        uint256 currentPeriod = getCurrentPeriod();
        return lastPeriod < currentPeriod ? 0: ((lastPeriod - currentPeriod) + WEEK) / WEEK;
    }

    /**
    * @notice Returns the current reduced bias of a gauge for a given Quest
    * @dev Returns the current reduced bias of a gauge for a given Quest
    * @param questID ID of the Quest
    * @return uint256 : current reduced bias of the gauge
    */
    function getCurrentReducedBias(uint256 questID) external view returns(uint256) {
        return BiasCalculator(biasCalculator).getCurrentReducedBias(
            questID,
            quests[questID].gauge,
            quests[questID].types.voteType
        );
    }

    /**
    * @notice Returns the address of the Quest creator
    * @dev Returns the address of the Quest creator
    * @param questID ID of the Quest
    * @return address : creator of the Quest
    */
    function getQuestCreator(uint256 questID) external view returns(address){
        return quests[questID].creator;
    }


    // Functions

    /**
    * @notice Creates a fixed rewards Quest based on the given parameters
    * @dev Creates a Quest based on the given parameters & the given types with the Fixed Rewards type
    * @param gauge Address of the gauge
    * @param rewardToken Address of the reward token
    * @param startNextPeriod (bool) true to start the Quest the next period
    * @param duration Duration of the Quest (in weeks)
    * @param rewardPerVote Amount of reward/vote (in wei)
    * @param totalRewardAmount Total amount of rewards available for the full Quest duration
    * @param feeAmount Amount of fees paid at creation
    * @param voteType Vote type for the Quest
    * @param closeType Close type for the Quest
    * @param voterList List of voters for the Quest (to be used for Blacklist or Whitelist)
    * @return uint256 : ID of the newly created Quest
    */
    function createFixedQuest(
        address gauge,
        address rewardToken,
        bool startNextPeriod,
        uint48 duration,
        uint256 rewardPerVote,
        uint256 totalRewardAmount,
        uint256 feeAmount,
        QuestDataTypes.QuestVoteType voteType,
        QuestDataTypes.QuestCloseType closeType,
        address[] calldata voterList
    ) external nonReentrant isAlive isInitialized returns(uint256) {
        // Set the Quest Types for the new Quest
        QuestTypes memory types = QuestTypes({
            voteType: voteType,
            rewardsType: QuestDataTypes.QuestRewardsType.FIXED,
            closeType: closeType
        });

        return _createQuest(
            gauge,
            rewardToken,
            types,
            startNextPeriod,
            duration,
            rewardPerVote,
            rewardPerVote,
            totalRewardAmount,
            feeAmount,
            voterList
        );
    }

    /**
    * @notice Creates a ranged rewards Quest based on the given parameters
    * @dev Creates a Quest based on the given parameters & the given types with the Ranged Rewards type
    * @param gauge Address of the gauge
    * @param rewardToken Address of the reward token
    * @param startNextPeriod (bool) true to start the Quest the next period
    * @param duration Duration of the Quest (in weeks)
    * @param minRewardPerVote Minimum amount of reward/vote (in wei)
    * @param maxRewardPerVote Maximum amount of reward/vote (in wei)
    * @param totalRewardAmount Total amount of rewards available for the full Quest duration
    * @param feeAmount Amount of fees paid at creation
    * @param voteType Vote type for the Quest
    * @param closeType Close type for the Quest
    * @param voterList List of voters for the Quest (to be used for Blacklist or Whitelist)
    * @return uint256 : ID of the newly created Quest
    */
    function createRangedQuest(
        address gauge,
        address rewardToken,
        bool startNextPeriod,
        uint48 duration,
        uint256 minRewardPerVote,
        uint256 maxRewardPerVote,
        uint256 totalRewardAmount,
        uint256 feeAmount,
        QuestDataTypes.QuestVoteType voteType,
        QuestDataTypes.QuestCloseType closeType,
        address[] calldata voterList
    ) external nonReentrant isAlive isInitialized returns(uint256) {
        // Set the Quest Types for the new Quest
        QuestTypes memory types = QuestTypes({
            voteType: voteType,
            rewardsType: QuestDataTypes.QuestRewardsType.RANGE,
            closeType: closeType
        });

        return _createQuest(
            gauge,
            rewardToken,
            types,
            startNextPeriod,
            duration,
            minRewardPerVote,
            maxRewardPerVote,
            totalRewardAmount,
            feeAmount,
            voterList
        );
    }

    /**
    * @notice Creates a Quest based on the given parameters
    * @dev Creates a Quest based on the given parameters & the given types
    * @param gauge Address of the gauge
    * @param rewardToken Address of the reward token
    * @param types Quest Types (Rewards, Vote & Close)
    * @param startNextPeriod (bool) true to start the Quest the next period
    * @param duration Duration of the Quest (in weeks)
    * @param minRewardPerVote Minimum amount of reward/vote (in wei)
    * @param maxRewardPerVote Maximum amount of reward/vote (in wei)
    * @param totalRewardAmount Total amount of rewards available for the full Quest duration
    * @param feeAmount Amount of fees paid at creation
    * @param voterList List of voters for the Quest (to be used for Blacklist or Whitelist)
    * @return newQuestID (uint256) : ID of the newly created Quest
    */
    function _createQuest(
        address gauge,
        address rewardToken,
        QuestTypes memory types,
        bool startNextPeriod,
        uint48 duration,
        uint256 minRewardPerVote,
        uint256 maxRewardPerVote,
        uint256 totalRewardAmount,
        uint256 feeAmount,
        address[] calldata voterList
    ) internal returns(uint256 newQuestID) {
        // Local memory variables
        CreateVars memory vars;
        vars.creator = msg.sender;

        // Check all parameters
        if(gauge == address(0) || rewardToken == address(0)) revert Errors.AddressZero();
        if(IGaugeController(GAUGE_CONTROLLER).gauge_types(gauge) < 0) revert Errors.InvalidGauge();
        if(!whitelistedTokens[rewardToken]) revert Errors.TokenNotWhitelisted();
        if(duration == 0) revert Errors.IncorrectDuration();
        if(minRewardPerVote == 0 || maxRewardPerVote == 0 || totalRewardAmount == 0 || feeAmount == 0) revert Errors.NullAmount();
        if(minRewardPerVote < minRewardPerVotePerToken[rewardToken]) revert Errors.RewardPerVoteTooLow();
        if(minRewardPerVote > maxRewardPerVote) revert Errors.MinValueOverMaxValue();
        if(types.rewardsType == QuestDataTypes.QuestRewardsType.FIXED && minRewardPerVote != maxRewardPerVote) revert Errors.InvalidQuestType();
        if((totalRewardAmount * _getFeeRatio(msg.sender))/MAX_BPS != feeAmount) revert Errors.IncorrectFeeAmount();

        // Calculate the reward per period, and the max vote objective per period
        vars.rewardPerPeriod = totalRewardAmount / duration;
        vars.maxObjective = (vars.rewardPerPeriod * UNIT) / minRewardPerVote;

        // And based on the Quest Rewards type, calculate the min vote objective per period
        if(types.rewardsType == QuestDataTypes.QuestRewardsType.RANGE) {
            // For a Ranged Quest, calculate it based on the max reward per vote
            vars.minObjective = (vars.rewardPerPeriod * UNIT) / maxRewardPerVote;
        } else {
            // Otherwise, min == max
            vars.minObjective = vars.maxObjective;
        }

        if(vars.minObjective < objectiveMinimalThreshold) revert Errors.ObjectiveTooLow();

        // Pull all the rewards in this contract
        IERC20(rewardToken).safeTransferFrom(vars.creator, address(this), totalRewardAmount);
        // And transfer the fees from the Quest creator to the Chest contract
        IERC20(rewardToken).safeTransferFrom(vars.creator, questChest, feeAmount);

        // Get the period when the Quest starts (current or next period)
        vars.startPeriod = getCurrentPeriod();
        if(startNextPeriod) vars.startPeriod += WEEK;

        // Get the ID for that new Quest and increment the nextID counter
        newQuestID = nextID;
        unchecked{ ++nextID; }

        // Fill the Quest struct data
        quests[newQuestID].creator = vars.creator;
        quests[newQuestID].rewardToken = rewardToken;
        quests[newQuestID].gauge = gauge;
        quests[newQuestID].types = types;
        quests[newQuestID].duration = duration;
        quests[newQuestID].totalRewardAmount = totalRewardAmount;
        quests[newQuestID].periodStart = safe48(vars.startPeriod);

        //Set the current Distributor as the one to receive the rewards for users for that Quest
        questDistributors[newQuestID] = distributor;

        // Iterate on periods based on Quest duration
        vars.periodIterator = vars.startPeriod;
        for(uint256 i; i < duration;){
            // Add the Quest on the list of Quests active on the period
            questsByPeriod[vars.periodIterator].push(newQuestID);
            questsByGaugeByPeriod[gauge][vars.periodIterator].push(newQuestID);

            // And add the period in the list of periods of the Quest
            questPeriods[newQuestID].push(safe48(vars.periodIterator));

            periodsByQuest[newQuestID][vars.periodIterator].periodStart = safe48(vars.periodIterator);
            periodsByQuest[newQuestID][vars.periodIterator].minObjectiveVotes = vars.minObjective;
            periodsByQuest[newQuestID][vars.periodIterator].maxObjectiveVotes = vars.maxObjective;
            periodsByQuest[newQuestID][vars.periodIterator].minRewardPerVote = minRewardPerVote;
            periodsByQuest[newQuestID][vars.periodIterator].maxRewardPerVote = maxRewardPerVote;
            periodsByQuest[newQuestID][vars.periodIterator].rewardAmountPerPeriod = vars.rewardPerPeriod;
            periodsByQuest[newQuestID][vars.periodIterator].currentState = QuestDataTypes.PeriodState.ACTIVE;

            vars.periodIterator = ((vars.periodIterator + WEEK) / WEEK) * WEEK;

            unchecked{ ++i; }
        }

        if(voterList.length > 0) {
            // Set the voterlist for this Quest
            BiasCalculator(biasCalculator).setQuestVoterList(newQuestID, voterList);
        }

        if(types.closeType == QuestDataTypes.QuestCloseType.DISTRIBUTE){
            // Check if the Board is allowed to distribute rewards to the gauge
            // If not, we want to revert and the creator to chose another Close type
            if(IGauge(gauge).reward_data(rewardToken).distributor != address(this)) revert Errors.BoardIsNotAllowedDistributor();
        }

        // Add that Quest & the reward token in the Distributor
        if(!MultiMerkleDistributor(distributor).addQuest(newQuestID, rewardToken)) revert Errors.DisitributorFail();

        emit NewQuest(
            newQuestID,
            msg.sender,
            gauge,
            rewardToken,
            duration,
            vars.startPeriod
        );
    }

    /**
    * @notice Increases the duration of a Quest
    * @dev Adds more QuestPeriods and extends the duration of a Quest
    * @param questID ID of the Quest
    * @param addedDuration Number of period to add
    * @param addedRewardAmount Amount of reward to add for the new periods (in wei)
    * @param feeAmount Platform fees amount (in wei)
    */
    function extendQuestDuration(
        uint256 questID,
        uint48 addedDuration,
        uint256 addedRewardAmount,
        uint256 feeAmount
    ) external nonReentrant isAlive isInitialized {
        // Local memory variables
        ExtendVars memory vars;

        if(questID >= nextID) revert Errors.InvalidQuestID();
        if(msg.sender != quests[questID].creator) revert Errors.CallerNotAllowed();
        if(addedRewardAmount == 0 || feeAmount == 0) revert Errors.NullAmount();
        if(addedDuration == 0) revert Errors.IncorrectAddDuration();

        // We take data from the last period of the Quest to account for any other changes in the Quest parameters
        if(questPeriods[questID].length == 0) revert Errors.EmptyQuest();
        vars.lastPeriod = questPeriods[questID][questPeriods[questID].length - 1];
        
        if(periodsByQuest[questID][questPeriods[questID][0]].periodStart >= block.timestamp) revert Errors.QuestNotStarted();
        if(vars.lastPeriod < getCurrentPeriod()) revert Errors.ExpiredQuest();

        // Check that the given amounts are correct
        vars.rewardPerPeriod = periodsByQuest[questID][vars.lastPeriod].rewardAmountPerPeriod;

        if((vars.rewardPerPeriod * addedDuration) != addedRewardAmount) revert Errors.IncorrectAddedRewardAmount();
        if((addedRewardAmount * _getFeeRatio(msg.sender))/MAX_BPS != feeAmount) revert Errors.IncorrectFeeAmount();

        vars.gauge = quests[questID].gauge;
        vars.rewardToken = quests[questID].rewardToken;
        // Pull all the rewards in this contract
        IERC20(vars.rewardToken).safeTransferFrom(msg.sender, address(this), addedRewardAmount);
        // And transfer the fees from the Quest creator to the Chest contract
        IERC20(vars.rewardToken).safeTransferFrom(msg.sender, questChest, feeAmount);

        vars.periodIterator = ((vars.lastPeriod + WEEK) / WEEK) * WEEK;

        // Update the Quest struct with added reward admounts & added duration
        quests[questID].totalRewardAmount += addedRewardAmount;
        quests[questID].duration += addedDuration;

        vars.minObjective = periodsByQuest[questID][vars.lastPeriod].minObjectiveVotes;
        vars.maxObjective = periodsByQuest[questID][vars.lastPeriod].maxObjectiveVotes;
        vars.minRewardPerVote = periodsByQuest[questID][vars.lastPeriod].minRewardPerVote;
        vars.maxRewardPerVote = periodsByQuest[questID][vars.lastPeriod].maxRewardPerVote;

        // Add QuestPeriods for the new added duration
        for(uint256 i; i < addedDuration;){
            questsByPeriod[vars.periodIterator].push(questID);
            questsByGaugeByPeriod[quests[questID].gauge][vars.periodIterator].push(questID);

            questPeriods[questID].push(safe48(vars.periodIterator));

            periodsByQuest[questID][vars.periodIterator].periodStart = safe48(vars.periodIterator);
            periodsByQuest[questID][vars.periodIterator].minObjectiveVotes = vars.minObjective;
            periodsByQuest[questID][vars.periodIterator].maxObjectiveVotes = vars.maxObjective;
            periodsByQuest[questID][vars.periodIterator].minRewardPerVote = vars.minRewardPerVote;
            periodsByQuest[questID][vars.periodIterator].maxRewardPerVote = vars.maxRewardPerVote;
            periodsByQuest[questID][vars.periodIterator].rewardAmountPerPeriod = vars.rewardPerPeriod;
            periodsByQuest[questID][vars.periodIterator].currentState = QuestDataTypes.PeriodState.ACTIVE;
            vars.periodIterator = ((vars.periodIterator + WEEK) / WEEK) * WEEK;

            unchecked{ ++i; }
        }

        emit ExtendQuestDuration(questID, addedDuration, addedRewardAmount);

    }
      
    /**
    * @notice Updates the parametes of the Quest
    * @dev Updates the reward/vote parameters, allowing to update the Quest objectives too
    * @param questID ID of the Quest
    * @param newMinRewardPerVote New min reward/vote value (in wei)
    * @param newMaxRewardPerVote New max reward/vote value (in wei)
    * @param addedPeriodRewardAmount Amount of reward to add for each period (in wei)
    * @param addedTotalRewardAmount Amount of reward to add for all periods (in wei)
    * @param feeAmount Platform fees amount (in wei)
    */
    function updateQuestParameters(
        uint256 questID,
        uint256 newMinRewardPerVote,
        uint256 newMaxRewardPerVote,
        uint256 addedPeriodRewardAmount,
        uint256 addedTotalRewardAmount,
        uint256 feeAmount
    ) external nonReentrant isAlive isInitialized {
        // Local memory variables
        UpdateVars memory vars;

        if(questID >= nextID) revert Errors.InvalidQuestID();
        if(msg.sender != quests[questID].creator) revert Errors.CallerNotAllowed();
        if(newMinRewardPerVote == 0 || newMaxRewardPerVote == 0) revert Errors.NullAmount();
        if(newMinRewardPerVote > newMaxRewardPerVote) revert Errors.MinValueOverMaxValue();
        if(quests[questID].types.rewardsType == QuestDataTypes.QuestRewardsType.FIXED && newMinRewardPerVote != newMaxRewardPerVote) revert Errors.InvalidQuestType();

        // Check the reamining duration, and that the given reward amounts are correct
        vars.remainingDuration = _getRemainingDuration(questID); //Also handles the Empty Quest check
        if(vars.remainingDuration == 0) revert Errors.ExpiredQuest();
        if(periodsByQuest[questID][questPeriods[questID][0]].periodStart >= block.timestamp) revert Errors.QuestNotStarted();
        if((addedPeriodRewardAmount * vars.remainingDuration) != addedTotalRewardAmount) revert Errors.IncorrectAddedRewardAmount();
        if((addedTotalRewardAmount * _getFeeRatio(msg.sender))/MAX_BPS != feeAmount) revert Errors.IncorrectFeeAmount();

        // The new min reward amount must be higher 
        vars.currentPeriod = getCurrentPeriod();
        if(newMinRewardPerVote < periodsByQuest[questID][vars.currentPeriod].minRewardPerVote) revert Errors.LowerRewardPerVote();

        // Get the amount of reward for each period
        vars.newRewardPerPeriod = periodsByQuest[questID][vars.currentPeriod].rewardAmountPerPeriod + addedPeriodRewardAmount;

        // Calculate the new max vote objective, and the min vote objective based on the Quest Rewards type
        vars.newMaxObjective = (vars.newRewardPerPeriod * UNIT) / newMinRewardPerVote;
        vars.newMinObjective;
        if(quests[questID].types.rewardsType == QuestDataTypes.QuestRewardsType.RANGE) {
            vars.newMinObjective = (vars.newRewardPerPeriod * UNIT) / newMaxRewardPerVote;
        } else {
            vars.newMinObjective = vars.newMaxObjective;
        }
        
        if(
            vars.newMinObjective < periodsByQuest[questID][vars.currentPeriod].minObjectiveVotes
        ) revert Errors.NewObjectiveTooLow();

        if(addedTotalRewardAmount > 0) {
            address rewardToken = quests[questID].rewardToken;
            // Pull all the rewards in this contract
            IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), addedTotalRewardAmount);
            // And transfer the fees from the Quest creator to the Chest contract
            IERC20(rewardToken).safeTransferFrom(msg.sender, questChest, feeAmount);
        }

        vars.periodIterator = vars.currentPeriod;

        vars.lastPeriod = questPeriods[questID][questPeriods[questID].length - 1];

        // Update the Quest struct with the added reward amount
        quests[questID].totalRewardAmount += addedTotalRewardAmount;

        // Update all QuestPeriods, starting with the currentPeriod one
        for(uint256 i; i < vars.remainingDuration;){

            if(vars.periodIterator > vars.lastPeriod) break; //Safety check, we never want to write on non-initialized QuestPeriods (that were not initialized)

            // And update each QuestPeriod with the new values
            periodsByQuest[questID][vars.periodIterator].minRewardPerVote = newMinRewardPerVote;
            periodsByQuest[questID][vars.periodIterator].maxRewardPerVote = newMaxRewardPerVote;
            periodsByQuest[questID][vars.periodIterator].minObjectiveVotes = vars.newMinObjective;
            periodsByQuest[questID][vars.periodIterator].maxObjectiveVotes = vars.newMaxObjective;
            periodsByQuest[questID][vars.periodIterator].rewardAmountPerPeriod = vars.newRewardPerPeriod;

            vars.periodIterator = ((vars.periodIterator + WEEK) / WEEK) * WEEK;

            unchecked{ ++i; }
        }

        emit UpdateQuestParameters(
            questID,
            vars.currentPeriod,
            newMinRewardPerVote,
            newMaxRewardPerVote,
            addedPeriodRewardAmount
        );
    }
   
    /**
    * @notice Withdraw all undistributed rewards from Closed Quest Periods
    * @dev Withdraw all undistributed rewards from Closed Quest Periods
    * @param questID ID of the Quest
    * @param recipient Address to send the reward tokens to
    */
    function withdrawUnusedRewards(uint256 questID, address recipient) external nonReentrant isAlive isInitialized {
        if(questID >= nextID) revert Errors.InvalidQuestID();
        if(msg.sender != quests[questID].creator) revert Errors.CallerNotAllowed();
        if(recipient == address(0)) revert Errors.AddressZero();

        // Total amount available to withdraw
        uint256 withdrawAmount = questWithdrawableAmount[questID];
        questWithdrawableAmount[questID] = 0;

        // If there is a non null amount of token to withdraw, execute a transfer
        if(withdrawAmount != 0){
            address rewardToken = quests[questID].rewardToken;
            IERC20(rewardToken).safeTransfer(recipient, withdrawAmount);

            emit WithdrawUnusedRewards(questID, recipient, withdrawAmount);
        }
    }
   
    /**
    * @notice Emergency withdraws all undistributed rewards from Closed Quest Periods & all rewards for Active Periods
    * @dev Emergency withdraws all undistributed rewards from Closed Quest Periods & all rewards for Active Periods
    * @param questID ID of the Quest
    * @param recipient Address to send the reward tokens to
    */
    function emergencyWithdraw(uint256 questID, address recipient) external nonReentrant {
        if(!isKilled) revert Errors.NotKilled();
        if(block.timestamp < killTs + KILL_DELAY) revert Errors.KillDelayNotExpired();

        if(questID >= nextID) revert Errors.InvalidQuestID();
        if(msg.sender != quests[questID].creator) revert Errors.CallerNotAllowed();
        if(recipient == address(0)) revert Errors.AddressZero();

        // Total amount to emergency withdraw
        uint256 withdrawAmount = questWithdrawableAmount[questID];
        questWithdrawableAmount[questID] = 0;

        uint48[] memory _questPeriods = questPeriods[questID];
        uint256 length = _questPeriods.length;
        for(uint256 i; i < length;){
            QuestPeriod storage _questPeriod = periodsByQuest[questID][_questPeriods[i]];

            // For ACTIVE periods
            if(_questPeriod.currentState == QuestDataTypes.PeriodState.ACTIVE){
                // For the active period, and the next ones, withdraw the total reward amount
                withdrawAmount += _questPeriod.rewardAmountPerPeriod;
                _questPeriod.rewardAmountPerPeriod = 0;
            }

            unchecked{ ++i; }
        }

        // If the total amount to emergency withdraw is non_null, execute a transfer
        if(withdrawAmount != 0){
            IERC20(quests[questID].rewardToken).safeTransfer(recipient, withdrawAmount);

            emit EmergencyWithdraw(questID, recipient, withdrawAmount);
        }

    }

    /**
    * @notice Get the fee ratio for a given Quest creator
    * @dev Returns the custom fee ratio for a Quest creator if set, otherwise returns the general fee ratio
    * @param questCreator address of the Quest creator
    * @return uint256 : fee ratio
    */
    function _getFeeRatio(address questCreator) internal view returns(uint256) {
        return customPlatformFeeRatio[questCreator] != 0 ? customPlatformFeeRatio[questCreator] : platformFeeRatio;
    }


    // Manager functions

    /**
    * @notice Gets the amount of rewards to be distributed for the period
    * @dev Gets the amount of rewards to be distributed for the
    * @param questRewardType Rewards type for the Quest
    * @param periodBias Bias of the gauge (reduced if nedded) for the given period
    * @param _questPeriod Data for the Quest Period
    * @return uint256 : Amount to be distributed
    */
    function _getDistributionAmount(
        QuestDataTypes.QuestRewardsType questRewardType,
        uint256 periodBias,
        QuestPeriod memory _questPeriod
    ) internal pure returns(uint256) {
        // Here, if the Gauge Bias is equal or greater than the objective, 
        // set all the period reward to be distributed.
        // If the bias is less, we take that bias, and calculate the amount of rewards based
        // on the rewardPerVote & the Gauge bias

        // If the votes received exceed the max objective of the Quest (for both types)
        // Distribute all the rewards for the period
        if(periodBias >= _questPeriod.maxObjectiveVotes) return _questPeriod.rewardAmountPerPeriod;

        if(questRewardType == QuestDataTypes.QuestRewardsType.FIXED) {
            return (periodBias * _questPeriod.minRewardPerVote) / UNIT;
        } else { // For QuestDataTypes.QuestRewardsType.RANGE
                // If the bias is under the minimum objective, use max reward/vote
            if(periodBias <= _questPeriod.minObjectiveVotes) return (periodBias * _questPeriod.maxRewardPerVote) / UNIT;
            else return _questPeriod.rewardAmountPerPeriod;
        }
    }

    /**
    * @notice Handles the Quest period undistributed rewards
    * @dev Handles the Quest period undistributed rewards based on the Quest Close type
    * @param questID ID of the Quest
    * @param currentPeriod Timestamp of the current period
    * @param questCloseType Close type for the Quest
    * @param rewardToken Address of the reward token
    * @param undistributedAmount Amount of token not distributed for voter rewards
    */
    function _handleUndistributedRewards(
        uint256 questID,
        uint256 currentPeriod,
        QuestDataTypes.QuestCloseType questCloseType,
        address rewardToken,
        uint256 undistributedAmount
    ) internal {
        if(undistributedAmount == 0) return;

        if(questCloseType == QuestDataTypes.QuestCloseType.ROLLOVER) {
            // Since this type is only allowed for FIXED Rewards Quests
            // We simply recalculate the next period reward/vote based on the current Objective
            uint256 nextPeriod = currentPeriod + WEEK;
            // If not the last period
            if(nextPeriod > questPeriods[questID][questPeriods[questID].length - 1]) {
                // This the Quest last period, no period to rollover to
                questWithdrawableAmount[questID] += undistributedAmount;
                return;
            }
            QuestPeriod storage _nextPeriod = periodsByQuest[questID][nextPeriod];

            // Calculate the new period parameters by adding undistributed rewards to the base period reward amount
            // & update the next period parameters based on new calculated parameters
            uint256 newRewardPerPeriod = _nextPeriod.rewardAmountPerPeriod + undistributedAmount;
            uint256 newMinRewardPerVote = (newRewardPerPeriod * UNIT) / _nextPeriod.maxObjectiveVotes;
            uint256 newMaxRewardPerVote = (newRewardPerPeriod * UNIT) / _nextPeriod.minObjectiveVotes;
            _nextPeriod.minRewardPerVote = newMinRewardPerVote;
            _nextPeriod.maxRewardPerVote = newMaxRewardPerVote;
            _nextPeriod.rewardAmountPerPeriod = newRewardPerPeriod;

            emit RewardsRollover(questID, newRewardPerPeriod, newMinRewardPerVote, newMaxRewardPerVote);
            
        } else if(questCloseType == QuestDataTypes.QuestCloseType.DISTRIBUTE) {
            address gauge = quests[questID].gauge;
            if(IGauge(gauge).reward_data(rewardToken).distributor == address(this)) {
                // Give allowance to the Gauge for distribution
                IERC20(rewardToken).safeApprove(gauge, undistributedAmount);
                // The QuestBoard should have given allowance to the Gauge at the Quest creation
                IGauge(gauge).deposit_reward_token(rewardToken, undistributedAmount);
            } else {
                // The Quest Board is not allowed to distribute the rewards, set them to be withdrawable
                questWithdrawableAmount[questID] += undistributedAmount;
            }
        } else { // For QuestDataTypes.QuestCloseType.NORMAL
            questWithdrawableAmount[questID] += undistributedAmount;
        }
    }

    /**
    * @notice Closes the Period, and all QuestPeriods for this period
    * @dev Closes all QuestPeriod for the given period, calculating rewards to distribute & send them to distributor
    * @param period Timestamp of the period
    */
    function _closeQuestPeriod(uint256 period, uint256 questID) internal returns(bool) {
        // We check that this period was not already closed
        if(periodsByQuest[questID][period].currentState != QuestDataTypes.PeriodState.ACTIVE) return false;

        IGaugeController gaugeController = IGaugeController(GAUGE_CONTROLLER);

        Quest memory _quest = quests[questID];
        QuestPeriod storage _questPeriod = periodsByQuest[questID][period];
        _questPeriod.currentState = QuestDataTypes.PeriodState.CLOSED;

        // Call a checkpoint on the Gauge, in case it was not written yet
        gaugeController.checkpoint_gauge(_quest.gauge);

        // Get the bias of the Gauge for the end of the period
        uint256 periodAdjustedBias = BiasCalculator(biasCalculator).getReducedBias(
            period + WEEK,
            questID,
            _quest.gauge,
            _quest.types.voteType
        );

        uint256 undistributedAmount;

        if(periodAdjustedBias == 0) { 
            // Because we don't want to divide by 0 here since the bias is 0, we consider 0% completion
            // => no rewards to be distributed
            // We do not change _questPeriod.rewardAmountDistributed since the default value is already 0
            undistributedAmount = _questPeriod.rewardAmountPerPeriod;
        }
        else{
            // Get the amount of rewards to be distributed
            uint256 distributionAmount = _getDistributionAmount(_quest.types.rewardsType, periodAdjustedBias, _questPeriod);
            _questPeriod.rewardAmountDistributed = distributionAmount;

            // And the rest is set as withdrawable amount, that the Quest creator can retrieve
            undistributedAmount = _questPeriod.rewardAmountPerPeriod - distributionAmount;

            // Send the rewards to be distributed to the Distrubutor
            address questDistributor = questDistributors[questID];
            if(!MultiMerkleDistributor(questDistributor).addQuestPeriod(questID, period, distributionAmount)) revert Errors.DisitributorFail();
            IERC20(_quest.rewardToken).safeTransfer(questDistributor, distributionAmount);
        }

        // Handle the undistributed rewards based on the Quest Close type
        _handleUndistributedRewards(questID, period, _quest.types.closeType, _quest.rewardToken, undistributedAmount);

        emit PeriodClosed(questID, period);

        return true;
    }
 
    /**
    * @notice Closes the Period, and all QuestPeriods for this period
    * @dev Closes all QuestPeriod for the given period, calculating rewards to distribute & send them to distributor
    * @param period Timestamp of the period
    */
    function closeQuestPeriod(uint256 period) external nonReentrant isAlive isInitialized onlyAllowed returns(uint256 closed, uint256 skipped) {
        period = (period / WEEK) * WEEK;
        if(period == 0) revert Errors.InvalidPeriod();
        if(period >= getCurrentPeriod()) revert Errors.PeriodStillActive();
        if(questsByPeriod[period].length == 0) revert Errors.EmptyPeriod();

        uint256[] memory questsForPeriod = questsByPeriod[period];

        // For each QuestPeriod
        uint256 length = questsForPeriod.length;
        for(uint256 i = 0; i < length;){
            bool result = _closeQuestPeriod(period, questsForPeriod[i]);

            if(result) closed++; 
            else skipped++;

            unchecked{ ++i; }
        }
    }

    /**
    * @notice Closes the given QuestPeriods for the Period
    * @dev Closes the given QuestPeriods for the Period, calculating rewards to distribute & send them to distributor
    * @param period Timestamp of the period
    * @param questIDs List of the Quest IDs to close
    */
    function closePartOfQuestPeriod(uint256 period, uint256[] calldata questIDs) external nonReentrant isAlive isInitialized onlyAllowed returns(uint256 closed, uint256 skipped) {
        period = (period / WEEK) * WEEK;
        uint256 questIDLength = questIDs.length;
        if(questIDLength == 0) revert Errors.EmptyArray();
        if(period == 0) revert Errors.InvalidPeriod();
        if(period >= getCurrentPeriod()) revert Errors.PeriodStillActive();
        if(questsByPeriod[period].length == 0) revert Errors.EmptyPeriod();

        // For each QuestPeriod for the given Quest IDs list
        for(uint256 i = 0; i < questIDLength;){
            bool result = _closeQuestPeriod(period, questIDs[i]);

            if(result) closed++; 
            else skipped++;

            unchecked{ ++i; }
        }
    }
   
    /**
    * @dev Sets the QuestPeriod as disitrbuted, and adds the MerkleRoot to the Distributor contract
    * @param questID ID of the Quest
    * @param period Timestamp of the period
    * @param totalAmount sum of all rewards for the Merkle Tree
    * @param merkleRoot MerkleRoot to add
    */
    function _addMerkleRoot(uint256 questID, uint256 period, uint256 totalAmount, bytes32 merkleRoot) internal {
        if(questID >= nextID) revert Errors.InvalidQuestID();
        if(merkleRoot == 0) revert Errors.EmptyMerkleRoot();
        if(totalAmount == 0) revert Errors.NullAmount();

        // This also allows to check if the given period is correct => If not, the currentState is never set to CLOSED for the QuestPeriod
        if(periodsByQuest[questID][period].currentState != QuestDataTypes.PeriodState.CLOSED) revert Errors.PeriodNotClosed();

        // Add the MerkleRoot to the Distributor & set the QuestPeriod as DISTRIBUTED
        if(!MultiMerkleDistributor(questDistributors[questID]).updateQuestPeriod(questID, period, totalAmount, merkleRoot)) revert Errors.DisitributorFail();

        periodsByQuest[questID][period].currentState = QuestDataTypes.PeriodState.DISTRIBUTED;
    }
   
    /**
    * @notice Sets the QuestPeriod as disitrbuted, and adds the MerkleRoot to the Distributor contract
    * @dev internal call to _addMerkleRoot()
    * @param questID ID of the Quest
    * @param period Timestamp of the period
    * @param totalAmount sum of all rewards for the Merkle Tree
    * @param merkleRoot MerkleRoot to add
    */
    function addMerkleRoot(uint256 questID, uint256 period, uint256 totalAmount, bytes32 merkleRoot) external nonReentrant isAlive isInitialized onlyAllowed {
        period = (period / WEEK) * WEEK;
        _addMerkleRoot(questID, period, totalAmount, merkleRoot);
    }

    /**
    * @notice Sets a list of QuestPeriods as disitrbuted, and adds the MerkleRoot to the Distributor contract for each
    * @dev Loop and internal call to _addMerkleRoot()
    * @param questIDs List of Quest IDs
    * @param period Timestamp of the period
    * @param totalAmounts List of sums of all rewards for the Merkle Tree
    * @param merkleRoots List of MerkleRoots to add
    */
    function addMultipleMerkleRoot(
        uint256[] calldata questIDs,
        uint256 period,
        uint256[] calldata totalAmounts,
        bytes32[] calldata merkleRoots
    ) external nonReentrant isAlive isInitialized onlyAllowed {
        period = (period / WEEK) * WEEK;
        uint256 length = questIDs.length;

        if(length != merkleRoots.length) revert Errors.InequalArraySizes();
        if(length != totalAmounts.length) revert Errors.InequalArraySizes();

        for(uint256 i = 0; i < length;){
            _addMerkleRoot(questIDs[i], period, totalAmounts[i], merkleRoots[i]);

            unchecked{ ++i; }
        }
    }
   
    /**
    * @notice Whitelists a reward token
    * @dev Whitelists a reward token
    * @param newToken Address of the reward token
    * @param minRewardPerVote Minimal threshold of reward per vote for the reward token
    */
    function whitelistToken(address newToken, uint256 minRewardPerVote) public onlyAllowed {
        if(newToken == address(0)) revert Errors.AddressZero();
        if(minRewardPerVote == 0) revert Errors.InvalidParameter();

        whitelistedTokens[newToken] = true;

        minRewardPerVotePerToken[newToken] = minRewardPerVote;

        emit WhitelistToken(newToken, minRewardPerVote);
    }
   
    /**
    * @notice Whitelists a list of reward tokens
    * @dev Whitelists a list of reward tokens
    * @param newTokens List of reward tokens addresses
    * @param minRewardPerVotes List of minimal threshold of reward per vote for the reward token
    */
    function whitelistMultipleTokens(address[] calldata newTokens, uint256[] calldata minRewardPerVotes) external onlyAllowed {
        uint256 length = newTokens.length;

        if(length == 0) revert Errors.EmptyArray();
        if(length != minRewardPerVotes.length) revert Errors.InequalArraySizes();

        for(uint256 i = 0; i < length;){
            whitelistToken(newTokens[i], minRewardPerVotes[i]);

            unchecked{ ++i; }
        }
    }
   
    /**
    * @notice Updates a reward token parameters
    * @dev Updates a reward token parameters
    * @param newToken Address of the reward token
    * @param newMinRewardPerVote New minimal threshold of reward per vote for the reward token
    */
    function updateRewardToken(address newToken, uint256 newMinRewardPerVote) external onlyAllowed {
        if(!whitelistedTokens[newToken]) revert Errors.TokenNotWhitelisted();
        if(newMinRewardPerVote == 0) revert Errors.InvalidParameter();

        minRewardPerVotePerToken[newToken] = newMinRewardPerVote;

        emit UpdateRewardToken(newToken, newMinRewardPerVote);
    }


    // Admin functions

   
    /**
    * @notice Approves a new address as manager 
    * @dev Approves a new address as manager
    * @param period Timestamp fo the period to fix
    * @param questID ID of the Quest
    * @param correctReducedBias Currect bias to be used for the Quest period
    */
    /*
        This method is needed for managers to force this contract, in case the reduced bias
        calculated for the Gauge is incorrect for the period to close.
        The following scenario can create a difference between the expected Gauge Bias & the one calculated:
        A voting address, listed in the Blacklist for the given Quest, is already voting for the target Gauge.
        Between the moment where the period is ended (Thursday 00:00 GMT) and the moment the closeQuestPeriod()
        method is called, and does the calculation for the Gauge Bias (and removes the blacklisted voter bias),
        the blacklisted voter changes its vote on the Gauge (by increasing it, or reducing it, or even removing it),
        allowing it to change its last VotedSlope for that Gauge, and causing our system not to account correctly
        for the Bias used for the voting period we are closing
        This method will then allow to send the correct reduced Bias for the Gauge for the given period,
        and calculate the rewards for that period correctly (and do the required reward token transfers)
    */
    function fixQuestPeriodBias(uint256 period, uint256 questID, uint256 correctReducedBias) external nonReentrant isAlive onlyOwner {
        period = (period / WEEK) * WEEK;
        if(questID >= nextID) revert Errors.InvalidQuestID();
        if(distributor == address(0)) revert Errors.NoDistributorSet();
        if(period == 0) revert Errors.InvalidPeriod();
        if(period > getCurrentPeriod()) revert Errors.InvalidPeriod();

        Quest memory _quest = quests[questID];
        QuestPeriod storage _questPeriod = periodsByQuest[questID][period];

        // This also allows to check if the given period is correct => If not, the currentState is never set to CLOSED for the QuestPeriod
        if(_questPeriod.currentState != QuestDataTypes.PeriodState.CLOSED) revert Errors.PeriodNotClosed();

        uint256 previousRewardAmountDistributed = _questPeriod.rewardAmountDistributed;
        uint256 previousWithdrawableAmount = _questPeriod.rewardAmountPerPeriod - previousRewardAmountDistributed;

        address questDistributor = questDistributors[questID];

        if(correctReducedBias == 0) { 
            // Set rewardAmountDistributed back to 0, get all rewards token back to the Board
            _questPeriod.rewardAmountDistributed = 0;

            if(!MultiMerkleDistributor(questDistributor).fixQuestPeriod(questID, period, 0)) revert Errors.DisitributorFail();

            if(_quest.types.closeType == QuestDataTypes.QuestCloseType.NORMAL) {
                questWithdrawableAmount[questID] = questWithdrawableAmount[questID] + _questPeriod.rewardAmountPerPeriod - previousWithdrawableAmount;
            } else {
                _handleUndistributedRewards(questID, period, _quest.types.closeType, _quest.rewardToken, previousRewardAmountDistributed);
            }
        }
        else{
            uint256 newToDistributeAmount = _getDistributionAmount(_quest.types.rewardsType, correctReducedBias, _questPeriod);

            _questPeriod.rewardAmountDistributed = newToDistributeAmount;

            // Fix the Period in the Distributor, and retrieve token in case too much was sent
            if(!MultiMerkleDistributor(questDistributor).fixQuestPeriod(questID, period, newToDistributeAmount)) revert Errors.DisitributorFail();

            if(_quest.types.closeType == QuestDataTypes.QuestCloseType.ROLLOVER) {
                // Since this type is only allowed for FIXED Rewards Quests
                // We simply recalculate the next period reward/vote based on the current Objective
                uint256 nextPeriod = period + WEEK;
                // If not the last period
                if(nextPeriod > questPeriods[questID][questPeriods[questID].length - 1]) {
                    // This the Quest last period, no period to rollover to
                    questWithdrawableAmount[questID] = questWithdrawableAmount[questID] + (_questPeriod.rewardAmountPerPeriod - newToDistributeAmount) - previousWithdrawableAmount;
                } else {
                    QuestPeriod storage _nextPeriod = periodsByQuest[questID][nextPeriod];

                    uint256 newRewardPerPeriod = newToDistributeAmount > previousRewardAmountDistributed ?
                        _nextPeriod.rewardAmountPerPeriod - (newToDistributeAmount - previousRewardAmountDistributed) :
                        _nextPeriod.rewardAmountPerPeriod + (previousRewardAmountDistributed - newToDistributeAmount);
                    uint256 newMinRewardPerVote = (newRewardPerPeriod * UNIT) / _nextPeriod.maxObjectiveVotes;
                    uint256 newMaxRewardPerVote = (newRewardPerPeriod * UNIT) / _nextPeriod.minObjectiveVotes;
                    
                    _nextPeriod.minRewardPerVote = newMinRewardPerVote;
                    _nextPeriod.maxRewardPerVote = newMaxRewardPerVote;
                    _nextPeriod.rewardAmountPerPeriod = newRewardPerPeriod;

                    emit RewardsRollover(questID, newRewardPerPeriod, newMinRewardPerVote, newMaxRewardPerVote);
                }

                if(newToDistributeAmount > previousRewardAmountDistributed){
                    uint256 missingAmount = newToDistributeAmount - previousRewardAmountDistributed;
                    IERC20(_quest.rewardToken).safeTransfer(questDistributor, missingAmount);
                }
                
            } else if(_quest.types.closeType == QuestDataTypes.QuestCloseType.DISTRIBUTE) {
                if(newToDistributeAmount > previousRewardAmountDistributed){
                    uint256 missingAmount = newToDistributeAmount - previousRewardAmountDistributed;

                    // Need to pull it, since it was already sent to the Gauge to be distributed
                    IERC20(_quest.rewardToken).safeTransferFrom(msg.sender, questDistributor, missingAmount);
                } else {
                    // Amount sent back by the Distributor
                    uint256 missingAmount = previousRewardAmountDistributed - newToDistributeAmount;
                    address gauge = _quest.gauge;
                    if(IGauge(gauge).reward_data(_quest.rewardToken).distributor == address(this)) {
                        // Give allowance to the Gauge for distribution
                        IERC20(_quest.rewardToken).safeApprove(gauge, missingAmount);
                        // The QuestBoard should have given allowance to the Gauge at the Quest creation
                        IGauge(gauge).deposit_reward_token(_quest.rewardToken, missingAmount);
                    } else {
                        // The Quest Board is not allowed to distribute the rewards, set them to be withdrawable
                        questWithdrawableAmount[questID] += missingAmount;
                    }
                }
                
            } else { // For QuestDataTypes.QuestCloseType.NORMAL
                questWithdrawableAmount[questID] = questWithdrawableAmount[questID] + (_questPeriod.rewardAmountPerPeriod - newToDistributeAmount) - previousWithdrawableAmount;

                if(newToDistributeAmount > previousRewardAmountDistributed){
                    uint256 missingAmount = newToDistributeAmount - previousRewardAmountDistributed;
                    IERC20(_quest.rewardToken).safeTransfer(questDistributor, missingAmount);
                }
            }
        }

        emit PeriodBiasFixed(period, questID, correctReducedBias);
    }
   
    /**
    * @notice Approves a new address as manager 
    * @dev Approves a new address as manager
    * @param newManager Address to add
    */
    function approveManager(address newManager) external onlyOwner {
        if(newManager == address(0)) revert Errors.AddressZero();
        approvedManagers[newManager] = true;

        emit ApprovedManager(newManager);
    }
   
    /**
    * @notice Removes an address from the managers
    * @dev Removes an address from the managers
    * @param manager Address to remove
    */
    function removeManager(address manager) external onlyOwner {
        if(manager == address(0)) revert Errors.AddressZero();
        approvedManagers[manager] = false;

        emit RemovedManager(manager);
    }
   
    /**
    * @notice Updates the Chest address
    * @dev Updates the Chest address
    * @param chest Address of the new Chest
    */
    function updateChest(address chest) external onlyOwner {
        if(chest == address(0)) revert Errors.AddressZero();
        address oldChest = questChest;
        questChest = chest;

        emit ChestUpdated(oldChest, chest);
    }
   
    /**
    * @notice Updates the Distributor address
    * @dev Updates the Distributor address
    * @param newDistributor Address of the new Distributor
    */
    function updateDistributor(address newDistributor) external onlyOwner {
        if(newDistributor == address(0)) revert Errors.AddressZero();
        address oldDistributor = distributor;
        distributor = newDistributor;

        emit DistributorUpdated(oldDistributor, distributor);
    }
   
    /**
    * @notice Updates the Platfrom fees BPS ratio
    * @dev Updates the Platfrom fees BPS ratio
    * @param newFee New fee ratio
    */
    function updatePlatformFee(uint256 newFee) external onlyOwner {
        if(newFee > 500) revert Errors.InvalidParameter();
        uint256 oldfee = platformFeeRatio;
        platformFeeRatio = newFee;

        emit PlatformFeeRatioUpdated(oldfee, newFee);
    }
   
    /**
    * @notice Updates the min objective value
    * @dev Updates the min objective value
    * @param newMinObjective New min objective
    */
    function updateMinObjective(uint256 newMinObjective) external onlyOwner {
        if(newMinObjective == 0) revert Errors.InvalidParameter();
        uint256 oldMinObjective = objectiveMinimalThreshold;
        objectiveMinimalThreshold = newMinObjective;

        emit MinObjectiveUpdated(oldMinObjective, newMinObjective);
    }
   
    /**
    * @notice Sets a custom fee ratio for a given address
    * @dev Sets a custom fee ratio for a given address
    * @param user User address
    * @param customFeeRatio Custom fee ratio
    */
    function setCustomFeeRatio(address user, uint256 customFeeRatio) external onlyOwner {
        if(customFeeRatio > platformFeeRatio) revert Errors.InvalidParameter();
        
        customPlatformFeeRatio[user] = customFeeRatio;

        emit SetCustomFeeRatio(user, customFeeRatio);
    }
   
    /**
    * @notice Recovers ERC2O tokens sent by mistake to the contract
    * @dev Recovers ERC2O tokens sent by mistake to the contract
    * @param token Address tof the EC2O token
    * @return bool: success
    */
    function recoverERC20(address token) external onlyOwner returns(bool) {
        if(whitelistedTokens[token]) revert Errors.CannotRecoverToken();

        uint256 amount = IERC20(token).balanceOf(address(this));
        if(amount == 0) revert Errors.NullAmount();
        IERC20(token).safeTransfer(owner(), amount);

        return true;
    }
   
    /**
    * @notice Kills the contract
    * @dev Kills the contract
    */
    function killBoard() external onlyOwner {
        if(isKilled) revert Errors.AlreadyKilled();
        isKilled = true;
        killTs = block.timestamp;

        emit Killed(killTs);
    }
   
    /**
    * @notice Unkills the contract
    * @dev Unkills the contract
    */
    function unkillBoard() external onlyOwner {
        if(!isKilled) revert Errors.NotKilled();
        if(block.timestamp >= killTs + KILL_DELAY) revert Errors.KillDelayExpired();
        isKilled = false;

        emit Unkilled(block.timestamp);
    }


    // Utils 

    function safe48(uint n) internal pure returns (uint48) {
        if(n > type(uint48).max) revert Errors.NumberExceed48Bits();
        return uint48(n);
    }

}

File 17 of 17 : Owner.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;

import "../oz/utils/Ownable.sol";
import "../libraries/Errors.sol";

/** @title 2-step Ownership  */
/// @author Paladin
/*
    Extends OZ Ownable contract to add 2-step ownership transfer
*/

contract Owner is Ownable {

    address public pendingOwner;

    event NewPendingOwner(address indexed previousPendingOwner, address indexed newPendingOwner);

    function transferOwnership(address newOwner) public override virtual onlyOwner {
        if(newOwner == address(0)) revert Errors.AddressZero();
        if(newOwner == owner()) revert Errors.CannotBeOwner();
        address oldPendingOwner = pendingOwner;

        pendingOwner = newOwner;

        emit NewPendingOwner(oldPendingOwner, newOwner);
    }

    function acceptOwnership() public virtual {
        if(msg.sender != pendingOwner) revert Errors.CallerNotPendingOwner();
        address newOwner = pendingOwner;
        _transferOwnership(pendingOwner);
        pendingOwner = address(0);

        emit NewPendingOwner(newOwner, address(0));
    }

}

Settings
{
  "optimizer": {
    "enabled": true,
    "runs": 200
  },
  "viaIR": false,
  "outputSelection": {
    "*": {
      "*": [
        "evm.bytecode",
        "evm.deployedBytecode",
        "devdoc",
        "userdoc",
        "metadata",
        "abi"
      ]
    }
  },
  "libraries": {}
}

Contract Security Audit

Contract ABI

[{"inputs":[{"internalType":"address","name":"_gaugeController","type":"address"},{"internalType":"address","name":"_questBoard","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressZero","type":"error"},{"inputs":[],"name":"AlreadyListed","type":"error"},{"inputs":[],"name":"CallerNotAllowed","type":"error"},{"inputs":[],"name":"EmptyArray","type":"error"},{"inputs":[],"name":"InvalidQuestID","type":"error"},{"inputs":[],"name":"MaxListSize","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":true,"internalType":"address","name":"account","type":"address"}],"name":"AddToVoterList","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":true,"internalType":"address","name":"account","type":"address"}],"name":"RemoveFromVoterList","type":"event"},{"inputs":[],"name":"GAUGE_CONTROLLER","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"address[]","name":"accounts","type":"address[]"}],"name":"addToVoterList","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getCurrentPeriod","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"address","name":"gauge","type":"address"},{"internalType":"enum QuestDataTypes.QuestVoteType","name":"questType","type":"uint8"}],"name":"getCurrentReducedBias","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"}],"name":"getQuestVoterList","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"period","type":"uint256"},{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"address","name":"gauge","type":"address"},{"internalType":"enum QuestDataTypes.QuestVoteType","name":"questType","type":"uint8"}],"name":"getReducedBias","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"questBoard","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"address","name":"account","type":"address"}],"name":"removeFromVoterList","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"address[]","name":"accounts","type":"address[]"}],"name":"setQuestVoterList","outputs":[],"stateMutability":"nonpayable","type":"function"}]

Deployed Bytecode

0x608060405234801561001057600080fd5b50600436106100935760003560e01c80632c2a336d116100665780632c2a336d1461012c57806352dedd5914610141578063aa412da814610154578063c456d75614610174578063ff1215361461018757600080fd5b8063086146d21461009857806313122798146100b357806322884e2c146100f257806326e5a82f14610119575b600080fd5b6100a061019a565b6040519081526020015b60405180910390f35b6100da7f000000000000000000000000f13e938d7a1214ae438761941bc0c651405e68a481565b6040516001600160a01b0390911681526020016100aa565b6100da7f0000000000000000000000002f50d538606fa9edd2b11e2446beb18c9d5846bb81565b6100a0610127366004610c88565b6101b9565b61013f61013a366004610cc6565b6101eb565b005b61013f61014f366004610cf6565b6104b4565b610167610162366004610d75565b61068e565b6040516100aa9190610d8e565b61013f610182366004610cf6565b6106f8565b6100a0610195366004610ddb565b6107ff565b600062093a806101aa8142610e39565b6101b49190610e5b565b905090565b60008062093a806101c861019a565b6101d29190610e7a565b90506101e0818686866107ff565b9150505b9392505050565b60008281526001602052604090205460ff1661021a57604051630316876760e51b815260040160405180910390fd5b60405163052de43f60e01b8152600481018390527f000000000000000000000000f13e938d7a1214ae438761941bc0c651405e68a46001600160a01b03169063052de43f90602401602060405180830381865afa15801561027f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102a39190610e93565b6001600160a01b0316336001600160a01b0316146102d45760405163015783e960e51b815260040160405180910390fd5b6001600160a01b0381166102fb57604051639fabe1c160e01b815260040160405180910390fd5b6000828152602081815260408083208054825181850281018501909352808352919290919083018282801561035957602002820191906000526020600020905b81546001600160a01b0316815260019091019060200180831161033b575b505083519394506000925050505b818110156104ad57836001600160a01b031683828151811061038b5761038b610eb0565b60200260200101516001600160a01b0316036104a5576103ac600183610ec6565b811461042857826103be600184610ec6565b815181106103ce576103ce610eb0565b602002602001015160008087815260200190815260200160002082815481106103f9576103f9610eb0565b9060005260206000200160006101000a8154816001600160a01b0302191690836001600160a01b031602179055505b600085815260208190526040902080548061044557610445610ed9565b600082815260208120820160001990810180546001600160a01b03191690559091019091556040516001600160a01b0386169187917f76519268bc5e1607e20b4a832a61ac9f11808dada182b8cff7c37519c819ab0f9190a35050505050565b600101610367565b5050505050565b8060008190036104d75760405163521299a960e01b815260040160405180910390fd5b60008481526001602052604090205460ff1661050657604051630316876760e51b815260040160405180910390fd5b60405163052de43f60e01b8152600481018590527f000000000000000000000000f13e938d7a1214ae438761941bc0c651405e68a46001600160a01b03169063052de43f90602401602060405180830381865afa15801561056b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061058f9190610e93565b6001600160a01b0316336001600160a01b0316146105c05760405163015783e960e51b815260040160405180910390fd5b600084815260208190526040902054600a906105dc9083610e7a565b11156105fb57604051630fb9cd9760e31b815260040160405180910390fd5b60005b818110156104ad57600084848381811061061a5761061a610eb0565b905060200201602081019061062f9190610eef565b6001600160a01b03160361065657604051639fabe1c160e01b815260040160405180910390fd5b6106868585858481811061066c5761066c610eb0565b90506020020160208101906106819190610eef565b6109ae565b6001016105fe565b600081815260208181526040918290208054835181840281018401909452808452606093928301828280156106ec57602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116106ce575b50505050509050919050565b336001600160a01b037f000000000000000000000000f13e938d7a1214ae438761941bc0c651405e68a416146107415760405163015783e960e51b815260040160405180910390fd5b80600a81111561076457604051630fb9cd9760e31b815260040160405180910390fd5b60005b818110156107dd57600084848381811061078357610783610eb0565b90506020020160208101906107989190610eef565b6001600160a01b0316036107bf57604051639fabe1c160e01b815260040160405180910390fd5b6107d58585858481811061066c5761066c610eb0565b600101610767565b505050600091825250600160208190526040909120805460ff19169091179055565b6000838152602081815260408083208054825181850281018501909352808352849383018282801561085a57602002820191906000526020600020905b81546001600160a01b0316815260019091019060200180831161083c575b505083519394507f0000000000000000000000002f50d538606fa9edd2b11e2446beb18c9d5846bb936000935091508290505b818110156108cd576108b9888683815181106108ab576108ab610eb0565b60200260200101518c610ae2565b6108c39084610e7a565b925060010161088d565b5060028660028111156108e2576108e2610f0c565b036108f2575092506109a6915050565b60405163edba527360e01b81526001600160a01b038881166004830152602482018b90526000919085169063edba5273906044016040805180830381865afa158015610942573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109669190610f22565b519050600187600281111561097d5761097d610f0c565b0361099f5780831015610999576109948382610ec6565b61099c565b60005b90505b9450505050505b949350505050565b60008281526020818152604080832080548251818502810185019093528083529192909190830182828015610a0c57602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116109ee575b505083519394506000925050505b81811015610a7557836001600160a01b0316838281518110610a3e57610a3e610eb0565b60200260200101516001600160a01b031603610a6d576040516328f560bb60e21b815260040160405180910390fd5b600101610a1a565b50600084815260208181526040808320805460018101825590845291832090910180546001600160a01b0319166001600160a01b0387169081179091559051909186917f7114dc481e2e6d0e4303b544611f49aceaf0cd1d68d7d80b46e9ade527221b709190a350505050565b6040516303f20c7d60e51b81526001600160a01b03838116600483015284811660248301526000917f0000000000000000000000002f50d538606fa9edd2b11e2446beb18c9d5846bb91839190831690637e418fa090604401602060405180830381865afa158015610b58573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b7c9190610f7f565b6040516301e8cff360e31b81526001600160a01b0387811660048301528881166024830152919250600091841690630f467f9890604401606060405180830381865afa158015610bd0573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610bf49190610f98565b9050848210610c0957600093505050506101e4565b84816040015111610c2057600093505050506101e4565b8051600003610c3557600093505050506101e4565b848160400151610c459190610ec6565b8151610c519190610e5b565b979650505050505050565b6001600160a01b0381168114610c7157600080fd5b50565b803560038110610c8357600080fd5b919050565b600080600060608486031215610c9d57600080fd5b833592506020840135610caf81610c5c565b9150610cbd60408501610c74565b90509250925092565b60008060408385031215610cd957600080fd5b823591506020830135610ceb81610c5c565b809150509250929050565b600080600060408486031215610d0b57600080fd5b83359250602084013567ffffffffffffffff80821115610d2a57600080fd5b818601915086601f830112610d3e57600080fd5b813581811115610d4d57600080fd5b8760208260051b8501011115610d6257600080fd5b6020830194508093505050509250925092565b600060208284031215610d8757600080fd5b5035919050565b6020808252825182820181905260009190848201906040850190845b81811015610dcf5783516001600160a01b031683529284019291840191600101610daa565b50909695505050505050565b60008060008060808587031215610df157600080fd5b84359350602085013592506040850135610e0a81610c5c565b9150610e1860608601610c74565b905092959194509250565b634e487b7160e01b600052601160045260246000fd5b600082610e5657634e487b7160e01b600052601260045260246000fd5b500490565b6000816000190483118215151615610e7557610e75610e23565b500290565b80820180821115610e8d57610e8d610e23565b92915050565b600060208284031215610ea557600080fd5b81516101e481610c5c565b634e487b7160e01b600052603260045260246000fd5b81810381811115610e8d57610e8d610e23565b634e487b7160e01b600052603160045260246000fd5b600060208284031215610f0157600080fd5b81356101e481610c5c565b634e487b7160e01b600052602160045260246000fd5b600060408284031215610f3457600080fd5b6040516040810181811067ffffffffffffffff82111715610f6557634e487b7160e01b600052604160045260246000fd5b604052825181526020928301519281019290925250919050565b600060208284031215610f9157600080fd5b5051919050565b600060608284031215610faa57600080fd5b6040516060810181811067ffffffffffffffff82111715610fdb57634e487b7160e01b600052604160045260246000fd5b8060405250825181526020830151602082015260408301516040820152809150509291505056fea264697066735822122078b983144ae72a4f0a379af2e98dd9e7ab803c116dba32caeb47d0564013526764736f6c63430008100033

Block Transaction Difficulty Gas Used Reward
View All Blocks Produced

Block Uncle Number Difficulty Gas Used Reward
View All Uncles
Loading...
Loading
Loading...
Loading

Validator Index Block Amount
View All Withdrawals

Transaction Hash Block Value Eth2 PubKey Valid
View All Deposits
Loading...
Loading
[ Download: CSV Export  ]

A contract address hosts a smart contract, which is a set of code stored on the blockchain that runs when predetermined conditions are met. Learn more about addresses in our Knowledge Base.