Skip to content

feat: add ApprovalRevocationEnforcer#177

Open
jeffsmale90 wants to merge 7 commits intomainfrom
feat/erc20-allowance-revocation-enforcer
Open

feat: add ApprovalRevocationEnforcer#177
jeffsmale90 wants to merge 7 commits intomainfrom
feat/erc20-allowance-revocation-enforcer

Conversation

@jeffsmale90
Copy link
Copy Markdown
Contributor

@jeffsmale90 jeffsmale90 commented Apr 22, 2026

What?

This enforcer grants the authority to revoke allowances granted by either:

  • ERC20 approve(spender,amount)
  • ERC721 approve(to,tokenId)
  • ERC721 setApprovalForAll(operator,approved)
  • Permit2 approve(token, spender, 0, 0)
  • Permit2 lockdown((address,address)[])
  • Permit2 invalidateNonces(token, spender, newNonce)

This covers approvals set for ERC20, ERC721, ERC1155 tokens, and permit2 approvals (both onchain, and offchain).

Why?

For ERC20, and ERC721 approvals, the enforcer now verifies that the target implements the expected token standard, by first invoking the standard-specific function to check the allowance being revoked. For Permit2 approvals, the execution must target the canonical Permit2 address.

Advanced Permissions currently has an erc20-token-revocation permission that grants the authority to revoke only ERC20 approvals. NFT approvals are also required.

By combining these revocations into a single enforcer, we get a number of benefits:

  • User must sign only a single permission to revoke all allowances across multiple token types
  • Reduced gas cost to invoke a revocation (single caveat, rather than composition of multiple caveats)

How?

The enforcer accepts terms of exactly 1 byte, interpreted as a bitmask of the following values:

  • Bit 0 (0x01) - ERC-20 approve(spender, 0) (spender non-zero, amount zero)
  • Bit 1 (0x02) - ERC-721 per-token approve(address(0), tokenId)
  • Bit 2 (0x04) - ERC-721 / ERC-1155 setApprovalForAll(operator, false)
  • Bit 3 (0x08) - Permit2 approve(token, spender, 0, 0) against the canonical Permit2 deployment
  • Bit 4 (0x10) - Permit2 lockdown((address,address)[]) against the canonical Permit2 deployment
  • Bit 5 (0x20) - Permit2 invalidateNonces(token, spender, newNonce) against the canonical Permit2 deployment

Indicating which revocation primitives the delegation authorizes. Terms must be non-zero, and the reserved upper bits must not be set.

The beforeHook only runs in single call type and default execution mode. It first performs general verification - no native value is sent (no additional value limiting caveat is required), and the calldata carries at least a 4-byte selector - then dispatches by selector.

Permit2 primitives are dispatched first, since each has its own unique selector and a target check against the canonical Permit2 deployment. approve(token, spender, uint160, uint48) additionally requires the calldata to be exactly 132 bytes with both the amount and expiration parameters zeroed. lockdown((address,address)[]) and invalidateNonces(address,address,uint48) have otherwise unconstrained calldata - the Permit2 contract itself structurally guarantees they can only zero per-pair allowance amounts or strictly increment nonces, never grant authority.

If the selector is not a Permit2 one, the calldata must be exactly 68 bytes (4-byte selector + two 32-byte words). setApprovalForAll and approve are then distinguished by selector. The two approve signatures share a selector and are distinguished by the spender / to (first) parameter — if it is the zero address, the call is treated as the ERC-721 approve(to, tokenId) form; otherwise it is treated as the ERC-20 approve(spender, amount) form. Any other selector is rejected.

The ERC-20, ERC-721, and setApprovalForAll branches then perform a check against the delegator's current approval state on the target, ensuring the revocation is to an existing approval - ensuring that the contract implementation is a valid target for the invocation. The Permit2 branches perform no such liveness pre-check; their structural calldata constraints already guarantee the call cannot grant authority, so an absent on-chain allowance is a harmless no-op (or, for invalidateNonces, reverts inside Permit2 itself).


Note

Medium Risk
Adds a new on-chain enforcer that constrains delegated calls by decoding calldata and querying token/Pemit2 state; mistakes could block legitimate revocations or allow unintended targets, especially around Permit2 and redelegation semantics.

Overview
Adds ApprovalRevocationEnforcer, a new caveat enforcer that only permits approval-revocation calls in single/default mode, gated by a 1-byte bitmask for ERC-20 approve(spender,0), ERC-721 approve(0,tokenId), ERC-721/1155 setApprovalForAll(_,false), and three canonical Permit2 revocation primitives.

Updates deployment and verification scripts to include the new enforcer, adds extensive unit/integration coverage (including redelegation edge cases), and documents expected behavior, composition guidance, and Permit2 trust/DoS considerations in CaveatEnforcers.md.

Reviewed by Cursor Bugbot for commit be5c72f. Bugbot is set up for automated code reviews on this repo. Configure here.

@jeffsmale90 jeffsmale90 requested a review from a team as a code owner April 22, 2026 03:02
@MoMannn MoMannn changed the title feat: add AllowanceRevocationEnforcer feat: add ApprovalRevocationEnforcer Apr 22, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 84896cd. Configure here.

Comment thread script/verification/verify-enforcer-contracts.sh
Copy link
Copy Markdown
Contributor Author

@jeffsmale90 jeffsmale90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

Minor comment regarding using the term permission to refer to the allowed revocation mechanism.

Comment thread src/enforcers/ApprovalRevocationEnforcer.sol Outdated
Comment thread src/enforcers/ApprovalRevocationEnforcer.sol Outdated
@MoMannn MoMannn self-requested a review April 28, 2026 06:59
Comment thread src/enforcers/ApprovalRevocationEnforcer.sol Outdated
Copy link
Copy Markdown
Contributor Author

@jeffsmale90 jeffsmale90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants