Skip to content
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

Closed
wants to merge 25 commits into from

Conversation

ngrinkevich
Copy link
Contributor

@ngrinkevich ngrinkevich commented May 18, 2018

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

@ngrinkevich
Copy link
Contributor Author

@Shadowfiend ok, I think I found a good way to avoid duplication in staking contracts, they now both inherit from StakeDelegatable. I haven't put events yet, should I go in detail like in the previous PR, and fire them for every case? i.e. operator staked, delegation removed ... or we add them later as we go

also I'm thinking to put delegateStakeTo and removeDelegate methods on staking proxy, a single function that will call those on every child contract

@Shadowfiend
Copy link
Contributor

Will be having a look at this today.

Copy link
Contributor

@Shadowfiend Shadowfiend left a 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;
Copy link
Contributor

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.
Copy link
Contributor

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) {
Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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];
Copy link
Contributor

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() {
Copy link
Contributor

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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delegated* and represents*

Copy link
Contributor

@Shadowfiend Shadowfiend left a 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
Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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

@ngrinkevich
Copy link
Contributor Author

@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

@Shadowfiend
Copy link
Contributor

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 <_<

@ngrinkevich
Copy link
Contributor Author

@mhluongo I think I'm ready here, pls have a look

@ngrinkevich ngrinkevich changed the title Add stake delegate functionality (Alternative approach) Add stake delegate functionality Jun 25, 2018
@ngrinkevich ngrinkevich force-pushed the delegate-stake-to-operator-alternative branch from 93097ab to c265be3 Compare June 25, 2018 16:53
@Shadowfiend
Copy link
Contributor

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.

@ngrinkevich
Copy link
Contributor Author

@Shadowfiend Merge conflicts solved @pschlump could you pls have a look at this one?

@Shadowfiend
Copy link
Contributor

Looks like we're still getting some out of gas errors here. I'm dismissing my review to let Philip take over from here.

@Shadowfiend Shadowfiend dismissed their stale review August 3, 2018 16:14

Outdated. @pschlump is on.

Split heavy ones and remove duplicates
@ngrinkevich
Copy link
Contributor Author

Cleaned up tests, @pschlump pls check

Copy link
Member

@mhluongo mhluongo left a 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
Copy link
Member

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.");
Copy link
Member

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.
Copy link
Member

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)
Copy link
Member

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) {
Copy link
Member

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).

Copy link
Member

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;
Copy link
Member

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?

Copy link
Member

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?

Copy link
Contributor Author

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.
Copy link
Member

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.
Copy link
Member

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.
Copy link
Member

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?

Copy link
Contributor Author

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?

@ngrinkevich ngrinkevich force-pushed the delegate-stake-to-operator-alternative branch from 3f87615 to 5f2c3ce Compare August 31, 2018 08:54
Copy link
Contributor

@pschlump pschlump left a 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;
Copy link
Contributor

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;
Copy link
Contributor

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.

@Shadowfiend
Copy link
Contributor

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.

@Shadowfiend Shadowfiend deleted the delegate-stake-to-operator-alternative branch May 15, 2019 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants