Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Commit

Permalink
Merge pull request #2109 from 0xProject/feature/3.0/staking/math-voodoo
Browse files Browse the repository at this point in the history
Staking math upgrades
  • Loading branch information
dorothy-zbornak authored Sep 5, 2019
2 parents e5dcf90 + 356660a commit 88e5635
Show file tree
Hide file tree
Showing 50 changed files with 2,283 additions and 939 deletions.
16 changes: 16 additions & 0 deletions contracts/staking/CHANGELOG.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@
{
"note": "First implementation",
"pr": 1910
},
{
"note": "Replace `LibFeeMath` with `LibFixedMath`.",
"pr": 2109
},
{
"note": "Use a more precise cobb-douglas implementation.",
"pr": 2109
},
{
"note": "Change the way operator stake is computed.",
"pr": 2109
},
{
"note": "Denominate pool operator shares in parts-per-million.",
"pr": 2109
}
]
}
Expand Down
124 changes: 111 additions & 13 deletions contracts/staking/contracts/src/fees/MixinExchangeFees.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
Copyright 2018 ZeroEx Intl.
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -20,8 +20,8 @@ pragma solidity ^0.5.9;

import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../libs/LibFeeMath.sol";
import "../libs/LibStakingRichErrors.sol";
import "../libs/LibFixedMath.sol";
import "../immutable/MixinStorage.sol";
import "../immutable/MixinConstants.sol";
import "../interfaces/IStakingEvents.sol";
Expand Down Expand Up @@ -56,13 +56,37 @@ contract MixinExchangeFees is
{
using LibSafeMath for uint256;

/// @dev Set the cobb douglas alpha value used when calculating rewards.
/// Valid inputs: 0 <= `numerator` / `denominator` <= 1.0
/// @param numerator The alpha numerator.
/// @param denominator The alpha denominator.
function setCobbDouglasAlpha(
uint256 numerator,
uint256 denominator
)
external
onlyOwner
{
if (int256(numerator) < 0 || int256(denominator) <= 0 || numerator > denominator) {
LibRichErrors.rrevert(LibStakingRichErrors.InvalidCobbDouglasAlphaError(
numerator,
denominator
));
}
cobbDouglasAlphaNumerator = numerator;
cobbDouglasAlphaDenomintor = denominator;
emit CobbDouglasAlphaChanged(numerator, denominator);
}

/// TODO(jalextowle): Add WETH to protocol fees. Should this be unwrapped?
/// @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,
// solhint-disable-next-line
address payerAddress,
// solhint-disable-next-line
uint256 protocolFeePaid
)
external
Expand All @@ -71,10 +95,16 @@ contract MixinExchangeFees is
{
uint256 amount = msg.value;
bytes32 poolId = getStakingPoolIdOfMaker(makerAddress);
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(amount);
if (_feesCollectedThisEpoch == 0) {
activePoolsThisEpoch.push(poolId);
if (poolId != NIL_MAKER_ID) {
// There is a pool associated with `makerAddress`.
// TODO(dorothy-zbornak): When we have epoch locks on delegating, we could
// preclude pools that have no delegated stake, since they will never have
// stake in this epoch and are therefore not entitled to rewards.
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(amount);
if (_feesCollectedThisEpoch == 0) {
activePoolsThisEpoch.push(poolId);
}
}
}

Expand Down Expand Up @@ -125,7 +155,7 @@ contract MixinExchangeFees is
}

/// @dev Pays rewards to market making pools that were active this epoch.
/// Each pool receives a portion of the fees generated this epoch (see LibFeeMath) that is
/// Each pool receives a portion of the fees generated this epoch (see _cobbDouglas) 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. Rebates are paid
/// into the Reward Vault (see MixinStakingPoolRewardVault) where they can be withdraw by makers and
Expand All @@ -139,7 +169,7 @@ contract MixinExchangeFees is
/// @return initialContractBalance Balance of this contract before paying rewards.
/// @return finalContractBalance Balance of this contract after paying rewards.
function _distributeFeesAmongMakerPools()
private
internal
returns (
uint256 totalActivePools,
uint256 totalFeesCollected,
Expand Down Expand Up @@ -176,11 +206,12 @@ contract MixinExchangeFees is

// compute weighted stake
uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId);
uint256 stakeHeldByPoolOperator = getActivatedAndUndelegatedStake(getStakingPoolOperator(poolId));
uint256 stakeHeldByPoolOperator = getStakeDelegatedToPoolByOwner(getStakingPoolOperator(poolId), poolId);
uint256 weightedStake = stakeHeldByPoolOperator.safeAdd(
totalStakeDelegatedToPool
.safeMul(REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE)
.safeDiv(PERCENTAGE_DENOMINATOR)
.safeSub(stakeHeldByPoolOperator)
.safeMul(REWARD_DELEGATED_STAKE_WEIGHT)
.safeDiv(PPM_DENOMINATOR)
);

