-
Notifications
You must be signed in to change notification settings - Fork 465
Conversation
bdd3d14
to
d355356
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic seems solid for the most part. Really nice work on the redesigned logic, and all things considered, you got this out quickly!
I had some nits and comments, which I think should be addressed before merging this PR.
uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId); | ||
uint256 stakeHeldByPoolOperator = getActivatedAndUndelegatedStake(getStakingPoolOperator(poolId)); | ||
uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId).current; | ||
uint256 stakeHeldByPoolOperator = getActiveStake(getStakingPoolOperator(poolId)).current; // @TODO Update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What needs to be updated here? Can this "TODO" be removed?
@@ -218,9 +223,20 @@ contract MixinExchangeFees is | |||
); | |||
|
|||
// record reward in vault | |||
_recordDepositInStakingPoolRewardVault(activePools[i].poolId, reward); | |||
bool rewardForOperatorOnly = activePools[i].delegatedStake == 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used anywhere else? I like the fact that we're naming this boolean, but I think a comment in the call to recordDepositFor
would be sufficient and it would save us from needing to use another stack slot. Alternatively, I think it could make sense to start making use of Solidity's block scoping features to make sure that the stack stays clean.
Ex.
uint256 poolPortion;
{
bool rewardForOperatorOnly = activePools[i].delegatedStake == 0; // This will get dropped after being used.
(, poolPortion) = rewardVault.recordDepositFor(activePools[i].poolId, reward, rewardForOperatorOnly);
}
The reason I call this out is that I think that reducing the number of stack variables in the top of a function's scope can be a boon to code maintenance since it reduces the likelihood that adding a stack variable will push the function over the stack limit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I actually thought that Solidity would stop managing stack variables once they were finished being used without needing an explicit scope ~ just verified this is not the case. I'm cool with inlining.
@@ -42,4 +42,6 @@ contract MixinConstants is | |||
uint64 constant internal INITIAL_EPOCH = 0; | |||
|
|||
uint64 constant internal INITIAL_TIMELOCK_PERIOD = INITIAL_EPOCH; | |||
|
|||
uint256 constant internal MIN_TOKEN_VALUE = 1000000000000000000; // 10**18 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out that 10 ** 18
will not be inlined by the Solidity compiler. I've verified this by debugging a transaction through the Remix debugger that uses this constant. With this in mind:
Nit: Use 10 ** 18
for readability.
|
||
// registered 0x Exchange contracts | ||
mapping (address => bool) internal validExchanges; | ||
|
||
// ZRX vault | ||
IZrxVault internal zrxVault; | ||
|
||
// Rebate Vault |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Can you update this comment? It isn't immediately clear to me what the difference between this "Rebate Vault" and the below "Rebate Vault", aside from the fact that I assume that this vault stores ETH.
@@ -0,0 +1,71 @@ | |||
/* | |||
|
|||
Copyright 2018 ZeroEx Intl. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Update to 2019
:)
) | ||
internal | ||
private |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand why this is being made private, but it may be wise to leave this as internal
for the purposes of unit testing. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think you're right. For unit testing these will have to be made internal =\
internal | ||
{ | ||
IZrxVault _zrxVault = zrxVault; | ||
require( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason not to use Rich Errors in this contract?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function doesn't do much other than call _zrxVault.depositFrom(owner, amount)
. I'd be in favor of removing it completely.
@@ -0,0 +1,196 @@ | |||
/* | |||
|
|||
Copyright 2018 ZeroEx Intl. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
Copyright 2018 ZeroEx Intl. | |
Copyright 2019 ZeroEx Intl. |
/// @param balancePtrA first storage pointer. | ||
/// @param balancePtrB second storage pointer. | ||
/// @return true iff pointers are equal. | ||
function _arePointersEqual( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
IStructs.DelayedBalance storage balancePtr, | ||
IStructs.DelayedBalance memory balance | ||
) | ||
private |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Same as above re testing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see above)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm probably about 1/4 - 1/3 through this review but won't be able to get to the rest tonight. Will add the rest tomorrow morning!
@@ -218,9 +223,20 @@ contract MixinExchangeFees is | |||
); | |||
|
|||
// record reward in vault | |||
_recordDepositInStakingPoolRewardVault(activePools[i].poolId, reward); | |||
bool rewardForOperatorOnly = activePools[i].delegatedStake == 0; | |||
(, uint256 poolPortion) = rewardVault.recordDepositFor(activePools[i].poolId, reward, rewardForOperatorOnly); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a pretty long line. We can break it down into multiple lines, which would also make it easier to inline the rewardForOperatorOnly
calculation with a comment to the right.
// fees collected this epoch | ||
mapping (bytes32 => uint256) internal protocolFeesThisEpochByPool; | ||
|
||
// pools that were active in the current epoch | ||
bytes32[] internal activePoolsThisEpoch; | ||
|
||
// mapping from POol Id to Shadow Rewards | ||
mapping (bytes32 => uint256) internal shadowRewardsByPoolId; | ||
// reward ratios by epoch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: spell out mapping from Pool Id to Epoch to Reward Ratio
|
||
|
||
/// @dev This mixin contains logic for managing ZRX tokens and Stake. | ||
/// Stake is minted when ZRX is deposited and burned when ZRX is withdrawn. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The diagrams will live in the spec ideally.
uint256 currentWithdrawableStake = getWithdrawableStake(owner); | ||
require( | ||
amount <= currentWithdrawableStake, | ||
"INSUFFICIENT_FUNDS" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rich revert?
@@ -25,6 +25,9 @@ import "../src/interfaces/IStructs.sol"; | |||
|
|||
|
|||
contract TestStorageLayout is | |||
MixinDeploymentConstants, | |||
Ownable, | |||
MixinConstants, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are already inherited by MixinStorage
(I suspect you are using a script to generate these).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, to remove any ambiguities during linearization it will print out the full ancestral tree. Ugly, but it works. I'll create a task to remove frivolous imports for readability to see if we can make this a bit prettier. In the meantime I'll cleanup the ones in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW this is an example demonstrating why explicitly inheriting the ancestral tree works:
Ex:
contract A {}
contract B {}
contract C {}
contract D is A, B {}
Suppose now that there is a contract E that will inherit from B{} and D{}
The following follows "most base-like" to "most-derived":
contract E is B, D {}
However, it unfolds to: B{}, A{}, B{}, D{}, E{} - which is wrong because B{} appears both before and after A{}.
The correct contract inheritance for E{} should be:
contract E is A{}, B{}, D{}
So, even though contract E{} does not directly depend on A{} - it still must be included in the dependency list.
uint256 newMembersBalance = uint256(balance.membersBalance).safeAdd(poolPortion); | ||
|
||
// save new balances | ||
balance.operatorBalance = LibSafeDowncast.downcastToUint96(newOperatorBalance); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use newOperatorBalance.downcastToUint96
here
using LibSafeMath for uint256; | ||
|
||
// mapping from Owner to ETH balance | ||
mapping (address => uint256) internal balances; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: _balances
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: we chatted offline. This will be done in a separate PR.
|
||
/// @dev This vault manages ETH. | ||
contract EthVault is | ||
Authorizable, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these are unnecessary exccept IEthVault
and MixinVaultCore
/// Note that only the Staking contract can call this. | ||
/// Note that this can only be called when *not* in Catostrophic Failure mode. | ||
/// @param amount of ETH to withdraw. | ||
function withdraw(uint256 amount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of these functions are exactly the same as in the ZrxVault. Maybe the repeat functions should live in MixinVaultCore
?
@@ -36,6 +36,7 @@ import "../interfaces/IStakingEvents.sol"; | |||
contract MixinScheduler is | |||
IStakingEvents, | |||
MixinDeploymentConstants, | |||
Ownable, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be removed.
e6d4dbf
to
dbbcf66
Compare
68c2ab6
to
20cabc1
Compare
@@ -443,4 +455,35 @@ library LibStakingRichErrors { | |||
poolId | |||
); | |||
} | |||
|
|||
function EthVaultNotSet() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these rich reverts should end with Error
.
/// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault. Unstake to retrieve the ZRX. | ||
/// Stake is in the 'Active' state. | ||
/// @param amount of ZRX to stake. | ||
function stake(uint256 amount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't we want newly minted stake to only become active at the next epoch as well? Otherwise one can stake before the very end of an epoch and dilute rewards for everyone else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: we chatted about this offline, but basically this won't dilute rewards because the stake isn't delegated.
external | ||
{ | ||
_mintStake(msg.sender, amount); | ||
address payable owner = msg.sender; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably happens elsewhere too if I had to guess. We might want to do a search for owner
usage throughout the staking contracts.
} | ||
|
||
/// @dev States that stake can exist in. | ||
enum StakeState { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally like Status
> State
here. I feel like State
implies storage to some degree (also consistent with other enums).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah I like that better too. Although a "status machine" doesn't quite have the same ring as a "state machine" 🌝
/// @dev Info used to describe a state. | ||
/// @param state of the stake. | ||
/// @param poolId Unique Id of pool. This is set when state=DELEGATED. | ||
struct StakeStateInfo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could probably just be StakeInfo
{ | ||
// sanity check on eth vault | ||
IEthVault _ethVault = ethVault; | ||
if (address(_ethVault) == address(0)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hoping we can remove this check too if _ethVault
is set in the constructor.
@@ -6,6 +6,8 @@ import "./LibSafeMathRichErrors.sol"; | |||
|
|||
library LibSafeMath { | |||
|
|||
using LibSafeMath for uint256; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is used anywhere.
@@ -2,7 +2,7 @@ | |||
|
|||
/* | |||
|
|||
Copyright 2018 ZeroEx Intl. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this file at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's removed by #2109
// mapping from Owner to Amount Staked | ||
mapping (address => uint256) internal stakeByOwner; | ||
// mapping from Owner to Amount of Active Stake | ||
mapping (address => IStructs.DelayedBalance) internal activeStakeByOwner; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: all of these variables should be prefixed with _
. Probably cleaner to do this in another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I've created a task for this.
// value is always zero in these two scenarios: | ||
// 1. The current epoch is zero: delegation begins at epoch 1 | ||
// 2. The owner's delegated is current as of this epoch: their rewards have been moved to the ETH vault. | ||
if (currentEpoch == 0 || delegatedStake.lastStored == currentEpoch) return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These checks should probably be reversed, since we don't need currentEpoch == 0
to be checked after epoch 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good eye! I agree!
cd23a16
to
b202a74
Compare
(, uint256 poolPortion) = rewardVault.recordDepositFor( | ||
activePools[i].poolId, | ||
reward, | ||
activePools[i].delegatedStake == 0 // true -> reward is for operator only |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, this comment makes up for the variable IMHO 👍
MixinStorage, | ||
MixinScheduler | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I don't think this line should be here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, yeah I think you're right. Seems to be 50/50 whether we have a newline here or not lol.
amount, | ||
getActivatedStake(owner) | ||
)); | ||
// cache the current withdrawal amoiunt, which may change if we're moving out of the inactive status. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// cache the current withdrawal amoiunt, which may change if we're moving out of the inactive status. | |
// cache the current withdrawal amount, which may change if we're moving out of the inactive status. |
address payable owner, | ||
uint256 amount | ||
) | ||
private |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of these functions have been made private. We should make them internal
for unit testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: we chatted about this offline. These will be set to internal
on as needed, unless a better alternative arises in the meantime.
IStructs.Fraction memory mostRecentCumulativeRewards = cumulativeRewardsByPoolPtr[cumulativeRewardsLastStored]; | ||
|
||
// compute new cumulative reward | ||
(uint256 numerator, uint256 denominator) = LibFractions.addFractions( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make more sense for addFractions
to return a fraction? It seems like we create a Fraction object a significant amount of the time after calling this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mm idk, since this is a general util library I'd prefer to not force a struct on future users. We actually only convert it to the Fraction struct in this fn for storage.
b202a74
to
da83f75
Compare
…olidityOnly-Squashed-Tests-Squashed Tests for new staking mechanics
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, some awesome readability improvements in here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work bud! You definitely deserve a Chimay after this one 👍
Just a couple of comments, but they are not merge-blocking.
{ | ||
// update balance | ||
uint256 amount = msg.value; | ||
balances[owner] = balances[owner].safeAdd(msg.value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might as well use amount
here since msg.value
was cached there, right? This said, CALLVALUE
is typically actually cheaper than using a stack variable (it's a Gbase opcode), so I'd be in favor of just using msg.value
for both uses and removing amount
entirely, unless there is a readability concern.
// update balance | ||
// note that this call will revert if trying to withdraw more | ||
// than the current balance | ||
balances[owner] = balances[owner].safeSub(amount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice reentrancy protection 👍
Description
This PR introduces a new mechanism for managing stake and tracking rewards. It is more flexible than the original implementation, allowing for a better user experience.
Note - Skip the Typescript in this review. There is a separate PR (#2126) that updates client-side code and adds tests. It will be merged into this PR before merging into
3.0
.Design Principles
Stake State Algorithm
There are three states that stake can exist in: Active, Inactive or Delegated. Each state has three fields:
Inactive stake includes a Withdrawable field (W) that reflects how much stake can be withdrawn at any given time.
Example
Reward Tracking Mechanics
Reward Vault vs Eth Vault
The reward vault tracks the balance of each pool: how much belongs to the operator and how much is to be split between the delegators. ETH is deposited into this vault when an epoch is finalized.
The ETH Vault stores balances on a per-address basis. Value is moved from the reward vault to the ETH vault when a delegator changes their stake in a given pool.
Testing instructions
Checklist:
[WIP]
if necessary.