Crypto Training

Mellow Verifier vs Zodiac Roles Modifier: Curator Calldata Checks

A code-level comparison of how Mellow Flexible Vaults and Zodiac Roles Modifier authorize curator transactions: allowlists, calldata constraints, allowances, adapters, and failure semantics.

Crypto Training2025-12-268 min read

When people compare these systems, they often mix up two different questions:

  • who is allowed to act
  • what exact calldata is allowed to pass

This article focuses on the second question: curator calldata checks.

Code references:

Scope and mental model#

Both systems gate outbound calls to external protocols, but they sit at different layers.

  • Mellow checks happen in a Subvault call gateway (CallModule + Verifier) inside a vault architecture.
  • Roles checks happen in a Safe modifier (Roles) between Safe modules and the Safe execution target.
flowchart LR subgraph MellowPath [Mellow path] direction TB C1["Curator caller"] --> SV["Subvault.call"] SV --> MV["Mellow Verifier"] MV --> EP1["External protocol"] end subgraph RolesPath [Roles path] direction TB M1["Enabled module"] --> RM["Roles modifier"] RM --> S["Safe avatar"] S --> EP2["External protocol"] end C1 --- M1

1) Entry points and where checks are enforced#

Mellow#

  • Subvault composes CallModule and VerifierModule: Subvault.sol
  • External call entrypoint is CallModule.call(where, value, data, payload): CallModule.sol
  • CallModule.call invokes verifier().verifyCall(...) before Address.functionCallWithValue(...).

Roles Modifier#

  • Main contract: Roles.sol
  • Main execution APIs: execTransactionFromModule, execTransactionWithRole, and ...ReturnData variants.
  • Authorization logic starts in _authorize(...) from PermissionChecker.sol.
sequenceDiagram autonumber participant Curator participant Subvault participant Verifier participant Protocol Curator->>Subvault: call(where, value, data, payload) Subvault->>Verifier: verifyCall(who, where, value, data, payload) alt verification fails Verifier-->>Subvault: revert VerificationFailed else verification passes Subvault->>Protocol: functionCallWithValue(where, data, value) end
sequenceDiagram autonumber participant Module participant Roles participant Checker participant Safe participant Protocol Module->>Roles: execTransactionWithRole(to, value, data, op, roleKey, shouldRevert) Roles->>Checker: _authorize(roleKey, to, value, data, op) alt authorization fails Checker-->>Roles: revert ConditionViolation(...) else authorization passes Roles->>Safe: exec(to, value, data, op) Safe->>Protocol: execute end

2) Mellow verifier semantics#

Mellow verifier logic is in Verifier.sol.

Role gate first#

getVerificationResult(...) starts by requiring vault().hasRole(CALLER_ROLE, who).

Verification types#

  • ONCHAIN_COMPACT: checks (who, where, selector) in on-chain set.
  • MERKLE_COMPACT: merkle-proved compact call hash.
  • MERKLE_EXTENDED: merkle-proved exact (who, where, value, full data) hash.
  • CUSTOM_VERIFIER: merkle-proved pointer to custom verifier + custom payload.
flowchart TB Start["verifyCall"] --> CallerRole{"CALLER_ROLE?"} CallerRole -->|No| Deny1["false"] CallerRole -->|Yes| VType{"verificationType"} VType --> OC["ONCHAIN_COMPACT"] VType --> MC["MERKLE_COMPACT"] VType --> ME["MERKLE_EXTENDED"] VType --> CV["CUSTOM_VERIFIER"] OC --> OCCheck["hash(who,where,selector) in allowed set"] MC --> MProof["Merkle proof valid?"] ME --> MProof CV --> MProof MC --> MCCheck["hash compact call == verificationData"] ME --> MECheck["hash extended call == verificationData"] CV --> CustomCheck["ICustomVerifier.verifyCall(...) returns true"] OCCheck --> Result MProof -->|No| Deny2["false"] MProof -->|Yes| Check2 Check2 --> MCCheck Check2 --> MECheck Check2 --> CustomCheck MCCheck --> Result MECheck --> Result CustomCheck --> Result

Practical implications#

  • Mellow can do exact full-calldata pinning cheaply with a merkle root update workflow.
  • Runtime numeric bounds (amount <= x) are not native unless encoded via custom verifier logic.

3) Roles Modifier semantics#

Core logic is split across:

Role membership and target/function clearance#

