编译器
0.8.16+commit.07a7930e
文件 1 的 17:Address.sol
pragma solidity ^0.8.1;
library Address {
function isContract(address account) internal view returns (bool) {
return account.code.length > 0;
}
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");
}
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
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");
}
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);
}
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
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);
}
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
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);
}
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
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 {
if (returndata.length > 0) {
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
文件 2 的 17:BiasCalculator.sol
pragma solidity 0.8.16;
import "../interfaces/IGaugeController.sol";
import "../libraries/QuestDataTypes.sol";
import "../QuestBoard.sol";
import "../libraries/Errors.sol";
contract BiasCalculator {
uint256 private constant WEEK = 604800;
uint256 private constant MAX_VOTERLIST_SIZE = 10;
address public immutable GAUGE_CONTROLLER;
address public immutable questBoard;
mapping(uint256 => address[]) private questVoterList;
mapping(uint256 => bool) private validQuests;
event AddToVoterList(uint256 indexed questID, address indexed account);
event RemoveFromVoterList(uint256 indexed questID, address indexed account);
modifier onlyBoard(){
if(msg.sender != questBoard) revert Errors.CallerNotAllowed();
_;
}
constructor(address _gaugeController, address _questBoard) {
if(_gaugeController == address(0) || _questBoard == address(0)) revert Errors.AddressZero();
questBoard = _questBoard;
GAUGE_CONTROLLER = _gaugeController;
}
function getCurrentPeriod() public view returns(uint256) {
return (block.timestamp / WEEK) * WEEK;
}
function getQuestVoterList(uint256 questID) external view returns(address[] memory){
return questVoterList[questID];
}
function getCurrentReducedBias(uint256 questID, address gauge, QuestDataTypes.QuestVoteType questType) external view returns(uint256) {
uint256 nextPeriod = getCurrentPeriod() + WEEK;
return getReducedBias(nextPeriod, questID, gauge, questType);
}
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++; }
}
if(questType == QuestDataTypes.QuestVoteType.WHITELIST) return voterListSumBias;
uint256 periodAdjustedBias = gaugeController.points_weight(gauge, period).bias;
if(questType == QuestDataTypes.QuestVoteType.BLACKLIST) {
periodAdjustedBias = voterListSumBias >= periodAdjustedBias ? 0 : periodAdjustedBias - voterListSumBias;
}
return periodAdjustedBias;
}
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);
}
function _addToVoterList(uint256 questID, address account) internal {
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);
}
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;
}
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;
}
}
}
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;
}
}
}
}
文件 3 的 17:Context.sol
pragma solidity ^0.8.0;
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
文件 4 的 17:Errors.sol
pragma solidity 0.8.16;
library 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();
error MerkleRootNotUpdated();
error AlreadyClaimed();
error InvalidProof();
error EmptyMerkleRoot();
error IncorrectRewardAmount();
error MerkleRootFrozen();
error NotFrozen();
error AlreadyFrozen();
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();
error NumberExceed48Bits();
}
文件 5 的 17:IERC20.sol
pragma solidity ^0.8.0;
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
文件 6 的 17:IERC20Permit.sol
pragma solidity ^0.8.0;
interface IERC20Permit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
}
文件 7 的 17:IGauge.sol
pragma solidity 0.8.16;
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;
}
文件 8 的 17:IGaugeController.sol
pragma solidity 0.8.16;
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);
}
文件 9 的 17:IQuestBoard.sol
pragma solidity 0.8.16;
import "../libraries/QuestDataTypes.sol";
interface IQuestBoard {
struct QuestPeriod {
uint256 rewardAmountPerPeriod;
uint256 minRewardPerVote;
uint256 maxRewardPerVote;
uint256 minObjectiveVotes;
uint256 maxObjectiveVotes;
uint256 rewardAmountDistributed;
uint48 periodStart;
QuestDataTypes.PeriodState currentState;
}
struct Quest {
address creator;
address rewardToken;
address gauge;
uint48 duration;
uint48 periodStart;
uint256 totalRewardAmount;
QuestTypes types;
}
struct QuestTypes {
QuestDataTypes.QuestVoteType voteType;
QuestDataTypes.QuestRewardsType rewardsType;
QuestDataTypes.QuestCloseType closeType;
}
struct CreateVars {
address creator;
uint256 rewardPerPeriod;
uint256 minObjective;
uint256 startPeriod;
uint256 periodIterator;
uint256 maxObjective;
}
struct ExtendVars {
uint256 lastPeriod;
address gauge;
address rewardToken;
uint256 rewardPerPeriod;
uint256 periodIterator;
uint256 minObjective;
uint256 maxObjective;
uint256 minRewardPerVote;
uint256 maxRewardPerVote;
}
struct UpdateVars {
uint256 remainingDuration;
uint256 currentPeriod;
uint256 newRewardPerPeriod;
uint256 newMaxObjective;
uint256 newMinObjective;
uint256 periodIterator;
uint256 lastPeriod;
address creator;
}
event Init(address distributor, address biasCalculator);
event NewQuest(
uint256 indexed questID,
address indexed creator,
address indexed gauge,
address rewardToken,
uint48 duration,
uint256 startPeriod
);
event ExtendQuestDuration(uint256 indexed questID, uint256 addedDuration, uint256 addedRewardAmount);
event UpdateQuestParameters(
uint256 indexed questID,
uint256 indexed updatePeriod,
uint256 newMinRewardPerVote,
uint256 newMaxRewardPerVote,
uint256 addedPeriodRewardAmount
);
event WithdrawUnusedRewards(uint256 indexed questID, address recipient, uint256 amount);
event PeriodClosed(uint256 indexed questID, uint256 indexed period);
event RewardsRollover(uint256 indexed questID, uint256 newRewardPeriod, uint256 newMinRewardPerVote, uint256 newMaxRewardPerVote);
event PeriodBiasFixed(uint256 indexed questID, uint256 indexed period, uint256 newBias);
event WhitelistToken(address indexed token, uint256 minRewardPerVote);
event UpdateRewardToken(address indexed token, uint256 newMinRewardPerVote);
event Killed(uint256 killTime);
event Unkilled(uint256 unkillTime);
event EmergencyWithdraw(uint256 indexed questID, address recipient, uint256 amount);
event ApprovedManager(address indexed manager);
event RemovedManager(address indexed manager);
event ChestUpdated(address oldChest, address newChest);
event SetCustomFeeRatio(address indexed creator, uint256 customFeeRatio);
event DistributorUpdated(address oldDistributor, address newDistributor);
event PlatformFeeRatioUpdated(uint256 oldFeeRatio, uint256 newFeeRatio);
event MinObjectiveUpdated(uint256 oldMinObjective, uint256 newMinObjective);
}
文件 10 的 17:MerkleProof.sol
pragma solidity ^0.8.0;
library MerkleProof {
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProofCalldata(proof, leaf) == root;
}
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;
}
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;
}
function multiProofVerify(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProof(proof, proofFlags, leaves) == root;
}
function multiProofVerifyCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProofCalldata(proof, proofFlags, leaves) == root;
}
function processMultiProof(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
uint256 leavesLen = leaves.length;
uint256 totalHashes = proofFlags.length;
require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
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 processMultiProofCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
uint256 leavesLen = leaves.length;
uint256 totalHashes = proofFlags.length;
require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
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) {
assembly {
mstore(0x00, a)
mstore(0x20, b)
value := keccak256(0x00, 0x40)
}
}
}
文件 11 的 17:MultiMerkleDistributor.sol
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";
contract MultiMerkleDistributor is Owner, ReentrancyGuard {
using SafeERC20 for IERC20;
uint256 private constant WEEK = 604800;
mapping(uint256 => address) public questRewardToken;
mapping(address => bool) public rewardTokens;
mapping(uint256 => uint256[]) public questClosedPeriods;
mapping(uint256 => mapping(uint256 => bytes32)) public questMerkleRootPerPeriod;
mapping(uint256 => mapping(uint256 => uint256)) public questRewardsPerPeriod;
mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) private questPeriodClaimedBitMap;
address public immutable questBoard;
event Claimed(
uint256 indexed questID,
uint256 indexed period,
uint256 index,
uint256 amount,
address rewardToken,
address indexed account
);
event NewQuest(uint256 indexed questID, address rewardToken);
event QuestPeriodUpdated(uint256 indexed questID, uint256 indexed period, bytes32 merkleRoot);
modifier onlyAllowed(){
if(msg.sender != questBoard && msg.sender != owner()) revert Errors.CallerNotAllowed();
_;
}
constructor(address _questBoard){
if(_questBoard == address(0)) revert Errors.AddressZero();
questBoard = _questBoard;
}
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;
}
function _setClaimed(uint256 questID, uint256 period, uint256 index) private {
uint256 claimedWordIndex = index >> 8;
uint256 claimedBitIndex = index & 0xff;
questPeriodClaimedBitMap[questID][period][claimedWordIndex] |= (1 << claimedBitIndex);
}
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();
bytes32 node = keccak256(abi.encodePacked(questID, period, index, account, amount));
if(!MerkleProof.verify(merkleProof, questMerkleRootPerPeriod[questID][period], node)) revert Errors.InvalidProof();
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 {
uint256 questID;
uint256 period;
uint256 index;
uint256 amount;
bytes32[] merkleProof;
}
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; }
}
}
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();
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();
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; }
}
IERC20(rewardToken).safeTransfer(account, totalClaimAmount);
}
function getClosedPeriodsByQuests(uint256 questID) external view returns (uint256[] memory) {
return questClosedPeriods[questID];
}
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();
questRewardToken[questID] = token;
if(!rewardTokens[token]) rewardTokens[token] = true;
emit NewQuest(questID, token);
return true;
}
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){
uint256 extraAmount = previousTotalRewardAmount - newTotalRewardAmount;
IERC20(questRewardToken[questID]).safeTransfer(questBoard, extraAmount);
}
return true;
}
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();
questClosedPeriods[questID].push(period);
if(totalAmount != questRewardsPerPeriod[questID][period]) revert Errors.IncorrectRewardAmount();
questMerkleRootPerPeriod[questID][period] = merkleRoot;
emit QuestPeriodUpdated(questID, period, merkleRoot);
return true;
}
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;
}
function emergencyUpdateQuestPeriod(uint256 questID, uint256 period, uint256 addedRewardAmount, bytes32 merkleRoot) external onlyOwner returns(bool) {
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;
}
}
文件 12 的 17:Ownable.sol
pragma solidity ^0.8.0;
import "./Context.sol";
abstract contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
_transferOwnership(_msgSender());
}
modifier onlyOwner() {
_checkOwner();
_;
}
function owner() public view virtual returns (address) {
return _owner;
}
function _checkOwner() internal view virtual {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
}
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
文件 13 的 17:Owner.sol
pragma solidity 0.8.16;
import "../oz/utils/Ownable.sol";
import "../libraries/Errors.sol";
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));
}
}
文件 14 的 17:QuestBoard.sol
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";
contract QuestBoard is IQuestBoard, Owner, ReentrancyGuard {
using SafeERC20 for IERC20;
address public immutable GAUGE_CONTROLLER;
uint256 private constant WEEK = 604800;
uint256 private constant UNIT = 1e18;
uint256 private constant MAX_BPS = 10000;
uint256 private constant KILL_DELAY = 2 * 604800;
uint256 public nextID;
mapping(uint256 => Quest) public quests;
mapping(uint256 => uint48[]) private questPeriods;
mapping(uint256 => mapping(uint256 => QuestPeriod)) public periodsByQuest;
mapping(uint256 => uint256[]) private questsByPeriod;
mapping(address => mapping(uint256 => uint256[])) private questsByGaugeByPeriod;
mapping(uint256 => address) public questDistributors;
mapping(uint256 => uint256) public questWithdrawableAmount;
uint256 public platformFeeRatio = 400;
mapping(address => uint256) public customPlatformFeeRatio;
uint256 public objectiveMinimalThreshold;
address public questChest;
address public distributor;
address public biasCalculator;
mapping(address => bool) private approvedManagers;
mapping(address => bool) public whitelistedTokens;
mapping(address => uint256) public minRewardPerVotePerToken;
bool public isKilled;
uint256 public killTs;
modifier onlyAllowed(){
if(!approvedManagers[msg.sender] && msg.sender != owner()) revert Errors.CallerNotAllowed();
_;
}
modifier isAlive(){
if(isKilled) revert Errors.Killed();
_;
}
modifier isInitialized(){
if(distributor == address(0)) revert Errors.NotInitialized();
_;
}
constructor(address _gaugeController, address _chest){
if(
_gaugeController == address(0)
|| _chest == address(0)
) revert Errors.AddressZero();
GAUGE_CONTROLLER = _gaugeController;
questChest = _chest;
objectiveMinimalThreshold = 1000 * UNIT;
}
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);
}
function getCurrentPeriod() public view returns(uint256) {
return (block.timestamp / WEEK) * WEEK;
}
function getQuestIdsForPeriod(uint256 period) external view returns(uint256[] memory) {
period = (period / WEEK) * WEEK;
return questsByPeriod[period];
}
function getQuestIdsForPeriodForGauge(address gauge, uint256 period) external view returns(uint256[] memory) {
period = (period / WEEK) * WEEK;
return questsByGaugeByPeriod[gauge][period];
}
function getAllPeriodsForQuestId(uint256 questId) external view returns(uint48[] memory) {
return questPeriods[questId];
}
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;
}
function _getRemainingDuration(uint256 questID) internal view returns(uint256) {
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;
}
function getCurrentReducedBias(uint256 questID) external view returns(uint256) {
return BiasCalculator(biasCalculator).getCurrentReducedBias(
questID,
quests[questID].gauge,
quests[questID].types.voteType
);
}
function getQuestCreator(uint256 questID) external view returns(address){
return quests[questID].creator;
}
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) {
QuestTypes memory types = QuestTypes({
voteType: voteType,
rewardsType: QuestDataTypes.QuestRewardsType.FIXED,
closeType: closeType
});
return _createQuest(
gauge,
rewardToken,
types,
startNextPeriod,
duration,
rewardPerVote,
rewardPerVote,
totalRewardAmount,
feeAmount,
voterList
);
}
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) {
QuestTypes memory types = QuestTypes({
voteType: voteType,
rewardsType: QuestDataTypes.QuestRewardsType.RANGE,
closeType: closeType
});
return _createQuest(
gauge,
rewardToken,
types,
startNextPeriod,
duration,
minRewardPerVote,
maxRewardPerVote,
totalRewardAmount,
feeAmount,
voterList
);
}
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) {
CreateVars memory vars;
vars.creator = msg.sender;
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();
vars.rewardPerPeriod = totalRewardAmount / duration;
vars.maxObjective = (vars.rewardPerPeriod * UNIT) / minRewardPerVote;
if(types.rewardsType == QuestDataTypes.QuestRewardsType.RANGE) {
vars.minObjective = (vars.rewardPerPeriod * UNIT) / maxRewardPerVote;
} else {
vars.minObjective = vars.maxObjective;
}
if(vars.minObjective < objectiveMinimalThreshold) revert Errors.ObjectiveTooLow();
IERC20(rewardToken).safeTransferFrom(vars.creator, address(this), totalRewardAmount);
IERC20(rewardToken).safeTransferFrom(vars.creator, questChest, feeAmount);
vars.startPeriod = getCurrentPeriod();
if(startNextPeriod) vars.startPeriod += WEEK;
newQuestID = nextID;
unchecked{ ++nextID; }
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);
questDistributors[newQuestID] = distributor;
vars.periodIterator = vars.startPeriod;
for(uint256 i; i < duration;){
questsByPeriod[vars.periodIterator].push(newQuestID);
questsByGaugeByPeriod[gauge][vars.periodIterator].push(newQuestID);
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) {
BiasCalculator(biasCalculator).setQuestVoterList(newQuestID, voterList);
}
if(types.closeType == QuestDataTypes.QuestCloseType.DISTRIBUTE){
if(IGauge(gauge).reward_data(rewardToken).distributor != address(this)) revert Errors.BoardIsNotAllowedDistributor();
}
if(!MultiMerkleDistributor(distributor).addQuest(newQuestID, rewardToken)) revert Errors.DisitributorFail();
emit NewQuest(
newQuestID,
msg.sender,
gauge,
rewardToken,
duration,
vars.startPeriod
);
}
function extendQuestDuration(
uint256 questID,
uint48 addedDuration,
uint256 addedRewardAmount,
uint256 feeAmount
) external nonReentrant isAlive isInitialized {
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();
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();
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;
IERC20(vars.rewardToken).safeTransferFrom(msg.sender, address(this), addedRewardAmount);
IERC20(vars.rewardToken).safeTransferFrom(msg.sender, questChest, feeAmount);
vars.periodIterator = ((vars.lastPeriod + WEEK) / WEEK) * WEEK;
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;
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);
}
function updateQuestParameters(
uint256 questID,
uint256 newMinRewardPerVote,
uint256 newMaxRewardPerVote,
uint256 addedPeriodRewardAmount,
uint256 addedTotalRewardAmount,
uint256 feeAmount
) external nonReentrant isAlive isInitialized {
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();
vars.remainingDuration = _getRemainingDuration(questID);
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();
vars.currentPeriod = getCurrentPeriod();
if(newMinRewardPerVote < periodsByQuest[questID][vars.currentPeriod].minRewardPerVote) revert Errors.LowerRewardPerVote();
vars.newRewardPerPeriod = periodsByQuest[questID][vars.currentPeriod].rewardAmountPerPeriod + addedPeriodRewardAmount;
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;
IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), addedTotalRewardAmount);
IERC20(rewardToken).safeTransferFrom(msg.sender, questChest, feeAmount);
}
vars.periodIterator = vars.currentPeriod;
vars.lastPeriod = questPeriods[questID][questPeriods[questID].length - 1];
quests[questID].totalRewardAmount += addedTotalRewardAmount;
for(uint256 i; i < vars.remainingDuration;){
if(vars.periodIterator > vars.lastPeriod) break;
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
);
}
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();
uint256 withdrawAmount = questWithdrawableAmount[questID];
questWithdrawableAmount[questID] = 0;
if(withdrawAmount != 0){
address rewardToken = quests[questID].rewardToken;
IERC20(rewardToken).safeTransfer(recipient, withdrawAmount);
emit WithdrawUnusedRewards(questID, recipient, withdrawAmount);
}
}
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();
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]];
if(_questPeriod.currentState == QuestDataTypes.PeriodState.ACTIVE){
withdrawAmount += _questPeriod.rewardAmountPerPeriod;
_questPeriod.rewardAmountPerPeriod = 0;
}
unchecked{ ++i; }
}
if(withdrawAmount != 0){
IERC20(quests[questID].rewardToken).safeTransfer(recipient, withdrawAmount);
emit EmergencyWithdraw(questID, recipient, withdrawAmount);
}
}
function _getFeeRatio(address questCreator) internal view returns(uint256) {
return customPlatformFeeRatio[questCreator] != 0 ? customPlatformFeeRatio[questCreator] : platformFeeRatio;
}
function _getDistributionAmount(
QuestDataTypes.QuestRewardsType questRewardType,
uint256 periodBias,
QuestPeriod memory _questPeriod
) internal pure returns(uint256) {
if(periodBias >= _questPeriod.maxObjectiveVotes) return _questPeriod.rewardAmountPerPeriod;
if(questRewardType == QuestDataTypes.QuestRewardsType.FIXED) {
return (periodBias * _questPeriod.minRewardPerVote) / UNIT;
} else {
if(periodBias <= _questPeriod.minObjectiveVotes) return (periodBias * _questPeriod.maxRewardPerVote) / UNIT;
else return _questPeriod.rewardAmountPerPeriod;
}
}
function _handleUndistributedRewards(
uint256 questID,
uint256 currentPeriod,
QuestDataTypes.QuestCloseType questCloseType,
address rewardToken,
uint256 undistributedAmount
) internal {
if(undistributedAmount == 0) return;
if(questCloseType == QuestDataTypes.QuestCloseType.ROLLOVER) {
uint256 nextPeriod = currentPeriod + WEEK;
if(nextPeriod > questPeriods[questID][questPeriods[questID].length - 1]) {
questWithdrawableAmount[questID] += undistributedAmount;
return;
}
QuestPeriod storage _nextPeriod = periodsByQuest[questID][nextPeriod];
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)) {
IERC20(rewardToken).safeApprove(gauge, undistributedAmount);
IGauge(gauge).deposit_reward_token(rewardToken, undistributedAmount);
} else {
questWithdrawableAmount[questID] += undistributedAmount;
}
} else {
questWithdrawableAmount[questID] += undistributedAmount;
}
}
function _closeQuestPeriod(uint256 period, uint256 questID) internal returns(bool) {
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;
gaugeController.checkpoint_gauge(_quest.gauge);
uint256 periodAdjustedBias = BiasCalculator(biasCalculator).getReducedBias(
period + WEEK,
questID,
_quest.gauge,
_quest.types.voteType
);
uint256 undistributedAmount;
if(periodAdjustedBias == 0) {
undistributedAmount = _questPeriod.rewardAmountPerPeriod;
}
else{
uint256 distributionAmount = _getDistributionAmount(_quest.types.rewardsType, periodAdjustedBias, _questPeriod);
_questPeriod.rewardAmountDistributed = distributionAmount;
undistributedAmount = _questPeriod.rewardAmountPerPeriod - distributionAmount;
address questDistributor = questDistributors[questID];
if(!MultiMerkleDistributor(questDistributor).addQuestPeriod(questID, period, distributionAmount)) revert Errors.DisitributorFail();
IERC20(_quest.rewardToken).safeTransfer(questDistributor, distributionAmount);
}
_handleUndistributedRewards(questID, period, _quest.types.closeType, _quest.rewardToken, undistributedAmount);
emit PeriodClosed(questID, period);
return true;
}
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];
uint256 length = questsForPeriod.length;
for(uint256 i = 0; i < length;){
bool result = _closeQuestPeriod(period, questsForPeriod[i]);
if(result) closed++;
else skipped++;
unchecked{ ++i; }
}
}
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(uint256 i = 0; i < questIDLength;){
bool result = _closeQuestPeriod(period, questIDs[i]);
if(result) closed++;
else skipped++;
unchecked{ ++i; }
}
}
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();
if(periodsByQuest[questID][period].currentState != QuestDataTypes.PeriodState.CLOSED) revert Errors.PeriodNotClosed();
if(!MultiMerkleDistributor(questDistributors[questID]).updateQuestPeriod(questID, period, totalAmount, merkleRoot)) revert Errors.DisitributorFail();
periodsByQuest[questID][period].currentState = QuestDataTypes.PeriodState.DISTRIBUTED;
}
function addMerkleRoot(uint256 questID, uint256 period, uint256 totalAmount, bytes32 merkleRoot) external nonReentrant isAlive isInitialized onlyAllowed {
period = (period / WEEK) * WEEK;
_addMerkleRoot(questID, period, totalAmount, merkleRoot);
}
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; }
}
}
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);
}
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; }
}
}
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);
}
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];
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) {
_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;
if(!MultiMerkleDistributor(questDistributor).fixQuestPeriod(questID, period, newToDistributeAmount)) revert Errors.DisitributorFail();
if(_quest.types.closeType == QuestDataTypes.QuestCloseType.ROLLOVER) {
uint256 nextPeriod = period + WEEK;
if(nextPeriod > questPeriods[questID][questPeriods[questID].length - 1]) {
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;
IERC20(_quest.rewardToken).safeTransferFrom(msg.sender, questDistributor, missingAmount);
} else {
uint256 missingAmount = previousRewardAmountDistributed - newToDistributeAmount;
address gauge = _quest.gauge;
if(IGauge(gauge).reward_data(_quest.rewardToken).distributor == address(this)) {
IERC20(_quest.rewardToken).safeApprove(gauge, missingAmount);
IGauge(gauge).deposit_reward_token(_quest.rewardToken, missingAmount);
} else {
questWithdrawableAmount[questID] += missingAmount;
}
}
} else {
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);
}
function approveManager(address newManager) external onlyOwner {
if(newManager == address(0)) revert Errors.AddressZero();
approvedManagers[newManager] = true;
emit ApprovedManager(newManager);
}
function removeManager(address manager) external onlyOwner {
if(manager == address(0)) revert Errors.AddressZero();
approvedManagers[manager] = false;
emit RemovedManager(manager);
}
function updateChest(address chest) external onlyOwner {
if(chest == address(0)) revert Errors.AddressZero();
address oldChest = questChest;
questChest = chest;
emit ChestUpdated(oldChest, chest);
}
function updateDistributor(address newDistributor) external onlyOwner {
if(newDistributor == address(0)) revert Errors.AddressZero();
address oldDistributor = distributor;
distributor = newDistributor;
emit DistributorUpdated(oldDistributor, distributor);
}
function updatePlatformFee(uint256 newFee) external onlyOwner {
if(newFee > 500) revert Errors.InvalidParameter();
uint256 oldfee = platformFeeRatio;
platformFeeRatio = newFee;
emit PlatformFeeRatioUpdated(oldfee, newFee);
}
function updateMinObjective(uint256 newMinObjective) external onlyOwner {
if(newMinObjective == 0) revert Errors.InvalidParameter();
uint256 oldMinObjective = objectiveMinimalThreshold;
objectiveMinimalThreshold = newMinObjective;
emit MinObjectiveUpdated(oldMinObjective, newMinObjective);
}
function setCustomFeeRatio(address user, uint256 customFeeRatio) external onlyOwner {
if(customFeeRatio > platformFeeRatio) revert Errors.InvalidParameter();
customPlatformFeeRatio[user] = customFeeRatio;
emit SetCustomFeeRatio(user, customFeeRatio);
}
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;
}
function killBoard() external onlyOwner {
if(isKilled) revert Errors.AlreadyKilled();
isKilled = true;
killTs = block.timestamp;
emit Killed(killTs);
}
function unkillBoard() external onlyOwner {
if(!isKilled) revert Errors.NotKilled();
if(block.timestamp >= killTs + KILL_DELAY) revert Errors.KillDelayExpired();
isKilled = false;
emit Unkilled(block.timestamp);
}
function safe48(uint n) internal pure returns (uint48) {
if(n > type(uint48).max) revert Errors.NumberExceed48Bits();
return uint48(n);
}
}
文件 15 的 17:QuestDataTypes.sol
pragma solidity 0.8.16;
library QuestDataTypes {
enum PeriodState { ZERO, ACTIVE, CLOSED, DISTRIBUTED }
enum QuestVoteType { NORMAL, BLACKLIST, WHITELIST }
enum QuestRewardsType { FIXED, RANGE }
enum QuestCloseType { NORMAL, ROLLOVER, DISTRIBUTE }
}
文件 16 的 17:ReentrancyGuard.sol
pragma solidity ^0.8.0;
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
_status = _ENTERED;
}
function _nonReentrantAfter() private {
_status = _NOT_ENTERED;
}
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
}
文件 17 的 17:SafeERC20.sol
pragma solidity ^0.8.0;
import "../interfaces/IERC20.sol";
import "../extensions/IERC20Permit.sol";
import "../utils/Address.sol";
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));
}
function safeApprove(IERC20 token, address spender, uint256 value) internal {
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");
}
function _callOptionalReturn(IERC20 token, bytes memory data) private {
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
{
"compilationTarget": {
"contracts/QuestBoard.sol": "QuestBoard"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_gaugeController","type":"address"},{"internalType":"address","name":"_chest","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressZero","type":"error"},{"inputs":[],"name":"AlreadyInitialized","type":"error"},{"inputs":[],"name":"AlreadyKilled","type":"error"},{"inputs":[],"name":"BoardIsNotAllowedDistributor","type":"error"},{"inputs":[],"name":"CallerNotAllowed","type":"error"},{"inputs":[],"name":"CallerNotPendingOwner","type":"error"},{"inputs":[],"name":"CannotBeOwner","type":"error"},{"inputs":[],"name":"CannotRecoverToken","type":"error"},{"inputs":[],"name":"DisitributorFail","type":"error"},{"inputs":[],"name":"EmptyArray","type":"error"},{"inputs":[],"name":"EmptyMerkleRoot","type":"error"},{"inputs":[],"name":"EmptyPeriod","type":"error"},{"inputs":[],"name":"EmptyQuest","type":"error"},{"inputs":[],"name":"ExpiredQuest","type":"error"},{"inputs":[],"name":"IncorrectAddDuration","type":"error"},{"inputs":[],"name":"IncorrectAddedRewardAmount","type":"error"},{"inputs":[],"name":"IncorrectDuration","type":"error"},{"inputs":[],"name":"IncorrectFeeAmount","type":"error"},{"inputs":[],"name":"InequalArraySizes","type":"error"},{"inputs":[],"name":"InvalidGauge","type":"error"},{"inputs":[],"name":"InvalidParameter","type":"error"},{"inputs":[],"name":"InvalidPeriod","type":"error"},{"inputs":[],"name":"InvalidQuestID","type":"error"},{"inputs":[],"name":"InvalidQuestType","type":"error"},{"inputs":[],"name":"KillDelayExpired","type":"error"},{"inputs":[],"name":"KillDelayNotExpired","type":"error"},{"inputs":[],"name":"Killed","type":"error"},{"inputs":[],"name":"LowerRewardPerVote","type":"error"},{"inputs":[],"name":"MinValueOverMaxValue","type":"error"},{"inputs":[],"name":"NewObjectiveTooLow","type":"error"},{"inputs":[],"name":"NoDistributorSet","type":"error"},{"inputs":[],"name":"NotInitialized","type":"error"},{"inputs":[],"name":"NotKilled","type":"error"},{"inputs":[],"name":"NullAmount","type":"error"},{"inputs":[],"name":"NumberExceed48Bits","type":"error"},{"inputs":[],"name":"ObjectiveTooLow","type":"error"},{"inputs":[],"name":"PeriodNotClosed","type":"error"},{"inputs":[],"name":"PeriodStillActive","type":"error"},{"inputs":[],"name":"QuestNotStarted","type":"error"},{"inputs":[],"name":"RewardPerVoteTooLow","type":"error"},{"inputs":[],"name":"TokenNotWhitelisted","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"manager","type":"address"}],"name":"ApprovedManager","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldChest","type":"address"},{"indexed":false,"internalType":"address","name":"newChest","type":"address"}],"name":"ChestUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldDistributor","type":"address"},{"indexed":false,"internalType":"address","name":"newDistributor","type":"address"}],"name":"DistributorUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"EmergencyWithdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"addedDuration","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"addedRewardAmount","type":"uint256"}],"name":"ExtendQuestDuration","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"distributor","type":"address"},{"indexed":false,"internalType":"address","name":"biasCalculator","type":"address"}],"name":"Init","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"killTime","type":"uint256"}],"name":"Killed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"oldMinObjective","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newMinObjective","type":"uint256"}],"name":"MinObjectiveUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousPendingOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newPendingOwner","type":"address"}],"name":"NewPendingOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":true,"internalType":"address","name":"creator","type":"address"},{"indexed":true,"internalType":"address","name":"gauge","type":"address"},{"indexed":false,"internalType":"address","name":"rewardToken","type":"address"},{"indexed":false,"internalType":"uint48","name":"duration","type":"uint48"},{"indexed":false,"internalType":"uint256","name":"startPeriod","type":"uint256"}],"name":"NewQuest","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"period","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newBias","type":"uint256"}],"name":"PeriodBiasFixed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"period","type":"uint256"}],"name":"PeriodClosed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"oldFeeRatio","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newFeeRatio","type":"uint256"}],"name":"PlatformFeeRatioUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"manager","type":"address"}],"name":"RemovedManager","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newRewardPeriod","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newMinRewardPerVote","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newMaxRewardPerVote","type":"uint256"}],"name":"RewardsRollover","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"creator","type":"address"},{"indexed":false,"internalType":"uint256","name":"customFeeRatio","type":"uint256"}],"name":"SetCustomFeeRatio","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"unkillTime","type":"uint256"}],"name":"Unkilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"updatePeriod","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newMinRewardPerVote","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newMaxRewardPerVote","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"addedPeriodRewardAmount","type":"uint256"}],"name":"UpdateQuestParameters","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"newMinRewardPerVote","type":"uint256"}],"name":"UpdateRewardToken","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"minRewardPerVote","type":"uint256"}],"name":"WhitelistToken","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"questID","type":"uint256"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"WithdrawUnusedRewards","type":"event"},{"inputs":[],"name":"GAUGE_CONTROLLER","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"uint256","name":"period","type":"uint256"},{"internalType":"uint256","name":"totalAmount","type":"uint256"},{"internalType":"bytes32","name":"merkleRoot","type":"bytes32"}],"name":"addMerkleRoot","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"questIDs","type":"uint256[]"},{"internalType":"uint256","name":"period","type":"uint256"},{"internalType":"uint256[]","name":"totalAmounts","type":"uint256[]"},{"internalType":"bytes32[]","name":"merkleRoots","type":"bytes32[]"}],"name":"addMultipleMerkleRoot","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newManager","type":"address"}],"name":"approveManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"biasCalculator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"period","type":"uint256"},{"internalType":"uint256[]","name":"questIDs","type":"uint256[]"}],"name":"closePartOfQuestPeriod","outputs":[{"internalType":"uint256","name":"closed","type":"uint256"},{"internalType":"uint256","name":"skipped","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"period","type":"uint256"}],"name":"closeQuestPeriod","outputs":[{"internalType":"uint256","name":"closed","type":"uint256"},{"internalType":"uint256","name":"skipped","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"gauge","type":"address"},{"internalType":"address","name":"rewardToken","type":"address"},{"internalType":"bool","name":"startNextPeriod","type":"bool"},{"internalType":"uint48","name":"duration","type":"uint48"},{"internalType":"uint256","name":"rewardPerVote","type":"uint256"},{"internalType":"uint256","name":"totalRewardAmount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"enum QuestDataTypes.QuestVoteType","name":"voteType","type":"uint8"},{"internalType":"enum QuestDataTypes.QuestCloseType","name":"closeType","type":"uint8"},{"internalType":"address[]","name":"voterList","type":"address[]"}],"name":"createFixedQuest","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"gauge","type":"address"},{"internalType":"address","name":"rewardToken","type":"address"},{"internalType":"bool","name":"startNextPeriod","type":"bool"},{"internalType":"uint48","name":"duration","type":"uint48"},{"internalType":"uint256","name":"minRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"maxRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"totalRewardAmount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"enum QuestDataTypes.QuestVoteType","name":"voteType","type":"uint8"},{"internalType":"enum QuestDataTypes.QuestCloseType","name":"closeType","type":"uint8"},{"internalType":"address[]","name":"voterList","type":"address[]"}],"name":"createRangedQuest","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"customPlatformFeeRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"distributor","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"emergencyWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"uint48","name":"addedDuration","type":"uint48"},{"internalType":"uint256","name":"addedRewardAmount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"}],"name":"extendQuestDuration","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"period","type":"uint256"},{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"uint256","name":"correctReducedBias","type":"uint256"}],"name":"fixQuestPeriodBias","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"questId","type":"uint256"}],"name":"getAllPeriodsForQuestId","outputs":[{"internalType":"uint48[]","name":"","type":"uint48[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questId","type":"uint256"}],"name":"getAllQuestPeriodsForQuestId","outputs":[{"components":[{"internalType":"uint256","name":"rewardAmountPerPeriod","type":"uint256"},{"internalType":"uint256","name":"minRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"maxRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"minObjectiveVotes","type":"uint256"},{"internalType":"uint256","name":"maxObjectiveVotes","type":"uint256"},{"internalType":"uint256","name":"rewardAmountDistributed","type":"uint256"},{"internalType":"uint48","name":"periodStart","type":"uint48"},{"internalType":"enum QuestDataTypes.PeriodState","name":"currentState","type":"uint8"}],"internalType":"struct IQuestBoard.QuestPeriod[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentPeriod","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"}],"name":"getCurrentReducedBias","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"}],"name":"getQuestCreator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"period","type":"uint256"}],"name":"getQuestIdsForPeriod","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"gauge","type":"address"},{"internalType":"uint256","name":"period","type":"uint256"}],"name":"getQuestIdsForPeriodForGauge","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_distributor","type":"address"},{"internalType":"address","name":"_biasCalculator","type":"address"}],"name":"init","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isKilled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"killBoard","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"killTs","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"minRewardPerVotePerToken","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextID","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"objectiveMinimalThreshold","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pendingOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"periodsByQuest","outputs":[{"internalType":"uint256","name":"rewardAmountPerPeriod","type":"uint256"},{"internalType":"uint256","name":"minRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"maxRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"minObjectiveVotes","type":"uint256"},{"internalType":"uint256","name":"maxObjectiveVotes","type":"uint256"},{"internalType":"uint256","name":"rewardAmountDistributed","type":"uint256"},{"internalType":"uint48","name":"periodStart","type":"uint48"},{"internalType":"enum QuestDataTypes.PeriodState","name":"currentState","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"platformFeeRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"questChest","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"questDistributors","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"questWithdrawableAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"quests","outputs":[{"internalType":"address","name":"creator","type":"address"},{"internalType":"address","name":"rewardToken","type":"address"},{"internalType":"address","name":"gauge","type":"address"},{"internalType":"uint48","name":"duration","type":"uint48"},{"internalType":"uint48","name":"periodStart","type":"uint48"},{"internalType":"uint256","name":"totalRewardAmount","type":"uint256"},{"components":[{"internalType":"enum QuestDataTypes.QuestVoteType","name":"voteType","type":"uint8"},{"internalType":"enum QuestDataTypes.QuestRewardsType","name":"rewardsType","type":"uint8"},{"internalType":"enum QuestDataTypes.QuestCloseType","name":"closeType","type":"uint8"}],"internalType":"struct IQuestBoard.QuestTypes","name":"types","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"recoverERC20","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"manager","type":"address"}],"name":"removeManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"uint256","name":"customFeeRatio","type":"uint256"}],"name":"setCustomFeeRatio","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unkillBoard","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"chest","type":"address"}],"name":"updateChest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newDistributor","type":"address"}],"name":"updateDistributor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newMinObjective","type":"uint256"}],"name":"updateMinObjective","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newFee","type":"uint256"}],"name":"updatePlatformFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"uint256","name":"newMinRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"newMaxRewardPerVote","type":"uint256"},{"internalType":"uint256","name":"addedPeriodRewardAmount","type":"uint256"},{"internalType":"uint256","name":"addedTotalRewardAmount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"}],"name":"updateQuestParameters","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newToken","type":"address"},{"internalType":"uint256","name":"newMinRewardPerVote","type":"uint256"}],"name":"updateRewardToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"newTokens","type":"address[]"},{"internalType":"uint256[]","name":"minRewardPerVotes","type":"uint256[]"}],"name":"whitelistMultipleTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newToken","type":"address"},{"internalType":"uint256","name":"minRewardPerVote","type":"uint256"}],"name":"whitelistToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"whitelistedTokens","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"questID","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"withdrawUnusedRewards","outputs":[],"stateMutability":"nonpayable","type":"function"}]