Skip to content

Commit 42376f0

Browse files
committed
docs(evm-wallet-experiment): Add some gator integration docs
1 parent f8ae223 commit 42376f0

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

  • packages/evm-wallet-experiment/src/lib
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Gator Enforcers and Endo Patterns
2+
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.
7+
8+
## Overlap at a glance
9+
10+
For a contract with a completely static ABI:
11+
12+
```
13+
Endo M.* patterns Gator enforcers
14+
┌────────────────────┐ ┌─────────────────────────┐
15+
│ │ │ │
16+
│ M.not() │ │ Stateful: │
17+
│ M.neq() │ │ ERC20Transfer │
18+
│ M.gt/gte/lt/ │ │ AmountEnforcer │
19+
│ lte() on args │ │ LimitedCalls │
20+
│ M.nat() │ │ NativeToken │
21+
│ M.splitRecord │ │ TransferAmount │
22+
│ M.splitArray │ │ │
23+
│ M.partial ┌────────────────────────┐ │
24+
│ M.record │ SHARED │ │
25+
│ M.array │ │ │
26+
│ │ Literal/eq pinning │ │
27+
│ │ AND (conjunction) │ │
28+
│ │ OR (disjunction) │ │
29+
│ │ Unconstrained │ │
30+
│ │ (any/string/scalar) │ │
31+
│ │ Temporal: │ │
32+
│ │ Timestamp │ │
33+
│ │ BlockNumber │ │
34+
│ └────────────────────────┘ │
35+
│ │ │ │
36+
└────────────────────┘ └──────────────────────┘
37+
38+
Endo-only: negation, Shared: equality, Gator-only: stateful
39+
range checks on args, logic operators, tracking, execution
40+
structural patterns, unconstrained, envelope, (target,
41+
dynamic ABI types temporal constraints selector, value)
42+
(feasibly)
43+
```
44+
45+
## Background
46+
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.
51+
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.
55+
56+
The two systems operate at different layers:
57+
58+
- Gator enforcers: on-chain, per-execution, byte-level calldata validation
59+
- Endo patterns: in-process, per-method-call, structured value validation
60+
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.
64+
65+
## The AllowedCalldataEnforcer
66+
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:
69+
70+
```
71+
terms = [32-byte offset] ++ [expected bytes]
72+
```
73+
74+
For a function with a static ABI, every argument occupies a fixed 32-byte slot
75+
at a known offset from the start of calldata (after the 4-byte selector):
76+
77+
| Arg index | Offset |
78+
| --------- | ------- |
79+
| 0 | 4 |
80+
| 1 | 36 |
81+
| 2 | 68 |
82+
| n | 4 + 32n |
83+
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.

0 commit comments

Comments
 (0)