_authorize(...) checks:

  1. role key is non-zero
  2. caller has role membership
  3. if a transaction unwrapper is configured, unwrap then check each child tx
  4. per tx, enforce Clearance.Target or Clearance.Function
  5. if function-scoped and not wildcarded, evaluate condition tree
flowchart TB A["_authorize(role,to,value,data,op)"] --> R0{"roleKey != 0"} R0 -->|No| X0["revert NoMembership"] R0 -->|Yes| R1{"member of role?"} R1 -->|No| X1["revert NoMembership"] R1 -->|Yes| U{"unwrapper configured?"} U -->|No| T["_transaction"] U -->|Yes| MU["unwrap into tx array"] MU --> TI["_transaction for each child tx"] T --> C{"target clearance"} TI --> C C -->|None| XT["TargetAddressNotAllowed"] C -->|Target| EO["execution options check"] C -->|Function| FS["selector config exists?"] FS -->|No| XF["FunctionNotAllowed"] FS -->|Yes| W{"wildcarded?"} W -->|Yes| EO W -->|No| CT["decode payload + walk condition tree"] CT --> EO EO -->|fail| XE["Send/DelegateCall violation"] EO -->|ok| OK["authorized"]

Condition tree engine#

Roles supports rich operators and ABI-aware decoding:

  • logical: And, Or, Nor
  • structure-aware: Matches, ArraySome, ArrayEvery, ArraySubset
  • comparisons: EqualTo, GreaterThan, LessThan, signed variants
  • bit-level: Bitmask
  • quota: WithinAllowance, EtherWithinAllowance, CallWithinAllowance
  • extension: Custom
flowchart LR Root["Condition node"] --> O1["Logical operators"] Root --> O2["Matches and array operators"] Root --> O3["Comparators"] Root --> O4["Allowance operators"] Root --> O5["Custom operator"] O1 --> O1a["And"] O1 --> O1b["Or"] O1 --> O1c["Nor"] O2 --> O2a["Matches"] O2 --> O2b["ArraySome"] O2 --> O2c["ArrayEvery"] O2 --> O2d["ArraySubset"] O3 --> O3a["EqualTo"] O3 --> O3b["GreaterThan / LessThan"] O3 --> O3c["SignedInt comparisons"] O3 --> O3d["Bitmask"] O4 --> O4a["WithinAllowance"] O4 --> O4b["EtherWithinAllowance"] O4 --> O4c["CallWithinAllowance"] O5 --> O5a["ICustomCondition.check"]

Integrity enforcement at config time#

When calling scopeFunction(...), Integrity.enforce(conditions) validates tree shape, BFS ordering, operator/type compatibility, and root suitability.

That means malformed condition trees are rejected at permission-write time, not at first execution.

4) Storage model differences#

Mellow#

  • compact allowlist entries: on-chain set of bytes32 hashes
  • merkle mode: one root + proofs supplied per call
  • custom mode: verifier address comes via merkle-proved payload

Roles#

  • scoped function condition tree is packed and stored through WriteOnce pointers
  • header in storage references packed body pointer
  • on execution, loader reconstructs condition tree and allowance references
flowchart TB subgraph MellowStorage["Mellow storage"] M1["compactCallHashes set"] M2["merkleRoot"] M3["compactCalls mapping"] end subgraph RolesStorage["Roles storage"] R1["roles[role].targets"] R2["roles[role].scopeConfig header"] R3["WriteOnce pointer contract code"] R4["allowances mapping"] end R2 --> R3

5) Allowance and quota semantics#

This is a major differentiator.

Roles has first-class allowance consumption with refill rules in AllowanceTracker.sol, and it uses a two-phase commit model:

  • _flushPrepare(consumptions) before execution
  • _flushCommit(consumptions, success) after execution
  • failed execution restores balances

Mellow verifier path has no native equivalent for allowance/rate tracking.

sequenceDiagram autonumber participant Caller participant Roles participant Checker participant Allow as AllowanceTracker participant Safe Caller->>Roles: execTransactionWithRole(...) Roles->>Checker: _authorize(...) => consumptions[] Roles->>Allow: _flushPrepare(consumptions) Roles->>Safe: exec(...) alt success Roles->>Allow: _flushCommit(consumptions, true) Allow-->>Caller: balances consumed else failure Roles->>Allow: _flushCommit(consumptions, false) Allow-->>Caller: balances restored end

6) Multi-call and bundled transaction handling#

Roles has native adapter hooks (setTransactionUnwrapper) via _Periphery.sol, allowing policy checks over decomposed subcalls.

