Skip to content

Commit 84896cd

Browse files
committed
add docs and deployment script
1 parent 90b5342 commit 84896cd

3 files changed

Lines changed: 59 additions & 2 deletions

File tree

documents/CaveatEnforcers.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,56 @@ Note that in this scenario we have the same end recipient (treasury) and the sam
174174

175175
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`.
176176

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:
180+
181+
- ERC-20 `approve(spender, 0)`
182+
- ERC-721 per-token `approve(address(0), tokenId)`
183+
- ERC-721 / ERC-1155 `setApprovalForAll(operator, false)` (both standards share the selector)
184+
185+
#### How It Works
186+
187+
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+
177227
## LogicalOrWrapperEnforcer Context Switching
178228

179229
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**.

script/DeployCaveatEnforcers.s.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ import { ValueLteEnforcer } from "../src/enforcers/ValueLteEnforcer.sol";
4141
import { ERC20MultiOperationIncreaseBalanceEnforcer } from "../src/enforcers/ERC20MultiOperationIncreaseBalanceEnforcer.sol";
4242
import { ERC721MultiOperationIncreaseBalanceEnforcer } from "../src/enforcers/ERC721MultiOperationIncreaseBalanceEnforcer.sol";
4343
import { ERC1155MultiOperationIncreaseBalanceEnforcer } from "../src/enforcers/ERC1155MultiOperationIncreaseBalanceEnforcer.sol";
44-
import { NativeTokenMultiOperationIncreaseBalanceEnforcer } from
45-
"../src/enforcers/NativeTokenMultiOperationIncreaseBalanceEnforcer.sol";
44+
import {
45+
NativeTokenMultiOperationIncreaseBalanceEnforcer
46+
} from "../src/enforcers/NativeTokenMultiOperationIncreaseBalanceEnforcer.sol";
47+
import { ApprovalRevocationEnforcer } from "../src/enforcers/ApprovalRevocationEnforcer.sol";
4648

4749
/**
4850
* @title DeployCaveatEnforcers
@@ -183,6 +185,9 @@ contract DeployCaveatEnforcers is Script {
183185
deployedAddress = address(new NativeTokenMultiOperationIncreaseBalanceEnforcer{ salt: salt }());
184186
console2.log("NativeTokenMultiOperationIncreaseBalanceEnforcer: %s", deployedAddress);
185187

188+
deployedAddress = address(new ApprovalRevocationEnforcer{ salt: salt }());
189+
console2.log("ApprovalRevocationEnforcer: %s", deployedAddress);
190+
186191
vm.stopBroadcast();
187192
}
188193
}

script/verification/verify-enforcer-contracts.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ ENFORCERS=(
5757
"ERC721MultiOperationIncreaseBalanceEnforcer"
5858
"ERC1155MultiOperationIncreaseBalanceEnforcer"
5959
"NativeTokenMultiOperationIncreaseBalanceEnforcer"
60+
"ApprovalRevocationEnforcer"
6061
)
6162

6263
ADDRESSES=(
@@ -94,6 +95,7 @@ ADDRESSES=(
9495
"0x44877cDAFC0d529ab144bb6B0e202eE377C90229"
9596
"0x9eB86bbdaA71D4D8d5Fb1B8A9457F04D3344797b"
9697
"0xaD551E9b971C1b0c02c577bFfCFAA20b81777276"
98+
"0x0000000000000000000000000000000000000000"
9799
)
98100

99101
###############################################################################

0 commit comments

Comments
 (0)