Skip to content

feat: add MetaSwap flexible settlement enforcer - #204

Open
hanzel98 wants to merge 3 commits into
mainfrom
feat/metaswap-flexible-settlement-enforcer
Open

hanzel98 wants to merge 3 commits into
mainfrom
feat/metaswap-flexible-settlement-enforcer

Conversation

@hanzel98

@hanzel98 hanzel98 commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add MetaSwapFlexibleSettlementEnforcer: MetaSwap-router caveat for one native/ERC-20 swap (optional approve / reset-approve).
  • Embeds min-output (ERC-20 or native balance change), redeemer allowlist, optional timestamp, and a required order id in one caveat.
  • Exact ApprovalMode pins the batch shape; route aggregatorId / data stay redeemer-selected.

Combined behavior

Concern Behavior
Settlement Exact MetaSwap.swap ± approval legs (BATCH_DEFAULT_MODE)
Min output Balance increase ≥ tokenOutMin (tokenOut == address(0) → native)
Redeemer Packed allowlist (≥1)
Timestamp Optional; 0 disables a bound (non-inclusive, like TimestampEnforcer)
Id Required, non-zero. Bitmap (manager, delegator, id) blocks a replay and any sibling order that shares the id

Native input requires None; ERC-20 requires SkipApproval / Approve / ResetApprove. Failed fills roll back the id.

Terms

metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) |
tokenOut(20) | recipient(20) | tokenOutMin(32) |
timestampAfter(16) | timestampBefore(16) | id(32) | redeemers(20*N)
bytes memory terms = abi.encodePacked(
    metaSwap, tokenIn, tokenInAmount,
    uint8(MetaSwapFlexibleSettlementEnforcer.ApprovalMode.Approve),
    tokenOut, recipient, tokenOutMin,
    uint128(0), uint128(0), // no timestamp
    uint256(42),            // required order id
    redeemer
);

Approve spender / swap token words must be canonical ABI addresses. Swap calldata ≥ 196 bytes.

Security

Trust MetaSwap, adapters, and redeemer-chosen routes. Min output can be met by any balance increase. Residual allowance may remain. Input ≠ output.

Deployment

script/DeployCaveatEnforcers.s.sol · verify via script/verification/verify-enforcer-contracts.sh


Note

Medium Risk
New settlement path touches approvals, swaps, and balance-based min-output checks with unrestricted routes and known gaming vectors (any balance increase counts); extensive tests mitigate but delegators must understand trust assumptions.

Overview
Adds MetaSwapFlexibleSettlementEnforcer, a single caveat that authorizes one MetaSwap-router settlement in batch default mode while folding several policies into packed terms.

Signed terms pin the router, input amount, output recipient, minimum balance increase (ERC-20 or native when tokenOut == address(0)), a required redeemer allowlist, and an ApprovalMode that fixes batch shape: native-only swap, or ERC-20 swap with skip / approve / reset-approve legs. Swap validation checks target, value, selector, token word, and amount; route aggregatorId / data stay redeemer-chosen.

One-shot consumption uses a required non-zero id. An IdEnforcer-style bitmap per (manager, delegator) blocks a replay and any replacement order that shares the id. Optional timestamp bounds mirror TimestampEnforcer. afterHook compares recipient balance delta to tokenOutMin; failed redemptions revert atomically so the id bit rolls back.

Also wires CREATE2 deploy in DeployCaveatEnforcers.s.sol, documents terms/trust assumptions in CaveatEnforcers.md (plus minor doc table formatting), and adds broad Foundry coverage including end-to-end redemption through DelegationManager.

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

@hanzel98
hanzel98 requested a review from a team as a code owner September 2, 2026 14:47
@hanzel98
hanzel98 force-pushed the feat/metaswap-flexible-settlement-enforcer branch 5 times, most recently from ff660b3 to 7995fa6 Compare September 3, 2026 13:52
@hanzel98 hanzel98 self-assigned this Sep 3, 2026
@hanzel98
hanzel98 force-pushed the feat/metaswap-flexible-settlement-enforcer branch 2 times, most recently from 6ac8476 to 4049a6b Compare September 25, 2026 03:05
Authorize one open-route MetaSwap settlement with exact input constraints, signed approval flexibility, minimum output, and atomic one-shot consumption.
Fold RedeemerEnforcer, TimestampEnforcer, IdEnforcer, and ERC20/Native
balance-change semantics into MetaSwapFlexibleSettlementEnforcer so one
MetaSwap-router caveat can settle native or ERC-20 swaps (optional
approval) with an allowlisted redeemer. Timestamp bounds of zero disable
time checks; id zero keeps hash-based one-shot consumption while a
non-zero id uses the per-delegator bitmap for mutual exclusion.
@hanzel98
hanzel98 force-pushed the feat/metaswap-flexible-settlement-enforcer branch from e40716c to 1455eac Compare September 25, 2026 03:39
The id bitmap already rejects a replay and any sibling order that shares it, so the hash one-shot is unused.
@hanzel98

Copy link
Copy Markdown
Contributor Author
image

Separate roots is the existing way based on main, uses 2 root delegations one for approval and one for the swap call

@hanzel98

Copy link
Copy Markdown
Contributor Author

Gas

Estimated transaction gas: 21,000 + calldata + execution. Each row is the same swap. Measured cold (one approach per test).

Calldata-pinned dual-root is the stacked-enforcer baseline on DelegationManager: approval root (when needed) then swap root, redeemed in one redeemDelegations call. The flexible enforcer collapses that into one batch delegation.

Swap Calldata-pinned dual-root DelegationManager + flexible settlement enforcer Saved
Native ETH in, token out 379,436 180,314 199,122 (52.5%)
Token in, native ETH out 504,458 230,748 273,710 (54.3%)
Token in, token out 510,530 235,363 275,167 (53.9%)
Token in, token out, with reset 535,951 244,069 291,882 (54.5%)

Calldata-pinned dual-root — how the delegations are built

Two root delegations in one redemption (native input: swap root only). Approval uses id 2, swap uses id 1, so they do not burn the same IdEnforcer bit.

Approval root (ERC-20 in only; single approve, or batch [approve(0), approve(amount)] for reset):

Enforcer Purpose
ExactExecution / ExactExecutionBatch Pins the exact approval execution(s) (target, value, calldata)
IdEnforcer One-shot for this approval root (distinct id from the swap)
RedeemerEnforcer Only allowlisted redeemers may redeem

Swap root (always single MetaSwap.swap):

Enforcer Purpose
AllowedTargetsEnforcer Target must be MetaSwap
AllowedMethodsEnforcer Selector must be swap
ValueLteEnforcer (ERC-20 in) / NativeTokenTransferAmountEnforcer (native in) Cap msg.value (0 for ERC-20 in; order size for native in)
AllowedCalldataEnforcer Pins tokenFrom + amount at callData[36:100] (one enforcer, 64 bytes). Leaves aggregatorId / route data free for the redeemer
ERC20BalanceChangeEnforcer or NativeBalanceChangeEnforcer Min token-out (balance increase ≥ tokenOutMin)
TimestampEnforcer Order expiry window
IdEnforcer One-shot / mutual exclusion for the order id
RedeemerEnforcer Same redeemer allowlist as approval

This pins the same settlement fields the flexible enforcer encodes in terms (tokenIn / tokenInAmount, min out, redeemer, timestamp, id), while still paying for two roots and stacked caveat hooks.

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.

1 participant