// store pool stats
Expand Down Expand Up @@ -209,12 +240,14 @@ contract MixinExchangeFees is
// step 2/3 - record reward for each pool
for (uint256 i = 0; i != totalActivePools; i++) {
// compute reward using cobb-douglas formula
uint256 reward = LibFeeMath._cobbDouglasSuperSimplified(
uint256 reward = _cobbDouglas(
initialContractBalance,
activePools[i].feesCollected,
totalFeesCollected,
activePools[i].weightedStake,
totalWeightedStake
totalWeightedStake,
cobbDouglasAlphaNumerator,
cobbDouglasAlphaDenomintor
);

// record reward in vault
Expand Down Expand Up @@ -250,4 +283,69 @@ contract MixinExchangeFees is
finalContractBalance
);
}

/// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch.
/// Note that in this function there is no limitation on alpha; we tend to get better rounding
/// on the simplified versions below.
/// @param totalRewards collected over an epoch.
/// @param ownerFees Fees attributed to the owner of the staking pool.
/// @param totalFees collected across all active staking pools in the epoch.
/// @param ownerStake Stake attributed to the owner of the staking pool.
/// @param totalStake collected across all active staking pools in the epoch.
/// @param alphaNumerator Numerator of `alpha` in the cobb-dougles function.
/// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function.
/// @return ownerRewards Rewards for the owner.
function _cobbDouglas(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake,
uint256 alphaNumerator,
uint256 alphaDenominator
)
internal
pure
returns (uint256 ownerRewards)
{
int256 feeRatio = LibFixedMath._toFixed(ownerFees, totalFees);
int256 stakeRatio = LibFixedMath._toFixed(ownerStake, totalStake);
if (feeRatio == 0 || stakeRatio == 0) {
return ownerRewards = 0;
}

// The cobb-doublas function has the form:
// `totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)`
// This is equivalent to:
// `totalRewards * stakeRatio * e^(alpha * (ln(feeRatio / stakeRatio)))`
// However, because `ln(x)` has the domain of `0 < x < 1`
// and `exp(x)` has the domain of `x < 0`,
// and fixed-point math easily overflows with multiplication,
// we will choose the following if `stakeRatio > feeRatio`:
// `totalRewards * stakeRatio / e^(alpha * (ln(stakeRatio / feeRatio)))`

// Compute
// `e^(alpha * (ln(feeRatio/stakeRatio)))` if feeRatio <= stakeRatio
// or
// `e^(ln(stakeRatio/feeRatio))` if feeRatio > stakeRatio
int256 n = feeRatio <= stakeRatio ?
LibFixedMath._div(feeRatio, stakeRatio) :
LibFixedMath._div(stakeRatio, feeRatio);
n = LibFixedMath._exp(
LibFixedMath._mulDiv(
LibFixedMath._ln(n),
int256(alphaNumerator),
int256(alphaDenominator)
)
);
// Compute
// `totalRewards * n` if feeRatio <= stakeRatio
// or
// `totalRewards / n` if stakeRatio > feeRatio
n = feeRatio <= stakeRatio ?
LibFixedMath._mul(stakeRatio, n) :
LibFixedMath._div(stakeRatio, n);
// Multiply the above with totalRewards.
ownerRewards = LibFixedMath._uintMul(n, totalRewards);
}
}
3 changes: 1 addition & 2 deletions contracts/staking/contracts/src/immutable/MixinConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import "./MixinDeploymentConstants.sol";
contract MixinConstants is
MixinDeploymentConstants
{
// TODO: Reevaluate this variable
uint8 constant internal PERCENTAGE_DENOMINATOR = 100;
uint32 constant internal PPM_DENOMINATOR = 1000000;

// The upper 16 bytes represent the pool id, so this would be pool id 1. See MixinStakinPool for more information.
bytes32 constant internal INITIAL_POOL_ID = 0x0000000000000000000000000000000100000000000000000000000000000000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ contract MixinDeploymentConstants {

uint256 constant internal TIMELOCK_DURATION_IN_EPOCHS = 3;

uint256 constant internal COBB_DOUGLAS_ALPHA_DENOMINATOR = 6;

uint256 constant internal REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE = 90;
// How much delegated stake is weighted vs operator stake, in ppm.
uint32 constant internal REWARD_DELEGATED_STAKE_WEIGHT = 900000; // 90%

uint256 constant internal CHAIN_ID = 1;
}
8 changes: 7 additions & 1 deletion contracts/staking/contracts/src/immutable/MixinStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import "../interfaces/IStakingPoolRewardVault.sol";
import "../interfaces/IStructs.sol";


// solhint-disable max-states-count
// solhint-disable max-states-count, no-empty-blocks
contract MixinStorage is
MixinDeploymentConstants,
MixinConstants,
Expand Down Expand Up @@ -106,4 +106,10 @@ contract MixinStorage is

// Rebate Vault
IStakingPoolRewardVault internal rewardVault;

// Numerator for cobb douglas alpha factor.
uint256 internal cobbDouglasAlphaNumerator = 1;

// Denominator for cobb douglas alpha factor.
uint256 internal cobbDouglasAlphaDenomintor = 6;
}
12 changes: 10 additions & 2 deletions contracts/staking/contracts/src/interfaces/IStakingEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ interface IStakingEvents {
uint256 earliestEndTimeInSeconds
);

