--- tags: rage-report, shaman --- # Rage Report: Early Execution Shaman ## Team Santiago Dekan ## Background Currently, calldata submited on a dao proposals can only be executed after it fully goes through sponsor/voting/grace periods and it fullfills DAO's `quorumPercent` and `minRetentionPercent` requirements. However, many interactions with other protocols are time sensitive so the full timelock of voting and grace may not be acceptable. Moreover, the community has also shown interest in having an early execution mechanism that allows certain proposal use cases to be executed earlier. For example, setting a custom minimum quorum percent < `dao.quorumPercent` requirement to allow a proposal calldata to be executed before it meets the default dao governace settings. ## Deliverables - Defined standard interfaces for implementing different Shaman flavours based on ERC165 standard - PoC of a all-in-one Governor Shaman + Safe module contract that allows dao proposals to be whitelisted for early execution. - Contract should be able to execute calldata from whitelisted proposals that meet the minimum quorum percent set at the shaman level. - A Shaman Summoner contract based on Zodiac ModuleProxyFactory. ## Known issues - How to properly flag a proposal as canceled by a governor shaman but executed actionData. - Should it consider custom `quorumPercent` at the shaman level vs proposal level - Should it also consider setting a custom `minRetentionPercent` as another requirement for evaluating early execution? ## Potential Resources - [ERC165 Standard Interface detection](https://eips.ethereum.org/EIPS/eip-165) - [Zodiac module contract](https://github.com/gnosisguild/zodiac/blob/v3.5.2/contracts/core/Module.sol) - [Zodiac ModuleProxyFactory](https://github.com/gnosisguild/zodiac/blob/v3.5.2/contracts/factory/ModuleProxyFactory.sol) ## Experiments **Experiment #1** Workflow overview 1. Spin up a DAO with 4 members and `quorumPercent=100%` 1. Summon a EarlyExecutionShaman with params: (address `baalAddress`, uint256`minimumQuorumPercent` (25%), bytes32 `saltNonce`, string `details`). 1. Execute `setupEarlyExecutionShaman`: multisend proposal that executes `baal.setShamans(...)` + `baal.executeAsBaal(avatar, 0, avatar.execTransactionFromModule(avatar, 0, avatar.enableModule(...), 0))` 1. Submit, vote and execute a proposal through EarlyExecutionShaman module * Anyone can call `earlyExecutionShaman.submitProposal(encodedAction, proposal.expiration, proposal.baalGas, proposal.details)`. * A DAO member with `member.shares >= dao.sponsorThreshold` MUST sponsor proposal. * A way to bypass sponsoring is to mint enough shares to the shaman contract * DAO members vote on the proposal by calling `baal.submitVote(...) * IFF proposal has been whitelisted through the shaman, a dao member sponsored the proposal, it is currently in voting period and `proposa.yesVotes *100 >= earlyExecutionShaman.minimumQuorumPercent * proposal.maxTotalSharesAtSponsor` then calling `earlyExecutionShaman.earlyExecute(proposal.id, proposal.encodedAction)` will instruct the shaman to cancel the proposal with provided id and then execute proposal `encodedAction` calldata **Contract Features** * Inherits Zodiac module capabilities: * Ability to execute arbitrary contract calls on behalf of the DAO avatar * Inherits Governor Shaman capabilities: * Ability to cancel proposals: enabled * Ability to set DAO Governance config: disabled * Ablity to set truster forwarder (for meta-txs): disabled * By default, both `submitProposal` & `earlyExecute` functions are permissionless. Optionally, you can assign the following roles to addresses: * DEFAULT_ADMIN_ROLE: by default it is assigned to DAO avatar, so DAO can grant/revoke other roles in the contract. * SHAMAN_PROPOSER_ROLE: assigned to addresses that can submit proposals for early execution (submitProposal(...)). * SHAMAN_EXECUTOR_ROLE: assigned to addresses can execute proposals that qualify for early execution (earlyExecute(...)). * Setup parameters: * *baalAddress*: DAO address * *executorAddress*: address that holds the SHAMAN_EXECUTOR_ROLE (address(0) if you want the `earlyExecute` function to be permissionless) * *minimumQuorumPercent*: minimum quorum needed proposals to qualify for early execution * Early execution criteria: * `baal.state(proposal.id) == IBaal.ProposalState.Voting` --> proposal voting period must be ongoing * `proposal.yesVotes * 100 < minimumQuorumPercent * proposal.maxTotalSharesAtSponsor` --> minimum quorum * `proposal.noVotes == 0`--> no dao member has voted against it * It allows to setup the following types of guard mechanisms to prevent anyone to submiting malicious proposals: * any proposal submitted through `shaman.submitProposal(...)` requires sponsoring from a DAO member (unless the dao mints enough shares to the shaman to bypass sponsorThreshold) * The DAO can grant SHAMAN_PROPOSER_ROLE SHAMAN_EXECUTOR_ROLE to specific addresses * The DAO can setup a `Zodiac Roles Modifier` module to assign on-chain permissions to the shaman contract to call specific contracts/functions within the proposal calldata **Contract Code:** [(source)](https://github.com/HausDAO/baal-shamans/blob/feat/early-execute-shaman/contracts/earlyExecution/EarlyExecutionShaman.sol) ``` // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.7; // import { IBaal } from "@daohaus/baal-contracts/contracts/interfaces/IBaal.sol"; import { IBaal } from "../interfaces/IBaal.sol"; import { Enum } from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; import { FactoryFriendly, Module } from "@gnosis.pm/zodiac/contracts/core/Module.sol"; import { ModuleProxyFactory } from "@gnosis.pm/zodiac/contracts/factory/ModuleProxyFactory.sol"; import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; import { IEarlyExecutionShaman } from "./IEarlyExecutionShaman.sol"; import { IERC165, GovernorShaman } from "../shaman/GovernorShaman.sol"; // import "hardhat/console.sol"; error EarlyExecutionShaman__OperationNotSupported(); error EarlyExecutionShaman__InvalidQuorumPercent(); error EarlyExecutionShaman__Proposal_NotWhitelisted(); error EarlyExecutionShaman__Proposal_NoVotingPeriod(); error EarlyExecutionShaman__Proposal_CalldataMismatch(); error EarlyExecutionShaman__MinimumThresholdFailed(); contract EarlyExecutionShaman is GovernorShaman, Module, AccessControlEnumerableUpgradeable, IEarlyExecutionShaman { bytes32 public constant EXECUTOR_ROLE = keccak256("SHAMAN_EXECUTOR_ROLE"); bytes32 public constant PROPOSER_ROLE = keccak256("SHAMAN_PROPOSER_ROLE"); address internal _multisendLibrary; uint256 internal _minimumQuorumPercent; mapping(uint256 => bool) private _whitelistedProposals; event ProposalWhitelisted(uint256 _proposalId); event ProposalEarlyExecuted(uint256 indexed _proposalId, bool _actionFailed); modifier hasRoleOrPermissionless (bytes32 _role) { if (getRoleMemberCount(_role) > 0) _checkRole(_role); _; } function __EarlyExecutionShaman_init( address baalAddress, address executorAddress, uint256 minimumQuorumPercent ) internal onlyInitializing { if (minimumQuorumPercent < 1 || minimumQuorumPercent > 100) revert EarlyExecutionShaman__InvalidQuorumPercent(); __GovernorShaman_init(baalAddress); // __AccessControlEnumerable_init(); __Ownable_init(); __EarlyExecutionShaman_init_unchained(executorAddress, minimumQuorumPercent); } function __EarlyExecutionShaman_init_unchained(address executorAddress, uint256 minimumQuorumPercent) internal onlyInitializing { // Baal config _multisendLibrary = _baal.multisendLibrary(); address _avatar = _baal.avatar(); // Zodiac Module config avatar = _avatar; target = _avatar; // Shaman config _minimumQuorumPercent = minimumQuorumPercent; _transferOwnership(_avatar); // AccessControl config _grantRole(DEFAULT_ADMIN_ROLE, avatar); if (executorAddress != address(0)) { _grantRole(EXECUTOR_ROLE, executorAddress); } } function setUp(bytes memory initializeParams) public override(FactoryFriendly, IEarlyExecutionShaman) initializer nonReentrant { (address baalAddress, address executorAddress, uint256 minimumQuorumPercent) = abi.decode(initializeParams, (address, address, uint256)); __EarlyExecutionShaman_init(baalAddress, executorAddress, minimumQuorumPercent); } /** * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override( GovernorShaman, AccessControlEnumerableUpgradeable, IERC165 ) returns (bool) { return interfaceId == type(IEarlyExecutionShaman).interfaceId || super.supportsInterface(interfaceId); } function setMinimumQuorumPercent(uint256 _quorumPercent) external onlyOwner { if (_quorumPercent > 100) revert EarlyExecutionShaman__InvalidQuorumPercent(); _minimumQuorumPercent = _quorumPercent; } function setGovernanceConfig(bytes memory /*_governanceConfig*/) public pure override(GovernorShaman) { revert EarlyExecutionShaman__OperationNotSupported(); } function setTrustedForwarder(address /*_trustForwarderAddress*/) public pure override(GovernorShaman) { revert EarlyExecutionShaman__OperationNotSupported(); } // TODO: should be sent via dao proposal? -> NO. explain need for sponsor to avoid spamming function submitProposal( bytes calldata _proposalData, uint32 _expiration, uint256 _baalGas, string calldata _details ) external payable hasRoleOrPermissionless(PROPOSER_ROLE) returns (uint256 proposalId) { proposalId = _baal.submitProposal{value: msg.value}(_proposalData, _expiration, _baalGas, _details); _whitelistedProposals[proposalId] = true; emit ProposalWhitelisted(proposalId); } function _checkEarlyExecution(IBaal.Proposal memory _proposal) internal view returns (bool) { // TODO: if (_baal.state(_proposal.id) != IBaal.ProposalState.Voting) revert EarlyExecutionShaman__Proposal_NoVotingPeriod(); // uint256 quorumPercent = _baal.quorumPercent(); /* minimum % of shares that must vote yes for it to pass*/ // standard validation -> proposal.yesVotes * 100 < quorumPercent * proposal.maxTotalSharesAtSponsor if (_proposal.yesVotes * 100 < _minimumQuorumPercent * _proposal.maxTotalSharesAtSponsor || _proposal.noVotes > 0) revert EarlyExecutionShaman__MinimumThresholdFailed(); // TODO: should it verify dilution? // /* auto-fails a proposal if more than (1- minRetentionPercent) * total shares exit before processing*/ // uint256 public minRetentionPercent = _baal.minRetentionPercent(); // (_baal.totalSupply()) < // (proposal.maxTotalSharesAndLootAtVote * minRetentionPercent) / 100 /*Check for dilution since high water mark during voting*/ return true; } function checkEarlyExecution(uint32 _proposalId) public view override returns (bool earlyExecuteReady) { IBaal.Proposal memory proposal = _baal.proposals(_proposalId); earlyExecuteReady = _checkEarlyExecution(proposal); } function earlyExecute( uint32 _proposalId, bytes calldata _proposalData ) external hasRoleOrPermissionless(EXECUTOR_ROLE) isBaalGovernor returns (bool success) { // TODO: check avatar module is enabled if (!_whitelistedProposals[_proposalId]) revert EarlyExecutionShaman__Proposal_NotWhitelisted(); IBaal.Proposal memory proposal = _baal.proposals(_proposalId); _checkEarlyExecution(proposal); // verify proposalData matches if (proposal.proposalDataHash != _baal.hashOperation(_proposalData)) revert EarlyExecutionShaman__Proposal_CalldataMismatch(); // cancel proposal cancelProposal(_proposalId); // execute proposal calldata as zodiac module success = exec( _multisendLibrary, 0, _proposalData, Enum.Operation.DelegateCall ); emit ProposalEarlyExecuted(_proposalId, success); } } contract EarlyExecutionShamanSummoner is Ownable { address public template; ModuleProxyFactory public moduleProxyFactory; event EarlyExecutionShamanSummoned( address indexed baal, address indexed shamanAddress, uint256 minimumQuorumPercent, string details ); constructor(address _moduleProxyFactory, address _template) Ownable() { moduleProxyFactory = ModuleProxyFactory(_moduleProxyFactory); template = _template; } function setSummonerConfig(address _moduleProxyFactory, address _template) external onlyOwner { moduleProxyFactory = ModuleProxyFactory(_moduleProxyFactory); template = _template; } function summon( address _baalAddress, address _executorAddress, uint256 _minimumQuorumPercent, uint256 _saltNonce, string calldata _details ) external returns (address proxy) { bytes memory initializationParams = abi.encode(_baalAddress, _executorAddress, _minimumQuorumPercent); proxy = moduleProxyFactory.deployModule( template, abi.encodeWithSelector(EarlyExecutionShaman.setUp.selector, (initializationParams)), _saltNonce ); emit EarlyExecutionShamanSummoned(_baalAddress, proxy, _minimumQuorumPercent, _details); } } ``` ## Expectations A shaman-based early execution mechanism would allow new use cases to be implemented on top of MolochV3 DAOs, such as integration with time sensitive (DeFi) protocols, emergency calls, etc ## Results/Findings `baal-shamans` repo branch - https://github.com/HausDAO/baal-shamans/tree/feat/early-execute-shaman ## Demos Run the following command on a terminal ``` yarn hardhat test test/earlyExecution/EarlyExecutionShaman.ts ``` ## Pull Requests TBD ## Proposed Next Steps - Complete natspec docs and test coverage for EarlyExecutionShaman contract. - Release a `baal-shaman` package that defines Shaman standard interfaces to allow the community to implement new kind of use cases. - Update DAOHaus subgraph indexing to include EarlyExecutionShaman contracts & update the state of whitelisted proposals. - Update DAOHaus Admin dApp to allow DAOs to deploy + add a EarlyExecutionShaman module - Update DAOHaus Admin dApp to include an new kind of proposal execution mode ## Other ideas to explore