Snapshot Voting

I think this is a useful point as well. It means that RPL will have two “pillars” of value: insurance for the protocol and governance over the protocol.

Just to think out loud a bit:
Would there ever be a scenario where a person holds a lot of RPL but does not intend to run a Node? Maybe only in the case where they believe RPL will have a higher return potential than straight ETH… Does that matter? – I guess not.

What will Node Operators do when their node reaches maximum collateralization? Under current config, they will probably sell RPL. However, under the plan above, Node Operators may indeed opt to keep RPL in their wallet even after the max collateralization ratio is reached. (This might in turn reduce sell pressure on RPL…)

Would there ever be a scenario where a person holds a lot of RPL but does not intend to run a Node? Maybe only in the case where they believe RPL will have a higher return potential than straight ETH… Does that matter? – I guess not.

Wander specifies staked RPL above. Especially if we’re using effective staked RPL to vote, this would prevent gaming RPL the same way as rETH could be gamed, as only NO’s could vote.

1 Like

Another reason for leaving rETH holders out of snapshot voting does not seem to have been discussed in this forum. It is because by becoming an rETH investor they have effectively placed themselves in the position of being a client of Rocketpool rather than an operator of Rocketpool. Their decision is therefore made on the basis of it being a better investment for them and their reward is the return and security of that investment. If that is unsatisfactory for them over time, they can do what any other paying customer does and withdraw their investment and place it elsewhere. That is how they vote.

RPL holders on the other hand have specifically chosen to hold a token which has governance rights. The ultimate in governance is using that token to vote on operational matters concerning the RP platform. If rETH investors are sufficiently concerned about how RP operates there is nothing stopping them from purchasing RPL.

There is perhaps some benefit in weighting the RPL voting rights so that those using it to secure nodes get a higher vote for that RPL, while those just passively holding still have a vote but at a lower value - maybe 2:1 in favour of staked RPL. That way substantial investors such as Worthalter can have an influence without having to stake. A decision will also have to be made whether the dev wallet is voted by Rocketpool Pty Ltd or if it considered to be a RP DAO treasury resource and therefore unable to vote.

6 Likes

I’ve been swayed to the side of leaving rETH holders out of the vote. I do worry about NO’s having the only say, and even though many NO’s currently believe in the health of the protocol and therefore rETH adoption, that may not always be the case, since rETH adoption doesn’t directly affect the NO once a minipool has already been created. The option to allow non-staked RPL does make sense, as it can be accumulated by rETH holders as well as anyone else who cares to invest in the protocol (Worthalter). I’m a big fan of using only RPL to vote, but effectively staked RPL carrying more weight. Maybe even significantly more weight (4:1). This seems a good compromise while still limiting how much the system can be gamed.

3 Likes

Hmmm… I think it does. The analyses I’ve seen about the long-term value of RPL find it’s more valuable when there are more NOs. If there isn’t demand for rETH, then folks won’t be able to start new minipools and RPL will be less valuable. Perhaps this is “indirect”, but I think the effect is substantial.

Ok so to summarise some of the discuss and add my own feedback:

Remove rETH voting - as this is extremely complex, is unlikely to yield high participation, and node operators have a strong interest in keeping rETH popular

Apply a quorum - we need a quorum to prevent low interest / participation topics being passed. Any thoughts on the percentage? We may have to adjust over time. 33%?

Remove quadratic voting - as this only applies when voting across options. RPIP-4 already includes a square-root of the RPL value so this reduces whale impact. If we wanted to we could go further and have 1/2 sqrt(x) - which would flatten whale impact more?

RPL voting should be effective stake - we should be explicit that the RPL stake is effective stake that way only RPL that is working for the protocol gets counted. It also stops gaming where a node op can stake with no minipools, vote, and unstake

Support/Oppose - to keep things consistent, we could standardise on a support/oppose options for the voting answers. There may be situations where this is not appropriate but I suspect most will fit.

@Wander if we get some sort of consensus on the above, would you like to update the RPIP in the new repo?

3 Likes

Sure thing. FYI, I can’t update the OP with the new official RPIP repository, so I’ll link to it at the bottom of this post.

Remove rETH voting - I already submitted a simple PR to remove rETH voting :+1:

