LIP-21: Simple On-chain Delegation

Gm from Aragon! Back in April, we decided to perform an extensive review of the LIP-21: Simple On-Chain Delegation and some other alternatives recently proposed in the Lido forum to provide feedback and a recommended course of action. You can find this extensive review below (some of the provided code examples may have had minor changes from April until now):

Authors: Jordan (Sr. Smart Contract Developer) Giorgi (Sr. Smart Contract Developer), CapitulationCapital (ex-Ecosystem Lead), Anthony Leuts (CEO) Aragon X
Who is Aragon X: Aragon X is the newest generation of builders from the Aragon Project who have recently launched Aragon OSx, a new generation modular DAO framework, and the Aragon App interface.

Summary

Lido recently introduced LIP-21: Simple On-chain Delegation which closed its snapshot round with near unanimous support of 70M LDO and is now available for an onchain vote . To quote said snapshot proposal:

“The Simple On-chain Delegation allows LDO token holders to delegate their voting power to other addresses and delegates to participate in on-chain voting on behalf of their delegated voters. Alongside that, it’s proposed to add TRP (Token Rewards Plan) participants the ability to delegate their LDO rewards.

This proposal is intended to address the security and operational hurdles arising from the lack of delegation for on-chain voting.”

Details of the LIP

Contracts Involved

LIP-21 (referred to from here as ‘the LIP’) represents a set of changes to the Voting.sol contract and the VotingAdapter.vy contract, where:

  • Voting.sol is a Solidity 0.4.24 Upgradeable Proxy that is built on Aragon OS. Its primary purpose is storing and retrieving Vote data on-chain based on the associated LDO token’s balance at a given block snapshot.
  • VotingAdapter.vy is a Vyper contract that allows tokens yet to be vested to vote, by serving as a communication layer between the vesting contracts and the Voting.sol contract. It can be updated by pointing a factory contract controlling the deployment of other vesting contracts to a new VotingAdapter implementation.

Additionally there is the LDO token, which is a non-upgradeable Minime ERC20 token which includes balance histories:

All MiniMe Tokens maintain a history of the balance changes that occur during each block. Two calls are introduced to read the totalSupply and the balance of any address at any block in the past.

function totalSupplyAt(uint _blockNumber) constant returns(uint)
function balanceOfAt(address _holder, uint _blockNumber) constant returns (uint)

Implementation

The LIP specification is pretty comprehensive and well documented, we see no reason to rewrite it here, but instead as a summary of key takeaways:

  • Delegation is tracked via a pair of mappings added to the Voting.sol contract. The pair of mappings is used to ensure it is both quick to check the delegate for a given voter and easy to loop over the list of voters for that same delegate.
  • Users can call setDelegate which will update the delegation mappings with their chosen delegate.
  • Delegates can vote on behalf of voters by calling attemptVoteForMultiple , this in turn reaches out to:
    • _isDelegateFor to confirm in Voting.sol if the user is delegated
    • _hasVotingPower to fetch the token balance of the voter from the LDO token at the block snapshot
    • _vote is then called on behalf of the voter, a new argument has been added to track if the user isDelegated and then VoterState is stored as DelegateYea or DelegateNay in place of Yea or Nay
  • Voting adapter adds methods to set and reset delegates, which simply proxies the calls to the Voting.sol contract
  • Changes to the interface in Voting.sol and VotingAdapter.vy can be summarised below:

Voting.sol

/// ----- new state ------
struct DelegatedAddressList {
    address[] addresses;
}
// delegate -> [delegated voter address]
mapping(address => DelegatedAddressList) private delegatedVoters;

struct Delegate {
    address delegate;
    uint96 voterIndex;
}
// voter -> delegate
mapping(address => Delegate) private delegates;

enum VoterState { Absent, Yea, Nay, DelegateYea, DelegateNay }

/// ----- new transactions -----
function setDelegate(address _delegate) public;
function resetDelegate() public;
function attemptVoteForMultiple(uint256 _voteId, bool _supports, address[] _voters) external;
function attemptVoteFor(uint256 _voteId,bool _supports,address _voter) external

/// ----- new views -----
function getDelegatedVoters(
    address _delegate,
    uint256 _offset,
    uint256 _limit
) external view returns (address[] memory, uint256[] memory);
function getDelegatedVotersAtVote(
    address _delegate,
    uint256 _offset,
    uint256 _limit,
    uint256 _voteId
) external view returns (address[] memory, uint256[] memory);
function getVotersStateAtVote(
    uint256 _voteId,
    address[] calldata _voters
) external view returns (VoterState[] memory voterStatesList);

/// ----- new events -----
event SetDelegate(address indexed voter,address indexed delegate);
event ResetDelegate(address indexed voter,address indexed delegate);
event CastVoteAsDelegate(
    uint256 indexed voteId,
    address indexed delegate,
    address indexed voter,
    bool supports,
    uint256 stake
)

VotingAdapter.vy

def setDelegate(_delegate: address): nonpayable
def resetDelegate(): nonpayable

Alternative Implementations

Alongside the LIP, we reviewed a couple of other options. The first was by Tally in a research grant and the second (mentioned in the grant post as well) was 1Hive’s TAO Voting built on Aragon OS.

Tao Voting