Examples:

Mellow handles one outward call(...) at a time and leaves higher-level batching semantics to external tooling or target contract behavior.

flowchart TB TX["Bundled tx"] --> RU["Roles unwrapper"] RU --> T1["subtx 1"] RU --> T2["subtx 2"] RU --> T3["subtx 3"] T1 --> PC["Permission check"] T2 --> PC T3 --> PC PC --> EX["execute only if all pass"]

7) Side-by-side comparison for curator calldata checks#

DimensionMellow VerifierZodiac Roles Modifier
Primary entrypointSubvault.callexecTransactionWithRole / module execution
Principal role gateCALLER_ROLE on Vault ACLmembership in role for module/signer
Selector allowlistYes (ONCHAIN_COMPACT)Yes (allowFunction wildcard / scoped)
Exact full calldata pinningYes (MERKLE_EXTENDED)Possible via conditions, but generally expression-based
Merkle-proof flowNativeNot core pattern
Runtime typed constraintsCustom verifier requiredNative condition operators
Built-in spending/call quotasNoYes (WithinAllowance, CallWithinAllowance, etc.)
Native bundle unwrappingNoYes (transaction unwrappers)
Policy update gateVault role systemRoles owner (onlyOwner)
Typical policy representationcompact hashes + merkle rootcondition trees + packed storage pointer

8) Failure surfaces and debugging shape#

Mellow#

  • Failures are relatively binary: caller not role-authorized, merkle proof invalid, hash mismatch, custom verifier false.
  • Good for deterministic pinning, less expressive for dynamic ranges unless custom verifier stack grows.

Roles#

  • Failures include granular status codes from condition engine (FunctionNotAllowed, BitmaskNotAllowed, allowance violations, etc.).
  • More expressive, but also more moving parts: decoder tree shape, operator semantics, and adapter correctness.
flowchart LR subgraph MellowFailures["Mellow failure class"] MF1["No caller role"] MF2["Proof invalid"] MF3["Hash mismatch"] MF4["Custom verifier reject"] end subgraph RolesFailures["Roles failure class"] RF1["No membership"] RF2["Target/function not allowed"] RF3["Execution options denied"] RF4["Condition operator violation"] RF5["Allowance exceeded"] RF6["Malformed unwrapper input"] end

9) Security tradeoff summary#

flowchart LR AX["Policy tradeoff"] --> E["High expressivity"] AX --> S["Low operational overhead"] AX --> C["High operational complexity"] E --> R1["Roles condition trees + allowances"] E --> R2["Mellow custom verifiers"] S --> M1["Mellow merkle extended"] S --> M2["Roles wildcard function"] C --> R1 C --> R2

Interpreting this:

  • Mellow Verifier is strongest when you want strict, pre-approved call shapes and minimal runtime interpretation.
  • Roles Modifier is strongest when you need expressive, runtime-checked policies and native quotas.

10) Practical selection criteria#

Choose Mellow-style verifier checks first when:

  • policy can be represented as compact allowlists or exact merkle-pinned calls
  • you value deterministic call pinning and simple verification logic
  • strategy execution already sits inside Mellow vault/subvault architecture

Choose Roles first when:

  • one role must support dynamic ranges and structured calldata predicates
  • you need native rate limits and allowance consumption
  • you rely on Safe modules and bundle-unwrapping workflows

Use custom logic in both systems when protocol semantics become too specific for generic selectors.

  • Mellow path: custom verifier contracts via CUSTOM_VERIFIER
  • Roles path: ICustomCondition or transaction unwrappers
flowchart TB Q1["Need exact immutable call pinning?"] -->|Yes| MellowPath["Prefer Mellow compact/merkle verifier"] Q1 -->|No| Q2["Need dynamic bounds and quotas?"] Q2 -->|Yes| RolesPath["Prefer Roles condition trees + allowances"] Q2 -->|No| Q3["Need Safe-native multi-call unwrapping?"] Q3 -->|Yes| RolesPath Q3 -->|No| Q4["Already in Mellow vault stack?"] Q4 -->|Yes| MellowPath Q4 -->|No| Hybrid["Evaluate both or hybrid boundaries"]

11) Final take#

For curator calldata checks only, the strongest concise framing is:

  • Mellow verifier is a hash/proof-first authorization system with optional custom verifier escape hatches.
  • Roles modifier is a typed-condition authorization VM with built-in quotas and adapter-aware bundle checks.

Neither is universally better. They optimize for different policy expression models and operational constraints.