You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: documents/CaveatEnforcers.md
+50Lines changed: 50 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -174,6 +174,56 @@ Note that in this scenario we have the same end recipient (treasury) and the sam
174
174
175
175
If you are delegating to an EOA in a delegation chain, the EOA cannot execute directly since it cannot redeem inner delegations. The EOA can become a deleGator by using EIP7702 or it can use an adapter contract to execute the delegation. An example for that is available in `./src/helpers/DelegationMetaSwapAdapter.sol`.
176
176
177
+
### ApprovalRevocationEnforcer
178
+
179
+
The `ApprovalRevocationEnforcer` lets a delegator grant a delegate the narrow authority to **clear an existing token approval** on the delegator's behalf, without granting any other power over the delegator's assets. It covers the three standard approval primitives:
The enforcer runs only in single call type and default execution mode, consumes no terms, and makes no assumption about the target contract. In `beforeHook` it:
188
+
189
+
1. Requires the execution to transfer zero native value and to carry calldata of exactly 68 bytes (4-byte selector + two 32-byte words).
190
+
2. Branches on the selector:
191
+
-`setApprovalForAll(address operator, bool approved)` — requires `approved == false` and `isApprovedForAll(delegator, operator) == true` on the target.
192
+
-`approve(address, uint256)` — shared by ERC-20 and ERC-721, disambiguated by the first parameter:
193
+
- First parameter is `address(0)` → treated as an ERC-721 per-token revocation; requires `getApproved(tokenId)` on the target to return a non-zero address.
194
+
- First parameter is non-zero → treated as an ERC-20 revocation; requires the second parameter (amount) to be zero and `allowance(delegator, spender) > 0` on the target.
195
+
3. Reverts on any other selector.
196
+
197
+
All three accepted calldatas structurally reduce permissions (amount `0`, spender `address(0)`, or `approved``false`). A delegate using this enforcer can therefore **never be granted new authority** over the delegator's assets — only existing approvals can be cleared.
198
+
199
+
#### Use Cases
200
+
201
+
-**Revocation bots / keepers**: Delegate to a third party that can proactively clean up stale or compromised approvals.
202
+
-**Post-incident remediation**: Issue a short-lived delegation to revoke a specific approval after a spender contract is found to be malicious.
203
+
-**User-facing "revoke all" flows**: Let a UI batch revocations on the user's behalf without asking for a new signature per clear.
204
+
205
+
#### Composition
206
+
207
+
The enforcer is not scoped to any particular token contract or spender. To restrict it further, compose it with existing enforcers:
208
+
209
+
-`AllowedTargetsEnforcer` — restrict revocation to specific token contracts.
210
+
-`AllowedCalldataEnforcer` / `ExactCalldataEnforcer` — pin the exact spender, operator, or tokenId.
211
+
212
+
#### Redelegation Caveat (Link-Local Semantics)
213
+
214
+
The `_delegator` argument passed to `beforeHook` is the delegator of the specific delegation that carries the caveat, **not** the root of a redelegation chain. The `DelegationManager` always executes the downstream `approve` / `setApprovalForAll` call against the root delegator's account. On a root-level delegation (chain length 1) the two are the same and the pre-check queries the account whose storage will actually be mutated — this is the intended usage.
215
+
216
+
On an intermediate (redelegation) link the two differ: the pre-check queries the intermediate delegator's approval state while the execution mutates the root delegator's storage. This is **never an authority escalation** (the structural constraints above still hold — the call can only reduce permissions), but the sanity guard becomes misaligned with the executed effect:
217
+
218
+
- If the intermediate delegator has no matching approval, the hook reverts even when the root does (the chain cannot be used, even though the revocation would have been valid for the root).
219
+
- If the intermediate delegator happens to have some approval, the hook passes and the execution clears the root's approval regardless of whether the root actually had one to clear.
220
+
221
+
If a redelegator needs a root-scoped guarantee (e.g. "Carol may only revoke one of Alice's specific approvals"), they should rely on structural caveats that compose cleanly across links, such as `AllowedTargetsEnforcer`, `AllowedCalldataEnforcer`, or `ExactCalldataEnforcer`. Placing `ApprovalRevocationEnforcer` on an intermediate link in the hope of validating the root's approval state does not achieve that.
222
+
223
+
#### Liveness vs. Race-Freedom
224
+
225
+
The "pre-existing approval" check is a liveness / sanity guard ensuring the call is not a no-op at the time the hook runs. It is not a race-free invariant: the delegator could independently clear the approval between the hook and the execution. In that case the execution is still safe — it simply becomes a no-op on the token contract.
226
+
177
227
## LogicalOrWrapperEnforcer Context Switching
178
228
179
229
The `LogicalOrWrapperEnforcer` enables logical OR functionality between groups of enforcers, allowing flexibility in delegation constraints. This enforcer is designed for a narrow set of use cases, and careful attention must be given when constructing caveats. The enforcer introduces an important architectural consideration: **context switching**.
0 commit comments