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 theVoting.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 inVoting.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 userisDelegated
and thenVoterState
is stored asDelegateYea
orDelegateNay
in place ofYea
orNay
- 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
andVotingAdapter.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 viachangeController
, the Tally design would add a separate contract to track on-chain delegations, which would be reached out to via the upgradedITokenController
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:
- 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.
- 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.
- 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.
- 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.