|
| 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