This reposity contains the codebase of the Taiko DAO, along with its 3 plugins and helper contracts.
The DAO contract is an Aragon DAO, on which an Optimistic Token Voting plugin has the permission to execute proposals. Proposals on this plugin can only be created by two distinct multisig plugins, belonging to Taiko's Security Council.
The Security Council operates a standard multisig plugin and an emergency variant. The standard Multisig is designed to be the place where DAO proposals start their journey. Any signer can submit a new proposal to the Security Council. After a certain approval ratio is reached, the proposal will be forwarded to the Optimistic voting plugin, where the community will need to ratify it.
The Emergency Multisig, is meant to only be used under exceptional circumstances, i.e. when a critical vulnerability needs to be addressed immediately. Any signer can submit proposals as well, but these proposals will need to be approved by a super majority before they can be executed directly on the DAO.
Another key difference is that the Emergency Multisig is designed such in a way that the human readable description and the actions are private to the signers until the proposal is finally executed.
See Deploying the DAO below and check out the contract deployments.
This plugin is an adapted version of Aragon's Optimistic Token Voting plugin.
Only addresses that have been granted PROPOSER_PERMISSION_ID
on the plugin can create proposals. These adresses belong to the two multisig's governed by the Security Council.
Proposals can only be executed when the veto threshold hasn't been reached after a given period of time.
The governance settings need to be defined when the plugin is installed but the DAO can update them at any time.
- Only proposers can create proposals on the plugin
- The plugin can execute actions on the DAO
- The DAO can update the plugin settings
- The DAO can upgrade the plugin
It allows the Security Council members to create and approve proposals. After a certain minimum of approvals is met, proposals can be relayed to the Optimistic Token Voting plugin only.
The ability to relay proposals to the Optimistic Token Voting plugin is restricted by a permission condition, which ensures that a minimum veto period is defined as part of the parameters.
- Only members can create proposals
- Only members can approve
- The plugin can only create proposals on the Optimistic Token Voting plugin provided that the
duration
is equal or greater than the minimum defined - The DAO can update the plugin settings
Like before, this plugin allows Security Council members to create and approve proposals. If a super majority approves, proposals can be relayed to the Optimistic Token Voting plugin with a delay period of potentially 0. This is, being executed immediately.
The address list of this plugin is taken from the standard Multisig plugin. Any changes on the former will effect both plugin instances.
There are two key differences with the standard Multisig:
- The proposal's metadata and the actions to execute are encrypted, only the Security Council members have the means to decrypt them
- When the proposal is executed, the metadata and the actions become publicly visible on the Optimistic Token Voting plugin. There is an integrity check to prevent any changes to the originally approved content.
The Emergency Multisig settings are the same as for the standard Multisig.
- Only members can create proposals
- Only members can approve
- The plugin can only create proposals on the Optimistic Token Voting plugin provided that the
duration
is equal or greater than the minimum defined - The DAO can update the plugin settings
This is a helper contract that allows Security Council members to register the public key of their deterministic ephemeral wallet. The available public keys will be used to encrypt the proposal metadata and actions. Refer to the UI repository for the encryption details.
NOTE: A published public key cannot be changed once published.
- A wallet can only generate one derived key pair.
- Public key registration is an automated process. No human error should be possible.
- Altering an encryption key is a strange edge case of which the rest of signers should be aware of.
This is a very simple contract that serves the purpose of storing the IPFS URI's pointing to the delegation profile posted by all candidates. Profiles can be updated by the owner and read by everyone.
This is taken care by the TaikoDAOFactory contract. It is invoked by scripts/Deploy.s.sol and it creates a holistic, immutable DAO deployment, given some settings. To create a new DAO with new settings, a new factory needs to be deployed.
Plugin changes need a proposal to be passed when the DAO already exists.
There are two steps, a permissionless preparation and a privileged application.
- Calling
pluginSetupProcessor.prepareInstallation()
- A new plugin instance is deployed with the desired settings
- The call stores the request of a set of permissions
- A proposal is passed to make the DAO call
applyInstallation()
on the PluginSetupProcessor- This applies the requested permissions and the plugin becomes installed
See OptimisticTokenVotingPluginSetup.
Learn more about plugin setup's and preparing installations.
OSx DAO's are designed to hold all the assets and rights by themselves. On the other hand, plugins are custom opt-in pieces of logic that can implement any type of governance. They are meant to eventually make the DAO execute a certain set of actions.
The whole ecosystem is governed by the DAO's permission database, which is used to restrict actions to only the role holding the appropriate permission.
An Aragon DAO is a set of permissions that are used to define who can do what, and where.
A permission looks like:
- An address
who
holdsMY_PERMISSION_ID
on a target contractwhere
Brand new DAO's are deployed with a ROOT_PERMISSION
assigned to its creator, but the DAO will typically deployed by the DAO factory, which will install all the requested plugins and drop the ROOT permission after the set up is done.
Managing permissions is made via two functions that are called on the DAO:
function grant(address _where, address _who, bytes32 _permissionId);
function revoke(address _where, address _who, bytes32 _permissionId);
For the cases where an unrestricted permission is not derisable, a Permission Condition can be used.
Conditional permissions look like this:
- An address
who
holdsMY_PERMISSION_ID
on a target contractwhere
, onlywhen
the condition contract approves it
Conditional permissions are granted like this:
function grantWithCondition(
address _where,
address _who,
bytes32 _permissionId,
IPermissionCondition _condition
);
See the condition contract boilerplate. It provides the plumbing to easily restrict what the different multisig plugins can propose on the OptimisticVotingPlugin.
Learn more about OSx permissions
Below are all the permissions that a PluginSetup contract may want to request:
EXECUTE_PERMISSION
is required to make the DAOexecute
a set of actions- Only governance plugins should have this permission
ROOT_PERMISSION
is required to make the DAOgrant
orrevoke
permissions- The DAO needs to be ROOT on itself (it is by default)
- Nobody else should be ROOT on the DAO
UPGRADE_PLUGIN_PERMISSION
is required for an address to be able to upgrade a plugin to a newer version published by the developer- Typically called by the DAO via proposal
- Optionally granted to an additional address for convenience
PROPOSER_PERMISSION
is required to be able to create optimistic proposals on the governance pluginUPDATE_MULTISIG_SETTINGS_PERMISSION_ID
is used by the DAO to update the settings of a multisig plugin, if the community approvesUPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID
is used by the DAO to update the settings of the optimistic voting plugin, if the community approves
Other DAO specific permissions:
UPGRADE_DAO_PERMISSION
SET_METADATA_PERMISSION
SET_TRUSTED_FORWARDER_PERMISSION
SET_SIGNATURE_VALIDATOR_PERMISSION
REGISTER_STANDARD_CALLBACK_PERMISSION
Making calls to the DAO is straightforward, however making execute arbitrary actions requires them to be encoded, stored on chain and be approved before they can be executed.
To this end, the DAO has a struct called Action { to, value, data }
, which will make the DAO call the to
address, with value
ether and call the given calldata (if any). Such calldata is an ABI encoded array of bytes with the function to call and the parameters it needs.
- Never grant
ROOT_PERMISSION
unless you are just trying things out - Never uninstall all plugins, as this would brick your DAO
- Ensure that there is at least always one plugin with
EXECUTE_PERMISSION
on the DAO - Ensure that the DAO is ROOT on itself
- Use the
_gap[]
variable for upgradeable plugins, as a way to reserve storage slots for future plugin implementations- Decrement the
_gap
number for every new variable (slot) you add in the future
- Decrement the
By default, only the DAO can upgrade plugins to newer versions. This requires passing a proposal.
Learn more about plugin upgrades
To work with the repository you need to install Foundry on your operating system.
$ forge build
$ forge test
$ forge fmt
$ forge snapshot
$ anvil
$ cast <subcommand>
- Edit
script/multisig-members.json
with the list of addresses to set as signers - Run
forge build && forge test
- Copy
.env.example
into.env
and define the settings - Run
source .env
to load them - Set the RPC URL and run the deployment script
RPC_URL="https://eth-holesky.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify
If you get the error Failed to get EIP-1559 fees
, add --legacy
to the last command:
forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --legacy
If a some contracts fail to verify on Etherscan, retry with this command:
forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --verify --legacy --private-key "$DEPLOYMENT_PRIVATE_KEY" --resume
See the test tree file for a visual representation of the implemented tests.
Tests can be described using yaml files. They will be automatically transformed into solidity test files with bulloak.
Create a file with .t.yaml
extension within the test
folder and describe a hierarchy of test cases:
# MyTest.t.yaml
MultisigTest:
- given: proposal exists
comment: Comment here
and:
- given: proposal is in the last stage
and:
- when: proposal can advance
then:
- it: Should return true
- when: proposal cannot advance
then:
- it: Should return false
- when: proposal is not in the last stage
then:
- it: should do A
comment: This is an important remark
- it: should do B
- it: should do C
- when: proposal doesn't exist
comment: Testing edge cases here
then:
- it: should revert
Then use make
to automatically sync the described branches into solidity test files.
$ make
Available targets:
Available targets:
- make all Builds all tree files and updates the test tree markdown
- make sync Scaffold or sync tree files into solidity tests
- make check Checks if solidity files are out of sync
- make markdown Generates a markdown file with the test definitions rendered as a tree
- make init Check the dependencies and prompt to install if needed
- make clean Clean the intermediary tree files
$ make sync
The final output will look like a human readable tree:
# MyTest.tree
EmergencyMultisigTest
├── Given proposal exists // Comment here
│ ├── Given proposal is in the last stage
│ │ ├── When proposal can advance
│ │ │ └── It Should return true
│ │ └── When proposal cannot advance
│ │ └── It Should return false
│ └── When proposal is not in the last stage
│ ├── It should do A // Careful here
│ ├── It should do B
│ └── It should do C
└── When proposal doesn't exist // Testing edge cases here
└── It should revert