Snapshot Voting

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

My main question is… how will allnodes users be able to vote, in absence of a CLI? On snapshot.org? Will there be a web3 ui for them to select a delegate?

2 Likes

The Snapshot UI allows you to set a delegate for a Space, so delegates can still be set however this comes with an increased risk exposure.

1 Like

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.

Cube root effective stake is fine!

1 Like

I would favour the Cube Root Effective Stake option. Comes closest to one person - one vote in my opinion.

I would lean towards straight Sqrt but half square is fine.

I would argue against cube rt or lower however as at that point you are providing very high motivation to split nodes for the purpose of voting. This would make individuals harder to identify and reduce future options for penalising RPL stake based on node rather than minipools.

It also dis-proportionally reduces the voting power of the early and very highly aligned whales with one or a few nodes in favour of later entrants who will know the benefit of splitting nodes and act accordingly.

3 Likes

I agree with Uisce here. Even with half square, thomasg vote is only 60x that of someone with one minipool and low collateral. That is far from whales dominating votes.

One person - one vote isn’t doable without sybil resistance and I fear that trying too hard will have the opposite effect in that a whale could dominate by creating many nodes.

2 Likes

Square root looks good to me.

Theoretical support for this being efficient

See Quadratic Vote Buying, Square Root Voting, and Corporate Governance and related.

Of course, that doesn’t handle the idea of folks splitting their stake to maximize voting power, but that’d be an extreme challenge without massive complexity.

Minor: Why is it half square root? It seems like dividing all the numbers by 2 doesn’t change anyone’s relative voting power? Seems like wasted math unless I’m missing something.

+1 for half square root

1 Like

Yes, there is really no difference between half square root and square root.

My vote would be for the squared option. I am biased in this vote because of the position that I have taken in RPL.

I agree with knoshua that multiplying and nth root number by a constant (1/2) does not change the relative voting power of results.

Maybe the intent was to have 1/2-squared mean to use the 2.5th root of the eff RPL staked? Sort of the compromise between squared and cubed root? - If so I could live with that.

Seems like wasted math unless I’m missing something.

Excellent point, scalers are wasted math here. Either I misunderstood langers’ original suggestion, or he just took a very quick look at the squished function graph without thinking too hard (exactly what I did).

With some thought, we may be able to come up with a more custom function that boosts small nodes and reduces very large nodes without incentivizing node splitting too much, but I’m not sure it’s worth the effort. 2.5 root seems like a good compromise to me, but I’m happy as long as we’re using a root function of some sort.

All node operators and their voting power

7 Likes

I also support the half square root method