1- # Gator Enforcers and Endo Patterns
1+ # Gator + Endo Integration
22
3- This document maps the constraint surface of [ MetaMask Delegation Framework
4- ("Gator")] ( https://github.com/MetaMask/delegation-framework ) caveat enforcers
5- onto [ Endo ] ( https://github.com/endojs/endo ) ` M.* ` pattern matchers from
6- ` @endo/patterns ` , and scopes out what level of integration is achievable .
3+ This document describes how [ MetaMask Delegation Framework
4+ ("Gator")] ( https://github.com/MetaMask/delegation-framework ) is integrated with
5+ the ocap kernel. Gator constructs capabilities. Endo ` M.* ` patterns make them
6+ discoverable .
77
8- ## Overlap at a glance
8+ ## Conceptual model
99
10- For a contract with a completely static ABI:
10+ ```
11+ Delegation grant
12+ ┌──────────────────────────────────────────────────────┐
13+ │ delegation ← redeemable bytestring (signed, EIP-7702)
14+ │ caveatSpecs ← readable description of active caveats
15+ │ methodName ← which catalog operation this enables
16+ │ token? ← ERC-20 contract in play (if any)
17+ └──────────────────────────────────────────────────────┘
18+ │
19+ │ makeDelegationTwin()
20+ ▼
21+ Delegation twin (discoverable exo)
22+ ┌──────────────────────────────────────────────────────┐
23+ │ transfer / approve / call ← ocap capability methods
24+ │ getBalance? ← optional read method
25+ │ SpendTracker ← local mirror of on-chain state
26+ │ InterfaceGuard ← M.* patterns derived from caveats
27+ └──────────────────────────────────────────────────────┘
28+ │
29+ │ makeDiscoverableExo()
30+ ▼
31+ Discoverable capability
32+ (surfaced to agents via kernel capability discovery)
33+ ```
34+
35+ ### Delegation grants
36+
37+ A delegation grant is a ** serializable, describable** version of a delegation.
38+ It holds two things together:
39+
40+ - ** ` delegation ` ** — the redeemable bytestring: a fully-formed, signed
41+ delegation struct ready to pass to ` redeemDelegation ` on-chain. This is
42+ the authoritative bytes; everything else is derived from it.
43+
44+ - ** ` caveatSpecs ` ** — a structured, human-readable description of the caveats
45+ in effect. Unlike raw ` caveats ` (which are opaque encoded calldata passed to
46+ enforcer contracts), ` caveatSpecs ` name the constraint and its parameters in
47+ terms the application can reason about: `{ type: 'cumulativeSpend', token,
48+ max }` , ` { type: 'allowedCalldata', dataStart, value }`, etc.
49+
50+ Grants are what get stored and transmitted. They can be reconstructed into
51+ twins whenever a live capability is needed.
52+
53+ ### Delegation twins
54+
55+ A delegation twin is a ** local capability** that wraps a grant and gives it an
56+ ocap interface. The twin:
57+
58+ - Exposes the delegation's permitted operations as callable methods
59+ - Derives its interface guard from the grant's ` caveatSpecs ` , so a call that
60+ would fail on-chain (e.g., wrong recipient, over-budget) is rejected locally
61+ first with a descriptive error
62+ - Tracks stateful caveats locally — cumulative spend, value limits — as a
63+ ** latent mirror** of on-chain state
64+
65+ The local tracker is advisory, not authoritative. On-chain state is the truth.
66+ If spend is tracked externally (e.g., another redemption outside this twin), the
67+ local tracker will optimistically allow a call that the chain will reject. The
68+ twin's job is to provide fast pre-rejection and a structured capability
69+ interface, not to replace the on-chain enforcer.
70+
71+ ### M.\* patterns and discoverability
72+
73+ ` M.* ` interface guards serve two purposes:
74+
75+ 1 . ** Discoverability** — ` makeDiscoverableExo ` attaches the interface guard and
76+ method schema to the exo. The kernel's capability discovery mechanism reads
77+ these to surface the capability to agents, including what methods are
78+ available and what arguments they accept.
79+
80+ 2 . ** Pre-validation** — the guard can narrow the accepted argument shapes based
81+ on the active caveats. If an ` allowedCalldata ` caveat pins the first argument
82+ to a specific address, the corresponding guard uses that literal as the
83+ pattern, so a call with any other address is rejected before hitting the
84+ network.
85+
86+ ---
87+
88+ ## Caveat → guard mapping
89+
90+ The following table maps Gator caveat enforcers to the ` M.* ` patterns used in
91+ delegation twin interface guards.
92+
93+ ### Execution-envelope caveats
94+
95+ These constrain the execution itself (target, selector, value), not individual
96+ calldata arguments. They are represented in ` caveatSpecs ` and influence the
97+ twin's behavior but do not correspond to argument-level ` M.* ` patterns.
98+
99+ | Caveat enforcer | CaveatSpec type | Twin behavior |
100+ | ----------------------------------- | ------------------ | --------------------------------------------------- |
101+ | ` AllowedTargetsEnforcer ` | _ (structural)_ | Determines which contract the twin calls |
102+ | ` AllowedMethodsEnforcer ` | _ (structural)_ | Determines which function selector the twin uses |
103+ | ` ValueLteEnforcer ` | ` valueLte ` | Local pre-check: rejects calls where ` value > max ` |
104+ | ` ERC20TransferAmountEnforcer ` | ` cumulativeSpend ` | Local SpendTracker: rejects when cumulative ` > max ` |
105+ | ` NativeTokenTransferAmountEnforcer ` | _ (not yet mapped)_ | — |
106+ | ` LimitedCallsEnforcer ` | _ (not yet mapped)_ | — |
107+ | ` TimestampEnforcer ` | ` blockWindow ` | Stored in caveatSpecs; not yet locally enforced |
108+
109+ ### Calldata argument caveats → M.\* patterns
110+
111+ | CaveatSpec type / enforcer | M.\* pattern | Notes |
112+ | ---------------------------------------------- | --------------------------- | ----------------------------------------------------------------------- |
113+ | ` allowedCalldata ` at offset 4 (first arg) | Literal address value | Pins the first argument of transfer/approve to a specific address |
114+ | ` allowedCalldata ` at offset N (any static arg) | Literal value (ABI-encoded) | Any static ABI type (address, uint256, bool, bytes32) at a known offset |
115+ | _ (no calldata constraint)_ | ` M.string() ` / ` M.scalar() ` | Unconstrained argument |
116+
117+ ### Overlap at a glance
11118
12119```
13120 Endo M.* patterns Gator enforcers
@@ -39,33 +146,56 @@ For a contract with a completely static ABI:
39146 range checks on args, logic operators, tracking, execution
40147 structural patterns, unconstrained, envelope, (target,
41148 dynamic ABI types temporal constraints selector, value)
42- (feasibly)
43149```
44150
45- ## Background
151+ ---
46152
47- A ** delegation** in Gator authorizes a delegate to execute transactions on
48- behalf of a delegator, subject to ** caveats** . Each caveat is an on-chain
49- enforcer contract that validates some property of the execution (target,
50- calldata, value, etc.) before it proceeds.
153+ ## What maps well
51154
52- An ** interface guard** in Endo is a local (in-process) contract that validates
53- method calls on an exo object. ` M.* ` patterns describe the shape of arguments
54- and return values.
155+ For contracts with a ** completely static ABI** (all arguments are fixed-size
156+ types like address, uint256, bool, bytes32):
157+
158+ 1 . ** Literal pinning** : Fully supported via ` AllowedCalldataEnforcer ` . Each
159+ pinned argument is one caveat. Maps to a literal value as the ` M.* ` pattern.
160+
161+ 2 . ** Conjunction** : Naturally expressed as multiple caveats on the same
162+ delegation. ` M.and ` is implicit.
163+
164+ 3 . ** Disjunction** : Supported via ` LogicalOrWrapperEnforcer ` , but note that the
165+ ** redeemer** chooses which group to satisfy — all groups must represent
166+ equally acceptable outcomes.
167+
168+ 4 . ** Unconstrained args** : Omit the enforcer. Use ` M.string() ` or ` M.scalar() ` .
169+
170+ ## What does not map
55171
56- The two systems operate at different layers:
172+ 1 . ** Range checks on calldata args** : ` M.gt(n) ` , ` M.gte(n) ` , ` M.lt(n) ` ,
173+ ` M.lte(n) ` , ` M.nat() ` have no calldata-level enforcer. ` ValueLteEnforcer `
174+ only constrains the execution's ` value ` field (native token amount). A custom
175+ enforcer contract would be needed.
57176
58- - Gator enforcers: on-chain, per-execution, byte-level calldata validation
59- - Endo patterns: in-process, per-method-call, structured value validation
177+ 2 . ** Negation** : ` M.not(p) ` , ` M.neq(v) ` have no on-chain equivalent. Gator
178+ enforcers are allowlists, not denylists.
179+
180+ 3 . ** Dynamic ABI types** : ` string ` , ` bytes ` , arrays, and nested structs use ABI
181+ offset indirection. ` AllowedCalldataEnforcer ` is fragile for these — you'd
182+ need to pin the offset pointer, the length, and the data separately. Not
183+ recommended.
60184
61- The goal is to derive Endo interface guards from Gator caveat configurations so
62- that the local exo twin rejects calls that would inevitably fail on-chain,
63- giving callers fast, descriptive errors without paying gas.
185+ 4 . ** Stateful patterns** : ` M.* ` patterns are stateless. Stateful enforcers
186+ (` ERC20TransferAmountEnforcer ` , ` LimitedCallsEnforcer ` , etc.) maintain
187+ on-chain state across invocations. The twin's local trackers mirror this
188+ state but are not authoritative.
189+
190+ 5 . ** Structural patterns** : ` M.splitRecord ` , ` M.splitArray ` , ` M.partial ` operate
191+ on JS object/array structure that doesn't exist in flat ABI calldata.
192+
193+ ---
64194
65195## The AllowedCalldataEnforcer
66196
67- The key bridge between the two worlds is ` AllowedCalldataEnforcer ` . It validates
68- that a byte range of the execution calldata matches an expected value:
197+ The key bridge between the two systems is ` AllowedCalldataEnforcer ` . It
198+ validates that a byte range of the execution calldata matches an expected value:
69199
70200```
71201terms = [32-byte offset] ++ [expected bytes]
@@ -81,95 +211,7 @@ at a known offset from the start of calldata (after the 4-byte selector):
81211| 2 | 68 |
82212| n | 4 + 32n |
83213
84- This means you can independently constrain any argument by stacking multiple
85- ` allowedCalldata ` caveats with different offsets.
86-
87- ### Current integration
88-
89- ` makeDelegationTwin ` reads ` allowedCalldata ` entries from ` caveatSpecs ` and
90- narrows the exo interface guard accordingly. Currently this is used to pin
91- the first argument (recipient/spender address) of ` transfer ` /` approve ` to a
92- literal value.
93-
94- ## M.\* to Gator enforcer mapping
95-
96- ### Direct mappings (static ABI types)
97-
98- | M.\* pattern | Gator enforcer | Notes |
99- | ------------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
100- | ` "literal" ` (string/bigint/number passed directly as pattern) | ` AllowedCalldataEnforcer ` | Pin a 32-byte slot to the ABI encoding of the literal value. Works for address, uint256, bool, bytes32, and other static types. |
101- | ` M.string() ` | _ (no enforcer)_ | Accepts any string. No calldata constraint needed; this is the default/unconstrained case. |
102- | ` M.scalar() ` | _ (no enforcer)_ | Accepts any scalar (string, number, bigint, etc.). Unconstrained. |
103- | ` M.any() ` | _ (no enforcer)_ | Accepts anything. Unconstrained. |
104- | ` M.lte(n) ` | ` ValueLteEnforcer ` | ** Only for the ` value ` field of the execution envelope** , not for calldata args. There is no per-argument LTE enforcer. |
105- | ` M.gte(n) ` , ` M.gt(n) ` , ` M.lt(n) ` | ** No enforcer** | Gator has no general-purpose comparison enforcers for calldata arguments. |
106- | ` M.or(p1, p2, ...) ` | ` LogicalOrWrapperEnforcer ` | Groups of caveats with OR semantics. Each group is a conjunction; the redeemer picks which group to satisfy. See caveats below. |
107- | ` M.and(p1, p2, ...) ` | Multiple caveats on same delegation | Caveats are AND-composed by default: every enforcer must pass. |
108- | ` M.not(p) ` | ** No enforcer** | No negation primitive in Gator. |
109- | ` M.eq(v) ` | ` AllowedCalldataEnforcer ` | Same as literal pinning. |
110- | ` M.neq(v) ` | ** No enforcer** | No negation/inequality. |
111- | ` M.nat() ` | ** No enforcer** | Non-negative bigint. No range-check enforcer for calldata args. |
112- | ` M.boolean() ` | ` AllowedCalldataEnforcer ` (partially) | Could pin to ` 0 ` or ` 1 ` via two ` LogicalOrWrapper ` groups, but this is a degenerate use. In practice, leave unconstrained or pin to a specific bool. |
113- | ` M.bigint() ` | _ (no enforcer)_ | Type-level only; any uint256 passes. |
114- | ` M.number() ` | _ (no enforcer)_ | Type-level only. |
115- | ` M.record() ` / ` M.array() ` | ** Not applicable** | ABI calldata for dynamic types uses offset indirection. See limitations below. |
116-
117- ### Execution-envelope-level mappings
118-
119- These constrain the execution itself, not individual calldata arguments:
120-
121- | Constraint | Gator enforcer | M.\* equivalent |
122- | -------------------------- | ----------------------------------- | ------------------------------------------ |
123- | Allowed target contracts | ` AllowedTargetsEnforcer ` | (not an arg guard; structural) |
124- | Allowed function selectors | ` AllowedMethodsEnforcer ` | (not an arg guard; method-level) |
125- | Max native value per call | ` ValueLteEnforcer ` | ` M.lte(n) ` on the ` value ` field |
126- | Cumulative ERC-20 amount | ` ERC20TransferAmountEnforcer ` | (stateful; tracked on-chain) |
127- | Cumulative native amount | ` NativeTokenTransferAmountEnforcer ` | (stateful; tracked on-chain) |
128- | Exact calldata match | ` ExactCalldataEnforcer ` | Equivalent to pinning ALL args as literals |
129- | Exact execution match | ` ExactExecutionEnforcer ` | Pin target + value + all calldata |
130- | Call count limit | ` LimitedCallsEnforcer ` | (stateful; no M.\* equivalent) |
131- | Time window | ` TimestampEnforcer ` | (temporal; no M.\* equivalent) |
132-
133- ## What works well
134-
135- For a contract with a ** completely static ABI** (all arguments are fixed-size
136- types like address, uint256, bool, bytes32):
137-
138- 1 . ** Literal pinning** (` M.eq ` / literal patterns): Fully supported via
139- ` AllowedCalldataEnforcer ` . Each pinned argument is one caveat.
140-
141- 2 . ** Conjunction** (` M.and ` ): Naturally expressed as multiple caveats on the
142- same delegation.
143-
144- 3 . ** Disjunction** (` M.or ` ): Supported via ` LogicalOrWrapperEnforcer ` , but
145- with an important security caveat: the ** redeemer** chooses which group to
146- satisfy, so all groups must represent equally acceptable outcomes.
147-
148- 4 . ** Unconstrained args** (` M.string() ` , ` M.any() ` , ` M.scalar() ` ): Simply
149- omit the enforcer for that argument slot.
150-
151- ## What does NOT map
152-
153- 1 . ** Inequality / range checks on calldata args** : ` M.gt(n) ` , ` M.gte(n) ` ,
154- ` M.lt(n) ` , ` M.lte(n) ` , ` M.nat() ` have no calldata-level enforcer.
155- ` ValueLteEnforcer ` only constrains the execution's ` value ` field (native
156- token amount), not encoded function arguments. A custom enforcer contract
157- would be needed.
158-
159- 2 . ** Negation** : ` M.not(p) ` , ` M.neq(v) ` have no on-chain equivalent. Gator
160- enforcers are allowlists, not denylists.
161-
162- 3 . ** Dynamic ABI types** : ` string ` , ` bytes ` , arrays, and nested structs use
163- ABI offset indirection. The data lives at a variable position in calldata,
164- making ` AllowedCalldataEnforcer ` fragile to use (you'd need to pin the
165- offset pointer AND the data AND the length). Not recommended.
166-
167- 4 . ** Stateful patterns** : ` M.* ` patterns are stateless. Gator enforcers like
168- ` ERC20TransferAmountEnforcer ` , ` LimitedCallsEnforcer ` , and
169- ` NativeTokenTransferAmountEnforcer ` maintain on-chain state across
170- invocations. These have no M.\* equivalent and are handled separately
171- via ` CaveatSpec ` (e.g., ` cumulativeSpend ` drives the local ` SpendTracker ` ).
172-
173- 5 . ** Structural patterns** : ` M.splitRecord ` , ` M.splitArray ` , ` M.partial ` —
174- these operate on JS object/array structure that doesn't exist in flat ABI
175- calldata.
214+ This means independent arguments can each be constrained by stacking multiple
215+ ` allowedCalldata ` caveats with different offsets. In ` delegation-twin.ts ` ,
216+ ` allowedCalldata ` entries at offset 4 are read from ` caveatSpecs ` and used to
217+ narrow the first-argument pattern in the exo interface guard.
0 commit comments