Quorum - I agree this is necessary, though I’m worried that 33% might be too high. Maybe we could do a test vote somehow to see how many “politically engaged” NOs we have?

Remove quadratic voting - Personally, I really like the idea of flattening further with the 1/2 sqrt(x), so I’ll submit another PR for this as well.

Effective vs. non-effective stake - I don’t have a strong opinion here since I recognize good reasoning on both sides, but I think the consensus is on effective-only voting? I can also submit a PR here if so.

Support/Oppose - FYI for lazy readers, this is already how RPIP-4 is written :stuck_out_tongue:

@langers I’m also happy to add you, @nickdoherty, or anyone else as authors, too. I believe there’s still a bit of research and math needed in the security section.

1 Like

I think it’s worth noting that the person operating a node and the person owning the RPL staked on that node aren’t necessarily equivalent going forward. Think patricio arrangement, NOA, SaaS smart contracts, …

Without that connection, I have a pretty hard time seeing how node operator interests fully capture the interests of the protocol as a whole including rETH holders.

I agree, staked RPL votes don’t perfectly capture individual participants’ interests and could even lead to SaaS dominance. However, the best sybil-resistance we have is the cost associated with node creation. To me, this seems like a least-bad solution, since all the other options (straight RPL voting, rETH voting, etc) are clearly worse.

Another, more thorough mechanism might be using some proof-of-humanity analysis like BrightID, but this has its own issues, of course.

Governance problems always reminds me of this quote from JFK: “We do these things not because they are easy but because they are hard.”

I think I would prefer an attempt to represent all stake holders over an attempt at sybil-resistance. What’s the point of having sybil resistance if all voters have shared interests that are detrimental to the protocol.

I suppose this is where we disagree: I don’t believe we can even come close to representing all stakeholders under any voting scheme, so we should go with the least gameable system for the sake of security. Better to have under-representation than exploitable governance.

I also see staked RPL as having at least indirect incentive to consider the health of the protocol at large. It seems far-fetched that NOs would vote in a 50% commission rate, for example, since that would clearly harm the protocol and impact long-term revenue. Maybe you’re more cynical than me here.

No, I think we simply disagree on what the main security issues are.

Excellent thanks @Wander, I have merged the changes so that it reflects the current approach and can be further debated and voted on.

As this is a chicken and egg situation we may need to rely on a Discord poll to legitimise the decision. We can promote the RPIP as an announcement and provide a TLDR.

@knoshua happy for you to supply some solutions to the stakeholder issues presented in this discussion?

1 Like

I think some of the earlier discussion in here that included all RPL, RPL that is in LPs and rETH holders would bring more stakeholders to the table. In general, I think the fact that RPL isn’t a pure governance token but has utility for some protocol participants (but not all) makes this challenging to get right.

Some form of community signaling is definitely better than none though. So if we can get staked RPL voting done sooner, I’m ok with doing that and maybe revisiting this when more formal governance is on the table.

3 Likes

Here’s a quick and dirty way to vote with effectively staked RPL on Snapshot. It was a fun half-day weekend project and it works with no changes to smart node.

Instructions

Setup

Contract

This contract lets your node delegate voting to another address (like your MetaMask). There are two steps: first you register your MetaMask address and get a number which is your registration index, then you send a miniscule amount of ETH from your node to confirm it.

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.14;

interface RocketNodeStakingInterface {
  function getNodeRPLStake(address _nodeAddress) external view returns (uint256);
  function getNodeEffectiveRPLStake(address _nodeAddress) external view returns (uint256);
}

interface ERC20 {
  function balanceOf(address _owner) external view returns (uint256 balance);
}

