Following our initial post, we’re proposing to roll out a new feature that boosts both flexibility and security: the Sub-Roles Modifier architecture.
The core benefits for the DAO—enabling the Swapper and Disassembler roles—remain unchanged from our previous posts. This updated version proposes a change in how these roles are managed—introducing a modular architecture that separates role execution from governance-level permissioning.
Rather than requiring a separate governance proposal for every sub-role update, the proposed structure allows the existing Manager Safe to manage sub-role creation, setup and updates directly. However, any transaction initiated through these sub-roles must still pass through the existing Manager Roles Modifier, meaning that the change is purely for efficient management of existing permissions, rather than permission creation or amendment.
This setup reduces the need for future DAO interactions, whilst retaining our existing guarantee that any transaction or function call outside the defined Manager Role cannot be executed—making it easier for the DAO to monitor and audit operations with confidence.
Motivation
The sub-roles setup brings several operational benefits:
Streamlined Operational Control
Agile execution roles can be spun up, adding responsiveness for edge-case scenarios, without having to duplicate permissions that are already in the Manager Roles contract.
Clear Separation of Duties
With sub-roles, we can define tightly-scoped permissions for different agents (i.e. Disassembler, Swapper and/or Harvester agents) while still routing all transactions through the Manager Roles Modifier. This maintains all the existing permissions in one place, but without sacrificing flexibility in having to reset permissions from the Avatar Safe every time an agent is added or changed.
Simplified Deployment & Management
The Avatar Safe deploys a Sub-Roles Modifier as a module in the Manager Roles once. The target of the sub-roles will be the parent Manager Role, and the owner will be the Manager Safe. This way, the Manager Safe can handle maintenance on all sub-roles without the need to seek additional permissions from the Avatar Safe.
In the above flowchart you can see that any sub-role that is set up in the lower level can initiate a transaction through different agents. But regardless of how the transaction is initiated, the calldata’s arguments will need to fit within the previously-allowed/listed permissions granted to the existing Manager Role.
For example: a “Harvester EOA” could be automated to routinely claim all reward tokens on the managed treasury through the “Harvester Role”; subsequently, a “Swapper EOA” could be automated to routinely swap all claimed tokens for ETH using the “Swapper Role”. These actions will be executed on the Avatar Safe so long as the permissions exist on the Manager Role. The primary benefit is that these automations can then be configured, amended or even disabled independently with complete flexibility (within the scope of existing permissions), allowing for granular management of each operation.
These sub-roles can be configured as part of our internal execution app allowing greater flexibility and granular control with minimal effort and risk. The benefits were described in the first iteration of this proposal.
Technical implementation
The process involves deploying a new Sub-Roles Modifier, configuring it as a sub-role of the existing Roles contract, and assigning it the appropriate permissions. Here’s what happens under the hood:
Enable ZRM B as a module in ZRM A (existing Manager Roles).
Set the default role for ZRM B inside ZRM A.
Assign ZRM B to the MANAGER role in ZRM A.
Configure ZRM B to target ZRM A.
Transfer ownership of ZRM B from the Avatar Safe to the Manager Safe.
Upon execution of this payload, the Manager Safe will then have full power to create and amend sub-roles under ZRM B, within the existing permissions defined already under ZRM A. No further action is required to implement the sub-roles architecture beyond this payload.
We want to share the following Tenderly simulation using Virtual TestNets to showcase how Sub-Roles functions using a test EOA and Roles.
The simulation covers:
Deployment and configuration of the Sub-Roles Modifier (first 8 transactions starting from the “deployModule” up; these are the same actions than the ones in the PR for this proposal).
Assigning a sample EOA (0x5c0c7…ae43) to a new MANAGER role (9th txn).
The following 2 transaction (10th and 11th) apply the policy to the MANAGER Role of the Sub-Roles Modifier, allowing the approval of wstETH with the Aave V3 Core Pool as spender.
A successful transaction execution through the sub-role (approving wstETH with the Aave V3 Core Pool as spender).
A failed transaction that correctly reverts due to insufficient permissions (attempting to approve USDC for the Aave V3 Core Pool, where only wstETH is allowed).
We hope these simulations help clarify the Sub-Roles architecture and its functionalities. As always, we’re happy to receive any questions or feedback.
Following the deployment of the kpk-managed treasury on Gnosis Chain via BIP-732, we also intend to update the Gnosis Chain setup to adopt the Sub-Roles Modifier architecture described in this proposal.
To support this, we’re sharing a Tenderly VTNet simulation, which demonstrates:
Deployment of the full payload for the Sub-Roles Modifier
Application of a test policy to a MANAGER role within the new architecture, replicating an existing permission from the main role
Two simulated transactions from a role member — one successful, one failing — confirming expected behaviour
0xcf4fF1e03830D692F52EB094c52A5A6A2181Ab3F not present on mainnet, there’s a different signer between the two chains
Both payloads are the same except chainId
The payloads will perform the following:
Perform the same operation on both Gnosis Chain and Mainnet, the only difference in payload is the chainId, the bytecode must match exactly between the two
Deploy a copy of Roles (same bytecode as the already deployed Roles contracts on Mainnet and Gnosis Chain)
Grant ownership of the New Roles Contract to the kpk Multisig (2/9)
The New Roles Contract will receive the roleKey: 0x4d414e4147455200000000000000000000000000000000000000000000000000 on the Mainnet Roles
We believe this allows the kpk multisig to assign new roles on the New Roles Contract
While the operations that can be performed on the 0x13c6 Roles Contract will be limited
We are missing a tool to be able to determine accurately what the roleKey will do and it’s limitations
We could build a tool to scrape logs, but would ask if there’s already a tool available
Asks for Kpk
Can you please provide us with a tool that would allow us and the public to determine the exact scopes and limitations for the roleKey: 0x4d414e4147455200000000000000000000000000000000000000000000000000?
We could build a tool to scrape logs if not available, but would expect such a tool to already exist
The Payload seems to set a default role and then assign the same role, is this necessary?
We would not necessarily suggest changes but believe it’s worth clarifying why both settings have been enabled instead of just one
/// @param roleKey Role to be set as default.
function setDefaultRole(
address module,
bytes32 roleKey
) external onlyOwner {
defaultRoles[module] = roleKey;
emit SetDefaultRole(module, roleKey);
}
0x4d414e4147455200000000000000000000000000000000000000000000000000 UNCLEAR what this is
Thank you @Entreprenerd for your thorough review and the time you’ve dedicated to auditing these payloads. Your commitment and detailed feedback are deeply appreciated.
Any modifications to permission payloads must be approached with utmost diligence and transparency. This ensures the DAO can maintain confidence in the integrity and reliability of all executed transactions.
Below, we address each of the points you’ve raised:
“0xcf4fF1e03830D692F52EB094c52A5A6A2181Ab3F not present on mainnet, there’s a different signer between the two chains”.
Great catch — thanks for flagging this.
This is a signer set change that has indeed been implemented on Ethereum Mainnet, and is currently in queue for Gnosis Chain. We’re coordinating its execution with Maxis.
“Can you please provide us with a tool that would allow us and the public to determine the exact scopes and limitations for the roleKey: 0x4d414e4147455200000000000000000000000000000000000000000000000000”
The Zodiac Roles UI has become a central part of our migration to Roles V2, as outlined in BIP-667.
You can use the following link to inspect the live, on-chain permissions currently assigned to the Manager Roles Module.
Where:
0x13c61a25DB73e7a94a244bD2205aDba8b4a60F4a is the Roles V2 contract instance.
0x4d414e4147455200000000000000000000000000000000000000000000000000 corresponds to the Manager Role key, assigned to the Manager Safe. (Note: You can use a Hex-to-Text decoder (e.g. this one) to convert the Role Key into its ASCII representation, which is; “MANAGER”).
All permissioned target contracts, their function selectors, and scoped parameters should be available for anyone to verify.
“The Payload seems to set a default role and then assign the same role, is this necessary?”
As you rightly pointed out—and as outlined in the architecture flowchart—we’ll deploy a Sub-Roles Module with its Role Key set to match the Manager Role Key (0x4d414e4147455200000000000000000000000000000000000000000000000000).
The key distinction is that ownership of this Sub-Role Module will be assigned to the Manager Safe. This design ensures that any nested roles created within the Sub-Role Module remain fully scoped and cannot exceed the permissions defined in the MANAGER Roles Module.
“We Recommend reviewing old roles contracts and possibly deprecating them”.
To keep things clean and consistent, we’ll queue the removal of the Manager Role V1 from the Managed Treasury and coordinate its execution with the Maxis.
“MultisendUnwrapper seems to allow you to perform any arbitrary call”.
The MultiSendUnwrapper enables the Roles contract to decode and evaluate each call within a batched transaction, such as those sent through the multiSend(bytes) function. This allows complex actions—like “claim, wrap, and send”—to be permission-checked individually, even when bundled into a single transaction.
You’ll find more about this function in the Zodiac Roles documentation.
Thank you for the prompt reply and additional information @kpk
Replies and additional review
Sounds good
I have verified the permissions through the interface
It’s worth noting that due to tooling limitation (mainly due to the inability if not outright impossibility to price an asset at a specific instance)
The operations rely on the operator being benign as they may otherwise skim value through repeated transactions with high slippage / requesting a low minOut.
It’s worth noting that Cowswap generally offers protection against it.
However, for swaps of a sufficiently high size, Cowswap may also fail to provide swap protection.
This is probably a known issue with the current implementation of the Roles module, and warrants that the community monitors the Managed Treasury for a pattern that matches this.
A good recommendation is to always use an MEV Protected RPC
The community should track the possible ETH refunds with the expectation that those refunds will be sent back to the Managed Treasuery
Setting a default role allows to use the shortcut function execTransactionFromModule as it will use the default role and then check for belonging to said role:
function execTransactionFromModule(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) public override returns (bool success) {
Consumption[] memory consumptions = _authorize(
defaultRoles[msg.sender],
to,
value,
data,
operation
);
This means that both calls (TX 5 and TX 6) are necessary as you will add the newly deployed Roles contract as Module of the current Roles Contract
Sounds good
I have verified the unwrapper logic, through checking the contract source as well as the provided documentation.
Assuming the contract works as intended, it will parse the calldata length, and the offsets, then proceed to return each individual multiSend payload as an individual transaction to be validated by the Roles Module
This will allow multiSend to be used, while ensuring every call is verified by the roles modules.
Next steps
I have no more questions, and agree with the suggested next steps of the removal of the Manager Role V1 from the Managed Treasury
Happy to answer any questions the community may have.
I recommend that in the future, a calldata payload is also provided, ensuring that all steps of the public review match the exact code that will be executed onchain