The Tao voting implementation shares many similarities with the Lido solution in its implementation, with the major differences being that a representativeManager contract is not defined separately and delegation is stored inside Voting.sol . Additionally, the Tao voting solution would require users wrapping the LDO tokens before votes could be held and we understand that this introduces additional complexity which may negatively impact delegation.

OnTokenTransfer hook

Tally proposed a solution whereby the non-upgradeable LDO contract can be placed behind an adapter contract that conforms (with some modifications) to the ERC20Votes standard - allowing it to be used with standardised frontends like Tally and the new Aragon App.

As a summary:

  • Onchain delegations are updated as part of the standard token transfers, this is done via a hook inside the LDO token’s doTransfer function:
function doTransfer(address _from, address _to, uint _amount) internal returns(bool) {
		// ... skipped for brevity
    // Alerts the token controller of the transfer
    if (isContract(controller)) {
        // Adding the ` == true` makes the linter shut up so...
        require(ITokenController(controller).onTransfer(_from, _to, _amount) == true);
    } 
  • The TokenController is both upgradeable (TokenManager.sol) and can be updated via changeController , the Tally design would add a separate contract to track on-chain delegations, which would be reached out to via the upgraded ITokenController contract.

Tally don’t specify the exact manner in which delegation will be implemented, other than it will likely track the OpenZeppelin implementation used in ERC20Votes , but mention that other areas of the Aragon governance contract could be replaced due to Aragon OS’s flexible permissions system (something we have made sure to keep and expand upon with OSx), which would allow closer conformity to the OpenZeppelin Governor standard.

Comparing

When reviewing the alternative solutions, one concern with Tao Voting (which would also apply in part to the LIP as it shares many similarities) would be related to its handling of large numbers of addresses:

*Aragon TAO Voting unfortunately hits the same constraints as the Lido DAO when seeking an implementation of delegation for the token. That is: there is no native way to track delegated voting power onchain…

…The implementation requires that the user pass in at vote time an array containing all of the voters that they are voting on behalf of. This is non-scalable, as the smart contract function loops through each individual delegate to calculate their voting power at voting time.

For a delegate such as Linda Xie 3 in Optimism, this would require passing in a list of over 80,000 delegates and executing the loop 80,000 times.*

Our impression from discussions with the Lido team are that they are aware of this issue and have accepted the tradeoff of higher costs for large delegates versus lower costs for the majority of voters and delegates. We should also note that, even the LIP snapshot vote states:

A proper full-fledged delegation mechanism involves maintaining an accounting to track actual delegated voting power at any moment accurately. Implementing this is a complex and time-consuming project.

It should also be noted that maintaining up-to-date delegation records inside doTransfer will increase gas costs for all users, not just delegators. On the other hand, as Voting.sol is separate from the LDO token (outside of fetching snapshot balances), adding the delegation functionality has no impact on the LDO transfer gas costs.

Specifically with the LDO token, one already has balance histories stored inside the contract. Adding a second contract to track historical delegations (as in the Tally solution) therefore has the gas costs of:

  • Tracking the historical token balances in the LDO token (Minime)
  • Tracking the delegation checkpoints as per ERC20Votes

The tradeoff here is that the costs for the delegates is increased much more, as voting power at a point in time must be computed on the fly. Whereas the Tally solution would greatly increase gas costs for the LDO token on all transactions moving forward both inside and (crucially) outside of governance. While a full delegation solution is being worked on, we agree with the current proposal of the LIP being a fair compromise that doesn’t significantly increase gas costs for LDO users.

UI Implications

While UI details are not explicitly provided inside the LIP or associated material, we did note that the LIP does not conform to a common interface standard, and as such will require a custom UI to be implemented. Given Lido’s prominence in the industry and the importance of the LDO token, along with the sheer value at stake in the Lido protocol, such a solution must also be sufficiently decentralised to avoid exposing the voting process to censorship or manipulation by a central party.

This should not be challenging for the Lido team to build and Aragon has offered use of its new open design system and any UX support to Lido, both of which could help generate a general standard on voting UI’s in the industry.

Closing Comments

In all, reviewing the solutions presented to Lido, we came to the following conclusions:

  1. The LIP is an elegant solution to achieve the basics of delegation, in a timely manner, to solve Lido’s present problem of low participation and votes failing to reach quorum.
  2. Given the importance and usage of LDO in the ecosystem we think optimising for everyday users and tokenholders at this stage over a smaller number of delegates, is a sensible tradeoff while a longer term strategy is envisaged.
  3. While adopting common voting standards may be desirable, the limitations of the LDO token, as it relates to upgradeability, make this a more complex problem that would need further thought and is a common trade-off in achieving safety versus flexibility.
  4. The Lido front-end should be decentralized, resistant to capture & censorship, tamper-proof, and not within reach of any 3rd party or nation state.

In conclusion, we appreciate the Lido team’s thought into devising a practical solution in a timely manner, and reiterate our initial commitment to offering any support or guidance, should the team have need of it. The Lido development team is world-class and has once again proven their creativity in finding excellent solutions to challenging problems, and under various constraints. We applaud their forward thinking approach and look forward to continue to working with them in the future and are happy that the community is signalling approval of LIP-21 and it will most likely pass onchain as well.

6 Likes