diff --git a/.env.example b/.env.example index 343258aa..d59a1d6a 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ PRIVATE_KEY= SALT=GATOR DELEGATION_MANAGER_ADDRESS= +META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS= ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032 MULTISIG_DELEGATOR_IMPLEMENTATION_ADDRESS= META_SWAP_ADAPTER_OWNER_ADDRESS= diff --git a/README.md b/README.md index b8f1fede..f43daf57 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ ### Getting Started 1. **Fork the repository**: - - Click the "Fork" button at the top right of the repository page. 2. **Clone your fork**: @@ -66,6 +65,12 @@ The Delegation Manager includes the logic for validating and executing Delegatio [Read more on "Delegation Manager" ->](/documents/DelegationManager.md) +### MetaSwap Order Delegation Manager + +One manager for two flows: an exact batch, and a MetaSwap limit order whose route is chosen at redemption inside signed bounds. + +[Read more on "MetaSwap Order Delegation Manager" ->](/documents/MetaSwapSpecializedDelegationManagers.md) + ### Caveat Enforcers Caveats are used to add restrictions and rules for Delegations. By default, a Delegation allows the delegate to make **any** onchain action so caveats are strongly recommended. They are managed by Caveat Enforcer contracts. diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md new file mode 100644 index 00000000..405aa4c2 --- /dev/null +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -0,0 +1,41 @@ +# MetaSwap Order Delegation Manager + +One delegation manager for two MetaSwap flows. It redeems a single root delegation, in batch mode, with one caveat whose enforcer is the manager itself. There is no redelegation chain and no external caveat hook. + +`executeFromExecutor` stays on the EIP-7702 account. An account that uses this manager cannot also redeem through `DelegationManager`. + +## Exact batch + +The user signs the hash of a batch. The submitter must send that same batch. Expiry stays off-chain. + +```text +terms = intent(1) | executionHash(32) +executionCallDatas[0] = ExecutionLib.encodeBatch(executions) +``` + +`executionHash` is `keccak256(executionCallDatas[0])`. The batch is executed directly. `delegate` is the redeemer, or `ANY_DELEGATE`. `disabledDelegations` is the one-shot. + +## Limit order + +The user signs the settlement bounds. The redeemer supplies the batch and chooses the route. The manager checks the approval and swap shape, not the aggregator id or the route data, then requires `tokenOutMin` to arrive at the signed recipient. + +```text +terms = intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) + | tokenOut(20) | recipient(20) | tokenOutMin(32) + | timestampAfter(16) | timestampBefore(16) | id(32) | redeemers(20*N) +``` + +- Redeemers are required (`N >= 1`). `address(0)` is a normal allowlist entry for Sentinel pre-sign simulation. A multi-signer order sets `delegate` to `ANY_DELEGATE` and lists the stations. +- Timestamp bounds are optional. `0` disables that bound. A set bound is exclusive (`timestamp > after` and `timestamp < before`). +- `id == 0` skips the bitmap. The delegation hash is still one-shot. `id != 0` also burns that id for the delegator, so a sibling order that shares it cannot fill. `disableDelegation` does not burn the id. + +Approval shapes are native swap, skip approval, approve then swap, and reset-approve then swap. + +Signatures try ECDSA first. If that misses, an account with no code reverts. An account with code falls back to ERC-1271. `disabledDelegations` is both cancel and one-shot consumption. A failed execution or insufficient output reverts atomically. + +## Limitations + +- No delegation chains, multiple caveats, multiple batches, try mode, or generic enforcers. +- No on-chain expiry for an exact batch. A limit order can set a timestamp window. +- No `enableDelegation` and no pause. +- A listed redeemer can choose any route that still pays the signed minimum to the signed recipient. diff --git a/script/DeployMetaSwapOrderDelegationManager.s.sol b/script/DeployMetaSwapOrderDelegationManager.s.sol new file mode 100644 index 00000000..bd3039ed --- /dev/null +++ b/script/DeployMetaSwapOrderDelegationManager.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; + +import { MetaSwapOrderDelegationManager } from "../src/MetaSwapOrderDelegationManager.sol"; + +/** + * @title DeployMetaSwapOrderDelegationManager.s.sol + * @notice Deploys MetaSwapOrderDelegationManager. + * @dev EIP-7702 accounts that redeem through this manager must use it as their delegation manager. + * @dev run the script with: + * forge script script/DeployMetaSwapOrderDelegationManager.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast + */ +contract DeployMetaSwapOrderDelegationManager is Script { + bytes32 salt; + address deployer; + + function setUp() public { + salt = bytes32(abi.encodePacked(vm.envString("SALT"))); + deployer = msg.sender; + + console2.log("~~~"); + console2.log("Deployer: %s", deployer); + console2.log("Salt:"); + console2.logBytes32(salt); + } + + function run() public { + console2.log("~~~"); + vm.startBroadcast(); + + address deployedAddress = address(new MetaSwapOrderDelegationManager{ salt: salt }()); + console2.log("MetaSwapOrderDelegationManager: %s", deployedAddress); + + vm.stopBroadcast(); + } +} diff --git a/script/verification/VerificationInstructions.md b/script/verification/VerificationInstructions.md index 1adc138c..24c180b1 100644 --- a/script/verification/VerificationInstructions.md +++ b/script/verification/VerificationInstructions.md @@ -62,6 +62,17 @@ Verifies an array of enforcer contracts. ./verify-enforcer-contracts.sh ``` +#### `verify-metaswap-order-delegation-manager.sh` + +Verifies `MetaSwapOrderDelegationManager`. The constructor takes no arguments. +Requires `META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS`. + +**Usage:** + +```bash +./verify-metaswap-order-delegation-manager.sh +``` + ## Notes - Ensure the `.env` file is correctly configured and contains all necessary API keys. diff --git a/script/verification/verify-metaswap-order-delegation-manager.sh b/script/verification/verify-metaswap-order-delegation-manager.sh new file mode 100755 index 00000000..4bfb487c --- /dev/null +++ b/script/verification/verify-metaswap-order-delegation-manager.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# verify-metaswap-order-delegation-manager.sh +# +# Usage: +# ./verify-metaswap-order-delegation-manager.sh +# +# Verifies MetaSwapOrderDelegationManager across configured chains. +# The constructor takes no arguments. +# Requires in .env: +# META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS + +set -e + +# Load environment variables +set -o allexport +source ../../.env +set +o allexport + +# Load shared logic +source ./verify-utils.sh + +########################################## +# Helper functions for contract configuration +########################################## + +# Function to add a contract to verify +add_contract() { + local name="$1" + local path="$2" + local address="$3" + local constructor_args="$4" + local lib_string="$5" + + # Add to contracts array + CONTRACTS+=("$name:$path:$address:$constructor_args:$lib_string") +} + +########################################## +# Contract Configurations +########################################## + +# Initialize empty array +declare -a CONTRACTS + +# MetaSwapOrderDelegationManager +add_contract \ + "MetaSwapOrderDelegationManager" \ + "src/MetaSwapOrderDelegationManager.sol" \ + "${META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS}" \ + "" \ + "" + +########################################## +# Process Contracts +########################################## + +# Process each contract +for contract in "${CONTRACTS[@]}"; do + # Split the configuration string + IFS=':' read -r name path address constructor_args lib_string <<< "$contract" + + echo "=============================================" + echo "Verifying contract: $name" + echo "=============================================" + + # Call the shared function + verify_across_chains \ + "$path" \ + "$name" \ + "$address" \ + "$constructor_args" \ + "$lib_string" + + echo "=============================================" + echo "Completed verification for: $name" + echo "=============================================" + echo +done diff --git a/src/MetaSwapDelegationManagerBase.sol b/src/MetaSwapDelegationManagerBase.sol new file mode 100644 index 00000000..c527015a --- /dev/null +++ b/src/MetaSwapDelegationManagerBase.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +import { EncoderLib } from "./libraries/EncoderLib.sol"; +import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; +import { DELEGATION_TYPEHASH, CAVEAT_TYPEHASH } from "./utils/Constants.sol"; +import { Caveat, Delegation, ModeCode } from "./utils/Types.sol"; + +/** + * @title MetaSwapDelegationManagerBase + * @notice Cheap one-shot redeem shell for purpose-specific MetaSwap managers. + * @dev Supports exactly one root delegation containing one manager-enforced caveat. + * No redelegation chains: `delegate` is the intended redeemer (or `ANY_DELEGATE`) and + * `authority` is `ROOT_AUTHORITY`. Redemption is `SIMPLE_BATCH_MODE` only. + * Settlement-specific decoding and min-output checks live in subclasses. + */ +abstract contract MetaSwapDelegationManagerBase is EIP712 { + string public constant DOMAIN_VERSION = "1"; + bytes32 public constant ROOT_AUTHORITY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + address public constant ANY_DELEGATE = address(0xa11); + /// @dev Equivalent to `ModeLib.encodeSimpleBatch()` (batch calltype, default exec). + ModeCode public constant SIMPLE_BATCH_MODE = ModeCode.wrap(0x0100000000000000000000000000000000000000000000000000000000000000); + + /// @notice Records delegations that were cancelled or successfully consumed. + mapping(bytes32 delegationHash => bool isUnavailable) public disabledDelegations; + + event DisabledDelegation( + bytes32 indexed delegationHash, address indexed delegator, address indexed delegate, Delegation delegation + ); + /// @dev `intent` is the first terms byte (`Intent` on MetaSwapOrderDelegationManager). + event RedeemedDelegation(address indexed rootDelegator, address indexed redeemer, bytes32 indexed delegationHash, uint8 intent); + + error AlreadyDisabled(); + error BatchDataLengthMismatch(); + error CannotUseADisabledDelegation(); + error InsufficientOutput(); + error InvalidAuthority(); + error InvalidCaveat(); + error InvalidDelegate(); + error InvalidDelegator(); + error InvalidEOASignature(); + error InvalidERC1271Signature(); + error InvalidMode(); + error InvalidPermissionContext(); + error InvalidTerms(); + + constructor(string memory name_) EIP712(name_, DOMAIN_VERSION) { } + + /** + * @notice Cancels a settlement delegation. + * @dev Successful settlements use the same state, so consumed delegations cannot be re-enabled. + * @param delegation_ Delegation to cancel. + */ + function disableDelegation(Delegation calldata delegation_) external { + if (delegation_.delegator != msg.sender) revert InvalidDelegator(); + + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + if (disabledDelegations[delegationHash_]) revert AlreadyDisabled(); + + disabledDelegations[delegationHash_] = true; + emit DisabledDelegation(delegationHash_, delegation_.delegator, delegation_.delegate, delegation_); + } + + /** + * @notice Redeems one specialized MetaSwap delegation. + * @dev These intents are leaf delegations: `delegate` is a specific redeemer (or `ANY_DELEGATE`), + * `authority` is always `ROOT_AUTHORITY` so there is no redelegation chain, and `modes_[0]` + * must be `SIMPLE_BATCH_MODE`. + * @param permissionContexts_ Must contain one ABI-encoded one-element `Delegation[]`. + * @param modes_ Must contain the canonical batch/default mode. + * @param executionContexts_ Manager-specific execution context. + */ + function redeemDelegations( + bytes[] calldata permissionContexts_, + ModeCode[] calldata modes_, + bytes[] calldata executionContexts_ + ) + external + { + if (permissionContexts_.length != 1 || modes_.length != 1 || executionContexts_.length != 1) { + revert BatchDataLengthMismatch(); + } + if (ModeCode.unwrap(modes_[0]) != ModeCode.unwrap(SIMPLE_BATCH_MODE)) revert InvalidMode(); + + Delegation[] memory delegations_ = abi.decode(permissionContexts_[0], (Delegation[])); + if (delegations_.length != 1) revert InvalidPermissionContext(); + + Delegation memory delegation_ = delegations_[0]; + if (delegation_.delegate != msg.sender && delegation_.delegate != ANY_DELEGATE) revert InvalidDelegate(); + if (delegation_.authority != ROOT_AUTHORITY) revert InvalidAuthority(); + if (delegation_.caveats.length != 1 || delegation_.caveats[0].enforcer != address(this)) revert InvalidCaveat(); + + bytes32 delegationHash_ = _getSingleCaveatDelegationHash(delegation_); + if (disabledDelegations[delegationHash_]) revert CannotUseADisabledDelegation(); + + _validateSignature(delegation_, delegationHash_); + + disabledDelegations[delegationHash_] = true; + bytes memory terms_ = delegation_.caveats[0].terms; + _executeIntent(delegation_.delegator, terms_, executionContexts_[0]); + + emit RedeemedDelegation(delegation_.delegator, msg.sender, delegationHash_, uint8(terms_[0])); + } + + /** + * @notice Returns the EIP-712 hash used to sign a delegation. + * @param delegation_ Delegation to hash. + */ + function getDelegationHash(Delegation calldata delegation_) external pure returns (bytes32) { + return EncoderLib._getDelegationHash(delegation_); + } + + /** + * @notice Returns this manager's EIP-712 domain separator. + */ + function getDomainHash() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @notice Executes the signed intent after the one-shot lock is recorded. + * @param delegator_ Root delegator account that will execute. + * @param terms_ Signed caveat terms. + * @param executionContext_ Redeemer-supplied execution context. + */ + function _executeIntent(address delegator_, bytes memory terms_, bytes calldata executionContext_) internal virtual; + + function _validateSignature(Delegation memory delegation_, bytes32 delegationHash_) private view { + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), delegationHash_); + (address recovered_, ECDSA.RecoverError error_,) = ECDSA.tryRecover(typedDataHash_, delegation_.signature); + if (error_ == ECDSA.RecoverError.NoError && recovered_ == delegation_.delegator) return; + + // Codeless delegators are EOAs: a non-matching recovery cannot succeed via ERC-1271. + if (delegation_.delegator.code.length == 0) revert InvalidEOASignature(); + + if ( + IERC1271(delegation_.delegator).isValidSignature(typedDataHash_, delegation_.signature) + != ERC1271Lib.EIP1271_MAGIC_VALUE + ) { + revert InvalidERC1271Signature(); + } + } + + function _getSingleCaveatDelegationHash(Delegation memory delegation_) internal pure returns (bytes32) { + Caveat memory caveat_ = delegation_.caveats[0]; + bytes32 caveatHash_ = keccak256(abi.encode(CAVEAT_TYPEHASH, caveat_.enforcer, keccak256(caveat_.terms))); + bytes32 caveatsHash_ = keccak256(abi.encodePacked(caveatHash_)); + + return keccak256( + abi.encode( + DELEGATION_TYPEHASH, + delegation_.delegate, + delegation_.delegator, + delegation_.authority, + caveatsHash_, + delegation_.salt + ) + ); + } + + function _balanceOf(address token_, address recipient_) internal view returns (uint256) { + return token_ == address(0) ? recipient_.balance : IERC20(token_).balanceOf(recipient_); + } +} diff --git a/src/MetaSwapOrderDelegationManager.sol b/src/MetaSwapOrderDelegationManager.sol new file mode 100644 index 00000000..65981c94 --- /dev/null +++ b/src/MetaSwapOrderDelegationManager.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; +import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; +import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; +import { Execution } from "./utils/Types.sol"; + +/** + * @title MetaSwapOrderDelegationManager + * @notice One purpose-specific manager for exact batches and flexible MetaSwap limit orders. + * @dev No external caveat hooks and no redelegation chains. Both intents redeem through a direct + * batch/default `executeFromExecutor`. + * + * Exact batch terms: `intent(1) | executionHash(32)` where `executionHash = keccak256(executionCallDatas[0])`. + * Expiry and the redeemer stay off this path: `delegate` is the redeemer (or `ANY_DELEGATE`), and + * `disabledDelegations` is the one-shot. + * + * Flexible terms: `intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) | + * tokenOut(20) | recipient(20) | tokenOutMin(32) | timestampAfter(16) | timestampBefore(16) | id(32) | + * redeemers(20*N)`. + * + * Limit-order policies, embedded because this manager does not call caveat hooks: + * - Redeemers are required. `address(0)` is a normal allowlist entry, so a Sentinel pre-sign (`from = 0x0`) + * can be included without authorizing every caller. + * - Timestamp bounds are optional. `0` disables that bound. Bounds are exclusive, matching `TimestampEnforcer`. + * - `id == 0` skips the bitmap. One-shot is still the delegation hash. `id != 0` also burns that id for the + * delegator, so sibling orders that share it cannot fill. `disableDelegation` does not burn the id. + */ +contract MetaSwapOrderDelegationManager is MetaSwapDelegationManagerBase { + using BitMaps for BitMaps.BitMap; + using ExecutionLib for bytes; + + enum Intent { + ExactBatch, + FlexibleSettlement + } + + enum ApprovalMode { + None, + SkipApproval, + Approve, + ResetApprove + } + + struct FlexibleTerms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + ApprovalMode approvalMode; + address tokenOut; + address recipient; + uint256 tokenOutMin; + uint128 timestampAfter; + uint128 timestampBefore; + uint256 id; + address[] redeemers; + } + + string public constant NAME = "MetaSwapOrderDelegationManager"; + + uint256 private constant EXACT_TERMS_LENGTH = 33; + /// @dev Intent through id. Redeemers are a non-empty 20-byte tail. + uint256 private constant FLEXIBLE_FIXED_TERMS_LENGTH = 210; + uint256 private constant APPROVE_CALL_LENGTH = 68; + uint256 private constant SWAP_CALL_MIN_LENGTH = 196; + + /// @dev IdEnforcer bitmap. Unused when `id == 0`. + mapping(address delegator => BitMaps.BitMap ids) private isUsedId; + + event UsedId(address indexed delegator, address indexed redeemer, uint256 id); + + error ApprovalShapeNotAllowed(); + error EarlyDelegation(); + error ExpiredDelegation(); + error IdAlreadyUsed(); + error InvalidApproval(); + error InvalidApprovalMode(); + error InvalidBatchLength(); + error InvalidExecutionHash(); + error InvalidIntent(); + error InvalidSwap(); + error UnauthorizedRedeemer(); + + constructor() MetaSwapDelegationManagerBase(NAME) { } + + /** + * @notice Decodes exact-batch terms. + * @param terms_ Packed as `intent(1) | executionHash(32)`. + */ + function getExactTermsInfo(bytes memory terms_) public pure returns (bytes32 executionHash_) { + if (terms_.length != EXACT_TERMS_LENGTH || uint8(terms_[0]) != uint8(Intent.ExactBatch)) { + revert InvalidTerms(); + } + assembly ("memory-safe") { + executionHash_ := mload(add(terms_, 33)) + } + } + + /** + * @notice Decodes flexible settlement terms. + * @param terms_ Packed as `intent(1) | settlement(145) | timestampAfter(16) | timestampBefore(16) | id(32) | + * redeemers(20*N)`. + */ + function getFlexibleTermsInfo(bytes memory terms_) public pure returns (FlexibleTerms memory termsInfo_) { + uint256 termsLength_ = terms_.length; + uint256 redeemerBytes_ = termsLength_ < FLEXIBLE_FIXED_TERMS_LENGTH ? 0 : termsLength_ - FLEXIBLE_FIXED_TERMS_LENGTH; + if ( + termsLength_ < FLEXIBLE_FIXED_TERMS_LENGTH + 20 || redeemerBytes_ % 20 != 0 + || uint8(terms_[0]) != uint8(Intent.FlexibleSettlement) + ) { + revert InvalidTerms(); + } + + assembly ("memory-safe") { + let termsData_ := add(terms_, 0x21) + mstore(termsInfo_, shr(96, mload(termsData_))) + mstore(add(termsInfo_, 0x20), shr(96, mload(add(termsData_, 20)))) + mstore(add(termsInfo_, 0x40), mload(add(termsData_, 40))) + mstore(add(termsInfo_, 0x80), shr(96, mload(add(termsData_, 73)))) + mstore(add(termsInfo_, 0xa0), shr(96, mload(add(termsData_, 93)))) + mstore(add(termsInfo_, 0xc0), mload(add(termsData_, 113))) + // Memory structs keep one 32-byte slot per field, so the packed uint128s are widened here. + mstore(add(termsInfo_, 0xe0), shr(128, mload(add(termsData_, 145)))) + mstore(add(termsInfo_, 0x100), shr(128, mload(add(termsData_, 161)))) + mstore(add(termsInfo_, 0x120), mload(add(termsData_, 177))) + } + uint8 approvalMode_ = uint8(terms_[73]); + + if ( + termsInfo_.metaSwap == address(0) || termsInfo_.tokenInAmount == 0 || termsInfo_.recipient == address(0) + || termsInfo_.tokenOutMin == 0 || termsInfo_.tokenIn == termsInfo_.tokenOut + ) { + revert InvalidTerms(); + } + if (approvalMode_ > uint8(ApprovalMode.ResetApprove)) revert InvalidApprovalMode(); + termsInfo_.approvalMode = ApprovalMode(approvalMode_); + + uint256 redeemerCount_ = redeemerBytes_ / 20; + address[] memory redeemers_ = new address[](redeemerCount_); + for (uint256 i_; i_ < redeemerCount_; ++i_) { + uint256 offset_ = FLEXIBLE_FIXED_TERMS_LENGTH + (i_ * 20); + redeemers_[i_] = address(bytes20(_loadWord(terms_, offset_))); + } + termsInfo_.redeemers = redeemers_; + } + + /** + * @notice Returns whether a limit-order id has been filled for this delegator. + * @dev `id == 0` is never recorded. Hash one-shot for that order lives in `disabledDelegations`. + */ + function getIsUsed(address delegator_, uint256 id_) external view returns (bool) { + return isUsedId[delegator_].get(id_); + } + + function _executeIntent(address delegator_, bytes memory terms_, bytes calldata executionContext_) internal override { + if (terms_.length == 0) revert InvalidTerms(); + + uint8 intent_ = uint8(terms_[0]); + if (intent_ == uint8(Intent.ExactBatch)) { + _executeExact(delegator_, terms_, executionContext_); + } else if (intent_ == uint8(Intent.FlexibleSettlement)) { + _executeFlexible(delegator_, terms_, executionContext_); + } else { + revert InvalidIntent(); + } + } + + function _executeExact(address delegator_, bytes memory terms_, bytes calldata executionContext_) private { + bytes32 expectedHash_ = getExactTermsInfo(terms_); + if (keccak256(executionContext_) != expectedHash_) revert InvalidExecutionHash(); + + IDeleGatorCore(delegator_).executeFromExecutor(SIMPLE_BATCH_MODE, executionContext_); + } + + function _executeFlexible(address delegator_, bytes memory terms_, bytes calldata executionContext_) private { + FlexibleTerms memory termsInfo_ = getFlexibleTermsInfo(terms_); + _validateRedeemer(termsInfo_.redeemers); + _validateTimestamp(termsInfo_.timestampAfter, termsInfo_.timestampBefore); + + Execution[] calldata executions_ = executionContext_.decodeBatch(); + _validateExecutions(executions_, termsInfo_); + _consumeId(delegator_, termsInfo_.id); + + uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + IDeleGatorCore(delegator_).executeFromExecutor(SIMPLE_BATCH_MODE, executionContext_); + uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + + if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { + revert InsufficientOutput(); + } + } + + function _validateRedeemer(address[] memory redeemers_) private view { + uint256 length_ = redeemers_.length; + for (uint256 i_; i_ < length_; ++i_) { + if (msg.sender == redeemers_[i_]) return; + } + revert UnauthorizedRedeemer(); + } + + /// @dev `0` disables a bound. A set bound is exclusive, matching TimestampEnforcer. + function _validateTimestamp(uint128 timestampAfter_, uint128 timestampBefore_) private view { + if (timestampAfter_ != 0 && block.timestamp <= timestampAfter_) revert EarlyDelegation(); + if (timestampBefore_ != 0 && block.timestamp >= timestampBefore_) revert ExpiredDelegation(); + } + + /// @dev `id == 0` keeps the delegation-hash one-shot only. A non-zero id also excludes sibling orders. + function _consumeId(address delegator_, uint256 id_) private { + if (id_ == 0) return; + if (isUsedId[delegator_].get(id_)) revert IdAlreadyUsed(); + isUsedId[delegator_].set(id_); + emit UsedId(delegator_, msg.sender, id_); + } + + function _loadWord(bytes memory data_, uint256 offset_) private pure returns (bytes32 value_) { + assembly ("memory-safe") { + value_ := mload(add(add(data_, 0x20), offset_)) + } + } + + function _validateExecutions(Execution[] calldata executions_, FlexibleTerms memory termsInfo_) private pure { + ApprovalMode approvalMode_ = termsInfo_.approvalMode; + + if (termsInfo_.tokenIn == address(0)) { + if (approvalMode_ != ApprovalMode.None) revert InvalidApprovalMode(); + if (executions_.length != 1) revert InvalidBatchLength(); + _validateSwap(executions_[0], termsInfo_.metaSwap, address(0), termsInfo_.tokenInAmount, termsInfo_.tokenInAmount); + return; + } + + if (approvalMode_ == ApprovalMode.SkipApproval) { + if (executions_.length != 1) revert ApprovalShapeNotAllowed(); + _validateSwap(executions_[0], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, 0); + } else if (approvalMode_ == ApprovalMode.Approve) { + if (executions_.length != 2) revert ApprovalShapeNotAllowed(); + _validateApproval(executions_[0], termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + _validateSwap(executions_[1], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, 0); + } else if (approvalMode_ == ApprovalMode.ResetApprove) { + if (executions_.length != 3) revert ApprovalShapeNotAllowed(); + _validateApproval(executions_[0], termsInfo_.tokenIn, termsInfo_.metaSwap, 0); + _validateApproval(executions_[1], termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + _validateSwap(executions_[2], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, 0); + } else { + revert InvalidApprovalMode(); + } + } + + function _validateApproval( + Execution calldata execution_, + address tokenIn_, + address metaSwap_, + uint256 expectedAmount_ + ) + private + pure + { + bytes calldata callData_ = execution_.callData; + if ( + execution_.target != tokenIn_ || execution_.value != 0 || callData_.length != APPROVE_CALL_LENGTH + || bytes4(callData_[0:4]) != IERC20.approve.selector + || bytes32(callData_[4:36]) != bytes32(uint256(uint160(metaSwap_))) + || uint256(bytes32(callData_[36:68])) != expectedAmount_ + ) { + revert InvalidApproval(); + } + } + + function _validateSwap( + Execution calldata execution_, + address metaSwap_, + address tokenIn_, + uint256 tokenInAmount_, + uint256 expectedValue_ + ) + private + pure + { + bytes calldata callData_ = execution_.callData; + if ( + execution_.target != metaSwap_ || execution_.value != expectedValue_ || callData_.length < SWAP_CALL_MIN_LENGTH + || bytes4(callData_[0:4]) != IMetaSwap.swap.selector + || bytes32(callData_[36:68]) != bytes32(uint256(uint160(tokenIn_))) + || uint256(bytes32(callData_[68:100])) != tokenInAmount_ + ) { + revert InvalidSwap(); + } + } +} diff --git a/test/MetaSwapOrderDelegationManager.t.sol b/test/MetaSwapOrderDelegationManager.t.sol new file mode 100644 index 00000000..552e377c --- /dev/null +++ b/test/MetaSwapOrderDelegationManager.t.sol @@ -0,0 +1,1315 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { EntryPoint } from "@account-abstraction/core/EntryPoint.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { MetaSwapDelegationManagerBase } from "../src/MetaSwapDelegationManagerBase.sol"; +import { MetaSwapOrderDelegationManager } from "../src/MetaSwapOrderDelegationManager.sol"; +import { DelegationManager } from "../src/DelegationManager.sol"; +import { EIP7702StatelessDeleGator } from "../src/EIP7702/EIP7702StatelessDeleGator.sol"; +import { ExactExecutionBatchEnforcer } from "../src/enforcers/ExactExecutionBatchEnforcer.sol"; +import { LimitedCallsEnforcer } from "../src/enforcers/LimitedCallsEnforcer.sol"; +import { IMetaSwap } from "../src/helpers/interfaces/IMetaSwap.sol"; +import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; +import { BasicERC20 } from "./utils/BasicERC20.t.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../src/utils/Types.sol"; +import { ERC1271Lib } from "../src/libraries/ERC1271Lib.sol"; + +contract OrderManager1271Account { + using ExecutionLib for bytes; + + function isValidSignature(bytes32 hash_, bytes memory signature_) external pure returns (bytes4) { + if (signature_.length == 32 && bytes32(signature_) == hash_) return ERC1271Lib.EIP1271_MAGIC_VALUE; + return ERC1271Lib.SIG_VALIDATION_FAILED; + } + + function executeFromExecutor(ModeCode, bytes calldata executionCallData_) + external + payable + returns (bytes[] memory returnData_) + { + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + returnData_ = new bytes[](executions_.length); + for (uint256 i; i < executions_.length; ++i) { + (bool ok_, bytes memory ret_) = executions_[i].target.call{ value: executions_[i].value }(executions_[i].callData); + require(ok_, "exec-failed"); + returnData_[i] = ret_; + } + } +} + +contract OrderManagerMetaSwapMock is IMetaSwap { + using SafeERC20 for IERC20; + + mapping(string aggregatorId => Adapter adapter) private adapters_; + + function setAdapter(string calldata aggregatorId_, address addr_, bytes4 selector_, bytes calldata data_) external { + adapters_[aggregatorId_] = Adapter({ addr: addr_, selector: selector_, data: data_ }); + } + + function removeAdapter(string calldata aggregatorId_) external { + delete adapters_[aggregatorId_]; + } + + function adapters(string memory aggregatorId_) external view returns (Adapter memory) { + return adapters_[aggregatorId_]; + } + + function swap(string calldata, IERC20 tokenFrom_, uint256 amount_, bytes calldata data_) external payable { + if (address(tokenFrom_) == address(0)) { + require(msg.value == amount_, "invalid-native-input"); + } else { + tokenFrom_.safeTransferFrom(msg.sender, address(this), amount_); + } + + (IERC20 tokenOut_, uint256 amountOut_) = abi.decode(data_, (IERC20, uint256)); + if (address(tokenOut_) == address(0)) { + (bool success_,) = msg.sender.call{ value: amountOut_ }(""); + require(success_, "native-output-failed"); + } else { + tokenOut_.safeTransfer(msg.sender, amountOut_); + } + } + + receive() external payable { } +} + +contract MetaSwapOrderDelegationManagerTest is Test { + uint256 private constant TOKEN_IN_AMOUNT = 100 ether; + uint256 private constant TOKEN_OUT_MIN = 190 ether; + uint256 private constant TOKEN_OUT_AMOUNT = 200 ether; + + uint256 private constant GENERIC_KEY = 0x1111; + uint256 private constant WRONG_SIGNER_KEY = 0x2222; + uint256 private constant ORDER_KEY = 0x3333; + + EntryPoint private entryPoint; + OrderManagerMetaSwapMock private metaSwap; + BasicERC20 private tokenIn; + BasicERC20 private tokenOut; + + DelegationManager private genericManager; + ExactExecutionBatchEnforcer private exactBatchEnforcer; + LimitedCallsEnforcer private limitedCallsEnforcer; + MetaSwapOrderDelegationManager private orderManager; + + address private genericAccount; + address private orderAccount; + address private relayer; + + function setUp() public { + entryPoint = new EntryPoint(); + metaSwap = new OrderManagerMetaSwapMock(); + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + relayer = makeAddr("Relayer"); + + genericManager = new DelegationManager(address(this)); + exactBatchEnforcer = new ExactExecutionBatchEnforcer(); + limitedCallsEnforcer = new LimitedCallsEnforcer(); + orderManager = new MetaSwapOrderDelegationManager(); + + genericAccount = vm.addr(GENERIC_KEY); + orderAccount = vm.addr(ORDER_KEY); + + _installDeleGator(genericAccount, address(genericManager)); + _installDeleGator(orderAccount, address(orderManager)); + + tokenIn.mint(genericAccount, 1_000 ether); + tokenIn.mint(orderAccount, 1_000 ether); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(genericAccount, 1_000 ether); + vm.deal(orderAccount, 1_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + // -------- Exact batch -------- + + function test_exactRedeemsApproveAndSwap() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 1); + + vm.expectEmit(true, true, true, true, address(orderManager)); + emit MetaSwapDelegationManagerBase.RedeemedDelegation( + orderAccount, + relayer, + orderManager.getDelegationHash(delegation_), + uint8(MetaSwapOrderDelegationManager.Intent.ExactBatch) + ); + _redeemIntent(delegation_, encoded_); + + assertEq(tokenIn.balanceOf(orderAccount), 900 ether); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + assertTrue(orderManager.disabledDelegations(orderManager.getDelegationHash(delegation_))); + } + + function test_exactRedeemsSkipApprovalSwap() public { + vm.prank(orderAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + Execution[] memory executions_ = _erc20Executions(0, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 2); + + _redeemIntent(delegation_, encoded_); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_exactRedeemsResetApproveAndSwap() public { + vm.prank(orderAccount); + tokenIn.approve(address(metaSwap), 1); + + Execution[] memory executions_ = _erc20Executions(2, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 3); + + _redeemIntent(delegation_, encoded_); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_exactRedeemsNativeSwap() public { + Execution[] memory executions_ = _nativeExecutions(TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 4); + uint256 nativeBefore_ = orderAccount.balance; + + _redeemIntent(delegation_, encoded_); + + assertEq(orderAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_exactRevertsForHashMismatch() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 5); + + executions_[1].value = 1; + vm.expectRevert(MetaSwapOrderDelegationManager.InvalidExecutionHash.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); + } + + function test_exactRevertsOnReplay() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 6); + + _redeemIntent(delegation_, encoded_); + + vm.expectRevert(MetaSwapDelegationManagerBase.CannotUseADisabledDelegation.selector); + _redeemIntent(delegation_, encoded_); + } + + function test_exactDisableDelegationCancels() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 7); + + vm.prank(orderAccount); + orderManager.disableDelegation(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.CannotUseADisabledDelegation.selector); + _redeemIntent(delegation_, encoded_); + } + + function test_exactRejectsWrongSigner() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + Delegation memory delegation_ = _signIntentWithKey(WRONG_SIGNER_KEY, _exactTerms(keccak256(encoded_)), 8); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidERC1271Signature.selector); + _redeemIntent(delegation_, encoded_); + } + + function test_exactRejectsWrongSignerOnCodelessEOA() public { + address eoa_ = vm.addr(0xE0A); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); + Delegation memory delegation_ = _signManager(orderManager, WRONG_SIGNER_KEY, eoa_, caveats_, 40); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + _redeemIntent(delegation_, encoded_); + } + + function test_exactRedeemsWithERC1271Fallback() public { + OrderManager1271Account account_ = new OrderManager1271Account(); + tokenIn.mint(address(account_), 1_000 ether); + + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + bytes memory terms_ = _exactTerms(keccak256(encoded_)); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: terms_, args: hex"" }); + Delegation memory delegation_ = Delegation({ + delegate: address(0xa11), + delegator: address(account_), + authority: orderManager.ROOT_AUTHORITY(), + caveats: caveats_, + salt: 41, + signature: hex"" + }); + bytes32 typedDataHash_ = + MessageHashUtils.toTypedDataHash(orderManager.getDomainHash(), orderManager.getDelegationHash(delegation_)); + delegation_.signature = abi.encodePacked(typedDataHash_); + + _redeemIntent(delegation_, encoded_); + + assertEq(tokenOut.balanceOf(address(account_)), TOKEN_OUT_AMOUNT); + } + + function test_exactRejectsInvalidERC1271Signature() public { + OrderManager1271Account account_ = new OrderManager1271Account(); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); + Delegation memory delegation_ = Delegation({ + delegate: address(0xa11), + delegator: address(account_), + authority: orderManager.ROOT_AUTHORITY(), + caveats: caveats_, + salt: 42, + signature: abi.encodePacked(bytes32(uint256(1))) + }); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidERC1271Signature.selector); + _redeemIntent(delegation_, encoded_); + } + + // -------- Flexible intent -------- + + function test_flexibleRedeemsApproveAndSwap() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 10); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + + vm.expectEmit(true, true, true, true, address(orderManager)); + emit MetaSwapDelegationManagerBase.RedeemedDelegation( + orderAccount, + relayer, + orderManager.getDelegationHash(delegation_), + uint8(MetaSwapOrderDelegationManager.Intent.FlexibleSettlement) + ); + _redeemIntent(delegation_, encoded_); + + assertEq(tokenIn.balanceOf(orderAccount), 900 ether); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsSkipApproval() public { + vm.prank(orderAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + bytes memory terms_ = _flexibleTerms(address(tokenIn), _skipApprovalMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 11); + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(0, TOKEN_OUT_AMOUNT))); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsResetApprove() public { + vm.prank(orderAccount); + tokenIn.approve(address(metaSwap), 1); + + bytes memory terms_ = _flexibleTerms(address(tokenIn), _resetApproveMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 12); + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(2, TOKEN_OUT_AMOUNT))); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsNativeInput() public { + bytes memory terms_ = _flexibleTerms(address(0), _noneMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 13); + uint256 nativeBefore_ = orderAccount.balance; + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); + + assertEq(orderAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsNativeOutput() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(0), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 19); + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = abi.encodeCall( + IMetaSwap.swap, + ("redeemer-route", IERC20(address(tokenIn)), TOKEN_IN_AMOUNT, abi.encode(IERC20(address(0)), TOKEN_OUT_AMOUNT)) + ); + uint256 nativeBefore_ = orderAccount.balance; + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); + + assertEq(tokenIn.balanceOf(orderAccount), 900 ether); + assertEq(orderAccount.balance, nativeBefore_ + TOKEN_OUT_AMOUNT); + } + + function test_flexibleAllowsDifferentRouteData() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); + + Execution[] memory first_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + first_[1].callData = abi.encodeCall( + IMetaSwap.swap, ("route-a", IERC20(address(tokenIn)), TOKEN_IN_AMOUNT, abi.encode(tokenOut, TOKEN_OUT_AMOUNT)) + ); + _redeemIntent(_signIntent(terms_, 14), ExecutionLib.encodeBatch(first_)); + + Execution[] memory second_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + second_[1].callData = abi.encodeCall( + IMetaSwap.swap, ("route-b", IERC20(address(tokenIn)), TOKEN_IN_AMOUNT, abi.encode(tokenOut, TOKEN_OUT_AMOUNT)) + ); + _redeemIntent(_signIntent(terms_, 15), ExecutionLib.encodeBatch(second_)); + + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT * 2); + } + + function test_flexibleRevertsAtomicallyForInsufficientOutput() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 16); + bytes32 hash_ = orderManager.getDelegationHash(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.InsufficientOutput.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_MIN - 1))); + + assertFalse(orderManager.disabledDelegations(hash_)); + assertEq(tokenIn.balanceOf(orderAccount), 1_000 ether); + } + + function test_flexibleRejectsInvalidApprovalMode() public { + bytes memory terms_ = _flexibleTerms(address(0), _approveMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 17); + + vm.expectRevert(MetaSwapOrderDelegationManager.InvalidApprovalMode.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); + } + + function test_rejectsUnknownIntent() public { + bytes memory terms_ = abi.encodePacked(uint8(2), bytes32(0)); + Delegation memory delegation_ = _signIntent(terms_, 18); + + vm.expectRevert(MetaSwapOrderDelegationManager.InvalidIntent.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + // -------- Flexible validation -------- + + function test_flexibleRejectsInvalidTermsLength() public { + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo(new bytes(209)); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo(new bytes(210)); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo(new bytes(229)); + } + + function test_flexibleDecodesOptionalPolicies() public { + address[] memory redeemers_ = new address[](2); + redeemers_[0] = address(0); + redeemers_[1] = relayer; + + MetaSwapOrderDelegationManager.FlexibleTerms memory info_ = orderManager.getFlexibleTermsInfo( + _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 100, 200, 7, redeemers_) + ); + + assertEq(info_.metaSwap, address(metaSwap)); + assertEq(info_.tokenIn, address(tokenIn)); + assertEq(info_.tokenInAmount, TOKEN_IN_AMOUNT); + assertEq(uint8(info_.approvalMode), uint8(_approveMode())); + assertEq(info_.tokenOut, address(tokenOut)); + assertEq(info_.recipient, orderAccount); + assertEq(info_.tokenOutMin, TOKEN_OUT_MIN); + assertEq(info_.timestampAfter, 100); + assertEq(info_.timestampBefore, 200); + assertEq(info_.id, 7); + assertEq(info_.redeemers.length, 2); + assertEq(info_.redeemers[0], address(0)); + assertEq(info_.redeemers[1], relayer); + } + + function test_flexibleAllowsZeroAddressInRedeemerList() public { + address[] memory redeemers_ = new address[](2); + redeemers_[0] = address(0); + redeemers_[1] = relayer; + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 0, 0, 0, redeemers_); + + _redeemIntent(_signIntent(terms_, 80), ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleZeroAddressDoesNotAuthorizeOtherRedeemers() public { + address[] memory redeemers_ = new address[](1); + redeemers_[0] = address(0); + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 0, 0, 0, redeemers_); + + Delegation memory delegation_ = _signIntent(terms_, 81); + vm.expectRevert(MetaSwapOrderDelegationManager.UnauthorizedRedeemer.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + function test_flexibleRejectsUnlistedRedeemer() public { + address[] memory redeemers_ = new address[](1); + redeemers_[0] = makeAddr("OtherRedeemer"); + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 0, 0, 0, redeemers_); + + Delegation memory delegation_ = _signIntent(terms_, 82); + vm.expectRevert(MetaSwapOrderDelegationManager.UnauthorizedRedeemer.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + function test_flexibleDelegateStillRestrictsListedRedeemer() public { + address station_ = makeAddr("Station"); + address[] memory redeemers_ = new address[](1); + redeemers_[0] = relayer; + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 0, 0, 0, redeemers_); + + Delegation memory delegation_ = _signIntentWithDelegate(terms_, 83, station_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidDelegate.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + function test_flexibleTimestampWindow() public { + uint128 opensAt_ = uint128(block.timestamp); + uint128 expiresAt_ = uint128(block.timestamp + 1 days); + address[] memory redeemers_ = _relayerRedeemers(); + bytes memory terms_ = _flexibleTerms( + address(tokenIn), _approveMode(), address(tokenOut), orderAccount, opensAt_, expiresAt_, 0, redeemers_ + ); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + + Delegation memory early_ = _signIntent(terms_, 84); + vm.expectRevert(MetaSwapOrderDelegationManager.EarlyDelegation.selector); + _redeemIntent(early_, encoded_); + + vm.warp(opensAt_ + 1); + _redeemIntent(_signIntent(terms_, 85), encoded_); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + + vm.warp(expiresAt_); + Delegation memory expired_ = _signIntent(terms_, 86); + vm.expectRevert(MetaSwapOrderDelegationManager.ExpiredDelegation.selector); + _redeemIntent(expired_, encoded_); + } + + function test_flexibleIdZeroSkipsBitmap() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + + _redeemIntent(_signIntent(terms_, 87), encoded_); + _redeemIntent(_signIntent(terms_, 88), encoded_); + + assertFalse(orderManager.getIsUsed(orderAccount, 0)); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT * 2); + } + + function test_flexibleIdExcludesSiblingOrders() public { + address[] memory redeemers_ = _relayerRedeemers(); + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 0, 0, 7, redeemers_); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + Delegation memory first_ = _signIntent(terms_, 89); + Delegation memory second_ = _signIntent(terms_, 90); + + vm.expectEmit(true, true, false, true, address(orderManager)); + emit MetaSwapOrderDelegationManager.UsedId(orderAccount, relayer, 7); + _redeemIntent(first_, encoded_); + + vm.expectRevert(MetaSwapOrderDelegationManager.IdAlreadyUsed.selector); + _redeemIntent(second_, encoded_); + + assertTrue(orderManager.getIsUsed(orderAccount, 7)); + assertFalse(orderManager.disabledDelegations(orderManager.getDelegationHash(second_))); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleDisableDoesNotConsumeId() public { + address[] memory redeemers_ = _relayerRedeemers(); + bytes memory terms_ = + _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 0, 0, 11, redeemers_); + Delegation memory cancelled_ = _signIntent(terms_, 91); + Delegation memory replacement_ = _signIntent(terms_, 92); + + vm.prank(orderAccount); + orderManager.disableDelegation(cancelled_); + assertFalse(orderManager.getIsUsed(orderAccount, 11)); + + _redeemIntent(replacement_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + assertTrue(orderManager.getIsUsed(orderAccount, 11)); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleIdRevertsAtomicallyForInsufficientOutput() public { + address[] memory redeemers_ = _relayerRedeemers(); + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount, 0, 0, 9, redeemers_); + Delegation memory delegation_ = _signIntent(terms_, 93); + + vm.expectRevert(MetaSwapDelegationManagerBase.InsufficientOutput.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_MIN - 1))); + + assertFalse(orderManager.getIsUsed(orderAccount, 9)); + assertFalse(orderManager.disabledDelegations(orderManager.getDelegationHash(delegation_))); + assertEq(tokenIn.balanceOf(orderAccount), 1_000 ether); + } + + function test_flexibleRejectsInvalidRequiredTerms() public { + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo( + _rawFlexibleTerms(address(0), address(tokenIn), TOKEN_IN_AMOUNT, uint8(_approveMode()), address(tokenOut), orderAccount) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo( + _rawFlexibleTerms(address(metaSwap), address(tokenIn), 0, uint8(_approveMode()), address(tokenOut), orderAccount) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo( + _rawFlexibleTerms( + address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, uint8(_approveMode()), address(tokenOut), address(0) + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo( + _rawFlexibleTerms( + address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, uint8(_approveMode()), address(tokenOut), orderAccount, 0 + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo( + _rawFlexibleTerms( + address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, uint8(_approveMode()), address(tokenIn), orderAccount + ) + ); + } + + function test_flexibleRejectsUndefinedApprovalMode() public { + vm.expectRevert(MetaSwapOrderDelegationManager.InvalidApprovalMode.selector); + orderManager.getFlexibleTermsInfo( + _rawFlexibleTerms(address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, 4, address(tokenOut), orderAccount) + ); + } + + function test_flexibleRejectsErc20NoneMode() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _noneMode(), address(tokenOut), orderAccount); + _expectFlexibleRevert( + terms_, _erc20Executions(0, TOKEN_OUT_AMOUNT), MetaSwapOrderDelegationManager.InvalidApprovalMode.selector, 50 + ); + } + + function test_flexibleRejectsWrongApprovalShape() public { + _expectFlexibleRevert( + _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount), + _erc20Executions(0, TOKEN_OUT_AMOUNT), + MetaSwapOrderDelegationManager.ApprovalShapeNotAllowed.selector, + 51 + ); + _expectFlexibleRevert( + _flexibleTerms(address(tokenIn), _skipApprovalMode(), address(tokenOut), orderAccount), + _erc20Executions(1, TOKEN_OUT_AMOUNT), + MetaSwapOrderDelegationManager.ApprovalShapeNotAllowed.selector, + 52 + ); + _expectFlexibleRevert( + _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount), + _erc20Executions(2, TOKEN_OUT_AMOUNT), + MetaSwapOrderDelegationManager.ApprovalShapeNotAllowed.selector, + 53 + ); + } + + function test_flexibleRejectsUnsupportedBatchLengths() public { + Execution[] memory empty_ = new Execution[](0); + _expectFlexibleRevert( + _flexibleTerms(address(tokenIn), _skipApprovalMode(), address(tokenOut), orderAccount), + empty_, + MetaSwapOrderDelegationManager.ApprovalShapeNotAllowed.selector, + 54 + ); + + Execution[] memory tooLong_ = new Execution[](4); + _expectFlexibleRevert( + _flexibleTerms(address(tokenIn), _resetApproveMode(), address(tokenOut), orderAccount), + tooLong_, + MetaSwapOrderDelegationManager.ApprovalShapeNotAllowed.selector, + 55 + ); + + Execution[] memory nativeTooLong_ = new Execution[](2); + _expectFlexibleRevert( + _flexibleTerms(address(0), _noneMode(), address(tokenOut), orderAccount), + nativeTooLong_, + MetaSwapOrderDelegationManager.InvalidBatchLength.selector, + 56 + ); + } + + function test_flexibleRejectsInvalidApproval() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].target = makeAddr("OtherToken"); + _expectInvalidApproval(executions_, 57); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].value = 1; + _expectInvalidApproval(executions_, 58); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodePacked(IERC20.approve.selector); + _expectInvalidApproval(executions_, 59); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(metaSwap), TOKEN_IN_AMOUNT)); + _expectInvalidApproval(executions_, 60); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodeCall(IERC20.approve, (makeAddr("OtherSpender"), TOKEN_IN_AMOUNT)); + _expectInvalidApproval(executions_, 61); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodePacked( + IERC20.approve.selector, bytes32(uint256(uint160(address(metaSwap))) | (uint256(1) << 255)), bytes32(TOKEN_IN_AMOUNT) + ); + _expectInvalidApproval(executions_, 62); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT - 1)); + _expectInvalidApproval(executions_, 63); + } + + function test_flexibleRejectsInvalidResetApproval() public { + Execution[] memory executions_ = _erc20Executions(2, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), 1)); + _expectInvalidApproval(executions_, 64); + } + + function test_flexibleRejectsInvalidSwap() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].target = makeAddr("OtherSwap"); + _expectInvalidSwap(executions_, 65); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].value = 1; + _expectInvalidSwap(executions_, 66); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = abi.encodePacked(IMetaSwap.swap.selector); + _expectInvalidSwap(executions_, 67); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT)); + _expectInvalidSwap(executions_, 68); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = _minimumSwapCalldata(bytes32(uint256(uint160(address(tokenIn))) | (uint256(1) << 255))); + _expectInvalidSwap(executions_, 69); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = abi.encodeCall( + IMetaSwap.swap, + ("redeemer-route", IERC20(makeAddr("OtherToken")), TOKEN_IN_AMOUNT, abi.encode(tokenOut, TOKEN_OUT_AMOUNT)) + ); + _expectInvalidSwap(executions_, 70); + + executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = abi.encodeCall( + IMetaSwap.swap, + ("redeemer-route", IERC20(address(tokenIn)), TOKEN_IN_AMOUNT - 1, abi.encode(tokenOut, TOKEN_OUT_AMOUNT)) + ); + _expectInvalidSwap(executions_, 71); + } + + function test_flexibleRejectsIncompleteSwapCalldata() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = abi.encodePacked( + IMetaSwap.swap.selector, + uint256(128), + bytes32(uint256(uint160(address(tokenIn)))), + TOKEN_IN_AMOUNT, + uint256(160), + uint256(0), + bytes31(0) + ); + assertEq(executions_[1].callData.length, 195); + _expectInvalidSwap(executions_, 72); + } + + function test_flexibleAcceptsMinimumLengthSwapCalldata() public { + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].callData = _minimumSwapCalldata(bytes32(uint256(uint160(address(tokenIn))))); + assertEq(executions_[1].callData.length, 196); + + bytes memory revertData_ = _redeemCatch( + _signIntent(_flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount), 73), + ExecutionLib.encodeBatch(executions_) + ); + if (revertData_.length >= 4) { + assertTrue(bytes4(revertData_) != MetaSwapOrderDelegationManager.InvalidSwap.selector); + } + } + + function test_exactTermsInfo() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + bytes memory terms_ = _exactTerms(keccak256(encoded_)); + assertEq(orderManager.getExactTermsInfo(terms_), keccak256(encoded_)); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getExactTermsInfo(hex"00"); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getExactTermsInfo(abi.encodePacked(uint8(1), bytes32(0))); + } + + function test_rejectsEmptyTerms() public { + Delegation memory delegation_ = _signIntent(hex"", 75); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + function test_disableDelegationGuards() public { + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256("batch")), 76); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidDelegator.selector); + orderManager.disableDelegation(delegation_); + + vm.prank(orderAccount); + orderManager.disableDelegation(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.AlreadyDisabled.selector); + vm.prank(orderAccount); + orderManager.disableDelegation(delegation_); + } + + function test_rejectsMalformedRedemption() public { + Delegation memory delegation_ = _signIntent(_exactTerms(bytes32(0)), 77); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, hex""); + + bytes[] memory noPermissions_ = new bytes[](0); + vm.expectRevert(MetaSwapDelegationManagerBase.BatchDataLengthMismatch.selector); + orderManager.redeemDelegations(noPermissions_, modes_, executionContexts_); + + ModeCode[] memory noModes_ = new ModeCode[](0); + vm.expectRevert(MetaSwapDelegationManagerBase.BatchDataLengthMismatch.selector); + orderManager.redeemDelegations(permissionContexts_, noModes_, executionContexts_); + + bytes[] memory noExecutions_ = new bytes[](0); + vm.expectRevert(MetaSwapDelegationManagerBase.BatchDataLengthMismatch.selector); + orderManager.redeemDelegations(permissionContexts_, modes_, noExecutions_); + + modes_[0] = ModeLib.encodeSimpleSingle(); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidMode.selector); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + + permissionContexts_[0] = abi.encode(new Delegation[](0)); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidPermissionContext.selector); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, _batchModes(), executionContexts_); + + delegation_.authority = bytes32(0); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidAuthority.selector); + _redeemIntent(delegation_, hex""); + + delegation_ = _signIntent(_exactTerms(bytes32(0)), 78); + delegation_.caveats = new Caveat[](0); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidCaveat.selector); + _redeemIntent(delegation_, hex""); + + delegation_ = _signIntent(_exactTerms(bytes32(0)), 79); + delegation_.caveats[0].enforcer = address(exactBatchEnforcer); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidCaveat.selector); + _redeemIntent(delegation_, hex""); + } + + function test_flexibleRejectsNativeSwapWithWrongValue() public { + Execution[] memory executions_ = _nativeExecutions(TOKEN_OUT_AMOUNT); + executions_[0].value = TOKEN_IN_AMOUNT - 1; + _expectInvalidSwap(executions_, 74, _flexibleTerms(address(0), _noneMode(), address(tokenOut), orderAccount)); + } + + // -------- Gas comparisons -------- + + function test_gas_genericExactBatchPlusLimitedCalls() public { + Execution[] memory executions_ = _erc20ExecutionsFor(genericAccount, 1, TOKEN_OUT_AMOUNT); + bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); + + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ enforcer: address(exactBatchEnforcer), terms: encoded_, args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + Delegation memory delegation_ = _signGeneric(caveats_, 100); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + genericManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("generic ExactBatch + LimitedCalls(1)", gasBefore_ - gasleft()); + } + + function test_gas_genericExactBatchPlusLimitedCallsResetApproval() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20ExecutionsFor(genericAccount, 2, TOKEN_OUT_AMOUNT)); + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ enforcer: address(exactBatchEnforcer), terms: encoded_, args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + Delegation memory delegation_ = _signGeneric(caveats_, 105); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + genericManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("generic ExactBatch + LimitedCalls(1), reset approval", gasBefore_ - gasleft()); + } + + function test_gas_genericExactBatchPlusLimitedCallsNative() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT)); + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ enforcer: address(exactBatchEnforcer), terms: encoded_, args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + Delegation memory delegation_ = _signGeneric(caveats_, 106); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + genericManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("generic ExactBatch + LimitedCalls(1), native", gasBefore_ - gasleft()); + } + + function test_gas_orderExact() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 103); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("order ExactBatch", gasBefore_ - gasleft()); + } + + function test_gas_orderExactResetApproval() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(2, TOKEN_OUT_AMOUNT)); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 109); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("order ExactBatch, reset approval", gasBefore_ - gasleft()); + } + + function test_gas_orderExactNative() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT)); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 110); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("order ExactBatch, native", gasBefore_ - gasleft()); + } + + function test_gas_orderFlexible() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 104); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("order FlexibleSettlement", gasBefore_ - gasleft()); + } + + function test_gas_orderFlexibleResetApproval() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _resetApproveMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 111); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(2, TOKEN_OUT_AMOUNT)); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("order FlexibleSettlement, reset approval", gasBefore_ - gasleft()); + } + + function test_gas_orderFlexibleNative() public { + bytes memory terms_ = _flexibleTerms(address(0), _noneMode(), address(tokenOut), orderAccount); + Delegation memory delegation_ = _signIntent(terms_, 112); + bytes memory encoded_ = ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT)); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("order FlexibleSettlement, native", gasBefore_ - gasleft()); + } + + // -------- Helpers -------- + + function _installDeleGator(address account_, address manager_) private { + EIP7702StatelessDeleGator implementation_ = new EIP7702StatelessDeleGator(IDelegationManager(manager_), entryPoint); + vm.etch(account_, bytes.concat(hex"ef0100", abi.encodePacked(implementation_))); + } + + function _exactTerms(bytes32 executionHash_) private pure returns (bytes memory) { + return abi.encodePacked(uint8(MetaSwapOrderDelegationManager.Intent.ExactBatch), executionHash_); + } + + function _flexibleTerms( + address tokenIn_, + MetaSwapOrderDelegationManager.ApprovalMode approvalMode_, + address tokenOut_, + address recipient_ + ) + private + view + returns (bytes memory) + { + address[] memory redeemers_ = _relayerRedeemers(); + return _flexibleTerms(tokenIn_, approvalMode_, tokenOut_, recipient_, 0, 0, 0, redeemers_); + } + + function _flexibleTerms( + address tokenIn_, + MetaSwapOrderDelegationManager.ApprovalMode approvalMode_, + address tokenOut_, + address recipient_, + uint128 timestampAfter_, + uint128 timestampBefore_, + uint256 id_, + address[] memory redeemers_ + ) + private + view + returns (bytes memory) + { + bytes memory packed_ = abi.encodePacked( + uint8(MetaSwapOrderDelegationManager.Intent.FlexibleSettlement), + address(metaSwap), + tokenIn_, + TOKEN_IN_AMOUNT, + uint8(approvalMode_), + tokenOut_, + recipient_, + TOKEN_OUT_MIN, + timestampAfter_, + timestampBefore_, + id_ + ); + uint256 redeemerCount_ = redeemers_.length; + for (uint256 i_; i_ < redeemerCount_; ++i_) { + packed_ = abi.encodePacked(packed_, redeemers_[i_]); + } + return packed_; + } + + function _relayerRedeemers() private view returns (address[] memory redeemers_) { + redeemers_ = new address[](1); + redeemers_[0] = relayer; + } + + function _rawFlexibleTerms( + address metaSwap_, + address tokenIn_, + uint256 tokenInAmount_, + uint8 approvalMode_, + address tokenOut_, + address recipient_ + ) + private + pure + returns (bytes memory) + { + return _rawFlexibleTerms(metaSwap_, tokenIn_, tokenInAmount_, approvalMode_, tokenOut_, recipient_, TOKEN_OUT_MIN); + } + + function _rawFlexibleTerms( + address metaSwap_, + address tokenIn_, + uint256 tokenInAmount_, + uint8 approvalMode_, + address tokenOut_, + address recipient_, + uint256 tokenOutMin_ + ) + private + pure + returns (bytes memory) + { + return abi.encodePacked( + uint8(MetaSwapOrderDelegationManager.Intent.FlexibleSettlement), + metaSwap_, + tokenIn_, + tokenInAmount_, + approvalMode_, + tokenOut_, + recipient_, + tokenOutMin_, + uint128(0), + uint128(0), + uint256(0), + address(1) + ); + } + + function _minimumSwapCalldata(bytes32 tokenInWord_) private pure returns (bytes memory) { + return abi.encodePacked( + IMetaSwap.swap.selector, uint256(128), tokenInWord_, TOKEN_IN_AMOUNT, uint256(160), uint256(0), uint256(0) + ); + } + + function _expectFlexibleRevert(bytes memory terms_, Execution[] memory executions_, bytes4 selector_, uint256 salt_) private { + Delegation memory delegation_ = _signIntent(terms_, salt_); + vm.expectRevert(selector_); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); + } + + function _expectInvalidApproval(Execution[] memory executions_, uint256 salt_) private { + bytes memory terms_ = executions_.length == 3 + ? _flexibleTerms(address(tokenIn), _resetApproveMode(), address(tokenOut), orderAccount) + : _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); + _expectFlexibleRevert(terms_, executions_, MetaSwapOrderDelegationManager.InvalidApproval.selector, salt_); + } + + function _expectInvalidSwap(Execution[] memory executions_, uint256 salt_) private { + _expectInvalidSwap(executions_, salt_, _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount)); + } + + function _expectInvalidSwap(Execution[] memory executions_, uint256 salt_, bytes memory terms_) private { + _expectFlexibleRevert(terms_, executions_, MetaSwapOrderDelegationManager.InvalidSwap.selector, salt_); + } + + function _redeemCatch(Delegation memory delegation_, bytes memory executionContext_) + private + returns (bytes memory revertData_) + { + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, executionContext_); + vm.prank(relayer); + try orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_) { } + catch (bytes memory reason_) { + return reason_; + } + } + + function _signIntent(bytes memory terms_, uint256 salt_) private view returns (Delegation memory) { + return _signIntentWithKey(ORDER_KEY, terms_, salt_); + } + + function _signIntentWithDelegate( + bytes memory terms_, + uint256 salt_, + address delegate_ + ) + private + view + returns (Delegation memory) + { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: terms_, args: hex"" }); + Delegation memory delegation_ = Delegation({ + delegate: delegate_, + delegator: orderAccount, + authority: orderManager.ROOT_AUTHORITY(), + caveats: caveats_, + salt: salt_, + signature: hex"" + }); + bytes32 typedDataHash_ = + MessageHashUtils.toTypedDataHash(orderManager.getDomainHash(), orderManager.getDelegationHash(delegation_)); + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(ORDER_KEY, typedDataHash_); + delegation_.signature = abi.encodePacked(r_, s_, v_); + return delegation_; + } + + function _signIntentWithKey( + uint256 signerKey_, + bytes memory terms_, + uint256 salt_ + ) + private + view + returns (Delegation memory delegation_) + { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: terms_, args: hex"" }); + return _signManager(orderManager, signerKey_, orderAccount, caveats_, salt_); + } + + function _signGeneric(Caveat[] memory caveats_, uint256 salt_) private view returns (Delegation memory) { + return _signManager(MetaSwapDelegationManagerBase(address(0)), GENERIC_KEY, genericAccount, caveats_, salt_, true); + } + + function _signManager( + MetaSwapDelegationManagerBase manager_, + uint256 signerKey_, + address delegator_, + Caveat[] memory caveats_, + uint256 salt_ + ) + private + view + returns (Delegation memory) + { + return _signManager(manager_, signerKey_, delegator_, caveats_, salt_, false); + } + + function _signManager( + MetaSwapDelegationManagerBase manager_, + uint256 signerKey_, + address delegator_, + Caveat[] memory caveats_, + uint256 salt_, + bool useGeneric_ + ) + private + view + returns (Delegation memory delegation_) + { + bytes32 rootAuthority_ = useGeneric_ ? genericManager.ROOT_AUTHORITY() : manager_.ROOT_AUTHORITY(); + delegation_ = Delegation({ + delegate: address(0xa11), + delegator: delegator_, + authority: rootAuthority_, + caveats: caveats_, + salt: salt_, + signature: hex"" + }); + + bytes32 delegationHash_; + bytes32 domainHash_; + if (useGeneric_) { + delegationHash_ = genericManager.getDelegationHash(delegation_); + domainHash_ = genericManager.getDomainHash(); + } else { + delegationHash_ = manager_.getDelegationHash(delegation_); + domainHash_ = manager_.getDomainHash(); + } + + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(domainHash_, delegationHash_); + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(signerKey_, typedDataHash_); + delegation_ = Delegation({ + delegate: delegation_.delegate, + delegator: delegation_.delegator, + authority: delegation_.authority, + caveats: delegation_.caveats, + salt: delegation_.salt, + signature: abi.encodePacked(r_, s_, v_) + }); + } + + function _redeemIntent(Delegation memory delegation_, bytes memory executionContext_) private { + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, executionContext_); + vm.prank(relayer); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function _batchModes() private pure returns (ModeCode[] memory modes_) { + modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + } + + function _redemptionInputs( + Delegation memory delegation_, + bytes memory executionContext_ + ) + private + pure + returns (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) + { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + executionContexts_ = new bytes[](1); + executionContexts_[0] = executionContext_; + } + + function _erc20Executions(uint8 approvalCount_, uint256 outputAmount_) private view returns (Execution[] memory) { + return _erc20ExecutionsFor(orderAccount, approvalCount_, outputAmount_); + } + + function _erc20ExecutionsFor( + address, + uint8 approvalCount_, + uint256 outputAmount_ + ) + private + view + returns (Execution[] memory executions_) + { + uint256 swapIndex_ = approvalCount_; + executions_ = new Execution[](swapIndex_ + 1); + if (approvalCount_ == 2) executions_[0] = _approvalExecution(0); + if (approvalCount_ != 0) executions_[swapIndex_ - 1] = _approvalExecution(TOKEN_IN_AMOUNT); + executions_[swapIndex_] = Execution({ + target: address(metaSwap), + value: 0, + callData: abi.encodeCall( + IMetaSwap.swap, + ("redeemer-route", IERC20(address(tokenIn)), TOKEN_IN_AMOUNT, abi.encode(IERC20(address(tokenOut)), outputAmount_)) + ) + }); + } + + function _nativeExecutions(uint256 outputAmount_) private view returns (Execution[] memory executions_) { + executions_ = new Execution[](1); + executions_[0] = Execution({ + target: address(metaSwap), + value: TOKEN_IN_AMOUNT, + callData: abi.encodeCall( + IMetaSwap.swap, + ("redeemer-route", IERC20(address(0)), TOKEN_IN_AMOUNT, abi.encode(IERC20(address(tokenOut)), outputAmount_)) + ) + }); + } + + function _approvalExecution(uint256 amount_) private view returns (Execution memory) { + return + Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(metaSwap), amount_)) + }); + } + + function _noneMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.None; + } + + function _skipApprovalMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.SkipApproval; + } + + function _approveMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.Approve; + } + + function _resetApproveMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.ResetApprove; + } +}