/// @dev Emitted by MixinExchangeFees when the cobb douglas alpha is updated.
/// @param numerator The alpha numerator.
/// @param denominator The alpha denominator.
event CobbDouglasAlphaChanged(
uint256 numerator,
uint256 denominator
);

/// @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.
Expand Down Expand Up @@ -70,11 +78,11 @@ interface IStakingEvents {
/// @dev Emitted by MixinStakingPool when a new pool is created.
/// @param poolId Unique id generated for pool.
/// @param operatorAddress Address of creator/operator of pool.
/// @param operatorShare The share of rewards given to the operator.
/// @param operatorShare The share of rewards given to the operator, in ppm.
event StakingPoolCreated(
bytes32 poolId,
address operatorAddress,
uint8 operatorShare
uint32 operatorShare
);

/// @dev Emitted by MixinStakingPool when a new maker is added to a pool.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ interface IStakingPoolRewardVault {

/// @dev Holds the balance for a staking pool.
/// @param initialzed True iff the balance struct is initialized.
/// @param operatorShare Percentage of the total balance owned by the operator.
/// @param operatorShare Fraction of the total balance owned by the operator, in ppm.
/// @param operatorBalance Balance in ETH of the operator.
/// @param membersBalance Balance in ETH co-owned by the pool members.
struct Balance {
bool initialized;
uint8 operatorShare;
uint32 operatorShare;
uint96 operatorBalance;
uint96 membersBalance;
}
Expand Down Expand Up @@ -69,10 +69,10 @@ interface IStakingPoolRewardVault {

/// @dev Emitted when a staking pool is registered.
/// @param poolId Unique Id of pool that was registered.
/// @param operatorShare Share of rewards owned by operator.
/// @param operatorShare Share of rewards owned by operator. in ppm.
event StakingPoolRegistered(
bytes32 poolId,
uint8 operatorShare
uint32 operatorShare
);

/// @dev Default constructor.
Expand Down Expand Up @@ -119,8 +119,8 @@ interface IStakingPoolRewardVault {
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param poolOperatorShare Percentage of rewards given to the pool operator.
function registerStakingPool(bytes32 poolId, uint8 poolOperatorShare)
/// @param poolOperatorShare Share of rewards given to the pool operator, in ppm.
function registerStakingPool(bytes32 poolId, uint32 poolOperatorShare)
external;

/// @dev Returns the total balance of a pool.
Expand Down
4 changes: 2 additions & 2 deletions contracts/staking/contracts/src/interfaces/IStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ interface IStructs {

/// @dev State for Staking Pools (see MixinStakingPool).
/// @param operatorAddress Address of pool operator.
/// @param operatorShare Portion of pool rewards owned by operator.
/// @param operatorShare Portion of pool rewards owned by operator, in ppm.
struct Pool {
address payable operatorAddress;
uint8 operatorShare;
uint32 operatorShare;
}

/// @dev State for a pool that actively traded during the current epoch.
Expand Down
Loading

0 comments on commit 88e5635

Please sign in to comment.