Skip to content

Commit

Permalink
Merge pull request #21 from lidofinance/feature/lip-18-limits-for-com…
Browse files Browse the repository at this point in the history
…mittees

Feature: LIP-18. Limits for committees
  • Loading branch information
TheDZhon authored Sep 21, 2022
2 parents 012ae9c + 6b8950b commit 22c9555
Show file tree
Hide file tree
Showing 48 changed files with 5,744 additions and 6,709 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

## Problem

Lido DAO governance currently relies on Aragon voting model. This means DAO approves or rejects proposals via direct governance token voting. Though transparent and reliable, it is not a convenient way to make decisions only affecting small groups of Lido DAO members. Besides, direct token voting doesn't exactly reflect all the decision making processes within the Lido DAO and is often used only to rubberstamp an existing consensus.
There are a few natural sub-governance groups within the DAO, e.g. validators commitee, financial operations team and LEGO commitee. Every day they need to take routine actions only related to their field of expertise. The decisions they make hardly ever spark any debate in the comunity, and votings on such decisions often struggle to attract wider DAO attention and thus, to pass.
Lido DAO governance currently relies on Aragon voting model. This means DAO approves or rejects proposals via direct governance token voting. Though transparent and reliable, it is not a convenient way to make decisions only affecting small groups of Lido DAO members. Besides, direct token voting doesn't exactly reflect all the decision making processes within the Lido DAO and is often used only to rubber-stamp an existing consensus.
There are a few natural sub-governance groups within the DAO, e.g. validators committee, financial operations team and LEGO committee. Every day they need to take routine actions only related to their field of expertise. The decisions they make hardly ever spark any debate in the community, and votings on such decisions often struggle to attract wider DAO attention and thus, to pass.

## Solution

