Skip to content

Commit 977cffe

Browse files
grypezclaude
andcommitted
docs(evm-wallet-experiment): rewrite GATOR.md with conceptual model
Explains the grant → twin → discoverable capability layering: - Delegation grants as serializable describable delegations (redeemable bytestring + readable caveat specs) - Delegation twins as local capabilities that mirror on-chain stateful caveats (latently; on-chain is authoritative) - M.* patterns as the mechanism for discoverability and pre-validation Reorganizes the enforcer mapping table around this model and preserves the existing M.*/Gator overlap reference content. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4c7a167 commit 977cffe

1 file changed

Lines changed: 158 additions & 116 deletions

File tree

  • packages/evm-wallet-experiment/src/lib
Lines changed: 158 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,120 @@
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
```
71201
terms = [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

Comments
 (0)