contract RocketNodeDelegateVote {
  event Registration(address delegateAddress, address nodeAddress, uint256 index);
  event Confirmation(address delegateAddress, address nodeAddress);

  mapping(uint256 => address) unconfirmedDelegateAddress;
  mapping(uint256 => address) unconfirmedNodeAddress;
  uint256 unconfirmedIndex;

  mapping(address => address) delegateToNode;
  mapping(address => address) nodeToDelegate;

  function registerDelegateAddress(address nodeAddress) public returns (uint256) {
    address delegateAddress = msg.sender;
    unconfirmedIndex += 1;
    unconfirmedDelegateAddress[unconfirmedIndex] = delegateAddress;
    unconfirmedNodeAddress[unconfirmedIndex] = nodeAddress;
    emit Registration(delegateAddress, nodeAddress, unconfirmedIndex);
    return unconfirmedIndex;
  }

  receive() external payable {
    uint256 index = msg.value;
    address nodeAddress = unconfirmedNodeAddress[index];
    address delegateAddress = unconfirmedDelegateAddress[index];
    require(delegateAddress != address(0), "Registration invalid");
    require(nodeAddress != address(0), "Node address is invalid");
    require(nodeAddress == msg.sender, "Registration is for another node address");
    require(nodeAddress != delegateAddress, "Node address and delegate address are the same");
    require(delegateToNode[delegateAddress] == address(0), "Delegate address already used");
    delegateToNode[delegateAddress] = nodeAddress;
    nodeToDelegate[nodeAddress] = delegateAddress;
    emit Confirmation(delegateAddress, nodeAddress);
  }

  function undelegate() public {
    address nodeAddress = msg.sender;
    address delegateAddress = nodeToDelegate[nodeAddress];
    delegateToNode[delegateAddress] = address(0);
    nodeToDelegate[nodeAddress] = address(0);
  }

  function getNodeAddressForDelegate(address delegateAddress) public view returns (address) {
    require(nodeToDelegate[delegateAddress] == address(0), "Cannot vote with node address once delegated");
    address nodeAddress = delegateToNode[delegateAddress];
    if (nodeAddress == address(0)) {
      nodeAddress = delegateAddress;
    }
    return nodeAddress; 
  }

  function getNodeRPLStake(address _rocketNodeStakingAddress, address _address) public view returns (uint256) {
    RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(_rocketNodeStakingAddress);
    return rocketNodeStaking.getNodeRPLStake(getNodeAddressForDelegate(_address));
  }

  function getNodeRPLStakeQuadratic(address _rocketNodeStakingAddress, address _address) public view returns (uint256) {
    return sqrt(getNodeRPLStake(_rocketNodeStakingAddress, _address));
  }

  function getNodeEffectiveRPLStake(address _rocketNodeStakingAddress, address _address) public view returns (uint256) {
    RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(_rocketNodeStakingAddress);
    return rocketNodeStaking.getNodeEffectiveRPLStake(getNodeAddressForDelegate(_address));
  }

  function getNodeEffectiveRPLStakeQuadratic(address _rocketNodeStakingAddress, address _address) public view returns (uint256) {
    return sqrt(getNodeEffectiveRPLStake(_rocketNodeStakingAddress, _address));
  }

  function getNodeRPLBalance(address _rplAddress, address _address) public view returns (uint256) {
    ERC20 rpl = ERC20(_rplAddress);
    return rpl.balanceOf(getNodeAddressForDelegate(_address));
  }

  function getNodeRPLBalanceQuadratic(address _rplAddress, address _address) public view returns (uint256) {
    return sqrt(getNodeRPLBalance(_rplAddress, _address));
  }

  // https://github.com/Uniswap/v2-core/blob/v1.0.1/contracts/libraries/Math.sol
  function sqrt(uint y) internal pure returns (uint z) {
    if (y > 3) {
      z = y;
      uint x = y / 2 + 1;
      while (x < z) {
        z = x;
        x = (y / x + x) / 2;
      }
    } else if (y != 0) {
      z = 1;
    }
  }
}

It is deployed on

Snapshot settings

Use the contract-call strategy with the following arguments:

{
  "args": [
    "0x3019227b2b8493e45Bf5d25302139c9a2713BF15",
    "%{address}"
  ],
  "symbol": "RPL",
  "address": "0xca80f2023f07024b3be40cfa7b20af1c9d7503bf",
  "decimals": 18,
  "methodABI": {
    "name": "getNodeEffectiveRPLStake",
    "type": "function",
    "inputs": [
      { "name": "rocketNodeStaking", "type": "address", "internalType": "address" },
      { "name": "voter", "type": "address", "internalType": "address" }
    ],
    "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }],
    "stateMutability": "view"
  }
}

mainnet

  • 0xca80f2023f07024b3be40cfa7b20af1c9d7503bf is the address of the contract above
  • 0x3019227b2b8493e45Bf5d25302139c9a2713BF15 is RocketNodeStaking

