-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add stake delegate
functionality
#121
Conversation
@Shadowfiend ok, I think I found a good way to avoid duplication in staking contracts, they now both inherit from also I'm thinking to put |
Will be having a look at this today. |
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.
Left some comments. This definitely feels cleaner than the previous pass! It feels like we're missing a couple of corner cases/potential nuisance attacks, so I'll think about those and try to put them in the PR later today or tomorrow morning.
|
||
mapping(address => uint256) public stakeBalances; | ||
mapping(address => address) public delegatorFor; | ||
mapping(address => address) public operatorFor; |
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.
Thoughts on stakerToOperator
and operatorToStaker
as these names?
|
||
/** | ||
* @dev Gets the stake balance of the specified address. | ||
* @param _staker The address to query the balance of. |
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 isn't necessarily the “staker” though, right? Should we name this _stakerOrOperator
? Or even simply _address
, since this parameter can be any address, staker, operator, or random?
* @param _staker The address to query the balance of. | ||
* @return The amount staked by the passed address. | ||
*/ | ||
function stakeBalanceOf(address _staker) public view returns (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.
Should we be checking for a 0 address?
} | ||
|
||
/** | ||
* @dev Delegates your stake balance to a specified address. |
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 little explanation of the implications would be good here (e.g., one address can only be an operator for one other address, delegating an address's stake means that address can no longer use the stake unless the delegate is removed, etc).
/** | ||
* @title Stake Delegatable | ||
* @dev A contract that allows delegate your stake balance to any address | ||
* that is not already a staker. |
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 probably worth noting in this doc that delegator
refers to a staker who has delegated their stake to another address, while operator
refers to an address that has had a stake delegated to it.
require(previousDelegator == address(0)); | ||
|
||
// Release previous operator address when you delegate stake to a new one. | ||
address previousOperator = operatorFor[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.
We'll need to consider how this interacts with the client. We may want it require a separate transaction to remove an existing operator…
await exceptThrow(stakingContract.delegateStakeTo(account_two)); | ||
}); | ||
|
||
it("should be able to delegate stake to an 'operator' address to represet your stake balance", async function() { |
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.
represent*
assert.equal(await stakingProxy.balanceOf(account_four), 200, "Updated operator account should represent delegator's stake balance."); | ||
}); | ||
|
||
it("should be able to remove delagated operator address that represent your stake balance", async function() { |
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.
delegated* and represents*
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.
Okay, so I wrote out some of the scenarios here. Here are a few situations I want to make sure we have tests/solutions for:
- Ensuring changes to the parent stake are reflected in an operator (e.g., in the case of a grant revocation, stake withdrawal, or additional staking).
- If an operator address stakes, what do we do? Right now we drop the operator status of that address, but this could result in adverse effects for the address the operator is operating for. What happens if operator B is using their stake? If operator B now stakes an amount of KEEP that is less than what address A had staked, any system behavior based on total stake amount is no longer guaranteed to be accurate. We should probably discuss this some more on Flowdock.
- If a staker address drops another address's operator status directly, what do we do? Right now this happens instantaneously, and the operator is simply cut off. For the threshold relay, this has no adverse effects; however, if the operator is running keeps, they may have ETH bonded for those keeps. What happens to the data in those keeps?
I think we may need a handshake for both delegating and undelegating stakes… But a handshake for undelegating stakes is a potential attack vector (delegate stake A to operator B, try to undelegate because staker A wants to profit from their stake directly, operator B will not complete the handshake), so we may want undelegating to come with a time window (e.g., undelegating completes after two weeks unless the operator agrees to it before then).
} | ||
|
||
/** | ||
* @dev Delegates your stake balance to a specified address. | ||
* @param _operator Address to where you want to delegate your balance. | ||
* @param _operator Address to where you want to delegate your |
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.
Let's move this into the dev docs. Parameter docs are generally short and sweet.
* @param _operator Address to where you want to delegate your | ||
* balance. An address can only have one operator address. You can | ||
* delegate stake to any ethereum address as long as it's not an | ||
* actual staker or is being used as an operator by someone already. |
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.
Not a current staker*. In fact, perhaps “as long as it isn't currently staking or operating someone else's stake”?
* delegate stake to any ethereum address as long as it's not an | ||
* actual staker or is being used as an operator by someone already. | ||
* In the current implementation delegating stake doesn't hide the | ||
* stake balance on your main stake address. |
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.
Given this, should we also have some sort of hasOperator
or something that would allow checking whether a given address's staking balance is being operated by a different address?
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 you think it will be useful? Is it going to be in the Keep Client somewhere to check this value?
My initial thoughts were that Keep Client doesn't care if it's a staker or operator. In case of new operator assigned, Keep Client is just informed by unstaked(for staker) and staked(for operator) events and when operator is removed the reverse unstaked(for operator) and staked(for staker).
So it's reusing existing events, and if we decide to remove operator functionality completely there is no need to update Keep Client software
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.
Staking and operation are the kinds of things that I would expect some people will want to interact with outside of the client. That is, enabling them to understand their current staker/operator situation feels like a valuable thing to do, because I might stake, then set up an operator, then walk away for a few months, then come back and not remember my setup, but want to know what it is.
Perhaps a good way to look at it is “we may want to put a basic UI on top of this”, alongside the staking UI.
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 see, yeah I'll make sure to update the staking UI for this, as I remember there are automatic getters for mappings, so probably can just read operatorToStaker
mapping
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.
Keep Client is just informed by unstaked(for staker) and staked(for operator) events and when operator is removed the reverse unstaked(for operator) and staked(for staker).
btw do you have any concerns with this? I'm about to update staking contracts with some additional "ifs" to emit events with operator address if it is set. It doesn't look elegant but not sure if there is better way
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 think we may need a handshake for both delegating and undelegating stakes… But a handshake for undelegating stakes is a potential attack vector (delegate stake A to operator B, try to undelegate because staker A wants to profit from their stake directly, operator B will not complete the handshake), so we may want undelegating to come with a time window (e.g., undelegating completes after two weeks unless the operator agrees to it before then).
I'm pretty sure we're ok with just a handshake for delegating to make sure you can't add someone else address.
I doubt anyone would want to add an operator address that they don't have ownership for.
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 they would add an operator they don't own, but I'd like to be prepared when someone inevitably does it out of silliness heh (or to be predatory).
The problem with unstaking being the same as undelegating is, when account A stakes and then bonds ETH, they bond their ETH. If they unstake and their ETH is lost, that's fine. If account A delegates to account B, and B bonds ETH, they're not bonding A's ETH. So if A now undelegates, particularly if it's instant, B's ETH is gone, and A has suffered no harm.
@mhluongo thoughts 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.
when account A stakes and then bonds ETH, they bond their ETH.
ah I think i misunderstood, how stakers bond their ETH? they don't send any ETH to participate in the relay, right?
I thought it's only people who request a service send ETH to pay
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.
For the relay, requestors just pay ETH. For keeps, requestors also pay ETH, but they can require that a keep provider bonds ETH in addition to their staked KEEP.
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 delegates to account B, and B bonds ETH, they're not bonding A's ETH. So if A now undelegates, particularly if it's instant, B's ETH is gone, and A has suffered no harm.
I guess we can make this additional "ETH bonding" feature into a separate contract and have a more flexible setup there, for example that operator account B doesn't have to bond its own ETH and can ask delegator A to bond ETH instead?
My feeling we should not worry about this until we start working implementing keeps functionality. A separate contract should be flexible enough to route everything for every case nicely
@Shadowfiend let me know if we're missing anything else here, would be good to merge asap ideally so I can update the dashboard as well pls see my latest comment in regard to ETH bonding |
I'm not wanting to do anything else to the dashboard until we've had a chance to review what's there. We also need to split that PR up. But! That doesn't mean I don't want to merge this PR :) Let's force @mhluongo to look at it when we can tie him to a chair next week <_< |
@mhluongo I think I'm ready here, pls have a look |
stake delegate
functionality (Alternative approach)stake delegate
functionality
Staker account A is now able to say that account B is allowed to operate a node using A's stake. This doesn't affect the rest of the logic i.e. withdrawals, punishments etc... those are are still on A's stake.
Add two step approach (Request/Approve) to simplify and secure delegate stake functionality between a staker and an operator.
93097ab
to
c265be3
Compare
Chair tying was a catastrophic failure, my apologies... @ngrinkevich is this PR salvageable with its conflicts? If so, I think let's resolve conflicts and let @pschlump merge if he is satisfied. |
@Shadowfiend Merge conflicts solved @pschlump could you pls have a look at this one? |
Looks like we're still getting some out of gas errors here. I'm dismissing my review to let Philip take over from here. |
Split heavy ones and remove duplicates
Cleaned up tests, @pschlump pls check |
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.
Took a first pass. I see why you weren't a fan of delegate
and delegator
as terms- they're incredibly confusing, especially as "delegate" is both a verb and noun.
I haven't run this yet, or fully grokked the tests- working on it.
|
||
/** | ||
* @title Stake Delegatable | ||
* @dev A contract that allows delegate your stake balance to any address |
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 contract that allows delegate
I suspect this should read "that allows a staker to delegate their staked balance to any..."?
* @dev Only non staker addresses can be passed to the functions with this modifier. | ||
*/ | ||
modifier notStaker(address _address) { | ||
require(stakeBalances[_address] == 0, "Provided address is not a staker."); |
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'd use <=
rather than ==
as a stronger guarantee against future code mistakes. I understand that stakeBalances
is a uint256
mapping of course, but in case it's ever signed in a future code update, you want to cover the negative case.
mapping(address => address) public delegateToDelegator; | ||
|
||
/** | ||
* @dev Gets the stake balance of the specified address. |
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.
Doc nit - "staked balance" rather than "stake balance"
* @param _address The address to query the balance of. | ||
* @return The amount staked by the passed address. | ||
*/ | ||
function stakeBalanceOf(address _address) |
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.
stakeBalanceOf()
sounds like a verb for staking tokens. Consider eg getStakedBalance()
or stakedBalanceOf()
// If provided address is a delegator return zero balance since the | ||
// balance is delegated to a delegate. | ||
address delegate = delegateToDelegator[_address]; | ||
if (delegate != address(0) && delegatorToDelegate[delegate] == _address) { |
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 appears this branch will never be hit- it's the same logic as the branch above, with a single variable name changed (delegator
-> delegate
).
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, now I see. This terminology continues to be difficult to parse.
public | ||
notNull(_address) | ||
{ | ||
delegatorToDelegate[msg.sender] = _address; |
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 seems like this would be safer if it confirmed the delegator has already requested this address be a delegate?
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 also seriously afraid we're switching delegate
and delegator
in these functions. Can we get some solid term definitions somewhere?
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 also seriously afraid we're switching delegate and delegator in these functions.
We're not actually switching, it's like a handshake, if those two mapping records match then delegation works, if any of them changes, delegation stops straight away.
It seems like this would be safer if it confirmed the delegator has already requested this address be a delegate?
with the current logic delegating won't work until both delegator and delegate add each other,
but I guess yeah would be safer to force delegator approve delegate address first (handshake only can be initiated by delegator)
Can we get some solid term definitions somewhere?
I'm also gonna look at ethereum/EIPs#1207 @Shadowfiend found, maybe we could use terms and concepts from there
/** | ||
* @dev Requests to delegate stake for a specified address. | ||
* An address must approve you first to delegate by calling | ||
* requestDelegateFor() method. |
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 second line of the doc here looks like copy-pasta from approveDelegateAt()
} | ||
|
||
/** | ||
* @dev Revert if a delegate try to stake. |
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.
*tries
} | ||
|
||
/** | ||
* @dev Removes delegate for your stake balance. |
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.
Can you explain why you want would to do this? Also... should you ever be allowed to, considering our threat model?
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 thinking first case would be: if a delegate account is compromised then you definitely would like to remove it right? Even though delegate doesn't have any access to transfer balances, withdraw stakes etc.. it still can behave bad in the network and you as the delegator would be punished for that?
This is to cover cases with potential signed integers in the future.
3f87615
to
5f2c3ce
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.
Just a couple of tiny changes.
@@ -0,0 +1,38 @@ | |||
pragma solidity ^0.4.21; |
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 know that this is a little detail - but let's get all of our contracts and tests up to version 0.4.24
so that they all work the same way.
@@ -0,0 +1,38 @@ | |||
pragma solidity ^0.4.21; |
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, 0.4.24.
Closing this for now, we'll be specing this out before continuing (see this flowdock thread)_. Leaving the branch intact so we can work off of it if we choose to. |
Stake delegate alternative approach to the previous one
Staker account A (delegator) is now able to say that account B (operator) is allowed to operate a node using A's stake. This doesn't affect the rest of the logic i.e. withdrawals, punishments etc... those are are still on A's stake.
Related issue #92