Skip to content

Commit 32a4aff

Browse files
committed
Add create / decode terms functions to @metamask/delegation-core for ApprovalRevocationEnforcer
1 parent 1098598 commit 32a4aff

5 files changed

Lines changed: 417 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/* eslint-disable no-bitwise */
2+
/**
3+
* ## ApprovalRevocationEnforcer
4+
*
5+
* Grants authority to revoke token approvals via:
6+
* - ERC-20 `approve(spender, 0)` (spender non-zero, amount zero)
7+
* - ERC-721 per-token `approve(address(0), tokenId)`
8+
* - ERC-721 / ERC-1155 `setApprovalForAll(operator, false)`
9+
*
10+
* Terms are encoded as exactly **one byte**, interpreted as a bitmask:
11+
* | Bit | Hex mask | Revocation primitive |
12+
* |-----|----------|-------------------|
13+
* | 0 | `0x01` | ERC-20 `approve(spender, 0)` |
14+
* | 1 | `0x02` | ERC-721 `approve(address(0), tokenId)` |
15+
* | 2 | `0x04` | `setApprovalForAll(operator, false)` (ERC-721 & ERC-1155) |
16+
* | 3 | `0x08` | Permit2 `approve(token, spender, 0, 0)` |
17+
* | 4 | `0x10` | Permit2 `lockdown((address,address)[])` |
18+
* | 5 | `0x20` | Permit2 `invalidateNonces(token, spender, newNonce)` | *
19+
*
20+
* Bits 6–7 must be zero on the wire.
21+
*/
22+
23+
import type { BytesLike } from '@metamask/utils';
24+
25+
import {
26+
assertHexByteExactLength,
27+
extractNumber,
28+
toHexString,
29+
} from '../internalUtils';
30+
import {
31+
bytesLikeToHex,
32+
defaultOptions,
33+
prepareResult,
34+
type EncodingOptions,
35+
type ResultValue,
36+
} from '../returns';
37+
import type { Hex } from '../types';
38+
39+
const BIT_ERC20_APPROVE_ZERO = 0x01;
40+
const BIT_ERC721_PER_TOKEN_CLEAR = 0x02;
41+
const BIT_SET_APPROVAL_FOR_ALL_REVOKE = 0x04;
42+
const BIT_PERMIT2_APPROVE_ZERO = 0x08;
43+
const BIT_PERMIT2_LOCKDOWN = 0x10;
44+
const BIT_PERMIT2_INVALIDATE_NONCES = 0x20;
45+
46+
const ALLOWED_APPROVAL_REVOCATION_MAX_MASK =
47+
BIT_ERC20_APPROVE_ZERO |
48+
BIT_ERC721_PER_TOKEN_CLEAR |
49+
BIT_SET_APPROVAL_FOR_ALL_REVOKE |
50+
BIT_PERMIT2_APPROVE_ZERO |
51+
BIT_PERMIT2_LOCKDOWN |
52+
BIT_PERMIT2_INVALIDATE_NONCES;
53+
54+
const NO_FLAGS_SET_ERROR =
55+
'Invalid ApprovalRevocation terms: at least one revocation primitive must be enabled';
56+
57+
/**
58+
* Human-readable selection of which revocation primitives the caveat allows.
59+
* Encode with {@link createApprovalRevocationTerms}; decode with {@link decodeApprovalRevocationTerms}.
60+
*/
61+
export type ApprovalRevocationTerms = {
62+
/** Allow revoking ERC-20 allowances via `approve(spender, 0)`. */
63+
erc20Approve: boolean;
64+
/** Allow clearing ERC-721 per-token approval via `approve(address(0), tokenId)`. */
65+
erc721Approve: boolean;
66+
/** Allow revoking operator access via `setApprovalForAll(operator, false)` (ERC-721 / ERC-1155). */
67+
erc721SetApprovalForAll: boolean;
68+
/** Allow revoking Permit2 approvals via `approve(token, spender, 0, 0)`. */
69+
permit2ApproveZero: boolean;
70+
/** Allow revoking Permit2 lockdown via `lockdown((address,address)[])`. */
71+
permit2Lockdown: boolean;
72+
/** Allow revoking Permit2 invalidateNonces via `invalidateNonces(token, spender, newNonce)`. */
73+
permit2InvalidateNonces: boolean;
74+
};
75+
76+
/**
77+
* Maps {@link ApprovalRevocationTerms} to the single-byte bitmask on the wire.
78+
*
79+
* @param terms - Selected Revocation primitives.
80+
* @returns Integer mask in `0x01`..`0x07`.
81+
* @throws Error if no flags are set.
82+
*/
83+
function termsToMask(terms: ApprovalRevocationTerms): number {
84+
let mask = 0;
85+
if (terms.erc20Approve) {
86+
mask |= BIT_ERC20_APPROVE_ZERO;
87+
}
88+
if (terms.erc721Approve) {
89+
mask |= BIT_ERC721_PER_TOKEN_CLEAR;
90+
}
91+
if (terms.erc721SetApprovalForAll) {
92+
mask |= BIT_SET_APPROVAL_FOR_ALL_REVOKE;
93+
}
94+
if (terms.permit2ApproveZero) {
95+
mask |= BIT_PERMIT2_APPROVE_ZERO;
96+
}
97+
if (terms.permit2Lockdown) {
98+
mask |= BIT_PERMIT2_LOCKDOWN;
99+
}
100+
if (terms.permit2InvalidateNonces) {
101+
mask |= BIT_PERMIT2_INVALIDATE_NONCES;
102+
}
103+
if (mask === 0) {
104+
throw new Error(NO_FLAGS_SET_ERROR);
105+
}
106+
return mask;
107+
}
108+
109+
/**
110+
* Parses a validated 1..7 bitmask into {@link ApprovalRevocationTerms}.
111+
*
112+
* @param mask - Integer byte value; only bits 0-2 may be set, and at least one must be set.
113+
* @returns Flag object for encoding/decoding.
114+
* @throws Error if reserved bits are set or the mask is zero.
115+
*/
116+
function maskToTerms(mask: number): ApprovalRevocationTerms {
117+
if (mask > ALLOWED_APPROVAL_REVOCATION_MAX_MASK) {
118+
throw new Error(
119+
'Invalid ApprovalRevocation terms: reserved bits must be zero (only bits 0-2 are defined)',
120+
);
121+
}
122+
123+
if (mask === 0) {
124+
throw new Error(NO_FLAGS_SET_ERROR);
125+
}
126+
127+
return {
128+
erc20Approve: (mask & BIT_ERC20_APPROVE_ZERO) !== 0,
129+
erc721Approve: (mask & BIT_ERC721_PER_TOKEN_CLEAR) !== 0,
130+
erc721SetApprovalForAll: (mask & BIT_SET_APPROVAL_FOR_ALL_REVOKE) !== 0,
131+
permit2ApproveZero: (mask & BIT_PERMIT2_APPROVE_ZERO) !== 0,
132+
permit2Lockdown: (mask & BIT_PERMIT2_LOCKDOWN) !== 0,
133+
permit2InvalidateNonces: (mask & BIT_PERMIT2_INVALIDATE_NONCES) !== 0,
134+
};
135+
}
136+
137+
/**
138+
* Creates terms for an ApprovalRevocation caveat.
139+
*
140+
* @param terms - Which Revocation primitives are permitted. At least one flag must be `true`.
141+
* @param encodingOptions - The encoding options for the result.
142+
* @returns Encoded terms (one byte).
143+
* @throws Error if no Revocation primitive is enabled.
144+
*/
145+
export function createApprovalRevocationTerms(
146+
terms: ApprovalRevocationTerms,
147+
encodingOptions?: EncodingOptions<'hex'>,
148+
): Hex;
149+
export function createApprovalRevocationTerms(
150+
terms: ApprovalRevocationTerms,
151+
encodingOptions: EncodingOptions<'bytes'>,
152+
): Uint8Array;
153+
/**
154+
* Creates terms for an ApprovalRevocation caveat.
155+
*
156+
* @param terms - Which Revocation primitives are permitted. At least one flag must be `true`.
157+
* @param encodingOptions - The encoding options for the result.
158+
* @returns Encoded terms (one byte).
159+
* @throws Error if no Revocation primitive is enabled.
160+
*/
161+
export function createApprovalRevocationTerms(
162+
terms: ApprovalRevocationTerms,
163+
encodingOptions: EncodingOptions<ResultValue> = defaultOptions,
164+
): Hex | Uint8Array {
165+
const mask = termsToMask(terms);
166+
const hexValue = `0x${toHexString({ value: mask, size: 1 })}`;
167+
return prepareResult(hexValue, encodingOptions);
168+
}
169+
170+
/**
171+
* Decodes terms for an ApprovalRevocation caveat from encoded data.
172+
*
173+
* @param terms - The encoded terms as a hex string or Uint8Array (exactly one byte).
174+
* @returns The decoded {@link ApprovalRevocationTerms}.
175+
*/
176+
export function decodeApprovalRevocationTerms(
177+
terms: BytesLike,
178+
): ApprovalRevocationTerms {
179+
const hexTerms = bytesLikeToHex(terms);
180+
assertHexByteExactLength(
181+
hexTerms,
182+
1,
183+
'Invalid ApprovalRevocation terms: must be exactly 1 byte',
184+
);
185+
const mask = extractNumber(hexTerms, 0, 1);
186+
return maskToTerms(mask);
187+
}

packages/delegation-core/src/caveats/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ export {
7777
createAllowedTargetsTerms,
7878
decodeAllowedTargetsTerms,
7979
} from './allowedTargets';
80+
export {
81+
createApprovalRevocationTerms,
82+
decodeApprovalRevocationTerms,
83+
} from './approvalRevocationEnforcer';
84+
export type { ApprovalRevocationTerms } from './approvalRevocationEnforcer';
8085
export {
8186
createArgsEqualityCheckTerms,
8287
decodeArgsEqualityCheckTerms,

packages/delegation-core/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export type {
66

77
export { BalanceChangeType } from './caveats/types';
88

9+
export type { ApprovalRevocationTerms } from './caveats';
10+
911
export {
1012
createValueLteTerms,
1113
decodeValueLteTerms,
@@ -51,6 +53,8 @@ export {
5153
decodeAllowedMethodsTerms,
5254
createAllowedTargetsTerms,
5355
decodeAllowedTargetsTerms,
56+
createApprovalRevocationTerms,
57+
decodeApprovalRevocationTerms,
5458
createArgsEqualityCheckTerms,
5559
decodeArgsEqualityCheckTerms,
5660
createBlockNumberTerms,

0 commit comments

Comments
 (0)