goerli

  • 0x28c309a478cf63b6519a44f57d3a4cd648b8605f is the address of the contract above
  • 0xc0367b558fcf45d5e6cadf55567d4fb94bf62703 is RocketNodeStaking

Snapshot will use the contract to check the voting power. It will check if your MetaMask is associated with a node and if so use the effectively staked RPL as the voting power of your node.

Usage

Go to

Click Connect to Web3. (Don’t forget to switch your MetaMask to Goerli testnet if you’re using that.)

Enter your node address and click Write.

Once the transaction is confirmed, open it on etherscan and go to the Logs tab. Note down the number next to index. In this case it’s 1.

Convert this number to wei. Go to https://eth-converter.com/ and enter it next to Wei. Copy the value that’s Ether.

Run this command on your node and replace 0.000000000000000001 with your Ether value.

mainnet:

rocketpool node send 0.000000000000000001 eth 0xca80f2023f07024b3be40cfa7b20af1c9d7503bf

goerli:

rocketpool node send 0.000000000000000001 eth 0x28c309a478cf63b6519a44f57d3a4cd648b8605f

This sends a miniscule amount of ETH to the delegation contract.

That’s it.

If you already had effectively staked RPL then you should be able to vote on this test snapshot:

(you can also vote with your node wallet, you don’t have to do any of this unless you want to)

6 Likes

This is great peteris, if the RP team can integrate the set delegate function into their website similar to the set withdrawal address implementation, and add a CLI command, this would be a fairly easy way to set up snapshot voting. Not bad for a day’s work.

1 Like

I’m in support of having a voting mechanism, even if it’s imperfect. I’m concerned that the protocol leans in too heavy in Node Operator’s voices. IMO it should include a certain percentage (15-20%?) of rETH stakers’ voices at some point in the future, although it can be complex to find the right people, considering some have their rETH in defi, etc.

Hey @peteris - that is a great idea!

I will get the team to review in the morning.

2 Likes

Hey @peteris - thank you very much for the effort! It was a very clever solution.

Unfortunately there are a couple of issues with it that means we would have to update the smart node software anyway - you need to have a data param for the send function rather than using msg.value as an index.

We could do that but we might as well add Snapshot’s delegate functionality instead. You inspired that because delegation is a much better approach that what we were originally thinking.

Thanks again!

3 Likes

Summary so far:

  • Voting should be based off of a custom snapshot strategy using a node operator’s effective RPL stake as it is the best way to to determine if RPL is working for the protocol
  • Voting should apply a quorum to prevent low interest/participation topics from being passed
  • Voting should use Support/Oppose to keep thing simple

During our initial research, for Snapshot voting to work using the effective stake of a node operator, the address connected to Snapshot to perform the vote will have be a registered node and contain an effective stake RPL balance. This means that transactions would need to come from the node wallet. Connecting the node wallet to the likes of Metamask is risky, ideally the CLI should be able to perform these transactions.

For security reasons, voting would need to be actioned via the Smart Node CLI. Voting via the CLI would require users to log into their node and interact with the Smart Node CLI a lot more, which increases their risk exposure.

Delegation

While investigating the great work performed by @peteris, his project influenced us to change our focus from providing voting options in the Smart Node CLI to simply allowing node operator’s to delegate their votes to another address or delegate representative.

Strategies for discussion

The data below uses a sample of node operator addresses from larger node operators to smaller node operators which shows the vote weight for different strategies.

Pure Effective Stake

Captures the node’s effective RPL stake using the pure effective stake numbers.

npm run test --strategy=rocketpool-node-operator-pure

Scores with latest snapshot [
  {
    '0x17Fa597cEc16Ab63A7ca00Fb351eb4B29Ffa6f46': 915782.4728596018,
    '0xca317A4ecCbe0Dd5832dE2A7407e3c03F88b2CdD': 385000.7551590216,
    '0x327260c50634136551bfE4e4eB082281555AAfAE': 65789.4897712646,
    '0x5d8172792a9e649053c07366E3a7C24a37F0C534': 360000,
    '0x701F4dcEAD1049FA01F321d49F6dca525cF4A5A5': 31163.697649235244,
    '0xb8ed9ea221bf33d37360A76DDD52bA7b1E66AA5C': 278324,
    '0xbfaf9BFa09F26EF8104A6d5FF09afdCC9300E5bc': 108069.95232078587,
    '0x174E0b45C03318B0C9bc03573028605B26764931': 17660.13855285447,
    '0x5f4cb66c9b1ed8a4758a059fdb10e0f72c307d8a': 202.94944811433797,
    '0x24609303b67051ef77735e34d671e2a13e3da35d': 845.807411429498,
    '0xe35854cde18a3cc4706134b4850dd861a55b9a30': 1012.4869184922566,
    '0x53938f795ab6c57070aad32905a70a2e5961a887': 257.78352692864814
  }
]

Half Square Root Effective Stake

Captures the node’s effective RPL stake and half square root the result.

npm run test --strategy=rocketpool-node-operator-half-square-root

Scores with latest snapshot [
  {
    '0x17Fa597cEc16Ab63A7ca00Fb351eb4B29Ffa6f46': 478.4826205985965,
    '0xca317A4ecCbe0Dd5832dE2A7407e3c03F88b2CdD': 310.24214541186274,
    '0x327260c50634136551bfE4e4eB082281555AAfAE': 128.24730969036406,
    '0x5d8172792a9e649053c07366E3a7C24a37F0C534': 300,
    '0x701F4dcEAD1049FA01F321d49F6dca525cF4A5A5': 88.26621331125976,
    '0xb8ed9ea221bf33d37360A76DDD52bA7b1E66AA5C': 263.78210705049725,
    '0xbfaf9BFa09F26EF8104A6d5FF09afdCC9300E5bc': 164.369973170882,
    '0x174E0b45C03318B0C9bc03573028605B26764931': 66.44572701245444,
    '0x5f4cb66c9b1ed8a4758a059fdb10e0f72c307d8a': 7.123016357455913,
    '0x24609303b67051ef77735e34d671e2a13e3da35d': 14.541384145169072,
    '0xe35854cde18a3cc4706134b4850dd861a55b9a30': 15.90979979833386,
    '0x53938f795ab6c57070aad32905a70a2e5961a887': 8.027819238881879
  }
]

Cube Root Effective Stake

Captures the node’s effective RPL stake and Math.cbrt() the result.

npm run test --strategy=rocketpool-node-operator-cubed-root

Scores with latest snapshot [
  {
    '0x17Fa597cEc16Ab63A7ca00Fb351eb4B29Ffa6f46': 97.11003465505995,
    '0xca317A4ecCbe0Dd5832dE2A7407e3c03F88b2CdD': 72.74791105169768,
    '0x327260c50634136551bfE4e4eB082281555AAfAE': 40.36938866880965,
    '0x5d8172792a9e649053c07366E3a7C24a37F0C534': 71.13786608980126,
    '0x701F4dcEAD1049FA01F321d49F6dca525cF4A5A5': 31.469003741138312,
    '0xb8ed9ea221bf33d37360A76DDD52bA7b1E66AA5C': 65.2905337712814,
    '0xbfaf9BFa09F26EF8104A6d5FF09afdCC9300E5bc': 47.63231104255781,
    '0x174E0b45C03318B0C9bc03573028605B26764931': 26.04142241031916,
    '0x5f4cb66c9b1ed8a4758a059fdb10e0f72c307d8a': 5.876642769716152,
    '0x24609303b67051ef77735e34d671e2a13e3da35d': 9.457082161279882,
    '0xe35854cde18a3cc4706134b4850dd861a55b9a30': 10.041451005652124,
    '0x53938f795ab6c57070aad32905a70a2e5961a887': 6.364315786097583
  }
]

Where to from here

To keep things simple, ideally we would want to pick one strategy (or iterate on the proposed ones, pull requests are welcome) for the Snapshot Space.

For example, if the community are happy with the Cubed Root Stake strategy, we would apply

  • Cube Root Effective Stake

to the Snapshot space and all proposals would use these strategies for voting.

We are currently working on adding delegation capabilities into the Smart Node Stack.

If you want to perform your own analysis and play with the code, the repo is available here GitHub - rocket-pool/snapshot-strategies at add-rocketpool-node-operator-strategies

4 Likes