Skip to content

Commit 1846eb9

Browse files
abarmatdavekay100
authored andcommitted
rebates: manage outstanding funds + improvements
- Remove ABDK math library and use 0x based implementation (cleaner) - Expose a parameter setter for the rebate ratio in Staking contract - Use alpha nominator and denominator in rebate formula - Track the rebate ratio value in each rebate pool on init to allow for future changes of the parameter - Ensure that no more funds are claimed than stored - Track the claimed rewards from the rebate pool to allow managing the remainder - Burn the remaining funds - Add configuration test for the rebate ratio - Add many tests specific to the calculations using a mock contract - Test the rebate formula against a Typescript implementation for various alpha and values - Test that sum of rebates is not greater that the deposited ones (to catch rounding errors) - Test how it behaves with edge values of alpha and fees - Fix tests in allocation.test.ts
1 parent f430146 commit 1846eb9

20 files changed

+1026
-1338
lines changed

.solcover.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const skipFiles = ['abdk-libraries-solidity', 'bancor', 'ens', 'erc1056']
1+
const skipFiles = ['bancor', 'ens', 'erc1056']
22

33
module.exports = {
44
providerOptions: {

.soliumignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ node_modules
22

33
contracts/bancor
44
contracts/discovery/erc1056
5-
contracts/staking/libs/abdk-libraries-solidity
65
contracts/token/IGraphToken.sol
76
contracts/upgrades/GraphProxy.sol
87
contracts/rewards/RewardsManager.sol

contracts/staking/IStaking.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ interface IStaking {
6767

6868
function setMaxAllocationEpochs(uint32 _maxAllocationEpochs) external;
6969

70+
function setRebateRatio(uint32 _alphaNumerator, uint32 _alphaDenominator) external;
71+
7072
function setDelegationCapacity(uint32 _delegationCapacity) external;
7173

7274
function setDelegationParameters(

contracts/staking/Staking.sol

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
171171
* @dev Check if the caller is the slasher.
172172
*/
173173
modifier onlySlasher {
174-
require(slashers[msg.sender] == true, "Caller is not a Slasher");
174+
require(slashers[msg.sender] == true, "!slasher");
175175
_;
176176
}
177177

@@ -194,6 +194,10 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
194194
*/
195195
function initialize(address _controller) external onlyImpl {
196196
Managed._initialize(_controller);
197+
198+
// By default 100% rebate ratio to fees
199+
alphaNumerator = 1;
200+
alphaDenominator = 1;
197201
}
198202

199203
/**
@@ -258,6 +262,22 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
258262
emit ParameterUpdated("maxAllocationEpochs");
259263
}
260264

265+
/**
266+
* @dev Set the rebate ratio (fees to allocated stake).
267+
* @param _alphaNumerator Numerator of `alpha` in the cobb-douglas function
268+
* @param _alphaDenominator Denominator of `alpha` in the cobb-douglas function
269+
*/
270+
function setRebateRatio(uint32 _alphaNumerator, uint32 _alphaDenominator)
271+
external
272+
override
273+
onlyGovernor
274+
{
275+
require(_alphaNumerator > 0 && _alphaDenominator > 0, "=zero");
276+
alphaNumerator = _alphaNumerator;
277+
alphaDenominator = _alphaDenominator;
278+
emit ParameterUpdated("rebateRatio");
279+
}
280+
261281
/**
262282
* @dev Set the delegation capacity multiplier.
263283
* @param _delegationCapacity Delegation capacity multiplier
@@ -565,7 +585,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
565585

566586
// Cannot slash stake of an indexer without any or enough stake
567587
require(indexerStake.hasTokens(), "Indexer has no stakes");
568-
require(_tokens <= indexerStake.tokensStaked, "cannot slash more than staked amount");
588+
require(_tokens <= indexerStake.tokensStaked, "Cannot slash more than staked amount");
569589

570590
// Validate beneficiary of slashed tokens
571591
require(_beneficiary != address(0), "Beneficiary must not be an empty address");
@@ -583,11 +603,10 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
583603
// Remove tokens to slash from the stake
584604
indexerStake.release(_tokens);
585605

606+
// -- Effects --
607+
586608
// Set apart the reward for the beneficiary and burn remaining slashed stake
587-
uint256 tokensToBurn = _tokens.sub(_reward);
588-
if (tokensToBurn > 0) {
589-
graphToken().burn(tokensToBurn);
590-
}
609+
_burnTokens(_tokens.sub(_reward));
591610

592611
// Give the beneficiary a reward for slashing
593612
if (_reward > 0) {
@@ -759,7 +778,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
759778
}
760779

761780
/**
762-
* @dev Collect query fees for an allocation.
781+
* @dev Collect query fees for an allocation from state channels.
763782
* Funds received are only accepted from a valid source.
764783
* @param _tokens Amount of tokens to collect
765784
*/
@@ -795,23 +814,16 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
795814
AllocationState allocState = _getAllocationState(_allocationID);
796815

797816
// Validate ownership
798-
require(_onlyAuthOrDelegator(alloc.indexer), "Caller must be authorized");
817+
require(_onlyAuthOrDelegator(alloc.indexer), "!auth");
799818

800819
// TODO: restake when delegator called should not be allowed?
801820

802821
// Funds can only be claimed after a period of time passed since allocation was closed
803822
require(allocState == AllocationState.Finalized, "Allocation must be in finalized state");
804823

805-
// Find a rebate pool for the epoch
806-
Rebates.Pool storage pool = rebates[alloc.closedAtEpoch];
807-
808-
// Process rebate
809-
uint256 tokensToClaim = pool.redeem(alloc.collectedFees, alloc.effectiveAllocation);
810-
811-
// When all allocations processed then prune rebate pool
812-
if (pool.unclaimedAllocationsCount == 0) {
813-
delete rebates[alloc.closedAtEpoch];
814-
}
824+
// Process rebate reward
825+
Rebates.Pool storage rebatePool = rebates[alloc.closedAtEpoch];
826+
uint256 tokensToClaim = rebatePool.redeem(alloc.collectedFees, alloc.effectiveAllocation);
815827

816828
// Calculate delegation rewards and add them to the delegation pool
817829
uint256 delegationRewards = _collectDelegationQueryRewards(alloc.indexer, tokensToClaim);
@@ -828,6 +840,14 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
828840
alloc.effectiveAllocation = 0;
829841
alloc.assetHolder = address(0); // This avoid collect() to be called
830842

843+
// -- Effects --
844+
845+
// When all allocations processed then burn unclaimed fees and prune rebate pool
846+
if (rebatePool.unclaimedAllocationsCount == 0) {
847+
_burnTokens(rebatePool.unclaimedFees());
848+
delete rebates[closedAtEpoch];
849+
}
850+
831851
// When there are tokens to claim from the rebate pool, transfer or restake
832852
if (tokensToClaim > 0) {
833853
// Assign claimed tokens
@@ -850,7 +870,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
850870
epochManager().currentEpoch(),
851871
closedAtEpoch,
852872
tokensToClaim,
853-
pool.unclaimedAllocationsCount,
873+
rebatePool.unclaimedAllocationsCount,
854874
delegationRewards
855875
);
856876
}
@@ -893,7 +913,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
893913
address _assetHolder,
894914
bytes32 _metadata
895915
) internal {
896-
require(_onlyAuth(_indexer), "Caller must be authorized");
916+
require(_onlyAuth(_indexer), "!auth");
897917

898918
Stakes.Indexer storage indexerStake = stakes[_indexer];
899919

@@ -982,20 +1002,23 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
9821002

9831003
// Validate ownership
9841004
if (epochs > maxAllocationEpochs) {
985-
// Verify that the allocation owner or delegator is settling
986-
require(_onlyAuthOrDelegator(alloc.indexer), "Caller must be authorized");
1005+
// Verify that the allocation owner or delegator is closing
1006+
require(_onlyAuthOrDelegator(alloc.indexer), "!auth");
9871007
} else {
988-
// Verify that the allocation owner is settling
989-
require(_onlyAuth(alloc.indexer), "Caller must be authorized");
1008+
// Verify that the allocation owner is closing
1009+
require(_onlyAuth(alloc.indexer), "!auth");
9901010
}
9911011

992-
// Close the allocation and start counting a period to finalize any other
993-
// withdrawal.
1012+
// Close the allocation and start counting a period to settle remaining payments from
1013+
// state channels.
9941014
alloc.closedAtEpoch = currentEpoch;
9951015
alloc.effectiveAllocation = _getEffectiveAllocation(alloc.tokens, epochs);
9961016

997-
// Send funds to rebate pool and account the effective allocation
1017+
// Account collected fees and effective allocation in rebate pool for the epoch
9981018
Rebates.Pool storage rebatePool = rebates[currentEpoch];
1019+
if (!rebatePool.exists()) {
1020+
rebatePool.init(alphaNumerator, alphaDenominator);
1021+
}
9991022
rebatePool.addToPool(alloc.collectedFees, alloc.effectiveAllocation);
10001023

10011024
// Distribute rewards if proof of indexing was presented
@@ -1025,17 +1048,17 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
10251048
}
10261049

10271050
/**
1028-
* @dev Withdraw and collect funds for an allocation.
1029-
* @param _allocationID Allocation that is receiving collected funds
1051+
* @dev Collect query fees for an allocation from the state channel.
1052+
* @param _allocationID Allocation that is receiving query fees
10301053
* @param _from Source of collected funds for the allocation
1031-
* @param _tokens Amount of tokens to withdraw
1054+
* @param _tokens Amount of tokens to collect
10321055
*/
10331056
function _collect(
10341057
address _allocationID,
10351058
address _from,
10361059
uint256 _tokens
10371060
) internal {
1038-
uint256 rebateFees = _tokens;
1061+
uint256 queryFees = _tokens;
10391062

10401063
// Get allocation
10411064
Allocation storage alloc = allocations[_allocationID];
@@ -1047,26 +1070,29 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
10471070
"Allocation must be active or closed"
10481071
);
10491072

1050-
// Collect protocol fees to be burned
1051-
uint256 protocolFees = _collectProtocolFees(rebateFees);
1052-
rebateFees = rebateFees.sub(protocolFees);
1073+
// Calculate protocol fees to be burned
1074+
uint256 protocolFees = _collectProtocolFees(queryFees);
1075+
queryFees = queryFees.sub(protocolFees);
10531076

1054-
// Calculate curation fees only if the subgraph deployment is curated
1055-
uint256 curationFees = _collectCurationFees(alloc.subgraphDeploymentID, rebateFees);
1056-
rebateFees = rebateFees.sub(curationFees);
1077+
// Calculate curation fees (only if the subgraph deployment is curated)
1078+
uint256 curationFees = _collectCurationFees(alloc.subgraphDeploymentID, queryFees);
1079+
queryFees = queryFees.sub(curationFees);
10571080

1058-
// Collect funds for the allocation
1059-
alloc.collectedFees = alloc.collectedFees.add(rebateFees);
1081+
// Collect funds on the allocation
1082+
alloc.collectedFees = alloc.collectedFees.add(queryFees);
10601083

10611084
// When allocation is closed redirect funds to the rebate pool
10621085
// This way we can keep collecting tokens even after the allocation is closed and
10631086
// before it gets to the finalized state.
10641087
if (allocState == AllocationState.Closed) {
10651088
Rebates.Pool storage rebatePool = rebates[alloc.closedAtEpoch];
1066-
rebatePool.fees = rebatePool.fees.add(rebateFees);
1089+
rebatePool.fees = rebatePool.fees.add(queryFees);
10671090
}
10681091

1069-
// TODO: for consistency we could burn protocol fees here
1092+
// -- Effects --
1093+
1094+
// Burn protocol fees
1095+
_burnTokens(protocolFees);
10701096

10711097
// Send curation fees to the curator reserve pool
10721098
if (curationFees > 0) {
@@ -1085,7 +1111,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
10851111
_allocationID,
10861112
_from,
10871113
curationFees,
1088-
rebateFees
1114+
queryFees
10891115
);
10901116
}
10911117

@@ -1240,11 +1266,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
12401266
if (protocolPercentage == 0) {
12411267
return 0;
12421268
}
1243-
uint256 protocolFees = uint256(protocolPercentage).mul(_tokens).div(MAX_PPM);
1244-
if (protocolFees > 0) {
1245-
graphToken().burn(protocolFees);
1246-
}
1247-
return protocolFees;
1269+
return uint256(protocolPercentage).mul(_tokens).div(MAX_PPM);
12481270
}
12491271

12501272
/**
@@ -1362,4 +1384,14 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
13621384

13631385
return totalRewards;
13641386
}
1387+
1388+
/**
1389+
* @dev Burn tokens held by this contract.
1390+
* @param _amount Amount of tokens to burn
1391+
*/
1392+
function _burnTokens(uint256 _amount) internal {
1393+
if (_amount > 0) {
1394+
graphToken().burn(_amount);
1395+
}
1396+
}
13651397
}

contracts/staking/StakingStorage.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ contract StakingV1Storage is Managed {
2020
// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%)
2121
uint32 public protocolPercentage;
2222

23-
// Need to pass this period for channel to be finalized
23+
// Period for allocation to be finalized
2424
uint32 public channelDisputeEpochs;
2525

2626
// Maximum allocation time
2727
uint32 public maxAllocationEpochs;
2828

29+
// Rebate ratio
30+
uint32 public alphaNumerator;
31+
uint32 public alphaDenominator;
32+
2933
// Indexer stakes : indexer => Stake
3034
mapping(address => Stakes.Indexer) public stakes;
3135

contracts/staking/libs/Cobbs.sol

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
3+
Copyright 2019 ZeroEx Intl.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
17+
*/
18+
19+
pragma solidity ^0.6.12;
20+
pragma experimental ABIEncoderV2;
21+
22+
import "./LibFixedMath.sol";
23+
24+
library LibCobbDouglas {
25+
/// @dev The cobb-douglas function used to compute fee-based rewards for
26+
/// staking pools in a given epoch. This function does not perform
27+
/// bounds checking on the inputs, but the following conditions
28+
/// need to be true:
29+
/// 0 <= fees / totalFees <= 1
30+
/// 0 <= stake / totalStake <= 1
31+
/// 0 <= alphaNumerator / alphaDenominator <= 1
32+
/// @param totalRewards collected over an epoch.
33+
/// @param fees Fees attributed to the the staking pool.
34+
/// @param totalFees Total fees collected across all pools that earned rewards.
35+
/// @param stake Stake attributed to the staking pool.
36+
/// @param totalStake Total stake across all pools that earned rewards.
37+
/// @param alphaNumerator Numerator of `alpha` in the cobb-douglas function.
38+
/// @param alphaDenominator Denominator of `alpha` in the cobb-douglas
39+
/// function.
40+
/// @return rewards Rewards owed to the staking pool.
41+
function cobbDouglas(
42+
uint256 totalRewards,
43+
uint256 fees,
44+
uint256 totalFees,
45+
uint256 stake,
46+
uint256 totalStake,
47+
uint32 alphaNumerator,
48+
uint32 alphaDenominator
49+
) internal pure returns (uint256 rewards) {
50+
int256 feeRatio = LibFixedMath.toFixed(fees, totalFees);
51+
int256 stakeRatio = LibFixedMath.toFixed(stake, totalStake);
52+
if (feeRatio == 0 || stakeRatio == 0) {
53+
return rewards = 0;
54+
}
55+
// The cobb-doublas function has the form:
56+
// `totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)`
57+
// This is equivalent to:
58+
// `totalRewards * stakeRatio * e^(alpha * (ln(feeRatio / stakeRatio)))`
59+
// However, because `ln(x)` has the domain of `0 < x < 1`
60+
// and `exp(x)` has the domain of `x < 0`,
61+
// and fixed-point math easily overflows with multiplication,
62+
// we will choose the following if `stakeRatio > feeRatio`:
63+
// `totalRewards * stakeRatio / e^(alpha * (ln(stakeRatio / feeRatio)))`
64+
65+
// Compute
66+
// `e^(alpha * ln(feeRatio/stakeRatio))` if feeRatio <= stakeRatio
67+
// or
68+
// `e^(alpa * ln(stakeRatio/feeRatio))` if feeRatio > stakeRatio
69+
int256 n = feeRatio <= stakeRatio
70+
? LibFixedMath.div(feeRatio, stakeRatio)
71+
: LibFixedMath.div(stakeRatio, feeRatio);
72+
n = LibFixedMath.exp(
73+
LibFixedMath.mulDiv(
74+
LibFixedMath.ln(n),
75+
int256(alphaNumerator),
76+
int256(alphaDenominator)
77+
)
78+
);
79+
// Compute
80+
// `totalRewards * n` if feeRatio <= stakeRatio
81+
// or
82+
// `totalRewards / n` if stakeRatio > feeRatio
83+
// depending on the choice we made earlier.
84+
n = feeRatio <= stakeRatio
85+
? LibFixedMath.mul(stakeRatio, n)
86+
: LibFixedMath.div(stakeRatio, n);
87+
// Multiply the above with totalRewards.
88+
rewards = LibFixedMath.uintMul(n, totalRewards);
89+
}
90+
}

0 commit comments

Comments
 (0)