Hello, Lido community! I’m back with the final deliverable on our Lido x Tally research grant.
Authors: Frisson (Tally), Dennison Bertram (Tally), Raphael Spannocchi (StableLab), Nneoma (StableLab), Dulguun (StableLab)
Contributors: Kadmil (Lido)
Background
Lido DAO would like to increase voter participation and decision quality by introducing delegation.
Tally believes we can help introduce delegation to the Lido DAO while improving participation and overall engagement with Tally’s best-in-class governance platform. The current implementation of the LDO token and Lido DAO governance structure does not allow for a straightforward integration with Tally, however. With this research deliverable, we evaluate implementation solutions available to the Lido DAO and recommend a path forward.
Analysis and Conclusion
The four options for supporting Lido on Tally and enabling delegation we will discuss are:
- Option One: Fully support delegation on top of the existing Lido DAO Aragon governance system on Tally. We only discuss this option briefly, because it is not technically viable.
- Option Two: Build a smart contract interface that makes the LDO token compatible with Tally’s interface.
- Option Three: Build an IOU voting proxy contract that enables delegation of the LDO token, then add the voting proxy contract as a module to the existing Aragon contracts. We adjusted the nature of option three from the original research proposal to reflect the most viable implementation path.
- Option Four: Migrate the whole Lido DAO to a Governor that works natively with Tally.
This deliverable includes an evaluation of each implementation option. It also includes a detailed spec for implementing our recommended solution, Option Two: Build a smart contract interface that makes the LDO token compatible with Tally’s interface. We’re excited to gather feedback from the Lido community on our analysis and make progress towards implementation.
We put together a visual matrix below that highlights the performance of each option on each factor of our analysis. Option one is not technically viable from our point of view. Option three is technically challenging and would require high maintenance. Option four requires almost uniform user participation and represents a major technical change to the current governance system. Option two stands out as the only path that is viable across all dimensions of our analysis. We’ve identified an elegant path to implementing option two that requires minimal change to the Lido governance system, is straightforward for Tally to support, and provides a smooth, non-disruptive user experience for Lido token holders.
Option One: Fully support delegation on top of the existing Lido DAO Aragon governance system on Tally.
We were not able to determine a technically viable path to supporting delegation on top of the existing Lido DAO Aragon governance system. All viable solutions involve either building a smart contract interface (option two), building a proxy contract (option three), or migrating to a Governor (option four).
One option we explored thanks to Griff’s helpful recommendation in this forum thread is TAO voting. The TAO system is a kind of implementation of liquid democracy which includes a system for delegated voting. To the best of our understanding however, the Aragon implementation of this system is not compatible with the demands or scale of an organization such as Lido.
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. In the TAO Voting implementation, ‘voteOnbehalfOf’ implements the ability to “vote on behalf” of someone, or delegation. 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 in Optimism, this would require passing in a list of over 80,000 delegates and executing the loop 80,000 times. This is prohibitive in terms of gas cost (even on L2), potentially impossible to execute, and might lead to operational inefficiency (to save gas costs, perhaps Linda would only vote with a subset of her delegated voting power, meaning the smaller delegates’ votes would essentially be wasted).
Option Two: Build a smart contract interface that makes the LDO token compatible with Tally’s interface.
Solution summary
To implement option two, we propose creating an interface contract which mimics the interface of a standard OpenZeppelin ERC20Votes smart contract. This interface would be connected to the Aragon Token Manager contract and its state updated as part of the internal “doTransfer” call, which is made on each LDO token transfer. In this way, we can implement delegation that tracks the delegated token balance at any point in time without requiring any changes to the LDO token or requiring some sort of wrapper.
Implementation cost
The architecture of option two is generally compatible with Tally today. With option two, Tally could and probably should make a product investment to support some aspects of the legacy Aragon system alongside the new system so that users can see old proposals and interact with the old system as needed.
Option two requires the creation of an interface contract which mimics the interface of a standard OpenZeppelin ERC20Votes smart contract. This interface would be connected to the Aragon Token Manager contract, and its state updated as part of the internal “doTransfer” call, which is made on each LDO token transfer. Adding the new interface to the Lido DAO is a relatively straightforward process and would involve packaging a Governor that inherits the Aragons forwarder and ACL controls, essentially making it a module.
Maintenance cost
The architecture of this solution is generally compatible with Tally today. As a result, the majority of the ongoing maintenance cost of this solution is already captured by the work Tally does to support its platform as a whole across all clients. If Tally makes incremental product investments to support some aspects of the legacy Aragon system alongside the new system, this will entail ongoing maintenance costs specific to Lido.
User experience
Option two enables delegation and Tally support without requiring LDO token holders to lock their tokens in a governance escrow or wrap their tokens.
Option two also enables Lido token holders to participate via the legacy system by over voting (voting with their own voting power on a proposal that their delegate has already voted on), if desired. The new system interface provides the logic for returning an individual addresses token power as well as their delegated voting power.
Option two enables partial voting: the ability for an address to delegate to multiple addresses. This is frequently useful for very large token holders looking to spread out their voting power between various addresses. This is a feature that would be straightforward to implement in the interface contract.
Security implications
This is a generally safe implementation, because it only requires a small change to the LDO token manager. We only need to make the token manager contract tell the new ERC20Votes interface contract when a balance changes. This is only a one-way integration. The token manager does not need to read from the ERC20Votes interface contract. With this implementation, we minimize the need for any changes on the existing Lido DAO smart contracts. We use the existing Aragon design and system to add a new smart contract to the system, which is within the scope and design of the original implementation/intention of the Aragon system.
We would recommend an audit of the smart contract code to ensure that there is no way for the ERC20Votes interface contract to change the state of the LDO token manager. We would also recommend an audit of the new Governor module that would be added to the Aragon system.
Implementation spec
Delegation can be thought of as a two-part system: a delegate registry that tracks who is delegating to whom, and a counter which tracks the amount of voting power that is delegated from one address to another. The delegate registry is relatively simple. The ERC20Votes OpenZeppelin contract already contains logic that manages a registry of delegations. There are other off-the shelf solutions that also exist, such as Delegate.xyz and Safe’s Delegate Registry. In its simplest form, the registry is a mapping of address → address.The complexity comes from implementing a counter system that tracks the delegated voting balance of an address at any time point, so that at any point in time the delegated voting power of an address can be determined onchain. The challenge that we have overcome in this implementation is updating the delegated token balance in a way that automatically stays in sync with a user’s token transfers.
While the LDO token does have a built-in checkpointing system that allows one to get the token balance of an address at any point in time, the token does not have a way to do delegation or track delegated token balances. The LDO token is also not upgradable, meaning it is not possible to add this functionality to the LDO token itself. Fortunately, the LDO token does have an internal hook function that calls to the Aragon Token Manager contract. There is an internal function called ‘doTransfer’, which informs the Aragon Token Manager contract of token transfers for implementing functionality around blocking transferability or transfer caps. The Aragon Token Manager contract is a proxy contract, and thus is designed to be upgradable. This offers us the opportunity to build a delegate registry and voting balance checkpointing as a function of the Aragon Token Manager contract.
To implement delegation for the LDO token, we propose the creation of an interface contract which mimics the interface of a standard OpenZeppelin ERC20Votes smart contract. This interface would be connected to the Aragon Token Manager contract and its state updated as part of the internal “doTransfer” call, which is made on each LDO token transfer. In this way, we can implement delegation that tracks the delegated token balance at any point in time without requiring any changes to the LDO token or requiring some sort of wrapper.
Logical flow of execution when a user transfers an LDO token today
The interface contract would exist to implement an ERC20Votes interface for contracts such as OpenZeppelin Governor, but would not actually be a token. The majority of the functional logic such as “balanceOf” or “name” would be removed and instead directly reference the LDO implementation. Public functions such as transfer, or approve would simply revert when called, as the ERC20Votes contract would exist as an interface to the LDO contract.
Users holding LDO would continue to use their LDO token contract in the normal way, they would have no need to interact with the interface contract, except for delegation. The interface would exist to simply interpret the LDO token with delegation added. Users would transfer and use their LDO tokens like before. For delegation, they would be required to call the LDO Interface contract to select their delegate. On the first delegation, the interface contract would check the LDO tokens balanceOof and set the first delegate along with the delegators token balance for voting power.
On each subsequent token transfer to or from the delegators addresses, the TokenManager would update the interface contract with the new balances to correctly represent the delegated voting power of the user.
Implementation of a Delegate Registry Interface
The Lido DAO is built upon a legacy version of Aragon DAO software. This system was developed as a kind of “operating system” where users could create unique programs which could be ‘attached’ to the DAO. The design envisioned a freeform flexible structure that users could customize to their needs. The system, while powerful, is also complicated and hard for 3rd parties to reason about without understanding its design system. At its core, the legacy Aragon system is essentially a permissions system controlled by a module called the “ACL” (Access Control List) which intermediates the permissions between various smart contracts. The structure of the Lido permission system can be loosely understood from the Aragon Lido DAO page.
Screenshot from the Aragon Lido DAO Page
In the above table, we can see four columns: “Action”, “On App”, “Assigned to Entity”, and “Managed By” describing the permission. Walking through the first line of this table, we have the first action which is the RadSpec definition of what a specific function on the “App” smart contract is supposed to do. So in this first row example, the entity (smart contract) called “Finance” has the permission to “Transfer Agent’s tokens” on the smart contract App called “Agent”, and this permission is managed by the smart contract called “Voting”.
In the Lido DAO system the smart contract “Voting” controls the permissions for all the smart contracts in the organization. Authorized users create proposals as “evmscript” and the Voting smart contract is able to forward and execute these scripts when a proposal passes and the vote is executed.
Adding Governor to the Lido DAO is a relatively straightforward process and would involve packaging a Governor that inherits the Aragon forwarder and ACL controls, essentially making it a module. Thanks to the flexibility of the Aragon permissions system, the governor smart contracts can be added with the appropriate permissions required to operate the Lido system. Topologically, this is replacing the Aragon “Voting” module with a governor module and giving it the correct permissions. The functionality and operation of the Lido DAO would be effectively the same.
References
The ‘doTransfer’ function of interest:
function doTransfer(address _from, address _to, uint _amount) internal returns(bool)
[....removed for brevity]
// Alerts the token controller of the transfer
[....removed for brevity]
require(ITokenController(controller).onTransfer(_from, _to, _amount) == true);
}
[....removed for brevity]
}
Explanation of flow
- Transfer Function Call (function transfer(address _to, uint256 _amount) public returns (bool success)):
- This is the initial entry point for a token transfer.
- It checks if transfers are enabled via require(transfersEnabled);.
- Calls the doTransfer function with msg.sender, _to, and _amount.
- doTransfer Function Logic (function doTransfer(address _from, address _to, uint _amount) internal returns(bool)):
- Checks if the amount to be transferred is zero (in which case it just returns true).
- Ensures that the transfer is not to address 0 or the token contract itself.
- Retrieves the balance of the sender at the current block (balanceOfAt(_from, block.number)) and checks if the sender has enough balance to transfer the amount.
- If the contract has a controller, it calls ITokenController(controller).onTransfer(_from, _to, _amount) to notify the controller about the transfer. The controller can potentially reject the transfer by returning false.
- Updates the sender’s balance by reducing the amount (updateValueAtNow(balances[_from], previousBalanceFrom - _amount)).
- Similarly, updates the recipient’s balance by adding the amount.
- Emits a Transfer event.
- Other Notes
- balanceOfAt and updateValueAtNow are used to manage balances at different block numbers, allowing the token to effectively handle balance snapshots.
- The contract includes checks for overflows and ensures that the transfer does not result in erroneous values (e.g., ensuring previousBalanceTo + _amount >= previousBalanceTo).
- The controller plays a significant role in this contract. It has the authority to enable or disable transfers, and it can intervene in the transfer process.
- The transferFrom function allows a third party to transfer tokens on behalf of the owner, provided that it’s approved by the owner. It has similar logic to transfer but with additional checks on allowances.
Change Required
function onTransfer(address _from, address _to, uint256 _amount) external onlyToken returns (bool) {
return _isBalanceIncreaseAllowed(_to,_amount) && _transferableBalance(_from, getTimestamp()) >= _amount;}
Interface IDelegateRegistryInterface {
function trackBalance(address _from, address _to, uint256 _amount) external;
}
….
IDelegateRegistryInterface IDelegateRegistry = “address Registery”
function onTransfer(address _from, address _to, uint256 _amount) external onlyToken returns (bool) {
IDelegateRegistry.trackBalance(_from, _to, _amount);
return _isBalanceIncreaseAllowed(_to, _amount) && _transferableBalance(_from, getTimestamp()) >= _amount;}
Option Three: Build an IOU voting proxy contract that enables delegation of the LDO token, then add the voting proxy contract as a module to the existing Aragon contracts.
Solution summary
To execute on option three, we would propose creating an LDO IOU voting proxy contract similar to the one used in MakerDAO. The MKR token is an ERC20 implementation with mint and burn functionality and some smaller technical differences, but no delegation functionality. If MKR holders want to vote on executive proposals and move the hat (the current set of active smart contracts) to a new spell, as the set of smart contracts that executes the latest bundle of approved proposals is called, they need to lock their MKR into the governance escrow. When token holders lock their MKR in the voting proxy or dss-chief smart contract, they receive IOU tokens in return for the amount of MKR they send into the escrow on a one-to-one basis.
Implementation cost
The architecture of option three is not compatible with Tally today. With option three, Tally would need to make a very large investment to integrate a MakerDAO-like system with our API and front end. The cost of this could potentially be offset to some extent by the fact that Tally would then likely be able to support MakerDAO governance.
Option three requires the creation of an LDO IOU voting proxy contract similar to the MKR voting proxy. Adding the new IOU voting proxy contract to the Lido DAO is a relatively straightforward process and would involve packaging it in a way that inherits the Aragons forwarder and ACL controls, essentially making it a module.
Maintenance cost
The architecture in option three is not compatible with Tally today. As a result, this solution would require significant ongoing additional maintenance cost for Tally.
User experience
Option three avoids some of the need to actively manage another LDO token. While the IOU tokens are transferable, they can only be redeemed by the same address that executed the lock transaction. Token holders can lock up their LDO tokens from their main wallet, but then use a less secure hot wallet for voting, since IOUs are worthless to anyone but the address that locked the tokens. Users can free their tokens at any time by triggering a transaction in the opposite direction. (free instead of lock).
IOU tokens can be engineered to support any kind of behavior, which would open the door to future governance innovations like dual token voting and partial voting/delegation.
Option three enables Lido token holders to participate via the legacy system by voting with their own voting power on a proposal that their delegate has already voted on, if desired. The new system interface provides the logic for returning an individual addresses token power as well as their delegated voting power.
Security implications
IOU tokens can be used to vote on executive proposals, and can be delegated. Delegations can be changed at any time, also during ongoing votes. This can lead to dramatic changes in voting outcomes, if large delegations get moved around during a contested vote. See the following link for a particular example. # 42 | Valkyrie: MakerDAO and Our Side of History (substack.com)
MakerDAO’s implementation of an IOU token doesn’t have a delegate function itself. Instead IOU tokens can come from different governance contracts. MKR holders can deposit their MKRs into their own vote proxy contract and vote directly, or into a delegates’ vote proxy contract, and receive IOUs from them. The delegates do not have the ability to access or transfer the tokens in their vote proxy, so user funds are SAFU at all times. The schema below visualizes the flow of tokens.
While it may seem that the delegate could have influence over whether or not the MKR get deposited into the governance contract, this is not the case, but part of the delegate’s smart contract functionality.
Option Four: Migrate the whole Lido DAO to a Governor that works natively with Tally
Solution Summary
To execute option four, we would deploy a wrapped Lido governance token (LDO) that implements the ERC-5805 standard, which includes the token interfaces for voting with delegation. A new Lido DAO Governor contract would also need to be deployed. Once live, the DAO’s onchain assets, presently with Aragon, including the treasury, protocol, and any associated administrative permissions, will transition to this Governor contract. A detailed view of impacted contracts can be found in Lido’s GitHub repository.
Implementation cost
The architecture of this solution is generally compatible with Tally today. As a result, the majority of the implementation cost of this solution is already captured by the work Tally does to support its platform as a whole across all clients.
Maintenance cost
The architecture of this solution is generally compatible with Tally today. As a result, the majority of the ongoing maintenance cost of this solution is already captured by the work Tally does to support its platform as a whole across all clients.
User experience
Option four requires LDO token holders to wrap their tokens into a new, liquid governance token that needs to be managed in parallel with the existing LDO token. In our opinion, this is a blocker for implementation.
Security implications
Option four requires migration to an entirely new governance token, which is a level of change that is likely unacceptable to the Lido DAO from a security perspective.