Skip to content

feat: add one-shot MetaSwap limit order enforcer - #203

Closed
hanzel98 wants to merge 1 commit into
feat/metaswap-batch-calldata-enforcerfrom
feat/metaswap-integrated-limit-order-enforcer
Closed

feat: add one-shot MetaSwap limit order enforcer#203
hanzel98 wants to merge 1 commit into
feat/metaswap-batch-calldata-enforcerfrom
feat/metaswap-integrated-limit-order-enforcer

Conversation

@hanzel98

@hanzel98 hanzel98 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add MetaSwapOneShotLimitOrderEnforcer, a purpose-specific caveat that combines MetaSwap batch validation, one-shot consumption, and minimum-output enforcement.
  • Allow a signed ERC-20 approval policy to authorize swap-only, approve(amount), and/or approve(0) + approve(amount) fills.
  • Integrate deployment into the shared caveat-enforcer script and document usage, state semantics, and trust assumptions.

This is stacked on #201 so that PR and its branch remain unchanged.

Behavior

One signed delegation authorizes exactly one successful direct BATCH_DEFAULT_MODE order:

Native:       [MetaSwap.swap{ value: tokenInAmount }(...)]
ERC-20 skip:  [MetaSwap.swap(...)]
ERC-20:       [token.approve(metaSwap, tokenInAmount), MetaSwap.swap(...)]
ERC-20 reset: [token.approve(metaSwap, 0), token.approve(metaSwap, tokenInAmount), MetaSwap.swap(...)]

Terms bind the MetaSwap target, input token and amount, signed approval policy, output token, recipient, and minimum net balance increase:

bytes memory terms = abi.encodePacked(
    metaSwap,
    tokenIn,
    tokenInAmount,
    uint8(0x01 | 0x02 | 0x04),
    tokenOut,
    recipient,
    tokenOutMin
);

The batch length chooses one of the signed policy bits; unsigned caveat args are not used. Native input uses policy 0.

One-slot lifecycle

A single mapping slot keyed by (DelegationManager, delegationHash) stores:

0                 unused
balanceBefore + 1 executing / balance cached
type(uint256).max consumed

This replaces the independent call-count, lock, and balance-cache mappings used by LimitedCallsEnforcer(1) plus a balance-change enforcer. A successful order is permanently consumed. Any revert, including insufficient output, atomically restores the unused state and permits retry.

A prototype benchmark measured approximately 29-36% lower gross estimated redemption gas than composing MetaSwapBatchCalldataEnforcer + LimitedCallsEnforcer(1) + ERC20BalanceChangeEnforcer; the final OrderConsumed event adds normal event cost in exchange for indexable successful fills.

Security

aggregatorId and route data remain redeemer-controlled. The delegator trusts the configured MetaSwap contract, its adapters, and the delegate-provided route. The enforcer fixes the direct input approval and MetaSwap input fields and guarantees the signed minimum output balance increase, but it cannot prevent arbitrary route side effects or protect unrelated assets already approved to MetaSwap or its adapters.

A residual input allowance may remain if MetaSwap spends less than the approved amount. Unrelated transfers or token rebases during execution may satisfy the balance increase, and malicious/non-standard output tokens may report misleading balances. Input and output tokens must differ.

Deployment and verification

Deploy through the existing shared script:

forge script script/DeployCaveatEnforcers.s.sol \
  --rpc-url <rpc_url> --private-key $PRIVATE_KEY --broadcast

After recording deployed addresses, use the existing script/verification/verify-enforcer-contracts.sh flow. No dedicated script or environment variable is introduced.

Test plan

  • Native input and ERC-20 input
  • ERC-20 output and native output
  • Existing allowance, one approval, and reset approval
  • Signed approval-policy combinations and rejected unsigned shapes
  • Exact targets, values, selectors, approval spender/amounts, swap token/amount, modes, terms, and batch ordering
  • Flexible aggregatorId and route data
  • One-shot consumption, execution locking, event emission, and storage isolation
  • Insufficient-output atomic rollback and retry
  • Sentinel collision edge cases
  • Canonical DelegationManager redemption
  • 100% production line, statement, branch, and function coverage
  • 1,088 non-fork regression tests passing
  • Two fork suites require LINEA_RPC_URL and ARBITRUM_RPC_URL

Note

Medium Risk
New on-chain swap/limit-order enforcement handles approvals and balance deltas; delegators still trust MetaSwap routes and face known balance-enforcer caveats (unrelated transfers, non-standard tokens, residual allowance).

Overview
Adds MetaSwapOneShotLimitOrderEnforcer, a single caveat that replaces composing MetaSwapBatchCalldataEnforcer, LimitedCallsEnforcer(1), and a balance-change enforcer for one-shot MetaSwap limit orders.

Signed 145-byte packed terms bind MetaSwap, exact input, an ERC-20 approval-policy bitmask (skip allowance / approve(amount) / reset-then-approve), output token, recipient, and tokenOutMin. Redemptions must be batch default mode with 1–3 executions matching the signed policy; route aggregatorId and data stay unconstrained.

Lifecycle uses one orderStates slot per (DelegationManager, delegationHash): cache pre-output balance and lock in beforeHook, enforce minimum net increase and mark consumed in afterHook (with OrderConsumed). Failed fills revert the tx so the order stays retryable; success is permanent.

Also wires deployment in DeployCaveatEnforcers.s.sol, documents behavior and trust assumptions in CaveatEnforcers.md, and adds broad Foundry tests (shapes, validation, redemption, retry, double-spend).

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

Combine exact swap authorization, signed approval flexibility, minimum output, and one-shot consumption to remove redundant caveat calls and storage writes.
@hanzel98
hanzel98 requested a review from a team as a code owner September 1, 2026 19:29
@hanzel98 hanzel98 closed this Sep 2, 2026
@hanzel98
hanzel98 deleted the feat/metaswap-integrated-limit-order-enforcer branch September 2, 2026 14:46
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