MetaMask / MetaMask/snap-7715-permissions
[Feature]: Add `erc20-token-swap`, an outcome-defined permission type for protocol exits and bounded swaps
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 11
- Forks
- 9
- Avg merge
- 3d 13h
- Merged PRs (30d)
- 2
Description
Technical Details
Description
Add an ERC-7715 permission type that is defined by its outcome rather than by the shape of the call: the delegate may call an allow-listed contract function that converts at most X of ERC-20 token A into at least Y of ERC-20 token B, delivered to the granting account.
Motivating use case: incident response. A user pre-signs a narrow permission, a watcher service redeems it when an exploit is detected on the protocol holding the position, and the funds land back in the user's own account. This is the exit counterpart of token-approval-revocation (revoke.delegate).
None of the seven existing types express this, and there is no function-call permission. Building it as a raw ERC-7710 delegation is blocked for MetaMask-managed accounts (including EOAs upgraded through the smart account flow) by validateDelegation in MetaMask/core signature-controller, which only lets external origins sign delegations for internal accounts when the request carries a decodedPermission, i.e. when it went through ERC-7715 and decodes into a known permission type.
The feat/promised-outcomes branch (#88) sketched this direction: the wallet interprets a promised outcome, generates enforcement caveats, shows the assured outcome to the user, and returns the caveats in the permission context. This proposal lifts that idea into a first-class permission type. It also addresses the concern raised on MetaMask/smart-accounts-kit#218 that showing 4-byte selectors and hex calldata in the confirmation is undesirable: the user reads the outcome, not the call.
Technical Details
Request shape (permission.data):
{
"justification": "Exit your Pendle LP position to USDC if an exploit is detected.",
"spend": { "tokenAddress": "0xPositionToken", "maxAmount": "0x…" },
"receive": { "tokenAddress": "0xUSDC", "minAmount": "0x…" },
"via": { "targets": ["0xRouter"], "selectors": ["removeLiquiditySingleToken(address,address,uint256,…)"] }
}
Supported rules: expiry, redeemer. payee is rejected (the recipient is always the granting account).
Caveats, all existing Delegation Framework v1.3.0 enforcers, no new contracts:
| Enforcer | Terms |
|---|---|
| AllowedTargetsEnforcer | via.targets |
| AllowedMethodsEnforcer | via.selectors (4-byte hex or ABI signature, hashed by the snap) |
| ValueLteEnforcer | 0 |
| ERC20BalanceChangeEnforcer | increase, receive.tokenAddress, grantor, receive.minAmount |
| ERC20BalanceChangeEnforcer | decrease, spend.tokenAddress, grantor, spend.maxAmount |
| LimitedCallsEnforcer | 1 |
plus the shared Timestamp, Redeemer and Nonce caveats appended by the grant pipeline.
The balance-change terms are 73 packed bytes (flag, token, recipient, amount), so a wallet can render "delivers at least 36,427.61 USDC to your own account" from the bytes alone, with no protocol-specific decoding and no simulation.
Confirmation UI: "Swap up to" and "Receive at least" (adjustable when isAdjustmentAllowed), "Delivered to" the selected account, and read-only "Allowed contracts" / "Allowed functions" lists. Targets and selectors are never user-editable.
Validated on a HyperEVM fork against a live Pendle market: with no calldata pins, the balance caveats alone reject a redirected payout, a wrong-asset payout and an under-delivering exit, while the honest exit passes, for both a Hybrid account and a 7702-upgraded EOA.
Known limitations (documented in packages/gator-permissions-snap/docs/erc20TokenSwap.md): single-call execution mode only; other assets already approved to via.targets remain reachable through the allowed selectors; minAmount is a theft bound rather than a slippage bound; spend/receive tokens must differ; ERC-20 only in v1.
Companion changes needed for MetaMask to sign it for internal accounts:
@metamask/7715-permission-types(MetaMask/smart-accounts-kit): type,createErc20TokenSwapCaveats,makeErc20TokenSwapDecoderConfig, schema entry, andEnforcerContractName+=erc20BalanceChangeEnforcer,allowedMethodsEnforcer,limitedCallsEnforcer.@metamask/gator-permissions-controller(MetaMask/core): map those three enforcers intoEnforcerAddressesByName.
Acceptance Criteria
erc20-token-swapis accepted bywallet_requestExecutionPermissions, listed bypermissionsProvider_getPermissionOffersandgetSupportedPermissionswith rule typesexpiryandredeemer.- Malformed requests (zero amounts, same spend/receive token, empty/oversized/duplicate targets or selectors, invalid selector strings, a
recipientthat is not the granting account, apayeerule) are rejected before the confirmation opens. - The confirmation shows both amounts with token icons, the delivery account, and the read-only allow-lists; amounts are editable only when
isAdjustmentAllowedis true; expiry is always editable. - The granted delegation carries exactly the caveats above, with
recipientequal to the granting account even after the user switches accounts in the confirmation. - Existing permission types are unaffected.
- Translations: new
en.jsonkeys (Crowdin fills the rest).
Scenario: exit a position on exploit detection
- GIVEN a user holds a Pendle LP position and has approved the Pendle router
- WHEN the watcher requests
erc20-token-swapwithspend= the LP token,receive= USDC,via= the router andremoveLiquiditySingleToken, and aredeemerrule naming the watcher - THEN the user sees "Swap up to … LP", "Receive at least … USDC", "Delivered to" their account, and the router and function names
- AND on approval the returned context decodes to the six caveats plus expiry, redeemer and nonce
- AND a redemption that under-delivers, pays out elsewhere, or pays in the wrong asset reverts, while the honest exit succeeds once
References
feat/promised-outcomesbranch and draft PR #88- MetaMask/smart-accounts-kit#218 (function-call permission types and the calldata rendering concern), MetaMask/smart-accounts-kit#263
MetaMask/corepackages/signature-controller/src/utils/validation.ts(validateDelegation) andpackages/gator-permissions-controller/src/decodePermission- Delegation Framework
ERC20BalanceChangeEnforcer.sol
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/gator-permissions-snap/docs/erc20TokenSwap.md, then trace wallet_requestExecutionPermissions, permissionsProvider_getPermissionOffers, getSupportedPermissions, and decodePermission. Review packages/signature-controller/src/utils/validation.ts and the referenced gator-permissions-controller mapping before assessing the companion smart-accounts-kit changes. Done means the acceptance scenarios pass across validation, confirmation, caveats, redemption, and existing permission types.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- solidity, typescript
- Domain
- authorization, blockchain, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100