Expand All @@ -23,7 +23,7 @@ See [specification.md](https://github.com/lidofinance/easy-track/blob/master/spe

## EVMScript Factory Requirements

### Methods Compability
### Methods Compatibility

**Every EVMScript factory must implement [`IEVMScriptFactory`](https://github.com/lidofinance/easy-track/blob/master/contracts/interfaces/IEVMScriptFactory.sol) interface.**

Expand All @@ -33,7 +33,7 @@ Methods from this interface are used by EasyTrack at the motion lifecycle.

**Every action done by EasyTrack must be allowed to do also by Aragon Voting.**

This requirement fills automatically in cases when easy tracks do actions provided by the Aragon application. But for contracts outside the Aragon ecosystem access to Voting must be provided explicitly. To grant such access you can use role-based control access contracts from the OpenZeppelin package. To see an example of how this pattern was used in EVMScript factories see the `RewardPrgoramsRegistry.sol` contract.
This requirement fills automatically in cases when easy tracks do actions provided by the Aragon application. But for contracts outside the Aragon ecosystem access to Voting must be provided explicitly. To grant such access you can use role-based control access contracts from the OpenZeppelin package. To see an example of how this pattern was used in EVMScript factories see the `RewardProgramsRegistry.sol` contract.

### Onchain EVMScript calldata decoding

Expand All @@ -60,11 +60,13 @@ To use the tools that this project provides, please pull the repository from Git
```bash
git clone https://github.com/lidofinance/easy-track
cd easy-track
npm install
yarn install
poetry install
poetry run brownie networks import network-config.yaml True
poetry shell
```


Compile the Smart Contracts:

```bash
Expand Down Expand Up @@ -148,5 +150,6 @@ Current brownie version has problems with coverage reports for some contracts. C
- [RemoveRewardProgram.sol](https://github.com/lidofinance/easy-track/blob/a72858804481009f2e09508ffbf93d8a4aee6c84/contracts/EVMScriptFactories/RemoveRewardProgram.sol#L23)
- [TopUpLegoProgram.sol](https://github.com/lidofinance/easy-track/blob/a72858804481009f2e09508ffbf93d8a4aee6c84/contracts/EVMScriptFactories/TopUpLegoProgram.sol#L26)
- [TopUpRewardProgram.sol](https://github.com/lidofinance/easy-track/blob/a72858804481009f2e09508ffbf93d8a4aee6c84/contracts/EVMScriptFactories/TopUpRewardPrograms.sol#L27)
- [TopUpAllowedRecipients.sol](https://github.com/lidofinance/easy-track/blob/522ae893f6c03516354a8d1950b29b3203adae52/contracts/EVMScriptFactories/TopUpAllowedRecipients.sol#L29)

The workaround for the coverage problem is removing the `immutable` modifier from the above contracts. Without modifier above contracts will be listed in the coverage report
6 changes: 2 additions & 4 deletions brownie-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ networks:
mnemonic: brownie
fork: mainnet

dependencies:
- OpenZeppelin/[email protected]

# path remapping to support OpenZepplin imports with NPM-style path
compiler:
solc:
version: 0.8.6
remappings:
- OpenZeppelin/[email protected]=./dependencies/OpenZeppelin/[email protected]
reports:
exclude_paths:
- contracts/test/**/*
Expand Down
152 changes: 152 additions & 0 deletions contracts/AllowedRecipientsRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: 2022 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.4;

import "./LimitsChecker.sol";

/// @author psirex, zuzueeka
/// @title Registry of allowed addresses for payouts
/// @notice Stores list of allowed addresses
contract AllowedRecipientsRegistry is LimitsChecker {
// -------------
// EVENTS
// -------------
event RecipientAdded(address indexed _recipient, string _title);
event RecipientRemoved(address indexed _recipient);

// -------------
// ROLES
// -------------
bytes32 public constant ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE =
keccak256("ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE");
bytes32 public constant REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE =
keccak256("REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE");

// -------------
// ERRORS
// -------------
string private constant ERROR_RECIPIENT_ALREADY_ADDED_TO_ALLOWED_LIST =
"RECIPIENT_ALREADY_ADDED_TO_ALLOWED_LIST";
string private constant ERROR_RECIPIENT_NOT_FOUND_IN_ALLOWED_LIST =
"RECIPIENT_NOT_FOUND_IN_ALLOWED_LIST";

// -------------
// VARIABLES
// -------------

/// @dev List of allowed addresses for payouts
address[] public allowedRecipients;

// Position of the address in the `allowedRecipients` array,
// plus 1 because index 0 means a value is not in the set.
mapping(address => uint256) private allowedRecipientIndices;

// -------------
// CONSTRUCTOR
// -------------

/// @param _admin Address which will be granted with role DEFAULT_ADMIN_ROLE
/// @param _addRecipientToAllowedListRoleHolders List of addresses which will be
/// granted with role ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE
/// @param _removeRecipientFromAllowedListRoleHolders List of addresses which will
/// be granted with role REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE
/// @param _setLimitParametersRoleHolders List of addresses which will
/// be granted with role SET_LIMIT_PARAMETERS_ROLE
/// @param _updateSpentAmountRoleHolders List of addresses which will
/// be granted with role UPDATE_SPENT_AMOUNT_ROLE
/// @param _bokkyPooBahsDateTimeContract Address of bokkyPooBahs DateTime Contract
constructor(
address _admin,
address[] memory _addRecipientToAllowedListRoleHolders,
address[] memory _removeRecipientFromAllowedListRoleHolders,
address[] memory _setLimitParametersRoleHolders,
address[] memory _updateSpentAmountRoleHolders,
IBokkyPooBahsDateTimeContract _bokkyPooBahsDateTimeContract
)
LimitsChecker(
_setLimitParametersRoleHolders,
_updateSpentAmountRoleHolders,
_bokkyPooBahsDateTimeContract
)
{
_setupRole(DEFAULT_ADMIN_ROLE, _admin);
for (uint256 i = 0; i < _addRecipientToAllowedListRoleHolders.length; i++) {
_setupRole(
ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE,
_addRecipientToAllowedListRoleHolders[i]
);
}
for (uint256 i = 0; i < _removeRecipientFromAllowedListRoleHolders.length; i++) {
_setupRole(
REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE,
_removeRecipientFromAllowedListRoleHolders[i]
);
}
}

// -------------
// EXTERNAL METHODS
// -------------

/// @notice Adds address to list of allowed addresses for payouts
function addRecipient(address _recipient, string memory _title)
external
onlyRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE)
{
require(
allowedRecipientIndices[_recipient] == 0,
ERROR_RECIPIENT_ALREADY_ADDED_TO_ALLOWED_LIST
);

allowedRecipients.push(_recipient);
allowedRecipientIndices[_recipient] = allowedRecipients.length;
emit RecipientAdded(_recipient, _title);
}

/// @notice Removes address from list of allowed addresses for payouts
/// @dev To delete an allowed address from the allowedRecipients array in O(1),
/// we swap the element to delete with the last one in the array,
/// and then remove the last element (sometimes called as 'swap and pop').
function removeRecipient(address _recipient)
external
onlyRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE)
{
uint256 index = _getAllowedRecipientIndex(_recipient);
uint256 lastIndex = allowedRecipients.length - 1;

if (index != lastIndex) {
address lastAllowedRecipient = allowedRecipients[lastIndex];
allowedRecipients[index] = lastAllowedRecipient;
allowedRecipientIndices[lastAllowedRecipient] = index + 1;
}

allowedRecipients.pop();
delete allowedRecipientIndices[_recipient];
emit RecipientRemoved(_recipient);
}

/// @notice Returns if passed address is listed as allowed recipient in the registry
function isRecipientAllowed(address _address) external view returns (bool) {
return allowedRecipientIndices[_address] > 0;
}

/// @notice Returns current list of allowed recipients
function getAllowedRecipients() external view returns (address[] memory) {
return allowedRecipients;
}

// ------------------
// PRIVATE METHODS
// ------------------

function _getAllowedRecipientIndex(address _evmScriptFactory)
private
view
returns (uint256 _index)
{
_index = allowedRecipientIndices[_evmScriptFactory];
require(_index > 0, ERROR_RECIPIENT_NOT_FOUND_IN_ALLOWED_LIST);
_index -= 1;
}
}
88 changes: 88 additions & 0 deletions contracts/EVMScriptFactories/AddAllowedRecipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2022 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.4;

import "../TrustedCaller.sol";
import "../AllowedRecipientsRegistry.sol";
import "../libraries/EVMScriptCreator.sol";
import "../interfaces/IEVMScriptFactory.sol";

/// @author psirex, zuzueeka
/// @notice Creates EVMScript to add new allowed recipient address to AllowedRecipientsRegistry
contract AddAllowedRecipient is TrustedCaller, IEVMScriptFactory {
// -------------
// ERRORS
// -------------
string private constant ERROR_ALLOWED_RECIPIENT_ALREADY_ADDED =
"ALLOWED_RECIPIENT_ALREADY_ADDED";

// -------------
// VARIABLES
// -------------

/// @notice Address of AllowedRecipientsRegistry
AllowedRecipientsRegistry public allowedRecipientsRegistry;

// -------------
// CONSTRUCTOR
// -------------

constructor(address _trustedCaller, address _allowedRecipientsRegistry)
TrustedCaller(_trustedCaller)
{
allowedRecipientsRegistry = AllowedRecipientsRegistry(_allowedRecipientsRegistry);
}

// -------------
// EXTERNAL METHODS
// -------------

/// @notice Creates EVMScript to add new allowed recipient address to allowedRecipientsRegistry
/// @param _creator Address who creates EVMScript
/// @param _evmScriptCallData Encoded tuple: (address recipientAddress)
function createEVMScript(address _creator, bytes memory _evmScriptCallData)
external
view
override
onlyTrustedCaller(_creator)
returns (bytes memory)
{
(address recipientAddress, ) = _decodeEVMScriptCallData(_evmScriptCallData);
require(
!allowedRecipientsRegistry.isRecipientAllowed(recipientAddress),
ERROR_ALLOWED_RECIPIENT_ALREADY_ADDED
);

return
EVMScriptCreator.createEVMScript(
address(allowedRecipientsRegistry),
allowedRecipientsRegistry.addRecipient.selector,
_evmScriptCallData
);
}

/// @notice Decodes call data used by createEVMScript method
/// @param _evmScriptCallData Encoded tuple: (address recipientAddress, string title)
/// @return Address of recipient to add
/// @return Title of the recipient
function decodeEVMScriptCallData(bytes memory _evmScriptCallData)
external
pure
returns (address, string memory)
{
return _decodeEVMScriptCallData(_evmScriptCallData);
}

// ------------------
// PRIVATE METHODS
// ------------------

function _decodeEVMScriptCallData(bytes memory _evmScriptCallData)
private
pure
returns (address, string memory)
{
return abi.decode(_evmScriptCallData, (address, string));
}
}
86 changes: 86 additions & 0 deletions contracts/EVMScriptFactories/RemoveAllowedRecipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2022 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.4;

import "../TrustedCaller.sol";
import "../AllowedRecipientsRegistry.sol";
import "../libraries/EVMScriptCreator.sol";
import "../interfaces/IEVMScriptFactory.sol";

/// @author psirex, zuzueeka
/// @notice Creates EVMScript to remove allowed recipient address from AllowedRecipientsRegistry
contract RemoveAllowedRecipient is TrustedCaller, IEVMScriptFactory {
// -------------
// ERRORS
// -------------
string private constant ERROR_ALLOWED_RECIPIENT_NOT_FOUND = "ALLOWED_RECIPIENT_NOT_FOUND";

// -------------
// VARIABLES
// -------------

/// @notice Address of AllowedRecipientsRegistry
AllowedRecipientsRegistry public allowedRecipientsRegistry;

// -------------
// CONSTRUCTOR
// -------------

constructor(address _trustedCaller, address _allowedRecipientsRegistry)
TrustedCaller(_trustedCaller)
{
allowedRecipientsRegistry = AllowedRecipientsRegistry(_allowedRecipientsRegistry);
}

// -------------
// EXTERNAL METHODS
// -------------

/// @notice Creates EVMScript to remove allowed recipient address from allowedRecipientsRegistry
/// @param _creator Address who creates EVMScript
/// @param _evmScriptCallData Encoded tuple: (address recipientAddress)
function createEVMScript(address _creator, bytes memory _evmScriptCallData)
external
view
override
onlyTrustedCaller(_creator)
returns (bytes memory)
{
require(
allowedRecipientsRegistry.isRecipientAllowed(
_decodeEVMScriptCallData(_evmScriptCallData)
),
ERROR_ALLOWED_RECIPIENT_NOT_FOUND
);
return
EVMScriptCreator.createEVMScript(
address(allowedRecipientsRegistry),
allowedRecipientsRegistry.removeRecipient.selector,
_evmScriptCallData
);
}

/// @notice Decodes call data used by createEVMScript method
/// @param _evmScriptCallData Encoded tuple: (address recipientAddress)
/// @return recipientAddress Address to remove
function decodeEVMScriptCallData(bytes memory _evmScriptCallData)
external
pure
returns (address recipientAddress)
{
return _decodeEVMScriptCallData(_evmScriptCallData);
}

// ------------------
// PRIVATE METHODS
// ------------------

function _decodeEVMScriptCallData(bytes memory _evmScriptCallData)
private
pure
returns (address)
{
return abi.decode(_evmScriptCallData, (address));
}
}
Loading

0 comments on commit 22c9555

Please sign in to comment.