// File: contracts/sol6/utils/zeppelin/SafeMath.sol
pragma solidity 0.6.6;
/**
* @dev Wrappers over Solidity's arithmetic operations with added overflow
* checks.
*
* Arithmetic operations in Solidity wrap on overflow. This can easily result
* in bugs, because programmers usually assume that an overflow raises an
* error, which is the standard behavior in high level programming languages.
* `SafeMath` restores this intuition by reverting the transaction when an
* operation overflows.
*
* Using this library instead of the unchecked operations eliminates an entire
* class of bugs, so it's recommended to use it always.
*/
library SafeMath {
/**
* @dev Returns the addition of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `+` operator.
*
* Requirements:
* - Addition cannot overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return sub(a, b, "SafeMath: subtraction overflow");
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting with custom message on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b <= a, errorMessage);
uint256 c = a - b;
return c;
}
/**
* @dev Returns the multiplication of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `*` operator.
*
* Requirements:
* - Multiplication cannot overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/**
* @dev Returns the integer division of two unsigned integers. Reverts on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
return div(a, b, "SafeMath: division by zero");
}
/**
* @dev Returns the integer division of two unsigned integers. Reverts with custom message on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0, errorMessage);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* Reverts when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
return mod(a, b, "SafeMath: modulo by zero");
}
/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* Reverts with custom message when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b != 0, errorMessage);
return a % b;
}
/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}
// File: contracts/sol6/Dao/IEpochUtils.sol
pragma solidity 0.6.6;
interface IEpochUtils {
function epochPeriodInSeconds() external view returns (uint256);
function firstEpochStartTimestamp() external view returns (uint256);
function getCurrentEpochNumber() external view returns (uint256);
function getEpochNumber(uint256 timestamp) external view returns (uint256);
}
// File: contracts/sol6/Dao/EpochUtils.sol
pragma solidity 0.6.6;
contract EpochUtils is IEpochUtils {
using SafeMath for uint256;
uint256 public override epochPeriodInSeconds;
uint256 public override firstEpochStartTimestamp;
function getCurrentEpochNumber() public view override returns (uint256) {
return getEpochNumber(now);
}
function getEpochNumber(uint256 timestamp) public view override returns (uint256) {
if (timestamp < firstEpochStartTimestamp || epochPeriodInSeconds == 0) {
return 0;
}
// ((timestamp - firstEpochStartTimestamp) / epochPeriodInSeconds) + 1;
return ((timestamp.sub(firstEpochStartTimestamp)).div(epochPeriodInSeconds)).add(1);
}
}
// File: contracts/sol6/Dao/DaoOperator.sol
pragma solidity 0.6.6;
contract DaoOperator {
address public daoOperator;
constructor(address _daoOperator) public {
require(_daoOperator != address(0), "daoOperator is 0");
daoOperator = _daoOperator;
}
modifier onlyDaoOperator() {
require(msg.sender == daoOperator, "only daoOperator");
_;
}
}
// File: contracts/sol6/IERC20.sol
pragma solidity 0.6.6;
interface IERC20 {
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
function approve(address _spender, uint256 _value) external returns (bool success);
function transfer(address _to, uint256 _value) external returns (bool success);
function transferFrom(
address _from,
address _to,
uint256 _value
) external returns (bool success);
function allowance(address _owner, address _spender) external view returns (uint256 remaining);
function balanceOf(address _owner) external view returns (uint256 balance);
function decimals() external view returns (uint8 digits);
function totalSupply() external view returns (uint256 supply);
}
// to support backward compatible contract name -- so function signature remains same
abstract contract ERC20 is IERC20 {
}
// File: contracts/sol6/utils/zeppelin/ReentrancyGuard.sol
pragma solidity 0.6.6;
/**
* @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].
*/
contract ReentrancyGuard {
bool private _notEntered;
constructor () internal {
// Storing an initial 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 percetange 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.
_notEntered = true;
}
/**
* @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 make it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
// On the first call to nonReentrant, _notEntered will be true
require(_notEntered, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_notEntered = false;
_;
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_notEntered = true;
}
}
// File: contracts/sol6/Dao/IKyberStaking.sol
pragma solidity 0.6.6;
interface IKyberStaking is IEpochUtils {
event Delegated(
address indexed staker,
address indexed representative,
uint256 indexed epoch,
bool isDelegated
);
event Deposited(uint256 curEpoch, address indexed staker, uint256 amount);
event Withdraw(uint256 indexed curEpoch, address indexed staker, uint256 amount);
function initAndReturnStakerDataForCurrentEpoch(address staker)
external
returns (
uint256 stake,
uint256 delegatedStake,
address representative
);
function deposit(uint256 amount) external;
function delegate(address dAddr) external;
function withdraw(uint256 amount) external;
/**
* @notice return combine data (stake, delegatedStake, representative) of a staker
* @dev allow to get staker data up to current epoch + 1
*/
function getStakerData(address staker, uint256 epoch)
external
view
returns (
uint256 stake,
uint256 delegatedStake,
address representative
);
function getLatestStakerData(address staker)
external
view
returns (
uint256 stake,
uint256 delegatedStake,
address representative
);
/**
* @notice return raw data of a staker for an epoch
* WARN: should be used only for initialized data
* if data has not been initialized, it will return all 0
* pool master shouldn't use this function to compute/distribute rewards of pool members
*/
function getStakerRawData(address staker, uint256 epoch)
external
view
returns (
uint256 stake,
uint256 delegatedStake,
address representative
);
}
// File: contracts/sol6/IKyberDao.sol
pragma solidity 0.6.6;
interface IKyberDao is IEpochUtils {
event Voted(address indexed staker, uint indexed epoch, uint indexed campaignID, uint option);
function getLatestNetworkFeeDataWithCache()
external
returns (uint256 feeInBps, uint256 expiryTimestamp);
function getLatestBRRDataWithCache()
external
returns (
uint256 burnInBps,
uint256 rewardInBps,
uint256 rebateInBps,
uint256 epoch,
uint256 expiryTimestamp
);
function handleWithdrawal(address staker, uint256 penaltyAmount) external;
function vote(uint256 campaignID, uint256 option) external;
function getLatestNetworkFeeData()
external
view
returns (uint256 feeInBps, uint256 expiryTimestamp);
function shouldBurnRewardForEpoch(uint256 epoch) external view returns (bool);
/**
* @dev return staker's reward percentage in precision for a past epoch only
* fee handler should call this function when a staker wants to claim reward
* return 0 if staker has no votes or stakes
*/
function getPastEpochRewardPercentageInPrecision(address staker, uint256 epoch)
external
view
returns (uint256);
/**
* @dev return staker's reward percentage in precision for the current epoch
* reward percentage is not finalized until the current epoch is ended
*/
function getCurrentEpochRewardPercentageInPrecision(address staker)
external
view
returns (uint256);
}
// File: contracts/sol6/Dao/KyberStaking.sol
pragma solidity 0.6.6;
/**
* @notice This contract is using SafeMath for uint, which is inherited from EpochUtils
* Some events are moved to interface, easier for public uses
* Staking contract will be deployed by KyberDao's contract
*/
contract KyberStaking is IKyberStaking, EpochUtils, ReentrancyGuard {
struct StakerData {
uint256 stake;
uint256 delegatedStake;
address representative;
}
IERC20 public immutable kncToken;
IKyberDao public immutable kyberDao;
// staker data per epoch, including stake, delegated stake and representative
mapping(uint256 => mapping(address => StakerData)) internal stakerPerEpochData;
// latest data of a staker, including stake, delegated stake, representative
mapping(address => StakerData) internal stakerLatestData;
// true/false: if data has been initialized at an epoch for a staker
mapping(uint256 => mapping(address => bool)) internal hasInited;
// event is fired if something is wrong with withdrawal
// even though the withdrawal is still successful
event WithdrawDataUpdateFailed(uint256 curEpoch, address staker, uint256 amount);
constructor(
IERC20 _kncToken,
uint256 _epochPeriod,
uint256 _startTimestamp,
IKyberDao _kyberDao
) public {
require(_epochPeriod > 0, "ctor: epoch period is 0");
require(_startTimestamp >= now, "ctor: start in the past");
require(_kncToken != IERC20(0), "ctor: kncToken 0");
require(_kyberDao != IKyberDao(0), "ctor: kyberDao 0");
epochPeriodInSeconds = _epochPeriod;
firstEpochStartTimestamp = _startTimestamp;
kncToken = _kncToken;
kyberDao = _kyberDao;
}
/**
* @dev calls to set delegation for msg.sender, will take effect from the next epoch
* @param newRepresentative address to delegate to
*/
function delegate(address newRepresentative) external override {
require(newRepresentative != address(0), "delegate: representative 0");
address staker = msg.sender;
uint256 curEpoch = getCurrentEpochNumber();
initDataIfNeeded(staker, curEpoch);
address curRepresentative = stakerPerEpochData[curEpoch + 1][staker].representative;
// nothing changes here
if (newRepresentative == curRepresentative) {
return;
}
uint256 updatedStake = stakerPerEpochData[curEpoch + 1][staker].stake;
// reduce delegatedStake for curRepresentative if needed
if (curRepresentative != staker) {
initDataIfNeeded(curRepresentative, curEpoch);
stakerPerEpochData[curEpoch + 1][curRepresentative].delegatedStake =
stakerPerEpochData[curEpoch + 1][curRepresentative].delegatedStake.sub(updatedStake);
stakerLatestData[curRepresentative].delegatedStake =
stakerLatestData[curRepresentative].delegatedStake.sub(updatedStake);
emit Delegated(staker, curRepresentative, curEpoch, false);
}
stakerLatestData[staker].representative = newRepresentative;
stakerPerEpochData[curEpoch + 1][staker].representative = newRepresentative;
// ignore if staker is delegating back to himself
if (newRepresentative != staker) {
initDataIfNeeded(newRepresentative, curEpoch);
stakerPerEpochData[curEpoch + 1][newRepresentative].delegatedStake =
stakerPerEpochData[curEpoch + 1][newRepresentative].delegatedStake.add(updatedStake);
stakerLatestData[newRepresentative].delegatedStake =
stakerLatestData[newRepresentative].delegatedStake.add(updatedStake);
emit Delegated(staker, newRepresentative, curEpoch, true);
}
}
/**
* @dev call to stake more KNC for msg.sender
* @param amount amount of KNC to stake
*/
function deposit(uint256 amount) external override {
require(amount > 0, "deposit: amount is 0");
uint256 curEpoch = getCurrentEpochNumber();
address staker = msg.sender;
// collect KNC token from staker
require(
kncToken.transferFrom(staker, address(this), amount),
"deposit: can not get token"
);
initDataIfNeeded(staker, curEpoch);
stakerPerEpochData[curEpoch + 1][staker].stake =
stakerPerEpochData[curEpoch + 1][staker].stake.add(amount);
stakerLatestData[staker].stake =
stakerLatestData[staker].stake.add(amount);
// increase delegated stake for address that staker has delegated to (if it is not staker)
address representative = stakerPerEpochData[curEpoch + 1][staker].representative;
if (representative != staker) {
initDataIfNeeded(representative, curEpoch);
stakerPerEpochData[curEpoch + 1][representative].delegatedStake =
stakerPerEpochData[curEpoch + 1][representative].delegatedStake.add(amount);
stakerLatestData[representative].delegatedStake =
stakerLatestData[representative].delegatedStake.add(amount);
}
emit Deposited(curEpoch, staker, amount);
}
/**
* @dev call to withdraw KNC from staking, it could affect reward when calling KyberDao handleWithdrawal
* @param amount amount of KNC to withdraw
*/
function withdraw(uint256 amount) external override nonReentrant {
require(amount > 0, "withdraw: amount is 0");
uint256 curEpoch = getCurrentEpochNumber();
address staker = msg.sender;
require(
stakerLatestData[staker].stake >= amount,
"withdraw: latest amount staked < withdrawal amount"
);
(bool success, ) = address(this).call(
abi.encodeWithSignature(
"handleWithdrawal(address,uint256,uint256)",
staker,
amount,
curEpoch
)
);
if (!success) {
// Note: should catch this event to check if something went wrong
emit WithdrawDataUpdateFailed(curEpoch, staker, amount);
}
stakerLatestData[staker].stake = stakerLatestData[staker].stake.sub(amount);
// transfer KNC back to staker
require(kncToken.transfer(staker, amount), "withdraw: can not transfer knc");
emit Withdraw(curEpoch, staker, amount);
}
/**
* @dev initialize data if needed, then return staker's data for current epoch
* @dev for safe, only allow calling this func from KyberDao address
* @param staker - staker's address to initialize and get data for
*/
function initAndReturnStakerDataForCurrentEpoch(address staker)
external
override
returns (
uint256 stake,
uint256 delegatedStake,
address representative
)
{
require(
msg.sender == address(kyberDao),
"initAndReturnData: only kyberDao"
);
uint256 curEpoch = getCurrentEpochNumber();
initDataIfNeeded(staker, curEpoch);
StakerData memory stakerData = stakerPerEpochData[curEpoch][staker];
stake = stakerData.stake;
delegatedStake = stakerData.delegatedStake;
representative = stakerData.representative;
}
/**
* @notice return raw data of a staker for an epoch
* WARN: should be used only for initialized data
* if data has not been initialized, it will return all 0
* pool master shouldn't use this function to compute/distribute rewards of pool members
* @dev in KyberDao contract, if staker wants to claim reward for past epoch,
* we must know the staker's data for that epoch
* if the data has not been initialized, it means staker hasn't done any action -> no reward
*/
function getStakerRawData(address staker, uint256 epoch)
external
view
override
returns (
uint256 stake,
uint256 delegatedStake,
address representative
)
{
StakerData memory stakerData = stakerPerEpochData[epoch][staker];
stake = stakerData.stake;
delegatedStake = stakerData.delegatedStake;
representative = stakerData.representative;
}
/**
* @dev allow to get data up to current epoch + 1
*/
function getStake(address staker, uint256 epoch) external view returns (uint256) {
uint256 curEpoch = getCurrentEpochNumber();
if (epoch > curEpoch + 1) {
return 0;
}
uint256 i = epoch;
while (true) {
if (hasInited[i][staker]) {
return stakerPerEpochData[i][staker].stake;
}
if (i == 0) {
break;
}
i--;
}
return 0;
}
/**
* @dev allow to get data up to current epoch + 1
*/
function getDelegatedStake(address staker, uint256 epoch) external view returns (uint256) {
uint256 curEpoch = getCurrentEpochNumber();
if (epoch > curEpoch + 1) {
return 0;
}
uint256 i = epoch;
while (true) {
if (hasInited[i][staker]) {
return stakerPerEpochData[i][staker].delegatedStake;
}
if (i == 0) {
break;
}
i--;
}
return 0;
}
/**
* @dev allow to get data up to current epoch + 1
*/
function getRepresentative(address staker, uint256 epoch) external view returns (address) {
uint256 curEpoch = getCurrentEpochNumber();
if (epoch > curEpoch + 1) {
return address(0);
}
uint256 i = epoch;
while (true) {
if (hasInited[i][staker]) {
return stakerPerEpochData[i][staker].representative;
}
if (i == 0) {
break;
}
i--;
}
// not delegated to anyone, default to yourself
return staker;
}
/**
* @notice return combine data (stake, delegatedStake, representative) of a staker
* @dev allow to get staker data up to current epoch + 1
*/
function getStakerData(address staker, uint256 epoch)
external view override
returns (
uint256 stake,
uint256 delegatedStake,
address representative
)
{
stake = 0;
delegatedStake = 0;
representative = address(0);
uint256 curEpoch = getCurrentEpochNumber();
if (epoch > curEpoch + 1) {
return (stake, delegatedStake, representative);
}
uint256 i = epoch;
while (true) {
if (hasInited[i][staker]) {
stake = stakerPerEpochData[i][staker].stake;
delegatedStake = stakerPerEpochData[i][staker].delegatedStake;
representative = stakerPerEpochData[i][staker].representative;
return (stake, delegatedStake, representative);
}
if (i == 0) {
break;
}
i--;
}
// not delegated to anyone, default to yourself
representative = staker;
}
function getLatestRepresentative(address staker) external view returns (address) {
return
stakerLatestData[staker].representative == address(0)
? staker
: stakerLatestData[staker].representative;
}
function getLatestDelegatedStake(address staker) external view returns (uint256) {
return stakerLatestData[staker].delegatedStake;
}
function getLatestStakeBalance(address staker) external view returns (uint256) {
return stakerLatestData[staker].stake;
}
function getLatestStakerData(address staker)
external view override
returns (
uint256 stake,
uint256 delegatedStake,
address representative
)
{
stake = stakerLatestData[staker].stake;
delegatedStake = stakerLatestData[staker].delegatedStake;
representative = stakerLatestData[staker].representative == address(0)
? staker
: stakerLatestData[staker].representative;
}
/**
* @dev separate logics from withdraw, so staker can withdraw as long as amount <= staker's deposit amount
calling this function from withdraw function, ignore reverting
* @param staker staker that is withdrawing
* @param amount amount to withdraw
* @param curEpoch current epoch
*/
function handleWithdrawal(
address staker,
uint256 amount,
uint256 curEpoch
) external {
require(msg.sender == address(this), "only staking contract");
initDataIfNeeded(staker, curEpoch);
// Note: update latest stake will be done after this function
// update staker's data for next epoch
stakerPerEpochData[curEpoch + 1][staker].stake =
stakerPerEpochData[curEpoch + 1][staker].stake.sub(amount);
address representative = stakerPerEpochData[curEpoch][staker].representative;
uint256 curStake = stakerPerEpochData[curEpoch][staker].stake;
uint256 lStakeBal = stakerLatestData[staker].stake.sub(amount);
uint256 newStake = curStake.min(lStakeBal);
uint256 reduceAmount = curStake.sub(newStake); // newStake is always <= curStake
if (reduceAmount > 0) {
if (representative != staker) {
initDataIfNeeded(representative, curEpoch);
// staker has delegated to representative, withdraw will affect representative's delegated stakes
stakerPerEpochData[curEpoch][representative].delegatedStake =
stakerPerEpochData[curEpoch][representative].delegatedStake.sub(reduceAmount);
}
stakerPerEpochData[curEpoch][staker].stake = newStake;
// call KyberDao to reduce reward, if staker has delegated, then pass his representative
if (address(kyberDao) != address(0)) {
// don't revert if KyberDao revert so data will be updated correctly
(bool success, ) = address(kyberDao).call(
abi.encodeWithSignature(
"handleWithdrawal(address,uint256)",
representative,
reduceAmount
)
);
if (!success) {
emit WithdrawDataUpdateFailed(curEpoch, staker, amount);
}
}
}
representative = stakerPerEpochData[curEpoch + 1][staker].representative;
if (representative != staker) {
initDataIfNeeded(representative, curEpoch);
stakerPerEpochData[curEpoch + 1][representative].delegatedStake =
stakerPerEpochData[curEpoch + 1][representative].delegatedStake.sub(amount);
stakerLatestData[representative].delegatedStake =
stakerLatestData[representative].delegatedStake.sub(amount);
}
}
/**
* @dev initialize data if it has not been initialized yet
* @param staker staker's address to initialize
* @param epoch should be current epoch
*/
function initDataIfNeeded(address staker, uint256 epoch) internal {
address representative = stakerLatestData[staker].representative;
if (representative == address(0)) {
// not delegate to anyone, consider as delegate to yourself
stakerLatestData[staker].representative = staker;
representative = staker;
}
uint256 ldStake = stakerLatestData[staker].delegatedStake;
uint256 lStakeBal = stakerLatestData[staker].stake;
if (!hasInited[epoch][staker]) {
hasInited[epoch][staker] = true;
StakerData storage stakerData = stakerPerEpochData[epoch][staker];
stakerData.representative = representative;
stakerData.delegatedStake = ldStake;
stakerData.stake = lStakeBal;
}
// whenever stakers deposit/withdraw/delegate, the current and next epoch data need to be updated
// as the result, we will also initialize data for staker at the next epoch
if (!hasInited[epoch + 1][staker]) {
hasInited[epoch + 1][staker] = true;
StakerData storage nextEpochStakerData = stakerPerEpochData[epoch + 1][staker];
nextEpochStakerData.representative = representative;
nextEpochStakerData.delegatedStake = ldStake;
nextEpochStakerData.stake = lStakeBal;
}
}
}
// File: contracts/sol6/utils/Utils5.sol
pragma solidity 0.6.6;
/**
* @title Kyber utility file
* mostly shared constants and rate calculation helpers
* inherited by most of kyber contracts.
* previous utils implementations are for previous solidity versions.
*/
contract Utils5 {
IERC20 internal constant ETH_TOKEN_ADDRESS = IERC20(
0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
);
uint256 internal constant PRECISION = (10**18);
uint256 internal constant MAX_QTY = (10**28); // 10B tokens
uint256 internal constant MAX_RATE = (PRECISION * 10**7); // up to 10M tokens per eth
uint256 internal constant MAX_DECIMALS = 18;
uint256 internal constant ETH_DECIMALS = 18;
uint256 constant BPS = 10000; // Basic Price Steps. 1 step = 0.01%
uint256 internal constant MAX_ALLOWANCE = uint256(-1); // token.approve inifinite
mapping(IERC20 => uint256) internal decimals;
function getUpdateDecimals(IERC20 token) internal returns (uint256) {
if (token == ETH_TOKEN_ADDRESS) return ETH_DECIMALS; // save storage access
uint256 tokenDecimals = decimals[token];
// moreover, very possible that old tokens have decimals 0
// these tokens will just have higher gas fees.
if (tokenDecimals == 0) {
tokenDecimals = token.decimals();
decimals[token] = tokenDecimals;
}
return tokenDecimals;
}
function setDecimals(IERC20 token) internal {
if (decimals[token] != 0) return; //already set
if (token == ETH_TOKEN_ADDRESS) {
decimals[token] = ETH_DECIMALS;
} else {
decimals[token] = token.decimals();
}
}
/// @dev get the balance of a user.
/// @param token The token type
/// @return The balance
function getBalance(IERC20 token, address user) internal view returns (uint256) {
if (token == ETH_TOKEN_ADDRESS) {
return user.balance;
} else {
return token.balanceOf(user);
}
}
function getDecimals(IERC20 token) internal view returns (uint256) {
if (token == ETH_TOKEN_ADDRESS) return ETH_DECIMALS; // save storage access
uint256 tokenDecimals = decimals[token];
// moreover, very possible that old tokens have decimals 0
// these tokens will just have higher gas fees.
if (tokenDecimals == 0) return token.decimals();
return tokenDecimals;
}
function calcDestAmount(
IERC20 src,
IERC20 dest,
uint256 srcAmount,
uint256 rate
) internal view returns (uint256) {
return calcDstQty(srcAmount, getDecimals(src), getDecimals(dest), rate);
}
function calcSrcAmount(
IERC20 src,
IERC20 dest,
uint256 destAmount,
uint256 rate
) internal view returns (uint256) {
return calcSrcQty(destAmount, getDecimals(src), getDecimals(dest), rate);
}
function calcDstQty(
uint256 srcQty,
uint256 srcDecimals,
uint256 dstDecimals,
uint256 rate
) internal pure returns (uint256) {
require(srcQty <= MAX_QTY, "srcQty > MAX_QTY");
require(rate <= MAX_RATE, "rate > MAX_RATE");
if (dstDecimals >= srcDecimals) {
require((dstDecimals - srcDecimals) <= MAX_DECIMALS, "dst - src > MAX_DECIMALS");
return (srcQty * rate * (10**(dstDecimals - srcDecimals))) / PRECISION;
} else {
require((srcDecimals - dstDecimals) <= MAX_DECIMALS, "src - dst > MAX_DECIMALS");
return (srcQty * rate) / (PRECISION * (10**(srcDecimals - dstDecimals)));
}
}
function calcSrcQty(
uint256 dstQty,
uint256 srcDecimals,
uint256 dstDecimals,
uint256 rate
) internal pure returns (uint256) {
require(dstQty <= MAX_QTY, "dstQty > MAX_QTY");
require(rate <= MAX_RATE, "rate > MAX_RATE");
//source quantity is rounded up. to avoid dest quantity being too low.
uint256 numerator;
uint256 denominator;
if (srcDecimals >= dstDecimals) {
require((srcDecimals - dstDecimals) <= MAX_DECIMALS, "src - dst > MAX_DECIMALS");
numerator = (PRECISION * dstQty * (10**(srcDecimals - dstDecimals)));
denominator = rate;
} else {
require((dstDecimals - srcDecimals) <= MAX_DECIMALS, "dst - src > MAX_DECIMALS");
numerator = (PRECISION * dstQty);
denominator = (rate * (10**(dstDecimals - srcDecimals)));
}
return (numerator + denominator - 1) / denominator; //avoid rounding down errors
}
function calcRateFromQty(
uint256 srcAmount,
uint256 destAmount,
uint256 srcDecimals,
uint256 dstDecimals
) internal pure returns (uint256) {
require(srcAmount <= MAX_QTY, "srcAmount > MAX_QTY");
require(destAmount <= MAX_QTY, "destAmount > MAX_QTY");
if (dstDecimals >= srcDecimals) {
require((dstDecimals - srcDecimals) <= MAX_DECIMALS, "dst - src > MAX_DECIMALS");
return ((destAmount * PRECISION) / ((10**(dstDecimals - srcDecimals)) * srcAmount));
} else {
require((srcDecimals - dstDecimals) <= MAX_DECIMALS, "src - dst > MAX_DECIMALS");
return ((destAmount * PRECISION * (10**(srcDecimals - dstDecimals))) / srcAmount);
}
}
function minOf(uint256 x, uint256 y) internal pure returns (uint256) {
return x > y ? y : x;
}
}
// File: contracts/sol6/Dao/KyberDao.sol
pragma solidity 0.6.6;
/**
* @notice This contract is using SafeMath for uint, which is inherited from EpochUtils
Some events are moved to interface, easier for public uses
* @dev Network fee campaign: options are fee in bps
* BRR fee handler campaign: options are combined of rebate (left most 128 bits) + reward (right most 128 bits)
* General campaign: options are from 1 to num_options
*/
contract KyberDao is IKyberDao, EpochUtils, ReentrancyGuard, Utils5, DaoOperator {
// max number of campaigns for each epoch
uint256 public constant MAX_EPOCH_CAMPAIGNS = 10;
// max number of options for each campaign
uint256 public constant MAX_CAMPAIGN_OPTIONS = 8;
uint256 internal constant POWER_128 = 2**128;
enum CampaignType {General, NetworkFee, FeeHandlerBRR}
struct FormulaData {
uint256 minPercentageInPrecision;
uint256 cInPrecision;
uint256 tInPrecision;
}
struct CampaignVoteData {
uint256 totalVotes;
uint256[] votePerOption;
}
struct Campaign {
CampaignType campaignType;
bool campaignExists;
uint256 startTimestamp;
uint256 endTimestamp;
uint256 totalKNCSupply; // total KNC supply at the time campaign was created
FormulaData formulaData; // formula params for concluding campaign result
bytes link; // link to KIP, explaination of options, etc.
uint256[] options; // data of options
CampaignVoteData campaignVoteData; // campaign vote data: total votes + vote per option
}
struct BRRData {
uint256 rewardInBps;
uint256 rebateInBps;
}
uint256 public minCampaignDurationInSeconds = 4 days;
IERC20 public immutable kncToken;
IKyberStaking public immutable staking;
// use to generate increasing campaign ID
uint256 public numberCampaigns = 0;
mapping(uint256 => Campaign) internal campaignData;
// epochCampaigns[epoch]: list campaign IDs for an epoch (epoch => campaign IDs)
mapping(uint256 => uint256[]) internal epochCampaigns;
// totalEpochPoints[epoch]: total points for an epoch (epoch => total points)
mapping(uint256 => uint256) internal totalEpochPoints;
// numberVotes[staker][epoch]: number of campaigns that the staker has voted in an epoch
mapping(address => mapping(uint256 => uint256)) public numberVotes;
// stakerVotedOption[staker][campaignID]: staker's voted option ID for a campaign
mapping(address => mapping(uint256 => uint256)) public stakerVotedOption;
uint256 internal latestNetworkFeeResult;
// epoch => campaignID for network fee campaigns
mapping(uint256 => uint256) public networkFeeCampaigns;
// latest BRR data (reward and rebate in bps)
BRRData internal latestBrrData;
// epoch => campaignID for brr campaigns
mapping(uint256 => uint256) public brrCampaigns;
event NewCampaignCreated(
CampaignType campaignType,
uint256 indexed campaignID,
uint256 startTimestamp,
uint256 endTimestamp,
uint256 minPercentageInPrecision,
uint256 cInPrecision,
uint256 tInPrecision,
uint256[] options,
bytes link
);
event CancelledCampaign(uint256 indexed campaignID);
constructor(
uint256 _epochPeriod,
uint256 _startTimestamp,
IERC20 _knc,
uint256 _defaultNetworkFeeBps,
uint256 _defaultRewardBps,
uint256 _defaultRebateBps,
address _daoOperator
) public DaoOperator(_daoOperator) {
require(_epochPeriod > 0, "ctor: epoch period is 0");
require(_startTimestamp >= now, "ctor: start in the past");
require(_knc != IERC20(0), "ctor: knc token 0");
// in Network, maximum fee that can be taken from 1 tx is (platform fee + 2 * network fee)
// so network fee should be less than 50%
require(_defaultNetworkFeeBps < BPS / 2, "ctor: network fee high");
require(_defaultRewardBps.add(_defaultRebateBps) <= BPS, "reward plus rebate high");
epochPeriodInSeconds = _epochPeriod;
firstEpochStartTimestamp = _startTimestamp;
kncToken = _knc;
latestNetworkFeeResult = _defaultNetworkFeeBps;
latestBrrData = BRRData({
rewardInBps: _defaultRewardBps,
rebateInBps: _defaultRebateBps
});
// deploy staking contract
staking = new KyberStaking({
_kncToken: _knc,
_epochPeriod: _epochPeriod,
_startTimestamp: _startTimestamp,
_kyberDao: IKyberDao(this)
});
}
modifier onlyStakingContract {
require(msg.sender == address(staking), "only staking contract");
_;
}
/**
* @dev called by staking contract when staker wanted to withdraw
* @param staker address of staker to reduce reward
* @param reduceAmount amount voting power to be reduced for each campaign staker has voted at this epoch
*/
function handleWithdrawal(address staker, uint256 reduceAmount) external override onlyStakingContract {
// staking shouldn't call this func with reduce amount = 0
if (reduceAmount == 0) {
return;
}
uint256 curEpoch = getCurrentEpochNumber();
uint256 numVotes = numberVotes[staker][curEpoch];
// staker has not participated in any campaigns at the current epoch
if (numVotes == 0) {
return;
}
// update total points for current epoch
totalEpochPoints[curEpoch] = totalEpochPoints[curEpoch].sub(numVotes.mul(reduceAmount));
// update voted count for each campaign staker has voted
uint256[] memory campaignIDs = epochCampaigns[curEpoch];
for (uint256 i = 0; i < campaignIDs.length; i++) {
uint256 campaignID = campaignIDs[i];
uint256 votedOption = stakerVotedOption[staker][campaignID];
if (votedOption == 0) {
continue;
} // staker has not voted yet
Campaign storage campaign = campaignData[campaignID];
if (campaign.endTimestamp >= now) {
// the staker has voted for this campaign and the campaign has not ended yet
// reduce total votes and vote count of staker's voted option
campaign.campaignVoteData.totalVotes =
campaign.campaignVoteData.totalVotes.sub(reduceAmount);
campaign.campaignVoteData.votePerOption[votedOption - 1] =
campaign.campaignVoteData.votePerOption[votedOption - 1].sub(reduceAmount);
}
}
}
/**
* @dev create new campaign, only called by daoOperator
* @param campaignType type of campaign (General, NetworkFee, FeeHandlerBRR)
* @param startTimestamp timestamp to start running the campaign
* @param endTimestamp timestamp to end this campaign
* @param minPercentageInPrecision min percentage (in precision) for formula to conclude campaign
* @param cInPrecision c value (in precision) for formula to conclude campaign
* @param tInPrecision t value (in precision) for formula to conclude campaign
* @param options list values of options to vote for this campaign
* @param link additional data for this campaign
*/
function submitNewCampaign(
CampaignType campaignType,
uint256 startTimestamp,
uint256 endTimestamp,
uint256 minPercentageInPrecision,
uint256 cInPrecision,
uint256 tInPrecision,
uint256[] calldata options,
bytes calldata link
) external onlyDaoOperator returns (uint256 campaignID) {
// campaign epoch could be different from current epoch
// as we allow to create campaign of next epoch as well
uint256 campaignEpoch = getEpochNumber(startTimestamp);
validateCampaignParams(
campaignType,
startTimestamp,
endTimestamp,
minPercentageInPrecision,
cInPrecision,
tInPrecision,
options
);
numberCampaigns = numberCampaigns.add(1);
campaignID = numberCampaigns;
// add campaignID into the list campaign IDs
epochCampaigns[campaignEpoch].push(campaignID);
// update network fee or fee handler brr campaigns
if (campaignType == CampaignType.NetworkFee) {
networkFeeCampaigns[campaignEpoch] = campaignID;
} else if (campaignType == CampaignType.FeeHandlerBRR) {
brrCampaigns[campaignEpoch] = campaignID;
}
FormulaData memory formulaData = FormulaData({
minPercentageInPrecision: minPercentageInPrecision,
cInPrecision: cInPrecision,
tInPrecision: tInPrecision
});
CampaignVoteData memory campaignVoteData = CampaignVoteData({
totalVotes: 0,
votePerOption: new uint256[](options.length)
});
campaignData[campaignID] = Campaign({
campaignExists: true,
campaignType: campaignType,
startTimestamp: startTimestamp,
endTimestamp: endTimestamp,
totalKNCSupply: kncToken.totalSupply(),
link: link,
formulaData: formulaData,
options: options,
campaignVoteData: campaignVoteData
});
emit NewCampaignCreated(
campaignType,
campaignID,
startTimestamp,
endTimestamp,
minPercentageInPrecision,
cInPrecision,
tInPrecision,
options,
link
);
}
/**
* @dev cancel a campaign with given id, called by daoOperator only
* only can cancel campaigns that have not started yet
* @param campaignID id of the campaign to cancel
*/
function cancelCampaign(uint256 campaignID) external onlyDaoOperator {
Campaign storage campaign = campaignData[campaignID];
require(campaign.campaignExists, "cancelCampaign: campaignID doesn't exist");
require(campaign.startTimestamp > now, "cancelCampaign: campaign already started");
uint256 epoch = getEpochNumber(campaign.startTimestamp);
if (campaign.campaignType == CampaignType.NetworkFee) {
delete networkFeeCampaigns[epoch];
} else if (campaign.campaignType == CampaignType.FeeHandlerBRR) {
delete brrCampaigns[epoch];
}
delete campaignData[campaignID];
uint256[] storage campaignIDs = epochCampaigns[epoch];
for (uint256 i = 0; i < campaignIDs.length; i++) {
if (campaignIDs[i] == campaignID) {
// remove this campaign id out of list
campaignIDs[i] = campaignIDs[campaignIDs.length - 1];
campaignIDs.pop();
break;
}
}
emit CancelledCampaign(campaignID);
}
/**
* @dev vote for an option of a campaign
* options are indexed from 1 to number of options
* @param campaignID id of campaign to vote for
* @param option id of options to vote for
*/
function vote(uint256 campaignID, uint256 option) external override {
validateVoteOption(campaignID, option);
address staker = msg.sender;
uint256 curEpoch = getCurrentEpochNumber();
(uint256 stake, uint256 dStake, address representative) =
staking.initAndReturnStakerDataForCurrentEpoch(staker);
uint256 totalStake = representative == staker ? stake.add(dStake) : dStake;
uint256 lastVotedOption = stakerVotedOption[staker][campaignID];
CampaignVoteData storage voteData = campaignData[campaignID].campaignVoteData;
if (lastVotedOption == 0) {
// increase number campaigns that the staker has voted at the current epoch
numberVotes[staker][curEpoch]++;
totalEpochPoints[curEpoch] = totalEpochPoints[curEpoch].add(totalStake);
// increase voted count for this option
voteData.votePerOption[option - 1] =
voteData.votePerOption[option - 1].add(totalStake);
// increase total votes
voteData.totalVotes = voteData.totalVotes.add(totalStake);
} else if (lastVotedOption != option) {
// deduce previous option voted count
voteData.votePerOption[lastVotedOption - 1] =
voteData.votePerOption[lastVotedOption - 1].sub(totalStake);
// increase new option voted count
voteData.votePerOption[option - 1] =
voteData.votePerOption[option - 1].add(totalStake);
}
stakerVotedOption[staker][campaignID] = option;
emit Voted(staker, curEpoch, campaignID, option);
}
/**
* @dev get latest network fee data + expiry timestamp
* conclude network fee campaign if needed and caching latest result in KyberDao
*/
function getLatestNetworkFeeDataWithCache()
external
override
returns (uint256 feeInBps, uint256 expiryTimestamp)
{
(feeInBps, expiryTimestamp) = getLatestNetworkFeeData();
// cache latest data
latestNetworkFeeResult = feeInBps;
}
/**
* @dev return latest burn/reward/rebate data, also affecting epoch + expiry timestamp
* conclude brr campaign if needed and caching latest result in KyberDao
*/
function getLatestBRRDataWithCache()
external
override
returns (
uint256 burnInBps,
uint256 rewardInBps,
uint256 rebateInBps,
uint256 epoch,
uint256 expiryTimestamp
)
{
(burnInBps, rewardInBps, rebateInBps, epoch, expiryTimestamp) = getLatestBRRData();
latestBrrData.rewardInBps = rewardInBps;
latestBrrData.rebateInBps = rebateInBps;
}
/**
* @dev some epochs have reward but no one can claim, for example: epoch 0
* return true if should burn all that reward
* @param epoch epoch to check for burning reward
*/
function shouldBurnRewardForEpoch(uint256 epoch) external view override returns (bool) {
uint256 curEpoch = getCurrentEpochNumber();
if (epoch >= curEpoch) {
return false;
}
return totalEpochPoints[epoch] == 0;
}
// return list campaign ids for epoch, excluding non-existed ones
function getListCampaignIDs(uint256 epoch) external view returns (uint256[] memory campaignIDs) {
campaignIDs = epochCampaigns[epoch];
}
// return total points for an epoch
function getTotalEpochPoints(uint256 epoch) external view returns (uint256) {
return totalEpochPoints[epoch];
}
function getCampaignDetails(uint256 campaignID)
external
view
returns (
CampaignType campaignType,
uint256 startTimestamp,
uint256 endTimestamp,
uint256 totalKNCSupply,
uint256 minPercentageInPrecision,
uint256 cInPrecision,
uint256 tInPrecision,
bytes memory link,
uint256[] memory options
)
{
Campaign storage campaign = campaignData[campaignID];
campaignType = campaign.campaignType;
startTimestamp = campaign.startTimestamp;
endTimestamp = campaign.endTimestamp;
totalKNCSupply = campaign.totalKNCSupply;
minPercentageInPrecision = campaign.formulaData.minPercentageInPrecision;
cInPrecision = campaign.formulaData.cInPrecision;
tInPrecision = campaign.formulaData.tInPrecision;
link = campaign.link;
options = campaign.options;
}
function getCampaignVoteCountData(uint256 campaignID)
external
view
returns (uint256[] memory voteCounts, uint256 totalVoteCount)
{
CampaignVoteData memory voteData = campaignData[campaignID].campaignVoteData;
totalVoteCount = voteData.totalVotes;
voteCounts = voteData.votePerOption;
}
/**
* @dev return staker's reward percentage in precision for a past epoch only
* fee handler should call this function when a staker wants to claim reward
* return 0 if staker has no votes or stakes
*/
function getPastEpochRewardPercentageInPrecision(address staker, uint256 epoch)
external
view
override
returns (uint256)
{
// return 0 if epoch is not past epoch
uint256 curEpoch = getCurrentEpochNumber();
if (epoch >= curEpoch) {
return 0;
}
return getRewardPercentageInPrecision(staker, epoch);
}
/**
* @dev return staker's reward percentage in precision for the current epoch
*/
function getCurrentEpochRewardPercentageInPrecision(address staker)
external
view
override
returns (uint256)
{
uint256 curEpoch = getCurrentEpochNumber();
return getRewardPercentageInPrecision(staker, curEpoch);
}
/**
* @dev return campaign winning option and its value
* return (0, 0) if campaign does not exist
* return (0, 0) if campaign has not ended yet
* return (0, 0) if campaign has no winning option based on the formula
* @param campaignID id of campaign to get result
*/
function getCampaignWinningOptionAndValue(uint256 campaignID)
public
view
returns (uint256 optionID, uint256 value)
{
Campaign storage campaign = campaignData[campaignID];
if (!campaign.campaignExists) {
return (0, 0);
} // not exist
// campaign has not ended yet, return 0 as winning option
if (campaign.endTimestamp > now) {
return (0, 0);
}
uint256 totalSupply = campaign.totalKNCSupply;
// something is wrong here, total KNC supply shouldn't be 0
if (totalSupply == 0) {
return (0, 0);
}
uint256 totalVotes = campaign.campaignVoteData.totalVotes;
uint256[] memory voteCounts = campaign.campaignVoteData.votePerOption;
// Finding option with most votes
uint256 winningOption = 0;
uint256 maxVotedCount = 0;
for (uint256 i = 0; i < voteCounts.length; i++) {
if (voteCounts[i] > maxVotedCount) {
winningOption = i + 1;
maxVotedCount = voteCounts[i];
} else if (voteCounts[i] == maxVotedCount) {
winningOption = 0;
}
}
// more than 1 options have same vote count
if (winningOption == 0) {
return (0, 0);
}
FormulaData memory formulaData = campaign.formulaData;
// compute voted percentage (in precision)
uint256 votedPercentage = totalVotes.mul(PRECISION).div(campaign.totalKNCSupply);
// total voted percentage is below min acceptable percentage, no winning option
if (formulaData.minPercentageInPrecision > votedPercentage) {
return (0, 0);
}
// as we already limit value for c & t, no need to check for overflow here
uint256 x = formulaData.tInPrecision.mul(votedPercentage).div(PRECISION);
if (x <= formulaData.cInPrecision) {
// threshold is not negative, need to compare with voted count
uint256 y = formulaData.cInPrecision.sub(x);
// (most voted option count / total votes) is below threshold, no winining option
if (maxVotedCount.mul(PRECISION) < y.mul(totalVotes)) {
return (0, 0);
}
}
optionID = winningOption;
value = campaign.options[optionID - 1];
}
/**
* @dev return latest network fee and expiry timestamp
*/
function getLatestNetworkFeeData()
public
view
override
returns (uint256 feeInBps, uint256 expiryTimestamp)
{
uint256 curEpoch = getCurrentEpochNumber();
feeInBps = latestNetworkFeeResult;
// expiryTimestamp = firstEpochStartTimestamp + curEpoch * epochPeriodInSeconds - 1;
expiryTimestamp = firstEpochStartTimestamp.add(curEpoch.mul(epochPeriodInSeconds)).sub(1);
if (curEpoch == 0) {
return (feeInBps, expiryTimestamp);
}
uint256 campaignID = networkFeeCampaigns[curEpoch.sub(1)];
if (campaignID == 0) {
// don't have network fee campaign, return latest result
return (feeInBps, expiryTimestamp);
}
uint256 winningOption;
(winningOption, feeInBps) = getCampaignWinningOptionAndValue(campaignID);
if (winningOption == 0) {
// fallback to previous result
feeInBps = latestNetworkFeeResult;
}
return (feeInBps, expiryTimestamp);
}
/**
* @dev return latest brr result, conclude brr campaign if needed
*/
function getLatestBRRData()
public
view
returns (
uint256 burnInBps,
uint256 rewardInBps,
uint256 rebateInBps,
uint256 epoch,
uint256 expiryTimestamp
)
{
epoch = getCurrentEpochNumber();
// expiryTimestamp = firstEpochStartTimestamp + epoch * epochPeriodInSeconds - 1;
expiryTimestamp = firstEpochStartTimestamp.add(epoch.mul(epochPeriodInSeconds)).sub(1);
rewardInBps = latestBrrData.rewardInBps;
rebateInBps = latestBrrData.rebateInBps;
if (epoch > 0) {
uint256 campaignID = brrCampaigns[epoch.sub(1)];
if (campaignID != 0) {
uint256 winningOption;
uint256 brrData;
(winningOption, brrData) = getCampaignWinningOptionAndValue(campaignID);
if (winningOption > 0) {
// has winning option, update reward and rebate value
(rebateInBps, rewardInBps) = getRebateAndRewardFromData(brrData);
}
}
}
burnInBps = BPS.sub(rebateInBps).sub(rewardInBps);
}
// Helper functions for squeezing data
function getRebateAndRewardFromData(uint256 data)
public
pure
returns (uint256 rebateInBps, uint256 rewardInBps)
{
rewardInBps = data & (POWER_128.sub(1));
rebateInBps = (data.div(POWER_128)) & (POWER_128.sub(1));
}
/**
* @dev helper func to get encoded reward and rebate
* revert if validation failed
*/
function getDataFromRewardAndRebateWithValidation(uint256 rewardInBps, uint256 rebateInBps)
public
pure
returns (uint256 data)
{
require(rewardInBps.add(rebateInBps) <= BPS, "reward plus rebate high");
data = (rebateInBps.mul(POWER_128)).add(rewardInBps);
}
/**
* @dev options are indexed from 1
*/
function validateVoteOption(uint256 campaignID, uint256 option) internal view {
Campaign storage campaign = campaignData[campaignID];
require(campaign.campaignExists, "vote: campaign doesn't exist");
require(campaign.startTimestamp <= now, "vote: campaign not started");
require(campaign.endTimestamp >= now, "vote: campaign already ended");
// option is indexed from 1 to options.length
require(option > 0, "vote: option is 0");
require(option <= campaign.options.length, "vote: option is not in range");
}
/**
* @dev Validate params to check if we could submit a new campaign with these params
*/
function validateCampaignParams(
CampaignType campaignType,
uint256 startTimestamp,
uint256 endTimestamp,
uint256 minPercentageInPrecision,
uint256 cInPrecision,
uint256 tInPrecision,
uint256[] memory options
) internal view {
// now <= start timestamp < end timestamp
require(startTimestamp >= now, "validateParams: start in the past");
// campaign duration must be at least min campaign duration
// endTimestamp - startTimestamp + 1 >= minCampaignDurationInSeconds,
require(
endTimestamp.add(1) >= startTimestamp.add(minCampaignDurationInSeconds),
"validateParams: campaign duration is low"
);
uint256 startEpoch = getEpochNumber(startTimestamp);
uint256 endEpoch = getEpochNumber(endTimestamp);
require(
epochCampaigns[startEpoch].length < MAX_EPOCH_CAMPAIGNS,
"validateParams: too many campaigns"
);
// start timestamp and end timestamp must be in the same epoch
require(startEpoch == endEpoch, "validateParams: start & end not same epoch");
uint256 currentEpoch = getCurrentEpochNumber();
require(
startEpoch <= currentEpoch.add(1),
"validateParams: only for current or next epochs"
);
// verify number of options
uint256 numOptions = options.length;
require(
numOptions > 1 && numOptions <= MAX_CAMPAIGN_OPTIONS,
"validateParams: invalid number of options"
);
// Validate option values based on campaign type
if (campaignType == CampaignType.General) {
// option must be positive number
for (uint256 i = 0; i < options.length; i++) {
require(options[i] > 0, "validateParams: general campaign option is 0");
}
} else if (campaignType == CampaignType.NetworkFee) {
require(
networkFeeCampaigns[startEpoch] == 0,
"validateParams: already had network fee campaign for this epoch"
);
// network fee campaign, option must be fee in bps
for (uint256 i = 0; i < options.length; i++) {
// in Network, maximum fee that can be taken from 1 tx is (platform fee + 2 * network fee)
// so network fee should be less than 50%
require(
options[i] < BPS / 2,
"validateParams: network fee must be smaller then BPS / 2"
);
}
} else {
require(
brrCampaigns[startEpoch] == 0,
"validateParams: already had brr campaign for this epoch"
);
// brr fee handler campaign, option must be combined for reward + rebate %
for (uint256 i = 0; i < options.length; i++) {
// rebate (left most 128 bits) + reward (right most 128 bits)
(uint256 rebateInBps, uint256 rewardInBps) =
getRebateAndRewardFromData(options[i]);
require(
rewardInBps.add(rebateInBps) <= BPS,
"validateParams: rebate + reward can't be bigger than BPS"
);
}
}
// percentage should be smaller than or equal 100%
require(minPercentageInPrecision <= PRECISION, "validateParams: min percentage is high");
// limit value of c and t to avoid overflow
require(cInPrecision < POWER_128, "validateParams: c is high");
require(tInPrecision < POWER_128, "validateParams: t is high");
}
/**
* @dev return staker's reward percentage in precision for an epoch
* return 0 if staker has no votes or stakes
* called by 2 functions in KyberDao
*/
function getRewardPercentageInPrecision(address staker, uint256 epoch)
internal
view
returns (uint256)
{
uint256 numVotes = numberVotes[staker][epoch];
// no votes, no rewards
if (numVotes == 0) {
return 0;
}
(uint256 stake, uint256 delegatedStake, address representative) =
staking.getStakerRawData(staker, epoch);
uint256 totalStake = representative == staker ? stake.add(delegatedStake) : delegatedStake;
if (totalStake == 0) {
return 0;
}
uint256 points = numVotes.mul(totalStake);
uint256 totalPts = totalEpochPoints[epoch];
// staker's reward percentage should be <= 100%
assert(points <= totalPts);
return points.mul(PRECISION).div(totalPts);
}
}
{
"compilationTarget": {
"KyberDao.sol": "KyberDao"
},
"evmVersion": "istanbul",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 430
},
"remappings": []
}
[{"inputs":[{"internalType":"uint256","name":"_epochPeriod","type":"uint256"},{"internalType":"uint256","name":"_startTimestamp","type":"uint256"},{"internalType":"contract IERC20","name":"_knc","type":"address"},{"internalType":"uint256","name":"_defaultNetworkFeeBps","type":"uint256"},{"internalType":"uint256","name":"_defaultRewardBps","type":"uint256"},{"internalType":"uint256","name":"_defaultRebateBps","type":"uint256"},{"internalType":"address","name":"_daoOperator","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"campaignID","type":"uint256"}],"name":"CancelledCampaign","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"enum KyberDao.CampaignType","name":"campaignType","type":"uint8"},{"indexed":true,"internalType":"uint256","name":"campaignID","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"startTimestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"endTimestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"minPercentageInPrecision","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"cInPrecision","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tInPrecision","type":"uint256"},{"indexed":false,"internalType":"uint256[]","name":"options","type":"uint256[]"},{"indexed":false,"internalType":"bytes","name":"link","type":"bytes"}],"name":"NewCampaignCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":true,"internalType":"uint256","name":"epoch","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"campaignID","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"option","type":"uint256"}],"name":"Voted","type":"event"},{"inputs":[],"name":"MAX_CAMPAIGN_OPTIONS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_EPOCH_CAMPAIGNS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"brrCampaigns","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"campaignID","type":"uint256"}],"name":"cancelCampaign","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"daoOperator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"epochPeriodInSeconds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"firstEpochStartTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"campaignID","type":"uint256"}],"name":"getCampaignDetails","outputs":[{"internalType":"enum KyberDao.CampaignType","name":"campaignType","type":"uint8"},{"internalType":"uint256","name":"startTimestamp","type":"uint256"},{"internalType":"uint256","name":"endTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalKNCSupply","type":"uint256"},{"internalType":"uint256","name":"minPercentageInPrecision","type":"uint256"},{"internalType":"uint256","name":"cInPrecision","type":"uint256"},{"internalType":"uint256","name":"tInPrecision","type":"uint256"},{"internalType":"bytes","name":"link","type":"bytes"},{"internalType":"uint256[]","name":"options","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"campaignID","type":"uint256"}],"name":"getCampaignVoteCountData","outputs":[{"internalType":"uint256[]","name":"voteCounts","type":"uint256[]"},{"internalType":"uint256","name":"totalVoteCount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"campaignID","type":"uint256"}],"name":"getCampaignWinningOptionAndValue","outputs":[{"internalType":"uint256","name":"optionID","type":"uint256"},{"internalType":"uint256","name":"value","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentEpochNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"staker","type":"address"}],"name":"getCurrentEpochRewardPercentageInPrecision","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"rewardInBps","type":"uint256"},{"internalType":"uint256","name":"rebateInBps","type":"uint256"}],"name":"getDataFromRewardAndRebateWithValidation","outputs":[{"internalType":"uint256","name":"data","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"getEpochNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLatestBRRData","outputs":[{"internalType":"uint256","name":"burnInBps","type":"uint256"},{"internalType":"uint256","name":"rewardInBps","type":"uint256"},{"internalType":"uint256","name":"rebateInBps","type":"uint256"},{"internalType":"uint256","name":"epoch","type":"uint256"},{"internalType":"uint256","name":"expiryTimestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLatestBRRDataWithCache","outputs":[{"internalType":"uint256","name":"burnInBps","type":"uint256"},{"internalType":"uint256","name":"rewardInBps","type":"uint256"},{"internalType":"uint256","name":"rebateInBps","type":"uint256"},{"internalType":"uint256","name":"epoch","type":"uint256"},{"internalType":"uint256","name":"expiryTimestamp","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getLatestNetworkFeeData","outputs":[{"internalType":"uint256","name":"feeInBps","type":"uint256"},{"internalType":"uint256","name":"expiryTimestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLatestNetworkFeeDataWithCache","outputs":[{"internalType":"uint256","name":"feeInBps","type":"uint256"},{"internalType":"uint256","name":"expiryTimestamp","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"epoch","type":"uint256"}],"name":"getListCampaignIDs","outputs":[{"internalType":"uint256[]","name":"campaignIDs","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"uint256","name":"epoch","type":"uint256"}],"name":"getPastEpochRewardPercentageInPrecision","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"data","type":"uint256"}],"name":"getRebateAndRewardFromData","outputs":[{"internalType":"uint256","name":"rebateInBps","type":"uint256"},{"internalType":"uint256","name":"rewardInBps","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"epoch","type":"uint256"}],"name":"getTotalEpochPoints","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"uint256","name":"reduceAmount","type":"uint256"}],"name":"handleWithdrawal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"kncToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minCampaignDurationInSeconds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"networkFeeCampaigns","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"numberCampaigns","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"numberVotes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"epoch","type":"uint256"}],"name":"shouldBurnRewardForEpoch","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"stakerVotedOption","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"staking","outputs":[{"internalType":"contract IKyberStaking","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"enum KyberDao.CampaignType","name":"campaignType","type":"uint8"},{"internalType":"uint256","name":"startTimestamp","type":"uint256"},{"internalType":"uint256","name":"endTimestamp","type":"uint256"},{"internalType":"uint256","name":"minPercentageInPrecision","type":"uint256"},{"internalType":"uint256","name":"cInPrecision","type":"uint256"},{"internalType":"uint256","name":"tInPrecision","type":"uint256"},{"internalType":"uint256[]","name":"options","type":"uint256[]"},{"internalType":"bytes","name":"link","type":"bytes"}],"name":"submitNewCampaign","outputs":[{"internalType":"uint256","name":"campaignID","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"campaignID","type":"uint256"},{"internalType":"uint256","name":"option","type":"uint256"}],"name":"vote","outputs":[],"stateMutability":"nonpayable","type":"function"}]