Documenting fees + rewards now use weighted stake in denominator of cobb douglas
This commit is contained in:
parent
9f1904ad3d
commit
3f2be5b2da
@ -31,6 +31,7 @@ contract StakingProxy is
|
|||||||
constructor(address _stakingContract)
|
constructor(address _stakingContract)
|
||||||
public
|
public
|
||||||
{
|
{
|
||||||
|
owner = msg.sender;
|
||||||
stakingContract = _stakingContract;
|
stakingContract = _stakingContract;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,17 +18,17 @@
|
|||||||
|
|
||||||
pragma solidity ^0.5.5;
|
pragma solidity ^0.5.5;
|
||||||
|
|
||||||
import "@0x/contracts-utils/contracts/src/Authorizable.sol";
|
|
||||||
import "../interfaces/IStakingEvents.sol";
|
import "../interfaces/IStakingEvents.sol";
|
||||||
import "../immutable/MixinConstants.sol";
|
import "../immutable/MixinConstants.sol";
|
||||||
import "../immutable/MixinStorage.sol";
|
import "../immutable/MixinStorage.sol";
|
||||||
|
import "./MixinOwnable.sol";
|
||||||
|
|
||||||
|
|
||||||
contract MixinExchangeManager is
|
contract MixinExchangeManager is
|
||||||
Authorizable,
|
|
||||||
IStakingEvents,
|
IStakingEvents,
|
||||||
MixinConstants,
|
MixinConstants,
|
||||||
MixinStorage
|
MixinStorage,
|
||||||
|
MixinOwnable
|
||||||
{
|
{
|
||||||
|
|
||||||
/// @dev This mixin contains logic for managing exchanges.
|
/// @dev This mixin contains logic for managing exchanges.
|
||||||
@ -73,8 +73,9 @@ contract MixinExchangeManager is
|
|||||||
emit ExchangeRemoved(addr);
|
emit ExchangeRemoved(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @dev Returns true iff the address is a valid exchange
|
/// @dev Returns true iff the address is a valid exchange.
|
||||||
/// @param addr Address of exchange contract
|
/// @param addr Address of exchange contract.
|
||||||
|
/// @return True iff input address is a valid exchange.
|
||||||
function isValidExchangeAddress(address addr)
|
function isValidExchangeAddress(address addr)
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
|
@ -44,6 +44,19 @@ contract MixinFees is
|
|||||||
|
|
||||||
using LibSafeMath for uint256;
|
using LibSafeMath for uint256;
|
||||||
|
|
||||||
|
/// @dev This mixin contains the logic for 0x protocol fees.
|
||||||
|
/// Protocol fees are sent by 0x exchanges every time there is a trade.
|
||||||
|
/// If the maker has associated their address with a pool (see MixinPools.sol), then
|
||||||
|
/// the fee will be attributed to their pool. At the end of an epoch the maker and
|
||||||
|
/// their pool will receive a rebate that is proportional to (i) the fee volume attributed
|
||||||
|
/// to their pool over the epoch, and (ii) the amount of stake provided by the maker and
|
||||||
|
/// their delegators. Note that delegated stake (see MixinStake) is weighted less than
|
||||||
|
/// stake provided by directly by the maker; this is a disincentive for market makers to
|
||||||
|
/// monopolize a single pool that they all delegate to.
|
||||||
|
|
||||||
|
/// @dev Pays a protocol fee in ETH.
|
||||||
|
/// Only a known 0x exchange can call this method. See (MixinExchangeManager).
|
||||||
|
/// @param makerAddress The address of the order's maker
|
||||||
function payProtocolFee(address makerAddress)
|
function payProtocolFee(address makerAddress)
|
||||||
external
|
external
|
||||||
payable
|
payable
|
||||||
@ -54,25 +67,37 @@ contract MixinFees is
|
|||||||
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
|
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
|
||||||
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch._add(amount);
|
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch._add(amount);
|
||||||
if (_feesCollectedThisEpoch == 0) {
|
if (_feesCollectedThisEpoch == 0) {
|
||||||
activePoolIdsThisEpoch.push(poolId);
|
activePoolsThisEpoch.push(poolId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @dev Pays the rebates for to market making pool that was active this epoch,
|
||||||
|
/// then updates the epoch and other time-based periods via the scheduler (see MixinScheduler).
|
||||||
|
/// This is intentionally permissionless, and may be called by anyone.
|
||||||
function finalizeFees()
|
function finalizeFees()
|
||||||
external
|
external
|
||||||
{
|
{
|
||||||
_payRebates();
|
// payout rewards
|
||||||
|
(uint256 totalActivePools,
|
||||||
|
uint256 totalFeesCollected,
|
||||||
|
uint256 totalWeightedStake,
|
||||||
|
uint256 totalRewardsPaid,
|
||||||
|
uint256 initialContractBalance,
|
||||||
|
uint256 finalContractBalance) = _payMakerRewards();
|
||||||
|
emit RewardsPaid(
|
||||||
|
totalActivePools,
|
||||||
|
totalFeesCollected,
|
||||||
|
totalWeightedStake,
|
||||||
|
totalRewardsPaid,
|
||||||
|
initialContractBalance,
|
||||||
|
finalContractBalance
|
||||||
|
);
|
||||||
|
|
||||||
_goToNextEpoch();
|
_goToNextEpoch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProtocolFeesThisEpochByPool(bytes32 poolId)
|
/// @dev Returns the total amount of fees collected thus far, in the current epoch.
|
||||||
public
|
/// @return Amount of fees.
|
||||||
view
|
|
||||||
returns (uint256)
|
|
||||||
{
|
|
||||||
return protocolFeesThisEpochByPool[poolId];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTotalProtocolFeesThisEpoch()
|
function getTotalProtocolFeesThisEpoch()
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
@ -81,78 +106,137 @@ contract MixinFees is
|
|||||||
return address(this).balance;
|
return address(this).balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _payRebates()
|
/// @dev Returns the amount of fees attributed to the input pool.
|
||||||
internal
|
/// @param poolId Pool Id to query.
|
||||||
|
/// @return Amount of fees.
|
||||||
|
function getProtocolFeesThisEpochByPool(bytes32 poolId)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
returns (uint256)
|
||||||
{
|
{
|
||||||
// Step 1 - compute total fees this epoch
|
return protocolFeesThisEpochByPool[poolId];
|
||||||
uint256 numberOfActivePoolIds = activePoolIdsThisEpoch.length;
|
}
|
||||||
IStructs.ActivePool[] memory activePoolIds = new IStructs.ActivePool[](activePoolIdsThisEpoch.length);
|
|
||||||
uint256 totalFees = 0;
|
|
||||||
for (uint i = 0; i != numberOfActivePoolIds; i++) {
|
|
||||||
activePoolIds[i].poolId = activePoolIdsThisEpoch[i];
|
|
||||||
activePoolIds[i].feesCollected = protocolFeesThisEpochByPool[activePoolIds[i].poolId];
|
|
||||||
totalFees = totalFees._add(activePoolIds[i].feesCollected);
|
|
||||||
}
|
|
||||||
uint256 totalRewards = address(this).balance;
|
|
||||||
uint256 totalStake = getActivatedStakeAcrossAllOwners();
|
|
||||||
|
|
||||||
emit EpochFinalized(
|
/// @dev Pays rewards to market making pools that were active this epoch.
|
||||||
numberOfActivePoolIds,
|
/// Each pool receives a portion of the fees generated this epoch (see LibFeeMath) that is
|
||||||
totalRewards,
|
/// proportional to (i) the fee volume attributed to their pool over the epoch, and
|
||||||
0
|
/// (ii) the amount of stake provided by the maker and their delegators. Rebates are paid
|
||||||
);
|
/// into the Reward Vault (see MixinRewardVault) where they can be withdraw by makers and
|
||||||
|
/// the members of their pool. There will be a small amount of ETH leftover in this contract
|
||||||
|
/// after paying out the rebates; at present, this rolls over into the next epoch. Eventually,
|
||||||
|
/// we plan to deposit this leftover into a DAO managed by the 0x community.
|
||||||
|
/// @return totalActivePools Total active pools this epoch.
|
||||||
|
/// @return totalFeesCollected Total fees collected this epoch, across all active pools.
|
||||||
|
/// @return totalWeightedStake Total weighted stake attributed to each pool. Delegated stake is weighted less.
|
||||||
|
/// @return totalRewardsPaid Total rewards paid out across all active pools.
|
||||||
|
/// @return initialContractBalance Balance of this contract before paying rewards.
|
||||||
|
/// @return finalContractBalance Balance of this contract after paying rewards.
|
||||||
|
function _payMakerRewards()
|
||||||
|
private
|
||||||
|
returns (
|
||||||
|
uint256 totalActivePools,
|
||||||
|
uint256 totalFeesCollected,
|
||||||
|
uint256 totalWeightedStake,
|
||||||
|
uint256 totalRewardsPaid,
|
||||||
|
uint256 initialContractBalance,
|
||||||
|
uint256 finalContractBalance
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// initialize return values
|
||||||
|
totalActivePools = activePoolsThisEpoch.length;
|
||||||
|
totalFeesCollected = 0;
|
||||||
|
totalWeightedStake = 0;
|
||||||
|
totalRewardsPaid = 0;
|
||||||
|
initialContractBalance = address(this).balance;
|
||||||
|
finalContractBalance = initialContractBalance;
|
||||||
|
|
||||||
// no rebates available
|
// sanity check - is there a balance to payout and were there any active pools?
|
||||||
// note that there is a case in cobb-douglas where if we weigh either fees or stake at 100%,
|
if (initialContractBalance == 0 || totalActivePools == 0) {
|
||||||
// then the other value doesn't matter. However, it's cheaper on gas to assume that there is some
|
return (
|
||||||
// non-zero split.
|
totalActivePools,
|
||||||
if (totalRewards == 0 || totalFees == 0 || totalStake == 0) {
|
totalFeesCollected,
|
||||||
return;
|
totalWeightedStake,
|
||||||
|
totalRewardsPaid,
|
||||||
|
initialContractBalance,
|
||||||
|
finalContractBalance
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2 - payout
|
// step 1/3 - compute stats for active maker pools
|
||||||
uint256 totalRewardsRecordedInVault = 0;
|
IStructs.ActivePool[] memory activePools = new IStructs.ActivePool[](activePoolsThisEpoch.length);
|
||||||
for (uint i = 0; i != numberOfActivePoolIds; i++) {
|
for (uint i = 0; i != totalActivePools; i++) {
|
||||||
uint256 stakeDelegatedToPool = getStakeDelegatedToPool(activePoolIds[i].poolId);
|
bytes32 poolId = activePoolsThisEpoch[i];
|
||||||
uint256 stakeHeldByPoolOperator = getActivatedAndUndelegatedStake(getPoolOperator(activePoolIds[i].poolId));
|
|
||||||
uint256 scaledStake = stakeHeldByPoolOperator._add(
|
// compute weighted stake
|
||||||
|
uint256 stakeDelegatedToPool = getStakeDelegatedToPool(poolId);
|
||||||
|
uint256 stakeHeldByPoolOperator = getActivatedAndUndelegatedStake(getPoolOperator(poolId));
|
||||||
|
uint256 weightedStake = stakeHeldByPoolOperator._add(
|
||||||
stakeDelegatedToPool
|
stakeDelegatedToPool
|
||||||
._mul(REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE)
|
._mul(REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE)
|
||||||
._div(100)
|
._div(100)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// store pool stats
|
||||||
|
activePools[i].poolId = poolId;
|
||||||
|
activePools[i].feesCollected = protocolFeesThisEpochByPool[poolId];
|
||||||
|
activePools[i].weightedStake = weightedStake;
|
||||||
|
|
||||||
|
// update cumulative amounts
|
||||||
|
totalFeesCollected = totalFeesCollected._add(activePools[i].feesCollected);
|
||||||
|
totalWeightedStake = totalWeightedStake._add(activePools[i].weightedStake);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanity check - this is a gas optimization that can be used because we assume a non-zero
|
||||||
|
// split between stake and fees generated in the cobb-douglas computation (see below).
|
||||||
|
if (totalFeesCollected == 0 || totalWeightedStake == 0) {
|
||||||
|
return (
|
||||||
|
totalActivePools,
|
||||||
|
totalFeesCollected,
|
||||||
|
totalWeightedStake,
|
||||||
|
totalRewardsPaid,
|
||||||
|
initialContractBalance,
|
||||||
|
finalContractBalance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// step 2/3 - record reward for each pool
|
||||||
|
for (uint i = 0; i != totalActivePools; i++) {
|
||||||
|
// compute reward using cobb-douglas formula
|
||||||
uint256 reward = LibFeeMath._cobbDouglasSuperSimplified(
|
uint256 reward = LibFeeMath._cobbDouglasSuperSimplified(
|
||||||
totalRewards,
|
initialContractBalance,
|
||||||
activePoolIds[i].feesCollected,
|
activePools[i].feesCollected,
|
||||||
totalFees,
|
totalFeesCollected,
|
||||||
scaledStake,
|
activePools[i].weightedStake,
|
||||||
totalStake
|
totalWeightedStake
|
||||||
);
|
);
|
||||||
|
|
||||||
// record reward in vault
|
// record reward in vault
|
||||||
_recordDepositInRewardVault(activePoolIds[i].poolId, reward);
|
_recordDepositInRewardVault(activePools[i].poolId, reward);
|
||||||
totalRewardsRecordedInVault = totalRewardsRecordedInVault._add(reward);
|
totalRewardsPaid = totalRewardsPaid._add(reward);
|
||||||
|
|
||||||
// clear state for refunds
|
// clear state for gas refunds
|
||||||
protocolFeesThisEpochByPool[activePoolIds[i].poolId] = 0;
|
protocolFeesThisEpochByPool[activePools[i].poolId] = 0;
|
||||||
activePoolIdsThisEpoch[i] = 0;
|
activePoolsThisEpoch[i] = 0;
|
||||||
}
|
}
|
||||||
activePoolIdsThisEpoch.length = 0;
|
activePoolsThisEpoch.length = 0;
|
||||||
|
|
||||||
// Step 3 send total payout to vault
|
// step 3/3 send total payout to vault
|
||||||
require(
|
require(
|
||||||
totalRewardsRecordedInVault <= totalRewards,
|
totalRewardsPaid <= initialContractBalance,
|
||||||
"MISCALCULATED_REWARDS"
|
"MISCALCULATED_REWARDS"
|
||||||
);
|
);
|
||||||
if (totalRewardsRecordedInVault > 0) {
|
if (totalRewardsPaid > 0) {
|
||||||
_depositIntoRewardVault(totalRewardsRecordedInVault);
|
_depositIntoRewardVault(totalRewardsPaid);
|
||||||
}
|
}
|
||||||
|
finalContractBalance = address(this).balance;
|
||||||
|
|
||||||
// Notify finalization
|
return (
|
||||||
emit EpochFinalized(
|
totalActivePools,
|
||||||
numberOfActivePoolIds,
|
totalFeesCollected,
|
||||||
totalRewards,
|
totalWeightedStake,
|
||||||
totalRewardsRecordedInVault
|
totalRewardsPaid,
|
||||||
|
initialContractBalance,
|
||||||
|
finalContractBalance
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,11 @@ import "../libs/LibSafeMath64.sol";
|
|||||||
import "../immutable/MixinConstants.sol";
|
import "../immutable/MixinConstants.sol";
|
||||||
import "../immutable/MixinStorage.sol";
|
import "../immutable/MixinStorage.sol";
|
||||||
import "../interfaces/IStructs.sol";
|
import "../interfaces/IStructs.sol";
|
||||||
|
import "../interfaces/IStakingEvents.sol";
|
||||||
|
|
||||||
|
|
||||||
contract MixinScheduler is
|
contract MixinScheduler is
|
||||||
|
IStakingEvents,
|
||||||
MixinConstants,
|
MixinConstants,
|
||||||
MixinStorage,
|
MixinStorage,
|
||||||
IMixinScheduler
|
IMixinScheduler
|
||||||
@ -43,6 +45,7 @@ contract MixinScheduler is
|
|||||||
/// and consistent scheduling metric than time. Timelocks, for example, are measured in epochs.
|
/// and consistent scheduling metric than time. Timelocks, for example, are measured in epochs.
|
||||||
|
|
||||||
/// @dev Returns the current epoch.
|
/// @dev Returns the current epoch.
|
||||||
|
/// @return Epoch.
|
||||||
function getCurrentEpoch()
|
function getCurrentEpoch()
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
@ -53,6 +56,7 @@ contract MixinScheduler is
|
|||||||
|
|
||||||
/// @dev Returns the current epoch period, measured in seconds.
|
/// @dev Returns the current epoch period, measured in seconds.
|
||||||
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
|
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
|
||||||
|
/// @return Time in seconds.
|
||||||
function getEpochPeriodInSeconds()
|
function getEpochPeriodInSeconds()
|
||||||
public
|
public
|
||||||
pure
|
pure
|
||||||
@ -63,6 +67,7 @@ contract MixinScheduler is
|
|||||||
|
|
||||||
/// @dev Returns the start time in seconds of the current epoch.
|
/// @dev Returns the start time in seconds of the current epoch.
|
||||||
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
|
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
|
||||||
|
/// @return Time in seconds.
|
||||||
function getCurrentEpochStartTimeInSeconds()
|
function getCurrentEpochStartTimeInSeconds()
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
@ -74,6 +79,7 @@ contract MixinScheduler is
|
|||||||
/// @dev Returns the earliest end time in seconds of this epoch.
|
/// @dev Returns the earliest end time in seconds of this epoch.
|
||||||
/// The next epoch can begin once this time is reached.
|
/// The next epoch can begin once this time is reached.
|
||||||
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
|
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
|
||||||
|
/// @return Time in seconds.
|
||||||
function getCurrentEpochEarliestEndTimeInSeconds()
|
function getCurrentEpochEarliestEndTimeInSeconds()
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
@ -82,7 +88,8 @@ contract MixinScheduler is
|
|||||||
return getCurrentEpochStartTimeInSeconds()._add(getEpochPeriodInSeconds());
|
return getCurrentEpochStartTimeInSeconds()._add(getEpochPeriodInSeconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @dev Returns the current timelock period
|
/// @dev Returns the current timelock period.
|
||||||
|
/// @return Timelock period.
|
||||||
function getCurrentTimelockPeriod()
|
function getCurrentTimelockPeriod()
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
@ -93,6 +100,7 @@ contract MixinScheduler is
|
|||||||
|
|
||||||
/// @dev Returns the length of a timelock period, measured in epochs.
|
/// @dev Returns the length of a timelock period, measured in epochs.
|
||||||
/// Timelock period = [startEpoch..endEpoch)
|
/// Timelock period = [startEpoch..endEpoch)
|
||||||
|
/// @return Timelock period end.
|
||||||
function getTimelockPeriodInEpochs()
|
function getTimelockPeriodInEpochs()
|
||||||
public
|
public
|
||||||
pure
|
pure
|
||||||
@ -103,6 +111,7 @@ contract MixinScheduler is
|
|||||||
|
|
||||||
/// @dev Returns the epoch that the current timelock period started at.
|
/// @dev Returns the epoch that the current timelock period started at.
|
||||||
/// Timelock period = [startEpoch..endEpoch)
|
/// Timelock period = [startEpoch..endEpoch)
|
||||||
|
/// @return Timelock period start.
|
||||||
function getCurrentTimelockPeriodStartEpoch()
|
function getCurrentTimelockPeriodStartEpoch()
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
@ -113,6 +122,7 @@ contract MixinScheduler is
|
|||||||
|
|
||||||
/// @dev Returns the epoch that the current timelock period will end.
|
/// @dev Returns the epoch that the current timelock period will end.
|
||||||
/// Timelock period = [startEpoch..endEpoch)
|
/// Timelock period = [startEpoch..endEpoch)
|
||||||
|
/// @return Timelock period.
|
||||||
function getCurrentTimelockPeriodEndEpoch()
|
function getCurrentTimelockPeriodEndEpoch()
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
@ -128,7 +138,6 @@ contract MixinScheduler is
|
|||||||
internal
|
internal
|
||||||
{
|
{
|
||||||
// get current timestamp
|
// get current timestamp
|
||||||
// solium-disable security/no-block-members
|
|
||||||
// solhint-disable-next-line not-rely-on-time
|
// solhint-disable-next-line not-rely-on-time
|
||||||
uint64 currentBlockTimestamp = block.timestamp._downcastToUint64();
|
uint64 currentBlockTimestamp = block.timestamp._downcastToUint64();
|
||||||
|
|
||||||
@ -142,11 +151,27 @@ contract MixinScheduler is
|
|||||||
uint64 nextEpoch = currentEpoch._add(1);
|
uint64 nextEpoch = currentEpoch._add(1);
|
||||||
currentEpoch = nextEpoch;
|
currentEpoch = nextEpoch;
|
||||||
currentEpochStartTimeInSeconds = currentBlockTimestamp;
|
currentEpochStartTimeInSeconds = currentBlockTimestamp;
|
||||||
|
uint64 earliestEndTimeInSeconds = currentEpochStartTimeInSeconds._add(getEpochPeriodInSeconds());
|
||||||
|
|
||||||
|
// notify of epoch change
|
||||||
|
emit EpochChanged(
|
||||||
|
currentEpoch,
|
||||||
|
currentEpochStartTimeInSeconds,
|
||||||
|
earliestEndTimeInSeconds
|
||||||
|
);
|
||||||
|
|
||||||
// increment timelock period, if needed
|
// increment timelock period, if needed
|
||||||
if (getCurrentTimelockPeriodEndEpoch() <= nextEpoch) {
|
if (getCurrentTimelockPeriodEndEpoch() <= nextEpoch) {
|
||||||
currentTimelockPeriod = currentTimelockPeriod._add(1);
|
currentTimelockPeriod = currentTimelockPeriod._add(1);
|
||||||
currentTimelockPeriodStartEpoch = currentEpoch;
|
currentTimelockPeriodStartEpoch = currentEpoch;
|
||||||
|
uint64 endEpoch = currentEpoch._add(getTimelockPeriodInEpochs());
|
||||||
|
|
||||||
|
// notify
|
||||||
|
emit TimelockPeriodChanged(
|
||||||
|
currentTimelockPeriod,
|
||||||
|
currentTimelockPeriodStartEpoch,
|
||||||
|
endEpoch
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ contract IMixinScheduler {
|
|||||||
/// Epochs serve as the basis for all other time intervals, which provides a more stable
|
/// Epochs serve as the basis for all other time intervals, which provides a more stable
|
||||||
/// and consistent scheduling metric than time. Timelocks, for example, are measured in epochs.
|
/// and consistent scheduling metric than time. Timelocks, for example, are measured in epochs.
|
||||||
|
|
||||||
|
/*
|
||||||
/// @dev Returns the current epoch.
|
/// @dev Returns the current epoch.
|
||||||
function getCurrentEpoch()
|
function getCurrentEpoch()
|
||||||
public
|
public
|
||||||
@ -82,4 +83,5 @@ contract IMixinScheduler {
|
|||||||
public
|
public
|
||||||
view
|
view
|
||||||
returns (uint64);
|
returns (uint64);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
@ -31,16 +31,15 @@ contract MixinStorage is
|
|||||||
|
|
||||||
// @TODO Add notes about which Mixin manages which state
|
// @TODO Add notes about which Mixin manages which state
|
||||||
|
|
||||||
|
// address of owner
|
||||||
|
address internal owner;
|
||||||
|
|
||||||
// address of staking contract
|
// address of staking contract
|
||||||
address internal stakingContract;
|
address internal stakingContract;
|
||||||
|
|
||||||
// mapping from Owner to Amount Staked
|
// mapping from Owner to Amount Staked
|
||||||
mapping (address => uint256) internal stakeByOwner;
|
mapping (address => uint256) internal stakeByOwner;
|
||||||
|
|
||||||
// @TODO Think about merging these different states
|
|
||||||
// It would be nice if the sum of the different states had to equal `stakeByOwner`
|
|
||||||
// and it were all in a single variable (stakeByOwner in its own)
|
|
||||||
|
|
||||||
// mapping from Owner to Amount of Instactive Stake
|
// mapping from Owner to Amount of Instactive Stake
|
||||||
mapping (address => uint256) internal activeStakeByOwner;
|
mapping (address => uint256) internal activeStakeByOwner;
|
||||||
|
|
||||||
@ -88,7 +87,7 @@ contract MixinStorage is
|
|||||||
mapping (bytes32 => uint256) internal protocolFeesThisEpochByPool;
|
mapping (bytes32 => uint256) internal protocolFeesThisEpochByPool;
|
||||||
|
|
||||||
//
|
//
|
||||||
bytes32[] internal activePoolIdsThisEpoch;
|
bytes32[] internal activePoolsThisEpoch;
|
||||||
|
|
||||||
// mapping from POol Id to Shadow Rewards
|
// mapping from POol Id to Shadow Rewards
|
||||||
mapping (bytes32 => uint256) internal shadowRewardsByPoolId;
|
mapping (bytes32 => uint256) internal shadowRewardsByPoolId;
|
||||||
|
@ -2,7 +2,7 @@ pragma solidity ^0.5.5;
|
|||||||
|
|
||||||
|
|
||||||
interface IStakingEvents {
|
interface IStakingEvents {
|
||||||
|
|
||||||
event StakeMinted(
|
event StakeMinted(
|
||||||
address owner,
|
address owner,
|
||||||
uint256 amount
|
uint256 amount
|
||||||
@ -27,9 +27,39 @@ interface IStakingEvents {
|
|||||||
address exchangeAddress
|
address exchangeAddress
|
||||||
);
|
);
|
||||||
|
|
||||||
event EpochFinalized(
|
/// @dev Emitted by MixinScheduler when the epoch is changed.
|
||||||
|
/// @param epoch The epoch we changed to.
|
||||||
|
/// @param startTimeInSeconds The start time of the epoch.
|
||||||
|
/// @param earliestEndTimeInSeconds The earliest this epoch can end.
|
||||||
|
event EpochChanged(
|
||||||
|
uint64 epoch,
|
||||||
|
uint64 startTimeInSeconds,
|
||||||
|
uint64 earliestEndTimeInSeconds
|
||||||
|
);
|
||||||
|
|
||||||
|
/// @dev Emitted by MixinScheduler when the timelock period is changed.
|
||||||
|
/// @param timelockPeriod The timelock period we changed to.
|
||||||
|
/// @param startEpoch The epoch this period started.
|
||||||
|
/// @param endEpoch The epoch this period ends.
|
||||||
|
event TimelockPeriodChanged(
|
||||||
|
uint64 timelockPeriod,
|
||||||
|
uint64 startEpoch,
|
||||||
|
uint64 endEpoch
|
||||||
|
);
|
||||||
|
|
||||||
|
/// @dev Emitted by MixinFees when rewards are paid out.
|
||||||
|
/// @param totalActivePools Total active pools this epoch.
|
||||||
|
/// @param totalFeesCollected Total fees collected this epoch, across all active pools.
|
||||||
|
/// @param totalWeightedStake Total weighted stake attributed to each pool. Delegated stake is weighted less.
|
||||||
|
/// @param totalRewardsPaid Total rewards paid out across all active pools.
|
||||||
|
/// @param initialContractBalance Balance of this contract before paying rewards.
|
||||||
|
/// @param finalContractBalance Balance of this contract after paying rewards.
|
||||||
|
event RewardsPaid(
|
||||||
uint256 totalActivePools,
|
uint256 totalActivePools,
|
||||||
uint256 totalFees,
|
uint256 totalFeesCollected,
|
||||||
uint256 totalRewards
|
uint256 totalWeightedStake,
|
||||||
|
uint256 totalRewardsPaid,
|
||||||
|
uint256 initialContractBalance,
|
||||||
|
uint256 finalContractBalance
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -50,5 +50,6 @@ interface IStructs {
|
|||||||
struct ActivePool {
|
struct ActivePool {
|
||||||
bytes32 poolId;
|
bytes32 poolId;
|
||||||
uint256 feesCollected;
|
uint256 feesCollected;
|
||||||
|
uint256 weightedStake;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -82,6 +82,7 @@ describe('Exchange Integrations', () => {
|
|||||||
stakingWrapper.removeExchangeAddressAsync(exchange),
|
stakingWrapper.removeExchangeAddressAsync(exchange),
|
||||||
RevertReason.ExchangeAddressNotRegistered,
|
RevertReason.ExchangeAddressNotRegistered,
|
||||||
);
|
);
|
||||||
|
// @todo should not be able to add / remove an exchange if not contract owner.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -118,13 +118,13 @@ describe('End-To-End Simulations', () => {
|
|||||||
/*
|
/*
|
||||||
\ // the expected payouts were computed by hand
|
\ // the expected payouts were computed by hand
|
||||||
// @TODO - get computations more accurate
|
// @TODO - get computations more accurate
|
||||||
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Scaled)
|
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Weighted) | Payout
|
||||||
0 | 0.304958 | 42 | 0 | 42
|
0 | 0.304958 | 42 | 0 | 42 | 3.0060373...
|
||||||
1 | 15.323258 | 84 | 0 | 84
|
1 | 15.323258 | 84 | 0 | 84 |
|
||||||
3 | 28.12222236 | 97 | 182 | 260.8
|
3 | 28.12222236 | 97 | 182 | 260.8
|
||||||
...
|
...
|
||||||
Cumulative Fees = 43.75043836
|
Cumulative Fees = 43.75043836
|
||||||
Cumulative Stake = 405
|
Cumulative Weighted Stake = 386.8
|
||||||
Total Rewards = 43.75043836
|
Total Rewards = 43.75043836
|
||||||
*/
|
*/
|
||||||
const simulationParams = {
|
const simulationParams = {
|
||||||
@ -164,27 +164,27 @@ describe('End-To-End Simulations', () => {
|
|||||||
StakingWrapper.toBaseUnitAmount(28.12222236),
|
StakingWrapper.toBaseUnitAmount(28.12222236),
|
||||||
],
|
],
|
||||||
expectedPayoutByPool: [
|
expectedPayoutByPool: [
|
||||||
new BigNumber('2.89303'), // 2.8930364057678784829875695710382241749912199174798475
|
new BigNumber('3.00603'), // 3.006037310109530277237724562632303034914024715508955780682
|
||||||
new BigNumber('9.90218'), // 9.9021783083174087034787071054543342142019746753770943
|
new BigNumber('10.28895'), // 10.28895363598396754741643198605226143579652264694121578135
|
||||||
new BigNumber('28.16463'), // 28.164631904035798614670299155719067954180760345463798
|
new BigNumber('29.26472'), // 29.26473180250053106672049765968527817034954761113582833460
|
||||||
],
|
],
|
||||||
expectedPayoutByPoolOperator: [
|
expectedPayoutByPoolOperator: [
|
||||||
new BigNumber('1.12828'), // 0.39 * 2.89303
|
new BigNumber('1.17235'), // 0.39 * 3.00603
|
||||||
new BigNumber('5.84228'), // 0.59 * 9.90218
|
new BigNumber('6.07048'), // 0.59 * 10.28895
|
||||||
new BigNumber('12.11079'), // 0.43 * 28.16463
|
new BigNumber('12.58383'), // 0.43 * 29.26472
|
||||||
],
|
],
|
||||||
expectedMembersPayoutByPool: [
|
expectedMembersPayoutByPool: [
|
||||||
new BigNumber('1.76475'), // (1 - 0.39) * 2.89303
|
new BigNumber('1.83368'), // (1 - 0.39) * 3.00603
|
||||||
new BigNumber('4.05989'), // (1 - 0.59) * 9.90218
|
new BigNumber('4.21847'), // (1 - 0.59) * 10.28895
|
||||||
new BigNumber('16.05383'), // (1 - 0.43) * 28.16463
|
new BigNumber('16.68089'), // (1 - 0.43) * 29.26472
|
||||||
],
|
],
|
||||||
expectedPayoutByDelegator: [
|
expectedPayoutByDelegator: [
|
||||||
// note that the on-chain values may be slightly different due to rounding down on each entry
|
// note that the on-chain values may be slightly different due to rounding down on each entry
|
||||||
// there is a carry over between calls, which we account for here. the result is that delegators
|
// there is a carry over between calls, which we account for here. the result is that delegators
|
||||||
// who withdraw later on will scoop up any rounding spillover from those who have already withdrawn.
|
// who withdraw later on will scoop up any rounding spillover from those who have already withdrawn.
|
||||||
new BigNumber('1.49953'), // (17 / 182) * 16.05383
|
new BigNumber('1.55810'), // (17 / 182) * 16.6809
|
||||||
new BigNumber('6.61559'), // (75 / 182) * 16.05383
|
new BigNumber('6.87399'), // (75 / 182) * 16.6809
|
||||||
new BigNumber('7.93871'), // (90 / 182) * 16.05383
|
new BigNumber('8.24879'), // (90 / 182) * 16.6809
|
||||||
],
|
],
|
||||||
exchangeAddress: exchange,
|
exchangeAddress: exchange,
|
||||||
};
|
};
|
||||||
@ -201,7 +201,7 @@ describe('End-To-End Simulations', () => {
|
|||||||
3 | 28.12222236 | 97 | 182 | 260.8
|
3 | 28.12222236 | 97 | 182 | 260.8
|
||||||
...
|
...
|
||||||
Cumulative Fees = 43.75043836
|
Cumulative Fees = 43.75043836
|
||||||
Cumulative Stake = 405
|
Cumulative Weighted Stake = 386.8
|
||||||
Total Rewards = 43.75043836
|
Total Rewards = 43.75043836
|
||||||
|
|
||||||
// In this case, there was already a pot of ETH in the delegator pool that nobody had claimed.
|
// In this case, there was already a pot of ETH in the delegator pool that nobody had claimed.
|
||||||
@ -280,7 +280,7 @@ describe('End-To-End Simulations', () => {
|
|||||||
3 | 28.12222236 | 97 | 182 | 260.8
|
3 | 28.12222236 | 97 | 182 | 260.8
|
||||||
...
|
...
|
||||||
Cumulative Fees = 43.75043836
|
Cumulative Fees = 43.75043836
|
||||||
Cumulative Stake = 405
|
Cumulative Weighted Stake = 386.8
|
||||||
Total Rewards = 43.75043836
|
Total Rewards = 43.75043836
|
||||||
|
|
||||||
// In this case, there was already a pot of ETH in the delegator pool that nobody had claimed.
|
// In this case, there was already a pot of ETH in the delegator pool that nobody had claimed.
|
||||||
|
@ -25,7 +25,7 @@ export class StakingWrapper {
|
|||||||
private readonly _web3Wrapper: Web3Wrapper;
|
private readonly _web3Wrapper: Web3Wrapper;
|
||||||
private readonly _provider: Provider;
|
private readonly _provider: Provider;
|
||||||
private readonly _logDecoder: LogDecoder;
|
private readonly _logDecoder: LogDecoder;
|
||||||
private readonly _ownerAddres: string;
|
private readonly _ownerAddress: string;
|
||||||
private readonly _erc20ProxyContract: ERC20ProxyContract;
|
private readonly _erc20ProxyContract: ERC20ProxyContract;
|
||||||
private readonly _zrxTokenContract: DummyERC20TokenContract;
|
private readonly _zrxTokenContract: DummyERC20TokenContract;
|
||||||
private readonly _accounts: string[];
|
private readonly _accounts: string[];
|
||||||
@ -73,7 +73,7 @@ export class StakingWrapper {
|
|||||||
this._provider = provider;
|
this._provider = provider;
|
||||||
const decoderArtifacts = _.merge(artifacts, erc20Artifacts);
|
const decoderArtifacts = _.merge(artifacts, erc20Artifacts);
|
||||||
this._logDecoder = new LogDecoder(this._web3Wrapper, decoderArtifacts);
|
this._logDecoder = new LogDecoder(this._web3Wrapper, decoderArtifacts);
|
||||||
this._ownerAddres = ownerAddres;
|
this._ownerAddress = ownerAddres;
|
||||||
this._erc20ProxyContract = erc20ProxyContract;
|
this._erc20ProxyContract = erc20ProxyContract;
|
||||||
this._zrxTokenContract = zrxTokenContract;
|
this._zrxTokenContract = zrxTokenContract;
|
||||||
this._accounts = accounts;
|
this._accounts = accounts;
|
||||||
@ -143,7 +143,7 @@ export class StakingWrapper {
|
|||||||
(this._zrxVaultContractIfExists).address,
|
(this._zrxVaultContractIfExists).address,
|
||||||
);
|
);
|
||||||
const setZrxVaultTxData = {
|
const setZrxVaultTxData = {
|
||||||
from: this._ownerAddres,
|
from: this._ownerAddress,
|
||||||
to: (this._stakingProxyContractIfExists).address,
|
to: (this._stakingProxyContractIfExists).address,
|
||||||
data: setZrxVaultCalldata,
|
data: setZrxVaultCalldata,
|
||||||
};
|
};
|
||||||
@ -161,7 +161,7 @@ export class StakingWrapper {
|
|||||||
(this._rewardVaultContractIfExists).address,
|
(this._rewardVaultContractIfExists).address,
|
||||||
);
|
);
|
||||||
const setRewardVaultTxData = {
|
const setRewardVaultTxData = {
|
||||||
from: this._ownerAddres,
|
from: this._ownerAddress,
|
||||||
to: (this._stakingProxyContractIfExists).address,
|
to: (this._stakingProxyContractIfExists).address,
|
||||||
data: setRewardVaultCalldata,
|
data: setRewardVaultCalldata,
|
||||||
};
|
};
|
||||||
@ -243,7 +243,7 @@ export class StakingWrapper {
|
|||||||
}
|
}
|
||||||
public async forceTimelockSyncAsync(owner: string): Promise<TransactionReceiptWithDecodedLogs> {
|
public async forceTimelockSyncAsync(owner: string): Promise<TransactionReceiptWithDecodedLogs> {
|
||||||
const calldata = this.getStakingContract().forceTimelockSync.getABIEncodedTransactionData(owner);
|
const calldata = this.getStakingContract().forceTimelockSync.getABIEncodedTransactionData(owner);
|
||||||
const txReceipt = await this._executeTransactionAsync(calldata, this._ownerAddres);
|
const txReceipt = await this._executeTransactionAsync(calldata, this._ownerAddress);
|
||||||
return txReceipt;
|
return txReceipt;
|
||||||
}
|
}
|
||||||
///// STAKE BALANCES /////
|
///// STAKE BALANCES /////
|
||||||
@ -411,6 +411,7 @@ export class StakingWrapper {
|
|||||||
logUtils.log(
|
logUtils.log(
|
||||||
`Finalization costed ${txReceipt.gasUsed} gas`,
|
`Finalization costed ${txReceipt.gasUsed} gas`,
|
||||||
);
|
);
|
||||||
|
console.log(JSON.stringify(txReceipt.logs, null, 4));
|
||||||
return txReceipt;
|
return txReceipt;
|
||||||
}
|
}
|
||||||
public async skipToNextEpochAsync(): Promise<TransactionReceiptWithDecodedLogs> {
|
public async skipToNextEpochAsync(): Promise<TransactionReceiptWithDecodedLogs> {
|
||||||
@ -511,14 +512,16 @@ export class StakingWrapper {
|
|||||||
const isValid = this.getStakingContract().isValidExchangeAddress.getABIDecodedReturnData(returnData);
|
const isValid = this.getStakingContract().isValidExchangeAddress.getABIDecodedReturnData(returnData);
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
public async addExchangeAddressAsync(exchangeAddress: string): Promise<TransactionReceiptWithDecodedLogs> {
|
public async addExchangeAddressAsync(exchangeAddress: string, ownerAddressIfExists?: string): Promise<TransactionReceiptWithDecodedLogs> {
|
||||||
const calldata = this.getStakingContract().addExchangeAddress.getABIEncodedTransactionData(exchangeAddress);
|
const calldata = this.getStakingContract().addExchangeAddress.getABIEncodedTransactionData(exchangeAddress);
|
||||||
const txReceipt = await this._executeTransactionAsync(calldata, this._ownerAddres);
|
const ownerAddress = ownerAddressIfExists !== undefined ? ownerAddressIfExists : this._ownerAddress;
|
||||||
|
const txReceipt = await this._executeTransactionAsync(calldata, ownerAddress);
|
||||||
return txReceipt;
|
return txReceipt;
|
||||||
}
|
}
|
||||||
public async removeExchangeAddressAsync(exchangeAddress: string): Promise<TransactionReceiptWithDecodedLogs> {
|
public async removeExchangeAddressAsync(exchangeAddress: string, ownerAddressIfExists?: string): Promise<TransactionReceiptWithDecodedLogs> {
|
||||||
const calldata = this.getStakingContract().removeExchangeAddress.getABIEncodedTransactionData(exchangeAddress);
|
const calldata = this.getStakingContract().removeExchangeAddress.getABIEncodedTransactionData(exchangeAddress);
|
||||||
const txReceipt = await this._executeTransactionAsync(calldata, this._ownerAddres);
|
const ownerAddress = ownerAddressIfExists !== undefined ? ownerAddressIfExists : this._ownerAddress;
|
||||||
|
const txReceipt = await this._executeTransactionAsync(calldata, ownerAddress);
|
||||||
return txReceipt;
|
return txReceipt;
|
||||||
}
|
}
|
||||||
///// REWARDS /////
|
///// REWARDS /////
|
||||||
@ -735,7 +738,7 @@ export class StakingWrapper {
|
|||||||
includeLogs?: boolean,
|
includeLogs?: boolean,
|
||||||
): Promise<TransactionReceiptWithDecodedLogs> {
|
): Promise<TransactionReceiptWithDecodedLogs> {
|
||||||
const txData = {
|
const txData = {
|
||||||
from: from ? from : this._ownerAddres,
|
from: from ? from : this._ownerAddress,
|
||||||
to: this.getStakingProxyContract().address,
|
to: this.getStakingProxyContract().address,
|
||||||
data: calldata,
|
data: calldata,
|
||||||
gas: 3000000,
|
gas: 3000000,
|
||||||
@ -750,7 +753,7 @@ export class StakingWrapper {
|
|||||||
}
|
}
|
||||||
private async _callAsync(calldata: string, from?: string): Promise<any> {
|
private async _callAsync(calldata: string, from?: string): Promise<any> {
|
||||||
const txData = {
|
const txData = {
|
||||||
from: from ? from : this._ownerAddres,
|
from: from ? from : this._ownerAddress,
|
||||||
to: this.getStakingProxyContract().address,
|
to: this.getStakingProxyContract().address,
|
||||||
data: calldata,
|
data: calldata,
|
||||||
gas: 3000000,
|
gas: 3000000,
|
||||||
|
@ -873,7 +873,7 @@
|
|||||||
|
|
||||||
"@0x/web3-wrapper@^4.0.1":
|
"@0x/web3-wrapper@^4.0.1":
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.npmjs.org/@0x/web3-wrapper/-/web3-wrapper-4.0.2.tgz#d4e0a4fa1217155e1aed4cd91086654fd99f2959"
|
resolved "https://registry.yarnpkg.com/@0x/web3-wrapper/-/web3-wrapper-4.0.2.tgz#d4e0a4fa1217155e1aed4cd91086654fd99f2959"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@0x/assert" "^2.0.2"
|
"@0x/assert" "^2.0.2"
|
||||||
"@0x/json-schemas" "^3.0.2"
|
"@0x/json-schemas" "^3.0.2"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user