Skip to content

Commit 1bf6b79

Browse files
committed
docs(evm-wallet-experiment): add GATOR.md 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 Documents the enforcer mapping table and M.*/Gator overlap.
1 parent fe26f56 commit 1bf6b79

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

  • packages/evm-wallet-experiment/src/lib
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Gator + Endo Integration
2+
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.
7+
8+
## Conceptual model
9+
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
118+
119+
```
120+
Endo M.* patterns Gator enforcers
121+
┌────────────────────┐ ┌─────────────────────────┐
122+
│ │ │ │
123+
│ M.not() │ │ Stateful: │
124+
│ M.neq() │ │ ERC20Transfer │
125+
│ M.gt/gte/lt/ │ │ AmountEnforcer │
126+
│ lte() on args │ │ LimitedCalls │
127+
│ M.nat() │ │ NativeToken │
128+
│ M.splitRecord │ │ TransferAmount │
129+
│ M.splitArray │ │ │
130+
│ M.partial ┌────────────────────────┐ │
131+
│ M.record │ SHARED │ │
132+
│ M.array │ │ │
133+
│ │ Literal/eq pinning │ │
134+
│ │ AND (conjunction) │ │
135+
│ │ OR (disjunction) │ │
136+
│ │ Unconstrained │ │
137+
│ │ (any/string/scalar) │ │
138+
│ │ Temporal: │ │
139+
│ │ Timestamp │ │
140+
│ │ BlockNumber │ │
141+
│ └────────────────────────┘ │
142+
│ │ │ │
143+
└────────────────────┘ └──────────────────────┘
144+
145+
Endo-only: negation, Shared: equality, Gator-only: stateful
146+
range checks on args, logic operators, tracking, execution
147+
structural patterns, unconstrained, envelope, (target,
148+
dynamic ABI types temporal constraints selector, value)
149+
```
150+
151+
---
152+
153+
## What maps well
154+
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
171+
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.
176+
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.
184+
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+
---
194+
195+
## The AllowedCalldataEnforcer
196+
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:
199+
200+
```
201+
terms = [32-byte offset] ++ [expected bytes]
202+
```
203+
204+
For a function with a static ABI, every argument occupies a fixed 32-byte slot
205+
at a known offset from the start of calldata (after the 4-byte selector):
206+
207+
| Arg index | Offset |
208+
| --------- | ------- |
209+
| 0 | 4 |
210+
| 1 | 36 |
211+
| 2 | 68 |
212+
| n | 4 + 32n |
213+
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)