From 7995fa64457328bf50ff13d46412d084c4d3e5bc Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 2 Sep 2026 08:42:01 -0600 Subject: [PATCH 01/13] feat: add MetaSwap flexible settlement enforcer Authorize one open-route MetaSwap settlement with exact input constraints, signed approval flexibility, minimum output, and atomic one-shot consumption. --- documents/CaveatEnforcers.md | 70 ++ script/DeployCaveatEnforcers.s.sol | 4 + .../MetaSwapFlexibleSettlementEnforcer.sol | 241 ++++++ .../MetaSwapFlexibleSettlementEnforcer.t.sol | 737 ++++++++++++++++++ 4 files changed, 1052 insertions(+) create mode 100644 src/enforcers/MetaSwapFlexibleSettlementEnforcer.sol create mode 100644 test/enforcers/MetaSwapFlexibleSettlementEnforcer.t.sol diff --git a/documents/CaveatEnforcers.md b/documents/CaveatEnforcers.md index f7a57fb9..809ec149 100644 --- a/documents/CaveatEnforcers.md +++ b/documents/CaveatEnforcers.md @@ -25,6 +25,76 @@ Enforcers can target specific call type modes: **single** or **batch**, and exec --- +### MetaSwapFlexibleSettlementEnforcer + +Authorizes one successful MetaSwap settlement with a redeemer-selected route. It binds the input, approval behavior, +output asset, recipient, and minimum output while keeping route discovery flexible. A successful settlement is +permanently consumed; a reverted fill, including insufficient output, rolls back the consumed state and remains +retryable. + +It accepts one direct `BATCH_DEFAULT_MODE` redemption with: + +- Native input: `MetaSwap.swap{ value: tokenInAmount }(...)` +- ERC-20 skipping approval: `MetaSwap.swap(...)` +- ERC-20 approval: `approve(metaSwap, tokenInAmount)`, then `MetaSwap.swap(...)` +- ERC-20 reset approval: `approve(metaSwap, 0)`, `approve(metaSwap, tokenInAmount)`, then `MetaSwap.swap(...)` + +Terms are packed as: + +```text +metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) | +tokenOut(20) | recipient(20) | tokenOutMin(32) +``` + +`address(0)` represents the native token. The one-byte `ApprovalMode` enum selects exactly one execution shape: + +- `0`: `None`, required for native input +- `1`: `SkipApproval` +- `2`: `Approve` +- `3`: `ResetApprove` + +ERC-20 input requires modes `1` through `3`. The execution count must match the signed mode; caveat args are not used. +`SkipApproval` does not inspect allowance; the swap must have sufficient allowance to execute successfully. + +Example terms for an ERC-20 settlement requiring `approve(amount)`: + +```solidity +bytes memory terms = abi.encodePacked( + metaSwap, + tokenIn, + tokenInAmount, + uint8(MetaSwapFlexibleSettlementEnforcer.ApprovalMode.Approve), + tokenOut, + recipient, + tokenOutMin +); +``` + +The enforcer permanently records successful use in a boolean mapping and temporarily caches the recipient's raw +pre-execution output balance in a separate mapping. Both mappings are keyed by the DelegationManager and delegation hash. +The balance snapshot is deleted after validation for a storage refund. Any reverted settlement atomically rolls back the +consumed flag and remains retryable. + +Approval spender and swap input token arguments must use canonical 32-byte ABI address words, including zeroed upper +bytes. Swap calldata must be at least 196 bytes: the selector, four-word static head, and two dynamic length words required +by `swap(string,address,uint256,bytes)`. + +#### Trust Assumptions + +MetaSwap's `aggregatorId` and route `data` remain unrestricted. The delegator trusts the delegate to provide safe route +data and trusts the configured MetaSwap contract and its adapters. The enforcer fixes the input token, input amount, +approval spender, approval amounts, output token, output recipient, and minimum net balance increase, but it cannot +prevent arbitrary route side effects or protect unrelated assets already approved to MetaSwap or its adapters. + +The minimum output may be satisfied by any balance increase during the execution, including unrelated transfers or token +rebases. A malicious or non-standard output token may report misleading balances. A residual input allowance may remain +if MetaSwap spends less than the approved amount. + +Deployment uses `script/DeployCaveatEnforcers.s.sol`. After recording deployed addresses, verification uses the shared +`script/verification/verify-enforcer-contracts.sh` flow. + +--- + ## Enforcer Details ### NativeTokenPaymentEnforcer diff --git a/script/DeployCaveatEnforcers.s.sol b/script/DeployCaveatEnforcers.s.sol index 629da134..babcdb04 100644 --- a/script/DeployCaveatEnforcers.s.sol +++ b/script/DeployCaveatEnforcers.s.sol @@ -26,6 +26,7 @@ import { ExactExecutionEnforcer } from "../src/enforcers/ExactExecutionEnforcer. import { IdEnforcer } from "../src/enforcers/IdEnforcer.sol"; import { LimitedCallsEnforcer } from "../src/enforcers/LimitedCallsEnforcer.sol"; import { LogicalOrWrapperEnforcer } from "../src/enforcers/LogicalOrWrapperEnforcer.sol"; +import { MetaSwapFlexibleSettlementEnforcer } from "../src/enforcers/MetaSwapFlexibleSettlementEnforcer.sol"; import { MultiTokenPeriodEnforcer } from "../src/enforcers/MultiTokenPeriodEnforcer.sol"; import { NativeBalanceChangeEnforcer } from "../src/enforcers/NativeBalanceChangeEnforcer.sol"; import { NativeTokenPaymentEnforcer } from "../src/enforcers/NativeTokenPaymentEnforcer.sol"; @@ -133,6 +134,9 @@ contract DeployCaveatEnforcers is Script { deployedAddress = address(new LogicalOrWrapperEnforcer{ salt: salt }(delegationManager)); console2.log("LogicalOrWrapperEnforcer: %s", deployedAddress); + deployedAddress = address(new MetaSwapFlexibleSettlementEnforcer{ salt: salt }()); + console2.log("MetaSwapFlexibleSettlementEnforcer: %s", deployedAddress); + deployedAddress = address(new MultiTokenPeriodEnforcer{ salt: salt }()); console2.log("MultiTokenPeriodEnforcer: %s", deployedAddress); diff --git a/src/enforcers/MetaSwapFlexibleSettlementEnforcer.sol b/src/enforcers/MetaSwapFlexibleSettlementEnforcer.sol new file mode 100644 index 00000000..9d1806b8 --- /dev/null +++ b/src/enforcers/MetaSwapFlexibleSettlementEnforcer.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { IMetaSwap } from "../helpers/interfaces/IMetaSwap.sol"; +import { Execution, ModeCode } from "../utils/Types.sol"; + +/** + * @title MetaSwapFlexibleSettlementEnforcer + * @notice Authorizes one MetaSwap settlement with a redeemer-selected route, exact input, and minimum output. + * @dev The settlement combines batch validation, one-shot consumption, and output enforcement. It accepts: + * - Native: `[swap{ value: tokenInAmount }(...)]` + * - ERC-20 without approval: `[swap(...)]` + * - ERC-20 with approval: `[approve(metaSwap, tokenInAmount), swap(...)]` + * - ERC-20 with reset: `[approve(metaSwap, 0), approve(metaSwap, tokenInAmount), swap(...)]` + * + * The signed approval mode selects one exact ERC-20 shape. MetaSwap's dynamic `aggregatorId` and route `data` + * remain unrestricted. The configured MetaSwap contract and its adapters must therefore be trusted. + */ +contract MetaSwapFlexibleSettlementEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + + enum ApprovalMode { + None, + SkipApproval, + Approve, + ResetApprove + } + + struct Terms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + ApprovalMode approvalMode; + address tokenOut; + address recipient; + uint256 tokenOutMin; + } + + uint256 private constant TERMS_LENGTH = 145; + uint256 private constant APPROVE_CALL_LENGTH = 68; + // Selector + four-word head + two dynamic length words. + uint256 private constant SWAP_CALL_MIN_LENGTH = 196; + + /// @notice Records settlements that have already been used. + mapping(bytes32 settlementKey => bool isUsed) public consumedSettlements; + + /// @dev Caches the recipient's balance between the DelegationManager's before and after hooks. + mapping(bytes32 settlementKey => uint256 balanceBefore) private balanceSnapshots; + + /** + * @notice Emitted after a settlement satisfies its minimum output and is permanently consumed. + * @param delegationManager DelegationManager that redeemed the settlement. + * @param delegationHash Hash identifying the signed delegation. + * @param redeemer Address that submitted the redemption. + */ + event SettlementConsumed(address indexed delegationManager, bytes32 indexed delegationHash, address indexed redeemer); + + /** + * @notice Returns the storage key used to isolate a settlement. + * @param delegationManager_ DelegationManager that redeems the delegation. + * @param delegationHash_ Hash identifying the delegation. + */ + function getSettlementKey(address delegationManager_, bytes32 delegationHash_) external pure returns (bytes32) { + return _getSettlementKey(delegationManager_, delegationHash_); + } + + /** + * @notice Validates the batch, caches the output balance, and locks the settlement against reuse. + * @param terms_ Packed settlement constraints. + * @param mode_ Execution mode; must be batch/default. + * @param executionCallData_ ABI-encoded `Execution[]`. + * @param delegationHash_ Hash identifying the signed delegation. + */ + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32 delegationHash_, + address, + address + ) + public + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + Terms memory termsInfo_ = getTermsInfo(terms_); + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + _validateExecutions(executions_, termsInfo_); + + bytes32 settlementKey_ = _getSettlementKey(msg.sender, delegationHash_); + require(!consumedSettlements[settlementKey_], "MetaSwapFlexibleSettlementEnforcer:settlement-already-used"); + + consumedSettlements[settlementKey_] = true; + balanceSnapshots[settlementKey_] = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + } + + /** + * @notice Enforces the minimum output and permanently consumes the successful settlement. + * @param terms_ Packed settlement constraints. + * @param delegationHash_ Hash identifying the signed delegation. + * @param redeemer_ Address that submitted the redemption. + */ + function afterHook( + bytes calldata terms_, + bytes calldata, + ModeCode, + bytes calldata, + bytes32 delegationHash_, + address, + address redeemer_ + ) + public + override + { + require(terms_.length == TERMS_LENGTH, "MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + + bytes32 settlementKey_ = _getSettlementKey(msg.sender, delegationHash_); + address tokenOut_ = address(bytes20(terms_[73:93])); + address recipient_ = address(bytes20(terms_[93:113])); + uint256 tokenOutMin_ = uint256(bytes32(terms_[113:145])); + uint256 balanceBefore_ = balanceSnapshots[settlementKey_]; + delete balanceSnapshots[settlementKey_]; + + uint256 balanceAfter_ = _balanceOf(tokenOut_, recipient_); + + require( + balanceAfter_ >= balanceBefore_ && balanceAfter_ - balanceBefore_ >= tokenOutMin_, + "MetaSwapFlexibleSettlementEnforcer:insufficient-output" + ); + + emit SettlementConsumed(msg.sender, delegationHash_, redeemer_); + } + + /** + * @notice Decodes and validates signed settlement terms. + * @param terms_ Packed as + * `metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) | tokenOut(20) | recipient(20) | tokenOutMin(32)`. + */ + function getTermsInfo(bytes calldata terms_) public pure returns (Terms memory termsInfo_) { + require(terms_.length == TERMS_LENGTH, "MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + + termsInfo_.metaSwap = address(bytes20(terms_[0:20])); + termsInfo_.tokenIn = address(bytes20(terms_[20:40])); + termsInfo_.tokenInAmount = uint256(bytes32(terms_[40:72])); + uint8 approvalMode_ = uint8(terms_[72]); + termsInfo_.tokenOut = address(bytes20(terms_[73:93])); + termsInfo_.recipient = address(bytes20(terms_[93:113])); + termsInfo_.tokenOutMin = uint256(bytes32(terms_[113:145])); + + require( + termsInfo_.metaSwap != address(0) && termsInfo_.tokenInAmount != 0 && termsInfo_.recipient != address(0) + && termsInfo_.tokenOutMin != 0 && termsInfo_.tokenIn != termsInfo_.tokenOut, + "MetaSwapFlexibleSettlementEnforcer:invalid-terms" + ); + + require(approvalMode_ <= uint8(ApprovalMode.ResetApprove), "MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + termsInfo_.approvalMode = ApprovalMode(approvalMode_); + } + + function _validateExecutions(Execution[] calldata executions_, Terms memory termsInfo_) private pure { + ApprovalMode approvalMode_ = termsInfo_.approvalMode; + + if (termsInfo_.tokenIn == address(0)) { + require(approvalMode_ == ApprovalMode.None, "MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + require(executions_.length == 1, "MetaSwapFlexibleSettlementEnforcer:invalid-batch-length"); + _validateSwap(executions_[0], termsInfo_.metaSwap, address(0), termsInfo_.tokenInAmount, termsInfo_.tokenInAmount); + return; + } + + if (approvalMode_ == ApprovalMode.SkipApproval) { + require(executions_.length == 1, "MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _validateSwap(executions_[0], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, 0); + } else if (approvalMode_ == ApprovalMode.Approve) { + require(executions_.length == 2, "MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _validateApproval(executions_[0], termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + _validateSwap(executions_[1], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, 0); + } else if (approvalMode_ == ApprovalMode.ResetApprove) { + require(executions_.length == 3, "MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _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("MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + } + } + + 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("MetaSwapFlexibleSettlementEnforcer:invalid-approval"); + } + } + + 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("MetaSwapFlexibleSettlementEnforcer:invalid-swap"); + } + } + + function _balanceOf(address token_, address recipient_) private view returns (uint256) { + return token_ == address(0) ? recipient_.balance : IERC20(token_).balanceOf(recipient_); + } + + function _getSettlementKey(address delegationManager_, bytes32 delegationHash_) private pure returns (bytes32) { + return keccak256(abi.encode(delegationManager_, delegationHash_)); + } +} diff --git a/test/enforcers/MetaSwapFlexibleSettlementEnforcer.t.sol b/test/enforcers/MetaSwapFlexibleSettlementEnforcer.t.sol new file mode 100644 index 00000000..53bbe448 --- /dev/null +++ b/test/enforcers/MetaSwapFlexibleSettlementEnforcer.t.sol @@ -0,0 +1,737 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import { DelegationManager } from "../../src/DelegationManager.sol"; +import { MetaSwapFlexibleSettlementEnforcer } from "../../src/enforcers/MetaSwapFlexibleSettlementEnforcer.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; + +contract FlexibleSettlementMetaSwapMock is IMetaSwap { + using SafeERC20 for IERC20; + + receive() external payable { } + + function swap(string calldata, IERC20 tokenFrom_, uint256 amount_, bytes calldata data_) external payable { + (IERC20 tokenOut_, uint256 amountOut_) = abi.decode(data_, (IERC20, uint256)); + + if (address(tokenFrom_) == address(0)) { + require(msg.value == amount_, "FlexibleSettlementMetaSwapMock:invalid-native-value"); + } else { + require(msg.value == 0, "FlexibleSettlementMetaSwapMock:unexpected-native-value"); + tokenFrom_.safeTransferFrom(msg.sender, address(this), amount_); + } + + if (address(tokenOut_) == address(0)) { + (bool success_,) = msg.sender.call{ value: amountOut_ }(""); + require(success_, "FlexibleSettlementMetaSwapMock:native-transfer-failed"); + } else { + tokenOut_.safeTransfer(msg.sender, amountOut_); + } + } + + function setAdapter(string calldata, address, bytes4, bytes calldata) external { } + + function removeAdapter(string calldata) external { } + + function adapters(string memory) external pure returns (Adapter memory adapter_) { + adapter_ = Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); + } +} + +contract MetaSwapFlexibleSettlementEnforcerTest is CaveatEnforcerBaseTest { + uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; + uint256 internal constant TOKEN_OUT_MIN = 190 ether; + uint256 internal constant TOKEN_OUT_AMOUNT = 200 ether; + MetaSwapFlexibleSettlementEnforcer.ApprovalMode internal constant NONE = MetaSwapFlexibleSettlementEnforcer.ApprovalMode.None; + MetaSwapFlexibleSettlementEnforcer.ApprovalMode internal constant SKIP = + MetaSwapFlexibleSettlementEnforcer.ApprovalMode.SkipApproval; + MetaSwapFlexibleSettlementEnforcer.ApprovalMode internal constant APPROVE = + MetaSwapFlexibleSettlementEnforcer.ApprovalMode.Approve; + MetaSwapFlexibleSettlementEnforcer.ApprovalMode internal constant RESET = + MetaSwapFlexibleSettlementEnforcer.ApprovalMode.ResetApprove; + + MetaSwapFlexibleSettlementEnforcer internal enforcer; + BasicERC20 internal tokenIn; + BasicERC20 internal tokenOut; + FlexibleSettlementMetaSwapMock internal metaSwap; + address internal alice; + address internal relayer; + + event SettlementConsumed(address indexed delegationManager, bytes32 indexed delegationHash, address indexed redeemer); + + function setUp() public override { + super.setUp(); + + enforcer = new MetaSwapFlexibleSettlementEnforcer(); + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + metaSwap = new FlexibleSettlementMetaSwapMock(); + alice = address(users.alice.deleGator); + relayer = makeAddr("Relayer"); + + tokenIn.mint(alice, 1_000 ether); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(alice, 1_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + function test_getTermsInfoDecodesERC20Settlement() public { + MetaSwapFlexibleSettlementEnforcer.Terms memory info_ = + enforcer.getTermsInfo(_terms(address(tokenIn), APPROVE, address(tokenOut), alice)); + + assertEq(info_.metaSwap, address(metaSwap)); + assertEq(info_.tokenIn, address(tokenIn)); + assertEq(info_.tokenInAmount, TOKEN_IN_AMOUNT); + assertEq(uint8(info_.approvalMode), uint8(APPROVE)); + assertEq(info_.tokenOut, address(tokenOut)); + assertEq(info_.recipient, alice); + assertEq(info_.tokenOutMin, TOKEN_OUT_MIN); + } + + function test_getTermsInfoDecodesNativeInputSettlement() public { + MetaSwapFlexibleSettlementEnforcer.Terms memory info_ = + enforcer.getTermsInfo(_terms(address(0), NONE, address(tokenOut), alice)); + + assertEq(info_.tokenIn, address(0)); + assertEq(uint8(info_.approvalMode), uint8(NONE)); + } + + function test_getSettlementKeyUsesDelegationManagerAndDelegationHash() public { + bytes32 delegationHash_ = keccak256("delegation"); + assertEq( + enforcer.getSettlementKey(address(delegationManager), delegationHash_), + keccak256(abi.encode(address(delegationManager), delegationHash_)) + ); + } + + function test_acceptsFlexibleAggregatorAndRouteData() public { + _before( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "a", hex"01"), + keccak256("first") + ); + _before( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "different-aggregator", new bytes(512)), + keccak256("second") + ); + } + + function test_acceptsMinimumLengthSwapCalldata() public { + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[1].callData = _minimumSwapCalldata(bytes32(uint256(uint160(address(tokenIn))))); + + assertEq(executions_[1].callData.length, 196); + _before(_terms(address(tokenIn), APPROVE, address(tokenOut), alice), executions_, keccak256("minimum-calldata")); + } + + function test_acceptsEachExactERC20ApprovalMode() public { + _before( + _terms(address(tokenIn), SKIP, address(tokenOut), alice), + _erc20Executions(0, address(tokenIn), TOKEN_IN_AMOUNT, "skip", hex""), + keccak256("skip") + ); + _before( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "approve", hex""), + keccak256("approve") + ); + _before( + _terms(address(tokenIn), RESET, address(tokenOut), alice), + _erc20Executions(2, address(tokenIn), TOKEN_IN_AMOUNT, "reset", hex""), + keccak256("reset") + ); + } + + function testFuzz_enforcesExactERC20ApprovalMode(uint8 rawMode_, uint8 approvalCount_) public { + rawMode_ = uint8(bound(rawMode_, uint8(SKIP), uint8(RESET))); + approvalCount_ = uint8(bound(approvalCount_, 0, 2)); + MetaSwapFlexibleSettlementEnforcer.ApprovalMode approvalMode_ = MetaSwapFlexibleSettlementEnforcer.ApprovalMode(rawMode_); + + bool validShape_ = (approvalMode_ == SKIP && approvalCount_ == 0) || (approvalMode_ == APPROVE && approvalCount_ == 1) + || (approvalMode_ == RESET && approvalCount_ == 2); + + if (!validShape_) { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + } + + _before( + _terms(address(tokenIn), approvalMode_, address(tokenOut), alice), + _erc20Executions(approvalCount_, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""), + keccak256(abi.encode(rawMode_, approvalCount_)) + ); + } + + function testFuzz_nativeInputOnlyAcceptsNone(uint8 rawMode_) public { + rawMode_ = uint8(bound(rawMode_, uint8(NONE), uint8(RESET))); + MetaSwapFlexibleSettlementEnforcer.ApprovalMode approvalMode_ = MetaSwapFlexibleSettlementEnforcer.ApprovalMode(rawMode_); + + if (approvalMode_ != NONE) { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + } + + _before( + _terms(address(0), approvalMode_, address(tokenOut), alice), + _nativeExecutions(TOKEN_IN_AMOUNT, address(tokenOut), TOKEN_OUT_AMOUNT), + keccak256(abi.encode(rawMode_)) + ); + } + + function testFuzz_revertsForUndefinedApprovalMode(uint8 rawMode_) public { + rawMode_ = uint8(bound(rawMode_, uint8(RESET) + 1, type(uint8).max)); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + enforcer.getTermsInfo( + _rawTerms(address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, rawMode_, address(tokenOut), alice, TOKEN_OUT_MIN) + ); + } + + function test_acceptsNativeInputShape() public { + _before( + _terms(address(0), NONE, address(tokenOut), alice), + _nativeExecutions(TOKEN_IN_AMOUNT, address(tokenOut), TOKEN_OUT_AMOUNT), + keccak256("native") + ); + } + + function test_revertsForSingleCallMode() public { + vm.expectRevert("CaveatEnforcer:invalid-call-type"); + enforcer.beforeHook( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + hex"", + singleDefaultMode, + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex"")), + bytes32(0), + alice, + relayer + ); + } + + function test_revertsForTryExecutionMode() public { + vm.expectRevert("CaveatEnforcer:invalid-execution-type"); + enforcer.beforeHook( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + hex"", + batchTryMode, + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex"")), + bytes32(0), + alice, + relayer + ); + } + + function test_revertsForInvalidTermsLength() public { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.getTermsInfo(new bytes(144)); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.getTermsInfo(new bytes(146)); + } + + function test_revertsForInvalidRequiredTerms() public { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.getTermsInfo( + _rawTerms(address(0), address(tokenIn), TOKEN_IN_AMOUNT, uint8(APPROVE), address(tokenOut), alice, TOKEN_OUT_MIN) + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.getTermsInfo( + _rawTerms(address(metaSwap), address(tokenIn), 0, uint8(APPROVE), address(tokenOut), alice, TOKEN_OUT_MIN) + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.getTermsInfo( + _rawTerms( + address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, uint8(APPROVE), address(tokenOut), address(0), TOKEN_OUT_MIN + ) + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.getTermsInfo( + _rawTerms(address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, uint8(APPROVE), address(tokenOut), alice, 0) + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.getTermsInfo( + _rawTerms(address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, uint8(APPROVE), address(tokenIn), alice, TOKEN_OUT_MIN) + ); + } + + function test_revertsForInvalidApprovalMode() public { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + _before( + _terms(address(0), APPROVE, address(tokenOut), alice), + _nativeExecutions(TOKEN_IN_AMOUNT, address(tokenOut), TOKEN_OUT_AMOUNT), + keccak256("native-approval-mode") + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + _before( + _terms(address(tokenIn), NONE, address(tokenOut), alice), + _erc20Executions(0, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""), + keccak256("erc20-none-mode") + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-approval-mode"); + enforcer.getTermsInfo( + _rawTerms(address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, 4, address(tokenOut), alice, TOKEN_OUT_MIN) + ); + } + + function test_revertsWhenApprovalShapeIsNotSigned() public { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _before( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + _erc20Executions(0, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""), + bytes32(0) + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _before( + _terms(address(tokenIn), SKIP, address(tokenOut), alice), + _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""), + bytes32(0) + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _before( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + _erc20Executions(2, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""), + bytes32(0) + ); + } + + function test_revertsForUnsupportedBatchLengths() public { + Execution[] memory empty_ = new Execution[](0); + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _before(_terms(address(tokenIn), SKIP, address(tokenOut), alice), empty_, bytes32(0)); + + Execution[] memory tooLong_ = new Execution[](4); + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:approval-shape-not-allowed"); + _before(_terms(address(tokenIn), RESET, address(tokenOut), alice), tooLong_, bytes32(0)); + + Execution[] memory nativeTooLong_ = new Execution[](2); + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-batch-length"); + _before(_terms(address(0), NONE, address(tokenOut), alice), nativeTooLong_, bytes32(0)); + } + + function test_revertsForInvalidApproval() public { + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + + executions_[0].target = makeAddr("OtherToken"); + _expectInvalidApproval(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[0].value = 1; + _expectInvalidApproval(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[0].callData = abi.encodePacked(IERC20.approve.selector); + _expectInvalidApproval(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(metaSwap), TOKEN_IN_AMOUNT)); + _expectInvalidApproval(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[0].callData = abi.encodeCall(IERC20.approve, (makeAddr("OtherSpender"), TOKEN_IN_AMOUNT)); + _expectInvalidApproval(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[0].callData = abi.encodePacked( + IERC20.approve.selector, bytes32(uint256(uint160(address(metaSwap))) | (uint256(1) << 255)), bytes32(TOKEN_IN_AMOUNT) + ); + _expectInvalidApproval(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT - 1)); + _expectInvalidApproval(executions_); + } + + function test_revertsForInvalidResetApproval() public { + Execution[] memory executions_ = _erc20Executions(2, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), 1)); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-approval"); + _before(_terms(address(tokenIn), RESET, address(tokenOut), alice), executions_, bytes32(0)); + } + + function test_revertsForInvalidSwap() public { + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[1].target = makeAddr("OtherSwap"); + _expectInvalidSwap(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[1].value = 1; + _expectInvalidSwap(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[1].callData = abi.encodePacked(IMetaSwap.swap.selector); + _expectInvalidSwap(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[1].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT)); + _expectInvalidSwap(executions_); + + executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + executions_[1].callData = _minimumSwapCalldata(bytes32(uint256(uint160(address(tokenIn))) | (uint256(1) << 255))); + _expectInvalidSwap(executions_); + + _expectInvalidSwap(_erc20Executions(1, makeAddr("OtherToken"), TOKEN_IN_AMOUNT, "route", hex"")); + _expectInvalidSwap(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT - 1, "route", hex"")); + } + + function test_revertsForStructurallyIncompleteSwapCalldata() public { + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + 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_); + } + + function test_revertsForNativeSwapWithWrongValue() public { + Execution[] memory executions_ = _nativeExecutions(TOKEN_IN_AMOUNT - 1, address(tokenOut), TOKEN_OUT_AMOUNT); + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-swap"); + _before(_terms(address(0), NONE, address(tokenOut), alice), executions_, bytes32(0)); + } + + function test_beforeHookMarksSettlementUsedAndRejectsReuse() public { + tokenOut.mint(alice, 10); + bytes32 delegationHash_ = keccak256("settlement"); + bytes32 settlementKey_ = enforcer.getSettlementKey(address(delegationManager), delegationHash_); + vm.prank(address(delegationManager)); + enforcer.beforeHook( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + hex"", + batchDefaultMode, + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex"")), + delegationHash_, + alice, + relayer + ); + + assertTrue(enforcer.consumedSettlements(settlementKey_)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:settlement-already-used"); + enforcer.beforeHook( + _terms(address(tokenIn), APPROVE, address(tokenOut), alice), + hex"", + batchDefaultMode, + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex"")), + delegationHash_, + alice, + relayer + ); + } + + function test_identicalDelegationHashIsIsolatedAcrossDelegationManagers() public { + DelegationManager secondDelegationManager_ = new DelegationManager(address(this)); + bytes32 delegationHash_ = keccak256("shared-delegation-hash"); + bytes memory terms_ = _terms(address(tokenIn), APPROVE, address(tokenOut), alice); + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""); + + _beforeAs(address(delegationManager), terms_, executions_, delegationHash_); + _beforeAs(address(secondDelegationManager_), terms_, executions_, delegationHash_); + + bytes32 firstKey_ = enforcer.getSettlementKey(address(delegationManager), delegationHash_); + bytes32 secondKey_ = enforcer.getSettlementKey(address(secondDelegationManager_), delegationHash_); + assertNotEq(firstKey_, secondKey_); + assertTrue(enforcer.consumedSettlements(firstKey_)); + assertTrue(enforcer.consumedSettlements(secondKey_)); + } + + function test_afterHookRevertsForInvalidTermsLength() public { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-terms"); + enforcer.afterHook(new bytes(144), hex"", batchDefaultMode, hex"", bytes32(0), alice, relayer); + } + + function test_afterHookConsumesSettlementAndEmitsEvent() public { + bytes32 delegationHash_ = keccak256("successful-settlement"); + bytes memory terms_ = _terms(address(tokenIn), APPROVE, address(tokenOut), alice); + _before(terms_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""), delegationHash_); + tokenOut.mint(alice, TOKEN_OUT_MIN); + + vm.prank(address(delegationManager)); + vm.expectEmit(true, true, true, true, address(enforcer)); + emit SettlementConsumed(address(delegationManager), delegationHash_, relayer); + enforcer.afterHook(terms_, hex"", batchDefaultMode, hex"", delegationHash_, alice, relayer); + + assertTrue(enforcer.consumedSettlements(enforcer.getSettlementKey(address(delegationManager), delegationHash_))); + } + + function test_afterHookRevertsForInsufficientOutput() public { + bytes32 delegationHash_ = keccak256("insufficient-settlement"); + bytes memory terms_ = _terms(address(tokenIn), APPROVE, address(tokenOut), alice); + _before(terms_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, "route", hex""), delegationHash_); + tokenOut.mint(alice, TOKEN_OUT_MIN - 1); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:insufficient-output"); + enforcer.afterHook(terms_, hex"", batchDefaultMode, hex"", delegationHash_, alice, relayer); + } + + function test_redeemsERC20ApprovalSettlement() public { + _redeem( + _sign(_terms(address(tokenIn), APPROVE, address(tokenOut), alice)), + _erc20Executions( + 1, address(tokenIn), TOKEN_IN_AMOUNT, "best-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_AMOUNT) + ) + ); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_AMOUNT); + } + + function test_redeemsERC20ResetApprovalSettlement() public { + vm.prank(alice); + tokenIn.approve(address(metaSwap), 1); + + _redeem( + _sign(_terms(address(tokenIn), RESET, address(tokenOut), alice)), + _erc20Executions( + 2, address(tokenIn), TOKEN_IN_AMOUNT, "best-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_AMOUNT) + ) + ); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenIn.allowance(alice, address(metaSwap)), 0); + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_AMOUNT); + } + + function test_redeemsERC20SettlementSkippingApproval() public { + vm.prank(alice); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + _redeem( + _sign(_terms(address(tokenIn), SKIP, address(tokenOut), alice)), + _erc20Executions( + 0, address(tokenIn), TOKEN_IN_AMOUNT, "best-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_AMOUNT) + ) + ); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_AMOUNT); + } + + function test_redeemsNativeInputSettlement() public { + uint256 nativeBefore_ = alice.balance; + _redeem( + _sign(_terms(address(0), NONE, address(tokenOut), alice)), + _nativeExecutions(TOKEN_IN_AMOUNT, address(tokenOut), TOKEN_OUT_AMOUNT) + ); + + assertEq(alice.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_AMOUNT); + } + + function test_redeemsERC20ForNativeOutput() public { + uint256 nativeBefore_ = alice.balance; + _redeem( + _sign(_terms(address(tokenIn), APPROVE, address(0), alice)), + _erc20Executions( + 1, address(tokenIn), TOKEN_IN_AMOUNT, "native-output", abi.encode(IERC20(address(0)), TOKEN_OUT_AMOUNT) + ) + ); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(alice.balance, nativeBefore_ + TOKEN_OUT_AMOUNT); + } + + function test_revertsAtomicallyForInsufficientOutputAndAllowsRetry() public { + bytes memory terms_ = _terms(address(tokenIn), APPROVE, address(tokenOut), alice); + Delegation memory delegation_ = _sign(terms_); + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + Execution[] memory insufficient_ = _erc20Executions( + 1, address(tokenIn), TOKEN_IN_AMOUNT, "bad-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_MIN - 1) + ); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:insufficient-output"); + _redeem(delegation_, insufficient_); + + assertEq(tokenIn.balanceOf(alice), 1_000 ether); + assertEq(tokenOut.balanceOf(alice), 0); + assertFalse(enforcer.consumedSettlements(enforcer.getSettlementKey(address(delegationManager), delegationHash_))); + + _redeem( + delegation_, + _erc20Executions( + 1, address(tokenIn), TOKEN_IN_AMOUNT, "new-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_MIN) + ) + ); + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); + } + + function test_successfulSettlementCannotBeRedeemedAgain() public { + Delegation memory delegation_ = _sign(_terms(address(tokenIn), APPROVE, address(tokenOut), alice)); + Execution[] memory executions_ = _erc20Executions( + 1, address(tokenIn), TOKEN_IN_AMOUNT, "best-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_AMOUNT) + ); + _redeem(delegation_, executions_); + + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:settlement-already-used"); + _redeem(delegation_, executions_); + } + + function _expectInvalidApproval(Execution[] memory executions_) private { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-approval"); + _before(_terms(address(tokenIn), APPROVE, address(tokenOut), alice), executions_, bytes32(0)); + } + + function _expectInvalidSwap(Execution[] memory executions_) private { + vm.expectRevert("MetaSwapFlexibleSettlementEnforcer:invalid-swap"); + _before(_terms(address(tokenIn), APPROVE, address(tokenOut), alice), executions_, bytes32(0)); + } + + function _terms( + address tokenIn_, + MetaSwapFlexibleSettlementEnforcer.ApprovalMode approvalMode_, + address tokenOut_, + address recipient_ + ) + private + view + returns (bytes memory) + { + return _rawTerms(address(metaSwap), tokenIn_, TOKEN_IN_AMOUNT, uint8(approvalMode_), tokenOut_, recipient_, TOKEN_OUT_MIN); + } + + function _rawTerms( + address metaSwap_, + address tokenIn_, + uint256 tokenInAmount_, + uint8 approvalMode_, + address tokenOut_, + address recipient_, + uint256 tokenOutMin_ + ) + private + pure + returns (bytes memory) + { + return abi.encodePacked(metaSwap_, tokenIn_, tokenInAmount_, approvalMode_, tokenOut_, recipient_, tokenOutMin_); + } + + function _nativeExecutions( + uint256 value_, + address outputToken_, + uint256 outputAmount_ + ) + private + view + returns (Execution[] memory executions_) + { + executions_ = new Execution[](1); + executions_[0] = + _swapExecution(address(0), TOKEN_IN_AMOUNT, value_, "native-route", abi.encode(IERC20(outputToken_), outputAmount_)); + } + + function _erc20Executions( + uint8 shape_, + address swapToken_, + uint256 swapAmount_, + string memory aggregatorId_, + bytes memory routeData_ + ) + private + view + returns (Execution[] memory executions_) + { + uint256 swapIndex_ = shape_; + executions_ = new Execution[](swapIndex_ + 1); + if (shape_ == 2) executions_[0] = _approvalExecution(0); + if (shape_ != 0) executions_[swapIndex_ - 1] = _approvalExecution(TOKEN_IN_AMOUNT); + executions_[swapIndex_] = _swapExecution(swapToken_, swapAmount_, 0, aggregatorId_, routeData_); + } + + 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 _swapExecution( + address swapToken_, + uint256 swapAmount_, + uint256 value_, + string memory aggregatorId_, + bytes memory routeData_ + ) + private + view + returns (Execution memory) + { + return Execution({ + target: address(metaSwap), + value: value_, + callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(swapToken_), swapAmount_, routeData_)) + }); + } + + 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 _before(bytes memory terms_, Execution[] memory executions_, bytes32 delegationHash_) private { + _beforeAs(address(delegationManager), terms_, executions_, delegationHash_); + } + + function _beforeAs( + address delegationManager_, + bytes memory terms_, + Execution[] memory executions_, + bytes32 delegationHash_ + ) + private + { + vm.prank(delegationManager_); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), delegationHash_, alice, relayer); + } + + function _sign(bytes memory terms_) private view returns (Delegation memory delegation_) { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(enforcer), terms: terms_, args: hex"" }); + delegation_ = Delegation({ + delegate: ANY_DELEGATE, delegator: alice, authority: ROOT_AUTHORITY, caveats: caveats_, salt: 0, signature: hex"" + }); + delegation_ = signDelegation(users.alice, delegation_); + } + + function _redeem(Delegation memory delegation_, Execution[] memory executions_) private { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = ExecutionLib.encodeBatch(executions_); + + vm.prank(relayer); + delegationManager.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); + } + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(enforcer)); + } +} From c1df161216c79823e7391fc54362dbc5ff764b8f Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Mon, 7 Sep 2026 19:00:01 +0200 Subject: [PATCH 02/13] feat: add specialized MetaSwap delegation managers --- .../MetaSwapSpecializedDelegationManagers.md | 53 ++ src/MetaSwapDelegationManagerBase.sol | 218 +++++++ ...aSwapExecutionBuilderDelegationManager.sol | 92 +++ src/MetaSwapHooklessDelegationManager.sol | 107 +++ ...etaSwapSpecializedDelegationManagers.t.sol | 612 ++++++++++++++++++ 5 files changed, 1082 insertions(+) create mode 100644 documents/MetaSwapSpecializedDelegationManagers.md create mode 100644 src/MetaSwapDelegationManagerBase.sol create mode 100644 src/MetaSwapExecutionBuilderDelegationManager.sol create mode 100644 src/MetaSwapHooklessDelegationManager.sol create mode 100644 test/MetaSwapSpecializedDelegationManagers.t.sol diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md new file mode 100644 index 00000000..36dd6785 --- /dev/null +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -0,0 +1,53 @@ +# MetaSwap Specialized Delegation Managers + +These experimental managers preserve the existing three-array `redeemDelegations` entrypoint but intentionally support +only one batch/default settlement, one root delegation, and one caveat. The caveat's enforcer address must be the manager +itself because settlement enforcement is internal and no caveat hooks are called. + +## MetaSwapHooklessDelegationManager + +The redeemer supplies a complete ABI-encoded `Execution[]`. The manager validates the exact approval and swap shape, +records the delegation hash as consumed, snapshots the recipient's output balance in memory, calls the delegator's +`executeFromExecutor`, and checks the minimum output. + +## MetaSwapExecutionBuilderDelegationManager + +The redeemer supplies only: + +```solidity +abi.encode(aggregatorId, routeData) +``` + +The manager constructs the signed `SkipApproval`, `Approve`, `ResetApprove`, or native-input execution shape. This makes +approval and swap targets, selectors, values, ordering, and amounts impossible for the redeemer to alter. + +## Signature modes + +- `DirectECDSA` recovers the EIP-712 signer directly and requires it to equal the EIP-7702 delegator address. It bypasses + the account's ERC-1271 policy and must only be used with EIP-7702 EOAs controlled by that key. +- `ERC1271` calls the delegator's configured signature validation policy and supports broader account types. + +The manager's disabled-delegation mapping also acts as permanent one-shot state. A successful delegation cannot be +re-enabled. Failed execution or insufficient output reverts the state update atomically. + +## Initial gas comparison + +Measured around `redeemDelegations` for an ERC-20 `approve(amount) + swap` using EIP-7702 accounts: + +- Standard DelegationManager plus MetaSwap settlement enforcer: `200,781` +- Hookless manager with ERC-1271: `168,404` — `32,377` lower (`16.1%`) +- Hookless manager with direct ECDSA: `166,465` — `34,316` lower (`17.1%`) +- Execution-builder manager with direct ECDSA: `168,631` — `32,150` lower (`16.0%`) + +Direct ECDSA saved `1,939` gas over ERC-1271. Constructing executions added `2,166` execution gas relative to validating +redeemer-provided calldata in this prototype; its benefit is stronger authorization and smaller transaction input rather +than lower EVM execution gas. + +## Limitations + +- Delegation chains, multiple caveats, multiple redemption batches, self-authorized empty contexts, try mode, and generic + enforcers are intentionally unsupported. +- The execution-builder manager reinterprets `_executionCallDatas[0]` as route context rather than `Execution[]`. +- `enableDelegation`, pause controls, and generic manager administration are intentionally absent. +- Flexible MetaSwap route data retains the same trusted-delegate and unrelated-balance-increase assumptions as the + settlement enforcer. diff --git a/src/MetaSwapDelegationManagerBase.sol b/src/MetaSwapDelegationManagerBase.sol new file mode 100644 index 00000000..87d6d7c8 --- /dev/null +++ b/src/MetaSwapDelegationManagerBase.sol @@ -0,0 +1,218 @@ +// 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 { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.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 Shared validation and settlement logic for specialized MetaSwap delegation managers. + * @dev Supports exactly one root delegation containing one manager-enforced settlement caveat. + */ +abstract contract MetaSwapDelegationManagerBase is EIP712 { + enum SignatureMode { + DirectECDSA, + ERC1271 + } + + enum ApprovalMode { + None, + SkipApproval, + Approve, + ResetApprove + } + + struct Terms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + ApprovalMode approvalMode; + address tokenOut; + address recipient; + uint256 tokenOutMin; + } + + string public constant DOMAIN_VERSION = "1"; + bytes32 public constant ROOT_AUTHORITY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + address public constant ANY_DELEGATE = address(0xa11); + + uint256 internal constant TERMS_LENGTH = 145; + + SignatureMode public immutable signatureMode; + + /// @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 + ); + event RedeemedDelegation(address indexed rootDelegator, address indexed redeemer, Delegation delegation); + + error AlreadyDisabled(); + error BatchDataLengthMismatch(); + error CannotUseADisabledDelegation(); + error InsufficientOutput(); + error InvalidApprovalMode(); + error InvalidAuthority(); + error InvalidCaveat(); + error InvalidDelegate(); + error InvalidDelegator(); + error InvalidEOASignature(); + error InvalidERC1271Signature(); + error InvalidMode(); + error InvalidPermissionContext(); + error InvalidTerms(); + + constructor(string memory name_, SignatureMode signatureMode_) EIP712(name_, DOMAIN_VERSION) { + signatureMode = signatureMode_; + } + + /** + * @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 settlement delegation. + * @param permissionContexts_ Must contain one ABI-encoded one-element `Delegation[]`. + * @param modes_ Must contain the canonical batch/default mode. + * @param executionContexts_ Manager-specific execution or route 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(ModeLib.encodeSimpleBatch())) 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(); + + Terms memory termsInfo_ = getTermsInfo(delegation_.caveats[0].terms); + _validateSignature(delegation_, delegationHash_); + + disabledDelegations[delegationHash_] = true; + uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + + _executeSettlement(delegation_.delegator, executionContexts_[0], termsInfo_); + + uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { + revert InsufficientOutput(); + } + + emit RedeemedDelegation(delegation_.delegator, msg.sender, delegation_); + } + + /** + * @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 Decodes and validates packed settlement terms. + * @param terms_ Packed settlement terms. + */ + function getTermsInfo(bytes memory terms_) public pure returns (Terms memory termsInfo_) { + if (terms_.length != TERMS_LENGTH) revert InvalidTerms(); + + // Terms are tightly packed. Loading their fixed offsets directly avoids allocating seven temporary byte arrays. + assembly ("memory-safe") { + let termsData_ := add(terms_, 0x20) + 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))) + } + uint8 approvalMode_ = uint8(terms_[72]); + + 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_); + } + + function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal virtual; + + function _validateSignature(Delegation memory delegation_, bytes32 delegationHash_) private view { + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), delegationHash_); + + if (signatureMode == SignatureMode.DirectECDSA) { + if (ECDSA.recover(typedDataHash_, delegation_.signature) != delegation_.delegator) { + revert InvalidEOASignature(); + } + } else { + bytes4 result_ = IERC1271(delegation_.delegator).isValidSignature(typedDataHash_, delegation_.signature); + if (result_ != ERC1271Lib.EIP1271_MAGIC_VALUE) revert InvalidERC1271Signature(); + } + } + + function _getSingleCaveatDelegationHash(Delegation memory delegation_) private 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_) private view returns (uint256) { + return token_ == address(0) ? recipient_.balance : IERC20(token_).balanceOf(recipient_); + } +} diff --git a/src/MetaSwapExecutionBuilderDelegationManager.sol b/src/MetaSwapExecutionBuilderDelegationManager.sol new file mode 100644 index 00000000..b4afb277 --- /dev/null +++ b/src/MetaSwapExecutionBuilderDelegationManager.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.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 MetaSwapExecutionBuilderDelegationManager + * @notice Constructs and executes one signed MetaSwap settlement from redeemer-supplied route data. + * @dev Approval and swap targets, amounts, ordering, selectors, and values are created by this manager. + */ +contract MetaSwapExecutionBuilderDelegationManager is MetaSwapDelegationManagerBase { + using ExecutionLib for Execution[]; + + string public constant NAME = "MetaSwapExecutionBuilderDelegationManager"; + + constructor(SignatureMode signatureMode_) MetaSwapDelegationManagerBase(NAME, signatureMode_) { } + + function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { + (string memory aggregatorId_, bytes memory routeData_) = abi.decode(executionContext_, (string, bytes)); + Execution[] memory executions_ = _buildExecutions(termsInfo_, aggregatorId_, routeData_); + + IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executions_.encodeBatch()); + } + + function _buildExecutions( + Terms memory termsInfo_, + string memory aggregatorId_, + bytes memory routeData_ + ) + private + pure + returns (Execution[] memory executions_) + { + ApprovalMode approvalMode_ = termsInfo_.approvalMode; + + if (termsInfo_.tokenIn == address(0)) { + if (approvalMode_ != ApprovalMode.None) revert InvalidApprovalMode(); + + executions_ = new Execution[](1); + executions_[0] = _swapExecution(termsInfo_, termsInfo_.tokenInAmount, aggregatorId_, routeData_); + return executions_; + } + + uint256 swapIndex_; + if (approvalMode_ == ApprovalMode.SkipApproval) { + executions_ = new Execution[](1); + } else if (approvalMode_ == ApprovalMode.Approve) { + executions_ = new Execution[](2); + executions_[0] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + swapIndex_ = 1; + } else if (approvalMode_ == ApprovalMode.ResetApprove) { + executions_ = new Execution[](3); + executions_[0] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, 0); + executions_[1] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + swapIndex_ = 2; + } else { + revert InvalidApprovalMode(); + } + + executions_[swapIndex_] = _swapExecution(termsInfo_, 0, aggregatorId_, routeData_); + } + + function _approvalExecution(address tokenIn_, address metaSwap_, uint256 amount_) private pure returns (Execution memory) { + return Execution({ target: tokenIn_, value: 0, callData: abi.encodeCall(IERC20.approve, (metaSwap_, amount_)) }); + } + + function _swapExecution( + Terms memory termsInfo_, + uint256 value_, + string memory aggregatorId_, + bytes memory routeData_ + ) + private + pure + returns (Execution memory) + { + return Execution({ + target: termsInfo_.metaSwap, + value: value_, + callData: abi.encodeCall( + IMetaSwap.swap, (aggregatorId_, IERC20(termsInfo_.tokenIn), termsInfo_.tokenInAmount, routeData_) + ) + }); + } +} diff --git a/src/MetaSwapHooklessDelegationManager.sol b/src/MetaSwapHooklessDelegationManager.sol new file mode 100644 index 00000000..e0bdebef --- /dev/null +++ b/src/MetaSwapHooklessDelegationManager.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.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 MetaSwapHooklessDelegationManager + * @notice Executes one signed MetaSwap settlement without invoking external caveat hooks. + * @dev The redeemer supplies a complete batch, which is validated directly by this manager. + */ +contract MetaSwapHooklessDelegationManager is MetaSwapDelegationManagerBase { + using ExecutionLib for bytes; + + string public constant NAME = "MetaSwapHooklessDelegationManager"; + + uint256 private constant APPROVE_CALL_LENGTH = 68; + uint256 private constant SWAP_CALL_MIN_LENGTH = 196; + + error ApprovalShapeNotAllowed(); + error InvalidApproval(); + error InvalidBatchLength(); + error InvalidSwap(); + + constructor(SignatureMode signatureMode_) MetaSwapDelegationManagerBase(NAME, signatureMode_) { } + + function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { + Execution[] calldata executions_ = executionContext_.decodeBatch(); + _validateExecutions(executions_, termsInfo_); + + IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); + } + + function _validateExecutions(Execution[] calldata executions_, Terms 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/MetaSwapSpecializedDelegationManagers.t.sol b/test/MetaSwapSpecializedDelegationManagers.t.sol new file mode 100644 index 00000000..8494b378 --- /dev/null +++ b/test/MetaSwapSpecializedDelegationManagers.t.sol @@ -0,0 +1,612 @@ +// 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 { MetaSwapExecutionBuilderDelegationManager } from "../src/MetaSwapExecutionBuilderDelegationManager.sol"; +import { MetaSwapHooklessDelegationManager } from "../src/MetaSwapHooklessDelegationManager.sol"; +import { DelegationManager } from "../src/DelegationManager.sol"; +import { EIP7702StatelessDeleGator } from "../src/EIP7702/EIP7702StatelessDeleGator.sol"; +import { MetaSwapFlexibleSettlementEnforcer } from "../src/enforcers/MetaSwapFlexibleSettlementEnforcer.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"; + +contract SpecializedManagerMetaSwapMock 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 MetaSwapSpecializedDelegationManagersTest 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 STANDARD_KEY = 0x5151; + uint256 private constant HOOKLESS_KEY = 0xA11CE; + uint256 private constant HOOKLESS_1271_KEY = 0x1271; + uint256 private constant BUILDER_KEY = 0xB0B; + + EntryPoint private entryPoint; + SpecializedManagerMetaSwapMock private metaSwap; + BasicERC20 private tokenIn; + BasicERC20 private tokenOut; + DelegationManager private standardManager; + MetaSwapFlexibleSettlementEnforcer private standardEnforcer; + MetaSwapHooklessDelegationManager private hooklessManager; + MetaSwapHooklessDelegationManager private hookless1271Manager; + MetaSwapExecutionBuilderDelegationManager private builderManager; + address private standardAccount; + address private hooklessAccount; + address private hookless1271Account; + address private builderAccount; + address private relayer; + + function setUp() public { + entryPoint = new EntryPoint(); + metaSwap = new SpecializedManagerMetaSwapMock(); + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + relayer = makeAddr("Relayer"); + + standardManager = new DelegationManager(address(this)); + standardEnforcer = new MetaSwapFlexibleSettlementEnforcer(); + hooklessManager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + hookless1271Manager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.ERC1271); + builderManager = new MetaSwapExecutionBuilderDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + + standardAccount = vm.addr(STANDARD_KEY); + hooklessAccount = vm.addr(HOOKLESS_KEY); + hookless1271Account = vm.addr(HOOKLESS_1271_KEY); + builderAccount = vm.addr(BUILDER_KEY); + _installDeleGator(standardAccount, address(standardManager)); + _installDeleGator(hooklessAccount, address(hooklessManager)); + _installDeleGator(hookless1271Account, address(hookless1271Manager)); + _installDeleGator(builderAccount, address(builderManager)); + + tokenIn.mint(standardAccount, 1_000 ether); + tokenIn.mint(hooklessAccount, 1_000 ether); + tokenIn.mint(hookless1271Account, 1_000 ether); + tokenIn.mint(builderAccount, 1_000 ether); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(standardAccount, 1_000 ether); + vm.deal(hooklessAccount, 1_000 ether); + vm.deal(hookless1271Account, 1_000 ether); + vm.deal(builderAccount, 1_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + function test_hooklessManagerRedeemsValidatedExecutionBatch() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 1); + + _redeemHookless(delegation_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenIn.balanceOf(hooklessAccount), 900 ether); + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + assertTrue(hooklessManager.disabledDelegations(hooklessManager.getDelegationHash(delegation_))); + } + + function test_hooklessManagerRedeemsSkipApprovalExecution() public { + vm.prank(hooklessAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + bytes memory terms_ = _terms(address(tokenIn), _skipApprovalMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 12); + + _redeemHookless(delegation_, _erc20Executions(0, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + } + + function test_hooklessManagerRedeemsResetApproveExecution() public { + vm.prank(hooklessAccount); + tokenIn.approve(address(metaSwap), 1); + + bytes memory terms_ = _terms(address(tokenIn), _resetApproveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 13); + + _redeemHookless(delegation_, _erc20Executions(2, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + } + + function test_hooklessManagerRedeemsNativeInputExecution() public { + bytes memory terms_ = _terms(address(0), _noneMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 14); + uint256 nativeBefore_ = hooklessAccount.balance; + + _redeemHookless(delegation_, _nativeExecutions(TOKEN_OUT_AMOUNT)); + + assertEq(hooklessAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + } + + function test_hooklessManagerSupportsERC1271SignatureOption() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hookless1271Account); + Delegation memory delegation_ = _sign(hookless1271Manager, HOOKLESS_1271_KEY, hookless1271Account, terms_, 2); + _redeemHookless(hookless1271Manager, delegation_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenOut.balanceOf(hookless1271Account), TOKEN_OUT_AMOUNT); + } + + function test_gas_standardManagerWithSettlementEnforcer() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), standardAccount); + Delegation memory delegation_ = _signStandard(STANDARD_KEY, standardAccount, terms_, 100); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + standardManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("standard manager + enforcer", gasBefore_ - gasleft()); + } + + function test_gas_hooklessManagerWithERC1271() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hookless1271Account); + Delegation memory delegation_ = _sign(hookless1271Manager, HOOKLESS_1271_KEY, hookless1271Account, terms_, 101); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + hookless1271Manager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("hookless manager + ERC1271", gasBefore_ - gasleft()); + } + + function test_gas_hooklessManagerWithDirectECDSA() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 102); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("hookless manager + direct ECDSA", gasBefore_ - gasleft()); + } + + function test_gas_executionBuilderManagerWithDirectECDSA() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 103); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, abi.encode("redeemer-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_AMOUNT))); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + builderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("execution builder + direct ECDSA", gasBefore_ - gasleft()); + } + + function test_hooklessManagerRejectsInvalidExecutionWithoutCallingHooks() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 3); + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT); + executions_[1].target = makeAddr("UnapprovedSwapTarget"); + + vm.expectRevert(MetaSwapHooklessDelegationManager.InvalidSwap.selector); + _redeemHookless(delegation_, executions_); + } + + function test_builderManagerConstructsApproveAndSwapExecutions() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 4); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(tokenIn.balanceOf(builderAccount), 900 ether); + assertEq(tokenIn.allowance(builderAccount, address(metaSwap)), 0); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerConstructsResetApproveAndSwapExecutions() public { + vm.prank(builderAccount); + tokenIn.approve(address(metaSwap), 1); + + bytes memory terms_ = _terms(address(tokenIn), _resetApproveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 5); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(tokenIn.balanceOf(builderAccount), 900 ether); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerConstructsSkipApprovalSwapExecution() public { + vm.prank(builderAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + bytes memory terms_ = _terms(address(tokenIn), _skipApprovalMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 6); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(tokenIn.balanceOf(builderAccount), 900 ether); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerConstructsNativeInputSwapExecution() public { + bytes memory terms_ = _terms(address(0), _noneMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 7); + uint256 nativeBefore_ = builderAccount.balance; + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(builderAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerRejectsNativeApprovalMode() public { + bytes memory terms_ = _terms(address(0), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 15); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_builderManagerRejectsNoneModeForERC20() public { + bytes memory terms_ = _terms(address(tokenIn), _noneMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 16); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_builderManagerRevertsAtomicallyForInsufficientOutput() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 8); + bytes32 delegationHash_ = builderManager.getDelegationHash(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.InsufficientOutput.selector); + _redeemBuilder(delegation_, TOKEN_OUT_MIN - 1); + + assertFalse(builderManager.disabledDelegations(delegationHash_)); + assertEq(tokenIn.balanceOf(builderAccount), 1_000 ether); + assertEq(tokenOut.balanceOf(builderAccount), 0); + } + + function test_successfulSettlementCannotBeReplayed() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 9); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + vm.expectRevert(MetaSwapDelegationManagerBase.CannotUseADisabledDelegation.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_disableDelegationUsesSameOneShotState() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 10); + + vm.prank(builderAccount); + builderManager.disableDelegation(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.CannotUseADisabledDelegation.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_rejectsSignatureFromDifferentEOA() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, HOOKLESS_KEY, builderAccount, terms_, 11); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_rejectsUnsupportedBatchShapeAndMode() public { + bytes[] memory emptyContexts_ = new bytes[](0); + ModeCode[] memory emptyModes_ = new ModeCode[](0); + vm.expectRevert(MetaSwapDelegationManagerBase.BatchDataLengthMismatch.selector); + hooklessManager.redeemDelegations(emptyContexts_, emptyModes_, emptyContexts_); + + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 17); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + modes_[0] = ModeLib.encodeSimpleSingle(); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidMode.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function test_rejectsDelegationChainAndInvalidRootFields() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 18); + bytes memory executionContext_ = + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = delegation_; + delegations_[1] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + bytes[] memory executionContexts_ = new bytes[](1); + executionContexts_[0] = executionContext_; + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidPermissionContext.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + + delegation_.delegate = makeAddr("WrongDelegate"); + (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidDelegate.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + + delegation_.delegate = address(0xa11); + delegation_.authority = bytes32(0); + (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidAuthority.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function test_rejectsNonManagerCaveatAndInvalidTerms() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 19); + bytes memory executionContext_ = + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + delegation_.caveats[0].enforcer = makeAddr("ExternalEnforcer"); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidCaveat.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + + delegation_.caveats[0].enforcer = address(hooklessManager); + delegation_.caveats[0].terms = new bytes(144); + (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function test_onlyDelegatorCanDisableDelegation() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 20); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidDelegator.selector); + builderManager.disableDelegation(delegation_); + } + + 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 _sign( + MetaSwapDelegationManagerBase manager_, + uint256 signerKey_, + address delegator_, + bytes memory terms_, + uint256 salt_ + ) + private + view + returns (Delegation memory delegation_) + { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(manager_), terms: terms_, args: hex"" }); + delegation_ = Delegation({ + delegate: address(0xa11), + delegator: delegator_, + authority: manager_.ROOT_AUTHORITY(), + caveats: caveats_, + salt: salt_, + signature: hex"" + }); + + bytes32 delegationHash_ = manager_.getDelegationHash(delegation_); + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(manager_.getDomainHash(), 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 _signStandard( + uint256 signerKey_, + address delegator_, + bytes memory terms_, + uint256 salt_ + ) + private + view + returns (Delegation memory delegation_) + { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(standardEnforcer), terms: terms_, args: hex"" }); + delegation_ = Delegation({ + delegate: address(0xa11), + delegator: delegator_, + authority: standardManager.ROOT_AUTHORITY(), + caveats: caveats_, + salt: salt_, + signature: hex"" + }); + + bytes32 delegationHash_ = standardManager.getDelegationHash(delegation_); + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(standardManager.getDomainHash(), 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 _redeemHookless(Delegation memory delegation_, Execution[] memory executions_) private { + _redeemHookless(hooklessManager, delegation_, executions_); + } + + function _redeemHookless( + MetaSwapHooklessDelegationManager manager_, + Delegation memory delegation_, + Execution[] memory executions_ + ) + private + { + bytes[] memory executionContexts_ = new bytes[](1); + executionContexts_[0] = ExecutionLib.encodeBatch(executions_); + _redeem(manager_, delegation_, executionContexts_); + } + + function _redeemBuilder(Delegation memory delegation_, uint256 outputAmount_) private { + bytes[] memory executionContexts_ = new bytes[](1); + executionContexts_[0] = abi.encode("redeemer-route", abi.encode(IERC20(address(tokenOut)), outputAmount_)); + _redeem(builderManager, delegation_, executionContexts_); + } + + function _redeem( + MetaSwapDelegationManagerBase manager_, + Delegation memory delegation_, + bytes[] memory executionContexts_ + ) + private + { + (bytes[] memory permissionContexts_, ModeCode[] memory modes_,) = _redemptionInputs(delegation_, executionContexts_[0]); + + vm.prank(relayer); + manager_.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + 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 _terms( + address tokenIn_, + MetaSwapDelegationManagerBase.ApprovalMode approvalMode_, + address tokenOut_, + address recipient_ + ) + private + view + returns (bytes memory) + { + return abi.encodePacked( + address(metaSwap), tokenIn_, TOKEN_IN_AMOUNT, uint8(approvalMode_), tokenOut_, recipient_, TOKEN_OUT_MIN + ); + } + + function _erc20Executions( + uint8 approvalCount_, + address swapToken_, + uint256 swapAmount_, + 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(swapToken_), swapAmount_, 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 (MetaSwapDelegationManagerBase.ApprovalMode) { + return MetaSwapDelegationManagerBase.ApprovalMode.None; + } + + function _skipApprovalMode() private pure returns (MetaSwapDelegationManagerBase.ApprovalMode) { + return MetaSwapDelegationManagerBase.ApprovalMode.SkipApproval; + } + + function _approveMode() private pure returns (MetaSwapDelegationManagerBase.ApprovalMode) { + return MetaSwapDelegationManagerBase.ApprovalMode.Approve; + } + + function _resetApproveMode() private pure returns (MetaSwapDelegationManagerBase.ApprovalMode) { + return MetaSwapDelegationManagerBase.ApprovalMode.ResetApprove; + } +} From 2367cd23e2c9e45b54fb0f7a9df326bfe39d7621 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Tue, 8 Sep 2026 17:28:06 +0200 Subject: [PATCH 03/13] feat: add unified MetaSwap intent delegation manager Support exact-calldata gasless swaps and flexible limit-order settlements in one hookless manager, with gas benchmarks against the generic ExactBatch+LimitedCalls and FlexibleSettlement paths. --- .../MetaSwapSpecializedDelegationManagers.md | 88 ++- src/MetaSwapDelegationManagerBase.sol | 75 +-- ...aSwapExecutionBuilderDelegationManager.sol | 6 +- src/MetaSwapFlexibleSettlementManagerBase.sol | 75 +++ src/MetaSwapHooklessDelegationManager.sol | 6 +- src/MetaSwapIntentDelegationManager.sol | 207 ++++++ test/MetaSwapIntentDelegationManager.t.sol | 598 ++++++++++++++++++ ...etaSwapSpecializedDelegationManagers.t.sol | 25 +- 8 files changed, 974 insertions(+), 106 deletions(-) create mode 100644 src/MetaSwapFlexibleSettlementManagerBase.sol create mode 100644 src/MetaSwapIntentDelegationManager.sol create mode 100644 test/MetaSwapIntentDelegationManager.t.sol diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md index 36dd6785..35eff683 100644 --- a/documents/MetaSwapSpecializedDelegationManagers.md +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -4,50 +4,82 @@ These experimental managers preserve the existing three-array `redeemDelegations only one batch/default settlement, one root delegation, and one caveat. The caveat's enforcer address must be the manager itself because settlement enforcement is internal and no caveat hooks are called. -## MetaSwapHooklessDelegationManager +`executeFromExecutor` remains on the EIP-7702 account. Managers only call it. -The redeemer supplies a complete ABI-encoded `Execution[]`. The manager validates the exact approval and swap shape, -records the delegation hash as consumed, snapshots the recipient's output balance in memory, calls the delegator's -`executeFromExecutor`, and checks the minimum output. +## MetaSwapIntentDelegationManager -## MetaSwapExecutionBuilderDelegationManager +One manager for both product intents. Terms start with a one-byte `Intent`. -The redeemer supplies only: +### ExactCalldata (gasless) -```solidity -abi.encode(aggregatorId, routeData) +Replaces `ExactExecutionBatchEnforcer` + `LimitedCallsEnforcer(1)`. The user is shown the real swap batch and signs its +hash. Expiry stays off-chain (relayer stops submitting). + +```text +terms = intent(1) | executionHash(32) +executionCallDatas[0] = ExecutionLib.encodeBatch(executions) +``` + +Require `keccak256(executionCallDatas[0]) == executionHash`, then execute the batch directly. No nested self-`execute` +wrap. Supported shapes: + +- `[swap]` +- `[approve(amount), swap]` +- `[approve(0), approve(amount), swap]` +- `[swap{value}]` native + +### FlexibleSettlement (limit order) + +Same economics as `MetaSwapFlexibleSettlementEnforcer` / the hookless manager: + +```text +terms = intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) + | tokenOut(20) | recipient(20) | tokenOutMin(32) ``` -The manager constructs the signed `SkipApproval`, `Approve`, `ResetApprove`, or native-input execution shape. This makes -approval and swap targets, selectors, values, ordering, and amounts impossible for the redeemer to alter. +Redeemer supplies an encoded `Execution[]`. The manager validates approval/swap shape (not `aggregatorId` / route), +snapshots the output balance in memory, executes, then enforces `tokenOutMin`. + +## Prototype managers kept for A/B + +### MetaSwapHooklessDelegationManager + +Flexible-only. Redeemer supplies a complete ABI-encoded `Execution[]`. Validates shape, then executes. + +### MetaSwapExecutionBuilderDelegationManager + +Flexible-only. Redeemer supplies `abi.encode(aggregatorId, routeData)`. Manager constructs approvals and swap. ## Signature modes -- `DirectECDSA` recovers the EIP-712 signer directly and requires it to equal the EIP-7702 delegator address. It bypasses - the account's ERC-1271 policy and must only be used with EIP-7702 EOAs controlled by that key. -- `ERC1271` calls the delegator's configured signature validation policy and supports broader account types. +- `DirectECDSA` recovers the EIP-712 signer directly and requires it to equal the EIP-7702 delegator address. +- `ERC1271` calls the delegator's signature policy. + +`disabledDelegations` is both cancel and one-shot consumption. Failed execution or insufficient output reverts atomically. -The manager's disabled-delegation mapping also acts as permanent one-shot state. A successful delegation cannot be -re-enabled. Failed execution or insufficient output reverts the state update atomically. +## Gas comparison (`approve(amount) + swap`, EIP-7702) -## Initial gas comparison +Measured around `redeemDelegations` in `test/MetaSwapIntentDelegationManager.t.sol` and the specialized suite: -Measured around `redeemDelegations` for an ERC-20 `approve(amount) + swap` using EIP-7702 accounts: +| Path | Gas | vs generic flexible | +|------|-----|---------------------| +| Generic DM + ExactBatch + LimitedCalls(1) | `230,987` | — | +| Generic DM + FlexibleSettlementEnforcer | `200,783` | baseline flexible | +| Hookless flexible (DirectECDSA) | `166,508` | −17.1% | +| Intent ExactCalldata | `158,997` | −31.2% vs exact generic | +| Intent FlexibleSettlement | `166,725` | −17.0% | -- Standard DelegationManager plus MetaSwap settlement enforcer: `200,781` -- Hookless manager with ERC-1271: `168,404` — `32,377` lower (`16.1%`) -- Hookless manager with direct ECDSA: `166,465` — `34,316` lower (`17.1%`) -- Execution-builder manager with direct ECDSA: `168,631` — `32,150` lower (`16.0%`) +Takeaways: -Direct ECDSA saved `1,939` gas over ERC-1271. Constructing executions added `2,166` execution gas relative to validating -redeemer-provided calldata in this prototype; its benefit is stronger authorization and smaller transaction input rather -than lower EVM execution gas. +- Flattened exact intent is the cheapest path: no second enforcer, no LimitedCalls nested mapping, no self-`execute` wrap. +- Intent flexible matches hookless (~same gas); the unified manager does not pay a meaningful premium for dispatch. +- DirectECDSA vs ERC-1271 on hookless saved ~2k gas in earlier benches. ## Limitations - Delegation chains, multiple caveats, multiple redemption batches, self-authorized empty contexts, try mode, and generic enforcers are intentionally unsupported. -- The execution-builder manager reinterprets `_executionCallDatas[0]` as route context rather than `Execution[]`. -- `enableDelegation`, pause controls, and generic manager administration are intentionally absent. -- Flexible MetaSwap route data retains the same trusted-delegate and unrelated-balance-increase assumptions as the - settlement enforcer. +- No on-chain expiry for exact/gasless; relayers enforce freshness off-chain. +- No `enableDelegation` or pause controls. +- Flexible route data retains the same trusted-delegate and unrelated-balance-increase assumptions as the settlement + enforcer. diff --git a/src/MetaSwapDelegationManagerBase.sol b/src/MetaSwapDelegationManagerBase.sol index 87d6d7c8..cf3ed40c 100644 --- a/src/MetaSwapDelegationManagerBase.sol +++ b/src/MetaSwapDelegationManagerBase.sol @@ -15,8 +15,9 @@ import { Caveat, Delegation, ModeCode } from "./utils/Types.sol"; /** * @title MetaSwapDelegationManagerBase - * @notice Shared validation and settlement logic for specialized MetaSwap delegation managers. - * @dev Supports exactly one root delegation containing one manager-enforced settlement caveat. + * @notice Cheap one-shot redeem shell for purpose-specific MetaSwap managers. + * @dev Supports exactly one root delegation containing one manager-enforced caveat. + * Settlement-specific decoding and min-output checks live in subclasses. */ abstract contract MetaSwapDelegationManagerBase is EIP712 { enum SignatureMode { @@ -24,29 +25,10 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { ERC1271 } - enum ApprovalMode { - None, - SkipApproval, - Approve, - ResetApprove - } - - struct Terms { - address metaSwap; - address tokenIn; - uint256 tokenInAmount; - ApprovalMode approvalMode; - address tokenOut; - address recipient; - uint256 tokenOutMin; - } - string public constant DOMAIN_VERSION = "1"; bytes32 public constant ROOT_AUTHORITY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; address public constant ANY_DELEGATE = address(0xa11); - uint256 internal constant TERMS_LENGTH = 145; - SignatureMode public immutable signatureMode; /// @notice Records delegations that were cancelled or successfully consumed. @@ -92,10 +74,10 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { } /** - * @notice Redeems one specialized MetaSwap settlement delegation. + * @notice Redeems one specialized MetaSwap delegation. * @param permissionContexts_ Must contain one ABI-encoded one-element `Delegation[]`. * @param modes_ Must contain the canonical batch/default mode. - * @param executionContexts_ Manager-specific execution or route context. + * @param executionContexts_ Manager-specific execution context. */ function redeemDelegations( bytes[] calldata permissionContexts_, @@ -120,18 +102,10 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { bytes32 delegationHash_ = _getSingleCaveatDelegationHash(delegation_); if (disabledDelegations[delegationHash_]) revert CannotUseADisabledDelegation(); - Terms memory termsInfo_ = getTermsInfo(delegation_.caveats[0].terms); _validateSignature(delegation_, delegationHash_); disabledDelegations[delegationHash_] = true; - uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); - - _executeSettlement(delegation_.delegator, executionContexts_[0], termsInfo_); - - uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); - if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { - revert InsufficientOutput(); - } + _executeIntent(delegation_.delegator, delegation_.caveats[0].terms, executionContexts_[0]); emit RedeemedDelegation(delegation_.delegator, msg.sender, delegation_); } @@ -152,35 +126,12 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { } /** - * @notice Decodes and validates packed settlement terms. - * @param terms_ Packed settlement terms. + * @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 getTermsInfo(bytes memory terms_) public pure returns (Terms memory termsInfo_) { - if (terms_.length != TERMS_LENGTH) revert InvalidTerms(); - - // Terms are tightly packed. Loading their fixed offsets directly avoids allocating seven temporary byte arrays. - assembly ("memory-safe") { - let termsData_ := add(terms_, 0x20) - 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))) - } - uint8 approvalMode_ = uint8(terms_[72]); - - 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_); - } - - function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal virtual; + 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_); @@ -195,7 +146,7 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { } } - function _getSingleCaveatDelegationHash(Delegation memory delegation_) private pure returns (bytes32) { + 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_)); @@ -212,7 +163,7 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { ); } - function _balanceOf(address token_, address recipient_) private view returns (uint256) { + function _balanceOf(address token_, address recipient_) internal view returns (uint256) { return token_ == address(0) ? recipient_.balance : IERC20(token_).balanceOf(recipient_); } } diff --git a/src/MetaSwapExecutionBuilderDelegationManager.sol b/src/MetaSwapExecutionBuilderDelegationManager.sol index b4afb277..a3145f86 100644 --- a/src/MetaSwapExecutionBuilderDelegationManager.sol +++ b/src/MetaSwapExecutionBuilderDelegationManager.sol @@ -5,7 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; -import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; +import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; import { Execution } from "./utils/Types.sol"; @@ -15,12 +15,12 @@ import { Execution } from "./utils/Types.sol"; * @notice Constructs and executes one signed MetaSwap settlement from redeemer-supplied route data. * @dev Approval and swap targets, amounts, ordering, selectors, and values are created by this manager. */ -contract MetaSwapExecutionBuilderDelegationManager is MetaSwapDelegationManagerBase { +contract MetaSwapExecutionBuilderDelegationManager is MetaSwapFlexibleSettlementManagerBase { using ExecutionLib for Execution[]; string public constant NAME = "MetaSwapExecutionBuilderDelegationManager"; - constructor(SignatureMode signatureMode_) MetaSwapDelegationManagerBase(NAME, signatureMode_) { } + constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { (string memory aggregatorId_, bytes memory routeData_) = abi.decode(executionContext_, (string, bytes)); diff --git a/src/MetaSwapFlexibleSettlementManagerBase.sol b/src/MetaSwapFlexibleSettlementManagerBase.sol new file mode 100644 index 00000000..4e236d99 --- /dev/null +++ b/src/MetaSwapFlexibleSettlementManagerBase.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; + +/** + * @title MetaSwapFlexibleSettlementManagerBase + * @notice Shared flexible MetaSwap settlement decoding and min-output enforcement. + * @dev Used by the hookless and execution-builder prototype managers. + */ +abstract contract MetaSwapFlexibleSettlementManagerBase is MetaSwapDelegationManagerBase { + enum ApprovalMode { + None, + SkipApproval, + Approve, + ResetApprove + } + + struct Terms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + ApprovalMode approvalMode; + address tokenOut; + address recipient; + uint256 tokenOutMin; + } + + uint256 internal constant TERMS_LENGTH = 145; + + constructor(string memory name_, SignatureMode signatureMode_) MetaSwapDelegationManagerBase(name_, signatureMode_) { } + + /** + * @notice Decodes and validates packed settlement terms. + * @param terms_ Packed settlement terms. + */ + function getTermsInfo(bytes memory terms_) public pure returns (Terms memory termsInfo_) { + if (terms_.length != TERMS_LENGTH) revert InvalidTerms(); + + // Terms are tightly packed. Loading their fixed offsets directly avoids allocating seven temporary byte arrays. + assembly ("memory-safe") { + let termsData_ := add(terms_, 0x20) + 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))) + } + uint8 approvalMode_ = uint8(terms_[72]); + + 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_); + } + + function _executeIntent(address delegator_, bytes memory terms_, bytes calldata executionContext_) internal override { + Terms memory termsInfo_ = getTermsInfo(terms_); + uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + + _executeSettlement(delegator_, executionContext_, termsInfo_); + + uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { + revert InsufficientOutput(); + } + } + + function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal virtual; +} diff --git a/src/MetaSwapHooklessDelegationManager.sol b/src/MetaSwapHooklessDelegationManager.sol index e0bdebef..d04008b7 100644 --- a/src/MetaSwapHooklessDelegationManager.sol +++ b/src/MetaSwapHooklessDelegationManager.sol @@ -5,7 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; -import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; +import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; import { Execution } from "./utils/Types.sol"; @@ -15,7 +15,7 @@ import { Execution } from "./utils/Types.sol"; * @notice Executes one signed MetaSwap settlement without invoking external caveat hooks. * @dev The redeemer supplies a complete batch, which is validated directly by this manager. */ -contract MetaSwapHooklessDelegationManager is MetaSwapDelegationManagerBase { +contract MetaSwapHooklessDelegationManager is MetaSwapFlexibleSettlementManagerBase { using ExecutionLib for bytes; string public constant NAME = "MetaSwapHooklessDelegationManager"; @@ -28,7 +28,7 @@ contract MetaSwapHooklessDelegationManager is MetaSwapDelegationManagerBase { error InvalidBatchLength(); error InvalidSwap(); - constructor(SignatureMode signatureMode_) MetaSwapDelegationManagerBase(NAME, signatureMode_) { } + constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { Execution[] calldata executions_ = executionContext_.decodeBatch(); diff --git a/src/MetaSwapIntentDelegationManager.sol b/src/MetaSwapIntentDelegationManager.sol new file mode 100644 index 00000000..e9f10500 --- /dev/null +++ b/src/MetaSwapIntentDelegationManager.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.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 MetaSwapIntentDelegationManager + * @notice One purpose-specific manager for exact gasless swaps and flexible MetaSwap limit orders. + * @dev No external caveat hooks. Both intents redeem through a direct batch/default `executeFromExecutor`. + * + * Exact terms: `intent(1) | executionHash(32)` where `executionHash = keccak256(executionCallDatas[0])`. + * Flexible terms: `intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) | + * tokenOut(20) | recipient(20) | tokenOutMin(32)`. + */ +contract MetaSwapIntentDelegationManager is MetaSwapDelegationManagerBase { + using ExecutionLib for bytes; + + enum Intent { + ExactCalldata, + FlexibleSettlement + } + + enum ApprovalMode { + None, + SkipApproval, + Approve, + ResetApprove + } + + struct FlexibleTerms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + ApprovalMode approvalMode; + address tokenOut; + address recipient; + uint256 tokenOutMin; + } + + string public constant NAME = "MetaSwapIntentDelegationManager"; + + uint256 private constant EXACT_TERMS_LENGTH = 33; + uint256 private constant FLEXIBLE_TERMS_LENGTH = 146; + uint256 private constant APPROVE_CALL_LENGTH = 68; + uint256 private constant SWAP_CALL_MIN_LENGTH = 196; + + error ApprovalShapeNotAllowed(); + error InvalidApproval(); + error InvalidBatchLength(); + error InvalidExecutionHash(); + error InvalidIntent(); + error InvalidSwap(); + + constructor(SignatureMode signatureMode_) MetaSwapDelegationManagerBase(NAME, signatureMode_) { } + + /** + * @notice Decodes exact-calldata 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.ExactCalldata)) { + revert InvalidTerms(); + } + assembly ("memory-safe") { + executionHash_ := mload(add(terms_, 33)) + } + } + + /** + * @notice Decodes flexible settlement terms. + * @param terms_ Packed as `intent(1) | settlement fields(145)`. + */ + function getFlexibleTermsInfo(bytes memory terms_) public pure returns (FlexibleTerms memory termsInfo_) { + if (terms_.length != FLEXIBLE_TERMS_LENGTH || 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))) + } + 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_); + } + + 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.ExactCalldata)) { + _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(ModeLib.encodeSimpleBatch(), executionContext_); + } + + function _executeFlexible(address delegator_, bytes memory terms_, bytes calldata executionContext_) private { + FlexibleTerms memory termsInfo_ = getFlexibleTermsInfo(terms_); + Execution[] calldata executions_ = executionContext_.decodeBatch(); + _validateExecutions(executions_, termsInfo_); + + uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); + uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + + if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { + revert InsufficientOutput(); + } + } + + 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/MetaSwapIntentDelegationManager.t.sol b/test/MetaSwapIntentDelegationManager.t.sol new file mode 100644 index 00000000..ef5462e0 --- /dev/null +++ b/test/MetaSwapIntentDelegationManager.t.sol @@ -0,0 +1,598 @@ +// 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 { MetaSwapFlexibleSettlementManagerBase } from "../src/MetaSwapFlexibleSettlementManagerBase.sol"; +import { MetaSwapHooklessDelegationManager } from "../src/MetaSwapHooklessDelegationManager.sol"; +import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegationManager.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 { MetaSwapFlexibleSettlementEnforcer } from "../src/enforcers/MetaSwapFlexibleSettlementEnforcer.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"; + +contract IntentManagerMetaSwapMock 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 MetaSwapIntentDelegationManagerTest 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 HOOKLESS_KEY = 0x2222; + uint256 private constant INTENT_KEY = 0x3333; + + EntryPoint private entryPoint; + IntentManagerMetaSwapMock private metaSwap; + BasicERC20 private tokenIn; + BasicERC20 private tokenOut; + + DelegationManager private genericManager; + ExactExecutionBatchEnforcer private exactBatchEnforcer; + LimitedCallsEnforcer private limitedCallsEnforcer; + MetaSwapFlexibleSettlementEnforcer private flexibleEnforcer; + MetaSwapHooklessDelegationManager private hooklessManager; + MetaSwapIntentDelegationManager private intentManager; + + address private genericAccount; + address private hooklessAccount; + address private intentAccount; + address private relayer; + + function setUp() public { + entryPoint = new EntryPoint(); + metaSwap = new IntentManagerMetaSwapMock(); + 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(); + flexibleEnforcer = new MetaSwapFlexibleSettlementEnforcer(); + hooklessManager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + intentManager = new MetaSwapIntentDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + + genericAccount = vm.addr(GENERIC_KEY); + hooklessAccount = vm.addr(HOOKLESS_KEY); + intentAccount = vm.addr(INTENT_KEY); + + _installDeleGator(genericAccount, address(genericManager)); + _installDeleGator(hooklessAccount, address(hooklessManager)); + _installDeleGator(intentAccount, address(intentManager)); + + tokenIn.mint(genericAccount, 1_000 ether); + tokenIn.mint(hooklessAccount, 1_000 ether); + tokenIn.mint(intentAccount, 1_000 ether); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(genericAccount, 1_000 ether); + vm.deal(hooklessAccount, 1_000 ether); + vm.deal(intentAccount, 1_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + // -------- Exact intent -------- + + 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); + + _redeemIntent(delegation_, encoded_); + + assertEq(tokenIn.balanceOf(intentAccount), 900 ether); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + assertTrue(intentManager.disabledDelegations(intentManager.getDelegationHash(delegation_))); + } + + function test_exactRedeemsSkipApprovalSwap() public { + vm.prank(intentAccount); + 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(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_exactRedeemsResetApproveAndSwap() public { + vm.prank(intentAccount); + 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(intentAccount), 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_ = intentAccount.balance; + + _redeemIntent(delegation_, encoded_); + + assertEq(intentAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(intentAccount), 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(MetaSwapIntentDelegationManager.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(intentAccount); + intentManager.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(HOOKLESS_KEY, _exactTerms(keccak256(encoded_)), 8); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + _redeemIntent(delegation_, encoded_); + } + + // -------- Flexible intent -------- + + function test_flexibleRedeemsApproveAndSwap() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 10); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + + _redeemIntent(delegation_, encoded_); + + assertEq(tokenIn.balanceOf(intentAccount), 900 ether); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsSkipApproval() public { + vm.prank(intentAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + bytes memory terms_ = _flexibleTerms(address(tokenIn), _skipApprovalMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 11); + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(0, TOKEN_OUT_AMOUNT))); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsResetApprove() public { + vm.prank(intentAccount); + tokenIn.approve(address(metaSwap), 1); + + bytes memory terms_ = _flexibleTerms(address(tokenIn), _resetApproveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 12); + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(2, TOKEN_OUT_AMOUNT))); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsNativeInput() public { + bytes memory terms_ = _flexibleTerms(address(0), _noneMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 13); + uint256 nativeBefore_ = intentAccount.balance; + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); + + assertEq(intentAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleAllowsDifferentRouteData() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + + 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(intentAccount), TOKEN_OUT_AMOUNT * 2); + } + + function test_flexibleRevertsAtomicallyForInsufficientOutput() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 16); + bytes32 hash_ = intentManager.getDelegationHash(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.InsufficientOutput.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_MIN - 1))); + + assertFalse(intentManager.disabledDelegations(hash_)); + assertEq(tokenIn.balanceOf(intentAccount), 1_000 ether); + } + + function test_flexibleRejectsInvalidApprovalMode() public { + bytes memory terms_ = _flexibleTerms(address(0), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 17); + + vm.expectRevert(MetaSwapDelegationManagerBase.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(MetaSwapIntentDelegationManager.InvalidIntent.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + // -------- 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_genericFlexibleSettlementEnforcer() public { + bytes memory terms_ = abi.encodePacked( + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(MetaSwapFlexibleSettlementEnforcer.ApprovalMode.Approve), + address(tokenOut), + genericAccount, + TOKEN_OUT_MIN + ); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(flexibleEnforcer), terms: terms_, args: hex"" }); + Delegation memory delegation_ = _signGeneric(caveats_, 101); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20ExecutionsFor(genericAccount, 1, TOKEN_OUT_AMOUNT)); + + (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 FlexibleSettlementEnforcer", gasBefore_ - gasleft()); + } + + function test_gas_hooklessFlexible() public { + bytes memory terms_ = abi.encodePacked( + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(MetaSwapFlexibleSettlementManagerBase.ApprovalMode.Approve), + address(tokenOut), + hooklessAccount, + TOKEN_OUT_MIN + ); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(hooklessManager), terms: terms_, args: hex"" }); + Delegation memory delegation_ = _signManager(hooklessManager, HOOKLESS_KEY, hooklessAccount, caveats_, 102); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20ExecutionsFor(hooklessAccount, 1, TOKEN_OUT_AMOUNT)); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("hookless flexible", gasBefore_ - gasleft()); + } + + function test_gas_intentExact() 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); + intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("intent ExactCalldata", gasBefore_ - gasleft()); + } + + function test_gas_intentFlexible() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + 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); + intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("intent FlexibleSettlement", 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(MetaSwapIntentDelegationManager.Intent.ExactCalldata), executionHash_); + } + + function _flexibleTerms( + address tokenIn_, + MetaSwapIntentDelegationManager.ApprovalMode approvalMode_, + address tokenOut_, + address recipient_ + ) + private + view + returns (bytes memory) + { + return abi.encodePacked( + uint8(MetaSwapIntentDelegationManager.Intent.FlexibleSettlement), + address(metaSwap), + tokenIn_, + TOKEN_IN_AMOUNT, + uint8(approvalMode_), + tokenOut_, + recipient_, + TOKEN_OUT_MIN + ); + } + + function _signIntent(bytes memory terms_, uint256 salt_) private view returns (Delegation memory) { + return _signIntentWithKey(INTENT_KEY, terms_, salt_); + } + + 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(intentManager), terms: terms_, args: hex"" }); + return _signManager(intentManager, signerKey_, intentAccount, 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); + intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + 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(intentAccount, 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 (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.None; + } + + function _skipApprovalMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.SkipApproval; + } + + function _approveMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.Approve; + } + + function _resetApproveMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.ResetApprove; + } +} diff --git a/test/MetaSwapSpecializedDelegationManagers.t.sol b/test/MetaSwapSpecializedDelegationManagers.t.sol index 8494b378..965248db 100644 --- a/test/MetaSwapSpecializedDelegationManagers.t.sol +++ b/test/MetaSwapSpecializedDelegationManagers.t.sol @@ -10,6 +10,7 @@ import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { MetaSwapDelegationManagerBase } from "../src/MetaSwapDelegationManagerBase.sol"; +import { MetaSwapFlexibleSettlementManagerBase } from "../src/MetaSwapFlexibleSettlementManagerBase.sol"; import { MetaSwapExecutionBuilderDelegationManager } from "../src/MetaSwapExecutionBuilderDelegationManager.sol"; import { MetaSwapHooklessDelegationManager } from "../src/MetaSwapHooklessDelegationManager.sol"; import { DelegationManager } from "../src/DelegationManager.sol"; @@ -394,10 +395,14 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { vm.expectRevert(MetaSwapDelegationManagerBase.InvalidCaveat.selector); hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.getTermsInfo(new bytes(144)); + + // Mutating signed terms changes the hash, so signature validation fails before terms decoding. delegation_.caveats[0].enforcer = address(hooklessManager); delegation_.caveats[0].terms = new bytes(144); (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); - vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); } @@ -538,7 +543,7 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { function _terms( address tokenIn_, - MetaSwapDelegationManagerBase.ApprovalMode approvalMode_, + MetaSwapFlexibleSettlementManagerBase.ApprovalMode approvalMode_, address tokenOut_, address recipient_ ) @@ -594,19 +599,19 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { }); } - function _noneMode() private pure returns (MetaSwapDelegationManagerBase.ApprovalMode) { - return MetaSwapDelegationManagerBase.ApprovalMode.None; + function _noneMode() private pure returns (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.None; } - function _skipApprovalMode() private pure returns (MetaSwapDelegationManagerBase.ApprovalMode) { - return MetaSwapDelegationManagerBase.ApprovalMode.SkipApproval; + function _skipApprovalMode() private pure returns (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.SkipApproval; } - function _approveMode() private pure returns (MetaSwapDelegationManagerBase.ApprovalMode) { - return MetaSwapDelegationManagerBase.ApprovalMode.Approve; + function _approveMode() private pure returns (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.Approve; } - function _resetApproveMode() private pure returns (MetaSwapDelegationManagerBase.ApprovalMode) { - return MetaSwapDelegationManagerBase.ApprovalMode.ResetApprove; + function _resetApproveMode() private pure returns (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.ResetApprove; } } From e16632d6d374eea298bef6e1588240ae8ba12676 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 9 Sep 2026 02:31:11 +0200 Subject: [PATCH 04/13] feat: add experimental MetaSwap intent delegation manager Introduce a purpose-specific manager for ExactCalldata gasless swaps and FlexibleSettlement limit orders, with prototype managers, docs, deploy and verify scripts, and ~100% line coverage on the new contracts. --- .env.example | 3 + .../MetaSwapSpecializedDelegationManagers.md | 80 ++ ...eployMetaSwapIntentDelegationManager.s.sol | 46 + .../verification/VerificationInstructions.md | 11 + ...rify-metaswap-intent-delegation-manager.sh | 64 ++ src/MetaSwapDelegationManagerBase.sol | 169 ++++ ...aSwapExecutionBuilderDelegationManager.sol | 92 ++ src/MetaSwapFlexibleSettlementManagerBase.sol | 75 ++ src/MetaSwapHooklessDelegationManager.sol | 107 +++ src/MetaSwapIntentDelegationManager.sol | 207 +++++ test/MetaSwapIntentDelegationManager.t.sol | 817 ++++++++++++++++++ ...etaSwapSpecializedDelegationManagers.t.sol | 662 ++++++++++++++ 12 files changed, 2333 insertions(+) create mode 100644 documents/MetaSwapSpecializedDelegationManagers.md create mode 100644 script/DeployMetaSwapIntentDelegationManager.s.sol create mode 100755 script/verification/verify-metaswap-intent-delegation-manager.sh create mode 100644 src/MetaSwapDelegationManagerBase.sol create mode 100644 src/MetaSwapExecutionBuilderDelegationManager.sol create mode 100644 src/MetaSwapFlexibleSettlementManagerBase.sol create mode 100644 src/MetaSwapHooklessDelegationManager.sol create mode 100644 src/MetaSwapIntentDelegationManager.sol create mode 100644 test/MetaSwapIntentDelegationManager.t.sol create mode 100644 test/MetaSwapSpecializedDelegationManagers.t.sol diff --git a/.env.example b/.env.example index 4083dfd8..3be83d2f 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,9 @@ PRIVATE_KEY= SALT=GATOR DELEGATION_MANAGER_ADDRESS= +META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS= +# 0 = DirectECDSA, 1 = ERC1271 (used by intent manager deploy/verify) +SIGNATURE_MODE=0 ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032 MULTISIG_DELEGATOR_IMPLEMENTATION_ADDRESS= META_SWAP_ADAPTER_OWNER_ADDRESS= diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md new file mode 100644 index 00000000..a5a7bcbd --- /dev/null +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -0,0 +1,80 @@ +# MetaSwap Intent Delegation Manager (Experimental) + +Status: **experimental**. Not a drop-in replacement for the generic `DelegationManager`. + +## What it is + +One purpose-specific manager for two MetaSwap flows: + +| Intent | Product | User signs | Redeemer may change | +|--------|---------|------------|---------------------| +| **ExactCalldata** | Gasless exact swap | Full batch hash | Nothing | +| **FlexibleSettlement** | Limit order | Economics + approval shape | Route / `aggregatorId` only | + +API kept: `redeemDelegations`, `disableDelegation`. Accounts still expose `executeFromExecutor`; this manager only calls it. + +## Why + +Generic gasless (`ExactExecutionBatchEnforcer` + `LimitedCallsEnforcer`) and generic flexible settlement pay full hook machinery. This manager inlines checks, uses one one-shot slot (`disabledDelegations`), and runs a direct batch (no self-`execute` wrap). + +## Gas (`approve + swap`, EIP-7702, DirectECDSA) + +| Approach | Gas | +|----------|-----| +| Generic ExactBatch + LimitedCalls(1) | ~231k | +| Unified ExactCalldata | ~159k (**~31% cheaper**) | +| Hookless flexible (prototype) | ~167k | +| Unified FlexibleSettlement | ~167k | +| Unified + ERC1271 | +~1.9k vs DirectECDSA | + +## Related experiments (not required) + +- `MetaSwapHooklessDelegationManager` — flexible-only, validates redeemer batch +- `MetaSwapExecutionBuilderDelegationManager` — flexible-only, builds approvals + swap from route data (~2k more gas than hookless) + +## Terms + +**ExactCalldata** + +```text +intent(1) | executionHash(32) +``` + +`executionHash = keccak256(executionCallDatas[0])` where the payload is `ExecutionLib.encodeBatch(...)`. + +**FlexibleSettlement** + +```text +intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) +| tokenOut(20) | recipient(20) | tokenOutMin(32) +``` + +## Signature modes + +- `DirectECDSA` (0) — cheapest; EIP-7702 EOA only +- `ERC1271` (1) — Multisig / Hybrid / custom validators + +## Deploy + +```bash +# SIGNATURE_MODE=0 DirectECDSA, 1 ERC1271 +forge script script/DeployMetaSwapIntentDelegationManager.s.sol \ + --rpc-url --private-key $PRIVATE_KEY --broadcast +``` + +Wire EIP-7702 / DeleGator implementations to the deployed manager address. + +## Verify + +```bash +# set META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS and SIGNATURE_MODE in .env +cd script/verification +./verify-metaswap-intent-delegation-manager.sh +``` + +## Limits + +- One root delegation, one self-caveat, batch/default only +- No chains, try-mode, pause, or `enableDelegation` +- Gasless expiry is off-chain +- Flexible routes trust MetaSwap + redeemer; min-out can be met by any balance increase diff --git a/script/DeployMetaSwapIntentDelegationManager.s.sol b/script/DeployMetaSwapIntentDelegationManager.s.sol new file mode 100644 index 00000000..1adbdc84 --- /dev/null +++ b/script/DeployMetaSwapIntentDelegationManager.s.sol @@ -0,0 +1,46 @@ +// 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 { MetaSwapDelegationManagerBase } from "../src/MetaSwapDelegationManagerBase.sol"; +import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegationManager.sol"; + +/** + * @title DeployMetaSwapIntentDelegationManager + * @notice Deploys the experimental MetaSwap intent delegation manager. + * @dev Experimental. EIP-7702 accounts must be wired to this manager address. + * + * forge script script/DeployMetaSwapIntentDelegationManager.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast + * + * Env: + * - SALT + * - SIGNATURE_MODE: 0 = DirectECDSA, 1 = ERC1271 + */ +contract DeployMetaSwapIntentDelegationManager is Script { + bytes32 salt; + MetaSwapDelegationManagerBase.SignatureMode signatureMode; + + function setUp() public { + salt = bytes32(abi.encodePacked(vm.envString("SALT"))); + uint256 mode_ = vm.envOr("SIGNATURE_MODE", uint256(0)); + require(mode_ <= 1, "SIGNATURE_MODE must be 0 or 1"); + signatureMode = MetaSwapDelegationManagerBase.SignatureMode(uint8(mode_)); + + console2.log("~~~"); + console2.log("Salt:"); + console2.logBytes32(salt); + console2.log("SignatureMode: %s", mode_ == 0 ? "DirectECDSA" : "ERC1271"); + } + + function run() public { + console2.log("~~~"); + vm.startBroadcast(); + + address deployedAddress = address(new MetaSwapIntentDelegationManager{ salt: salt }(signatureMode)); + console2.log("MetaSwapIntentDelegationManager: %s", deployedAddress); + + vm.stopBroadcast(); + } +} diff --git a/script/verification/VerificationInstructions.md b/script/verification/VerificationInstructions.md index 1adc138c..73c7fa64 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-intent-delegation-manager.sh` + +Experimental. Verifies `MetaSwapIntentDelegationManager`. +Requires `META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS` and `SIGNATURE_MODE` (`0` DirectECDSA, `1` ERC1271). + +**Usage:** + +```bash +./verify-metaswap-intent-delegation-manager.sh +``` + ## Notes - Ensure the `.env` file is correctly configured and contains all necessary API keys. diff --git a/script/verification/verify-metaswap-intent-delegation-manager.sh b/script/verification/verify-metaswap-intent-delegation-manager.sh new file mode 100755 index 00000000..b2cf4c10 --- /dev/null +++ b/script/verification/verify-metaswap-intent-delegation-manager.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# verify-metaswap-intent-delegation-manager.sh +# +# Usage: +# ./verify-metaswap-intent-delegation-manager.sh +# +# Experimental. Verifies MetaSwapIntentDelegationManager across configured chains. +# Requires in .env: +# META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS +# SIGNATURE_MODE # 0 = DirectECDSA, 1 = ERC1271 + +set -e + +set -o allexport +source ../../.env +set +o allexport + +source ./verify-utils.sh + +encode_args() { + local signature="$1" + shift + cast abi-encode "$signature" "$@" +} + +declare -a CONTRACTS + +add_contract() { + local name="$1" + local path="$2" + local address="$3" + local constructor_args="$4" + local lib_string="$5" + CONTRACTS+=("$name:$path:$address:$constructor_args:$lib_string") +} + +MODE="${SIGNATURE_MODE:-0}" + +add_contract \ + "MetaSwapIntentDelegationManager" \ + "src/MetaSwapIntentDelegationManager.sol" \ + "${META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS}" \ + "$(encode_args "constructor(uint8)" "$MODE")" \ + "" + +for contract in "${CONTRACTS[@]}"; do + IFS=':' read -r name path address constructor_args lib_string <<< "$contract" + + echo "=============================================" + echo "Verifying contract: $name" + echo "=============================================" + + 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..cf3ed40c --- /dev/null +++ b/src/MetaSwapDelegationManagerBase.sol @@ -0,0 +1,169 @@ +// 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 { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.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. + * Settlement-specific decoding and min-output checks live in subclasses. + */ +abstract contract MetaSwapDelegationManagerBase is EIP712 { + enum SignatureMode { + DirectECDSA, + ERC1271 + } + + string public constant DOMAIN_VERSION = "1"; + bytes32 public constant ROOT_AUTHORITY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + address public constant ANY_DELEGATE = address(0xa11); + + SignatureMode public immutable signatureMode; + + /// @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 + ); + event RedeemedDelegation(address indexed rootDelegator, address indexed redeemer, Delegation delegation); + + error AlreadyDisabled(); + error BatchDataLengthMismatch(); + error CannotUseADisabledDelegation(); + error InsufficientOutput(); + error InvalidApprovalMode(); + error InvalidAuthority(); + error InvalidCaveat(); + error InvalidDelegate(); + error InvalidDelegator(); + error InvalidEOASignature(); + error InvalidERC1271Signature(); + error InvalidMode(); + error InvalidPermissionContext(); + error InvalidTerms(); + + constructor(string memory name_, SignatureMode signatureMode_) EIP712(name_, DOMAIN_VERSION) { + signatureMode = signatureMode_; + } + + /** + * @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. + * @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(ModeLib.encodeSimpleBatch())) 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; + _executeIntent(delegation_.delegator, delegation_.caveats[0].terms, executionContexts_[0]); + + emit RedeemedDelegation(delegation_.delegator, msg.sender, delegation_); + } + + /** + * @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_); + + if (signatureMode == SignatureMode.DirectECDSA) { + if (ECDSA.recover(typedDataHash_, delegation_.signature) != delegation_.delegator) { + revert InvalidEOASignature(); + } + } else { + bytes4 result_ = IERC1271(delegation_.delegator).isValidSignature(typedDataHash_, delegation_.signature); + if (result_ != 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/MetaSwapExecutionBuilderDelegationManager.sol b/src/MetaSwapExecutionBuilderDelegationManager.sol new file mode 100644 index 00000000..a3145f86 --- /dev/null +++ b/src/MetaSwapExecutionBuilderDelegationManager.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; +import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; +import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; +import { Execution } from "./utils/Types.sol"; + +/** + * @title MetaSwapExecutionBuilderDelegationManager + * @notice Constructs and executes one signed MetaSwap settlement from redeemer-supplied route data. + * @dev Approval and swap targets, amounts, ordering, selectors, and values are created by this manager. + */ +contract MetaSwapExecutionBuilderDelegationManager is MetaSwapFlexibleSettlementManagerBase { + using ExecutionLib for Execution[]; + + string public constant NAME = "MetaSwapExecutionBuilderDelegationManager"; + + constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } + + function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { + (string memory aggregatorId_, bytes memory routeData_) = abi.decode(executionContext_, (string, bytes)); + Execution[] memory executions_ = _buildExecutions(termsInfo_, aggregatorId_, routeData_); + + IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executions_.encodeBatch()); + } + + function _buildExecutions( + Terms memory termsInfo_, + string memory aggregatorId_, + bytes memory routeData_ + ) + private + pure + returns (Execution[] memory executions_) + { + ApprovalMode approvalMode_ = termsInfo_.approvalMode; + + if (termsInfo_.tokenIn == address(0)) { + if (approvalMode_ != ApprovalMode.None) revert InvalidApprovalMode(); + + executions_ = new Execution[](1); + executions_[0] = _swapExecution(termsInfo_, termsInfo_.tokenInAmount, aggregatorId_, routeData_); + return executions_; + } + + uint256 swapIndex_; + if (approvalMode_ == ApprovalMode.SkipApproval) { + executions_ = new Execution[](1); + } else if (approvalMode_ == ApprovalMode.Approve) { + executions_ = new Execution[](2); + executions_[0] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + swapIndex_ = 1; + } else if (approvalMode_ == ApprovalMode.ResetApprove) { + executions_ = new Execution[](3); + executions_[0] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, 0); + executions_[1] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + swapIndex_ = 2; + } else { + revert InvalidApprovalMode(); + } + + executions_[swapIndex_] = _swapExecution(termsInfo_, 0, aggregatorId_, routeData_); + } + + function _approvalExecution(address tokenIn_, address metaSwap_, uint256 amount_) private pure returns (Execution memory) { + return Execution({ target: tokenIn_, value: 0, callData: abi.encodeCall(IERC20.approve, (metaSwap_, amount_)) }); + } + + function _swapExecution( + Terms memory termsInfo_, + uint256 value_, + string memory aggregatorId_, + bytes memory routeData_ + ) + private + pure + returns (Execution memory) + { + return Execution({ + target: termsInfo_.metaSwap, + value: value_, + callData: abi.encodeCall( + IMetaSwap.swap, (aggregatorId_, IERC20(termsInfo_.tokenIn), termsInfo_.tokenInAmount, routeData_) + ) + }); + } +} diff --git a/src/MetaSwapFlexibleSettlementManagerBase.sol b/src/MetaSwapFlexibleSettlementManagerBase.sol new file mode 100644 index 00000000..4e236d99 --- /dev/null +++ b/src/MetaSwapFlexibleSettlementManagerBase.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; + +/** + * @title MetaSwapFlexibleSettlementManagerBase + * @notice Shared flexible MetaSwap settlement decoding and min-output enforcement. + * @dev Used by the hookless and execution-builder prototype managers. + */ +abstract contract MetaSwapFlexibleSettlementManagerBase is MetaSwapDelegationManagerBase { + enum ApprovalMode { + None, + SkipApproval, + Approve, + ResetApprove + } + + struct Terms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + ApprovalMode approvalMode; + address tokenOut; + address recipient; + uint256 tokenOutMin; + } + + uint256 internal constant TERMS_LENGTH = 145; + + constructor(string memory name_, SignatureMode signatureMode_) MetaSwapDelegationManagerBase(name_, signatureMode_) { } + + /** + * @notice Decodes and validates packed settlement terms. + * @param terms_ Packed settlement terms. + */ + function getTermsInfo(bytes memory terms_) public pure returns (Terms memory termsInfo_) { + if (terms_.length != TERMS_LENGTH) revert InvalidTerms(); + + // Terms are tightly packed. Loading their fixed offsets directly avoids allocating seven temporary byte arrays. + assembly ("memory-safe") { + let termsData_ := add(terms_, 0x20) + 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))) + } + uint8 approvalMode_ = uint8(terms_[72]); + + 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_); + } + + function _executeIntent(address delegator_, bytes memory terms_, bytes calldata executionContext_) internal override { + Terms memory termsInfo_ = getTermsInfo(terms_); + uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + + _executeSettlement(delegator_, executionContext_, termsInfo_); + + uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { + revert InsufficientOutput(); + } + } + + function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal virtual; +} diff --git a/src/MetaSwapHooklessDelegationManager.sol b/src/MetaSwapHooklessDelegationManager.sol new file mode 100644 index 00000000..d04008b7 --- /dev/null +++ b/src/MetaSwapHooklessDelegationManager.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; +import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; +import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; +import { Execution } from "./utils/Types.sol"; + +/** + * @title MetaSwapHooklessDelegationManager + * @notice Executes one signed MetaSwap settlement without invoking external caveat hooks. + * @dev The redeemer supplies a complete batch, which is validated directly by this manager. + */ +contract MetaSwapHooklessDelegationManager is MetaSwapFlexibleSettlementManagerBase { + using ExecutionLib for bytes; + + string public constant NAME = "MetaSwapHooklessDelegationManager"; + + uint256 private constant APPROVE_CALL_LENGTH = 68; + uint256 private constant SWAP_CALL_MIN_LENGTH = 196; + + error ApprovalShapeNotAllowed(); + error InvalidApproval(); + error InvalidBatchLength(); + error InvalidSwap(); + + constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } + + function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { + Execution[] calldata executions_ = executionContext_.decodeBatch(); + _validateExecutions(executions_, termsInfo_); + + IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); + } + + function _validateExecutions(Execution[] calldata executions_, Terms 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/src/MetaSwapIntentDelegationManager.sol b/src/MetaSwapIntentDelegationManager.sol new file mode 100644 index 00000000..e9f10500 --- /dev/null +++ b/src/MetaSwapIntentDelegationManager.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.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 MetaSwapIntentDelegationManager + * @notice One purpose-specific manager for exact gasless swaps and flexible MetaSwap limit orders. + * @dev No external caveat hooks. Both intents redeem through a direct batch/default `executeFromExecutor`. + * + * Exact terms: `intent(1) | executionHash(32)` where `executionHash = keccak256(executionCallDatas[0])`. + * Flexible terms: `intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) | + * tokenOut(20) | recipient(20) | tokenOutMin(32)`. + */ +contract MetaSwapIntentDelegationManager is MetaSwapDelegationManagerBase { + using ExecutionLib for bytes; + + enum Intent { + ExactCalldata, + FlexibleSettlement + } + + enum ApprovalMode { + None, + SkipApproval, + Approve, + ResetApprove + } + + struct FlexibleTerms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + ApprovalMode approvalMode; + address tokenOut; + address recipient; + uint256 tokenOutMin; + } + + string public constant NAME = "MetaSwapIntentDelegationManager"; + + uint256 private constant EXACT_TERMS_LENGTH = 33; + uint256 private constant FLEXIBLE_TERMS_LENGTH = 146; + uint256 private constant APPROVE_CALL_LENGTH = 68; + uint256 private constant SWAP_CALL_MIN_LENGTH = 196; + + error ApprovalShapeNotAllowed(); + error InvalidApproval(); + error InvalidBatchLength(); + error InvalidExecutionHash(); + error InvalidIntent(); + error InvalidSwap(); + + constructor(SignatureMode signatureMode_) MetaSwapDelegationManagerBase(NAME, signatureMode_) { } + + /** + * @notice Decodes exact-calldata 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.ExactCalldata)) { + revert InvalidTerms(); + } + assembly ("memory-safe") { + executionHash_ := mload(add(terms_, 33)) + } + } + + /** + * @notice Decodes flexible settlement terms. + * @param terms_ Packed as `intent(1) | settlement fields(145)`. + */ + function getFlexibleTermsInfo(bytes memory terms_) public pure returns (FlexibleTerms memory termsInfo_) { + if (terms_.length != FLEXIBLE_TERMS_LENGTH || 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))) + } + 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_); + } + + 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.ExactCalldata)) { + _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(ModeLib.encodeSimpleBatch(), executionContext_); + } + + function _executeFlexible(address delegator_, bytes memory terms_, bytes calldata executionContext_) private { + FlexibleTerms memory termsInfo_ = getFlexibleTermsInfo(terms_); + Execution[] calldata executions_ = executionContext_.decodeBatch(); + _validateExecutions(executions_, termsInfo_); + + uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); + uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); + + if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { + revert InsufficientOutput(); + } + } + + 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/MetaSwapIntentDelegationManager.t.sol b/test/MetaSwapIntentDelegationManager.t.sol new file mode 100644 index 00000000..2c132846 --- /dev/null +++ b/test/MetaSwapIntentDelegationManager.t.sol @@ -0,0 +1,817 @@ +// 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 { MetaSwapFlexibleSettlementManagerBase } from "../src/MetaSwapFlexibleSettlementManagerBase.sol"; +import { MetaSwapHooklessDelegationManager } from "../src/MetaSwapHooklessDelegationManager.sol"; +import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegationManager.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"; + +contract IntentManagerMetaSwapMock 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 MetaSwapIntentDelegationManagerTest 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 HOOKLESS_KEY = 0x2222; + uint256 private constant INTENT_KEY = 0x3333; + uint256 private constant INTENT_1271_KEY = 0x4444; + + EntryPoint private entryPoint; + IntentManagerMetaSwapMock private metaSwap; + BasicERC20 private tokenIn; + BasicERC20 private tokenOut; + + DelegationManager private genericManager; + ExactExecutionBatchEnforcer private exactBatchEnforcer; + LimitedCallsEnforcer private limitedCallsEnforcer; + MetaSwapHooklessDelegationManager private hooklessManager; + MetaSwapIntentDelegationManager private intentManager; + MetaSwapIntentDelegationManager private intent1271Manager; + + address private genericAccount; + address private hooklessAccount; + address private intentAccount; + address private intent1271Account; + address private relayer; + + function setUp() public { + entryPoint = new EntryPoint(); + metaSwap = new IntentManagerMetaSwapMock(); + 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(); + hooklessManager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + intentManager = new MetaSwapIntentDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + intent1271Manager = new MetaSwapIntentDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.ERC1271); + + genericAccount = vm.addr(GENERIC_KEY); + hooklessAccount = vm.addr(HOOKLESS_KEY); + intentAccount = vm.addr(INTENT_KEY); + intent1271Account = vm.addr(INTENT_1271_KEY); + + _installDeleGator(genericAccount, address(genericManager)); + _installDeleGator(hooklessAccount, address(hooklessManager)); + _installDeleGator(intentAccount, address(intentManager)); + _installDeleGator(intent1271Account, address(intent1271Manager)); + + tokenIn.mint(genericAccount, 1_000 ether); + tokenIn.mint(hooklessAccount, 1_000 ether); + tokenIn.mint(intentAccount, 1_000 ether); + tokenIn.mint(intent1271Account, 1_000 ether); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(genericAccount, 1_000 ether); + vm.deal(hooklessAccount, 1_000 ether); + vm.deal(intentAccount, 1_000 ether); + vm.deal(intent1271Account, 1_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + // -------- Exact intent -------- + + 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); + + _redeemIntent(delegation_, encoded_); + + assertEq(tokenIn.balanceOf(intentAccount), 900 ether); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + assertTrue(intentManager.disabledDelegations(intentManager.getDelegationHash(delegation_))); + } + + function test_exactRedeemsSkipApprovalSwap() public { + vm.prank(intentAccount); + 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(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_exactRedeemsResetApproveAndSwap() public { + vm.prank(intentAccount); + 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(intentAccount), 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_ = intentAccount.balance; + + _redeemIntent(delegation_, encoded_); + + assertEq(intentAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(intentAccount), 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(MetaSwapIntentDelegationManager.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(intentAccount); + intentManager.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(HOOKLESS_KEY, _exactTerms(keccak256(encoded_)), 8); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + _redeemIntent(delegation_, encoded_); + } + + // -------- Flexible intent -------- + + function test_flexibleRedeemsApproveAndSwap() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 10); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + + _redeemIntent(delegation_, encoded_); + + assertEq(tokenIn.balanceOf(intentAccount), 900 ether); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsSkipApproval() public { + vm.prank(intentAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + bytes memory terms_ = _flexibleTerms(address(tokenIn), _skipApprovalMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 11); + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(0, TOKEN_OUT_AMOUNT))); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsResetApprove() public { + vm.prank(intentAccount); + tokenIn.approve(address(metaSwap), 1); + + bytes memory terms_ = _flexibleTerms(address(tokenIn), _resetApproveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 12); + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(2, TOKEN_OUT_AMOUNT))); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleRedeemsNativeInput() public { + bytes memory terms_ = _flexibleTerms(address(0), _noneMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 13); + uint256 nativeBefore_ = intentAccount.balance; + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); + + assertEq(intentAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + } + + function test_flexibleAllowsDifferentRouteData() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + + 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(intentAccount), TOKEN_OUT_AMOUNT * 2); + } + + function test_flexibleRevertsAtomicallyForInsufficientOutput() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 16); + bytes32 hash_ = intentManager.getDelegationHash(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.InsufficientOutput.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_MIN - 1))); + + assertFalse(intentManager.disabledDelegations(hash_)); + assertEq(tokenIn.balanceOf(intentAccount), 1_000 ether); + } + + function test_flexibleRejectsInvalidApprovalMode() public { + bytes memory terms_ = _flexibleTerms(address(0), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 17); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); + } + + function test_flexibleRejectsNoneModeForERC20() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _noneMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 26); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + function test_rejectsUnknownIntent() public { + bytes memory terms_ = abi.encodePacked(uint8(2), bytes32(0)); + Delegation memory delegation_ = _signIntent(terms_, 18); + + vm.expectRevert(MetaSwapIntentDelegationManager.InvalidIntent.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + function test_rejectsEmptyTerms() public { + Delegation memory delegation_ = _signIntent(hex"", 19); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); + } + + function test_getExactTermsInfoRevertsForInvalidTerms() public { + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getExactTermsInfo(new bytes(32)); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getExactTermsInfo(abi.encodePacked(uint8(1), bytes32(0))); + } + + function test_getFlexibleTermsInfoRevertsForInvalidTerms() public { + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getFlexibleTermsInfo(new bytes(145)); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getFlexibleTermsInfo( + abi.encodePacked( + uint8(0), + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenOut), + intentAccount, + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getFlexibleTermsInfo( + abi.encodePacked( + uint8(1), + address(0), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenOut), + intentAccount, + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getFlexibleTermsInfo( + abi.encodePacked( + uint8(1), + address(metaSwap), + address(tokenIn), + uint256(0), + uint8(_approveMode()), + address(tokenOut), + intentAccount, + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getFlexibleTermsInfo( + abi.encodePacked( + uint8(1), + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenOut), + address(0), + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getFlexibleTermsInfo( + abi.encodePacked( + uint8(1), + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenOut), + intentAccount, + uint256(0) + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + intentManager.getFlexibleTermsInfo( + abi.encodePacked( + uint8(1), + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenIn), + intentAccount, + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + intentManager.getFlexibleTermsInfo( + abi.encodePacked( + uint8(1), + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(4), + address(tokenOut), + intentAccount, + TOKEN_OUT_MIN + ) + ); + } + + function test_flexibleRejectsMismatchedApprovalShape() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 20); + + vm.expectRevert(MetaSwapIntentDelegationManager.ApprovalShapeNotAllowed.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(0, TOKEN_OUT_AMOUNT))); + } + + function test_flexibleRejectsInvalidApprovalCall() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 21); + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodeCall(IERC20.approve, (makeAddr("Other"), TOKEN_IN_AMOUNT)); + + vm.expectRevert(MetaSwapIntentDelegationManager.InvalidApproval.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); + } + + function test_flexibleRejectsInvalidSwapCall() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 22); + Execution[] memory executions_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); + executions_[1].target = makeAddr("OtherSwap"); + + vm.expectRevert(MetaSwapIntentDelegationManager.InvalidSwap.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); + } + + function test_flexibleRejectsNativeWrongBatchLength() public { + bytes memory terms_ = _flexibleTerms(address(0), _noneMode(), address(tokenOut), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 23); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = _nativeExecutions(TOKEN_OUT_AMOUNT)[0]; + executions_[1] = executions_[0]; + + vm.expectRevert(MetaSwapIntentDelegationManager.InvalidBatchLength.selector); + _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); + } + + function test_flexibleRedeemsNativeOutput() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(0), intentAccount); + Delegation memory delegation_ = _signIntent(terms_, 24); + uint256 nativeBefore_ = intentAccount.balance; + + 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)) + ); + + _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); + assertEq(intentAccount.balance, nativeBefore_ + TOKEN_OUT_AMOUNT); + } + + function test_disableDelegationRevertsWhenAlreadyDisabled() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 25); + + vm.prank(intentAccount); + intentManager.disableDelegation(delegation_); + + vm.prank(intentAccount); + vm.expectRevert(MetaSwapDelegationManagerBase.AlreadyDisabled.selector); + intentManager.disableDelegation(delegation_); + } + + function test_getExactAndFlexibleTermsInfoSucceed() public { + bytes32 hash_ = keccak256("batch"); + assertEq(intentManager.getExactTermsInfo(_exactTerms(hash_)), hash_); + + MetaSwapIntentDelegationManager.FlexibleTerms memory info_ = + intentManager.getFlexibleTermsInfo(_flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount)); + 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, intentAccount); + assertEq(info_.tokenOutMin, TOKEN_OUT_MIN); + } + + // -------- 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_hooklessFlexible() public { + bytes memory terms_ = abi.encodePacked( + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(MetaSwapFlexibleSettlementManagerBase.ApprovalMode.Approve), + address(tokenOut), + hooklessAccount, + TOKEN_OUT_MIN + ); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(hooklessManager), terms: terms_, args: hex"" }); + Delegation memory delegation_ = _signManager(hooklessManager, HOOKLESS_KEY, hooklessAccount, caveats_, 102); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20ExecutionsFor(hooklessAccount, 1, TOKEN_OUT_AMOUNT)); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("hookless flexible", gasBefore_ - gasleft()); + } + + function test_gas_intentExact() 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); + intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("intent ExactCalldata + DirectECDSA", gasBefore_ - gasleft()); + } + + function test_gas_intentExactERC1271() public { + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20ExecutionsFor(intent1271Account, 1, TOKEN_OUT_AMOUNT)); + bytes memory terms_ = _exactTerms(keccak256(encoded_)); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(intent1271Manager), terms: terms_, args: hex"" }); + Delegation memory delegation_ = _signManager(intent1271Manager, INTENT_1271_KEY, intent1271Account, caveats_, 105); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + intent1271Manager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("intent ExactCalldata + ERC1271", gasBefore_ - gasleft()); + } + + function test_gas_intentFlexible() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + 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); + intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("intent FlexibleSettlement + DirectECDSA", gasBefore_ - gasleft()); + } + + function test_gas_intentFlexibleERC1271() public { + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intent1271Account); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(intent1271Manager), terms: terms_, args: hex"" }); + Delegation memory delegation_ = _signManager(intent1271Manager, INTENT_1271_KEY, intent1271Account, caveats_, 106); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20ExecutionsFor(intent1271Account, 1, TOKEN_OUT_AMOUNT)); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, encoded_); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + intent1271Manager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("intent FlexibleSettlement + ERC1271", 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(MetaSwapIntentDelegationManager.Intent.ExactCalldata), executionHash_); + } + + function _flexibleTerms( + address tokenIn_, + MetaSwapIntentDelegationManager.ApprovalMode approvalMode_, + address tokenOut_, + address recipient_ + ) + private + view + returns (bytes memory) + { + return abi.encodePacked( + uint8(MetaSwapIntentDelegationManager.Intent.FlexibleSettlement), + address(metaSwap), + tokenIn_, + TOKEN_IN_AMOUNT, + uint8(approvalMode_), + tokenOut_, + recipient_, + TOKEN_OUT_MIN + ); + } + + function _signIntent(bytes memory terms_, uint256 salt_) private view returns (Delegation memory) { + return _signIntentWithKey(INTENT_KEY, terms_, salt_); + } + + 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(intentManager), terms: terms_, args: hex"" }); + return _signManager(intentManager, signerKey_, intentAccount, 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); + intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + 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(intentAccount, 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 (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.None; + } + + function _skipApprovalMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.SkipApproval; + } + + function _approveMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.Approve; + } + + function _resetApproveMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { + return MetaSwapIntentDelegationManager.ApprovalMode.ResetApprove; + } +} diff --git a/test/MetaSwapSpecializedDelegationManagers.t.sol b/test/MetaSwapSpecializedDelegationManagers.t.sol new file mode 100644 index 00000000..333ec5ec --- /dev/null +++ b/test/MetaSwapSpecializedDelegationManagers.t.sol @@ -0,0 +1,662 @@ +// 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 { MetaSwapFlexibleSettlementManagerBase } from "../src/MetaSwapFlexibleSettlementManagerBase.sol"; +import { MetaSwapExecutionBuilderDelegationManager } from "../src/MetaSwapExecutionBuilderDelegationManager.sol"; +import { MetaSwapHooklessDelegationManager } from "../src/MetaSwapHooklessDelegationManager.sol"; +import { EIP7702StatelessDeleGator } from "../src/EIP7702/EIP7702StatelessDeleGator.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"; + +contract SpecializedManagerMetaSwapMock 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 MetaSwapSpecializedDelegationManagersTest 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 HOOKLESS_KEY = 0xA11CE; + uint256 private constant HOOKLESS_1271_KEY = 0x1271; + uint256 private constant BUILDER_KEY = 0xB0B; + + EntryPoint private entryPoint; + SpecializedManagerMetaSwapMock private metaSwap; + BasicERC20 private tokenIn; + BasicERC20 private tokenOut; + MetaSwapHooklessDelegationManager private hooklessManager; + MetaSwapHooklessDelegationManager private hookless1271Manager; + MetaSwapExecutionBuilderDelegationManager private builderManager; + address private hooklessAccount; + address private hookless1271Account; + address private builderAccount; + address private relayer; + + function setUp() public { + entryPoint = new EntryPoint(); + metaSwap = new SpecializedManagerMetaSwapMock(); + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + relayer = makeAddr("Relayer"); + + hooklessManager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + hookless1271Manager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.ERC1271); + builderManager = new MetaSwapExecutionBuilderDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + + hooklessAccount = vm.addr(HOOKLESS_KEY); + hookless1271Account = vm.addr(HOOKLESS_1271_KEY); + builderAccount = vm.addr(BUILDER_KEY); + _installDeleGator(hooklessAccount, address(hooklessManager)); + _installDeleGator(hookless1271Account, address(hookless1271Manager)); + _installDeleGator(builderAccount, address(builderManager)); + + tokenIn.mint(hooklessAccount, 1_000 ether); + tokenIn.mint(hookless1271Account, 1_000 ether); + tokenIn.mint(builderAccount, 1_000 ether); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(hooklessAccount, 1_000 ether); + vm.deal(hookless1271Account, 1_000 ether); + vm.deal(builderAccount, 1_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + function test_hooklessManagerRedeemsValidatedExecutionBatch() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 1); + + _redeemHookless(delegation_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenIn.balanceOf(hooklessAccount), 900 ether); + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + assertTrue(hooklessManager.disabledDelegations(hooklessManager.getDelegationHash(delegation_))); + } + + function test_hooklessManagerRedeemsSkipApprovalExecution() public { + vm.prank(hooklessAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + bytes memory terms_ = _terms(address(tokenIn), _skipApprovalMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 12); + + _redeemHookless(delegation_, _erc20Executions(0, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + } + + function test_hooklessManagerRedeemsResetApproveExecution() public { + vm.prank(hooklessAccount); + tokenIn.approve(address(metaSwap), 1); + + bytes memory terms_ = _terms(address(tokenIn), _resetApproveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 13); + + _redeemHookless(delegation_, _erc20Executions(2, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + } + + function test_hooklessManagerRedeemsNativeInputExecution() public { + bytes memory terms_ = _terms(address(0), _noneMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 14); + uint256 nativeBefore_ = hooklessAccount.balance; + + _redeemHookless(delegation_, _nativeExecutions(TOKEN_OUT_AMOUNT)); + + assertEq(hooklessAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); + } + + function test_hooklessManagerSupportsERC1271SignatureOption() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hookless1271Account); + Delegation memory delegation_ = _sign(hookless1271Manager, HOOKLESS_1271_KEY, hookless1271Account, terms_, 2); + _redeemHookless(hookless1271Manager, delegation_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + assertEq(tokenOut.balanceOf(hookless1271Account), TOKEN_OUT_AMOUNT); + } + + function test_gas_hooklessManagerWithERC1271() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hookless1271Account); + Delegation memory delegation_ = _sign(hookless1271Manager, HOOKLESS_1271_KEY, hookless1271Account, terms_, 101); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + hookless1271Manager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("hookless manager + ERC1271", gasBefore_ - gasleft()); + } + + function test_gas_hooklessManagerWithDirectECDSA() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 102); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("hookless manager + direct ECDSA", gasBefore_ - gasleft()); + } + + function test_gas_executionBuilderManagerWithDirectECDSA() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 103); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, abi.encode("redeemer-route", abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_AMOUNT))); + + uint256 gasBefore_ = gasleft(); + vm.prank(relayer); + builderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("execution builder + direct ECDSA", gasBefore_ - gasleft()); + } + + function test_hooklessManagerRejectsInvalidExecutionWithoutCallingHooks() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 3); + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT); + executions_[1].target = makeAddr("UnapprovedSwapTarget"); + + vm.expectRevert(MetaSwapHooklessDelegationManager.InvalidSwap.selector); + _redeemHookless(delegation_, executions_); + } + + function test_builderManagerConstructsApproveAndSwapExecutions() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 4); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(tokenIn.balanceOf(builderAccount), 900 ether); + assertEq(tokenIn.allowance(builderAccount, address(metaSwap)), 0); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerConstructsResetApproveAndSwapExecutions() public { + vm.prank(builderAccount); + tokenIn.approve(address(metaSwap), 1); + + bytes memory terms_ = _terms(address(tokenIn), _resetApproveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 5); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(tokenIn.balanceOf(builderAccount), 900 ether); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerConstructsSkipApprovalSwapExecution() public { + vm.prank(builderAccount); + tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + bytes memory terms_ = _terms(address(tokenIn), _skipApprovalMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 6); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(tokenIn.balanceOf(builderAccount), 900 ether); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerConstructsNativeInputSwapExecution() public { + bytes memory terms_ = _terms(address(0), _noneMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 7); + uint256 nativeBefore_ = builderAccount.balance; + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + assertEq(builderAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(builderAccount), TOKEN_OUT_AMOUNT); + } + + function test_builderManagerRejectsNativeApprovalMode() public { + bytes memory terms_ = _terms(address(0), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 15); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_builderManagerRejectsNoneModeForERC20() public { + bytes memory terms_ = _terms(address(tokenIn), _noneMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 16); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_builderManagerRevertsAtomicallyForInsufficientOutput() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 8); + bytes32 delegationHash_ = builderManager.getDelegationHash(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.InsufficientOutput.selector); + _redeemBuilder(delegation_, TOKEN_OUT_MIN - 1); + + assertFalse(builderManager.disabledDelegations(delegationHash_)); + assertEq(tokenIn.balanceOf(builderAccount), 1_000 ether); + assertEq(tokenOut.balanceOf(builderAccount), 0); + } + + function test_successfulSettlementCannotBeReplayed() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 9); + + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + + vm.expectRevert(MetaSwapDelegationManagerBase.CannotUseADisabledDelegation.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_disableDelegationUsesSameOneShotState() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 10); + + vm.prank(builderAccount); + builderManager.disableDelegation(delegation_); + + vm.expectRevert(MetaSwapDelegationManagerBase.CannotUseADisabledDelegation.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_rejectsSignatureFromDifferentEOA() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, HOOKLESS_KEY, builderAccount, terms_, 11); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); + } + + function test_rejectsUnsupportedBatchShapeAndMode() public { + bytes[] memory emptyContexts_ = new bytes[](0); + ModeCode[] memory emptyModes_ = new ModeCode[](0); + vm.expectRevert(MetaSwapDelegationManagerBase.BatchDataLengthMismatch.selector); + hooklessManager.redeemDelegations(emptyContexts_, emptyModes_, emptyContexts_); + + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 17); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + modes_[0] = ModeLib.encodeSimpleSingle(); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidMode.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function test_rejectsDelegationChainAndInvalidRootFields() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 18); + bytes memory executionContext_ = + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = delegation_; + delegations_[1] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + bytes[] memory executionContexts_ = new bytes[](1); + executionContexts_[0] = executionContext_; + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidPermissionContext.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + + delegation_.delegate = makeAddr("WrongDelegate"); + (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidDelegate.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + + delegation_.delegate = address(0xa11); + delegation_.authority = bytes32(0); + (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidAuthority.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function test_rejectsNonManagerCaveatAndInvalidTerms() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 19); + bytes memory executionContext_ = + ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + + delegation_.caveats[0].enforcer = makeAddr("ExternalEnforcer"); + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = + _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidCaveat.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.getTermsInfo(new bytes(144)); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.getTermsInfo( + abi.encodePacked( + address(0), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenOut), + hooklessAccount, + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.getTermsInfo( + abi.encodePacked( + address(metaSwap), + address(tokenIn), + uint256(0), + uint8(_approveMode()), + address(tokenOut), + hooklessAccount, + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.getTermsInfo( + abi.encodePacked( + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenOut), + address(0), + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.getTermsInfo( + abi.encodePacked( + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenOut), + hooklessAccount, + uint256(0) + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + hooklessManager.getTermsInfo( + abi.encodePacked( + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(_approveMode()), + address(tokenIn), + hooklessAccount, + TOKEN_OUT_MIN + ) + ); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + hooklessManager.getTermsInfo( + abi.encodePacked( + address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, uint8(4), address(tokenOut), hooklessAccount, TOKEN_OUT_MIN + ) + ); + + // Mutating signed terms changes the hash, so signature validation fails before terms decoding. + delegation_.caveats[0].enforcer = address(hooklessManager); + delegation_.caveats[0].terms = new bytes(144); + (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function test_hooklessManagerRejectsNoneModeForERC20() public { + bytes memory terms_ = _terms(address(tokenIn), _noneMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 21); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + _redeemHookless(delegation_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); + } + + function test_hooklessManagerRejectsInvalidApprovalCall() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); + Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 22); + Execution[] memory executions_ = _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT); + executions_[0].callData = abi.encodeCall(IERC20.approve, (makeAddr("Other"), TOKEN_IN_AMOUNT)); + + vm.expectRevert(MetaSwapHooklessDelegationManager.InvalidApproval.selector); + _redeemHookless(delegation_, executions_); + } + + function test_rejectsInvalidERC1271Signature() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hookless1271Account); + Delegation memory delegation_ = _sign(hookless1271Manager, HOOKLESS_1271_KEY, hookless1271Account, terms_, 23); + // Valid length, wrong signer. + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(HOOKLESS_KEY, keccak256("not-the-delegation")); + delegation_.signature = abi.encodePacked(r_, s_, v_); + + (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( + delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) + ); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidERC1271Signature.selector); + hookless1271Manager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + function test_onlyDelegatorCanDisableDelegation() public { + bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); + Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 20); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidDelegator.selector); + builderManager.disableDelegation(delegation_); + } + + 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 _sign( + MetaSwapDelegationManagerBase manager_, + uint256 signerKey_, + address delegator_, + bytes memory terms_, + uint256 salt_ + ) + private + view + returns (Delegation memory delegation_) + { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(manager_), terms: terms_, args: hex"" }); + delegation_ = Delegation({ + delegate: address(0xa11), + delegator: delegator_, + authority: manager_.ROOT_AUTHORITY(), + caveats: caveats_, + salt: salt_, + signature: hex"" + }); + + bytes32 delegationHash_ = manager_.getDelegationHash(delegation_); + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(manager_.getDomainHash(), 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 _redeemHookless(Delegation memory delegation_, Execution[] memory executions_) private { + _redeemHookless(hooklessManager, delegation_, executions_); + } + + function _redeemHookless( + MetaSwapHooklessDelegationManager manager_, + Delegation memory delegation_, + Execution[] memory executions_ + ) + private + { + bytes[] memory executionContexts_ = new bytes[](1); + executionContexts_[0] = ExecutionLib.encodeBatch(executions_); + _redeem(manager_, delegation_, executionContexts_); + } + + function _redeemBuilder(Delegation memory delegation_, uint256 outputAmount_) private { + bytes[] memory executionContexts_ = new bytes[](1); + executionContexts_[0] = abi.encode("redeemer-route", abi.encode(IERC20(address(tokenOut)), outputAmount_)); + _redeem(builderManager, delegation_, executionContexts_); + } + + function _redeem( + MetaSwapDelegationManagerBase manager_, + Delegation memory delegation_, + bytes[] memory executionContexts_ + ) + private + { + (bytes[] memory permissionContexts_, ModeCode[] memory modes_,) = _redemptionInputs(delegation_, executionContexts_[0]); + + vm.prank(relayer); + manager_.redeemDelegations(permissionContexts_, modes_, executionContexts_); + } + + 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 _terms( + address tokenIn_, + MetaSwapFlexibleSettlementManagerBase.ApprovalMode approvalMode_, + address tokenOut_, + address recipient_ + ) + private + view + returns (bytes memory) + { + return abi.encodePacked( + address(metaSwap), tokenIn_, TOKEN_IN_AMOUNT, uint8(approvalMode_), tokenOut_, recipient_, TOKEN_OUT_MIN + ); + } + + function _erc20Executions( + uint8 approvalCount_, + address swapToken_, + uint256 swapAmount_, + 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(swapToken_), swapAmount_, 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 (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.None; + } + + function _skipApprovalMode() private pure returns (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.SkipApproval; + } + + function _approveMode() private pure returns (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.Approve; + } + + function _resetApproveMode() private pure returns (MetaSwapFlexibleSettlementManagerBase.ApprovalMode) { + return MetaSwapFlexibleSettlementManagerBase.ApprovalMode.ResetApprove; + } +} From 8f05cca423fec4579c11dda3960528f30dda001d Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Thu, 10 Sep 2026 23:06:18 +0200 Subject: [PATCH 05/13] feat: add specialized MetaSwap delegation managers --- .../MetaSwapSpecializedDelegationManagers.md | 20 +- src/GaslessSwapDelegationManager.sol | 271 ++++++ src/MetaSwapDelegationManagerBase.sol | 49 +- src/MetaSwapIntentDelegationManager.sol | 8 +- src/MetaSwapMinimalDelegationManager.sol | 270 ++++++ .../MetaSwap7702CalldataEnforcer.sol | 173 ++++ src/enforcers/MetaSwapApproveSwapEnforcer.sol | 163 ++++ .../MetaSwapBatchCalldataEnforcer.sol | 137 +++ src/enforcers/MetaSwapPrefundEnforcer.sol | 144 +++ .../MetaSwapTransferSwapEnforcer.sol | 147 +++ ...aSwapExecutionBuilderDelegationManager.sol | 11 +- .../MetaSwapFlexibleSettlementManagerBase.sol | 6 +- .../MetaSwapHooklessDelegationManager.sol | 11 +- test/GaslessSwapDelegationManager.t.sol | 477 +++++++++ test/MetaSwapIntentDelegationManager.t.sol | 102 +- test/MetaSwapMinimalDelegationManager.t.sol | 231 +++++ ...etaSwapSpecializedDelegationManagers.t.sol | 55 +- .../MetaSwap7702CalldataEnforcer.t.sol | 164 ++++ .../MetaSwapApproveSwapEnforcer.t.sol | 805 ++++++++++++++++ .../MetaSwapBatchCalldataEnforcer.t.sol | 129 +++ .../MetaSwapBatchDesignGasComparison.t.sol | 844 ++++++++++++++++ .../MetaSwapBatchHashGasComparison.t.sol | 861 +++++++++++++++++ .../AllowedCalldataLimitOrder.t.sol | 554 +++++++++++ test/helpers/DelegationMetaSwapAdapter2.t.sol | 906 ++++++++++++++++++ test/helpers/MetaSwapForwardingAdapter.t.sol | 486 ++++++++++ test/utils/MockLimitOrderRouter.sol | 58 ++ 26 files changed, 6981 insertions(+), 101 deletions(-) create mode 100644 src/GaslessSwapDelegationManager.sol create mode 100644 src/MetaSwapMinimalDelegationManager.sol create mode 100644 src/enforcers/MetaSwap7702CalldataEnforcer.sol create mode 100644 src/enforcers/MetaSwapApproveSwapEnforcer.sol create mode 100644 src/enforcers/MetaSwapBatchCalldataEnforcer.sol create mode 100644 src/enforcers/MetaSwapPrefundEnforcer.sol create mode 100644 src/enforcers/MetaSwapTransferSwapEnforcer.sol rename src/{ => experiments}/MetaSwapExecutionBuilderDelegationManager.sol (88%) rename src/{ => experiments}/MetaSwapFlexibleSettlementManagerBase.sol (93%) rename src/{ => experiments}/MetaSwapHooklessDelegationManager.sol (90%) create mode 100644 test/GaslessSwapDelegationManager.t.sol create mode 100644 test/MetaSwapMinimalDelegationManager.t.sol create mode 100644 test/enforcers/MetaSwap7702CalldataEnforcer.t.sol create mode 100644 test/enforcers/MetaSwapApproveSwapEnforcer.t.sol create mode 100644 test/enforcers/MetaSwapBatchCalldataEnforcer.t.sol create mode 100644 test/experiments/MetaSwapBatchDesignGasComparison.t.sol create mode 100644 test/experiments/MetaSwapBatchHashGasComparison.t.sol create mode 100644 test/experiments/allowed-calldata-limit-order/AllowedCalldataLimitOrder.t.sol create mode 100644 test/helpers/DelegationMetaSwapAdapter2.t.sol create mode 100644 test/helpers/MetaSwapForwardingAdapter.t.sol create mode 100644 test/utils/MockLimitOrderRouter.sol diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md index 35eff683..e05bca71 100644 --- a/documents/MetaSwapSpecializedDelegationManagers.md +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -50,10 +50,7 @@ Flexible-only. Redeemer supplies a complete ABI-encoded `Execution[]`. Validates Flexible-only. Redeemer supplies `abi.encode(aggregatorId, routeData)`. Manager constructs approvals and swap. -## Signature modes - -- `DirectECDSA` recovers the EIP-712 signer directly and requires it to equal the EIP-7702 delegator address. -- `ERC1271` calls the delegator's signature policy. +Signatures try ECDSA first (EOA and EIP-7702 ETH keys). If that misses, empty accounts revert; accounts with code fall back to ERC-1271. `disabledDelegations` is both cancel and one-shot consumption. Failed execution or insufficient output reverts atomically. @@ -61,19 +58,18 @@ Flexible-only. Redeemer supplies `abi.encode(aggregatorId, routeData)`. Manager Measured around `redeemDelegations` in `test/MetaSwapIntentDelegationManager.t.sol` and the specialized suite: -| Path | Gas | vs generic flexible | -|------|-----|---------------------| -| Generic DM + ExactBatch + LimitedCalls(1) | `230,987` | — | -| Generic DM + FlexibleSettlementEnforcer | `200,783` | baseline flexible | -| Hookless flexible (DirectECDSA) | `166,508` | −17.1% | -| Intent ExactCalldata | `158,997` | −31.2% vs exact generic | -| Intent FlexibleSettlement | `166,725` | −17.0% | +| Path | Gas | vs generic flexible | +| ----------------------------------------- | --------- | ----------------------- | +| Generic DM + ExactBatch + LimitedCalls(1) | `230,987` | — | +| Generic DM + FlexibleSettlementEnforcer | `200,783` | baseline flexible | +| Hookless flexible | `166,508` | −17.1% | +| Intent ExactCalldata | `158,997` | −31.2% vs exact generic | +| Intent FlexibleSettlement | `166,725` | −17.0% | Takeaways: - Flattened exact intent is the cheapest path: no second enforcer, no LimitedCalls nested mapping, no self-`execute` wrap. - Intent flexible matches hookless (~same gas); the unified manager does not pay a meaningful premium for dispatch. -- DirectECDSA vs ERC-1271 on hookless saved ~2k gas in earlier benches. ## Limitations diff --git a/src/GaslessSwapDelegationManager.sol b/src/GaslessSwapDelegationManager.sol new file mode 100644 index 00000000..9e1da123 --- /dev/null +++ b/src/GaslessSwapDelegationManager.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import { ICaveatEnforcer } from "./interfaces/ICaveatEnforcer.sol"; +import { IDelegationManager } from "./interfaces/IDelegationManager.sol"; +import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; +import { EncoderLib } from "./libraries/EncoderLib.sol"; +import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; +import { Caveat, Delegation, ModeCode } from "./utils/Types.sol"; + +/** + * @title GaslessSwapDelegationManager + * @notice Specialized ERC-7710 manager for exact one-shot swaps and one-shot limit orders. + * @dev Supports one root delegation and one redemption per call. Two signed caveat profiles are accepted: + * + * Gasless swap: + * 1. ExactExecutionEnforcer + * 2. LimitedCallsEnforcer with limit = 1 + * + * Limit order: + * 1. ExactExecutionEnforcer OR MetaSwap7702CalldataEnforcer + * 2. LimitedCallsEnforcer with limit = 1 + * 3. NativeBalanceChangeEnforcer OR ERC20BalanceChangeEnforcer, configured for a minimum increase + * of the root delegator's output-token balance. + * + * @dev Existing enforcers are reused unchanged. The manager invokes every `beforeHook`, executes once through + * `executeFromExecutor`, and invokes only the limit-order balance caveat's `afterHook`. Reverted executions or + * insufficient output revert the LimitedCalls counter too, so the same signed order remains retryable. + * @dev Designed for {EIP7702MultiManagerDeleGatorCore}: this manager must first be approved by the 7702 account. + */ +contract GaslessSwapDelegationManager is EIP712 { + using MessageHashUtils for bytes32; + + string public constant NAME = "GaslessSwapDelegationManager"; + string public constant VERSION = "1.0.0"; + string public constant DOMAIN_VERSION = "1"; + + bytes32 public constant ROOT_AUTHORITY = bytes32(type(uint256).max); + address public constant ANY_DELEGATE = address(0xa11); + + address public immutable exactExecutionEnforcer; + address public immutable metaSwap7702CalldataEnforcer; + address public immutable limitedCallsEnforcer; + address public immutable nativeBalanceChangeEnforcer; + address public immutable erc20BalanceChangeEnforcer; + + mapping(bytes32 delegationHash => bool isDisabled) public disabledDelegations; + + event RedeemedDelegation(address indexed rootDelegator, address indexed redeemer, bytes32 indexed delegationHash); + + error InvalidConfiguration(); + error InvalidProfile(); + error InvalidBalanceTerms(); + error InvalidDelegatorAccount(); + + modifier onlyDeleGator(address delegator_) { + if (delegator_ != msg.sender) revert IDelegationManager.InvalidDelegator(); + _; + } + + /** + * @notice Configures the only enforcers accepted by this manager. + * @param exactExecutionEnforcer_ ExactExecutionEnforcer deployment. + * @param metaSwap7702CalldataEnforcer_ MetaSwap7702CalldataEnforcer deployment. + * @param limitedCallsEnforcer_ LimitedCallsEnforcer deployment. + * @param nativeBalanceChangeEnforcer_ NativeBalanceChangeEnforcer deployment. + * @param erc20BalanceChangeEnforcer_ ERC20BalanceChangeEnforcer deployment. + */ + constructor( + address exactExecutionEnforcer_, + address metaSwap7702CalldataEnforcer_, + address limitedCallsEnforcer_, + address nativeBalanceChangeEnforcer_, + address erc20BalanceChangeEnforcer_ + ) + EIP712(NAME, DOMAIN_VERSION) + { + if ( + exactExecutionEnforcer_ == address(0) || metaSwap7702CalldataEnforcer_ == address(0) + || limitedCallsEnforcer_ == address(0) + || nativeBalanceChangeEnforcer_ == address(0) || erc20BalanceChangeEnforcer_ == address(0) + ) { + revert InvalidConfiguration(); + } + + exactExecutionEnforcer = exactExecutionEnforcer_; + metaSwap7702CalldataEnforcer = metaSwap7702CalldataEnforcer_; + limitedCallsEnforcer = limitedCallsEnforcer_; + nativeBalanceChangeEnforcer = nativeBalanceChangeEnforcer_; + erc20BalanceChangeEnforcer = erc20BalanceChangeEnforcer_; + + emit IDelegationManager.SetDomain(_domainSeparatorV4(), NAME, DOMAIN_VERSION, block.chainid, address(this)); + } + + /** + * @notice Permanently disables a signed swap or limit-order delegation. + * @param delegation_ Delegation being cancelled by its delegator. + */ + function disableDelegation(Delegation calldata delegation_) external onlyDeleGator(delegation_.delegator) { + bytes32 delegationHash_ = getDelegationHash(delegation_); + if (disabledDelegations[delegationHash_]) revert IDelegationManager.AlreadyDisabled(); + + disabledDelegations[delegationHash_] = true; + emit IDelegationManager.DisabledDelegation(delegationHash_, delegation_.delegator, delegation_.delegate, delegation_); + } + + /** + * @notice Redeems one exact gasless swap or one exact limit order. + * @param permissionContexts_ Exactly one `abi.encode(Delegation[])` containing one root delegation. + * @param modes_ Exactly one execution mode; ExactExecutionEnforcer requires single/default mode. + * @param executionCallDatas_ Exactly one packed single execution. + */ + function redeemDelegations( + bytes[] calldata permissionContexts_, + ModeCode[] calldata modes_, + bytes[] calldata executionCallDatas_ + ) + external + { + if (permissionContexts_.length != 1 || modes_.length != 1 || executionCallDatas_.length != 1) { + revert IDelegationManager.BatchDataLengthMismatch(); + } + + Delegation[] memory delegations_ = abi.decode(permissionContexts_[0], (Delegation[])); + if (delegations_.length != 1) revert InvalidProfile(); + + Delegation memory delegation_ = delegations_[0]; + if (delegation_.delegate != msg.sender && delegation_.delegate != ANY_DELEGATE) { + revert IDelegationManager.InvalidDelegate(); + } + if (delegation_.authority != ROOT_AUTHORITY) revert IDelegationManager.InvalidAuthority(); + if (delegation_.delegator.code.length == 0) revert InvalidDelegatorAccount(); + + bool isLimitOrder_ = _validateProfile(delegation_); + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + + _validateSignature( + delegation_.delegator, MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), delegationHash_), delegation_.signature + ); + if (disabledDelegations[delegationHash_]) revert IDelegationManager.CannotUseADisabledDelegation(); + + Caveat[] memory caveats_ = delegation_.caveats; + uint256 caveatCount_ = caveats_.length; + for (uint256 i; i < caveatCount_; ++i) { + _beforeHook(caveats_[i], modes_[0], executionCallDatas_[0], delegationHash_, delegation_.delegator); + } + + IDeleGatorCore(delegation_.delegator).executeFromExecutor(modes_[0], executionCallDatas_[0]); + + if (isLimitOrder_) { + Caveat memory balanceCaveat_ = caveats_[2]; + ICaveatEnforcer(balanceCaveat_.enforcer) + .afterHook( + balanceCaveat_.terms, + balanceCaveat_.args, + modes_[0], + executionCallDatas_[0], + delegationHash_, + delegation_.delegator, + msg.sender + ); + } + + emit RedeemedDelegation(delegation_.delegator, msg.sender, delegationHash_); + } + + /** + * @notice Returns this manager's EIP-712 domain separator. + * @return domainHash_ Current domain separator. + */ + function getDomainHash() public view returns (bytes32 domainHash_) { + domainHash_ = _domainSeparatorV4(); + } + + /** + * @notice Hashes a delegation without its signature or caveat args. + * @param delegation_ Delegation to hash. + * @return delegationHash_ Delegation struct hash. + */ + function getDelegationHash(Delegation calldata delegation_) public pure returns (bytes32 delegationHash_) { + delegationHash_ = EncoderLib._getDelegationHash(delegation_); + } + + function _validateProfile(Delegation memory delegation_) private view returns (bool isLimitOrder_) { + Caveat[] memory caveats_ = delegation_.caveats; + uint256 count_ = caveats_.length; + if (count_ != 2 && count_ != 3) revert InvalidProfile(); + + bool exactExecution_ = caveats_[0].enforcer == exactExecutionEnforcer; + bool flexibleMetaSwap_ = caveats_[0].enforcer == metaSwap7702CalldataEnforcer; + if ( + (!exactExecution_ && !flexibleMetaSwap_) || (count_ == 2 && !exactExecution_) + || caveats_[1].enforcer != limitedCallsEnforcer + || caveats_[0].args.length != 0 || caveats_[1].args.length != 0 || caveats_[1].terms.length != 32 + || _loadWord(caveats_[1].terms, 0) != 1 + ) { + revert InvalidProfile(); + } + + if (count_ == 2) return false; + + Caveat memory balanceCaveat_ = caveats_[2]; + if (balanceCaveat_.args.length != 0) revert InvalidProfile(); + + if (balanceCaveat_.enforcer == nativeBalanceChangeEnforcer) { + _validateNativeBalanceTerms(balanceCaveat_.terms, delegation_.delegator); + } else if (balanceCaveat_.enforcer == erc20BalanceChangeEnforcer) { + _validateERC20BalanceTerms(balanceCaveat_.terms, delegation_.delegator); + } else { + revert InvalidProfile(); + } + + return true; + } + + function _validateNativeBalanceTerms(bytes memory terms_, address delegator_) private pure { + // packed: bool enforceDecrease | address recipient | uint256 tokenOutMin + if (terms_.length != 53 || terms_[0] != bytes1(0) || _loadAddress(terms_, 1) != delegator_ || _loadWord(terms_, 21) == 0) { + revert InvalidBalanceTerms(); + } + } + + function _validateERC20BalanceTerms(bytes memory terms_, address delegator_) private pure { + // packed: bool enforceDecrease | address tokenOut | address recipient | uint256 tokenOutMin + if ( + terms_.length != 73 || terms_[0] != bytes1(0) || _loadAddress(terms_, 1) == address(0) + || _loadAddress(terms_, 21) != delegator_ || _loadWord(terms_, 41) == 0 + ) { + revert InvalidBalanceTerms(); + } + } + + function _beforeHook( + Caveat memory caveat_, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32 delegationHash_, + address delegator_ + ) + private + { + ICaveatEnforcer(caveat_.enforcer) + .beforeHook(caveat_.terms, caveat_.args, mode_, executionCallData_, delegationHash_, delegator_, msg.sender); + } + + function _validateSignature(address delegator_, bytes32 typedDataHash_, bytes memory signature_) private view { + (address recovered_, ECDSA.RecoverError error_,) = ECDSA.tryRecover(typedDataHash_, signature_); + if (error_ == ECDSA.RecoverError.NoError && recovered_ == delegator_) return; + + if (IERC1271(delegator_).isValidSignature(typedDataHash_, signature_) != ERC1271Lib.EIP1271_MAGIC_VALUE) { + revert IDelegationManager.InvalidERC1271Signature(); + } + } + + function _loadAddress(bytes memory data_, uint256 offset_) private pure returns (address value_) { + assembly { + value_ := shr(96, mload(add(add(data_, 0x20), offset_))) + } + } + + function _loadWord(bytes memory data_, uint256 offset_) private pure returns (uint256 value_) { + assembly { + value_ := mload(add(add(data_, 0x20), offset_)) + } + } +} diff --git a/src/MetaSwapDelegationManagerBase.sol b/src/MetaSwapDelegationManagerBase.sol index cf3ed40c..5eb49ec3 100644 --- a/src/MetaSwapDelegationManagerBase.sol +++ b/src/MetaSwapDelegationManagerBase.sol @@ -3,10 +3,9 @@ pragma solidity 0.8.23; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.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 { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { EncoderLib } from "./libraries/EncoderLib.sol"; import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; @@ -17,19 +16,16 @@ 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 { - enum SignatureMode { - DirectECDSA, - ERC1271 - } - string public constant DOMAIN_VERSION = "1"; bytes32 public constant ROOT_AUTHORITY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; address public constant ANY_DELEGATE = address(0xa11); - - SignatureMode public immutable signatureMode; + /// @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; @@ -37,13 +33,13 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { event DisabledDelegation( bytes32 indexed delegationHash, address indexed delegator, address indexed delegate, Delegation delegation ); - event RedeemedDelegation(address indexed rootDelegator, address indexed redeemer, Delegation delegation); + /// @dev `intent` is the first terms byte (`Intent` on MetaSwapIntentDelegationManager). + event RedeemedDelegation(address indexed rootDelegator, address indexed redeemer, bytes32 indexed delegationHash, uint8 intent); error AlreadyDisabled(); error BatchDataLengthMismatch(); error CannotUseADisabledDelegation(); error InsufficientOutput(); - error InvalidApprovalMode(); error InvalidAuthority(); error InvalidCaveat(); error InvalidDelegate(); @@ -54,9 +50,7 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { error InvalidPermissionContext(); error InvalidTerms(); - constructor(string memory name_, SignatureMode signatureMode_) EIP712(name_, DOMAIN_VERSION) { - signatureMode = signatureMode_; - } + constructor(string memory name_) EIP712(name_, DOMAIN_VERSION) { } /** * @notice Cancels a settlement delegation. @@ -75,6 +69,9 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { /** * @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. @@ -89,7 +86,7 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { if (permissionContexts_.length != 1 || modes_.length != 1 || executionContexts_.length != 1) { revert BatchDataLengthMismatch(); } - if (ModeCode.unwrap(modes_[0]) != ModeCode.unwrap(ModeLib.encodeSimpleBatch())) revert InvalidMode(); + 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(); @@ -105,9 +102,10 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { _validateSignature(delegation_, delegationHash_); disabledDelegations[delegationHash_] = true; - _executeIntent(delegation_.delegator, delegation_.caveats[0].terms, executionContexts_[0]); + bytes memory terms_ = delegation_.caveats[0].terms; + _executeIntent(delegation_.delegator, terms_, executionContexts_[0]); - emit RedeemedDelegation(delegation_.delegator, msg.sender, delegation_); + emit RedeemedDelegation(delegation_.delegator, msg.sender, delegationHash_, uint8(terms_[0])); } /** @@ -135,14 +133,17 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { 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 (signatureMode == SignatureMode.DirectECDSA) { - if (ECDSA.recover(typedDataHash_, delegation_.signature) != delegation_.delegator) { - revert InvalidEOASignature(); - } - } else { - bytes4 result_ = IERC1271(delegation_.delegator).isValidSignature(typedDataHash_, delegation_.signature); - if (result_ != ERC1271Lib.EIP1271_MAGIC_VALUE) revert InvalidERC1271Signature(); + if ( + IERC1271(delegation_.delegator).isValidSignature(typedDataHash_, delegation_.signature) + != ERC1271Lib.EIP1271_MAGIC_VALUE + ) { + revert InvalidERC1271Signature(); } } diff --git a/src/MetaSwapIntentDelegationManager.sol b/src/MetaSwapIntentDelegationManager.sol index e9f10500..238b1416 100644 --- a/src/MetaSwapIntentDelegationManager.sol +++ b/src/MetaSwapIntentDelegationManager.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.23; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; @@ -53,12 +52,13 @@ contract MetaSwapIntentDelegationManager is MetaSwapDelegationManagerBase { error ApprovalShapeNotAllowed(); error InvalidApproval(); + error InvalidApprovalMode(); error InvalidBatchLength(); error InvalidExecutionHash(); error InvalidIntent(); error InvalidSwap(); - constructor(SignatureMode signatureMode_) MetaSwapDelegationManagerBase(NAME, signatureMode_) { } + constructor() MetaSwapDelegationManagerBase(NAME) { } /** * @notice Decodes exact-calldata terms. @@ -120,7 +120,7 @@ contract MetaSwapIntentDelegationManager is MetaSwapDelegationManagerBase { bytes32 expectedHash_ = getExactTermsInfo(terms_); if (keccak256(executionContext_) != expectedHash_) revert InvalidExecutionHash(); - IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); + IDeleGatorCore(delegator_).executeFromExecutor(SIMPLE_BATCH_MODE, executionContext_); } function _executeFlexible(address delegator_, bytes memory terms_, bytes calldata executionContext_) private { @@ -129,7 +129,7 @@ contract MetaSwapIntentDelegationManager is MetaSwapDelegationManagerBase { _validateExecutions(executions_, termsInfo_); uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); - IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); + IDeleGatorCore(delegator_).executeFromExecutor(SIMPLE_BATCH_MODE, executionContext_); uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { diff --git a/src/MetaSwapMinimalDelegationManager.sol b/src/MetaSwapMinimalDelegationManager.sol new file mode 100644 index 00000000..96463152 --- /dev/null +++ b/src/MetaSwapMinimalDelegationManager.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.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 { IDelegationManager } from "./interfaces/IDelegationManager.sol"; +import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; +import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; +import { EncoderLib } from "./libraries/EncoderLib.sol"; +import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; +import { CallType, Caveat, Delegation, Execution, ExecType, ModeCode } from "./utils/Types.sol"; +import { CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT } from "./utils/Constants.sol"; + +/** + * @title MetaSwapMinimalDelegationManager + * @notice Minimal one-shot manager for exact gasless swaps and dynamically routed MetaSwap limit orders. + * @dev This manager does not invoke external caveat enforcers. Each delegation contains exactly one caveat whose enforcer is + * this manager and whose first terms byte selects one of two profiles: + * + * - Gasless exact (`0x00`): terms commit to `keccak256(mode || executionCallData)`. + * - Limit order (`0x01`): terms commit to MetaSwap, tokenIn, tokenOut, tokenInAmount, tokenOutMin and approval shape. + * + * For a limit order, `executionCallDatas[0]` is `abi.encode(string aggregatorId, bytes routeData)`. The manager constructs the + * approval-and-swap batch itself, so there is no caller-supplied batch to validate. Output balance is checked directly after + * execution. A reverting execution or insufficient output rolls back the one-shot marker. + */ +contract MetaSwapMinimalDelegationManager is EIP712 { + using MessageHashUtils for bytes32; + using ModeLib for ModeCode; + + string public constant NAME = "MetaSwapMinimalDelegationManager"; + string public constant VERSION = "1.0.0"; + string public constant DOMAIN_VERSION = "1"; + + bytes32 public constant ROOT_AUTHORITY = bytes32(type(uint256).max); + address public constant ANY_DELEGATE = address(0xa11); + + uint8 public constant GASLESS_EXACT_PROFILE = 0; + uint8 public constant LIMIT_ORDER_PROFILE = 1; + + uint256 private constant GASLESS_TERMS_LENGTH = 33; + uint256 private constant LIMIT_ORDER_TERMS_LENGTH = 126; + + mapping(bytes32 delegationHash => bool isDisabled) public disabledDelegations; + mapping(bytes32 delegationHash => bool isUsed) public usedDelegations; + + event RedeemedDelegation( + address indexed rootDelegator, address indexed redeemer, bytes32 indexed delegationHash, uint8 profile + ); + + error InvalidProfile(); + error InvalidTerms(); + error InvalidMode(); + error InvalidDelegatorAccount(); + error DelegationAlreadyUsed(); + error InsufficientOutput(uint256 minimum, uint256 received); + + struct LimitOrderTerms { + address metaSwap; + address tokenIn; + address tokenOut; + uint256 tokenInAmount; + uint256 tokenOutMin; + bool resetApproval; + } + + modifier onlyDeleGator(address delegator_) { + if (delegator_ != msg.sender) revert IDelegationManager.InvalidDelegator(); + _; + } + + constructor() EIP712(NAME, DOMAIN_VERSION) { + emit IDelegationManager.SetDomain(_domainSeparatorV4(), NAME, DOMAIN_VERSION, block.chainid, address(this)); + } + + /** + * @notice Permanently cancels a delegation. + * @param delegation_ Delegation being cancelled by its delegator. + */ + function disableDelegation(Delegation calldata delegation_) external onlyDeleGator(delegation_.delegator) { + bytes32 delegationHash_ = getDelegationHash(delegation_); + if (disabledDelegations[delegationHash_]) revert IDelegationManager.AlreadyDisabled(); + disabledDelegations[delegationHash_] = true; + emit IDelegationManager.DisabledDelegation(delegationHash_, delegation_.delegator, delegation_.delegate, delegation_); + } + + /** + * @notice Executes one exact gasless action or fills one dynamically routed limit order. + * @param permissionContexts_ Exactly one ABI-encoded array containing one root delegation. + * @param modes_ Gasless execution mode, or batch/default for the limit-order profile. + * @param executionCallDatas_ Exact ERC-7579 execution data for gasless, or ABI-encoded aggregator ID and route for a limit. + */ + function redeemDelegations( + bytes[] calldata permissionContexts_, + ModeCode[] calldata modes_, + bytes[] calldata executionCallDatas_ + ) + external + { + if (permissionContexts_.length != 1 || modes_.length != 1 || executionCallDatas_.length != 1) { + revert IDelegationManager.BatchDataLengthMismatch(); + } + + Delegation[] memory delegations_ = abi.decode(permissionContexts_[0], (Delegation[])); + if (delegations_.length != 1) revert InvalidProfile(); + + Delegation memory delegation_ = delegations_[0]; + if (delegation_.delegate != msg.sender && delegation_.delegate != ANY_DELEGATE) { + revert IDelegationManager.InvalidDelegate(); + } + if (delegation_.authority != ROOT_AUTHORITY) revert IDelegationManager.InvalidAuthority(); + if (delegation_.delegator.code.length == 0) revert InvalidDelegatorAccount(); + if ( + delegation_.caveats.length != 1 || delegation_.caveats[0].enforcer != address(this) + || delegation_.caveats[0].args.length != 0 || delegation_.caveats[0].terms.length == 0 + ) { + revert InvalidProfile(); + } + + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + _validateSignature( + delegation_.delegator, MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), delegationHash_), delegation_.signature + ); + if (disabledDelegations[delegationHash_]) revert IDelegationManager.CannotUseADisabledDelegation(); + if (usedDelegations[delegationHash_]) revert DelegationAlreadyUsed(); + + Caveat memory profileCaveat_ = delegation_.caveats[0]; + uint8 profile_ = uint8(profileCaveat_.terms[0]); + usedDelegations[delegationHash_] = true; + + if (profile_ == GASLESS_EXACT_PROFILE) { + _executeGasless(delegation_.delegator, profileCaveat_.terms, modes_[0], executionCallDatas_[0]); + } else if (profile_ == LIMIT_ORDER_PROFILE) { + _executeLimitOrder(delegation_.delegator, profileCaveat_.terms, modes_[0], executionCallDatas_[0]); + } else { + revert InvalidProfile(); + } + + emit RedeemedDelegation(delegation_.delegator, msg.sender, delegationHash_, profile_); + } + + /** + * @notice Returns the exact gasless commitment signed in profile terms. + */ + function getGaslessExecutionHash(ModeCode mode_, bytes calldata executionCallData_) public pure returns (bytes32) { + return keccak256(abi.encodePacked(ModeCode.unwrap(mode_), executionCallData_)); + } + + /** + * @notice Returns this manager's EIP-712 domain separator. + */ + function getDomainHash() public view returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @notice Returns the EIP-712 delegation struct hash. + */ + function getDelegationHash(Delegation calldata delegation_) public pure returns (bytes32) { + return EncoderLib._getDelegationHash(delegation_); + } + + function _executeGasless(address delegator_, bytes memory terms_, ModeCode mode_, bytes calldata executionCallData_) private { + if (terms_.length != GASLESS_TERMS_LENGTH) revert InvalidTerms(); + (CallType callType_, ExecType execType_,,) = mode_.decode(); + if ( + ExecType.unwrap(execType_) != ExecType.unwrap(EXECTYPE_DEFAULT) + || (CallType.unwrap(callType_) != CallType.unwrap(CALLTYPE_SINGLE) + && CallType.unwrap(callType_) != CallType.unwrap(CALLTYPE_BATCH)) + || bytes32(_loadWord(terms_, 1)) != getGaslessExecutionHash(mode_, executionCallData_) + ) { + revert InvalidMode(); + } + + IDeleGatorCore(delegator_).executeFromExecutor(mode_, executionCallData_); + } + + function _executeLimitOrder(address delegator_, bytes memory terms_, ModeCode mode_, bytes calldata routePayload_) private { + if (ModeCode.unwrap(mode_) != ModeCode.unwrap(ModeLib.encodeSimpleBatch())) revert InvalidMode(); + LimitOrderTerms memory order_ = _decodeLimitOrderTerms(terms_); + (string memory aggregatorId_, bytes memory routeData_) = abi.decode(routePayload_, (string, bytes)); + + uint256 balanceBefore_ = _balanceOf(order_.tokenOut, delegator_); + Execution[] memory executions_ = _buildLimitOrderExecutions(order_, aggregatorId_, routeData_); + IDeleGatorCore(delegator_).executeFromExecutor(mode_, ExecutionLib.encodeBatch(executions_)); + + uint256 balanceAfter_ = _balanceOf(order_.tokenOut, delegator_); + uint256 received_ = balanceAfter_ > balanceBefore_ ? balanceAfter_ - balanceBefore_ : 0; + if (received_ < order_.tokenOutMin) revert InsufficientOutput(order_.tokenOutMin, received_); + } + + function _decodeLimitOrderTerms(bytes memory terms_) private pure returns (LimitOrderTerms memory order_) { + if (terms_.length != LIMIT_ORDER_TERMS_LENGTH) revert InvalidTerms(); + + order_.metaSwap = _loadAddress(terms_, 1); + order_.tokenIn = _loadAddress(terms_, 21); + order_.tokenOut = _loadAddress(terms_, 41); + order_.tokenInAmount = _loadWord(terms_, 61); + order_.tokenOutMin = _loadWord(terms_, 93); + uint8 resetApprovalValue_ = uint8(terms_[125]); + if ( + order_.metaSwap == address(0) || order_.tokenInAmount == 0 || order_.tokenOutMin == 0 + || order_.tokenIn == order_.tokenOut || resetApprovalValue_ > 1 + || (order_.tokenIn == address(0) && resetApprovalValue_ == 1) + ) { + revert InvalidTerms(); + } + order_.resetApproval = resetApprovalValue_ == 1; + } + + function _buildLimitOrderExecutions( + LimitOrderTerms memory order_, + string memory aggregatorId_, + bytes memory routeData_ + ) + private + pure + returns (Execution[] memory executions_) + { + bytes memory swapCallData_ = + abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(order_.tokenIn), order_.tokenInAmount, routeData_)); + + if (order_.tokenIn == address(0)) { + executions_ = new Execution[](1); + executions_[0] = Execution({ target: order_.metaSwap, value: order_.tokenInAmount, callData: swapCallData_ }); + return executions_; + } + + uint256 swapIndex_ = order_.resetApproval ? 2 : 1; + executions_ = new Execution[](swapIndex_ + 1); + if (order_.resetApproval) { + executions_[0] = + Execution({ target: order_.tokenIn, value: 0, callData: abi.encodeCall(IERC20.approve, (order_.metaSwap, 0)) }); + } + executions_[swapIndex_ - 1] = Execution({ + target: order_.tokenIn, value: 0, callData: abi.encodeCall(IERC20.approve, (order_.metaSwap, order_.tokenInAmount)) + }); + executions_[swapIndex_] = Execution({ target: order_.metaSwap, value: 0, callData: swapCallData_ }); + } + + function _balanceOf(address token_, address account_) private view returns (uint256) { + return token_ == address(0) ? account_.balance : IERC20(token_).balanceOf(account_); + } + + function _validateSignature(address delegator_, bytes32 typedDataHash_, bytes memory signature_) private view { + (address recovered_, ECDSA.RecoverError error_,) = ECDSA.tryRecover(typedDataHash_, signature_); + if (error_ == ECDSA.RecoverError.NoError && recovered_ == delegator_) return; + if (IERC1271(delegator_).isValidSignature(typedDataHash_, signature_) != ERC1271Lib.EIP1271_MAGIC_VALUE) { + revert IDelegationManager.InvalidERC1271Signature(); + } + } + + function _loadAddress(bytes memory data_, uint256 offset_) private pure returns (address value_) { + assembly { + value_ := shr(96, mload(add(add(data_, 0x20), offset_))) + } + } + + function _loadWord(bytes memory data_, uint256 offset_) private pure returns (uint256 value_) { + assembly { + value_ := mload(add(add(data_, 0x20), offset_)) + } + } +} diff --git a/src/enforcers/MetaSwap7702CalldataEnforcer.sol b/src/enforcers/MetaSwap7702CalldataEnforcer.sol new file mode 100644 index 00000000..a24478aa --- /dev/null +++ b/src/enforcers/MetaSwap7702CalldataEnforcer.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { IMetaSwap } from "../helpers/interfaces/IMetaSwap.sol"; +import { IERC7821 } from "../interfaces/IERC7821.sol"; +import { Execution, ModeCode } from "../utils/Types.sol"; + +/** + * @title MetaSwap7702CalldataEnforcer + * @notice Validates a direct MetaSwap limit-order call nested inside one EIP-7702 self-call batch. + * @dev The DelegationManager sees one single execution targeting the delegator: + * `delegator.execute(BATCH_DEFAULT_MODE, innerExecutions)`. + * + * ERC-20 input supports one of two signed shapes: + * - `[approve(metaSwap, tokenInAmount), swap(...)]` + * - `[approve(metaSwap, 0), approve(metaSwap, tokenInAmount), swap(...)]` + * + * Native input requires `[swap{ value: tokenInAmount }(...)]`. + * + * @dev MetaSwap's aggregator ID and dynamic route bytes remain completely flexible. The enforcer reads only the static + * `tokenFrom` and `amount` ABI words, avoiding allocation or decoding of either dynamic argument. Output token and + * minimum output are intentionally enforced by a separate NativeBalanceChangeEnforcer or ERC20BalanceChangeEnforcer. + * Address zero represents native input. + */ +contract MetaSwap7702CalldataEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + + struct Terms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + bool resetApproval; + } + + uint256 private constant TERMS_LENGTH = 73; + uint256 private constant OUTER_CALL_MIN_LENGTH = 100; + uint256 private constant OUTER_DYNAMIC_OFFSET = 64; + uint256 private constant APPROVE_CALL_LENGTH = 68; + uint256 private constant SWAP_CALL_MIN_LENGTH = 132; + + /** + * @notice Validates the fixed security fields while leaving MetaSwap route selection flexible. + * @param terms_ Packed as `metaSwap(20) | tokenIn(20) | tokenInAmount(32) | resetApproval(1)`. + * @param mode_ DelegationManager execution mode; must be single/default. + * @param executionCallData_ Packed outer single execution targeting the delegator's 7702 account. + * @param delegator_ Root 7702 account that must be the outer execution target. + */ + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address delegator_, + address + ) + public + pure + override + onlySingleCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + Terms memory termsInfo_ = getTermsInfo(terms_); + (address outerTarget_, uint256 outerValue_, bytes calldata outerCallData_) = executionCallData_.decodeSingle(); + + require(outerTarget_ == delegator_ && outerValue_ == 0, "MetaSwap7702CalldataEnforcer:invalid-outer-execution"); + + bytes calldata innerExecutionCallData_ = _decodeOuterExecute(outerCallData_); + Execution[] calldata executions_ = innerExecutionCallData_.decodeBatch(); + + if (termsInfo_.tokenIn == address(0)) { + require(!termsInfo_.resetApproval && executions_.length == 1, "MetaSwap7702CalldataEnforcer:invalid-batch-length"); + _validateSwap( + executions_[0], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, termsInfo_.tokenInAmount + ); + return; + } + + if (termsInfo_.resetApproval) { + require(executions_.length == 3, "MetaSwap7702CalldataEnforcer:invalid-batch-length"); + _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 { + require(executions_.length == 2, "MetaSwap7702CalldataEnforcer:invalid-batch-length"); + _validateApproval(executions_[0], termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + _validateSwap(executions_[1], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, 0); + } + } + + /** + * @notice Decodes the packed signed terms. + * @return termsInfo_ Decoded MetaSwap, input token, input amount, and approval shape. + */ + function getTermsInfo(bytes calldata terms_) public pure returns (Terms memory termsInfo_) { + require(terms_.length == TERMS_LENGTH, "MetaSwap7702CalldataEnforcer:invalid-terms"); + + termsInfo_.metaSwap = address(bytes20(terms_[0:20])); + termsInfo_.tokenIn = address(bytes20(terms_[20:40])); + termsInfo_.tokenInAmount = uint256(bytes32(terms_[40:72])); + uint8 resetApprovalValue_ = uint8(terms_[72]); + + require( + termsInfo_.metaSwap != address(0) && termsInfo_.tokenInAmount != 0 && resetApprovalValue_ <= 1, + "MetaSwap7702CalldataEnforcer:invalid-terms" + ); + termsInfo_.resetApproval = resetApprovalValue_ == 1; + } + + function _decodeOuterExecute(bytes calldata callData_) private pure returns (bytes calldata innerExecutionCallData_) { + if (callData_.length < OUTER_CALL_MIN_LENGTH || bytes4(callData_[0:4]) != IERC7821.execute.selector) { + revert("MetaSwap7702CalldataEnforcer:invalid-outer-execution"); + } + if (ModeCode.unwrap(ModeCode.wrap(bytes32(callData_[4:36]))) != ModeCode.unwrap(ModeLib.encodeSimpleBatch())) { + revert("MetaSwap7702CalldataEnforcer:invalid-inner-mode"); + } + require(uint256(bytes32(callData_[36:68])) == OUTER_DYNAMIC_OFFSET, "MetaSwap7702CalldataEnforcer:invalid-inner-encoding"); + + uint256 innerLength_ = uint256(bytes32(callData_[68:100])); + require(innerLength_ <= callData_.length - OUTER_CALL_MIN_LENGTH, "MetaSwap7702CalldataEnforcer:invalid-inner-encoding"); + + uint256 paddedInnerLength_ = (innerLength_ + 31) & ~uint256(31); + require( + callData_.length == OUTER_CALL_MIN_LENGTH + paddedInnerLength_, "MetaSwap7702CalldataEnforcer:invalid-inner-encoding" + ); + + innerExecutionCallData_ = callData_[OUTER_CALL_MIN_LENGTH:OUTER_CALL_MIN_LENGTH + innerLength_]; + } + + function _validateApproval( + Execution calldata execution_, + address tokenIn_, + address metaSwap_, + uint256 expectedAmount_ + ) + private + pure + { + if ( + execution_.target != tokenIn_ || execution_.value != 0 || execution_.callData.length != APPROVE_CALL_LENGTH + || bytes4(execution_.callData[0:4]) != IERC20.approve.selector + || address(uint160(uint256(bytes32(execution_.callData[4:36])))) != metaSwap_ + || uint256(bytes32(execution_.callData[36:68])) != expectedAmount_ + ) { + revert("MetaSwap7702CalldataEnforcer:invalid-approval"); + } + } + + function _validateSwap( + Execution calldata execution_, + address metaSwap_, + address tokenIn_, + uint256 tokenInAmount_, + uint256 expectedValue_ + ) + private + pure + { + if ( + execution_.target != metaSwap_ || execution_.value != expectedValue_ + || execution_.callData.length < SWAP_CALL_MIN_LENGTH || bytes4(execution_.callData[0:4]) != IMetaSwap.swap.selector + || address(uint160(uint256(bytes32(execution_.callData[36:68])))) != tokenIn_ + || uint256(bytes32(execution_.callData[68:100])) != tokenInAmount_ + ) { + revert("MetaSwap7702CalldataEnforcer:invalid-swap"); + } + } +} diff --git a/src/enforcers/MetaSwapApproveSwapEnforcer.sol b/src/enforcers/MetaSwapApproveSwapEnforcer.sol new file mode 100644 index 00000000..7e85ca3d --- /dev/null +++ b/src/enforcers/MetaSwapApproveSwapEnforcer.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { IMetaSwap } from "../helpers/interfaces/IMetaSwap.sol"; +import { Execution, ModeCode } from "../utils/Types.sol"; + +/** + * @title MetaSwapApproveSwapEnforcer + * @notice Enforces a one-shot batch of ERC20 approval followed by a direct MetaSwap swap. + * @dev Supports `approve(amount), swap` and `approve(0), approve(amount), swap`. + */ +contract MetaSwapApproveSwapEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + + struct Terms { + address metaSwap; + address tokenIn; + address tokenOut; + uint256 tokenInAmount; + uint256 minTokenOut; + } + + struct OutputSnapshot { + uint256 balanceBefore; + bool active; + } + + mapping(address manager => mapping(bytes32 delegationHash => bool used)) public usedDelegations; + mapping(address manager => mapping(bytes32 delegationHash => OutputSnapshot snapshot)) public outputSnapshots; + + event DelegationExecuted(address indexed delegationManager, bytes32 indexed delegationHash, address indexed delegator); + + function beforeHook( + bytes calldata _terms, + bytes calldata, + ModeCode _mode, + bytes calldata _executionCallData, + bytes32 _delegationHash, + address _delegator, + address + ) + public + override + onlyBatchCallTypeMode(_mode) + onlyDefaultExecutionMode(_mode) + { + require(!usedDelegations[msg.sender][_delegationHash], "MetaSwapApproveSwapEnforcer:delegation-already-used"); + + Terms memory terms_ = abi.decode(_terms, (Terms)); + require(terms_.tokenIn != terms_.tokenOut, "MetaSwapApproveSwapEnforcer:identical-tokens"); + + Execution[] calldata executions_ = _executionCallData.decodeBatch(); + + if (executions_.length == 2) { + _validateApprove(executions_[0], terms_, terms_.tokenInAmount); + _validateSwap(executions_[1], terms_); + } else if (executions_.length == 3) { + _validateApprove(executions_[0], terms_, 0); + _validateApprove(executions_[1], terms_, terms_.tokenInAmount); + _validateSwap(executions_[2], terms_); + } else { + revert("MetaSwapApproveSwapEnforcer:invalid-batch-length"); + } + + OutputSnapshot storage snapshot_ = outputSnapshots[msg.sender][_delegationHash]; + require(!snapshot_.active, "MetaSwapApproveSwapEnforcer:output-snapshot-active"); + snapshot_.active = true; + snapshot_.balanceBefore = IERC20(terms_.tokenOut).balanceOf(_delegator); + usedDelegations[msg.sender][_delegationHash] = true; + + emit DelegationExecuted(msg.sender, _delegationHash, _delegator); + } + + function afterHook( + bytes calldata _terms, + bytes calldata, + ModeCode, + bytes calldata, + bytes32 _delegationHash, + address _delegator, + address + ) + public + override + { + Terms memory terms_ = abi.decode(_terms, (Terms)); + OutputSnapshot memory snapshot_ = outputSnapshots[msg.sender][_delegationHash]; + delete outputSnapshots[msg.sender][_delegationHash]; + + uint256 remainingAllowance_ = IERC20(terms_.tokenIn).allowance(_delegator, terms_.metaSwap); + require(remainingAllowance_ == 0, "MetaSwapApproveSwapEnforcer:remaining-allowance"); + + uint256 received_ = IERC20(terms_.tokenOut).balanceOf(_delegator) - snapshot_.balanceBefore; + require(received_ >= terms_.minTokenOut, "MetaSwapApproveSwapEnforcer:insufficient-output"); + } + + function _validateApprove(Execution calldata _execution, Terms memory _terms, uint256 _expectedAmount) private pure { + require( + _execution.target == _terms.tokenIn && _execution.value == 0, + "MetaSwapApproveSwapEnforcer:invalid-approve-call" + ); + require( + bytes4(_execution.callData[:4]) == IERC20.approve.selector, + "MetaSwapApproveSwapEnforcer:invalid-approve-call" + ); + + (address spender_, uint256 amount_) = abi.decode(_execution.callData[4:], (address, uint256)); + require( + spender_ == _terms.metaSwap && amount_ == _expectedAmount, + "MetaSwapApproveSwapEnforcer:invalid-approve-call" + ); + } + + function _validateSwap(Execution calldata _execution, Terms memory _terms) private pure { + require( + _execution.target == _terms.metaSwap && _execution.value == 0, + "MetaSwapApproveSwapEnforcer:invalid-swap-call" + ); + require( + bytes4(_execution.callData[:4]) == IMetaSwap.swap.selector, "MetaSwapApproveSwapEnforcer:invalid-swap-call" + ); + + (string memory aggregatorId_, IERC20 tokenFrom_, uint256 amountFrom_, bytes memory swapData_) = + abi.decode(_execution.callData[4:], (string, IERC20, uint256, bytes)); + aggregatorId_; + + require( + address(tokenFrom_) == _terms.tokenIn && amountFrom_ == _terms.tokenInAmount, + "MetaSwapApproveSwapEnforcer:invalid-swap-call" + ); + + (, // address(0) + IERC20 swapTokenFrom_, + IERC20 swapTokenTo_, + uint256 swapAmountFrom_, + uint256 amountTo_,, // metadata + uint256 feeAmount_,, // feeWallet + bool feeTo_ + ) = abi.decode( + abi.encodePacked(abi.encode(address(0)), swapData_), + (address, IERC20, IERC20, uint256, uint256, bytes, uint256, address, bool) + ); + + require( + swapTokenFrom_ == tokenFrom_ && address(swapTokenTo_) == _terms.tokenOut, + "MetaSwapApproveSwapEnforcer:invalid-swap-call" + ); + require(feeTo_ || feeAmount_ + swapAmountFrom_ == amountFrom_, "MetaSwapApproveSwapEnforcer:amount-from-mismatch"); + require(amountTo_ >= _terms.minTokenOut, "MetaSwapApproveSwapEnforcer:invalid-swap-call"); + } + + function getTermsInfo(bytes calldata _terms) external pure returns (Terms memory terms_) { + terms_ = abi.decode(_terms, (Terms)); + } + + function encodeTerms(Terms calldata _terms) external pure returns (bytes memory) { + return abi.encode(_terms); + } +} diff --git a/src/enforcers/MetaSwapBatchCalldataEnforcer.sol b/src/enforcers/MetaSwapBatchCalldataEnforcer.sol new file mode 100644 index 00000000..d2ab7282 --- /dev/null +++ b/src/enforcers/MetaSwapBatchCalldataEnforcer.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { IMetaSwap } from "../helpers/interfaces/IMetaSwap.sol"; +import { Execution, ModeCode } from "../utils/Types.sol"; + +/** + * @title MetaSwapBatchCalldataEnforcer + * @notice Restricts a direct DelegationManager batch while leaving MetaSwap route selection flexible. + * @dev Supported signed shapes: + * - Native: `[swap{ value: tokenInAmount }(...)]` + * - ERC-20: `[approve(metaSwap, tokenInAmount), swap(...)]` + * - ERC-20 reset: `[approve(metaSwap, 0), approve(metaSwap, tokenInAmount), swap(...)]` + * + * MetaSwap's dynamic `aggregatorId` and route `data` are never copied or decoded. The enforcer reads only the selector, + * `tokenFrom`, and `amount` words from swap calldata. Output constraints belong in a separate balance-change enforcer. + */ +contract MetaSwapBatchCalldataEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + + struct Terms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + bool resetApproval; + } + + uint256 private constant TERMS_LENGTH = 73; + uint256 private constant APPROVE_CALL_LENGTH = 68; + uint256 private constant SWAP_CALL_MIN_LENGTH = 132; + + /** + * @notice Validates the direct batch shape and all security-relevant static fields. + * @param terms_ Packed `metaSwap(20) | tokenIn(20) | tokenInAmount(32) | resetApproval(1)`. + * @param mode_ DelegationManager execution mode; must be batch/default. + * @param executionCallData_ ABI-encoded `Execution[]`. + */ + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + Terms memory termsInfo_ = getTermsInfo(terms_); + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + + if (termsInfo_.tokenIn == address(0)) { + require(!termsInfo_.resetApproval && executions_.length == 1, "MetaSwapBatchCalldataEnforcer:invalid-batch-length"); + _validateSwap(executions_[0], termsInfo_.metaSwap, address(0), termsInfo_.tokenInAmount, termsInfo_.tokenInAmount); + return; + } + + if (termsInfo_.resetApproval) { + require(executions_.length == 3, "MetaSwapBatchCalldataEnforcer:invalid-batch-length"); + _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 { + require(executions_.length == 2, "MetaSwapBatchCalldataEnforcer:invalid-batch-length"); + _validateApproval(executions_[0], termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); + _validateSwap(executions_[1], termsInfo_.metaSwap, termsInfo_.tokenIn, termsInfo_.tokenInAmount, 0); + } + } + + /** + * @notice Decodes the compact signed terms. + * @param terms_ Packed batch constraints. + * @return termsInfo_ Decoded MetaSwap, input token, amount, and approval shape. + */ + function getTermsInfo(bytes calldata terms_) public pure returns (Terms memory termsInfo_) { + require(terms_.length == TERMS_LENGTH, "MetaSwapBatchCalldataEnforcer:invalid-terms"); + + termsInfo_.metaSwap = address(bytes20(terms_[0:20])); + termsInfo_.tokenIn = address(bytes20(terms_[20:40])); + termsInfo_.tokenInAmount = uint256(bytes32(terms_[40:72])); + uint8 resetApprovalValue_ = uint8(terms_[72]); + require( + termsInfo_.metaSwap != address(0) && termsInfo_.tokenInAmount != 0 && resetApprovalValue_ <= 1, + "MetaSwapBatchCalldataEnforcer:invalid-terms" + ); + termsInfo_.resetApproval = resetApprovalValue_ == 1; + } + + 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 + || address(uint160(uint256(bytes32(callData_[4:36])))) != metaSwap_ + || uint256(bytes32(callData_[36:68])) != expectedAmount_ + ) { + revert("MetaSwapBatchCalldataEnforcer:invalid-approval"); + } + } + + 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 + || address(uint160(uint256(bytes32(callData_[36:68])))) != tokenIn_ + || uint256(bytes32(callData_[68:100])) != tokenInAmount_ + ) { + revert("MetaSwapBatchCalldataEnforcer:invalid-swap"); + } + } +} diff --git a/src/enforcers/MetaSwapPrefundEnforcer.sol b/src/enforcers/MetaSwapPrefundEnforcer.sol new file mode 100644 index 00000000..02ca9c85 --- /dev/null +++ b/src/enforcers/MetaSwapPrefundEnforcer.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { MetaSwapForwardingAdapter } from "../helpers/MetaSwapForwardingAdapter.sol"; +import { CallType, Execution, ModeCode } from "../utils/Types.sol"; +import { CALLTYPE_BATCH } from "../utils/Constants.sol"; + +/** + * @title MetaSwapPrefundEnforcer + * @notice Enforces a one-shot input transfer followed by a call to an immutable MetaSwap forwarding adapter. + * @dev ERC20 input is transferred with `transfer(adapter, amount)`. Native input is transferred with an empty + * call to the adapter carrying the exact amount. The second execution must call `swap` on the adapter and + * declare the same input token and amount. The adapter validates the signed route and settlement. + */ +contract MetaSwapPrefundEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + using ModeLib for ModeCode; + + uint256 private constant API_QUOTE_OFFSET = 5 * 32; + uint256 private constant MIN_SWAP_CALLDATA_LENGTH = 4 + API_QUOTE_OFFSET + 3 * 32; + + struct Terms { + address tokenIn; + uint256 tokenInAmount; + } + + MetaSwapForwardingAdapter public immutable adapter; + + mapping(address manager => mapping(bytes32 delegationHash => bool used)) public usedDelegations; + + event DelegationExecuted(address indexed delegationManager, bytes32 indexed delegationHash, address indexed delegator); + + /** + * @notice Binds this enforcer to one forwarding adapter. + */ + constructor(MetaSwapForwardingAdapter _adapter) { + require(address(_adapter) != address(0), "MetaSwapPrefundEnforcer:invalid-zero-address"); + adapter = _adapter; + } + + /** + * @notice Validates an atomic prefund-and-swap batch. + */ + function beforeHook( + bytes calldata _terms, + bytes calldata, + ModeCode _mode, + bytes calldata _executionCallData, + bytes32 _delegationHash, + address _delegator, + address + ) + public + override + onlyDefaultExecutionMode(_mode) + { + require(!usedDelegations[msg.sender][_delegationHash], "MetaSwapPrefundEnforcer:delegation-already-used"); + require( + CallType.unwrap(_mode.getCallType()) == CallType.unwrap(CALLTYPE_BATCH), + "MetaSwapPrefundEnforcer:invalid-call-type" + ); + + Terms memory terms_ = abi.decode(_terms, (Terms)); + require(terms_.tokenInAmount != 0, "MetaSwapPrefundEnforcer:invalid-zero-amount"); + + Execution[] calldata executions_ = _executionCallData.decodeBatch(); + require(executions_.length == 2, "MetaSwapPrefundEnforcer:invalid-batch-length"); + + _validatePrefund(executions_[0], terms_); + _validateSwap(executions_[1], terms_); + + usedDelegations[msg.sender][_delegationHash] = true; + emit DelegationExecuted(msg.sender, _delegationHash, _delegator); + } + + function _validatePrefund(Execution calldata _execution, Terms memory _terms) private view { + address adapter_ = address(adapter); + if (_terms.tokenIn == address(0)) { + if (_execution.target != adapter_ || _execution.value != _terms.tokenInAmount || _execution.callData.length != 0) { + revert("MetaSwapPrefundEnforcer:invalid-prefund-call"); + } + return; + } + + if (_execution.target != _terms.tokenIn || _execution.value != 0 || _execution.callData.length != 68) { + revert("MetaSwapPrefundEnforcer:invalid-prefund-call"); + } + + bytes calldata callData_ = _execution.callData; + require(bytes4(callData_[:4]) == IERC20.transfer.selector, "MetaSwapPrefundEnforcer:invalid-prefund-call"); + + address recipient_; + uint256 amount_; + assembly ("memory-safe") { + recipient_ := and(calldataload(add(callData_.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) + amount_ := calldataload(add(callData_.offset, 36)) + } + require( + recipient_ == adapter_ && amount_ == _terms.tokenInAmount, "MetaSwapPrefundEnforcer:invalid-prefund-call" + ); + } + + function _validateSwap(Execution calldata _execution, Terms memory _terms) private view { + bytes calldata callData_ = _execution.callData; + if ( + _execution.target != address(adapter) || _execution.value != 0 || callData_.length < MIN_SWAP_CALLDATA_LENGTH + || bytes4(callData_[:4]) != MetaSwapForwardingAdapter.swap.selector + ) { + revert("MetaSwapPrefundEnforcer:invalid-swap-call"); + } + + address tokenIn_; + uint256 tokenInAmount_; + uint256 quoteOffset_; + assembly ("memory-safe") { + tokenIn_ := and(calldataload(add(callData_.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) + tokenInAmount_ := calldataload(add(callData_.offset, 68)) + quoteOffset_ := calldataload(add(callData_.offset, 132)) + } + + if (tokenIn_ != _terms.tokenIn || tokenInAmount_ != _terms.tokenInAmount || quoteOffset_ != API_QUOTE_OFFSET) { + revert("MetaSwapPrefundEnforcer:invalid-swap-call"); + } + } + + /** + * @notice Decodes prefund terms. + */ + function getTermsInfo(bytes calldata _terms) external pure returns (Terms memory terms_) { + terms_ = abi.decode(_terms, (Terms)); + } + + /** + * @notice Encodes prefund terms. + */ + function encodeTerms(Terms calldata _terms) external pure returns (bytes memory) { + return abi.encode(_terms); + } +} diff --git a/src/enforcers/MetaSwapTransferSwapEnforcer.sol b/src/enforcers/MetaSwapTransferSwapEnforcer.sol new file mode 100644 index 00000000..fe1d2b73 --- /dev/null +++ b/src/enforcers/MetaSwapTransferSwapEnforcer.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { MetaSwapAdapter } from "../helpers/MetaSwapAdapter.sol"; +import { CallType, Execution, ModeCode } from "../utils/Types.sol"; +import { CALLTYPE_BATCH, CALLTYPE_SINGLE } from "../utils/Constants.sol"; + +/** + * @title MetaSwapTransferSwapEnforcer + * @notice Enforces a one-shot MetaSwap adapter execution. + * @dev MetaSwap API calldata remains flexible and is authenticated and decoded by the adapter. + * ERC-20 input requires an exact transfer-and-swap batch. Native input requires a single swap call whose + * execution value equals `tokenInAmount`. Address zero represents the native token. + */ +contract MetaSwapTransferSwapEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + using ModeLib for ModeCode; + + uint256 private constant API_QUOTE_OFFSET = 5 * 32; + uint256 private constant MIN_SWAP_CALLDATA_LENGTH = 4 + API_QUOTE_OFFSET + 3 * 32; + + struct Terms { + address adapter; + address tokenIn; + address tokenOut; + uint256 tokenInAmount; + uint256 minTokenOut; + } + + mapping(address manager => mapping(bytes32 delegationHash => bool used)) public usedDelegations; + + event DelegationExecuted(address indexed delegationManager, bytes32 indexed delegationHash, address indexed delegator); + + function beforeHook( + bytes calldata _terms, + bytes calldata, + ModeCode _mode, + bytes calldata _executionCallData, + bytes32 _delegationHash, + address _delegator, + address + ) + public + override + onlyDefaultExecutionMode(_mode) + { + require(!usedDelegations[msg.sender][_delegationHash], "MetaSwapTransferSwapEnforcer:delegation-already-used"); + + Terms memory terms_ = abi.decode(_terms, (Terms)); + require(terms_.adapter != address(0), "MetaSwapTransferSwapEnforcer:invalid-zero-address"); + require( + terms_.tokenInAmount != 0 && terms_.minTokenOut != 0, "MetaSwapTransferSwapEnforcer:invalid-zero-amount" + ); + require(terms_.tokenIn != terms_.tokenOut, "MetaSwapTransferSwapEnforcer:identical-tokens"); + + if (terms_.tokenIn == address(0)) { + require( + CallType.unwrap(_mode.getCallType()) == CallType.unwrap(CALLTYPE_SINGLE), + "MetaSwapTransferSwapEnforcer:invalid-call-type" + ); + (address target_, uint256 value_, bytes calldata callData_) = _executionCallData.decodeSingle(); + _validateSwap(target_, value_, callData_, terms_); + } else { + require( + CallType.unwrap(_mode.getCallType()) == CallType.unwrap(CALLTYPE_BATCH), + "MetaSwapTransferSwapEnforcer:invalid-call-type" + ); + Execution[] calldata executions_ = _executionCallData.decodeBatch(); + require(executions_.length == 2, "MetaSwapTransferSwapEnforcer:invalid-batch-length"); + + _validateTransfer(executions_[0], terms_); + _validateSwap(executions_[1].target, executions_[1].value, executions_[1].callData, terms_); + } + + usedDelegations[msg.sender][_delegationHash] = true; + + emit DelegationExecuted(msg.sender, _delegationHash, _delegator); + } + + function _validateTransfer(Execution calldata _execution, Terms memory _terms) private pure { + if (_execution.target != _terms.tokenIn || _execution.value != 0 || _execution.callData.length != 68) { + revert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); + } + require( + bytes4(_execution.callData[:4]) == IERC20.transfer.selector, + "MetaSwapTransferSwapEnforcer:invalid-transfer-call" + ); + + bytes calldata transferCallData_ = _execution.callData; + address recipient_; + uint256 amount_; + assembly ("memory-safe") { + recipient_ := and(calldataload(add(transferCallData_.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) + amount_ := calldataload(add(transferCallData_.offset, 36)) + } + require( + recipient_ == _terms.adapter && amount_ == _terms.tokenInAmount, + "MetaSwapTransferSwapEnforcer:invalid-transfer-call" + ); + } + + function _validateSwap(address _target, uint256 _value, bytes calldata _callData, Terms memory _terms) private pure { + uint256 expectedValue_ = _terms.tokenIn == address(0) ? _terms.tokenInAmount : 0; + if (_target != _terms.adapter || _value != expectedValue_ || _callData.length < MIN_SWAP_CALLDATA_LENGTH) { + revert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + } + require( + bytes4(_callData[:4]) == MetaSwapAdapter.swap.selector, "MetaSwapTransferSwapEnforcer:invalid-swap-call" + ); + + address tokenIn_; + address tokenOut_; + uint256 tokenInAmount_; + uint256 minTokenOut_; + uint256 quoteOffset_; + assembly ("memory-safe") { + tokenIn_ := and(calldataload(add(_callData.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) + tokenOut_ := and(calldataload(add(_callData.offset, 36)), 0xffffffffffffffffffffffffffffffffffffffff) + tokenInAmount_ := calldataload(add(_callData.offset, 68)) + minTokenOut_ := calldataload(add(_callData.offset, 100)) + quoteOffset_ := calldataload(add(_callData.offset, 132)) + } + + // Enforce the canonical five-word ABI head without copying the dynamic quote into memory. + require(quoteOffset_ == API_QUOTE_OFFSET, "MetaSwapTransferSwapEnforcer:invalid-swap-call"); + + if ( + tokenIn_ != _terms.tokenIn || tokenOut_ != _terms.tokenOut || tokenInAmount_ != _terms.tokenInAmount + || minTokenOut_ < _terms.minTokenOut + ) { + revert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + } + } + + function getTermsInfo(bytes calldata _terms) external pure returns (Terms memory terms_) { + terms_ = abi.decode(_terms, (Terms)); + } + + function encodeTerms(Terms calldata _terms) external pure returns (bytes memory) { + return abi.encode(_terms); + } +} diff --git a/src/MetaSwapExecutionBuilderDelegationManager.sol b/src/experiments/MetaSwapExecutionBuilderDelegationManager.sol similarity index 88% rename from src/MetaSwapExecutionBuilderDelegationManager.sol rename to src/experiments/MetaSwapExecutionBuilderDelegationManager.sol index a3145f86..5f075bf9 100644 --- a/src/MetaSwapExecutionBuilderDelegationManager.sol +++ b/src/experiments/MetaSwapExecutionBuilderDelegationManager.sol @@ -3,12 +3,11 @@ pragma solidity 0.8.23; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; -import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; -import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; -import { Execution } from "./utils/Types.sol"; +import { IMetaSwap } from "../helpers/interfaces/IMetaSwap.sol"; +import { IDeleGatorCore } from "../interfaces/IDeleGatorCore.sol"; +import { Execution } from "../utils/Types.sol"; /** * @title MetaSwapExecutionBuilderDelegationManager @@ -20,13 +19,13 @@ contract MetaSwapExecutionBuilderDelegationManager is MetaSwapFlexibleSettlement string public constant NAME = "MetaSwapExecutionBuilderDelegationManager"; - constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } + constructor() MetaSwapFlexibleSettlementManagerBase(NAME) { } function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { (string memory aggregatorId_, bytes memory routeData_) = abi.decode(executionContext_, (string, bytes)); Execution[] memory executions_ = _buildExecutions(termsInfo_, aggregatorId_, routeData_); - IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executions_.encodeBatch()); + IDeleGatorCore(delegator_).executeFromExecutor(SIMPLE_BATCH_MODE, executions_.encodeBatch()); } function _buildExecutions( diff --git a/src/MetaSwapFlexibleSettlementManagerBase.sol b/src/experiments/MetaSwapFlexibleSettlementManagerBase.sol similarity index 93% rename from src/MetaSwapFlexibleSettlementManagerBase.sol rename to src/experiments/MetaSwapFlexibleSettlementManagerBase.sol index 4e236d99..a4f33f09 100644 --- a/src/MetaSwapFlexibleSettlementManagerBase.sol +++ b/src/experiments/MetaSwapFlexibleSettlementManagerBase.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT AND Apache-2.0 pragma solidity 0.8.23; -import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; +import { MetaSwapDelegationManagerBase } from "../MetaSwapDelegationManagerBase.sol"; /** * @title MetaSwapFlexibleSettlementManagerBase @@ -28,7 +28,9 @@ abstract contract MetaSwapFlexibleSettlementManagerBase is MetaSwapDelegationMan uint256 internal constant TERMS_LENGTH = 145; - constructor(string memory name_, SignatureMode signatureMode_) MetaSwapDelegationManagerBase(name_, signatureMode_) { } + error InvalidApprovalMode(); + + constructor(string memory name_) MetaSwapDelegationManagerBase(name_) { } /** * @notice Decodes and validates packed settlement terms. diff --git a/src/MetaSwapHooklessDelegationManager.sol b/src/experiments/MetaSwapHooklessDelegationManager.sol similarity index 90% rename from src/MetaSwapHooklessDelegationManager.sol rename to src/experiments/MetaSwapHooklessDelegationManager.sol index d04008b7..18806b32 100644 --- a/src/MetaSwapHooklessDelegationManager.sol +++ b/src/experiments/MetaSwapHooklessDelegationManager.sol @@ -3,12 +3,11 @@ pragma solidity 0.8.23; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; -import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; -import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; -import { Execution } from "./utils/Types.sol"; +import { IMetaSwap } from "../helpers/interfaces/IMetaSwap.sol"; +import { IDeleGatorCore } from "../interfaces/IDeleGatorCore.sol"; +import { Execution } from "../utils/Types.sol"; /** * @title MetaSwapHooklessDelegationManager @@ -28,13 +27,13 @@ contract MetaSwapHooklessDelegationManager is MetaSwapFlexibleSettlementManagerB error InvalidBatchLength(); error InvalidSwap(); - constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } + constructor() MetaSwapFlexibleSettlementManagerBase(NAME) { } function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { Execution[] calldata executions_ = executionContext_.decodeBatch(); _validateExecutions(executions_, termsInfo_); - IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); + IDeleGatorCore(delegator_).executeFromExecutor(SIMPLE_BATCH_MODE, executionContext_); } function _validateExecutions(Execution[] calldata executions_, Terms memory termsInfo_) private pure { diff --git a/test/GaslessSwapDelegationManager.t.sol b/test/GaslessSwapDelegationManager.t.sol new file mode 100644 index 00000000..3583a69d --- /dev/null +++ b/test/GaslessSwapDelegationManager.t.sol @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import { BaseTest } from "./utils/BaseTest.t.sol"; +import { BasicERC20 } from "./utils/BasicERC20.t.sol"; +import { MockLimitOrderRouter } from "./utils/MockLimitOrderRouter.sol"; +import { Implementation, SignatureType } from "./utils/Types.t.sol"; +import { GaslessSwapDelegationManager } from "../src/GaslessSwapDelegationManager.sol"; +import { EIP7702MultiManagerDeleGator } from "../src/EIP7702/EIP7702MultiManagerDeleGator.sol"; +import { EIP7702MultiManagerDeleGatorCore } from "../src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol"; +import { ERC20BalanceChangeEnforcer } from "../src/enforcers/ERC20BalanceChangeEnforcer.sol"; +import { ExactExecutionEnforcer } from "../src/enforcers/ExactExecutionEnforcer.sol"; +import { LimitedCallsEnforcer } from "../src/enforcers/LimitedCallsEnforcer.sol"; +import { MetaSwap7702CalldataEnforcer } from "../src/enforcers/MetaSwap7702CalldataEnforcer.sol"; +import { NativeBalanceChangeEnforcer } from "../src/enforcers/NativeBalanceChangeEnforcer.sol"; +import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; +import { IERC7821 } from "../src/interfaces/IERC7821.sol"; +import { IMetaSwap } from "../src/helpers/interfaces/IMetaSwap.sol"; +import { EncoderLib } from "../src/libraries/EncoderLib.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../src/utils/Types.sol"; + +/** + * @title GaslessSwapDelegationManagerTest + * @notice Exercises both supported profiles through an EIP-7702 account that approves multiple delegation managers. + */ +contract GaslessSwapDelegationManagerTest is BaseTest { + using MessageHashUtils for bytes32; + + uint256 internal constant SWAP_AMOUNT = 1 ether; + uint256 internal constant TOKEN_OUT_MIN = 0.9 ether; + + ExactExecutionEnforcer internal exactExecutionEnforcer; + MetaSwap7702CalldataEnforcer internal metaSwap7702CalldataEnforcer; + LimitedCallsEnforcer internal limitedCallsEnforcer; + NativeBalanceChangeEnforcer internal nativeBalanceChangeEnforcer; + ERC20BalanceChangeEnforcer internal erc20BalanceChangeEnforcer; + GaslessSwapDelegationManager internal swapManager; + + EIP7702MultiManagerDeleGator internal multiManagerImplementation; + EIP7702MultiManagerDeleGator internal aliceAccount; + + BasicERC20 internal tokenIn; + BasicERC20 internal tokenOut; + MockLimitOrderRouter internal router; + + address internal alice; + address internal relayer; + + constructor() { + IMPLEMENTATION = Implementation.EIP7702Stateless; + SIGNATURE_TYPE = SignatureType.EOA; + } + + function setUp() public override { + super.setUp(); + + exactExecutionEnforcer = new ExactExecutionEnforcer(); + metaSwap7702CalldataEnforcer = new MetaSwap7702CalldataEnforcer(); + limitedCallsEnforcer = new LimitedCallsEnforcer(); + nativeBalanceChangeEnforcer = new NativeBalanceChangeEnforcer(); + erc20BalanceChangeEnforcer = new ERC20BalanceChangeEnforcer(); + swapManager = new GaslessSwapDelegationManager( + address(exactExecutionEnforcer), + address(metaSwap7702CalldataEnforcer), + address(limitedCallsEnforcer), + address(nativeBalanceChangeEnforcer), + address(erc20BalanceChangeEnforcer) + ); + + alice = users.alice.addr; + relayer = makeAddr("Relayer"); + + multiManagerImplementation = new EIP7702MultiManagerDeleGator(); + vm.etch(alice, bytes.concat(hex"ef0100", abi.encodePacked(address(multiManagerImplementation)))); + aliceAccount = EIP7702MultiManagerDeleGator(payable(alice)); + + // The same 7702 account retains the canonical manager and opts into the specialized manager. + vm.startPrank(alice); + aliceAccount.approveDelegationManager(IDelegationManager(address(delegationManager))); + aliceAccount.approveDelegationManager(IDelegationManager(address(swapManager))); + vm.stopPrank(); + + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + router = new MockLimitOrderRouter(); + + tokenIn.mint(alice, 100 ether); + tokenOut.mint(address(router), 100 ether); + vm.deal(address(router), 100 ether); + router.setERC20AmountOut(SWAP_AMOUNT); + router.setNativeAmountOut(SWAP_AMOUNT); + } + + function test_multiManagerAccountApprovesCanonicalAndSwapManagers() public { + assertTrue(aliceAccount.isApprovedDelegationManager(IDelegationManager(address(delegationManager)))); + assertTrue(aliceAccount.isApprovedDelegationManager(IDelegationManager(address(swapManager)))); + } + + function test_gaslessProfile_nativeInERC20Out() public { + Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); + Delegation memory delegation_ = _signDelegation(_gaslessCaveats(execution_)); + + uint256 tokenBefore_ = tokenOut.balanceOf(alice); + uint256 nativeBefore_ = alice.balance; + + _redeem(delegation_, execution_); + + assertEq(tokenOut.balanceOf(alice), tokenBefore_ + SWAP_AMOUNT); + assertEq(alice.balance, nativeBefore_ - SWAP_AMOUNT); + } + + function test_gaslessProfile_erc20InNativeOut() public { + Execution memory execution_ = _wrap7702Batch(_erc20InNativeOutExecutions()); + Delegation memory delegation_ = _signDelegation(_gaslessCaveats(execution_)); + + uint256 tokenBefore_ = tokenIn.balanceOf(alice); + uint256 nativeBefore_ = alice.balance; + + _redeem(delegation_, execution_); + + assertEq(tokenIn.balanceOf(alice), tokenBefore_ - SWAP_AMOUNT); + assertEq(alice.balance, nativeBefore_ + SWAP_AMOUNT); + } + + function test_gaslessProfile_replayReverts() public { + Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); + Delegation memory delegation_ = _signDelegation(_gaslessCaveats(execution_)); + + _redeem(delegation_, execution_); + + vm.expectRevert("LimitedCallsEnforcer:limit-exceeded"); + _redeem(delegation_, execution_); + } + + function test_limitOrder_erc20OutputEnforcesMinimumIncrease() public { + Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), + args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); + + uint256 balanceBefore_ = tokenOut.balanceOf(alice); + _redeem(delegation_, execution_); + + assertEq(tokenOut.balanceOf(alice), balanceBefore_ + SWAP_AMOUNT); + } + + function test_limitOrder_nativeOutputEnforcesMinimumIncrease() public { + Execution memory execution_ = _wrap7702Batch(_erc20InNativeOutExecutions()); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(nativeBalanceChangeEnforcer), terms: abi.encodePacked(false, alice, TOKEN_OUT_MIN), args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); + + uint256 balanceBefore_ = alice.balance; + _redeem(delegation_, execution_); + + assertEq(alice.balance, balanceBefore_ + SWAP_AMOUNT); + } + + function test_flexibleMetaSwapLimitOrder_erc20OneApproval() public { + Execution memory execution_ = + _wrap7702Batch(_metaSwapERC20Executions(false, "best-route", abi.encode(tokenOut, SWAP_AMOUNT))); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), + args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), false, balanceCaveat_)); + + _redeem(delegation_, execution_); + + assertEq(tokenIn.balanceOf(alice), 99 ether); + assertEq(tokenOut.balanceOf(alice), SWAP_AMOUNT); + } + + function test_flexibleMetaSwapLimitOrder_erc20ResetApproval() public { + vm.prank(alice); + tokenIn.approve(address(router), 1); + + Execution memory execution_ = + _wrap7702Batch(_metaSwapERC20Executions(true, "best-route", abi.encode(tokenOut, SWAP_AMOUNT))); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), + args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), true, balanceCaveat_)); + + _redeem(delegation_, execution_); + + assertEq(tokenIn.balanceOf(alice), 99 ether); + assertEq(tokenOut.balanceOf(alice), SWAP_AMOUNT); + assertEq(tokenIn.allowance(alice, address(router)), 0); + } + + function test_flexibleMetaSwapLimitOrder_nativeInput() public { + Execution memory execution_ = _wrap7702Batch(_metaSwapNativeExecutions("best-route", abi.encode(tokenOut, SWAP_AMOUNT))); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), + args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(0), false, balanceCaveat_)); + + uint256 nativeBefore_ = alice.balance; + _redeem(delegation_, execution_); + + assertEq(alice.balance, nativeBefore_ - SWAP_AMOUNT); + assertEq(tokenOut.balanceOf(alice), SWAP_AMOUNT); + } + + function test_flexibleMetaSwapLimitOrder_nativeOutput() public { + Execution memory execution_ = _wrap7702Batch( + _metaSwapERC20Executions(false, "best-route", abi.encode(IERC20(address(0)), SWAP_AMOUNT)) + ); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(nativeBalanceChangeEnforcer), terms: abi.encodePacked(false, alice, TOKEN_OUT_MIN), args: hex"" + }); + Delegation memory delegation_ = + _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), false, balanceCaveat_)); + + uint256 nativeBefore_ = alice.balance; + _redeem(delegation_, execution_); + + assertEq(tokenIn.balanceOf(alice), 99 ether); + assertEq(alice.balance, nativeBefore_ + SWAP_AMOUNT); + } + + function test_flexibleMetaSwapLimitOrder_badRouteCanRetryWithDifferentCalldata() public { + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), + args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), false, balanceCaveat_)); + + Execution memory badExecution_ = + _wrap7702Batch(_metaSwapERC20Executions(false, "bad", abi.encode(tokenOut, TOKEN_OUT_MIN - 1))); + vm.expectRevert("ERC20BalanceChangeEnforcer:insufficient-balance-increase"); + _redeem(delegation_, badExecution_); + + Execution memory goodExecution_ = + _wrap7702Batch(_metaSwapERC20Executions(false, "new-route", abi.encode(tokenOut, TOKEN_OUT_MIN))); + _redeem(delegation_, goodExecution_); + + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); + } + + function test_limitOrder_insufficientOutputRevertsAndRemainsRetryable() public { + router.setERC20AmountOut(TOKEN_OUT_MIN - 1); + + Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), + args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); + bytes32 delegationHash_ = swapManager.getDelegationHash(delegation_); + + vm.expectRevert("ERC20BalanceChangeEnforcer:insufficient-balance-increase"); + _redeem(delegation_, execution_); + + assertEq(limitedCallsEnforcer.callCounts(address(swapManager), delegationHash_), 0); + assertFalse( + erc20BalanceChangeEnforcer.isLocked( + erc20BalanceChangeEnforcer.getHashKey(address(swapManager), address(tokenOut), delegationHash_) + ) + ); + + router.setERC20AmountOut(TOKEN_OUT_MIN); + _redeem(delegation_, execution_); + + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); + assertEq(limitedCallsEnforcer.callCounts(address(swapManager), delegationHash_), 1); + } + + function test_limitOrder_rejectsBalanceRecipientOtherThanDelegator() public { + Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); + Caveat memory balanceCaveat_ = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut), makeAddr("OtherRecipient"), TOKEN_OUT_MIN), + args: hex"" + }); + Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); + + vm.expectRevert(GaslessSwapDelegationManager.InvalidBalanceTerms.selector); + _redeem(delegation_, execution_); + } + + function test_rejectsUnapprovedManagerAtAccountBoundary() public { + GaslessSwapDelegationManager unapprovedManager_ = new GaslessSwapDelegationManager( + address(exactExecutionEnforcer), + address(metaSwap7702CalldataEnforcer), + address(limitedCallsEnforcer), + address(nativeBalanceChangeEnforcer), + address(erc20BalanceChangeEnforcer) + ); + Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); + Delegation memory delegation_ = _signDelegationFor(unapprovedManager_, _gaslessCaveats(execution_)); + + vm.expectRevert(EIP7702MultiManagerDeleGatorCore.NotDelegationManager.selector); + _redeemThrough(unapprovedManager_, delegation_, execution_); + } + + function _nativeInERC20OutExecutions() internal view returns (Execution[] memory executions_) { + executions_ = new Execution[](1); + executions_[0] = Execution({ + target: address(router), + value: SWAP_AMOUNT, + callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) + }); + } + + function _erc20InNativeOutExecutions() internal view returns (Execution[] memory executions_) { + executions_ = new Execution[](2); + executions_[0] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), SWAP_AMOUNT)) + }); + executions_[1] = Execution({ + target: address(router), + value: 0, + callData: abi.encodeCall( + MockLimitOrderRouter.swapERC20ForNative, (IERC20(address(tokenIn)), SWAP_AMOUNT, payable(alice)) + ) + }); + } + + function _metaSwapERC20Executions( + bool resetApproval_, + string memory aggregatorId_, + bytes memory route_ + ) + internal + view + returns (Execution[] memory executions_) + { + uint256 swapIndex_ = resetApproval_ ? 2 : 1; + executions_ = new Execution[](swapIndex_ + 1); + if (resetApproval_) { + executions_[0] = + Execution({ target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), 0)) }); + } + executions_[swapIndex_ - 1] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), SWAP_AMOUNT)) + }); + executions_[swapIndex_] = Execution({ + target: address(router), + value: 0, + callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(address(tokenIn)), SWAP_AMOUNT, route_)) + }); + } + + function _metaSwapNativeExecutions( + string memory aggregatorId_, + bytes memory route_ + ) + internal + view + returns (Execution[] memory executions_) + { + executions_ = new Execution[](1); + executions_[0] = Execution({ + target: address(router), + value: SWAP_AMOUNT, + callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(address(0)), SWAP_AMOUNT, route_)) + }); + } + + function _wrap7702Batch(Execution[] memory executions_) internal view returns (Execution memory execution_) { + execution_ = Execution({ + target: alice, + value: 0, + callData: abi.encodeCall(IERC7821.execute, (ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(executions_))) + }); + } + + function _gaslessCaveats(Execution memory execution_) internal view returns (Caveat[] memory caveats_) { + caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ + enforcer: address(exactExecutionEnforcer), + terms: ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + args: hex"" + }); + caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + } + + function _limitOrderCaveats( + Execution memory execution_, + Caveat memory balanceCaveat_ + ) + internal + view + returns (Caveat[] memory caveats_) + { + Caveat[] memory gaslessCaveats_ = _gaslessCaveats(execution_); + caveats_ = new Caveat[](3); + caveats_[0] = gaslessCaveats_[0]; + caveats_[1] = gaslessCaveats_[1]; + caveats_[2] = balanceCaveat_; + } + + function _dynamicLimitOrderCaveats( + address tokenIn_, + bool resetApproval_, + Caveat memory balanceCaveat_ + ) + internal + view + returns (Caveat[] memory caveats_) + { + caveats_ = new Caveat[](3); + caveats_[0] = Caveat({ + enforcer: address(metaSwap7702CalldataEnforcer), + terms: abi.encodePacked(address(router), tokenIn_, SWAP_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)), + args: hex"" + }); + caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + caveats_[2] = balanceCaveat_; + } + + function _signDelegation(Caveat[] memory caveats_) internal view returns (Delegation memory delegation_) { + delegation_ = _signDelegationFor(swapManager, caveats_); + } + + function _signDelegationFor( + GaslessSwapDelegationManager manager_, + Caveat[] memory caveats_ + ) + internal + view + returns (Delegation memory delegation_) + { + delegation_ = Delegation({ + delegate: ANY_DELEGATE, delegator: alice, authority: ROOT_AUTHORITY, caveats: caveats_, salt: 0, signature: hex"" + }); + + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(manager_.getDomainHash(), delegationHash_); + delegation_.signature = signHash(users.alice, typedDataHash_); + } + + function _redeem(Delegation memory delegation_, Execution memory execution_) internal { + _redeemThrough(swapManager, delegation_, execution_); + } + + function _redeemThrough( + GaslessSwapDelegationManager manager_, + Delegation memory delegation_, + Execution memory execution_ + ) + internal + { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = singleDefaultMode; + + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + vm.prank(relayer); + manager_.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); + } +} diff --git a/test/MetaSwapIntentDelegationManager.t.sol b/test/MetaSwapIntentDelegationManager.t.sol index ef5462e0..f47ebbff 100644 --- a/test/MetaSwapIntentDelegationManager.t.sol +++ b/test/MetaSwapIntentDelegationManager.t.sol @@ -10,9 +10,9 @@ import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { MetaSwapDelegationManagerBase } from "../src/MetaSwapDelegationManagerBase.sol"; -import { MetaSwapFlexibleSettlementManagerBase } from "../src/MetaSwapFlexibleSettlementManagerBase.sol"; -import { MetaSwapHooklessDelegationManager } from "../src/MetaSwapHooklessDelegationManager.sol"; import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegationManager.sol"; +import { MetaSwapFlexibleSettlementManagerBase } from "../src/experiments/MetaSwapFlexibleSettlementManagerBase.sol"; +import { MetaSwapHooklessDelegationManager } from "../src/experiments/MetaSwapHooklessDelegationManager.sol"; import { DelegationManager } from "../src/DelegationManager.sol"; import { EIP7702StatelessDeleGator } from "../src/EIP7702/EIP7702StatelessDeleGator.sol"; import { ExactExecutionBatchEnforcer } from "../src/enforcers/ExactExecutionBatchEnforcer.sol"; @@ -22,6 +22,30 @@ 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 IntentManager1271Account { + 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 IntentManagerMetaSwapMock is IMetaSwap { using SafeERC20 for IERC20; @@ -96,8 +120,8 @@ contract MetaSwapIntentDelegationManagerTest is Test { exactBatchEnforcer = new ExactExecutionBatchEnforcer(); limitedCallsEnforcer = new LimitedCallsEnforcer(); flexibleEnforcer = new MetaSwapFlexibleSettlementEnforcer(); - hooklessManager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); - intentManager = new MetaSwapIntentDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + hooklessManager = new MetaSwapHooklessDelegationManager(); + intentManager = new MetaSwapIntentDelegationManager(); genericAccount = vm.addr(GENERIC_KEY); hooklessAccount = vm.addr(HOOKLESS_KEY); @@ -124,6 +148,13 @@ contract MetaSwapIntentDelegationManagerTest is Test { bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 1); + vm.expectEmit(true, true, true, true, address(intentManager)); + emit MetaSwapDelegationManagerBase.RedeemedDelegation( + intentAccount, + relayer, + intentManager.getDelegationHash(delegation_), + uint8(MetaSwapIntentDelegationManager.Intent.ExactCalldata) + ); _redeemIntent(delegation_, encoded_); assertEq(tokenIn.balanceOf(intentAccount), 900 ether); @@ -205,10 +236,64 @@ contract MetaSwapIntentDelegationManagerTest is Test { bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); Delegation memory delegation_ = _signIntentWithKey(HOOKLESS_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(intentManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); + Delegation memory delegation_ = _signManager(intentManager, HOOKLESS_KEY, eoa_, caveats_, 40); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); _redeemIntent(delegation_, encoded_); } + function test_exactRedeemsWithERC1271Fallback() public { + IntentManager1271Account account_ = new IntentManager1271Account(); + 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(intentManager), terms: terms_, args: hex"" }); + Delegation memory delegation_ = Delegation({ + delegate: address(0xa11), + delegator: address(account_), + authority: intentManager.ROOT_AUTHORITY(), + caveats: caveats_, + salt: 41, + signature: hex"" + }); + bytes32 typedDataHash_ = + MessageHashUtils.toTypedDataHash(intentManager.getDomainHash(), intentManager.getDelegationHash(delegation_)); + delegation_.signature = abi.encodePacked(typedDataHash_); + + _redeemIntent(delegation_, encoded_); + + assertEq(tokenOut.balanceOf(address(account_)), TOKEN_OUT_AMOUNT); + } + + function test_exactRejectsInvalidERC1271Signature() public { + IntentManager1271Account account_ = new IntentManager1271Account(); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(intentManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); + Delegation memory delegation_ = Delegation({ + delegate: address(0xa11), + delegator: address(account_), + authority: intentManager.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 { @@ -216,6 +301,13 @@ contract MetaSwapIntentDelegationManagerTest is Test { Delegation memory delegation_ = _signIntent(terms_, 10); bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); + vm.expectEmit(true, true, true, true, address(intentManager)); + emit MetaSwapDelegationManagerBase.RedeemedDelegation( + intentAccount, + relayer, + intentManager.getDelegationHash(delegation_), + uint8(MetaSwapIntentDelegationManager.Intent.FlexibleSettlement) + ); _redeemIntent(delegation_, encoded_); assertEq(tokenIn.balanceOf(intentAccount), 900 ether); @@ -289,7 +381,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { bytes memory terms_ = _flexibleTerms(address(0), _approveMode(), address(tokenOut), intentAccount); Delegation memory delegation_ = _signIntent(terms_, 17); - vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + vm.expectRevert(MetaSwapIntentDelegationManager.InvalidApprovalMode.selector); _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); } diff --git a/test/MetaSwapMinimalDelegationManager.t.sol b/test/MetaSwapMinimalDelegationManager.t.sol new file mode 100644 index 00000000..6cb61ec7 --- /dev/null +++ b/test/MetaSwapMinimalDelegationManager.t.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { BaseTest } from "./utils/BaseTest.t.sol"; +import { BasicERC20 } from "./utils/BasicERC20.t.sol"; +import { MockLimitOrderRouter } from "./utils/MockLimitOrderRouter.sol"; +import { Implementation, SignatureType } from "./utils/Types.t.sol"; +import { EIP7702MultiManagerDeleGator } from "../src/EIP7702/EIP7702MultiManagerDeleGator.sol"; +import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; +import { IMetaSwap } from "../src/helpers/interfaces/IMetaSwap.sol"; +import { EncoderLib } from "../src/libraries/EncoderLib.sol"; +import { MetaSwapMinimalDelegationManager } from "../src/MetaSwapMinimalDelegationManager.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../src/utils/Types.sol"; + +contract MetaSwapMinimalDelegationManagerTest is BaseTest { + uint256 internal constant TOKEN_IN_AMOUNT = 1 ether; + uint256 internal constant TOKEN_OUT_MIN = 0.9 ether; + + MetaSwapMinimalDelegationManager internal minimalManager; + EIP7702MultiManagerDeleGator internal multiManagerImplementation; + EIP7702MultiManagerDeleGator internal aliceAccount; + + BasicERC20 internal tokenIn; + BasicERC20 internal tokenOut; + MockLimitOrderRouter internal metaSwap; + + address internal alice; + address internal relayer; + + constructor() { + IMPLEMENTATION = Implementation.EIP7702Stateless; + SIGNATURE_TYPE = SignatureType.EOA; + } + + function setUp() public override { + super.setUp(); + + minimalManager = new MetaSwapMinimalDelegationManager(); + multiManagerImplementation = new EIP7702MultiManagerDeleGator(); + alice = users.alice.addr; + relayer = makeAddr("Relayer"); + + vm.etch(alice, bytes.concat(hex"ef0100", abi.encodePacked(address(multiManagerImplementation)))); + aliceAccount = EIP7702MultiManagerDeleGator(payable(alice)); + vm.startPrank(alice); + aliceAccount.approveDelegationManager(IDelegationManager(address(delegationManager))); + aliceAccount.approveDelegationManager(IDelegationManager(address(minimalManager))); + vm.stopPrank(); + + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + metaSwap = new MockLimitOrderRouter(); + + tokenIn.mint(alice, 100 ether); + tokenOut.mint(address(metaSwap), 100 ether); + vm.deal(address(metaSwap), 100 ether); + metaSwap.setERC20AmountOut(TOKEN_IN_AMOUNT); + } + + function test_gaslessExact_executesSignedExecution() public { + Execution memory execution_ = Execution({ + target: address(metaSwap), + value: TOKEN_IN_AMOUNT, + callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) + }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes32 executionHash_ = minimalManager.getGaslessExecutionHash(singleDefaultMode, executionCallData_); + Delegation memory delegation_ = _sign(_gaslessTerms(executionHash_)); + + _redeem(delegation_, singleDefaultMode, executionCallData_); + + assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); + } + + function test_gaslessExact_rejectsTamperedExecution() public { + Execution memory execution_ = Execution({ + target: address(metaSwap), + value: TOKEN_IN_AMOUNT, + callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) + }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + Delegation memory delegation_ = + _sign(_gaslessTerms(minimalManager.getGaslessExecutionHash(singleDefaultMode, executionCallData_))); + + execution_.value++; + vm.expectRevert(MetaSwapMinimalDelegationManager.InvalidMode.selector); + _redeem(delegation_, singleDefaultMode, ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData)); + } + + function test_gaslessExact_isOneShot() public { + Execution memory execution_ = Execution({ + target: address(metaSwap), + value: TOKEN_IN_AMOUNT, + callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) + }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + Delegation memory delegation_ = + _sign(_gaslessTerms(minimalManager.getGaslessExecutionHash(singleDefaultMode, executionCallData_))); + + _redeem(delegation_, singleDefaultMode, executionCallData_); + + vm.expectRevert(MetaSwapMinimalDelegationManager.DelegationAlreadyUsed.selector); + _redeem(delegation_, singleDefaultMode, executionCallData_); + } + + function test_limitOrder_erc20OneApproval() public { + Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), false)); + + _fill(delegation_, "best-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); + + assertEq(tokenIn.balanceOf(alice), 99 ether); + assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); + } + + function test_limitOrder_erc20ResetApproval() public { + vm.prank(alice); + tokenIn.approve(address(metaSwap), 1); + Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), true)); + + _fill(delegation_, "best-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); + + assertEq(tokenIn.balanceOf(alice), 99 ether); + assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); + assertEq(tokenIn.allowance(alice, address(metaSwap)), 0); + } + + function test_limitOrder_nativeInput() public { + Delegation memory delegation_ = _sign(_limitTerms(address(0), address(tokenOut), false)); + uint256 nativeBefore_ = alice.balance; + + _fill(delegation_, "best-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); + + assertEq(alice.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); + } + + function test_limitOrder_nativeOutput() public { + Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(0), false)); + uint256 nativeBefore_ = alice.balance; + + _fill(delegation_, "best-route", abi.encode(IERC20(address(0)), TOKEN_IN_AMOUNT)); + + assertEq(tokenIn.balanceOf(alice), 99 ether); + assertEq(alice.balance, nativeBefore_ + TOKEN_IN_AMOUNT); + } + + function test_limitOrder_managerOverridesCallerSuppliedInputTokenAndAmount() public { + BasicERC20 otherToken_ = new BasicERC20(address(this), "Other", "OTHER", 0); + otherToken_.mint(alice, 10 ether); + vm.prank(alice); + otherToken_.approve(address(metaSwap), 10 ether); + Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), false)); + + // The route payload contains no tokenFrom or amount fields used to construct the MetaSwap call. + _fill(delegation_, "caller-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); + + assertEq(otherToken_.balanceOf(alice), 10 ether); + assertEq(tokenIn.balanceOf(alice), 99 ether); + } + + function test_limitOrder_insufficientOutputCanRetryWithNewRoute() public { + Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), false)); + + vm.expectRevert( + abi.encodeWithSelector(MetaSwapMinimalDelegationManager.InsufficientOutput.selector, TOKEN_OUT_MIN, TOKEN_OUT_MIN - 1) + ); + _fill(delegation_, "bad-route", abi.encode(tokenOut, TOKEN_OUT_MIN - 1)); + + _fill(delegation_, "new-route", abi.encode(tokenOut, TOKEN_OUT_MIN)); + + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); + } + + function _gaslessTerms(bytes32 executionHash_) private view returns (Caveat[] memory caveats_) { + caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ + enforcer: address(minimalManager), + terms: abi.encodePacked(bytes1(minimalManager.GASLESS_EXACT_PROFILE()), executionHash_), + args: hex"" + }); + } + + function _limitTerms(address tokenIn_, address tokenOut_, bool resetApproval_) private view returns (Caveat[] memory caveats_) { + caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ + enforcer: address(minimalManager), + terms: abi.encodePacked( + bytes1(minimalManager.LIMIT_ORDER_PROFILE()), + address(metaSwap), + tokenIn_, + tokenOut_, + TOKEN_IN_AMOUNT, + TOKEN_OUT_MIN, + bytes1(resetApproval_ ? 0x01 : 0x00) + ), + args: hex"" + }); + } + + function _sign(Caveat[] memory caveats_) private view returns (Delegation memory delegation_) { + delegation_ = Delegation({ + delegate: ANY_DELEGATE, delegator: alice, authority: ROOT_AUTHORITY, caveats: caveats_, salt: 0, signature: hex"" + }); + + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(minimalManager.getDomainHash(), delegationHash_); + delegation_.signature = signHash(users.alice, typedDataHash_); + } + + function _fill(Delegation memory delegation_, string memory aggregatorId_, bytes memory routeData_) private { + _redeem(delegation_, batchDefaultMode, abi.encode(aggregatorId_, routeData_)); + } + + function _redeem(Delegation memory delegation_, ModeCode mode_, bytes memory executionCallData_) private { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = mode_; + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = executionCallData_; + + vm.prank(relayer); + minimalManager.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); + } +} diff --git a/test/MetaSwapSpecializedDelegationManagers.t.sol b/test/MetaSwapSpecializedDelegationManagers.t.sol index 965248db..2ca7bf52 100644 --- a/test/MetaSwapSpecializedDelegationManagers.t.sol +++ b/test/MetaSwapSpecializedDelegationManagers.t.sol @@ -10,9 +10,9 @@ import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { MetaSwapDelegationManagerBase } from "../src/MetaSwapDelegationManagerBase.sol"; -import { MetaSwapFlexibleSettlementManagerBase } from "../src/MetaSwapFlexibleSettlementManagerBase.sol"; -import { MetaSwapExecutionBuilderDelegationManager } from "../src/MetaSwapExecutionBuilderDelegationManager.sol"; -import { MetaSwapHooklessDelegationManager } from "../src/MetaSwapHooklessDelegationManager.sol"; +import { MetaSwapFlexibleSettlementManagerBase } from "../src/experiments/MetaSwapFlexibleSettlementManagerBase.sol"; +import { MetaSwapExecutionBuilderDelegationManager } from "../src/experiments/MetaSwapExecutionBuilderDelegationManager.sol"; +import { MetaSwapHooklessDelegationManager } from "../src/experiments/MetaSwapHooklessDelegationManager.sol"; import { DelegationManager } from "../src/DelegationManager.sol"; import { EIP7702StatelessDeleGator } from "../src/EIP7702/EIP7702StatelessDeleGator.sol"; import { MetaSwapFlexibleSettlementEnforcer } from "../src/enforcers/MetaSwapFlexibleSettlementEnforcer.sol"; @@ -63,7 +63,6 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { uint256 private constant TOKEN_OUT_AMOUNT = 200 ether; uint256 private constant STANDARD_KEY = 0x5151; uint256 private constant HOOKLESS_KEY = 0xA11CE; - uint256 private constant HOOKLESS_1271_KEY = 0x1271; uint256 private constant BUILDER_KEY = 0xB0B; EntryPoint private entryPoint; @@ -73,11 +72,9 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { DelegationManager private standardManager; MetaSwapFlexibleSettlementEnforcer private standardEnforcer; MetaSwapHooklessDelegationManager private hooklessManager; - MetaSwapHooklessDelegationManager private hookless1271Manager; MetaSwapExecutionBuilderDelegationManager private builderManager; address private standardAccount; address private hooklessAccount; - address private hookless1271Account; address private builderAccount; address private relayer; @@ -90,27 +87,22 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { standardManager = new DelegationManager(address(this)); standardEnforcer = new MetaSwapFlexibleSettlementEnforcer(); - hooklessManager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); - hookless1271Manager = new MetaSwapHooklessDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.ERC1271); - builderManager = new MetaSwapExecutionBuilderDelegationManager(MetaSwapDelegationManagerBase.SignatureMode.DirectECDSA); + hooklessManager = new MetaSwapHooklessDelegationManager(); + builderManager = new MetaSwapExecutionBuilderDelegationManager(); standardAccount = vm.addr(STANDARD_KEY); hooklessAccount = vm.addr(HOOKLESS_KEY); - hookless1271Account = vm.addr(HOOKLESS_1271_KEY); builderAccount = vm.addr(BUILDER_KEY); _installDeleGator(standardAccount, address(standardManager)); _installDeleGator(hooklessAccount, address(hooklessManager)); - _installDeleGator(hookless1271Account, address(hookless1271Manager)); _installDeleGator(builderAccount, address(builderManager)); tokenIn.mint(standardAccount, 1_000 ether); tokenIn.mint(hooklessAccount, 1_000 ether); - tokenIn.mint(hookless1271Account, 1_000 ether); tokenIn.mint(builderAccount, 1_000 ether); tokenOut.mint(address(metaSwap), 10_000 ether); vm.deal(standardAccount, 1_000 ether); vm.deal(hooklessAccount, 1_000 ether); - vm.deal(hookless1271Account, 1_000 ether); vm.deal(builderAccount, 1_000 ether); vm.deal(address(metaSwap), 10_000 ether); } @@ -161,14 +153,6 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { assertEq(tokenOut.balanceOf(hooklessAccount), TOKEN_OUT_AMOUNT); } - function test_hooklessManagerSupportsERC1271SignatureOption() public { - bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hookless1271Account); - Delegation memory delegation_ = _sign(hookless1271Manager, HOOKLESS_1271_KEY, hookless1271Account, terms_, 2); - _redeemHookless(hookless1271Manager, delegation_, _erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)); - - assertEq(tokenOut.balanceOf(hookless1271Account), TOKEN_OUT_AMOUNT); - } - function test_gas_standardManagerWithSettlementEnforcer() public { bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), standardAccount); Delegation memory delegation_ = _signStandard(STANDARD_KEY, standardAccount, terms_, 100); @@ -182,20 +166,7 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { emit log_named_uint("standard manager + enforcer", gasBefore_ - gasleft()); } - function test_gas_hooklessManagerWithERC1271() public { - bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hookless1271Account); - Delegation memory delegation_ = _sign(hookless1271Manager, HOOKLESS_1271_KEY, hookless1271Account, terms_, 101); - (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( - delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, address(tokenIn), TOKEN_IN_AMOUNT, TOKEN_OUT_AMOUNT)) - ); - - uint256 gasBefore_ = gasleft(); - vm.prank(relayer); - hookless1271Manager.redeemDelegations(permissionContexts_, modes_, executionContexts_); - emit log_named_uint("hookless manager + ERC1271", gasBefore_ - gasleft()); - } - - function test_gas_hooklessManagerWithDirectECDSA() public { + function test_gas_hooklessManager() public { bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), hooklessAccount); Delegation memory delegation_ = _sign(hooklessManager, HOOKLESS_KEY, hooklessAccount, terms_, 102); (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs( @@ -205,10 +176,10 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { uint256 gasBefore_ = gasleft(); vm.prank(relayer); hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); - emit log_named_uint("hookless manager + direct ECDSA", gasBefore_ - gasleft()); + emit log_named_uint("hookless manager", gasBefore_ - gasleft()); } - function test_gas_executionBuilderManagerWithDirectECDSA() public { + function test_gas_executionBuilderManager() public { bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 103); (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = @@ -217,7 +188,7 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { uint256 gasBefore_ = gasleft(); vm.prank(relayer); builderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); - emit log_named_uint("execution builder + direct ECDSA", gasBefore_ - gasleft()); + emit log_named_uint("execution builder", gasBefore_ - gasleft()); } function test_hooklessManagerRejectsInvalidExecutionWithoutCallingHooks() public { @@ -282,7 +253,7 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { bytes memory terms_ = _terms(address(0), _approveMode(), address(tokenOut), builderAccount); Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 15); - vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + vm.expectRevert(MetaSwapFlexibleSettlementManagerBase.InvalidApprovalMode.selector); _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); } @@ -290,7 +261,7 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { bytes memory terms_ = _terms(address(tokenIn), _noneMode(), address(tokenOut), builderAccount); Delegation memory delegation_ = _sign(builderManager, BUILDER_KEY, builderAccount, terms_, 16); - vm.expectRevert(MetaSwapDelegationManagerBase.InvalidApprovalMode.selector); + vm.expectRevert(MetaSwapFlexibleSettlementManagerBase.InvalidApprovalMode.selector); _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); } @@ -332,7 +303,7 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { bytes memory terms_ = _terms(address(tokenIn), _approveMode(), address(tokenOut), builderAccount); Delegation memory delegation_ = _sign(builderManager, HOOKLESS_KEY, builderAccount, terms_, 11); - vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidERC1271Signature.selector); _redeemBuilder(delegation_, TOKEN_OUT_AMOUNT); } @@ -402,7 +373,7 @@ contract MetaSwapSpecializedDelegationManagersTest is Test { delegation_.caveats[0].enforcer = address(hooklessManager); delegation_.caveats[0].terms = new bytes(144); (permissionContexts_, modes_, executionContexts_) = _redemptionInputs(delegation_, executionContext_); - vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidERC1271Signature.selector); hooklessManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); } diff --git a/test/enforcers/MetaSwap7702CalldataEnforcer.t.sol b/test/enforcers/MetaSwap7702CalldataEnforcer.t.sol new file mode 100644 index 00000000..aa763857 --- /dev/null +++ b/test/enforcers/MetaSwap7702CalldataEnforcer.t.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { MetaSwap7702CalldataEnforcer } from "../../src/enforcers/MetaSwap7702CalldataEnforcer.sol"; +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { IERC7821 } from "../../src/interfaces/IERC7821.sol"; +import { Execution, ModeCode } from "../../src/utils/Types.sol"; + +contract MetaSwap7702CalldataEnforcerTest is Test { + uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; + + MetaSwap7702CalldataEnforcer internal enforcer; + address internal delegator; + address internal metaSwap; + address internal tokenIn; + + ModeCode internal singleDefaultMode = ModeLib.encodeSimpleSingle(); + + function setUp() public { + enforcer = new MetaSwap7702CalldataEnforcer(); + delegator = makeAddr("Delegator"); + metaSwap = makeAddr("MetaSwap"); + tokenIn = makeAddr("TokenIn"); + } + + function test_erc20OneApproval_acceptsArbitraryDynamicRoute() public view { + _enforce(_terms(tokenIn, false), _outer(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "aggregator-a", hex"01"))); + _enforce( + _terms(tokenIn, false), _outer(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "different-aggregator", new bytes(512))) + ); + } + + function test_erc20ResetApproval_acceptsSignedThreeCallShape() public view { + _enforce(_terms(tokenIn, true), _outer(_erc20Inner(true, tokenIn, TOKEN_IN_AMOUNT, "aggregator", new bytes(96)))); + } + + function test_nativeInput_acceptsOneSwapWithExactValue() public view { + Execution[] memory executions_ = new Execution[](1); + executions_[0] = _swapExecution(address(0), TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, "aggregator", hex"1234"); + + _enforce(_terms(address(0), false), _outer(executions_)); + } + + function test_revertsOnDifferentSwapTokenFrom() public { + Execution memory outer_ = _outer(_erc20Inner(false, makeAddr("OtherToken"), TOKEN_IN_AMOUNT, "aggregator", hex"")); + + vm.expectRevert("MetaSwap7702CalldataEnforcer:invalid-swap"); + _enforce(_terms(tokenIn, false), outer_); + } + + function test_revertsOnDifferentSwapAmount() public { + Execution memory outer_ = _outer(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT - 1, "aggregator", hex"")); + + vm.expectRevert("MetaSwap7702CalldataEnforcer:invalid-swap"); + _enforce(_terms(tokenIn, false), outer_); + } + + function test_revertsWhenResetShapeDoesNotMatchTerms() public { + Execution memory outer_ = _outer(_erc20Inner(true, tokenIn, TOKEN_IN_AMOUNT, "aggregator", hex"")); + + vm.expectRevert("MetaSwap7702CalldataEnforcer:invalid-batch-length"); + _enforce(_terms(tokenIn, false), outer_); + } + + function test_revertsWhenApprovalAmountDoesNotMatch() public { + Execution[] memory executions_ = _erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "aggregator", hex""); + executions_[0].callData = abi.encodeCall(IERC20.approve, (metaSwap, TOKEN_IN_AMOUNT - 1)); + + vm.expectRevert("MetaSwap7702CalldataEnforcer:invalid-approval"); + _enforce(_terms(tokenIn, false), _outer(executions_)); + } + + function test_revertsWhenOuterTargetIsNotDelegator() public { + Execution memory outer_ = _outer(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "aggregator", hex"")); + outer_.target = makeAddr("OtherTarget"); + + vm.expectRevert("MetaSwap7702CalldataEnforcer:invalid-outer-execution"); + _enforce(_terms(tokenIn, false), outer_); + } + + function test_revertsWhenInnerModeIsNotBatchDefault() public { + Execution[] memory executions_ = _erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "aggregator", hex""); + Execution memory outer_ = Execution({ + target: delegator, + value: 0, + callData: abi.encodeCall(IERC7821.execute, (ModeLib.encodeSimpleSingle(), ExecutionLib.encodeBatch(executions_))) + }); + + vm.expectRevert("MetaSwap7702CalldataEnforcer:invalid-inner-mode"); + _enforce(_terms(tokenIn, false), outer_); + } + + function _terms(address tokenIn_, bool resetApproval_) private view returns (bytes memory) { + return abi.encodePacked(metaSwap, tokenIn_, TOKEN_IN_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)); + } + + function _erc20Inner( + bool resetApproval_, + address swapToken_, + uint256 swapAmount_, + string memory aggregatorId_, + bytes memory route_ + ) + private + view + returns (Execution[] memory executions_) + { + uint256 swapIndex_ = resetApproval_ ? 2 : 1; + executions_ = new Execution[](swapIndex_ + 1); + + if (resetApproval_) { + executions_[0] = _approvalExecution(0); + } + executions_[swapIndex_ - 1] = _approvalExecution(TOKEN_IN_AMOUNT); + executions_[swapIndex_] = _swapExecution(swapToken_, swapAmount_, 0, aggregatorId_, route_); + } + + function _approvalExecution(uint256 amount_) private view returns (Execution memory) { + return Execution({ target: tokenIn, value: 0, callData: abi.encodeCall(IERC20.approve, (metaSwap, amount_)) }); + } + + function _swapExecution( + address swapToken_, + uint256 swapAmount_, + uint256 value_, + string memory aggregatorId_, + bytes memory route_ + ) + private + view + returns (Execution memory) + { + return Execution({ + target: metaSwap, + value: value_, + callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(swapToken_), swapAmount_, route_)) + }); + } + + function _outer(Execution[] memory executions_) private view returns (Execution memory) { + return Execution({ + target: delegator, + value: 0, + callData: abi.encodeCall(IERC7821.execute, (ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(executions_))) + }); + } + + function _enforce(bytes memory terms_, Execution memory outer_) private view { + enforcer.beforeHook( + terms_, + hex"", + singleDefaultMode, + ExecutionLib.encodeSingle(outer_.target, outer_.value, outer_.callData), + bytes32(0), + delegator, + address(0) + ); + } +} diff --git a/test/enforcers/MetaSwapApproveSwapEnforcer.t.sol b/test/enforcers/MetaSwapApproveSwapEnforcer.t.sol new file mode 100644 index 00000000..ff7b5baa --- /dev/null +++ b/test/enforcers/MetaSwapApproveSwapEnforcer.t.sol @@ -0,0 +1,805 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { MetaSwapApproveSwapEnforcer } from "../../src/enforcers/MetaSwapApproveSwapEnforcer.sol"; +import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; + +contract MockMetaSwap is IMetaSwap { + using SafeERC20 for IERC20; + + IERC20 internal immutable tokenOut; + + constructor(IERC20 _tokenOut) { + tokenOut = _tokenOut; + } + + function swap(string calldata, IERC20 _tokenFrom, uint256 _amount, bytes calldata _data) external payable { + (,,,, uint256 amountTo_,,,,) = abi.decode( + abi.encodePacked(abi.encode(address(0)), _data), + (address, IERC20, IERC20, uint256, uint256, bytes, uint256, address, bool) + ); + _tokenFrom.safeTransferFrom(msg.sender, address(this), _amount); + tokenOut.safeTransfer(msg.sender, amountTo_); + } + + function setAdapter(string calldata, address, bytes4, bytes calldata) external { } + function removeAdapter(string calldata) external { } + + function adapters(string memory) external pure returns (Adapter memory) { + return Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); + } +} + +contract MockMetaSwapUnderpay is IMetaSwap { + using SafeERC20 for IERC20; + + IERC20 internal immutable tokenOut; + uint256 internal immutable payoutAmount; + + constructor(IERC20 _tokenOut, uint256 _payoutAmount) { + tokenOut = _tokenOut; + payoutAmount = _payoutAmount; + } + + function swap(string calldata, IERC20 _tokenFrom, uint256 _amount, bytes calldata) external payable { + _tokenFrom.safeTransferFrom(msg.sender, address(this), _amount); + tokenOut.safeTransfer(msg.sender, payoutAmount); + } + + function setAdapter(string calldata, address, bytes4, bytes calldata) external { } + function removeAdapter(string calldata) external { } + + function adapters(string memory) external pure returns (Adapter memory) { + return Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); + } +} + +contract MockMetaSwapNoPull is IMetaSwap { + using SafeERC20 for IERC20; + + IERC20 internal immutable tokenOut; + + constructor(IERC20 _tokenOut) { + tokenOut = _tokenOut; + } + + function swap(string calldata, IERC20, uint256, bytes calldata _data) external payable { + (,,,, uint256 amountTo_,,,,) = abi.decode( + abi.encodePacked(abi.encode(address(0)), _data), + (address, IERC20, IERC20, uint256, uint256, bytes, uint256, address, bool) + ); + tokenOut.safeTransfer(msg.sender, amountTo_); + } + + function setAdapter(string calldata, address, bytes4, bytes calldata) external { } + function removeAdapter(string calldata) external { } + + function adapters(string memory) external pure returns (Adapter memory) { + return Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); + } +} + +contract MetaSwapApproveSwapEnforcerTest is CaveatEnforcerBaseTest { + uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; + uint256 internal constant MIN_TOKEN_OUT = 190 ether; + uint256 internal constant ACTUAL_TOKEN_OUT = 200 ether; + + BasicERC20 internal tokenIn; + BasicERC20 internal tokenOut; + MockMetaSwap internal metaSwap; + MetaSwapApproveSwapEnforcer internal enforcer; + RedeemerEnforcer internal redeemerEnforcer; + + address internal automation; + + function setUp() public override { + super.setUp(); + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + metaSwap = new MockMetaSwap(tokenOut); + enforcer = new MetaSwapApproveSwapEnforcer(); + redeemerEnforcer = new RedeemerEnforcer(); + automation = makeAddr("metamask-automation"); + + tokenIn.mint(address(users.alice.deleGator), TOKEN_IN_AMOUNT); + tokenOut.mint(address(metaSwap), 1_000 ether); + + vm.label(address(enforcer), "MetaSwap Approve Swap Enforcer"); + vm.label(address(metaSwap), "Mock MetaSwap"); + } + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(enforcer)); + } + + ////////////////////// Valid cases ////////////////////// + + function test_validBatchExecution() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + + vm.prank(address(delegationManager)); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, keccak256("test"), address(0), address(0)); + } + + function test_validBatchExecutionWithBetterOutput() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(ACTUAL_TOKEN_OUT); + + vm.prank(address(delegationManager)); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, keccak256("test"), address(0), address(0)); + } + + function test_revertWithIdenticalTokens() public { + MetaSwapApproveSwapEnforcer.Terms memory terms_ = MetaSwapApproveSwapEnforcer.Terms({ + metaSwap: address(metaSwap), + tokenIn: address(tokenIn), + tokenOut: address(tokenIn), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + (, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:identical-tokens"); + enforcer.beforeHook( + abi.encode(terms_), hex"", batchDefaultMode, executionCallData_, keccak256("test"), address(0), address(0) + ); + } + + function test_validResetBatchExecution() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidResetBatch(ACTUAL_TOKEN_OUT); + + vm.prank(address(delegationManager)); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, keccak256("test"), address(0), address(0)); + } + + function test_getTermsInfo() public { + MetaSwapApproveSwapEnforcer.Terms memory terms_ = MetaSwapApproveSwapEnforcer.Terms({ + metaSwap: address(metaSwap), + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + bytes memory encoded_ = abi.encode(terms_); + MetaSwapApproveSwapEnforcer.Terms memory decoded_ = enforcer.getTermsInfo(encoded_); + + assertEq(decoded_.metaSwap, address(metaSwap)); + assertEq(decoded_.tokenIn, address(tokenIn)); + assertEq(decoded_.tokenOut, address(tokenOut)); + assertEq(decoded_.tokenInAmount, TOKEN_IN_AMOUNT); + assertEq(decoded_.minTokenOut, MIN_TOKEN_OUT); + assertEq(keccak256(enforcer.encodeTerms(terms_)), keccak256(encoded_)); + } + + function test_emitsDelegationExecuted() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + bytes32 delegationHash_ = keccak256("event"); + address delegator_ = address(users.alice.deleGator); + + vm.prank(address(delegationManager)); + vm.expectEmit(true, true, true, true, address(enforcer)); + emit MetaSwapApproveSwapEnforcer.DelegationExecuted(address(delegationManager), delegationHash_, delegator_); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, delegationHash_, delegator_, address(0)); + } + + function test_allowsFeeTakenFromInput() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT, + _encodeSwapData( + address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT - 1 ether, MIN_TOKEN_OUT, 1 ether, address(this), false + ) + ); + + vm.prank(address(delegationManager)); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("fee-in"), address(0), address(0) + ); + } + + function test_allowsFeeToTrueWithUnmatchedInputSplit() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT, + _encodeSwapData( + address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT - 1 ether, MIN_TOKEN_OUT, 1 ether, address(this), true + ) + ); + + vm.prank(address(delegationManager)); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("fee-out"), address(0), address(0) + ); + } + + ////////////////////// Invalid cases ////////////////////// + + function test_revertWithInvalidCallTypeMode() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + + vm.prank(address(delegationManager)); + vm.expectRevert("CaveatEnforcer:invalid-call-type"); + enforcer.beforeHook(terms_, hex"", singleDefaultMode, executionCallData_, keccak256("test"), address(0), address(0)); + } + + function test_revertWithInvalidExecutionMode() public { + vm.prank(address(delegationManager)); + vm.expectRevert("CaveatEnforcer:invalid-execution-type"); + enforcer.beforeHook(hex"", hex"", batchTryMode, hex"", bytes32(0), address(0), address(0)); + } + + function test_revertOnDelegationReuse() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + bytes32 delegationHash_ = keccak256("test"); + + vm.startPrank(address(delegationManager)); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, delegationHash_, address(0), address(0)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:delegation-already-used"); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, delegationHash_, address(0), address(0)); + vm.stopPrank(); + } + + function test_revertWithInvalidBatchSize() public { + Execution[] memory executions_ = new Execution[](1); + executions_[0] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT)) + }); + (bytes memory terms_,) = _buildValidBatch(MIN_TOKEN_OUT); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-batch-length"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidBatchSizeFour() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + Execution[] memory fourExecutions_ = new Execution[](4); + fourExecutions_[0] = executions_[0]; + fourExecutions_[1] = executions_[0]; + fourExecutions_[2] = executions_[0]; + fourExecutions_[3] = executions_[1]; + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-batch-length"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(fourExecutions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidResetAmount() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidResetBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), 1)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-approve-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidResetSecondApproveAmount() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidResetBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT - 1)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-approve-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidResetSwapTarget() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidResetBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[2].target = address(tokenIn); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_afterHook_revertsInsufficientOutput() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(ACTUAL_TOKEN_OUT); + bytes32 delegationHash_ = keccak256("after-hook"); + address delegator_ = address(users.alice.deleGator); + + vm.startPrank(address(delegationManager)); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, delegationHash_, delegator_, address(0)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:insufficient-output"); + enforcer.afterHook(terms_, hex"", batchDefaultMode, executionCallData_, delegationHash_, delegator_, address(0)); + vm.stopPrank(); + } + + function test_revertWithInvalidApproveTarget() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[0].target = address(tokenOut); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-approve-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidApproveValue() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[0].value = 1 ether; + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-approve-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidApproveSelector() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(metaSwap), TOKEN_IN_AMOUNT)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-approve-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidApproveSpender() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(this), TOKEN_IN_AMOUNT)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-approve-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidApproveAmount() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT - 1)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-approve-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidSwapTarget() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].target = address(tokenIn); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidSwapValue() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].value = 1 ether; + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidSwapSelector() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeCall(IERC20.transfer, (address(tokenOut), TOKEN_IN_AMOUNT)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidOuterTokenIn() public { + BasicERC20 wrongTokenIn_ = new BasicERC20(address(this), "Wrong In", "WIN", 0); + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + wrongTokenIn_, + TOKEN_IN_AMOUNT, + _encodeSwapData(address(wrongTokenIn_), address(tokenOut), TOKEN_IN_AMOUNT, MIN_TOKEN_OUT) + ); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidOuterAmount() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT - 1, + _encodeSwapData(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT - 1, MIN_TOKEN_OUT) + ); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidInnerTokenOut() public { + BasicERC20 wrongTokenOut_ = new BasicERC20(address(this), "Wrong Out", "WOUT", 0); + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT, + _encodeSwapData(address(tokenIn), address(wrongTokenOut_), TOKEN_IN_AMOUNT, MIN_TOKEN_OUT) + ); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithInvalidInnerTokenIn() public { + BasicERC20 wrongTokenIn_ = new BasicERC20(address(this), "Wrong In", "WIN", 0); + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT, + _encodeSwapData(address(wrongTokenIn_), address(tokenOut), TOKEN_IN_AMOUNT, MIN_TOKEN_OUT) + ); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithAmountFromMismatch() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(executionCallData_, (Execution[])); + executions_[1].callData = abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT, + _encodeSwapData(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT - 1 ether, MIN_TOKEN_OUT) + ); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:amount-from-mismatch"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_revertWithBelowMinimumOutput() public { + (bytes memory terms_, bytes memory executionCallData_) = _buildValidBatch(MIN_TOKEN_OUT - 1); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapApproveSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook(terms_, hex"", batchDefaultMode, executionCallData_, keccak256("test"), address(0), address(0)); + } + + ////////////////////// Integration ////////////////////// + + function test_integration_happyPath() public { + (Delegation memory delegation_, bytes memory executionCallData_) = + _buildDelegation(ACTUAL_TOKEN_OUT, automation, automation); + + _redeem(delegation_, executionCallData_, automation); + + assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), ACTUAL_TOKEN_OUT); + assertEq(tokenIn.balanceOf(address(metaSwap)), TOKEN_IN_AMOUNT); + } + + function test_integration_resetApprovalHappyPath() public { + (Delegation memory delegation_, bytes memory executionCallData_) = + _buildDelegation(ACTUAL_TOKEN_OUT, automation, automation, true); + + vm.prank(address(delegationManager)); + users.alice.deleGator + .executeFromExecutor( + singleDefaultMode, + ExecutionLib.encodeSingle(address(tokenIn), 0, abi.encodeCall(IERC20.approve, (address(metaSwap), uint256(1)))) + ); + assertEq(tokenIn.allowance(address(users.alice.deleGator), address(metaSwap)), 1); + + _redeem(delegation_, executionCallData_, automation); + + assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), ACTUAL_TOKEN_OUT); + assertEq(tokenIn.allowance(address(users.alice.deleGator), address(metaSwap)), 0); + } + + function test_integration_revertsUnauthorizedRedeemer() public { + (Delegation memory delegation_, bytes memory executionCallData_) = + _buildDelegation(ACTUAL_TOKEN_OUT, ANY_DELEGATE, automation); + + vm.expectRevert("RedeemerEnforcer:unauthorized-redeemer"); + _redeem(delegation_, executionCallData_, users.bob.addr); + } + + function test_integration_revertsReplay() public { + (Delegation memory delegation_, bytes memory executionCallData_) = + _buildDelegation(ACTUAL_TOKEN_OUT, automation, automation); + _redeem(delegation_, executionCallData_, automation); + + vm.expectRevert("MetaSwapApproveSwapEnforcer:delegation-already-used"); + _redeem(delegation_, executionCallData_, automation); + } + + function test_integration_revertsInsufficientOutput() public { + MockMetaSwapUnderpay underpayMetaSwap_ = new MockMetaSwapUnderpay(tokenOut, MIN_TOKEN_OUT - 1); + tokenOut.mint(address(underpayMetaSwap_), 1_000 ether); + + (Delegation memory delegation_, bytes memory executionCallData_) = + _buildDelegationWithMetaSwap(underpayMetaSwap_, ACTUAL_TOKEN_OUT, automation, automation); + + vm.expectRevert("MetaSwapApproveSwapEnforcer:insufficient-output"); + _redeem(delegation_, executionCallData_, automation); + } + + function test_integration_revertsRemainingAllowance() public { + MockMetaSwapNoPull noPullMetaSwap_ = new MockMetaSwapNoPull(tokenOut); + tokenOut.mint(address(noPullMetaSwap_), 1_000 ether); + + (Delegation memory delegation_, bytes memory executionCallData_) = + _buildDelegationWithMetaSwap(noPullMetaSwap_, ACTUAL_TOKEN_OUT, automation, automation); + + vm.expectRevert("MetaSwapApproveSwapEnforcer:remaining-allowance"); + _redeem(delegation_, executionCallData_, automation); + } + + ////////////////////// Helpers ////////////////////// + + function _buildValidBatch(uint256 _amountTo) private view returns (bytes memory terms_, bytes memory executionCallData_) { + MetaSwapApproveSwapEnforcer.Terms memory termsData_ = MetaSwapApproveSwapEnforcer.Terms({ + metaSwap: address(metaSwap), + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + terms_ = abi.encode(termsData_); + executionCallData_ = _encodeBatchExecution(_amountTo); + } + + function _buildValidResetBatch(uint256 _amountTo) private view returns (bytes memory terms_, bytes memory executionCallData_) { + MetaSwapApproveSwapEnforcer.Terms memory termsData_ = MetaSwapApproveSwapEnforcer.Terms({ + metaSwap: address(metaSwap), + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + terms_ = abi.encode(termsData_); + executionCallData_ = _encodeResetBatchExecution(_amountTo); + } + + function _buildDelegation( + uint256 _amountTo, + address _delegate, + address _allowedRedeemer + ) + private + view + returns (Delegation memory delegation_, bytes memory executionCallData_) + { + return _buildDelegation(_amountTo, _delegate, _allowedRedeemer, false); + } + + function _buildDelegation( + uint256 _amountTo, + address _delegate, + address _allowedRedeemer, + bool _resetApproval + ) + private + view + returns (Delegation memory delegation_, bytes memory executionCallData_) + { + MetaSwapApproveSwapEnforcer.Terms memory termsData_ = MetaSwapApproveSwapEnforcer.Terms({ + metaSwap: address(metaSwap), + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ enforcer: address(enforcer), terms: abi.encode(termsData_), args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(redeemerEnforcer), terms: abi.encodePacked(_allowedRedeemer), args: hex"" }); + + delegation_ = signDelegation( + users.alice, + Delegation({ + delegate: _delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 42, + signature: hex"" + }) + ); + executionCallData_ = _resetApproval ? _encodeResetBatchExecution(_amountTo) : _encodeBatchExecution(_amountTo); + } + + function _buildDelegationWithMetaSwap( + IMetaSwap _metaSwap, + uint256 _amountTo, + address _delegate, + address _allowedRedeemer + ) + private + view + returns (Delegation memory delegation_, bytes memory executionCallData_) + { + MetaSwapApproveSwapEnforcer.Terms memory termsData_ = MetaSwapApproveSwapEnforcer.Terms({ + metaSwap: address(_metaSwap), + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ enforcer: address(enforcer), terms: abi.encode(termsData_), args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(redeemerEnforcer), terms: abi.encodePacked(_allowedRedeemer), args: hex"" }); + + delegation_ = signDelegation( + users.alice, + Delegation({ + delegate: _delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 43, + signature: hex"" + }) + ); + executionCallData_ = _encodeBatchExecution(_metaSwap, _amountTo); + } + + function _encodeBatchExecution(uint256 _amountTo) private view returns (bytes memory) { + return _encodeBatchExecution(metaSwap, _amountTo); + } + + function _encodeBatchExecution(IMetaSwap _metaSwap, uint256 _amountTo) private view returns (bytes memory) { + Execution[] memory executions_ = new Execution[](2); + executions_[0] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(_metaSwap), TOKEN_IN_AMOUNT)) + }); + executions_[1] = Execution({ + target: address(_metaSwap), + value: 0, + callData: abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT, + _encodeSwapData(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT, _amountTo) + ) + }); + return ExecutionLib.encodeBatch(executions_); + } + + function _encodeResetBatchExecution(uint256 _amountTo) private view returns (bytes memory) { + Execution[] memory executions_ = new Execution[](3); + executions_[0] = + Execution({ target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(metaSwap), 0)) }); + executions_[1] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT)) + }); + executions_[2] = Execution({ + target: address(metaSwap), + value: 0, + callData: abi.encodeWithSelector( + IMetaSwap.swap.selector, + "mock-aggregator", + tokenIn, + TOKEN_IN_AMOUNT, + _encodeSwapData(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT, _amountTo) + ) + }); + return ExecutionLib.encodeBatch(executions_); + } + + function _encodeSwapData( + address _tokenFrom, + address _tokenTo, + uint256 _amountFrom, + uint256 _amountTo + ) + private + pure + returns (bytes memory) + { + return _encodeSwapData(_tokenFrom, _tokenTo, _amountFrom, _amountTo, 0, address(0), false); + } + + function _encodeSwapData( + address _tokenFrom, + address _tokenTo, + uint256 _amountFrom, + uint256 _amountTo, + uint256 _fee, + address _feeWallet, + bool _feeTo + ) + private + pure + returns (bytes memory) + { + return abi.encode(_tokenFrom, _tokenTo, _amountFrom, _amountTo, hex"", _fee, _feeWallet, _feeTo); + } + + function _redeem(Delegation memory _delegation, bytes memory _execution, address _redeemer) private { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _delegation; + + bytes[] memory contexts_ = new bytes[](1); + contexts_[0] = abi.encode(delegations_); + + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + + bytes[] memory executions_ = new bytes[](1); + executions_[0] = _execution; + + vm.prank(_redeemer); + delegationManager.redeemDelegations(contexts_, modes_, executions_); + } +} diff --git a/test/enforcers/MetaSwapBatchCalldataEnforcer.t.sol b/test/enforcers/MetaSwapBatchCalldataEnforcer.t.sol new file mode 100644 index 00000000..6cb1b383 --- /dev/null +++ b/test/enforcers/MetaSwapBatchCalldataEnforcer.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { MetaSwapBatchCalldataEnforcer } from "../../src/enforcers/MetaSwapBatchCalldataEnforcer.sol"; +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { Execution, ModeCode } from "../../src/utils/Types.sol"; + +contract MetaSwapBatchCalldataEnforcerTest is Test { + uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; + + MetaSwapBatchCalldataEnforcer internal enforcer; + address internal metaSwap; + address internal tokenIn; + ModeCode internal batchDefaultMode = ModeLib.encodeSimpleBatch(); + + function setUp() public { + enforcer = new MetaSwapBatchCalldataEnforcer(); + metaSwap = makeAddr("MetaSwap"); + tokenIn = makeAddr("TokenIn"); + } + + function test_erc20OneApproval_acceptsFlexibleRoute() public view { + _enforce(_terms(tokenIn, false), _erc20Batch(false, tokenIn, TOKEN_IN_AMOUNT, "a", hex"01")); + _enforce(_terms(tokenIn, false), _erc20Batch(false, tokenIn, TOKEN_IN_AMOUNT, "different", new bytes(512))); + } + + function test_erc20ResetApproval_acceptsSignedShape() public view { + _enforce(_terms(tokenIn, true), _erc20Batch(true, tokenIn, TOKEN_IN_AMOUNT, "aggregator", new bytes(96))); + } + + function test_nativeInput_acceptsSingleSwap() public view { + Execution[] memory executions_ = new Execution[](1); + executions_[0] = _swap(address(0), TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, "aggregator", hex"1234"); + _enforce(_terms(address(0), false), executions_); + } + + function test_revertsOnSingleDelegationManagerMode() public { + Execution[] memory executions_ = _erc20Batch(false, tokenIn, TOKEN_IN_AMOUNT, "aggregator", hex""); + + vm.expectRevert("CaveatEnforcer:invalid-call-type"); + enforcer.beforeHook( + _terms(tokenIn, false), + hex"", + ModeLib.encodeSimpleSingle(), + ExecutionLib.encodeBatch(executions_), + bytes32(0), + address(0), + address(0) + ); + } + + function test_revertsOnDifferentSwapToken() public { + vm.expectRevert("MetaSwapBatchCalldataEnforcer:invalid-swap"); + _enforce(_terms(tokenIn, false), _erc20Batch(false, makeAddr("OtherToken"), TOKEN_IN_AMOUNT, "aggregator", hex"")); + } + + function test_revertsOnDifferentSwapAmount() public { + vm.expectRevert("MetaSwapBatchCalldataEnforcer:invalid-swap"); + _enforce(_terms(tokenIn, false), _erc20Batch(false, tokenIn, TOKEN_IN_AMOUNT - 1, "aggregator", hex"")); + } + + function test_revertsOnWrongApprovalSpender() public { + Execution[] memory executions_ = _erc20Batch(false, tokenIn, TOKEN_IN_AMOUNT, "aggregator", hex""); + executions_[0].callData = abi.encodeCall(IERC20.approve, (makeAddr("OtherSpender"), TOKEN_IN_AMOUNT)); + + vm.expectRevert("MetaSwapBatchCalldataEnforcer:invalid-approval"); + _enforce(_terms(tokenIn, false), executions_); + } + + function test_revertsWhenApprovalShapeDoesNotMatchTerms() public { + vm.expectRevert("MetaSwapBatchCalldataEnforcer:invalid-batch-length"); + _enforce(_terms(tokenIn, false), _erc20Batch(true, tokenIn, TOKEN_IN_AMOUNT, "aggregator", hex"")); + } + + function _terms(address tokenIn_, bool resetApproval_) private view returns (bytes memory) { + return abi.encodePacked(metaSwap, tokenIn_, TOKEN_IN_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)); + } + + function _erc20Batch( + bool resetApproval_, + address swapToken_, + uint256 swapAmount_, + string memory aggregatorId_, + bytes memory route_ + ) + private + view + returns (Execution[] memory executions_) + { + uint256 swapIndex_ = resetApproval_ ? 2 : 1; + executions_ = new Execution[](swapIndex_ + 1); + if (resetApproval_) executions_[0] = _approval(0); + executions_[swapIndex_ - 1] = _approval(TOKEN_IN_AMOUNT); + executions_[swapIndex_] = _swap(swapToken_, swapAmount_, 0, aggregatorId_, route_); + } + + function _approval(uint256 amount_) private view returns (Execution memory) { + return Execution({ target: tokenIn, value: 0, callData: abi.encodeCall(IERC20.approve, (metaSwap, amount_)) }); + } + + function _swap( + address swapToken_, + uint256 swapAmount_, + uint256 value_, + string memory aggregatorId_, + bytes memory route_ + ) + private + view + returns (Execution memory) + { + return Execution({ + target: metaSwap, + value: value_, + callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(swapToken_), swapAmount_, route_)) + }); + } + + function _enforce(bytes memory terms_, Execution[] memory executions_) private view { + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), bytes32(0), address(0), address(0) + ); + } +} diff --git a/test/experiments/MetaSwapBatchDesignGasComparison.t.sol b/test/experiments/MetaSwapBatchDesignGasComparison.t.sol new file mode 100644 index 00000000..7189c7b5 --- /dev/null +++ b/test/experiments/MetaSwapBatchDesignGasComparison.t.sol @@ -0,0 +1,844 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test, console2 } from "forge-std/Test.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcer } from "../../src/enforcers/CaveatEnforcer.sol"; +import { ERC20BalanceChangeEnforcer } from "../../src/enforcers/ERC20BalanceChangeEnforcer.sol"; +import { LimitedCallsEnforcer } from "../../src/enforcers/LimitedCallsEnforcer.sol"; +import { MetaSwapBatchCalldataEnforcer } from "../../src/enforcers/MetaSwapBatchCalldataEnforcer.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import { Implementation, SignatureType } from "../utils/Types.t.sol"; +import { MockLimitOrderRouter } from "../utils/MockLimitOrderRouter.sol"; + +abstract contract MetaSwapDesignValidationBase is CaveatEnforcer { + uint256 internal constant APPROVE_CALL_LENGTH = 68; + uint256 internal constant SWAP_CALL_MIN_LENGTH = 132; + + function _validateApproval( + Execution calldata execution_, + address tokenIn_, + address metaSwap_, + uint256 expectedAmount_ + ) + internal + pure + { + bytes calldata callData_ = execution_.callData; + require( + execution_.target == tokenIn_ && execution_.value == 0 && callData_.length == APPROVE_CALL_LENGTH + && bytes4(callData_[0:4]) == IERC20.approve.selector + && address(uint160(uint256(bytes32(callData_[4:36])))) == metaSwap_ + && uint256(bytes32(callData_[36:68])) == expectedAmount_, + "MetaSwapDesignValidationBase:invalid-approval" + ); + } + + function _validateSwap( + address target_, + uint256 value_, + bytes calldata callData_, + address metaSwap_, + address tokenIn_, + uint256 tokenInAmount_, + uint256 expectedValue_ + ) + internal + pure + { + require( + target_ == metaSwap_ && value_ == expectedValue_ && callData_.length >= SWAP_CALL_MIN_LENGTH + && bytes4(callData_[0:4]) == IMetaSwap.swap.selector + && address(uint160(uint256(bytes32(callData_[36:68])))) == tokenIn_ + && uint256(bytes32(callData_[68:100])) == tokenInAmount_, + "MetaSwapDesignValidationBase:invalid-swap" + ); + } +} + +/// @notice ERC-20-only copy of PR 201 with the native branch removed. +contract MetaSwapERC20OnlyEnforcer is MetaSwapDesignValidationBase { + using ExecutionLib for bytes; + + uint256 private constant TERMS_LENGTH = 73; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "MetaSwapERC20OnlyEnforcer:invalid-terms"); + address metaSwap_ = address(bytes20(terms_[0:20])); + address tokenIn_ = address(bytes20(terms_[20:40])); + uint256 tokenInAmount_ = uint256(bytes32(terms_[40:72])); + uint8 resetApproval_ = uint8(terms_[72]); + require( + metaSwap_ != address(0) && tokenIn_ != address(0) && tokenInAmount_ != 0 && resetApproval_ <= 1, + "MetaSwapERC20OnlyEnforcer:invalid-terms" + ); + + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + if (resetApproval_ == 1) { + require(executions_.length == 3, "MetaSwapERC20OnlyEnforcer:invalid-batch-length"); + _validateApproval(executions_[0], tokenIn_, metaSwap_, 0); + _validateApproval(executions_[1], tokenIn_, metaSwap_, tokenInAmount_); + _validateSwap( + executions_[2].target, executions_[2].value, executions_[2].callData, metaSwap_, tokenIn_, tokenInAmount_, 0 + ); + } else { + require(executions_.length == 2, "MetaSwapERC20OnlyEnforcer:invalid-batch-length"); + _validateApproval(executions_[0], tokenIn_, metaSwap_, tokenInAmount_); + _validateSwap( + executions_[1].target, executions_[1].value, executions_[1].callData, metaSwap_, tokenIn_, tokenInAmount_, 0 + ); + } + } +} + +/// @notice Native-only copy retaining PR 201's one-element batch mode. +contract MetaSwapNativeBatchOnlyEnforcer is MetaSwapDesignValidationBase { + using ExecutionLib for bytes; + + uint256 private constant TERMS_LENGTH = 52; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "MetaSwapNativeBatchOnlyEnforcer:invalid-terms"); + address metaSwap_ = address(bytes20(terms_[0:20])); + uint256 tokenInAmount_ = uint256(bytes32(terms_[20:52])); + require(metaSwap_ != address(0) && tokenInAmount_ != 0, "MetaSwapNativeBatchOnlyEnforcer:invalid-terms"); + + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + require(executions_.length == 1, "MetaSwapNativeBatchOnlyEnforcer:invalid-batch-length"); + _validateSwap( + executions_[0].target, + executions_[0].value, + executions_[0].callData, + metaSwap_, + address(0), + tokenInAmount_, + tokenInAmount_ + ); + } +} + +/// @notice Native-only copy using DelegationManager single mode. +contract MetaSwapNativeSingleOnlyEnforcer is MetaSwapDesignValidationBase { + using ExecutionLib for bytes; + + uint256 private constant TERMS_LENGTH = 52; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlySingleCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "MetaSwapNativeSingleOnlyEnforcer:invalid-terms"); + address metaSwap_ = address(bytes20(terms_[0:20])); + uint256 tokenInAmount_ = uint256(bytes32(terms_[20:52])); + require(metaSwap_ != address(0) && tokenInAmount_ != 0, "MetaSwapNativeSingleOnlyEnforcer:invalid-terms"); + + (address target_, uint256 value_, bytes calldata callData_) = executionCallData_.decodeSingle(); + _validateSwap(target_, value_, callData_, metaSwap_, address(0), tokenInAmount_, tokenInAmount_); + } +} + +/** + * @notice PR 201 copy where the final signed byte is an approval policy. + * @dev Batch length selects the mode, avoiding unsigned caveat args: + * bit 0 = swap only, bit 1 = approve(amount) + swap, bit 2 = approve(0) + approve(amount) + swap. + */ +contract MetaSwapApprovalPolicyEnforcer is MetaSwapDesignValidationBase { + using ExecutionLib for bytes; + + uint8 public constant ALLOW_SKIP_APPROVAL = 1; + uint8 public constant ALLOW_APPROVAL = 2; + uint8 public constant ALLOW_RESET_APPROVAL = 4; + uint256 private constant TERMS_LENGTH = 73; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "MetaSwapApprovalPolicyEnforcer:invalid-terms"); + address metaSwap_ = address(bytes20(terms_[0:20])); + address tokenIn_ = address(bytes20(terms_[20:40])); + uint256 tokenInAmount_ = uint256(bytes32(terms_[40:72])); + uint8 policy_ = uint8(terms_[72]); + require( + metaSwap_ != address(0) && tokenInAmount_ != 0 && policy_ <= 7, + "MetaSwapApprovalPolicyEnforcer:invalid-terms" + ); + + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + if (tokenIn_ == address(0)) { + require(policy_ == 0 && executions_.length == 1, "MetaSwapApprovalPolicyEnforcer:shape-not-allowed"); + _validateSwap( + executions_[0].target, + executions_[0].value, + executions_[0].callData, + metaSwap_, + address(0), + tokenInAmount_, + tokenInAmount_ + ); + return; + } + + require(policy_ != 0, "MetaSwapApprovalPolicyEnforcer:invalid-terms"); + uint256 swapIndex_; + if (executions_.length == 1) { + require(policy_ & ALLOW_SKIP_APPROVAL != 0, "MetaSwapApprovalPolicyEnforcer:shape-not-allowed"); + } else if (executions_.length == 2) { + require(policy_ & ALLOW_APPROVAL != 0, "MetaSwapApprovalPolicyEnforcer:shape-not-allowed"); + _validateApproval(executions_[0], tokenIn_, metaSwap_, tokenInAmount_); + swapIndex_ = 1; + } else { + require( + executions_.length == 3 && policy_ & ALLOW_RESET_APPROVAL != 0, "MetaSwapApprovalPolicyEnforcer:shape-not-allowed" + ); + _validateApproval(executions_[0], tokenIn_, metaSwap_, 0); + _validateApproval(executions_[1], tokenIn_, metaSwap_, tokenInAmount_); + swapIndex_ = 2; + } + + _validateSwap( + executions_[swapIndex_].target, + executions_[swapIndex_].value, + executions_[swapIndex_].callData, + metaSwap_, + tokenIn_, + tokenInAmount_, + 0 + ); + } +} + +/** + * @notice Experimental complete one-shot limit-order enforcer. + * @dev It combines PR 201 validation, minimum output, and one-shot consumption in one storage slot per delegation. + * State is `0` before use, `balanceBefore + 1` during execution, and `type(uint256).max` after success. + */ +contract MetaSwapIntegratedOneShotEnforcer is MetaSwapDesignValidationBase { + using ExecutionLib for bytes; + + struct Terms { + address metaSwap; + address tokenIn; + uint256 tokenInAmount; + bool resetApproval; + address tokenOut; + address recipient; + uint256 tokenOutMin; + } + + uint256 private constant TERMS_LENGTH = 145; + mapping(bytes32 key => uint256 state) public delegationState; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32 delegationHash_, + address, + address + ) + public + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + Terms memory info_ = _getTerms(terms_); + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + _validateExecutions(executions_, info_); + + bytes32 key_ = _key(msg.sender, delegationHash_); + require(delegationState[key_] == 0, "MetaSwapIntegratedOneShotEnforcer:already-used"); + uint256 balanceBefore_ = _balanceOf(info_.tokenOut, info_.recipient); + require(balanceBefore_ != type(uint256).max, "MetaSwapIntegratedOneShotEnforcer:balance-overflow"); + delegationState[key_] = balanceBefore_ + 1; + } + + function afterHook( + bytes calldata terms_, + bytes calldata, + ModeCode, + bytes calldata, + bytes32 delegationHash_, + address, + address + ) + public + override + { + require(terms_.length == TERMS_LENGTH, "MetaSwapIntegratedOneShotEnforcer:invalid-terms"); + address tokenOut_ = address(bytes20(terms_[73:93])); + address recipient_ = address(bytes20(terms_[93:113])); + uint256 tokenOutMin_ = uint256(bytes32(terms_[113:145])); + bytes32 key_ = _key(msg.sender, delegationHash_); + uint256 cachedState_ = delegationState[key_]; + require(cachedState_ != 0 && cachedState_ != type(uint256).max, "MetaSwapIntegratedOneShotEnforcer:invalid-state"); + + uint256 balanceBefore_ = cachedState_ - 1; + uint256 balanceAfter_ = _balanceOf(tokenOut_, recipient_); + require( + balanceAfter_ >= balanceBefore_ && balanceAfter_ - balanceBefore_ >= tokenOutMin_, + "MetaSwapIntegratedOneShotEnforcer:insufficient-output" + ); + delegationState[key_] = type(uint256).max; + } + + function _getTerms(bytes calldata terms_) private pure returns (Terms memory info_) { + require(terms_.length == TERMS_LENGTH, "MetaSwapIntegratedOneShotEnforcer:invalid-terms"); + info_.metaSwap = address(bytes20(terms_[0:20])); + info_.tokenIn = address(bytes20(terms_[20:40])); + info_.tokenInAmount = uint256(bytes32(terms_[40:72])); + uint8 resetApproval_ = uint8(terms_[72]); + info_.resetApproval = resetApproval_ == 1; + info_.tokenOut = address(bytes20(terms_[73:93])); + info_.recipient = address(bytes20(terms_[93:113])); + info_.tokenOutMin = uint256(bytes32(terms_[113:145])); + require( + info_.metaSwap != address(0) && info_.tokenInAmount != 0 && resetApproval_ <= 1 && info_.recipient != address(0) + && info_.tokenOutMin != 0, + "MetaSwapIntegratedOneShotEnforcer:invalid-terms" + ); + } + + function _validateExecutions(Execution[] calldata executions_, Terms memory info_) private pure { + if (info_.tokenIn == address(0)) { + require(!info_.resetApproval && executions_.length == 1, "MetaSwapIntegratedOneShotEnforcer:invalid-batch-length"); + _validateSwap( + executions_[0].target, + executions_[0].value, + executions_[0].callData, + info_.metaSwap, + address(0), + info_.tokenInAmount, + info_.tokenInAmount + ); + } else if (info_.resetApproval) { + require(executions_.length == 3, "MetaSwapIntegratedOneShotEnforcer:invalid-batch-length"); + _validateApproval(executions_[0], info_.tokenIn, info_.metaSwap, 0); + _validateApproval(executions_[1], info_.tokenIn, info_.metaSwap, info_.tokenInAmount); + _validateSwap( + executions_[2].target, + executions_[2].value, + executions_[2].callData, + info_.metaSwap, + info_.tokenIn, + info_.tokenInAmount, + 0 + ); + } else { + require(executions_.length == 2, "MetaSwapIntegratedOneShotEnforcer:invalid-batch-length"); + _validateApproval(executions_[0], info_.tokenIn, info_.metaSwap, info_.tokenInAmount); + _validateSwap( + executions_[1].target, + executions_[1].value, + executions_[1].callData, + info_.metaSwap, + info_.tokenIn, + info_.tokenInAmount, + 0 + ); + } + } + + function _balanceOf(address token_, address recipient_) private view returns (uint256) { + return token_ == address(0) ? recipient_.balance : IERC20(token_).balanceOf(recipient_); + } + + function _key(address caller_, bytes32 delegationHash_) private pure returns (bytes32) { + return keccak256(abi.encode(caller_, delegationHash_)); + } +} + +contract MetaSwapBatchDesignGasComparisonTest is BaseTest { + 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 INITIAL_TOKEN_OUT_BALANCE = 1 ether; + uint256 private constant INTRINSIC_GAS = 21_000; + uint8 private constant ALL_APPROVAL_MODES = 7; + + struct GasMeasurement { + uint256 executionGas; + uint256 calldataBytes; + uint256 calldataGas; + uint256 estimatedTransactionGas; + } + + MetaSwapBatchCalldataEnforcer private baseline; + MetaSwapERC20OnlyEnforcer private erc20Only; + MetaSwapNativeBatchOnlyEnforcer private nativeBatchOnly; + MetaSwapNativeSingleOnlyEnforcer private nativeSingleOnly; + MetaSwapApprovalPolicyEnforcer private approvalPolicy; + MetaSwapIntegratedOneShotEnforcer private integratedOneShot; + LimitedCallsEnforcer private limitedCalls; + ERC20BalanceChangeEnforcer private balanceChange; + MockLimitOrderRouter private router; + BasicERC20 private tokenIn; + BasicERC20 private tokenOut; + address private relayer; + + constructor() { + IMPLEMENTATION = Implementation.MultiSig; + SIGNATURE_TYPE = SignatureType.MultiSig; + } + + function setUp() public override { + super.setUp(); + + baseline = new MetaSwapBatchCalldataEnforcer(); + erc20Only = new MetaSwapERC20OnlyEnforcer(); + nativeBatchOnly = new MetaSwapNativeBatchOnlyEnforcer(); + nativeSingleOnly = new MetaSwapNativeSingleOnlyEnforcer(); + approvalPolicy = new MetaSwapApprovalPolicyEnforcer(); + integratedOneShot = new MetaSwapIntegratedOneShotEnforcer(); + limitedCalls = new LimitedCallsEnforcer(); + balanceChange = new ERC20BalanceChangeEnforcer(); + router = new MockLimitOrderRouter(); + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + relayer = makeAddr("Relayer"); + + tokenIn.mint(address(users.alice.deleGator), 1_000 ether); + tokenOut.mint(address(router), 10_000 ether); + tokenOut.mint(address(users.alice.deleGator), INITIAL_TOKEN_OUT_BALANCE); + vm.deal(address(users.alice.deleGator), 1_000 ether); + } + + function test_gas_split_baselineNativeBatch() public { + _report( + "split / baseline combined native batch", + _measure(_oneCaveat(address(baseline), _baselineTerms(address(0), false)), _nativeBatch(), true) + ); + } + + function test_gas_split_nativeOnlyBatch() public { + _report("split / native-only batch", _measure(_oneCaveat(address(nativeBatchOnly), _nativeTerms()), _nativeBatch(), true)); + } + + function test_gas_split_nativeOnlySingle() public { + Execution memory execution_ = _swap(address(0), TOKEN_IN_AMOUNT); + _report( + "split / native-only single", + _measureRaw( + _oneCaveat(address(nativeSingleOnly), _nativeTerms()), + ModeLib.encodeSimpleSingle(), + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData) + ) + ); + } + + function test_gas_split_baselineERC20Approval() public { + _report( + "split / baseline combined approve(amount)", + _measure(_oneCaveat(address(baseline), _baselineTerms(address(tokenIn), false)), _erc20Batch(false, false), true) + ); + } + + function test_gas_split_erc20OnlyApproval() public { + _report( + "split / ERC20-only approve(amount)", + _measure(_oneCaveat(address(erc20Only), _baselineTerms(address(tokenIn), false)), _erc20Batch(false, false), true) + ); + } + + function test_gas_split_baselineResetApproval() public { + _setAllowance(1); + _report( + "split / baseline combined reset approval", + _measure(_oneCaveat(address(baseline), _baselineTerms(address(tokenIn), true)), _erc20Batch(true, false), true) + ); + } + + function test_gas_split_erc20OnlyResetApproval() public { + _setAllowance(1); + _report( + "split / ERC20-only reset approval", + _measure(_oneCaveat(address(erc20Only), _baselineTerms(address(tokenIn), true)), _erc20Batch(true, false), true) + ); + } + + function test_gas_policy_baselineApproval() public { + _report( + "policy / baseline approve(amount)", + _measure(_oneCaveat(address(baseline), _baselineTerms(address(tokenIn), false)), _erc20Batch(false, false), true) + ); + } + + function test_gas_policy_native() public { + _report( + "policy / mask native", + _measure(_oneCaveat(address(approvalPolicy), _baselineTerms(address(0), false)), _nativeBatch(), true) + ); + } + + function test_gas_policy_allowsApproval() public { + _report( + "policy / mask approve(amount)", + _measure(_oneCaveat(address(approvalPolicy), _policyTerms()), _erc20Batch(false, false), true) + ); + } + + function test_gas_policy_baselineResetApproval() public { + _setAllowance(1); + _report( + "policy / baseline reset approval", + _measure(_oneCaveat(address(baseline), _baselineTerms(address(tokenIn), true)), _erc20Batch(true, false), true) + ); + } + + function test_gas_policy_allowsResetApproval() public { + _setAllowance(1); + _report( + "policy / mask reset approval", + _measure(_oneCaveat(address(approvalPolicy), _policyTerms()), _erc20Batch(true, false), true) + ); + } + + function test_gas_policy_skipsApprovalWhenAllowanceExists() public { + _setAllowance(TOKEN_IN_AMOUNT); + _report( + "policy / mask swap-only with existing allowance", + _measure(_oneCaveat(address(approvalPolicy), _policyTerms()), _erc20Batch(false, true), true) + ); + } + + function test_gas_integrated_baselineBundleNative() public { + _report( + "integrated / baseline + limited + balance native", _measure(_baselineBundle(address(0), false), _nativeBatch(), true) + ); + } + + function test_gas_integrated_oneShotNative() public { + _report( + "integrated / one caveat native", + _measure(_oneCaveat(address(integratedOneShot), _integratedTerms(address(0), false)), _nativeBatch(), true) + ); + } + + function test_gas_integrated_baselineBundleERC20Approval() public { + _report( + "integrated / baseline + limited + balance approve(amount)", + _measure(_baselineBundle(address(tokenIn), false), _erc20Batch(false, false), true) + ); + } + + function test_gas_integrated_oneShotERC20Approval() public { + _report( + "integrated / one caveat approve(amount)", + _measure( + _oneCaveat(address(integratedOneShot), _integratedTerms(address(tokenIn), false)), _erc20Batch(false, false), true + ) + ); + } + + function test_gas_integrated_baselineBundleResetApproval() public { + _setAllowance(1); + _report( + "integrated / baseline + limited + balance reset approval", + _measure(_baselineBundle(address(tokenIn), true), _erc20Batch(true, false), true) + ); + } + + function test_gas_integrated_oneShotResetApproval() public { + _setAllowance(1); + _report( + "integrated / one caveat reset approval", + _measure( + _oneCaveat(address(integratedOneShot), _integratedTerms(address(tokenIn), true)), _erc20Batch(true, false), true + ) + ); + } + + function test_integratedOneShotRejectsSecondRedemption() public { + Caveat[] memory caveats_ = _oneCaveat(address(integratedOneShot), _integratedTerms(address(tokenIn), false)); + Delegation memory delegation_ = _sign(caveats_); + _redeem(delegation_, ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(_erc20Batch(false, false))); + + vm.expectRevert("MetaSwapIntegratedOneShotEnforcer:already-used"); + _redeem(delegation_, ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(_erc20Batch(false, false))); + } + + function test_integratedOneShotRevertsAtomicallyForInsufficientOutputAndAllowsRetry() public { + Caveat[] memory caveats_ = _oneCaveat(address(integratedOneShot), _integratedTerms(address(tokenIn), false)); + Delegation memory delegation_ = _sign(caveats_); + + vm.expectRevert("MetaSwapIntegratedOneShotEnforcer:insufficient-output"); + _redeem(delegation_, ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(_erc20Batch(false, false, TOKEN_OUT_MIN - 1))); + + _redeem(delegation_, ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(_erc20Batch(false, false))); + } + + function test_policyRejectsUnsignedShape() public { + uint8 onlyApproval_ = approvalPolicy.ALLOW_APPROVAL(); + Caveat[] memory caveats_ = _oneCaveat( + address(approvalPolicy), abi.encodePacked(address(router), address(tokenIn), TOKEN_IN_AMOUNT, onlyApproval_) + ); + Delegation memory delegation_ = _sign(caveats_); + + vm.expectRevert("MetaSwapApprovalPolicyEnforcer:shape-not-allowed"); + _redeem(delegation_, ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(_erc20Batch(false, true))); + } + + function test_reportRuntimeSizes() public view { + console2.log("baseline combined runtime bytes", address(baseline).code.length); + console2.log("ERC20-only runtime bytes", address(erc20Only).code.length); + console2.log("native batch-only runtime bytes", address(nativeBatchOnly).code.length); + console2.log("native single-only runtime bytes", address(nativeSingleOnly).code.length); + console2.log("approval-policy runtime bytes", address(approvalPolicy).code.length); + console2.log("integrated one-shot runtime bytes", address(integratedOneShot).code.length); + } + + function _measure( + Caveat[] memory caveats_, + Execution[] memory executions_, + bool batch_ + ) + private + returns (GasMeasurement memory) + { + require(batch_, "MetaSwapBatchDesignGasComparisonTest:batch-required"); + return _measureRaw(caveats_, ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(executions_)); + } + + function _measureRaw( + Caveat[] memory caveats_, + ModeCode mode_, + bytes memory executionCallData_ + ) + private + returns (GasMeasurement memory measurement_) + { + Delegation memory delegation_ = _sign(caveats_); + bytes memory redeemCallData_ = _encodeRedeem(delegation_, mode_, executionCallData_); + measurement_.calldataBytes = redeemCallData_.length; + measurement_.calldataGas = _calldataGas(redeemCallData_); + + vm.prank(relayer); + uint256 gasBefore_ = gasleft(); + (bool success_, bytes memory returnData_) = address(delegationManager).call(redeemCallData_); + measurement_.executionGas = gasBefore_ - gasleft(); + assertTrue(success_, string(returnData_)); + + measurement_.estimatedTransactionGas = INTRINSIC_GAS + measurement_.calldataGas + measurement_.executionGas; + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), INITIAL_TOKEN_OUT_BALANCE + TOKEN_OUT_AMOUNT); + } + + function _baselineBundle(address tokenIn_, bool resetApproval_) private view returns (Caveat[] memory caveats_) { + caveats_ = new Caveat[](3); + caveats_[0] = Caveat({ enforcer: address(baseline), terms: _baselineTerms(tokenIn_, resetApproval_), args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(limitedCalls), terms: abi.encode(uint256(1)), args: hex"" }); + caveats_[2] = Caveat({ + enforcer: address(balanceChange), + terms: abi.encodePacked(false, address(tokenOut), address(users.alice.deleGator), TOKEN_OUT_MIN), + args: hex"" + }); + } + + function _oneCaveat(address enforcer_, bytes memory terms_) private pure returns (Caveat[] memory caveats_) { + caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: enforcer_, terms: terms_, args: hex"" }); + } + + function _sign(Caveat[] memory caveats_) private view returns (Delegation memory delegation_) { + delegation_ = Delegation({ + delegate: ANY_DELEGATE, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + delegation_ = signDelegation(users.alice, delegation_); + } + + function _redeem(Delegation memory delegation_, ModeCode mode_, bytes memory executionCallData_) private { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = mode_; + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = executionCallData_; + + vm.prank(relayer); + delegationManager.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); + } + + function _encodeRedeem( + Delegation memory delegation_, + ModeCode mode_, + bytes memory executionCallData_ + ) + private + pure + returns (bytes memory) + { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = mode_; + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = executionCallData_; + return + abi.encodeWithSelector(IDelegationManager.redeemDelegations.selector, permissionContexts_, modes_, executionCallDatas_); + } + + function _nativeBatch() private view returns (Execution[] memory executions_) { + executions_ = new Execution[](1); + executions_[0] = _swap(address(0), TOKEN_IN_AMOUNT); + } + + function _erc20Batch(bool resetApproval_, bool skipApproval_) private view returns (Execution[] memory) { + return _erc20Batch(resetApproval_, skipApproval_, TOKEN_OUT_AMOUNT); + } + + function _erc20Batch( + bool resetApproval_, + bool skipApproval_, + uint256 tokenOutAmount_ + ) + private + view + returns (Execution[] memory executions_) + { + if (skipApproval_) { + executions_ = new Execution[](1); + executions_[0] = _swap(address(tokenIn), 0, tokenOutAmount_); + return executions_; + } + + uint256 swapIndex_ = resetApproval_ ? 2 : 1; + executions_ = new Execution[](swapIndex_ + 1); + if (resetApproval_) executions_[0] = _approval(0); + executions_[swapIndex_ - 1] = _approval(TOKEN_IN_AMOUNT); + executions_[swapIndex_] = _swap(address(tokenIn), 0, tokenOutAmount_); + } + + function _approval(uint256 amount_) private view returns (Execution memory) { + return + Execution({ target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), amount_)) }); + } + + function _swap(address tokenIn_, uint256 value_) private view returns (Execution memory) { + return _swap(tokenIn_, value_, TOKEN_OUT_AMOUNT); + } + + function _swap(address tokenIn_, uint256 value_, uint256 tokenOutAmount_) private view returns (Execution memory) { + return Execution({ + target: address(router), + value: value_, + callData: abi.encodeCall( + IMetaSwap.swap, + ( + "route-selected-by-redeemer", + IERC20(tokenIn_), + TOKEN_IN_AMOUNT, + abi.encode(IERC20(address(tokenOut)), tokenOutAmount_) + ) + ) + }); + } + + function _baselineTerms(address tokenIn_, bool resetApproval_) private view returns (bytes memory) { + return abi.encodePacked(address(router), tokenIn_, TOKEN_IN_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)); + } + + function _nativeTerms() private view returns (bytes memory) { + return abi.encodePacked(address(router), TOKEN_IN_AMOUNT); + } + + function _policyTerms() private view returns (bytes memory) { + return abi.encodePacked(address(router), address(tokenIn), TOKEN_IN_AMOUNT, bytes1(ALL_APPROVAL_MODES)); + } + + function _integratedTerms(address tokenIn_, bool resetApproval_) private view returns (bytes memory) { + return abi.encodePacked( + address(router), + tokenIn_, + TOKEN_IN_AMOUNT, + bytes1(resetApproval_ ? 0x01 : 0x00), + address(tokenOut), + address(users.alice.deleGator), + TOKEN_OUT_MIN + ); + } + + function _setAllowance(uint256 amount_) private { + vm.prank(address(users.alice.deleGator)); + tokenIn.approve(address(router), amount_); + } + + function _calldataGas(bytes memory data_) private pure returns (uint256 gas_) { + for (uint256 i_; i_ < data_.length; ++i_) { + gas_ += data_[i_] == 0 ? 4 : 16; + } + } + + function _report(string memory label_, GasMeasurement memory measurement_) private pure { + console2.log(label_); + console2.log(" execution gas", measurement_.executionGas); + console2.log(" calldata bytes", measurement_.calldataBytes); + console2.log(" calldata gas", measurement_.calldataGas); + console2.log(" estimated transaction gas", measurement_.estimatedTransactionGas); + } +} diff --git a/test/experiments/MetaSwapBatchHashGasComparison.t.sol b/test/experiments/MetaSwapBatchHashGasComparison.t.sol new file mode 100644 index 00000000..88e2941b --- /dev/null +++ b/test/experiments/MetaSwapBatchHashGasComparison.t.sol @@ -0,0 +1,861 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test, console2 } from "forge-std/Test.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcer } from "../../src/enforcers/CaveatEnforcer.sol"; +import { MetaSwapBatchCalldataEnforcer } from "../../src/enforcers/MetaSwapBatchCalldataEnforcer.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import { Implementation, SignatureType } from "../utils/Types.t.sol"; +import { MockLimitOrderRouter } from "../utils/MockLimitOrderRouter.sol"; + +abstract contract MetaSwapHashEnforcerBase is CaveatEnforcer { + using ExecutionLib for bytes; + + bytes32 internal constant APPROVAL_TYPEHASH = keccak256("MetaSwapApproval(address target,uint256 value,bytes32 callDataHash)"); + bytes32 internal constant SWAP_TYPEHASH = + keccak256("MetaSwapSwap(address target,uint256 value,bytes4 selector,address tokenFrom,uint256 amount)"); + bytes32 internal constant APPROVAL_SEQUENCE_TYPEHASH = + keccak256("MetaSwapApprovalSequence(bytes32 firstApproval,bytes32 secondApproval)"); + bytes32 internal constant BATCH_TYPEHASH = + keccak256("MetaSwapBatch(uint8 executionCount,bytes32 approvalSequenceHash,bytes32 swapHash)"); + + uint256 internal constant SWAP_CALL_MIN_LENGTH = 132; + + function _approvalHash(Execution calldata execution_) internal pure returns (bytes32) { + return keccak256(abi.encode(APPROVAL_TYPEHASH, execution_.target, execution_.value, keccak256(execution_.callData))); + } + + function _swapHash(Execution calldata execution_) internal pure returns (bytes32) { + bytes calldata callData_ = execution_.callData; + require(callData_.length >= SWAP_CALL_MIN_LENGTH, "MetaSwapHashEnforcer:invalid-swap"); + return keccak256( + abi.encode( + SWAP_TYPEHASH, + execution_.target, + execution_.value, + bytes4(callData_[0:4]), + address(uint160(uint256(bytes32(callData_[36:68])))), + uint256(bytes32(callData_[68:100])) + ) + ); + } + + function _approvalSequenceAndSwap(Execution[] calldata executions_) + internal + pure + returns (bytes32 approvalSequenceHash_, bytes32 swapHash_) + { + uint256 length_ = executions_.length; + require(length_ >= 1 && length_ <= 3, "MetaSwapHashEnforcer:invalid-batch-length"); + + if (length_ == 1) { + swapHash_ = _swapHash(executions_[0]); + } else if (length_ == 2) { + approvalSequenceHash_ = _approvalHash(executions_[0]); + swapHash_ = _swapHash(executions_[1]); + } else { + approvalSequenceHash_ = + keccak256(abi.encode(APPROVAL_SEQUENCE_TYPEHASH, _approvalHash(executions_[0]), _approvalHash(executions_[1]))); + swapHash_ = _swapHash(executions_[2]); + } + } +} + +/// @notice Compares every approval hash independently and compares one partial swap hash. +contract IndividualExecutionHashEnforcer is MetaSwapHashEnforcerBase { + using ExecutionLib for bytes; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + uint256 length_ = executions_.length; + require(length_ >= 1 && length_ <= 3, "IndividualExecutionHashEnforcer:invalid-batch-length"); + require(terms_.length == length_ * 32, "IndividualExecutionHashEnforcer:invalid-terms"); + + if (length_ == 1) { + require(_swapHash(executions_[0]) == bytes32(terms_[0:32]), "IndividualExecutionHashEnforcer:invalid-swap"); + } else if (length_ == 2) { + require(_approvalHash(executions_[0]) == bytes32(terms_[0:32]), "IndividualExecutionHashEnforcer:invalid-approval"); + require(_swapHash(executions_[1]) == bytes32(terms_[32:64]), "IndividualExecutionHashEnforcer:invalid-swap"); + } else { + require(_approvalHash(executions_[0]) == bytes32(terms_[0:32]), "IndividualExecutionHashEnforcer:invalid-approval"); + require(_approvalHash(executions_[1]) == bytes32(terms_[32:64]), "IndividualExecutionHashEnforcer:invalid-approval"); + require(_swapHash(executions_[2]) == bytes32(terms_[64:96]), "IndividualExecutionHashEnforcer:invalid-swap"); + } + } +} + +/// @notice Compares one approval-sequence hash and one partial swap hash. +contract ApprovalSequenceHashEnforcer is MetaSwapHashEnforcerBase { + using ExecutionLib for bytes; + + uint256 private constant TERMS_LENGTH = 64; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "ApprovalSequenceHashEnforcer:invalid-terms"); + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + (bytes32 approvalSequenceHash_, bytes32 swapHash_) = _approvalSequenceAndSwap(executions_); + require(approvalSequenceHash_ == bytes32(terms_[0:32]), "ApprovalSequenceHashEnforcer:invalid-approval-sequence"); + require(swapHash_ == bytes32(terms_[32:64]), "ApprovalSequenceHashEnforcer:invalid-swap"); + } +} + +/// @notice Compares one hash committing to the execution count, approval sequence, and partial swap hash. +contract CombinedConstraintHashEnforcer is MetaSwapHashEnforcerBase { + using ExecutionLib for bytes; + + uint256 private constant TERMS_LENGTH = 32; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "CombinedConstraintHashEnforcer:invalid-terms"); + bytes32 constraintHash_ = _constraintHash(executionCallData_); + require(constraintHash_ == bytes32(terms_[0:32]), "CombinedConstraintHashEnforcer:invalid-constraints"); + } + + function _constraintHash(bytes calldata executionCallData_) private pure returns (bytes32) { + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + (bytes32 approvalSequenceHash_, bytes32 swapHash_) = _approvalSequenceAndSwap(executions_); + return keccak256(abi.encode(BATCH_TYPEHASH, uint8(executions_.length), approvalSequenceHash_, swapHash_)); + } +} + +/// @notice Uses the swap hash directly for native and one combined constraint hash for ERC-20 batches. +contract HybridConstraintHashEnforcer is MetaSwapHashEnforcerBase { + using ExecutionLib for bytes; + + uint256 private constant TERMS_LENGTH = 32; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "HybridConstraintHashEnforcer:invalid-terms"); + bytes32 constraintHash_ = _constraintHash(executionCallData_); + require(constraintHash_ == bytes32(terms_[0:32]), "HybridConstraintHashEnforcer:invalid-constraints"); + } + + function _constraintHash(bytes calldata executionCallData_) private pure returns (bytes32) { + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + (bytes32 approvalSequenceHash_, bytes32 swapHash_) = _approvalSequenceAndSwap(executions_); + return executions_.length == 1 + ? swapHash_ + : keccak256(abi.encode(BATCH_TYPEHASH, uint8(executions_.length), approvalSequenceHash_, swapHash_)); + } +} + +/// @notice Uses one fixed-field hash per approval instead of hashing approval calldata separately. +contract CombinedFieldHashEnforcer is MetaSwapHashEnforcerBase { + using ExecutionLib for bytes; + + bytes32 private constant APPROVAL_FIELDS_TYPEHASH = keccak256( + "MetaSwapApprovalFields(address target,uint256 value,bytes4 selector,address spender,uint256 amount)" + ); + uint256 private constant TERMS_LENGTH = 32; + uint256 private constant APPROVAL_CALL_LENGTH = 68; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "CombinedFieldHashEnforcer:invalid-terms"); + bytes32 constraintHash_ = _constraintHash(executionCallData_); + require(constraintHash_ == bytes32(terms_[0:32]), "CombinedFieldHashEnforcer:invalid-constraints"); + } + + function _constraintHash(bytes calldata executionCallData_) private pure returns (bytes32) { + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + uint256 length_ = executions_.length; + require(length_ >= 1 && length_ <= 3, "CombinedFieldHashEnforcer:invalid-batch-length"); + + bytes32 approvalSequenceHash_; + bytes32 swapHash_; + if (length_ == 1) { + swapHash_ = _swapHash(executions_[0]); + } else if (length_ == 2) { + approvalSequenceHash_ = _approvalFieldsHash(executions_[0]); + swapHash_ = _swapHash(executions_[1]); + } else { + approvalSequenceHash_ = keccak256( + abi.encode( + APPROVAL_SEQUENCE_TYPEHASH, + _approvalFieldsHash(executions_[0]), + _approvalFieldsHash(executions_[1]) + ) + ); + swapHash_ = _swapHash(executions_[2]); + } + return keccak256(abi.encode(BATCH_TYPEHASH, uint8(length_), approvalSequenceHash_, swapHash_)); + } + + function _approvalFieldsHash(Execution calldata execution_) private pure returns (bytes32) { + bytes calldata callData_ = execution_.callData; + require(callData_.length == APPROVAL_CALL_LENGTH, "CombinedFieldHashEnforcer:invalid-approval"); + return keccak256( + abi.encode( + APPROVAL_FIELDS_TYPEHASH, + execution_.target, + execution_.value, + bytes4(callData_[0:4]), + address(uint160(uint256(bytes32(callData_[4:36])))), + uint256(bytes32(callData_[36:68])) + ) + ); + } +} + +/// @notice Keeps domain separation only on the final signed constraint commitment. +contract OuterTypehashConstraintEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + + bytes32 private constant BATCH_TYPEHASH = + keccak256("MetaSwapBatch(uint8 executionCount,bytes32 approvalSequenceHash,bytes32 swapHash)"); + uint256 private constant TERMS_LENGTH = 32; + uint256 private constant SWAP_CALL_MIN_LENGTH = 132; + + function beforeHook( + bytes calldata terms_, + bytes calldata, + ModeCode mode_, + bytes calldata executionCallData_, + bytes32, + address, + address + ) + public + pure + override + onlyBatchCallTypeMode(mode_) + onlyDefaultExecutionMode(mode_) + { + require(terms_.length == TERMS_LENGTH, "OuterTypehashConstraintEnforcer:invalid-terms"); + bytes32 constraintHash_ = _constraintHash(executionCallData_); + require(constraintHash_ == bytes32(terms_[0:32]), "OuterTypehashConstraintEnforcer:invalid-constraints"); + } + + function _constraintHash(bytes calldata executionCallData_) private pure returns (bytes32) { + Execution[] calldata executions_ = executionCallData_.decodeBatch(); + uint256 length_ = executions_.length; + require(length_ >= 1 && length_ <= 3, "OuterTypehashConstraintEnforcer:invalid-batch-length"); + + bytes32 approvalSequenceHash_; + bytes32 swapHash_; + if (length_ == 1) { + swapHash_ = _swapHash(executions_[0]); + } else if (length_ == 2) { + approvalSequenceHash_ = _approvalHash(executions_[0]); + swapHash_ = _swapHash(executions_[1]); + } else { + approvalSequenceHash_ = + keccak256(abi.encode(_approvalHash(executions_[0]), _approvalHash(executions_[1]))); + swapHash_ = _swapHash(executions_[2]); + } + return keccak256(abi.encode(BATCH_TYPEHASH, uint8(length_), approvalSequenceHash_, swapHash_)); + } + + function _approvalHash(Execution calldata execution_) private pure returns (bytes32) { + return keccak256(abi.encode(execution_.target, execution_.value, keccak256(execution_.callData))); + } + + function _swapHash(Execution calldata execution_) private pure returns (bytes32) { + bytes calldata callData_ = execution_.callData; + require(callData_.length >= SWAP_CALL_MIN_LENGTH, "OuterTypehashConstraintEnforcer:invalid-swap"); + return keccak256( + abi.encode( + execution_.target, + execution_.value, + bytes4(callData_[0:4]), + address(uint160(uint256(bytes32(callData_[36:68])))), + uint256(bytes32(callData_[68:100])) + ) + ); + } +} + +contract MetaSwapBatchHashGasComparisonTest is BaseTest { + uint256 private constant TOKEN_IN_AMOUNT = 100 ether; + uint256 private constant TOKEN_OUT_AMOUNT = 200 ether; + uint256 private constant INTRINSIC_GAS = 21_000; + + bytes32 private constant APPROVAL_TYPEHASH = keccak256("MetaSwapApproval(address target,uint256 value,bytes32 callDataHash)"); + bytes32 private constant SWAP_TYPEHASH = + keccak256("MetaSwapSwap(address target,uint256 value,bytes4 selector,address tokenFrom,uint256 amount)"); + bytes32 private constant APPROVAL_SEQUENCE_TYPEHASH = + keccak256("MetaSwapApprovalSequence(bytes32 firstApproval,bytes32 secondApproval)"); + bytes32 private constant APPROVAL_FIELDS_TYPEHASH = keccak256( + "MetaSwapApprovalFields(address target,uint256 value,bytes4 selector,address spender,uint256 amount)" + ); + bytes32 private constant BATCH_TYPEHASH = + keccak256("MetaSwapBatch(uint8 executionCount,bytes32 approvalSequenceHash,bytes32 swapHash)"); + + struct GasMeasurement { + uint256 executionGas; + uint256 calldataBytes; + uint256 calldataGas; + uint256 estimatedTransactionGas; + } + + MetaSwapBatchCalldataEnforcer private baseline; + IndividualExecutionHashEnforcer private individualHashes; + ApprovalSequenceHashEnforcer private sequenceHash; + CombinedConstraintHashEnforcer private combinedHash; + HybridConstraintHashEnforcer private hybridHash; + CombinedFieldHashEnforcer private combinedFieldHash; + OuterTypehashConstraintEnforcer private outerTypehash; + MockLimitOrderRouter private router; + BasicERC20 private tokenIn; + BasicERC20 private tokenOut; + address private relayer; + + constructor() { + IMPLEMENTATION = Implementation.MultiSig; + SIGNATURE_TYPE = SignatureType.MultiSig; + } + + function setUp() public override { + super.setUp(); + + router = new MockLimitOrderRouter(); + baseline = new MetaSwapBatchCalldataEnforcer(); + individualHashes = new IndividualExecutionHashEnforcer(); + sequenceHash = new ApprovalSequenceHashEnforcer(); + combinedHash = new CombinedConstraintHashEnforcer(); + hybridHash = new HybridConstraintHashEnforcer(); + combinedFieldHash = new CombinedFieldHashEnforcer(); + outerTypehash = new OuterTypehashConstraintEnforcer(); + tokenIn = new BasicERC20(address(users.alice.deleGator), "Token In", "TIN", 1_000 ether); + tokenOut = new BasicERC20(address(router), "Token Out", "TOUT", 10_000 ether); + relayer = makeAddr("Relayer"); + vm.deal(address(users.alice.deleGator), 1_000 ether); + } + + function test_gas_baseline_native() public { + Execution[] memory executions_ = _nativeBatch("route-a"); + _report("baseline / native", _measure(address(baseline), _baselineTerms(address(0), false), executions_)); + } + + function test_gas_individualHashes_native() public { + Execution[] memory executions_ = _nativeBatch("route-a"); + _report("individual hashes / native", _measure(address(individualHashes), _individualTerms(executions_), executions_)); + } + + function test_gas_sequenceHash_native() public { + Execution[] memory executions_ = _nativeBatch("route-a"); + _report( + "approval-sequence + swap hashes / native", _measure(address(sequenceHash), _sequenceTerms(executions_), executions_) + ); + } + + function test_gas_combinedHash_native() public { + Execution[] memory executions_ = _nativeBatch("route-a"); + _report("combined hash / native", _measure(address(combinedHash), _combinedTerms(executions_), executions_)); + } + + function test_gas_hybridHash_native() public { + Execution[] memory executions_ = _nativeBatch("route-a"); + _report("hybrid hash / native", _measure(address(hybridHash), _hybridTerms(executions_), executions_)); + } + + function test_gas_combinedFieldHash_native() public { + Execution[] memory executions_ = _nativeBatch("route-a"); + _report( + "combined field hash / native", + _measure(address(combinedFieldHash), _combinedFieldTerms(executions_), executions_) + ); + } + + function test_gas_outerTypehash_native() public { + Execution[] memory executions_ = _nativeBatch("route-a"); + _report( + "outer typehash only / native", + _measure(address(outerTypehash), _outerTypehashTerms(executions_), executions_) + ); + } + + function test_gas_baseline_oneApproval() public { + Execution[] memory executions_ = _erc20Batch(false, "route-a"); + _report("baseline / approve(amount)", _measure(address(baseline), _baselineTerms(address(tokenIn), false), executions_)); + } + + function test_gas_individualHashes_oneApproval() public { + Execution[] memory executions_ = _erc20Batch(false, "route-a"); + _report( + "individual hashes / approve(amount)", _measure(address(individualHashes), _individualTerms(executions_), executions_) + ); + } + + function test_gas_sequenceHash_oneApproval() public { + Execution[] memory executions_ = _erc20Batch(false, "route-a"); + _report( + "approval-sequence + swap hashes / approve(amount)", + _measure(address(sequenceHash), _sequenceTerms(executions_), executions_) + ); + } + + function test_gas_combinedHash_oneApproval() public { + Execution[] memory executions_ = _erc20Batch(false, "route-a"); + _report("combined hash / approve(amount)", _measure(address(combinedHash), _combinedTerms(executions_), executions_)); + } + + function test_gas_hybridHash_oneApproval() public { + Execution[] memory executions_ = _erc20Batch(false, "route-a"); + _report("hybrid hash / approve(amount)", _measure(address(hybridHash), _hybridTerms(executions_), executions_)); + } + + function test_gas_combinedFieldHash_oneApproval() public { + Execution[] memory executions_ = _erc20Batch(false, "route-a"); + _report( + "combined field hash / approve(amount)", + _measure(address(combinedFieldHash), _combinedFieldTerms(executions_), executions_) + ); + } + + function test_gas_outerTypehash_oneApproval() public { + Execution[] memory executions_ = _erc20Batch(false, "route-a"); + _report( + "outer typehash only / approve(amount)", + _measure(address(outerTypehash), _outerTypehashTerms(executions_), executions_) + ); + } + + function test_gas_baseline_resetApproval() public { + _setAllowance(1); + Execution[] memory executions_ = _erc20Batch(true, "route-a"); + _report( + "baseline / approve(0) + approve(amount)", + _measure(address(baseline), _baselineTerms(address(tokenIn), true), executions_) + ); + } + + function test_gas_individualHashes_resetApproval() public { + _setAllowance(1); + Execution[] memory executions_ = _erc20Batch(true, "route-a"); + _report( + "individual hashes / approve(0) + approve(amount)", + _measure(address(individualHashes), _individualTerms(executions_), executions_) + ); + } + + function test_gas_sequenceHash_resetApproval() public { + _setAllowance(1); + Execution[] memory executions_ = _erc20Batch(true, "route-a"); + _report( + "approval-sequence + swap hashes / approve(0) + approve(amount)", + _measure(address(sequenceHash), _sequenceTerms(executions_), executions_) + ); + } + + function test_gas_combinedHash_resetApproval() public { + _setAllowance(1); + Execution[] memory executions_ = _erc20Batch(true, "route-a"); + _report( + "combined hash / approve(0) + approve(amount)", + _measure(address(combinedHash), _combinedTerms(executions_), executions_) + ); + } + + function test_gas_hybridHash_resetApproval() public { + _setAllowance(1); + Execution[] memory executions_ = _erc20Batch(true, "route-a"); + _report("hybrid hash / approve(0) + approve(amount)", _measure(address(hybridHash), _hybridTerms(executions_), executions_)); + } + + function test_gas_combinedFieldHash_resetApproval() public { + _setAllowance(1); + Execution[] memory executions_ = _erc20Batch(true, "route-a"); + _report( + "combined field hash / approve(0) + approve(amount)", + _measure(address(combinedFieldHash), _combinedFieldTerms(executions_), executions_) + ); + } + + function test_gas_outerTypehash_resetApproval() public { + _setAllowance(1); + Execution[] memory executions_ = _erc20Batch(true, "route-a"); + _report( + "outer typehash only / approve(0) + approve(amount)", + _measure(address(outerTypehash), _outerTypehashTerms(executions_), executions_) + ); + } + + function test_hashTermsAllowDifferentDynamicRouteData() public { + Execution[] memory signedExecutions_ = _erc20Batch(false, "route-a"); + Execution[] memory redeemedExecutions_ = _erc20Batch(false, "a-much-longer-route-name"); + redeemedExecutions_[1].callData = + abi.encodeCall(IMetaSwap.swap, ("different-aggregator", tokenIn, TOKEN_IN_AMOUNT, _route())); + + _enforce(address(individualHashes), _individualTerms(signedExecutions_), redeemedExecutions_); + _enforce(address(sequenceHash), _sequenceTerms(signedExecutions_), redeemedExecutions_); + _enforce(address(combinedHash), _combinedTerms(signedExecutions_), redeemedExecutions_); + _enforce(address(hybridHash), _hybridTerms(signedExecutions_), redeemedExecutions_); + _enforce(address(combinedFieldHash), _combinedFieldTerms(signedExecutions_), redeemedExecutions_); + _enforce(address(outerTypehash), _outerTypehashTerms(signedExecutions_), redeemedExecutions_); + } + + function test_hashTermsRejectChangedApproval() public { + Execution[] memory signedExecutions_ = _erc20Batch(false, "route-a"); + Execution[] memory tamperedExecutions_ = _erc20Batch(false, "route-a"); + tamperedExecutions_[0].callData = abi.encodeCall(IERC20.approve, (makeAddr("OtherSpender"), TOKEN_IN_AMOUNT)); + + vm.expectRevert("IndividualExecutionHashEnforcer:invalid-approval"); + _enforce(address(individualHashes), _individualTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("ApprovalSequenceHashEnforcer:invalid-approval-sequence"); + _enforce(address(sequenceHash), _sequenceTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("CombinedConstraintHashEnforcer:invalid-constraints"); + _enforce(address(combinedHash), _combinedTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("HybridConstraintHashEnforcer:invalid-constraints"); + _enforce(address(hybridHash), _hybridTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("CombinedFieldHashEnforcer:invalid-constraints"); + _enforce(address(combinedFieldHash), _combinedFieldTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("OuterTypehashConstraintEnforcer:invalid-constraints"); + _enforce(address(outerTypehash), _outerTypehashTerms(signedExecutions_), tamperedExecutions_); + } + + function test_hashTermsRejectChangedSwapStaticFields() public { + Execution[] memory signedExecutions_ = _erc20Batch(false, "route-a"); + Execution[] memory tamperedExecutions_ = _erc20Batch(false, "route-a"); + tamperedExecutions_[1].callData = + abi.encodeCall(IMetaSwap.swap, ("route-a", IERC20(makeAddr("OtherToken")), TOKEN_IN_AMOUNT, _route())); + + vm.expectRevert("IndividualExecutionHashEnforcer:invalid-swap"); + _enforce(address(individualHashes), _individualTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("ApprovalSequenceHashEnforcer:invalid-swap"); + _enforce(address(sequenceHash), _sequenceTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("CombinedConstraintHashEnforcer:invalid-constraints"); + _enforce(address(combinedHash), _combinedTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("HybridConstraintHashEnforcer:invalid-constraints"); + _enforce(address(hybridHash), _hybridTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("CombinedFieldHashEnforcer:invalid-constraints"); + _enforce(address(combinedFieldHash), _combinedFieldTerms(signedExecutions_), tamperedExecutions_); + + vm.expectRevert("OuterTypehashConstraintEnforcer:invalid-constraints"); + _enforce(address(outerTypehash), _outerTypehashTerms(signedExecutions_), tamperedExecutions_); + } + + function test_runtimeCodeSizes() public view { + console2.log("baseline runtime bytes", address(baseline).code.length); + console2.log("individual hashes runtime bytes", address(individualHashes).code.length); + console2.log("sequence hash runtime bytes", address(sequenceHash).code.length); + console2.log("combined hash runtime bytes", address(combinedHash).code.length); + console2.log("hybrid hash runtime bytes", address(hybridHash).code.length); + console2.log("combined field hash runtime bytes", address(combinedFieldHash).code.length); + console2.log("outer typehash only runtime bytes", address(outerTypehash).code.length); + } + + function _measure( + address enforcer_, + bytes memory terms_, + Execution[] memory executions_ + ) + private + returns (GasMeasurement memory measurement_) + { + Delegation memory delegation_ = _delegation(enforcer_, terms_); + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes memory redeemCallData_ = _encodeRedeem(delegations_, ExecutionLib.encodeBatch(executions_)); + + measurement_.calldataBytes = redeemCallData_.length; + measurement_.calldataGas = _calldataGas(redeemCallData_); + + vm.prank(relayer); + uint256 gasBefore_ = gasleft(); + (bool success_,) = address(delegationManager).call(redeemCallData_); + measurement_.executionGas = gasBefore_ - gasleft(); + assertTrue(success_); + + measurement_.estimatedTransactionGas = INTRINSIC_GAS + measurement_.calldataGas + measurement_.executionGas; + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), TOKEN_OUT_AMOUNT); + } + + function _delegation(address enforcer_, bytes memory terms_) private view returns (Delegation memory delegation_) { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: enforcer_, terms: terms_, args: hex"" }); + delegation_ = Delegation({ + delegate: ANY_DELEGATE, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + delegation_ = signDelegation(users.alice, delegation_); + } + + function _encodeRedeem(Delegation[] memory delegations_, bytes memory executionCallData_) private pure returns (bytes memory) { + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = executionCallData_; + return + abi.encodeWithSelector(IDelegationManager.redeemDelegations.selector, permissionContexts_, modes_, executionCallDatas_); + } + + function _nativeBatch(string memory aggregatorId_) private view returns (Execution[] memory executions_) { + executions_ = new Execution[](1); + executions_[0] = _swap(address(0), TOKEN_IN_AMOUNT, aggregatorId_); + } + + function _erc20Batch(bool resetApproval_, string memory aggregatorId_) private view returns (Execution[] memory executions_) { + uint256 swapIndex_ = resetApproval_ ? 2 : 1; + executions_ = new Execution[](swapIndex_ + 1); + if (resetApproval_) executions_[0] = _approval(0); + executions_[swapIndex_ - 1] = _approval(TOKEN_IN_AMOUNT); + executions_[swapIndex_] = _swap(address(tokenIn), 0, aggregatorId_); + } + + function _approval(uint256 amount_) private view returns (Execution memory) { + return + Execution({ target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), amount_)) }); + } + + function _swap(address tokenIn_, uint256 value_, string memory aggregatorId_) private view returns (Execution memory) { + return Execution({ + target: address(router), + value: value_, + callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(tokenIn_), TOKEN_IN_AMOUNT, _route())) + }); + } + + function _route() private view returns (bytes memory) { + return abi.encode(IERC20(address(tokenOut)), TOKEN_OUT_AMOUNT); + } + + function _baselineTerms(address tokenIn_, bool resetApproval_) private view returns (bytes memory) { + return abi.encodePacked(address(router), tokenIn_, TOKEN_IN_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)); + } + + function _individualTerms(Execution[] memory executions_) private view returns (bytes memory terms_) { + uint256 length_ = executions_.length; + if (length_ == 1) { + return abi.encodePacked(_swapHash(executions_[0], address(0))); + } + if (length_ == 2) { + return abi.encodePacked(_approvalHash(executions_[0]), _swapHash(executions_[1], address(tokenIn))); + } + return abi.encodePacked( + _approvalHash(executions_[0]), _approvalHash(executions_[1]), _swapHash(executions_[2], address(tokenIn)) + ); + } + + function _sequenceTerms(Execution[] memory executions_) private view returns (bytes memory) { + (bytes32 approvalSequenceHash_, bytes32 swapHash_) = _approvalSequenceAndSwap(executions_); + return abi.encodePacked(approvalSequenceHash_, swapHash_); + } + + function _combinedTerms(Execution[] memory executions_) private view returns (bytes memory) { + (bytes32 approvalSequenceHash_, bytes32 swapHash_) = _approvalSequenceAndSwap(executions_); + return abi.encodePacked(keccak256(abi.encode(BATCH_TYPEHASH, uint8(executions_.length), approvalSequenceHash_, swapHash_))); + } + + function _hybridTerms(Execution[] memory executions_) private view returns (bytes memory) { + if (executions_.length == 1) return abi.encodePacked(_swapHash(executions_[0], address(0))); + return _combinedTerms(executions_); + } + + function _combinedFieldTerms(Execution[] memory executions_) private view returns (bytes memory) { + uint256 length_ = executions_.length; + bytes32 approvalSequenceHash_; + bytes32 swapHash_; + if (length_ == 1) { + swapHash_ = _swapHash(executions_[0], address(0)); + } else if (length_ == 2) { + approvalSequenceHash_ = _approvalFieldsHash(TOKEN_IN_AMOUNT); + swapHash_ = _swapHash(executions_[1], address(tokenIn)); + } else { + approvalSequenceHash_ = keccak256( + abi.encode( + APPROVAL_SEQUENCE_TYPEHASH, + _approvalFieldsHash(0), + _approvalFieldsHash(TOKEN_IN_AMOUNT) + ) + ); + swapHash_ = _swapHash(executions_[2], address(tokenIn)); + } + return abi.encodePacked(keccak256(abi.encode(BATCH_TYPEHASH, uint8(length_), approvalSequenceHash_, swapHash_))); + } + + function _outerTypehashTerms(Execution[] memory executions_) private view returns (bytes memory) { + uint256 length_ = executions_.length; + bytes32 approvalSequenceHash_; + bytes32 swapHash_; + if (length_ == 1) { + swapHash_ = _swapHashWithoutTypehash(executions_[0], address(0)); + } else if (length_ == 2) { + approvalSequenceHash_ = _approvalHashWithoutTypehash(executions_[0]); + swapHash_ = _swapHashWithoutTypehash(executions_[1], address(tokenIn)); + } else { + approvalSequenceHash_ = keccak256( + abi.encode( + _approvalHashWithoutTypehash(executions_[0]), + _approvalHashWithoutTypehash(executions_[1]) + ) + ); + swapHash_ = _swapHashWithoutTypehash(executions_[2], address(tokenIn)); + } + return abi.encodePacked(keccak256(abi.encode(BATCH_TYPEHASH, uint8(length_), approvalSequenceHash_, swapHash_))); + } + + function _approvalSequenceAndSwap(Execution[] memory executions_) + private + view + returns (bytes32 approvalSequenceHash_, bytes32 swapHash_) + { + if (executions_.length == 1) { + swapHash_ = _swapHash(executions_[0], address(0)); + } else if (executions_.length == 2) { + approvalSequenceHash_ = _approvalHash(executions_[0]); + swapHash_ = _swapHash(executions_[1], address(tokenIn)); + } else { + approvalSequenceHash_ = + keccak256(abi.encode(APPROVAL_SEQUENCE_TYPEHASH, _approvalHash(executions_[0]), _approvalHash(executions_[1]))); + swapHash_ = _swapHash(executions_[2], address(tokenIn)); + } + } + + function _approvalHash(Execution memory execution_) private pure returns (bytes32) { + return keccak256(abi.encode(APPROVAL_TYPEHASH, execution_.target, execution_.value, keccak256(execution_.callData))); + } + + function _approvalHashWithoutTypehash(Execution memory execution_) private pure returns (bytes32) { + return keccak256(abi.encode(execution_.target, execution_.value, keccak256(execution_.callData))); + } + + function _approvalFieldsHash(uint256 amount_) private view returns (bytes32) { + return keccak256( + abi.encode( + APPROVAL_FIELDS_TYPEHASH, + address(tokenIn), + uint256(0), + IERC20.approve.selector, + address(router), + amount_ + ) + ); + } + + function _swapHash(Execution memory execution_, address tokenIn_) private pure returns (bytes32) { + return keccak256( + abi.encode(SWAP_TYPEHASH, execution_.target, execution_.value, IMetaSwap.swap.selector, tokenIn_, TOKEN_IN_AMOUNT) + ); + } + + function _swapHashWithoutTypehash(Execution memory execution_, address tokenIn_) private pure returns (bytes32) { + return keccak256( + abi.encode(execution_.target, execution_.value, IMetaSwap.swap.selector, tokenIn_, TOKEN_IN_AMOUNT) + ); + } + + function _setAllowance(uint256 amount_) private { + vm.prank(address(users.alice.deleGator)); + tokenIn.approve(address(router), amount_); + } + + function _enforce(address enforcer_, bytes memory terms_, Execution[] memory executions_) private { + CaveatEnforcer(enforcer_) + .beforeHook( + terms_, + hex"", + ModeLib.encodeSimpleBatch(), + ExecutionLib.encodeBatch(executions_), + bytes32(0), + address(users.alice.deleGator), + relayer + ); + } + + function _calldataGas(bytes memory data_) private pure returns (uint256 gas_) { + for (uint256 i_; i_ < data_.length; ++i_) { + gas_ += data_[i_] == 0 ? 4 : 16; + } + } + + function _report(string memory label_, GasMeasurement memory measurement_) private pure { + console2.log(label_); + console2.log(" execution gas", measurement_.executionGas); + console2.log(" calldata bytes", measurement_.calldataBytes); + console2.log(" calldata gas", measurement_.calldataGas); + console2.log(" estimated transaction gas", measurement_.estimatedTransactionGas); + } +} diff --git a/test/experiments/allowed-calldata-limit-order/AllowedCalldataLimitOrder.t.sol b/test/experiments/allowed-calldata-limit-order/AllowedCalldataLimitOrder.t.sol new file mode 100644 index 00000000..6bc7a398 --- /dev/null +++ b/test/experiments/allowed-calldata-limit-order/AllowedCalldataLimitOrder.t.sol @@ -0,0 +1,554 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { BaseTest } from "../../utils/BaseTest.t.sol"; +import { BasicERC20 } from "../../utils/BasicERC20.t.sol"; +import { Implementation, SignatureType } from "../../utils/Types.t.sol"; +import { AllowedCalldataEnforcer } from "../../../src/enforcers/AllowedCalldataEnforcer.sol"; +import { AllowedTargetsEnforcer } from "../../../src/enforcers/AllowedTargetsEnforcer.sol"; +import { ERC20BalanceChangeEnforcer } from "../../../src/enforcers/ERC20BalanceChangeEnforcer.sol"; +import { LimitedCallsEnforcer } from "../../../src/enforcers/LimitedCallsEnforcer.sol"; +import { MetaSwapBatchCalldataEnforcer } from "../../../src/enforcers/MetaSwapBatchCalldataEnforcer.sol"; +import { MetaSwap7702CalldataEnforcer } from "../../../src/enforcers/MetaSwap7702CalldataEnforcer.sol"; +import { NativeBalanceChangeEnforcer } from "../../../src/enforcers/NativeBalanceChangeEnforcer.sol"; +import { NativeTokenTransferAmountEnforcer } from "../../../src/enforcers/NativeTokenTransferAmountEnforcer.sol"; +import { IERC7821 } from "../../../src/interfaces/IERC7821.sol"; +import { IMetaSwap } from "../../../src/helpers/interfaces/IMetaSwap.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../../../src/utils/Types.sol"; + +contract FlexibleMetaSwapMock is IMetaSwap { + using SafeERC20 for IERC20; + + receive() external payable { } + + function swap(string calldata, IERC20 tokenFrom_, uint256 amount_, bytes calldata data_) external payable { + (IERC20 tokenOut_, uint256 amountOut_,) = abi.decode(data_, (IERC20, uint256, bytes)); + + if (address(tokenFrom_) == address(0)) { + require(msg.value == amount_, "invalid-native-input"); + } else { + require(msg.value == 0, "unexpected-value"); + tokenFrom_.safeTransferFrom(msg.sender, address(this), amount_); + } + + if (address(tokenOut_) == address(0)) { + (bool success_,) = msg.sender.call{ value: amountOut_ }(""); + require(success_, "native-output-failed"); + } else { + tokenOut_.safeTransfer(msg.sender, amountOut_); + } + } + + function setAdapter(string calldata, address, bytes4, bytes calldata) external { } + + function removeAdapter(string calldata) external { } + + function adapters(string memory) external pure returns (Adapter memory adapter_) { + adapter_ = Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); + } +} + +/** + * @notice Proof that disjoint AllowedCalldata checks can secure a flexible MetaSwap route inside one outer 7702 execution. + * @dev This intentionally uses the canonical manager. It validates the caveat composition before changing the specialized manager. + */ +contract AllowedCalldataLimitOrderTest is BaseTest { + using SafeERC20 for IERC20; + + uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; + uint256 internal constant TOKEN_OUT_MIN = 190 ether; + uint256 internal constant ACTUAL_TOKEN_OUT = 200 ether; + + // IERC7821.execute(ModeCode,bytes): selector + mode + bytes offset. + uint256 internal constant OUTER_HEADER_LENGTH = 68; + // The dynamic batch bytes begin after selector + two ABI head words + bytes length. + uint256 internal constant INNER_BATCH_START = 100; + + AllowedCalldataEnforcer internal allowedCalldataEnforcer; + AllowedTargetsEnforcer internal allowedTargetsEnforcer; + NativeTokenTransferAmountEnforcer internal outerValueEnforcer; + LimitedCallsEnforcer internal limitedCallsEnforcer; + MetaSwapBatchCalldataEnforcer internal metaSwapBatchCalldataEnforcer; + MetaSwap7702CalldataEnforcer internal metaSwap7702CalldataEnforcer; + ERC20BalanceChangeEnforcer internal erc20BalanceChangeEnforcer; + NativeBalanceChangeEnforcer internal nativeBalanceChangeEnforcer; + + BasicERC20 internal tokenIn; + BasicERC20 internal tokenOut; + BasicERC20 internal preapprovedToken; + FlexibleMetaSwapMock internal metaSwap; + + address internal alice; + address internal relayer; + + constructor() { + IMPLEMENTATION = Implementation.EIP7702Stateless; + SIGNATURE_TYPE = SignatureType.EOA; + } + + function setUp() public override { + super.setUp(); + + allowedCalldataEnforcer = new AllowedCalldataEnforcer(); + allowedTargetsEnforcer = new AllowedTargetsEnforcer(); + outerValueEnforcer = new NativeTokenTransferAmountEnforcer(); + limitedCallsEnforcer = new LimitedCallsEnforcer(); + metaSwapBatchCalldataEnforcer = new MetaSwapBatchCalldataEnforcer(); + metaSwap7702CalldataEnforcer = new MetaSwap7702CalldataEnforcer(); + erc20BalanceChangeEnforcer = new ERC20BalanceChangeEnforcer(); + nativeBalanceChangeEnforcer = new NativeBalanceChangeEnforcer(); + + alice = address(users.alice.deleGator); + relayer = makeAddr("Relayer"); + + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + preapprovedToken = new BasicERC20(address(this), "Preapproved Token", "OLD", 0); + metaSwap = new FlexibleMetaSwapMock(); + + tokenIn.mint(alice, 1_000 ether); + preapprovedToken.mint(alice, 1_000 ether); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + function test_erc20OneApproval_routeAndAggregatorRemainFlexible() public { + Execution memory template_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex"01"))); + Delegation memory delegation_ = _sign(_secureCaveats(template_, tokenOut, TOKEN_OUT_MIN)); + + // Different dynamic string and bytes lengths are accepted after the user signs. + Execution memory fill_ = _wrap( + _erc20Inner( + false, + tokenIn, + TOKEN_IN_AMOUNT, + "a-different-aggregator", + _route(tokenOut, ACTUAL_TOKEN_OUT, hex"010203040506070809") + ) + ); + + _redeem(delegation_, fill_); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT); + } + + function test_erc20ResetAndApproval_routeRemainsFlexible() public { + vm.prank(alice); + tokenIn.approve(address(metaSwap), 1); + + Execution memory template_ = + _wrap(_erc20Inner(true, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex""))); + Delegation memory delegation_ = _sign(_secureCaveats(template_, tokenOut, TOKEN_OUT_MIN)); + Execution memory fill_ = + _wrap(_erc20Inner(true, tokenIn, TOKEN_IN_AMOUNT, "new-route", _route(tokenOut, ACTUAL_TOKEN_OUT, new bytes(96)))); + + _redeem(delegation_, fill_); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT); + assertEq(tokenIn.allowance(alice, address(metaSwap)), 0); + } + + function test_customEnforcer_erc20OneApproval_sameCanonicalManager() public { + Delegation memory delegation_ = _sign(_customCaveats(false, tokenOut, TOKEN_OUT_MIN)); + Execution memory fill_ = _wrap( + _erc20Inner( + false, tokenIn, TOKEN_IN_AMOUNT, "a-different-aggregator", _route(tokenOut, ACTUAL_TOKEN_OUT, new bytes(96)) + ) + ); + + _redeem(delegation_, fill_); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT); + } + + function test_customEnforcer_erc20ResetApproval_sameCanonicalManager() public { + vm.prank(alice); + tokenIn.approve(address(metaSwap), 1); + + Delegation memory delegation_ = _sign(_customCaveats(true, tokenOut, TOKEN_OUT_MIN)); + Execution memory fill_ = + _wrap(_erc20Inner(true, tokenIn, TOKEN_IN_AMOUNT, "new-route", _route(tokenOut, ACTUAL_TOKEN_OUT, new bytes(96)))); + + _redeem(delegation_, fill_); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT); + } + + function test_directBatchEnforcer_erc20OneApproval_sameCanonicalManager() public { + Delegation memory delegation_ = _sign(_directBatchCaveats(false, tokenOut, TOKEN_OUT_MIN)); + Execution[] memory executions_ = + _erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "best-route", _route(tokenOut, ACTUAL_TOKEN_OUT, new bytes(96))); + + _redeemBatch(delegation_, executions_); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT); + } + + function test_directBatchEnforcer_erc20ResetApproval_sameCanonicalManager() public { + vm.prank(alice); + tokenIn.approve(address(metaSwap), 1); + + Delegation memory delegation_ = _sign(_directBatchCaveats(true, tokenOut, TOKEN_OUT_MIN)); + Execution[] memory executions_ = + _erc20Inner(true, tokenIn, TOKEN_IN_AMOUNT, "best-route", _route(tokenOut, ACTUAL_TOKEN_OUT, new bytes(96))); + + _redeemBatch(delegation_, executions_); + + assertEq(tokenIn.balanceOf(alice), 900 ether); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT); + } + + function test_nativeInput_singleInnerExecutionAndFlexibleRoute() public { + Execution memory template_ = _wrap(_nativeInner("template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex"01"))); + Delegation memory delegation_ = _sign(_secureCaveats(template_, tokenOut, TOKEN_OUT_MIN)); + Execution memory fill_ = + _wrap(_nativeInner("better-aggregator", _route(tokenOut, ACTUAL_TOKEN_OUT, hex"010203040506070809"))); + + uint256 nativeBefore_ = alice.balance; + _redeem(delegation_, fill_); + + assertEq(alice.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT); + } + + function test_nativeOutput_balanceCaveatWorksWithFlexibleRoute() public { + Execution memory template_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "template", _route(IERC20(address(0)), ACTUAL_TOKEN_OUT, hex""))); + Delegation memory delegation_ = _sign(_secureNativeOutputCaveats(template_, TOKEN_OUT_MIN)); + Execution memory fill_ = _wrap( + _erc20Inner( + false, tokenIn, TOKEN_IN_AMOUNT, "better-aggregator", _route(IERC20(address(0)), ACTUAL_TOKEN_OUT, new bytes(64)) + ) + ); + + uint256 nativeBefore_ = alice.balance; + _redeem(delegation_, fill_); + + assertEq(alice.balance, nativeBefore_ + ACTUAL_TOKEN_OUT); + } + + function test_insufficientOutputRevertsButDifferentRouteCanRetry() public { + Execution memory template_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, TOKEN_OUT_MIN, hex""))); + Delegation memory delegation_ = _sign(_secureCaveats(template_, tokenOut, TOKEN_OUT_MIN)); + + Execution memory badFill_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "bad", _route(tokenOut, TOKEN_OUT_MIN - 1, bytes("bad")))); + vm.expectRevert("ERC20BalanceChangeEnforcer:insufficient-balance-increase"); + _redeem(delegation_, badFill_); + + Execution memory goodFill_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "good", _route(tokenOut, TOKEN_OUT_MIN, bytes("good")))); + _redeem(delegation_, goodFill_); + + assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); + } + + function test_secureSlicesRejectDifferentTokenIn() public { + Execution memory template_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex""))); + Delegation memory delegation_ = _sign(_secureCaveats(template_, tokenOut, TOKEN_OUT_MIN)); + Execution memory tampered_ = + _wrap(_erc20Inner(false, preapprovedToken, TOKEN_IN_AMOUNT, "route", _route(tokenOut, ACTUAL_TOKEN_OUT, hex""))); + + vm.expectRevert("AllowedCalldataEnforcer:invalid-calldata"); + _redeem(delegation_, tampered_); + } + + function test_singlePrefixCannotKeepDifferentLengthRouteFlexible() public { + Execution memory template_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex""))); + Execution memory fill_ = _wrap( + _erc20Inner( + false, tokenIn, TOKEN_IN_AMOUNT, "longer-aggregator-name", _route(tokenOut, ACTUAL_TOKEN_OUT, new bytes(96)) + ) + ); + + uint256 selectorOffset_ = _indexOf(template_.callData, IMetaSwap.swap.selector); + bytes memory onePrefixTerms_ = abi.encodePacked(uint256(0), _slice(template_.callData, 0, selectorOffset_ + 4)); + bytes memory fillExecutionCallData_ = ExecutionLib.encodeSingle(fill_.target, fill_.value, fill_.callData); + + vm.prank(address(delegationManager)); + vm.expectRevert("AllowedCalldataEnforcer:invalid-calldata"); + allowedCalldataEnforcer.beforeHook( + onePrefixTerms_, hex"", singleDefaultMode, fillExecutionCallData_, bytes32(0), alice, relayer + ); + } + + function test_oneApprovalTermsDoNotAlsoPermitResetApprovalShape() public { + Execution memory template_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex""))); + Delegation memory delegation_ = _sign(_secureCaveats(template_, tokenOut, TOKEN_OUT_MIN)); + Execution memory resetFill_ = + _wrap(_erc20Inner(true, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex""))); + + vm.expectRevert("AllowedCalldataEnforcer:invalid-calldata"); + _redeem(delegation_, resetFill_); + } + + function test_ignoringSwapInputsCanDrainAnotherPreapprovedToken() public { + vm.prank(alice); + preapprovedToken.approve(address(metaSwap), TOKEN_IN_AMOUNT); + + Execution memory template_ = + _wrap(_erc20Inner(false, tokenIn, TOKEN_IN_AMOUNT, "template", _route(tokenOut, ACTUAL_TOKEN_OUT, hex""))); + // Deliberately omit the tokenFrom + amount slice, matching the proposed "ignore swap inputs" version. + Delegation memory unsafeDelegation_ = _sign(_unsafeCaveats(template_, tokenOut, TOKEN_OUT_MIN)); + Execution memory maliciousFill_ = _wrap( + _erc20Inner(false, preapprovedToken, TOKEN_IN_AMOUNT, "malicious-route", _route(tokenOut, ACTUAL_TOKEN_OUT, hex"")) + ); + + _redeem(unsafeDelegation_, maliciousFill_); + + assertEq(tokenIn.balanceOf(alice), 1_000 ether, "intended token was not spent"); + assertEq(preapprovedToken.balanceOf(alice), 900 ether, "unbound preapproved token was drained"); + assertEq(tokenOut.balanceOf(alice), ACTUAL_TOKEN_OUT, "output check still passed"); + } + + function _secureCaveats( + Execution memory template_, + IERC20 tokenOut_, + uint256 minOut_ + ) + private + view + returns (Caveat[] memory caveats_) + { + Caveat[] memory calldataCaveats_ = _calldataCaveats(template_, true); + caveats_ = _commonCaveats(calldataCaveats_, calldataCaveats_.length + 4); + caveats_[caveats_.length - 2] = + Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + caveats_[caveats_.length - 1] = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut_), alice, minOut_), + args: hex"" + }); + } + + function _customCaveats(bool resetApproval_, IERC20 tokenOut_, uint256 minOut_) + private + view + returns (Caveat[] memory caveats_) + { + caveats_ = new Caveat[](3); + caveats_[0] = Caveat({ + enforcer: address(metaSwap7702CalldataEnforcer), + terms: abi.encodePacked(address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)), + args: hex"" + }); + caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + caveats_[2] = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut_), alice, minOut_), + args: hex"" + }); + } + + function _directBatchCaveats( + bool resetApproval_, + IERC20 tokenOut_, + uint256 minOut_ + ) + private + view + returns (Caveat[] memory caveats_) + { + caveats_ = new Caveat[](3); + caveats_[0] = Caveat({ + enforcer: address(metaSwapBatchCalldataEnforcer), + terms: abi.encodePacked(address(metaSwap), address(tokenIn), TOKEN_IN_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)), + args: hex"" + }); + caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + caveats_[2] = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut_), alice, minOut_), + args: hex"" + }); + } + + function _secureNativeOutputCaveats( + Execution memory template_, + uint256 minOut_ + ) + private + view + returns (Caveat[] memory caveats_) + { + Caveat[] memory calldataCaveats_ = _calldataCaveats(template_, true); + caveats_ = _commonCaveats(calldataCaveats_, calldataCaveats_.length + 4); + caveats_[caveats_.length - 2] = + Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + caveats_[caveats_.length - 1] = + Caveat({ enforcer: address(nativeBalanceChangeEnforcer), terms: abi.encodePacked(false, alice, minOut_), args: hex"" }); + } + + function _unsafeCaveats( + Execution memory template_, + IERC20 tokenOut_, + uint256 minOut_ + ) + private + view + returns (Caveat[] memory caveats_) + { + Caveat[] memory calldataCaveats_ = _calldataCaveats(template_, false); + caveats_ = _commonCaveats(calldataCaveats_, calldataCaveats_.length + 4); + caveats_[caveats_.length - 2] = + Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); + caveats_[caveats_.length - 1] = Caveat({ + enforcer: address(erc20BalanceChangeEnforcer), + terms: abi.encodePacked(false, address(tokenOut_), alice, minOut_), + args: hex"" + }); + } + + function _commonCaveats(Caveat[] memory calldataCaveats_, uint256 totalLength_) + private + view + returns (Caveat[] memory caveats_) + { + caveats_ = new Caveat[](totalLength_); + caveats_[0] = Caveat({ enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(alice), args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(outerValueEnforcer), terms: abi.encode(uint256(0)), args: hex"" }); + for (uint256 i; i < calldataCaveats_.length; ++i) { + caveats_[i + 2] = calldataCaveats_[i]; + } + } + + function _calldataCaveats(Execution memory template_, bool bindSwapInputs_) private view returns (Caveat[] memory caveats_) { + uint256 selectorOffset_ = _indexOf(template_.callData, IMetaSwap.swap.selector); + uint256 swapCallDataLengthOffset_ = selectorOffset_ - 32; + uint256 count_ = bindSwapInputs_ ? 4 : 3; + caveats_ = new Caveat[](count_); + + // Skip the outer dynamic-bytes length at [68:100], which changes with route length. + caveats_[0] = _allowedCalldataCaveat(0, _slice(template_.callData, 0, OUTER_HEADER_LENGTH)); + // Bind inner count, offsets, exact approvals, and the final MetaSwap target/value; stop before swap calldata length. + caveats_[1] = _allowedCalldataCaveat( + INNER_BATCH_START, _slice(template_.callData, INNER_BATCH_START, swapCallDataLengthOffset_ - INNER_BATCH_START) + ); + caveats_[2] = _allowedCalldataCaveat(selectorOffset_, abi.encodePacked(IMetaSwap.swap.selector)); + + if (bindSwapInputs_) { + // IMetaSwap.swap head: selector | string offset | tokenFrom | amount | bytes offset. + caveats_[3] = _allowedCalldataCaveat(selectorOffset_ + 36, _slice(template_.callData, selectorOffset_ + 36, 64)); + } + } + + function _allowedCalldataCaveat(uint256 offset_, bytes memory expected_) private view returns (Caveat memory caveat_) { + caveat_ = Caveat({ enforcer: address(allowedCalldataEnforcer), terms: abi.encodePacked(offset_, expected_), args: hex"" }); + } + + function _erc20Inner( + bool resetApproval_, + IERC20 swapToken_, + uint256 swapAmount_, + string memory aggregatorId_, + bytes memory route_ + ) + private + view + returns (Execution[] memory executions_) + { + uint256 swapIndex_ = resetApproval_ ? 2 : 1; + executions_ = new Execution[](swapIndex_ + 1); + if (resetApproval_) { + executions_[0] = + Execution({ target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(metaSwap), 0)) }); + } + executions_[swapIndex_ - 1] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(metaSwap), TOKEN_IN_AMOUNT)) + }); + executions_[swapIndex_] = Execution({ + target: address(metaSwap), + value: 0, + callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, swapToken_, swapAmount_, route_)) + }); + } + + function _nativeInner(string memory aggregatorId_, bytes memory route_) 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, (aggregatorId_, IERC20(address(0)), TOKEN_IN_AMOUNT, route_)) + }); + } + + function _route(IERC20 tokenOut_, uint256 amountOut_, bytes memory routeData_) private pure returns (bytes memory) { + return abi.encode(tokenOut_, amountOut_, routeData_); + } + + function _wrap(Execution[] memory inner_) private view returns (Execution memory execution_) { + execution_ = Execution({ + target: alice, + value: 0, + callData: abi.encodeCall(IERC7821.execute, (ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(inner_))) + }); + } + + function _sign(Caveat[] memory caveats_) private view returns (Delegation memory delegation_) { + delegation_ = Delegation({ + delegate: ANY_DELEGATE, delegator: alice, authority: ROOT_AUTHORITY, caveats: caveats_, salt: 0, signature: hex"" + }); + delegation_ = signDelegation(users.alice, delegation_); + } + + function _redeem(Delegation memory delegation_, Execution memory execution_) private { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = singleDefaultMode; + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + vm.prank(relayer); + delegationManager.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); + } + + function _redeemBatch(Delegation memory delegation_, Execution[] memory executions_) private { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = batchDefaultMode; + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = ExecutionLib.encodeBatch(executions_); + + vm.prank(relayer); + delegationManager.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); + } + + function _indexOf(bytes memory data_, bytes4 needle_) private pure returns (uint256 index_) { + for (uint256 i; i + 4 <= data_.length; ++i) { + bytes4 candidate_; + assembly { + candidate_ := mload(add(add(data_, 0x20), i)) + } + if (candidate_ == needle_) return i; + } + revert("selector-not-found"); + } + + function _slice(bytes memory data_, uint256 start_, uint256 length_) private pure returns (bytes memory result_) { + result_ = new bytes(length_); + for (uint256 i; i < length_; ++i) { + result_[i] = data_[start_ + i]; + } + } +} diff --git a/test/helpers/DelegationMetaSwapAdapter2.t.sol b/test/helpers/DelegationMetaSwapAdapter2.t.sol new file mode 100644 index 00000000..5177c6ff --- /dev/null +++ b/test/helpers/DelegationMetaSwapAdapter2.t.sol @@ -0,0 +1,906 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { MetaSwapTransferSwapEnforcer } from "../../src/enforcers/MetaSwapTransferSwapEnforcer.sol"; +import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; +import { MetaSwapAdapter as DelegationMetaSwapAdapter2 } from "../../src/helpers/MetaSwapAdapter.sol"; +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import { CaveatEnforcerBaseTest } from "../enforcers/CaveatEnforcerBaseTest.t.sol"; + +contract MetaSwapAdapter2Mock is IMetaSwap { + using SafeERC20 for IERC20; + + bool internal pullInput = true; + bool internal returnInput; + bool internal usePayoutOverride; + uint256 internal payoutOverride; + + receive() external payable { } + + function setBehavior(bool _pullInput, bool _returnInput, bool _usePayoutOverride, uint256 _payoutOverride) external { + pullInput = _pullInput; + returnInput = _returnInput; + usePayoutOverride = _usePayoutOverride; + payoutOverride = _payoutOverride; + } + + function swap(string calldata, IERC20 _tokenIn, uint256 _amountIn, bytes calldata _swapData) external payable { + (,, IERC20 tokenOut_,, uint256 quotedOutput_,,,,) = abi.decode( + abi.encodePacked(abi.encode(address(0)), _swapData), + (address, IERC20, IERC20, uint256, uint256, bytes, uint256, address, bool) + ); + + if (address(_tokenIn) == address(0)) { + require(msg.value == _amountIn, "invalid-native-input"); + if (returnInput) { + (bool refundSuccess_,) = msg.sender.call{ value: 1 }(""); + require(refundSuccess_, "native-refund-failed"); + } + } else { + require(msg.value == 0, "unexpected-value"); + if (pullInput) _tokenIn.safeTransferFrom(msg.sender, address(this), _amountIn); + if (returnInput) _tokenIn.safeTransfer(msg.sender, 1); + } + + uint256 payout_ = usePayoutOverride ? payoutOverride : quotedOutput_; + if (address(tokenOut_) == address(0)) { + (bool payoutSuccess_,) = msg.sender.call{ value: payout_ }(""); + require(payoutSuccess_, "native-output-failed"); + } else { + tokenOut_.safeTransfer(msg.sender, payout_); + } + } + + function setAdapter(string calldata, address, bytes4, bytes calldata) external { } + function removeAdapter(string calldata) external { } + + function adapters(string memory) external pure returns (Adapter memory) { + return Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); + } +} + +contract RejectNativeRecipient { + function execute( + DelegationMetaSwapAdapter2 _adapter, + IERC20 _tokenIn, + uint256 _tokenInAmount, + uint256 _minTokenOut, + DelegationMetaSwapAdapter2.ApiQuote calldata _quote + ) + external + { + _adapter.swap(_tokenIn, IERC20(address(0)), _tokenInAmount, _minTokenOut, _quote); + } + + receive() external payable { + revert(); + } +} + +contract ZeroFirstERC20 is ERC20 { + constructor() ERC20("Zero First", "ZERO") { } + + function mint(address _recipient, uint256 _amount) external { + _mint(_recipient, _amount); + } + + function seedAllowance(address _owner, address _spender, uint256 _amount) external { + _approve(_owner, _spender, _amount); + } + + function approve(address _spender, uint256 _amount) public override returns (bool) { + require(_amount == 0 || allowance(msg.sender, _spender) == 0, "zero-first"); + return super.approve(_spender, _amount); + } +} + +contract MetaSwapAdapterTest is CaveatEnforcerBaseTest { + uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; + uint256 internal constant MIN_TOKEN_OUT = 190 ether; + uint256 internal constant ACTUAL_TOKEN_OUT = 200 ether; + + BasicERC20 internal tokenIn; + BasicERC20 internal tokenOut; + MetaSwapAdapter2Mock internal metaSwap; + DelegationMetaSwapAdapter2 internal adapter; + MetaSwapTransferSwapEnforcer internal enforcer; + RedeemerEnforcer internal redeemerEnforcer; + + address internal automation; + address internal apiSigner; + uint256 internal apiSignerKey; + + function setUp() public override { + super.setUp(); + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + metaSwap = new MetaSwapAdapter2Mock(); + (apiSigner, apiSignerKey) = makeAddrAndKey("api-signer"); + adapter = new DelegationMetaSwapAdapter2(address(this), apiSigner, metaSwap); + enforcer = new MetaSwapTransferSwapEnforcer(); + redeemerEnforcer = new RedeemerEnforcer(); + automation = makeAddr("metamask-automation"); + + tokenIn.mint(address(users.alice.deleGator), TOKEN_IN_AMOUNT); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + } + + receive() external payable { } + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(enforcer)); + } + + function test_constructorRejectsZeroAddresses() public { + vm.expectRevert(); + new DelegationMetaSwapAdapter2(address(0), apiSigner, metaSwap); + + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAddress.selector); + new DelegationMetaSwapAdapter2(address(this), address(0), metaSwap); + + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAddress.selector); + new DelegationMetaSwapAdapter2(address(this), apiSigner, IMetaSwap(address(0))); + } + + function test_withdrawsErc20AndNativeTokens() public { + address recipient_ = makeAddr("withdraw-recipient"); + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + vm.deal(address(adapter), TOKEN_IN_AMOUNT); + + adapter.withdraw(tokenIn, recipient_, TOKEN_IN_AMOUNT); + adapter.withdraw(IERC20(address(0)), recipient_, TOKEN_IN_AMOUNT); + + assertEq(tokenIn.balanceOf(recipient_), TOKEN_IN_AMOUNT); + assertEq(recipient_.balance, TOKEN_IN_AMOUNT); + } + + function test_withdrawRejectsZeroRecipientAndNonOwner() public { + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAddress.selector); + adapter.withdraw(tokenIn, address(0), 1); + + vm.prank(makeAddr("not-owner")); + vm.expectRevert(); + adapter.withdraw(tokenIn, address(this), 1); + } + + function test_withdrawRevertsWhenRecipientRejectsNative() public { + RejectNativeRecipient recipient_ = new RejectNativeRecipient(); + vm.deal(address(adapter), 1); + + vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.FailedNativeTokenTransfer.selector, address(recipient_))); + adapter.withdraw(IERC20(address(0)), address(recipient_), 1); + } + + function test_adapterSwapHappyPath() public { + tokenIn.mint(address(this), TOKEN_IN_AMOUNT); + tokenIn.transfer(address(adapter), TOKEN_IN_AMOUNT); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + uint256 received_ = adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(received_, ACTUAL_TOKEN_OUT); + assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); + assertEq(tokenIn.balanceOf(address(adapter)), 0); + assertEq(tokenIn.allowance(address(adapter), address(metaSwap)), 0); + } + + function test_adapterSwapsNativeInputForErc20() public { + IERC20 nativeToken_ = IERC20(address(0)); + vm.deal(address(this), TOKEN_IN_AMOUNT); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + uint256 received_ = adapter.swap{ value: TOKEN_IN_AMOUNT }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(received_, ACTUAL_TOKEN_OUT); + assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); + assertEq(address(adapter).balance, 0); + } + + function test_adapterSwapsNativeInputWithoutConsumingExistingDust() public { + IERC20 nativeToken_ = IERC20(address(0)); + vm.deal(address(adapter), 1 ether); + vm.deal(address(this), TOKEN_IN_AMOUNT); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + adapter.swap{ value: TOKEN_IN_AMOUNT }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(address(adapter).balance, 1 ether); + } + + function test_adapterSwapsErc20InputForNative() public { + IERC20 nativeToken_ = IERC20(address(0)); + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + uint256 nativeBefore_ = address(this).balance; + + uint256 received_ = adapter.swap(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(received_, ACTUAL_TOKEN_OUT); + assertEq(address(this).balance - nativeBefore_, ACTUAL_TOKEN_OUT); + assertEq(address(adapter).balance, 0); + } + + function test_adapterRejectsIncorrectNativeValueAndUnexpectedErc20Value() public { + IERC20 nativeToken_ = IERC20(address(0)); + vm.deal(address(this), TOKEN_IN_AMOUNT); + DelegationMetaSwapAdapter2.ApiQuote memory nativeQuote_ = + _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert( + abi.encodeWithSelector(DelegationMetaSwapAdapter2.InvalidValue.selector, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT - 1) + ); + adapter.swap{ value: TOKEN_IN_AMOUNT - 1 }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, nativeQuote_); + + DelegationMetaSwapAdapter2.ApiQuote memory erc20Quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.InvalidValue.selector, 0, 1)); + adapter.swap{ value: 1 }(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, erc20Quote_); + } + + function test_adapterRevertsWhenMetaSwapRefundsNativeInput() public { + IERC20 nativeToken_ = IERC20(address(0)); + vm.deal(address(this), TOKEN_IN_AMOUNT); + metaSwap.setBehavior(true, true, false, 0); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.RemainingInputBalance.selector, 1)); + adapter.swap{ value: TOKEN_IN_AMOUNT }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterAllowsSignedQuoteReuse() public { + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT * 2); + } + + function test_adapterRevertsWhenNativeRecipientRejectsOutput() public { + IERC20 nativeToken_ = IERC20(address(0)); + RejectNativeRecipient recipient_ = new RejectNativeRecipient(); + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.FailedNativeTokenTransfer.selector, address(recipient_))); + recipient_.execute(adapter, tokenIn, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterForceApproveHandlesZeroFirstToken() public { + ZeroFirstERC20 zeroFirst_ = new ZeroFirstERC20(); + zeroFirst_.mint(address(adapter), TOKEN_IN_AMOUNT); + zeroFirst_.seedAllowance(address(adapter), address(metaSwap), 1); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(zeroFirst_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + adapter.swap(zeroFirst_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(zeroFirst_.balanceOf(address(adapter)), 0); + assertEq(zeroFirst_.allowance(address(adapter), address(metaSwap)), 0); + assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); + } + + function test_adapterRevertsExpiredQuote() public { + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + vm.warp(quote_.expiration); + + vm.expectRevert(DelegationMetaSwapAdapter2.ApiQuoteExpired.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsInvalidSignature() public { + (, uint256 wrongKey_) = makeAddrAndKey("wrong-signer"); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, wrongKey_); + + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidApiSignature.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsZeroInputAmount() public { + DelegationMetaSwapAdapter2.ApiQuote memory quote_; + + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAmount.selector); + adapter.swap(tokenIn, tokenOut, 0, MIN_TOKEN_OUT, quote_); + + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAmount.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, 0, quote_); + } + + function test_adapterRevertsUnexpectedInputBalance() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT + 1); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert( + abi.encodeWithSelector(DelegationMetaSwapAdapter2.UnexpectedInputBalance.selector, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT + 1) + ); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsInvalidApiSelector() public { + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = _signedQuote(hex"deadbeef", apiSignerKey); + + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidApiData.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsEmptyAggregatorId() public { + bytes memory swapData_ = _swapData(tokenIn, tokenOut, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false); + bytes memory apiData_ = abi.encodeWithSelector(IMetaSwap.swap.selector, "", tokenIn, TOKEN_IN_AMOUNT, swapData_); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = _signedQuote(apiData_, apiSignerKey); + + vm.expectRevert(DelegationMetaSwapAdapter2.InvalidApiData.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsOuterTokenMismatch() public { + BasicERC20 wrongToken_ = new BasicERC20(address(this), "Wrong", "WRONG", 0); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(wrongToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert(DelegationMetaSwapAdapter2.TokenInMismatch.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsOuterAmountMismatch() public { + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT - 1, TOKEN_IN_AMOUNT - 1, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert(DelegationMetaSwapAdapter2.AmountInMismatch.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsInnerTokenOutMismatch() public { + BasicERC20 wrongToken_ = new BasicERC20(address(this), "Wrong", "WRONG", 0); + bytes memory swapData_ = _swapData(tokenIn, wrongToken_, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _signedQuote(_apiData(tokenIn, TOKEN_IN_AMOUNT, swapData_), apiSignerKey); + + vm.expectRevert(DelegationMetaSwapAdapter2.TokenOutMismatch.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsInnerTokenInMismatch() public { + BasicERC20 wrongToken_ = new BasicERC20(address(this), "Wrong", "WRONG", 0); + bytes memory swapData_ = _swapData(wrongToken_, tokenOut, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _signedQuote(_apiData(tokenIn, TOKEN_IN_AMOUNT, swapData_), apiSignerKey); + + vm.expectRevert(DelegationMetaSwapAdapter2.TokenInMismatch.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsInputFeeMismatch() public { + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT - 2, ACTUAL_TOKEN_OUT, 1, false, apiSignerKey); + + vm.expectRevert(DelegationMetaSwapAdapter2.AmountInMismatch.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterAllowsFeeFromOutput() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT - 2, ACTUAL_TOKEN_OUT, 1, true, apiSignerKey); + + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); + } + + function test_adapterRevertsQuotedOutputBelowMinimum() public { + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT - 1, 0, false, apiSignerKey); + + vm.expectRevert( + abi.encodeWithSelector(DelegationMetaSwapAdapter2.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) + ); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsActualOutputBelowMinimum() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + metaSwap.setBehavior(true, false, true, MIN_TOKEN_OUT - 1); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert( + abi.encodeWithSelector(DelegationMetaSwapAdapter2.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) + ); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsRemainingAllowance() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + metaSwap.setBehavior(false, false, false, 0); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.RemainingAllowance.selector, TOKEN_IN_AMOUNT)); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRevertsRemainingInputBalance() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + metaSwap.setBehavior(true, true, false, 0); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.RemainingInputBalance.selector, 1)); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_enforcerRejectsInvalidBatchLength() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + Execution[] memory oneExecution_ = new Execution[](1); + oneExecution_[0] = executions_[0]; + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-batch-length"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(oneExecution_), keccak256("test"), address(0), address(0) + ); + } + + function test_enforcerTermsHelpers() public { + MetaSwapTransferSwapEnforcer.Terms memory expected_ = MetaSwapTransferSwapEnforcer.Terms({ + adapter: address(adapter), + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + + bytes memory encoded_ = enforcer.encodeTerms(expected_); + MetaSwapTransferSwapEnforcer.Terms memory decoded_ = enforcer.getTermsInfo(encoded_); + + assertEq(decoded_.adapter, expected_.adapter); + assertEq(decoded_.tokenIn, expected_.tokenIn); + assertEq(decoded_.tokenOut, expected_.tokenOut); + assertEq(decoded_.tokenInAmount, expected_.tokenInAmount); + assertEq(decoded_.minTokenOut, expected_.minTokenOut); + } + + function test_enforcerRejectsInvalidTerms() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + MetaSwapTransferSwapEnforcer.Terms memory termsData_ = abi.decode(terms_, (MetaSwapTransferSwapEnforcer.Terms)); + + termsData_.adapter = address(0); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-zero-address"); + _beforeHook(abi.encode(termsData_), batchDefaultMode, execution_, keccak256("zero-adapter")); + + termsData_.adapter = address(adapter); + termsData_.minTokenOut = 0; + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-zero-amount"); + _beforeHook(abi.encode(termsData_), batchDefaultMode, execution_, keccak256("zero-output")); + + termsData_.minTokenOut = MIN_TOKEN_OUT; + termsData_.tokenOut = address(tokenIn); + vm.expectRevert("MetaSwapTransferSwapEnforcer:identical-tokens"); + _beforeHook(abi.encode(termsData_), batchDefaultMode, execution_, keccak256("identical")); + } + + function test_enforcerRejectsTryExecutionMode() public { + vm.expectRevert("CaveatEnforcer:invalid-execution-type"); + _beforeHook(hex"", batchTryMode, hex"", keccak256("try-mode")); + } + + function test_enforcerAllowsNativeInputSingleCall() public { + (bytes memory terms_, bytes memory execution_) = _nativeInputOrder(TOKEN_IN_AMOUNT); + _beforeHook(terms_, singleDefaultMode, execution_, keccak256("native-single")); + } + + function test_enforcerRejectsWrongCallTypeForNativeAndErc20() public { + (bytes memory nativeTerms_, bytes memory nativeExecution_) = _nativeInputOrder(TOKEN_IN_AMOUNT); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-call-type"); + _beforeHook(nativeTerms_, batchDefaultMode, nativeExecution_, keccak256("native-batch")); + + (bytes memory erc20Terms_, bytes memory erc20Execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-call-type"); + _beforeHook(erc20Terms_, singleDefaultMode, erc20Execution_, keccak256("erc20-single")); + } + + function test_enforcerRejectsWrongNativeExecutionValue() public { + (bytes memory terms_, bytes memory execution_) = _nativeInputOrder(TOKEN_IN_AMOUNT - 1); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, singleDefaultMode, execution_, keccak256("native-value")); + } + + function test_enforcerRejectsZeroAmountTerms() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + MetaSwapTransferSwapEnforcer.Terms memory termsData_ = abi.decode(terms_, (MetaSwapTransferSwapEnforcer.Terms)); + termsData_.tokenInAmount = 0; + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-zero-amount"); + enforcer.beforeHook(abi.encode(termsData_), hex"", batchDefaultMode, execution_, keccak256("test"), address(0), address(0)); + } + + function test_enforcerRejectsInvalidTransfer() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT - 1)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_enforcerRejectsMalformedTransferExecutions() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + + executions_[0].target = address(tokenOut); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-target")); + + executions_[0].target = address(tokenIn); + executions_[0].value = 1; + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-value")); + + executions_[0].value = 0; + executions_[0].callData = hex"1234"; + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-length")); + + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(adapter), TOKEN_IN_AMOUNT)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-selector")); + + executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(this), TOKEN_IN_AMOUNT)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-recipient")); + } + + function test_enforcerRejectsInvalidSwapBounds() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT - 1, quote_)); + + vm.prank(address(delegationManager)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + enforcer.beforeHook( + terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) + ); + } + + function test_enforcerRejectsMalformedSwapExecutions() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + bytes memory validCallData_ = executions_[1].callData; + + executions_[1].target = address(metaSwap); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-target")); + + executions_[1].target = address(adapter); + executions_[1].value = 1; + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-value")); + + executions_[1].value = 0; + executions_[1].callData = hex"1234"; + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-length")); + + executions_[1].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-selector")); + + executions_[1].callData = validCallData_; + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-valid")); + } + + /// @notice Ensures the enforcer requires a complete canonical ABI head while leaving quote-body decoding to the adapter. + function test_enforcerRejectsMalformedQuoteHead() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + + executions_[1].callData = abi.encodePacked( + adapter.swap.selector, abi.encode(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, uint256(160)) + ); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("quote-truncated")); + + executions_[1].callData = abi.encodePacked( + adapter.swap.selector, + abi.encode(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, uint256(0)), + new bytes(96) + ); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("quote-offset")); + } + + function test_enforcerRejectsMismatchedSwapArguments() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + + executions_[1].callData = abi.encodeCall(adapter.swap, (tokenOut, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-token-in")); + + executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenIn, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-token-out")); + + executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT - 1, MIN_TOKEN_OUT, quote_)); + vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-amount")); + } + + function test_integrationRevertsWhenAdapterUnderpays() public { + (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + Delegation memory delegation_ = _buildDelegation(automation, terms_, 80); + metaSwap.setBehavior(true, false, true, MIN_TOKEN_OUT - 1); + + vm.expectRevert( + abi.encodeWithSelector(DelegationMetaSwapAdapter2.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) + ); + _redeem(delegation_, execution_, automation); + } + + function test_integrationHappyPath() public { + (Delegation memory delegation_, bytes memory execution_) = _delegation(automation); + + _redeem(delegation_, execution_, automation); + + assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), ACTUAL_TOKEN_OUT); + assertEq(tokenIn.balanceOf(address(adapter)), 0); + assertEq(tokenIn.allowance(address(adapter), address(metaSwap)), 0); + } + + function test_integrationNativeInputHappyPath() public { + (bytes memory terms_, bytes memory execution_) = _nativeInputOrder(TOKEN_IN_AMOUNT); + Delegation memory delegation_ = _buildDelegation(automation, terms_, 78); + vm.deal(address(users.alice.deleGator), TOKEN_IN_AMOUNT); + + _redeemWithMode(delegation_, execution_, automation, ModeLib.encodeSimpleSingle()); + + assertEq(address(users.alice.deleGator).balance, 0); + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), ACTUAL_TOKEN_OUT); + } + + function test_integrationNativeOutputHappyPath() public { + (bytes memory terms_, bytes memory execution_) = _nativeOutputOrder(); + Delegation memory delegation_ = _buildDelegation(automation, terms_, 79); + uint256 nativeBefore_ = address(users.alice.deleGator).balance; + + _redeemWithMode(delegation_, execution_, automation, ModeLib.encodeSimpleBatch()); + + assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); + assertEq(address(users.alice.deleGator).balance - nativeBefore_, ACTUAL_TOKEN_OUT); + } + + function test_integrationRejectsUnauthorizedRedeemer() public { + (Delegation memory delegation_, bytes memory execution_) = _delegation(ANY_DELEGATE); + + vm.expectRevert("RedeemerEnforcer:unauthorized-redeemer"); + _redeem(delegation_, execution_, users.bob.addr); + } + + function test_integrationRejectsReplay() public { + (Delegation memory delegation_, bytes memory execution_) = _delegation(automation); + _redeem(delegation_, execution_, automation); + + vm.expectRevert("MetaSwapTransferSwapEnforcer:delegation-already-used"); + _redeem(delegation_, execution_, automation); + } + + function _delegation(address _delegate) private view returns (Delegation memory delegation_, bytes memory execution_) { + (bytes memory terms_, bytes memory order_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); + delegation_ = _buildDelegation(_delegate, terms_, 77); + execution_ = order_; + } + + function _buildDelegation( + address _delegate, + bytes memory _terms, + uint256 _salt + ) + private + view + returns (Delegation memory delegation_) + { + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ enforcer: address(enforcer), terms: _terms, args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(redeemerEnforcer), terms: abi.encodePacked(automation), args: hex"" }); + + delegation_ = signDelegation( + users.alice, + Delegation({ + delegate: _delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: _salt, + signature: hex"" + }) + ); + } + + function _validOrder( + uint256 _minOutput, + uint256 _quotedOutput + ) + private + view + returns (bytes memory terms_, bytes memory execution_) + { + MetaSwapTransferSwapEnforcer.Terms memory termsData_ = MetaSwapTransferSwapEnforcer.Terms({ + adapter: address(adapter), + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }); + terms_ = abi.encode(termsData_); + + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, _quotedOutput, 0, false, apiSignerKey); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)) + }); + executions_[1] = Execution({ + target: address(adapter), + value: 0, + callData: abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT, _minOutput, quote_)) + }); + execution_ = ExecutionLib.encodeBatch(executions_); + } + + function _nativeInputOrder(uint256 _executionValue) private view returns (bytes memory terms_, bytes memory execution_) { + IERC20 nativeToken_ = IERC20(address(0)); + terms_ = abi.encode( + MetaSwapTransferSwapEnforcer.Terms({ + adapter: address(adapter), + tokenIn: address(0), + tokenOut: address(tokenOut), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }) + ); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + execution_ = ExecutionLib.encodeSingle( + address(adapter), + _executionValue, + abi.encodeCall(adapter.swap, (nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)) + ); + } + + function _nativeOutputOrder() private view returns (bytes memory terms_, bytes memory execution_) { + IERC20 nativeToken_ = IERC20(address(0)); + terms_ = abi.encode( + MetaSwapTransferSwapEnforcer.Terms({ + adapter: address(adapter), + tokenIn: address(tokenIn), + tokenOut: address(0), + tokenInAmount: TOKEN_IN_AMOUNT, + minTokenOut: MIN_TOKEN_OUT + }) + ); + DelegationMetaSwapAdapter2.ApiQuote memory quote_ = + _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = Execution({ + target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)) + }); + executions_[1] = Execution({ + target: address(adapter), + value: 0, + callData: abi.encodeCall(adapter.swap, (tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)) + }); + execution_ = ExecutionLib.encodeBatch(executions_); + } + + function _beforeHook(bytes memory _terms, ModeCode _mode, bytes memory _execution, bytes32 _hash) private { + vm.prank(address(delegationManager)); + enforcer.beforeHook(_terms, hex"", _mode, _execution, _hash, address(users.alice.deleGator), address(0)); + } + + function _quote( + IERC20 _outerTokenIn, + IERC20 _innerTokenOut, + uint256 _outerAmountIn, + uint256 _innerAmountIn, + uint256 _quotedOutput, + uint256 _fee, + bool _feeFromOutput, + uint256 _signerKey + ) + private + view + returns (DelegationMetaSwapAdapter2.ApiQuote memory) + { + bytes memory swapData_ = _swapData(_outerTokenIn, _innerTokenOut, _innerAmountIn, _quotedOutput, _fee, _feeFromOutput); + return _signedQuote(_apiData(_outerTokenIn, _outerAmountIn, swapData_), _signerKey); + } + + function _signedQuote( + bytes memory _apiDataValue, + uint256 _signerKey + ) + private + view + returns (DelegationMetaSwapAdapter2.ApiQuote memory quote_) + { + quote_.apiData = _apiDataValue; + quote_.expiration = block.timestamp + 5 minutes; + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_signerKey, adapter.getQuoteDigest(quote_.apiData, quote_.expiration)); + quote_.signature = abi.encodePacked(r_, s_, v_); + } + + function _apiData(IERC20 _token, uint256 _amount, bytes memory _swapDataValue) private pure returns (bytes memory) { + return abi.encodeWithSelector(IMetaSwap.swap.selector, "mock-aggregator", _token, _amount, _swapDataValue); + } + + function _swapData( + IERC20 _input, + IERC20 _output, + uint256 _amountIn, + uint256 _amountOut, + uint256 _fee, + bool _feeFromOutput + ) + private + pure + returns (bytes memory) + { + return abi.encode(_input, _output, _amountIn, _amountOut, hex"", _fee, address(0), _feeFromOutput); + } + + function _redeem(Delegation memory _delegationValue, bytes memory _execution, address _redeemer) private { + _redeemWithMode(_delegationValue, _execution, _redeemer, ModeLib.encodeSimpleBatch()); + } + + function _redeemWithMode( + Delegation memory _delegationValue, + bytes memory _execution, + address _redeemer, + ModeCode _mode + ) + private + { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _delegationValue; + bytes[] memory contexts_ = new bytes[](1); + contexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = _mode; + bytes[] memory executions_ = new bytes[](1); + executions_[0] = _execution; + + vm.prank(_redeemer); + delegationManager.redeemDelegations(contexts_, modes_, executions_); + } +} diff --git a/test/helpers/MetaSwapForwardingAdapter.t.sol b/test/helpers/MetaSwapForwardingAdapter.t.sol new file mode 100644 index 00000000..6517ffa7 --- /dev/null +++ b/test/helpers/MetaSwapForwardingAdapter.t.sol @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { MetaSwapPrefundEnforcer } from "../../src/enforcers/MetaSwapPrefundEnforcer.sol"; +import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; +import { MetaSwapForwardingAdapter } from "../../src/helpers/MetaSwapForwardingAdapter.sol"; +import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; +import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; +import { CaveatEnforcerBaseTest } from "../enforcers/CaveatEnforcerBaseTest.t.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; + +contract ForwardingMetaSwapMock is IMetaSwap { + using SafeERC20 for IERC20; + + bool internal skipInputPull; + bool internal refundInput; + bool internal useOutputOverride; + bool internal forceRevert; + uint256 internal outputOverride; + + error MockSwapFailed(); + error InvalidValue(); + error NativeTransferFailed(); + + receive() external payable { } + + function setBehavior( + bool _skipInputPull, + bool _refundInput, + bool _useOutputOverride, + uint256 _outputOverride, + bool _forceRevert + ) + external + { + skipInputPull = _skipInputPull; + refundInput = _refundInput; + useOutputOverride = _useOutputOverride; + outputOverride = _outputOverride; + forceRevert = _forceRevert; + } + + function swap(string calldata, IERC20 _tokenIn, uint256 _amountIn, bytes calldata _swapData) external payable { + if (forceRevert) revert MockSwapFailed(); + + (IERC20 tokenOut_, uint256 quotedOutput_) = abi.decode(_swapData, (IERC20, uint256)); + + if (address(_tokenIn) == address(0)) { + if (msg.value != _amountIn) revert InvalidValue(); + if (refundInput) { + (bool refundSuccess_,) = msg.sender.call{ value: 1 }(""); + if (!refundSuccess_) revert NativeTransferFailed(); + } + } else { + if (msg.value != 0) revert InvalidValue(); + if (!skipInputPull) _tokenIn.safeTransferFrom(msg.sender, address(this), _amountIn); + if (refundInput) _tokenIn.safeTransfer(msg.sender, 1); + } + + uint256 output_ = useOutputOverride ? outputOverride : quotedOutput_; + if (address(tokenOut_) == address(0)) { + (bool success_,) = msg.sender.call{ value: output_ }(""); + if (!success_) revert NativeTransferFailed(); + } else { + tokenOut_.safeTransfer(msg.sender, output_); + } + } + + function setAdapter(string calldata, address, bytes4, bytes calldata) external { } + function removeAdapter(string calldata) external { } + + function adapters(string memory) external pure returns (Adapter memory) { + return Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); + } +} + +contract MetaSwapForwardingAdapterTest is CaveatEnforcerBaseTest { + uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; + uint256 internal constant MIN_TOKEN_OUT = 190 ether; + uint256 internal constant TOKEN_OUT_AMOUNT = 200 ether; + + BasicERC20 internal tokenIn; + BasicERC20 internal tokenOut; + ForwardingMetaSwapMock internal metaSwap; + MetaSwapForwardingAdapter internal adapter; + MetaSwapPrefundEnforcer internal enforcer; + RedeemerEnforcer internal redeemerEnforcer; + + address internal automation; + address internal apiSigner; + uint256 internal apiSignerKey; + + function setUp() public override { + super.setUp(); + + tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); + tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); + metaSwap = new ForwardingMetaSwapMock(); + (apiSigner, apiSignerKey) = makeAddrAndKey("forwarding-api-signer"); + adapter = new MetaSwapForwardingAdapter(address(this), apiSigner, metaSwap); + enforcer = new MetaSwapPrefundEnforcer(adapter); + redeemerEnforcer = new RedeemerEnforcer(); + automation = makeAddr("forwarding-automation"); + + tokenIn.mint(address(users.alice.deleGator), TOKEN_IN_AMOUNT); + tokenOut.mint(address(metaSwap), 10_000 ether); + vm.deal(address(metaSwap), 10_000 ether); + vm.deal(address(users.alice.deleGator), TOKEN_IN_AMOUNT); + } + + receive() external payable { } + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(enforcer)); + } + + function test_adapterForwardsExactApiDataForErc20Input() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + vm.expectCall(address(metaSwap), quote_.apiData); + + uint256 output_ = adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(output_, TOKEN_OUT_AMOUNT); + assertEq(tokenOut.balanceOf(address(this)), TOKEN_OUT_AMOUNT); + assertEq(tokenIn.balanceOf(address(adapter)), 0); + assertEq(tokenIn.allowance(address(adapter), address(metaSwap)), 0); + } + + function test_adapterForwardsPrefundedNativeInput() public { + IERC20 nativeToken_ = IERC20(address(0)); + vm.deal(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + vm.expectCall(address(metaSwap), quote_.apiData); + + adapter.swap(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(tokenOut.balanceOf(address(this)), TOKEN_OUT_AMOUNT); + assertEq(address(adapter).balance, 0); + } + + function test_adapterForwardsNativeOutput() public { + IERC20 nativeToken_ = IERC20(address(0)); + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + uint256 balanceBefore_ = address(this).balance; + + adapter.swap(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + assertEq(address(this).balance - balanceBefore_, TOKEN_OUT_AMOUNT); + } + + function test_adapterRejectsInvalidApiSelector() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _signedQuote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, hex"deadbeef", apiSignerKey); + + vm.expectRevert(MetaSwapForwardingAdapter.InvalidApiData.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRejectsManifestTampering() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + + vm.expectRevert(MetaSwapForwardingAdapter.InvalidApiSignature.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT - 1, quote_); + } + + function test_adapterRejectsExpiredAndInvalidSignatures() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory expiredQuote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + vm.warp(expiredQuote_.expiration); + vm.expectRevert(MetaSwapForwardingAdapter.ApiQuoteExpired.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, expiredQuote_); + + (, uint256 wrongSignerKey_) = makeAddrAndKey("wrong-forwarding-signer"); + MetaSwapForwardingAdapter.ApiQuote memory invalidQuote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, wrongSignerKey_); + vm.expectRevert(MetaSwapForwardingAdapter.InvalidApiSignature.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, invalidQuote_); + } + + function test_adapterRejectsInvalidInputState() public { + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + + vm.expectRevert(abi.encodeWithSelector(MetaSwapForwardingAdapter.UnexpectedInputBalance.selector, TOKEN_IN_AMOUNT, 0)); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + vm.expectRevert(MetaSwapForwardingAdapter.IdenticalTokens.selector); + adapter.swap(tokenIn, tokenIn, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterRejectsInsufficientOutputAndResidualInput() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + + metaSwap.setBehavior(false, false, true, MIN_TOKEN_OUT - 1, false); + vm.expectRevert( + abi.encodeWithSelector(MetaSwapForwardingAdapter.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) + ); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + + metaSwap.setBehavior(false, true, false, 0, false); + vm.expectRevert(abi.encodeWithSelector(MetaSwapForwardingAdapter.RemainingInputBalance.selector, 1)); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_adapterBubblesMetaSwapRevert() public { + tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + metaSwap.setBehavior(false, false, false, 0, true); + + vm.expectRevert(ForwardingMetaSwapMock.MockSwapFailed.selector); + adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); + } + + function test_enforcerAllowsErc20AndNativePrefundBatches() public { + (bytes memory erc20Terms_, bytes memory erc20Execution_) = + _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + _beforeHook(erc20Terms_, batchDefaultMode, erc20Execution_, keccak256("erc20-prefund")); + + (bytes memory nativeTerms_, bytes memory nativeExecution_) = + _order(IERC20(address(0)), tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + _beforeHook(nativeTerms_, batchDefaultMode, nativeExecution_, keccak256("native-prefund")); + } + + function test_enforcerRejectsWrongCallTypeAndBatchLength() public { + (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-call-type"); + _beforeHook(terms_, singleDefaultMode, execution_, keccak256("single")); + + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + Execution[] memory shortBatch_ = new Execution[](1); + shortBatch_[0] = executions_[0]; + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-batch-length"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(shortBatch_), keccak256("short")); + } + + function test_enforcerRejectsMalformedErc20Prefund() public { + (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + + executions_[0].target = address(tokenOut); + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("prefund-target")); + + executions_[0].target = address(tokenIn); + executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT - 1)); + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("prefund-amount")); + + executions_[0].callData = abi.encodeCall(IERC20.approve, (address(adapter), TOKEN_IN_AMOUNT)); + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("prefund-selector")); + } + + function test_enforcerRejectsMalformedNativePrefund() public { + (bytes memory terms_, bytes memory execution_) = + _order(IERC20(address(0)), tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + + executions_[0].value = TOKEN_IN_AMOUNT - 1; + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("native-value")); + + executions_[0].value = TOKEN_IN_AMOUNT; + executions_[0].callData = hex"00"; + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("native-data")); + } + + function test_enforcerRejectsMalformedSwapCall() public { + (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + bytes memory validSwapCall_ = executions_[1].callData; + + executions_[1].target = address(metaSwap); + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-target")); + + executions_[1].target = address(adapter); + executions_[1].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)); + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-selector")); + + executions_[1].callData = validSwapCall_; + executions_[1].value = 1; + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-value")); + } + + function test_enforcerRejectsSwapInputMismatch() public { + (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + Execution[] memory executions_ = abi.decode(execution_, (Execution[])); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + + executions_[1].callData = abi.encodeCall(adapter.swap, (tokenOut, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)); + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-token")); + + executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT - 1, MIN_TOKEN_OUT, quote_)); + vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); + _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-amount")); + } + + function test_enforcerRejectsReplay() public { + (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + bytes32 delegationHash_ = keccak256("prefund-replay"); + + _beforeHook(terms_, batchDefaultMode, execution_, delegationHash_); + vm.expectRevert("MetaSwapPrefundEnforcer:delegation-already-used"); + _beforeHook(terms_, batchDefaultMode, execution_, delegationHash_); + } + + function test_integrationErc20PrefundAndForward() public { + (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) = _delegation(tokenIn); + vm.expectCall(address(metaSwap), apiData_); + + _redeem(delegation_, execution_, automation); + + assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), TOKEN_OUT_AMOUNT); + } + + function test_integrationNativePrefundAndForward() public { + (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) = _delegation(IERC20(address(0))); + vm.expectCall(address(metaSwap), apiData_); + + _redeem(delegation_, execution_, automation); + + assertEq(address(adapter).balance, 0); + assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), TOKEN_OUT_AMOUNT); + } + + function test_integrationRejectsUnauthorizedRedeemer() public { + (Delegation memory delegation_, bytes memory execution_,) = _delegationFor(ANY_DELEGATE, tokenIn); + + vm.expectRevert("RedeemerEnforcer:unauthorized-redeemer"); + _redeem(delegation_, execution_, users.bob.addr); + } + + function _delegation(IERC20 _tokenIn) + private + view + returns (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) + { + return _delegationFor(automation, _tokenIn); + } + + function _delegationFor( + address _delegate, + IERC20 _tokenIn + ) + private + view + returns (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) + { + (bytes memory terms_, bytes memory order_) = _order(_tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(_tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); + + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ enforcer: address(enforcer), terms: terms_, args: hex"" }); + caveats_[1] = Caveat({ enforcer: address(redeemerEnforcer), terms: abi.encodePacked(automation), args: hex"" }); + + delegation_ = signDelegation( + users.alice, + Delegation({ + delegate: _delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 1, + signature: hex"" + }) + ); + execution_ = order_; + apiData_ = quote_.apiData; + } + + function _order( + IERC20 _tokenIn, + IERC20 _tokenOut, + uint256 _tokenInAmount, + uint256 _minTokenOut, + uint256 _tokenOutAmount + ) + private + view + returns (bytes memory terms_, bytes memory execution_) + { + MetaSwapForwardingAdapter.ApiQuote memory quote_ = + _quote(_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, _tokenOutAmount, apiSignerKey); + + terms_ = abi.encode(MetaSwapPrefundEnforcer.Terms({ tokenIn: address(_tokenIn), tokenInAmount: _tokenInAmount })); + + Execution[] memory executions_ = new Execution[](2); + if (address(_tokenIn) == address(0)) { + executions_[0] = Execution({ target: address(adapter), value: _tokenInAmount, callData: hex"" }); + } else { + executions_[0] = Execution({ + target: address(_tokenIn), value: 0, callData: abi.encodeCall(IERC20.transfer, (address(adapter), _tokenInAmount)) + }); + } + executions_[1] = Execution({ + target: address(adapter), + value: 0, + callData: abi.encodeCall(adapter.swap, (_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, quote_)) + }); + execution_ = ExecutionLib.encodeBatch(executions_); + } + + function _quote( + IERC20 _tokenIn, + IERC20 _tokenOut, + uint256 _tokenInAmount, + uint256 _minTokenOut, + uint256 _tokenOutAmount, + uint256 _signerKey + ) + private + view + returns (MetaSwapForwardingAdapter.ApiQuote memory quote_) + { + bytes memory apiData_ = abi.encodeCall( + IMetaSwap.swap, ("forwarding-aggregator", _tokenIn, _tokenInAmount, abi.encode(_tokenOut, _tokenOutAmount)) + ); + quote_ = _signedQuote(_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, apiData_, _signerKey); + } + + function _signedQuote( + IERC20 _tokenIn, + IERC20 _tokenOut, + uint256 _tokenInAmount, + uint256 _minTokenOut, + bytes memory _apiData, + uint256 _signerKey + ) + private + view + returns (MetaSwapForwardingAdapter.ApiQuote memory quote_) + { + quote_.apiData = _apiData; + quote_.expiration = block.timestamp + 5 minutes; + bytes32 digest_ = + adapter.getQuoteDigest(_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, quote_.apiData, quote_.expiration); + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_signerKey, digest_); + quote_.signature = abi.encodePacked(r_, s_, v_); + } + + function _beforeHook(bytes memory _terms, ModeCode _mode, bytes memory _execution, bytes32 _hash) private { + vm.prank(address(delegationManager)); + enforcer.beforeHook(_terms, hex"", _mode, _execution, _hash, address(users.alice.deleGator), address(0)); + } + + function _redeem(Delegation memory _delegationValue, bytes memory _execution, address _redeemer) private { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _delegationValue; + bytes[] memory contexts_ = new bytes[](1); + contexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = ModeLib.encodeSimpleBatch(); + bytes[] memory executions_ = new bytes[](1); + executions_[0] = _execution; + + vm.prank(_redeemer); + delegationManager.redeemDelegations(contexts_, modes_, executions_); + } +} diff --git a/test/utils/MockLimitOrderRouter.sol b/test/utils/MockLimitOrderRouter.sol new file mode 100644 index 00000000..e0e4dd70 --- /dev/null +++ b/test/utils/MockLimitOrderRouter.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @notice Adjustable-output swap stand-in used to prove post-execution limit-order checks. + */ +contract MockLimitOrderRouter { + using SafeERC20 for IERC20; + + uint256 public erc20AmountOut; + uint256 public nativeAmountOut; + + error NativeTransferFailed(); + error InvalidNativeValue(); + error UnexpectedNativeValue(); + + receive() external payable { } + + function setERC20AmountOut(uint256 amountOut_) external { + erc20AmountOut = amountOut_; + } + + function setNativeAmountOut(uint256 amountOut_) external { + nativeAmountOut = amountOut_; + } + + function swapNativeForERC20(IERC20 tokenOut_, address recipient_) external payable { + tokenOut_.safeTransfer(recipient_, erc20AmountOut); + } + + function swapERC20ForNative(IERC20 tokenIn_, uint256 amountIn_, address payable recipient_) external { + tokenIn_.safeTransferFrom(msg.sender, address(this), amountIn_); + (bool success_,) = recipient_.call{ value: nativeAmountOut }(""); + if (!success_) revert NativeTransferFailed(); + } + + /// @notice IMetaSwap-compatible entry point with intentionally flexible aggregator and route data. + function swap(string calldata, IERC20 tokenFrom_, uint256 amount_, bytes calldata route_) external payable { + (IERC20 tokenOut_, uint256 amountOut_) = abi.decode(route_, (IERC20, uint256)); + + if (address(tokenFrom_) == address(0)) { + if (msg.value != amount_) revert InvalidNativeValue(); + } else { + if (msg.value != 0) revert UnexpectedNativeValue(); + tokenFrom_.safeTransferFrom(msg.sender, address(this), amount_); + } + + if (address(tokenOut_) == address(0)) { + (bool success_,) = msg.sender.call{ value: amountOut_ }(""); + if (!success_) revert NativeTransferFailed(); + } else { + tokenOut_.safeTransfer(msg.sender, amountOut_); + } + } +} From 50169ec773efb68e1336e2e8a1d43bd68ab97505 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Thu, 10 Sep 2026 23:12:53 +0200 Subject: [PATCH 06/13] fix: align intent manager deployment tooling --- .env.example | 2 - ...eployMetaSwapIntentDelegationManager.s.sol | 9 +- .../verification/VerificationInstructions.md | 2 +- ...rify-metaswap-intent-delegation-manager.sh | 11 +- ...aSwapExecutionBuilderDelegationManager.sol | 92 --------------- src/MetaSwapFlexibleSettlementManagerBase.sol | 75 ------------ src/MetaSwapHooklessDelegationManager.sol | 107 ------------------ 7 files changed, 3 insertions(+), 295 deletions(-) delete mode 100644 src/MetaSwapExecutionBuilderDelegationManager.sol delete mode 100644 src/MetaSwapFlexibleSettlementManagerBase.sol delete mode 100644 src/MetaSwapHooklessDelegationManager.sol diff --git a/.env.example b/.env.example index 3be83d2f..a391fdea 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,6 @@ PRIVATE_KEY= SALT=GATOR DELEGATION_MANAGER_ADDRESS= META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS= -# 0 = DirectECDSA, 1 = ERC1271 (used by intent manager deploy/verify) -SIGNATURE_MODE=0 ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032 MULTISIG_DELEGATOR_IMPLEMENTATION_ADDRESS= META_SWAP_ADAPTER_OWNER_ADDRESS= diff --git a/script/DeployMetaSwapIntentDelegationManager.s.sol b/script/DeployMetaSwapIntentDelegationManager.s.sol index 1adbdc84..196555ed 100644 --- a/script/DeployMetaSwapIntentDelegationManager.s.sol +++ b/script/DeployMetaSwapIntentDelegationManager.s.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.23; import "forge-std/Script.sol"; import { console2 } from "forge-std/console2.sol"; -import { MetaSwapDelegationManagerBase } from "../src/MetaSwapDelegationManagerBase.sol"; import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegationManager.sol"; /** @@ -16,29 +15,23 @@ import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegation * * Env: * - SALT - * - SIGNATURE_MODE: 0 = DirectECDSA, 1 = ERC1271 */ contract DeployMetaSwapIntentDelegationManager is Script { bytes32 salt; - MetaSwapDelegationManagerBase.SignatureMode signatureMode; function setUp() public { salt = bytes32(abi.encodePacked(vm.envString("SALT"))); - uint256 mode_ = vm.envOr("SIGNATURE_MODE", uint256(0)); - require(mode_ <= 1, "SIGNATURE_MODE must be 0 or 1"); - signatureMode = MetaSwapDelegationManagerBase.SignatureMode(uint8(mode_)); console2.log("~~~"); console2.log("Salt:"); console2.logBytes32(salt); - console2.log("SignatureMode: %s", mode_ == 0 ? "DirectECDSA" : "ERC1271"); } function run() public { console2.log("~~~"); vm.startBroadcast(); - address deployedAddress = address(new MetaSwapIntentDelegationManager{ salt: salt }(signatureMode)); + address deployedAddress = address(new MetaSwapIntentDelegationManager{ salt: salt }()); console2.log("MetaSwapIntentDelegationManager: %s", deployedAddress); vm.stopBroadcast(); diff --git a/script/verification/VerificationInstructions.md b/script/verification/VerificationInstructions.md index 73c7fa64..665bfe01 100644 --- a/script/verification/VerificationInstructions.md +++ b/script/verification/VerificationInstructions.md @@ -65,7 +65,7 @@ Verifies an array of enforcer contracts. #### `verify-metaswap-intent-delegation-manager.sh` Experimental. Verifies `MetaSwapIntentDelegationManager`. -Requires `META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS` and `SIGNATURE_MODE` (`0` DirectECDSA, `1` ERC1271). +Requires `META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS`. **Usage:** diff --git a/script/verification/verify-metaswap-intent-delegation-manager.sh b/script/verification/verify-metaswap-intent-delegation-manager.sh index b2cf4c10..d7f246b9 100755 --- a/script/verification/verify-metaswap-intent-delegation-manager.sh +++ b/script/verification/verify-metaswap-intent-delegation-manager.sh @@ -7,7 +7,6 @@ # Experimental. Verifies MetaSwapIntentDelegationManager across configured chains. # Requires in .env: # META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS -# SIGNATURE_MODE # 0 = DirectECDSA, 1 = ERC1271 set -e @@ -17,12 +16,6 @@ set +o allexport source ./verify-utils.sh -encode_args() { - local signature="$1" - shift - cast abi-encode "$signature" "$@" -} - declare -a CONTRACTS add_contract() { @@ -34,13 +27,11 @@ add_contract() { CONTRACTS+=("$name:$path:$address:$constructor_args:$lib_string") } -MODE="${SIGNATURE_MODE:-0}" - add_contract \ "MetaSwapIntentDelegationManager" \ "src/MetaSwapIntentDelegationManager.sol" \ "${META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS}" \ - "$(encode_args "constructor(uint8)" "$MODE")" \ + "" \ "" for contract in "${CONTRACTS[@]}"; do diff --git a/src/MetaSwapExecutionBuilderDelegationManager.sol b/src/MetaSwapExecutionBuilderDelegationManager.sol deleted file mode 100644 index a3145f86..00000000 --- a/src/MetaSwapExecutionBuilderDelegationManager.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; - -import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; -import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; -import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; -import { Execution } from "./utils/Types.sol"; - -/** - * @title MetaSwapExecutionBuilderDelegationManager - * @notice Constructs and executes one signed MetaSwap settlement from redeemer-supplied route data. - * @dev Approval and swap targets, amounts, ordering, selectors, and values are created by this manager. - */ -contract MetaSwapExecutionBuilderDelegationManager is MetaSwapFlexibleSettlementManagerBase { - using ExecutionLib for Execution[]; - - string public constant NAME = "MetaSwapExecutionBuilderDelegationManager"; - - constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } - - function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { - (string memory aggregatorId_, bytes memory routeData_) = abi.decode(executionContext_, (string, bytes)); - Execution[] memory executions_ = _buildExecutions(termsInfo_, aggregatorId_, routeData_); - - IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executions_.encodeBatch()); - } - - function _buildExecutions( - Terms memory termsInfo_, - string memory aggregatorId_, - bytes memory routeData_ - ) - private - pure - returns (Execution[] memory executions_) - { - ApprovalMode approvalMode_ = termsInfo_.approvalMode; - - if (termsInfo_.tokenIn == address(0)) { - if (approvalMode_ != ApprovalMode.None) revert InvalidApprovalMode(); - - executions_ = new Execution[](1); - executions_[0] = _swapExecution(termsInfo_, termsInfo_.tokenInAmount, aggregatorId_, routeData_); - return executions_; - } - - uint256 swapIndex_; - if (approvalMode_ == ApprovalMode.SkipApproval) { - executions_ = new Execution[](1); - } else if (approvalMode_ == ApprovalMode.Approve) { - executions_ = new Execution[](2); - executions_[0] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); - swapIndex_ = 1; - } else if (approvalMode_ == ApprovalMode.ResetApprove) { - executions_ = new Execution[](3); - executions_[0] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, 0); - executions_[1] = _approvalExecution(termsInfo_.tokenIn, termsInfo_.metaSwap, termsInfo_.tokenInAmount); - swapIndex_ = 2; - } else { - revert InvalidApprovalMode(); - } - - executions_[swapIndex_] = _swapExecution(termsInfo_, 0, aggregatorId_, routeData_); - } - - function _approvalExecution(address tokenIn_, address metaSwap_, uint256 amount_) private pure returns (Execution memory) { - return Execution({ target: tokenIn_, value: 0, callData: abi.encodeCall(IERC20.approve, (metaSwap_, amount_)) }); - } - - function _swapExecution( - Terms memory termsInfo_, - uint256 value_, - string memory aggregatorId_, - bytes memory routeData_ - ) - private - pure - returns (Execution memory) - { - return Execution({ - target: termsInfo_.metaSwap, - value: value_, - callData: abi.encodeCall( - IMetaSwap.swap, (aggregatorId_, IERC20(termsInfo_.tokenIn), termsInfo_.tokenInAmount, routeData_) - ) - }); - } -} diff --git a/src/MetaSwapFlexibleSettlementManagerBase.sol b/src/MetaSwapFlexibleSettlementManagerBase.sol deleted file mode 100644 index 4e236d99..00000000 --- a/src/MetaSwapFlexibleSettlementManagerBase.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { MetaSwapDelegationManagerBase } from "./MetaSwapDelegationManagerBase.sol"; - -/** - * @title MetaSwapFlexibleSettlementManagerBase - * @notice Shared flexible MetaSwap settlement decoding and min-output enforcement. - * @dev Used by the hookless and execution-builder prototype managers. - */ -abstract contract MetaSwapFlexibleSettlementManagerBase is MetaSwapDelegationManagerBase { - enum ApprovalMode { - None, - SkipApproval, - Approve, - ResetApprove - } - - struct Terms { - address metaSwap; - address tokenIn; - uint256 tokenInAmount; - ApprovalMode approvalMode; - address tokenOut; - address recipient; - uint256 tokenOutMin; - } - - uint256 internal constant TERMS_LENGTH = 145; - - constructor(string memory name_, SignatureMode signatureMode_) MetaSwapDelegationManagerBase(name_, signatureMode_) { } - - /** - * @notice Decodes and validates packed settlement terms. - * @param terms_ Packed settlement terms. - */ - function getTermsInfo(bytes memory terms_) public pure returns (Terms memory termsInfo_) { - if (terms_.length != TERMS_LENGTH) revert InvalidTerms(); - - // Terms are tightly packed. Loading their fixed offsets directly avoids allocating seven temporary byte arrays. - assembly ("memory-safe") { - let termsData_ := add(terms_, 0x20) - 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))) - } - uint8 approvalMode_ = uint8(terms_[72]); - - 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_); - } - - function _executeIntent(address delegator_, bytes memory terms_, bytes calldata executionContext_) internal override { - Terms memory termsInfo_ = getTermsInfo(terms_); - uint256 balanceBefore_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); - - _executeSettlement(delegator_, executionContext_, termsInfo_); - - uint256 balanceAfter_ = _balanceOf(termsInfo_.tokenOut, termsInfo_.recipient); - if (balanceAfter_ < balanceBefore_ || balanceAfter_ - balanceBefore_ < termsInfo_.tokenOutMin) { - revert InsufficientOutput(); - } - } - - function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal virtual; -} diff --git a/src/MetaSwapHooklessDelegationManager.sol b/src/MetaSwapHooklessDelegationManager.sol deleted file mode 100644 index d04008b7..00000000 --- a/src/MetaSwapHooklessDelegationManager.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; - -import { MetaSwapFlexibleSettlementManagerBase } from "./MetaSwapFlexibleSettlementManagerBase.sol"; -import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; -import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; -import { Execution } from "./utils/Types.sol"; - -/** - * @title MetaSwapHooklessDelegationManager - * @notice Executes one signed MetaSwap settlement without invoking external caveat hooks. - * @dev The redeemer supplies a complete batch, which is validated directly by this manager. - */ -contract MetaSwapHooklessDelegationManager is MetaSwapFlexibleSettlementManagerBase { - using ExecutionLib for bytes; - - string public constant NAME = "MetaSwapHooklessDelegationManager"; - - uint256 private constant APPROVE_CALL_LENGTH = 68; - uint256 private constant SWAP_CALL_MIN_LENGTH = 196; - - error ApprovalShapeNotAllowed(); - error InvalidApproval(); - error InvalidBatchLength(); - error InvalidSwap(); - - constructor(SignatureMode signatureMode_) MetaSwapFlexibleSettlementManagerBase(NAME, signatureMode_) { } - - function _executeSettlement(address delegator_, bytes calldata executionContext_, Terms memory termsInfo_) internal override { - Execution[] calldata executions_ = executionContext_.decodeBatch(); - _validateExecutions(executions_, termsInfo_); - - IDeleGatorCore(delegator_).executeFromExecutor(ModeLib.encodeSimpleBatch(), executionContext_); - } - - function _validateExecutions(Execution[] calldata executions_, Terms 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(); - } - } -} From 8e74755b285c20c2f87bb6eb7069dcfca10d724a Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 11 Sep 2026 03:48:42 +0200 Subject: [PATCH 07/13] refactor: rename MetaSwap intent manager to order manager --- .env.example | 2 +- .../MetaSwapSpecializedDelegationManagers.md | 4 +- ...eployMetaSwapOrderDelegationManager.s.sol} | 14 +- .../verification/VerificationInstructions.md | 8 +- ...rify-metaswap-order-delegation-manager.sh} | 14 +- src/MetaSwapDelegationManagerBase.sol | 2 +- ...sol => MetaSwapOrderDelegationManager.sol} | 6 +- ...l => MetaSwapOrderDelegationManager.t.sol} | 170 +++++++++--------- 8 files changed, 110 insertions(+), 110 deletions(-) rename script/{DeployMetaSwapIntentDelegationManager.s.sol => DeployMetaSwapOrderDelegationManager.s.sol} (51%) rename script/verification/{verify-metaswap-intent-delegation-manager.sh => verify-metaswap-order-delegation-manager.sh} (72%) rename src/{MetaSwapIntentDelegationManager.sol => MetaSwapOrderDelegationManager.sol} (97%) rename test/{MetaSwapIntentDelegationManager.t.sol => MetaSwapOrderDelegationManager.t.sol} (81%) diff --git a/.env.example b/.env.example index a391fdea..f304e7fa 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ PRIVATE_KEY= SALT=GATOR DELEGATION_MANAGER_ADDRESS= -META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS= +META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS= ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032 MULTISIG_DELEGATOR_IMPLEMENTATION_ADDRESS= META_SWAP_ADAPTER_OWNER_ADDRESS= diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md index e05bca71..1a312c29 100644 --- a/documents/MetaSwapSpecializedDelegationManagers.md +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -6,7 +6,7 @@ itself because settlement enforcement is internal and no caveat hooks are called `executeFromExecutor` remains on the EIP-7702 account. Managers only call it. -## MetaSwapIntentDelegationManager +## MetaSwapOrderDelegationManager One manager for both product intents. Terms start with a one-byte `Intent`. @@ -56,7 +56,7 @@ Signatures try ECDSA first (EOA and EIP-7702 ETH keys). If that misses, empty ac ## Gas comparison (`approve(amount) + swap`, EIP-7702) -Measured around `redeemDelegations` in `test/MetaSwapIntentDelegationManager.t.sol` and the specialized suite: +Measured around `redeemDelegations` in `test/MetaSwapOrderDelegationManager.t.sol` and the specialized suite: | Path | Gas | vs generic flexible | | ----------------------------------------- | --------- | ----------------------- | diff --git a/script/DeployMetaSwapIntentDelegationManager.s.sol b/script/DeployMetaSwapOrderDelegationManager.s.sol similarity index 51% rename from script/DeployMetaSwapIntentDelegationManager.s.sol rename to script/DeployMetaSwapOrderDelegationManager.s.sol index 196555ed..ec3e9576 100644 --- a/script/DeployMetaSwapIntentDelegationManager.s.sol +++ b/script/DeployMetaSwapOrderDelegationManager.s.sol @@ -4,19 +4,19 @@ pragma solidity 0.8.23; import "forge-std/Script.sol"; import { console2 } from "forge-std/console2.sol"; -import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegationManager.sol"; +import { MetaSwapOrderDelegationManager } from "../src/MetaSwapOrderDelegationManager.sol"; /** - * @title DeployMetaSwapIntentDelegationManager - * @notice Deploys the experimental MetaSwap intent delegation manager. + * @title DeployMetaSwapOrderDelegationManager + * @notice Deploys the experimental MetaSwap order delegation manager. * @dev Experimental. EIP-7702 accounts must be wired to this manager address. * - * forge script script/DeployMetaSwapIntentDelegationManager.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast + * forge script script/DeployMetaSwapOrderDelegationManager.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast * * Env: * - SALT */ -contract DeployMetaSwapIntentDelegationManager is Script { +contract DeployMetaSwapOrderDelegationManager is Script { bytes32 salt; function setUp() public { @@ -31,8 +31,8 @@ contract DeployMetaSwapIntentDelegationManager is Script { console2.log("~~~"); vm.startBroadcast(); - address deployedAddress = address(new MetaSwapIntentDelegationManager{ salt: salt }()); - console2.log("MetaSwapIntentDelegationManager: %s", deployedAddress); + 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 665bfe01..074b0042 100644 --- a/script/verification/VerificationInstructions.md +++ b/script/verification/VerificationInstructions.md @@ -62,15 +62,15 @@ Verifies an array of enforcer contracts. ./verify-enforcer-contracts.sh ``` -#### `verify-metaswap-intent-delegation-manager.sh` +#### `verify-metaswap-order-delegation-manager.sh` -Experimental. Verifies `MetaSwapIntentDelegationManager`. -Requires `META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS`. +Experimental. Verifies `MetaSwapOrderDelegationManager`. +Requires `META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS`. **Usage:** ```bash -./verify-metaswap-intent-delegation-manager.sh +./verify-metaswap-order-delegation-manager.sh ``` ## Notes diff --git a/script/verification/verify-metaswap-intent-delegation-manager.sh b/script/verification/verify-metaswap-order-delegation-manager.sh similarity index 72% rename from script/verification/verify-metaswap-intent-delegation-manager.sh rename to script/verification/verify-metaswap-order-delegation-manager.sh index d7f246b9..5e6b2112 100755 --- a/script/verification/verify-metaswap-intent-delegation-manager.sh +++ b/script/verification/verify-metaswap-order-delegation-manager.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# verify-metaswap-intent-delegation-manager.sh +# verify-metaswap-order-delegation-manager.sh # # Usage: -# ./verify-metaswap-intent-delegation-manager.sh +# ./verify-metaswap-order-delegation-manager.sh # -# Experimental. Verifies MetaSwapIntentDelegationManager across configured chains. +# Experimental. Verifies MetaSwapOrderDelegationManager across configured chains. # Requires in .env: -# META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS +# META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS set -e @@ -28,9 +28,9 @@ add_contract() { } add_contract \ - "MetaSwapIntentDelegationManager" \ - "src/MetaSwapIntentDelegationManager.sol" \ - "${META_SWAP_INTENT_DELEGATION_MANAGER_ADDRESS}" \ + "MetaSwapOrderDelegationManager" \ + "src/MetaSwapOrderDelegationManager.sol" \ + "${META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS}" \ "" \ "" diff --git a/src/MetaSwapDelegationManagerBase.sol b/src/MetaSwapDelegationManagerBase.sol index 5eb49ec3..c527015a 100644 --- a/src/MetaSwapDelegationManagerBase.sol +++ b/src/MetaSwapDelegationManagerBase.sol @@ -33,7 +33,7 @@ abstract contract MetaSwapDelegationManagerBase is EIP712 { event DisabledDelegation( bytes32 indexed delegationHash, address indexed delegator, address indexed delegate, Delegation delegation ); - /// @dev `intent` is the first terms byte (`Intent` on MetaSwapIntentDelegationManager). + /// @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(); diff --git a/src/MetaSwapIntentDelegationManager.sol b/src/MetaSwapOrderDelegationManager.sol similarity index 97% rename from src/MetaSwapIntentDelegationManager.sol rename to src/MetaSwapOrderDelegationManager.sol index 238b1416..c2ae0629 100644 --- a/src/MetaSwapIntentDelegationManager.sol +++ b/src/MetaSwapOrderDelegationManager.sol @@ -10,7 +10,7 @@ import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; import { Execution } from "./utils/Types.sol"; /** - * @title MetaSwapIntentDelegationManager + * @title MetaSwapOrderDelegationManager * @notice One purpose-specific manager for exact gasless swaps and flexible MetaSwap limit orders. * @dev No external caveat hooks. Both intents redeem through a direct batch/default `executeFromExecutor`. * @@ -18,7 +18,7 @@ import { Execution } from "./utils/Types.sol"; * Flexible terms: `intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1) | * tokenOut(20) | recipient(20) | tokenOutMin(32)`. */ -contract MetaSwapIntentDelegationManager is MetaSwapDelegationManagerBase { +contract MetaSwapOrderDelegationManager is MetaSwapDelegationManagerBase { using ExecutionLib for bytes; enum Intent { @@ -43,7 +43,7 @@ contract MetaSwapIntentDelegationManager is MetaSwapDelegationManagerBase { uint256 tokenOutMin; } - string public constant NAME = "MetaSwapIntentDelegationManager"; + string public constant NAME = "MetaSwapOrderDelegationManager"; uint256 private constant EXACT_TERMS_LENGTH = 33; uint256 private constant FLEXIBLE_TERMS_LENGTH = 146; diff --git a/test/MetaSwapIntentDelegationManager.t.sol b/test/MetaSwapOrderDelegationManager.t.sol similarity index 81% rename from test/MetaSwapIntentDelegationManager.t.sol rename to test/MetaSwapOrderDelegationManager.t.sol index f47ebbff..390bc3e5 100644 --- a/test/MetaSwapIntentDelegationManager.t.sol +++ b/test/MetaSwapOrderDelegationManager.t.sol @@ -10,7 +10,7 @@ import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { MetaSwapDelegationManagerBase } from "../src/MetaSwapDelegationManagerBase.sol"; -import { MetaSwapIntentDelegationManager } from "../src/MetaSwapIntentDelegationManager.sol"; +import { MetaSwapOrderDelegationManager } from "../src/MetaSwapOrderDelegationManager.sol"; import { MetaSwapFlexibleSettlementManagerBase } from "../src/experiments/MetaSwapFlexibleSettlementManagerBase.sol"; import { MetaSwapHooklessDelegationManager } from "../src/experiments/MetaSwapHooklessDelegationManager.sol"; import { DelegationManager } from "../src/DelegationManager.sol"; @@ -24,7 +24,7 @@ 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 IntentManager1271Account { +contract OrderManager1271Account { using ExecutionLib for bytes; function isValidSignature(bytes32 hash_, bytes memory signature_) external pure returns (bytes4) { @@ -47,7 +47,7 @@ contract IntentManager1271Account { } } -contract IntentManagerMetaSwapMock is IMetaSwap { +contract OrderManagerMetaSwapMock is IMetaSwap { using SafeERC20 for IERC20; mapping(string aggregatorId => Adapter adapter) private adapters_; @@ -83,17 +83,17 @@ contract IntentManagerMetaSwapMock is IMetaSwap { receive() external payable { } } -contract MetaSwapIntentDelegationManagerTest is Test { +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 HOOKLESS_KEY = 0x2222; - uint256 private constant INTENT_KEY = 0x3333; + uint256 private constant ORDER_KEY = 0x3333; EntryPoint private entryPoint; - IntentManagerMetaSwapMock private metaSwap; + OrderManagerMetaSwapMock private metaSwap; BasicERC20 private tokenIn; BasicERC20 private tokenOut; @@ -102,16 +102,16 @@ contract MetaSwapIntentDelegationManagerTest is Test { LimitedCallsEnforcer private limitedCallsEnforcer; MetaSwapFlexibleSettlementEnforcer private flexibleEnforcer; MetaSwapHooklessDelegationManager private hooklessManager; - MetaSwapIntentDelegationManager private intentManager; + MetaSwapOrderDelegationManager private orderManager; address private genericAccount; address private hooklessAccount; - address private intentAccount; + address private orderAccount; address private relayer; function setUp() public { entryPoint = new EntryPoint(); - metaSwap = new IntentManagerMetaSwapMock(); + metaSwap = new OrderManagerMetaSwapMock(); tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); relayer = makeAddr("Relayer"); @@ -121,23 +121,23 @@ contract MetaSwapIntentDelegationManagerTest is Test { limitedCallsEnforcer = new LimitedCallsEnforcer(); flexibleEnforcer = new MetaSwapFlexibleSettlementEnforcer(); hooklessManager = new MetaSwapHooklessDelegationManager(); - intentManager = new MetaSwapIntentDelegationManager(); + orderManager = new MetaSwapOrderDelegationManager(); genericAccount = vm.addr(GENERIC_KEY); hooklessAccount = vm.addr(HOOKLESS_KEY); - intentAccount = vm.addr(INTENT_KEY); + orderAccount = vm.addr(ORDER_KEY); _installDeleGator(genericAccount, address(genericManager)); _installDeleGator(hooklessAccount, address(hooklessManager)); - _installDeleGator(intentAccount, address(intentManager)); + _installDeleGator(orderAccount, address(orderManager)); tokenIn.mint(genericAccount, 1_000 ether); tokenIn.mint(hooklessAccount, 1_000 ether); - tokenIn.mint(intentAccount, 1_000 ether); + tokenIn.mint(orderAccount, 1_000 ether); tokenOut.mint(address(metaSwap), 10_000 ether); vm.deal(genericAccount, 1_000 ether); vm.deal(hooklessAccount, 1_000 ether); - vm.deal(intentAccount, 1_000 ether); + vm.deal(orderAccount, 1_000 ether); vm.deal(address(metaSwap), 10_000 ether); } @@ -148,22 +148,22 @@ contract MetaSwapIntentDelegationManagerTest is Test { bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 1); - vm.expectEmit(true, true, true, true, address(intentManager)); + vm.expectEmit(true, true, true, true, address(orderManager)); emit MetaSwapDelegationManagerBase.RedeemedDelegation( - intentAccount, + orderAccount, relayer, - intentManager.getDelegationHash(delegation_), - uint8(MetaSwapIntentDelegationManager.Intent.ExactCalldata) + orderManager.getDelegationHash(delegation_), + uint8(MetaSwapOrderDelegationManager.Intent.ExactCalldata) ); _redeemIntent(delegation_, encoded_); - assertEq(tokenIn.balanceOf(intentAccount), 900 ether); - assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); - assertTrue(intentManager.disabledDelegations(intentManager.getDelegationHash(delegation_))); + 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(intentAccount); + vm.prank(orderAccount); tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); Execution[] memory executions_ = _erc20Executions(0, TOKEN_OUT_AMOUNT); @@ -171,11 +171,11 @@ contract MetaSwapIntentDelegationManagerTest is Test { Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 2); _redeemIntent(delegation_, encoded_); - assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); } function test_exactRedeemsResetApproveAndSwap() public { - vm.prank(intentAccount); + vm.prank(orderAccount); tokenIn.approve(address(metaSwap), 1); Execution[] memory executions_ = _erc20Executions(2, TOKEN_OUT_AMOUNT); @@ -183,19 +183,19 @@ contract MetaSwapIntentDelegationManagerTest is Test { Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 3); _redeemIntent(delegation_, encoded_); - assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + 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_ = intentAccount.balance; + uint256 nativeBefore_ = orderAccount.balance; _redeemIntent(delegation_, encoded_); - assertEq(intentAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); - assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + assertEq(orderAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); } function test_exactRevertsForHashMismatch() public { @@ -204,7 +204,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 5); executions_[1].value = 1; - vm.expectRevert(MetaSwapIntentDelegationManager.InvalidExecutionHash.selector); + vm.expectRevert(MetaSwapOrderDelegationManager.InvalidExecutionHash.selector); _redeemIntent(delegation_, ExecutionLib.encodeBatch(executions_)); } @@ -224,8 +224,8 @@ contract MetaSwapIntentDelegationManagerTest is Test { bytes memory encoded_ = ExecutionLib.encodeBatch(executions_); Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 7); - vm.prank(intentAccount); - intentManager.disableDelegation(delegation_); + vm.prank(orderAccount); + orderManager.disableDelegation(delegation_); vm.expectRevert(MetaSwapDelegationManagerBase.CannotUseADisabledDelegation.selector); _redeemIntent(delegation_, encoded_); @@ -244,31 +244,31 @@ contract MetaSwapIntentDelegationManagerTest is Test { 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(intentManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); - Delegation memory delegation_ = _signManager(intentManager, HOOKLESS_KEY, eoa_, caveats_, 40); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); + Delegation memory delegation_ = _signManager(orderManager, HOOKLESS_KEY, eoa_, caveats_, 40); vm.expectRevert(MetaSwapDelegationManagerBase.InvalidEOASignature.selector); _redeemIntent(delegation_, encoded_); } function test_exactRedeemsWithERC1271Fallback() public { - IntentManager1271Account account_ = new IntentManager1271Account(); + 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(intentManager), terms: terms_, args: hex"" }); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: terms_, args: hex"" }); Delegation memory delegation_ = Delegation({ delegate: address(0xa11), delegator: address(account_), - authority: intentManager.ROOT_AUTHORITY(), + authority: orderManager.ROOT_AUTHORITY(), caveats: caveats_, salt: 41, signature: hex"" }); bytes32 typedDataHash_ = - MessageHashUtils.toTypedDataHash(intentManager.getDomainHash(), intentManager.getDelegationHash(delegation_)); + MessageHashUtils.toTypedDataHash(orderManager.getDomainHash(), orderManager.getDelegationHash(delegation_)); delegation_.signature = abi.encodePacked(typedDataHash_); _redeemIntent(delegation_, encoded_); @@ -277,14 +277,14 @@ contract MetaSwapIntentDelegationManagerTest is Test { } function test_exactRejectsInvalidERC1271Signature() public { - IntentManager1271Account account_ = new IntentManager1271Account(); + 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(intentManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); + caveats_[0] = Caveat({ enforcer: address(orderManager), terms: _exactTerms(keccak256(encoded_)), args: hex"" }); Delegation memory delegation_ = Delegation({ delegate: address(0xa11), delegator: address(account_), - authority: intentManager.ROOT_AUTHORITY(), + authority: orderManager.ROOT_AUTHORITY(), caveats: caveats_, salt: 42, signature: abi.encodePacked(bytes32(uint256(1))) @@ -297,58 +297,58 @@ contract MetaSwapIntentDelegationManagerTest is Test { // -------- Flexible intent -------- function test_flexibleRedeemsApproveAndSwap() public { - bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + 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(intentManager)); + vm.expectEmit(true, true, true, true, address(orderManager)); emit MetaSwapDelegationManagerBase.RedeemedDelegation( - intentAccount, + orderAccount, relayer, - intentManager.getDelegationHash(delegation_), - uint8(MetaSwapIntentDelegationManager.Intent.FlexibleSettlement) + orderManager.getDelegationHash(delegation_), + uint8(MetaSwapOrderDelegationManager.Intent.FlexibleSettlement) ); _redeemIntent(delegation_, encoded_); - assertEq(tokenIn.balanceOf(intentAccount), 900 ether); - assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + assertEq(tokenIn.balanceOf(orderAccount), 900 ether); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); } function test_flexibleRedeemsSkipApproval() public { - vm.prank(intentAccount); + vm.prank(orderAccount); tokenIn.approve(address(metaSwap), TOKEN_IN_AMOUNT); - bytes memory terms_ = _flexibleTerms(address(tokenIn), _skipApprovalMode(), address(tokenOut), intentAccount); + 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(intentAccount), TOKEN_OUT_AMOUNT); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); } function test_flexibleRedeemsResetApprove() public { - vm.prank(intentAccount); + vm.prank(orderAccount); tokenIn.approve(address(metaSwap), 1); - bytes memory terms_ = _flexibleTerms(address(tokenIn), _resetApproveMode(), address(tokenOut), intentAccount); + 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(intentAccount), TOKEN_OUT_AMOUNT); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); } function test_flexibleRedeemsNativeInput() public { - bytes memory terms_ = _flexibleTerms(address(0), _noneMode(), address(tokenOut), intentAccount); + bytes memory terms_ = _flexibleTerms(address(0), _noneMode(), address(tokenOut), orderAccount); Delegation memory delegation_ = _signIntent(terms_, 13); - uint256 nativeBefore_ = intentAccount.balance; + uint256 nativeBefore_ = orderAccount.balance; _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); - assertEq(intentAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); - assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT); + assertEq(orderAccount.balance, nativeBefore_ - TOKEN_IN_AMOUNT); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT); } function test_flexibleAllowsDifferentRouteData() public { - bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); Execution[] memory first_ = _erc20Executions(1, TOKEN_OUT_AMOUNT); first_[1].callData = abi.encodeCall( @@ -362,26 +362,26 @@ contract MetaSwapIntentDelegationManagerTest is Test { ); _redeemIntent(_signIntent(terms_, 15), ExecutionLib.encodeBatch(second_)); - assertEq(tokenOut.balanceOf(intentAccount), TOKEN_OUT_AMOUNT * 2); + assertEq(tokenOut.balanceOf(orderAccount), TOKEN_OUT_AMOUNT * 2); } function test_flexibleRevertsAtomicallyForInsufficientOutput() public { - bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), orderAccount); Delegation memory delegation_ = _signIntent(terms_, 16); - bytes32 hash_ = intentManager.getDelegationHash(delegation_); + bytes32 hash_ = orderManager.getDelegationHash(delegation_); vm.expectRevert(MetaSwapDelegationManagerBase.InsufficientOutput.selector); _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_MIN - 1))); - assertFalse(intentManager.disabledDelegations(hash_)); - assertEq(tokenIn.balanceOf(intentAccount), 1_000 ether); + assertFalse(orderManager.disabledDelegations(hash_)); + assertEq(tokenIn.balanceOf(orderAccount), 1_000 ether); } function test_flexibleRejectsInvalidApprovalMode() public { - bytes memory terms_ = _flexibleTerms(address(0), _approveMode(), address(tokenOut), intentAccount); + bytes memory terms_ = _flexibleTerms(address(0), _approveMode(), address(tokenOut), orderAccount); Delegation memory delegation_ = _signIntent(terms_, 17); - vm.expectRevert(MetaSwapIntentDelegationManager.InvalidApprovalMode.selector); + vm.expectRevert(MetaSwapOrderDelegationManager.InvalidApprovalMode.selector); _redeemIntent(delegation_, ExecutionLib.encodeBatch(_nativeExecutions(TOKEN_OUT_AMOUNT))); } @@ -389,7 +389,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { bytes memory terms_ = abi.encodePacked(uint8(2), bytes32(0)); Delegation memory delegation_ = _signIntent(terms_, 18); - vm.expectRevert(MetaSwapIntentDelegationManager.InvalidIntent.selector); + vm.expectRevert(MetaSwapOrderDelegationManager.InvalidIntent.selector); _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); } @@ -470,12 +470,12 @@ contract MetaSwapIntentDelegationManagerTest is Test { uint256 gasBefore_ = gasleft(); vm.prank(relayer); - intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); emit log_named_uint("intent ExactCalldata", gasBefore_ - gasleft()); } function test_gas_intentFlexible() public { - bytes memory terms_ = _flexibleTerms(address(tokenIn), _approveMode(), address(tokenOut), intentAccount); + 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)); @@ -484,7 +484,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { uint256 gasBefore_ = gasleft(); vm.prank(relayer); - intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); emit log_named_uint("intent FlexibleSettlement", gasBefore_ - gasleft()); } @@ -496,12 +496,12 @@ contract MetaSwapIntentDelegationManagerTest is Test { } function _exactTerms(bytes32 executionHash_) private pure returns (bytes memory) { - return abi.encodePacked(uint8(MetaSwapIntentDelegationManager.Intent.ExactCalldata), executionHash_); + return abi.encodePacked(uint8(MetaSwapOrderDelegationManager.Intent.ExactCalldata), executionHash_); } function _flexibleTerms( address tokenIn_, - MetaSwapIntentDelegationManager.ApprovalMode approvalMode_, + MetaSwapOrderDelegationManager.ApprovalMode approvalMode_, address tokenOut_, address recipient_ ) @@ -510,7 +510,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { returns (bytes memory) { return abi.encodePacked( - uint8(MetaSwapIntentDelegationManager.Intent.FlexibleSettlement), + uint8(MetaSwapOrderDelegationManager.Intent.FlexibleSettlement), address(metaSwap), tokenIn_, TOKEN_IN_AMOUNT, @@ -522,7 +522,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { } function _signIntent(bytes memory terms_, uint256 salt_) private view returns (Delegation memory) { - return _signIntentWithKey(INTENT_KEY, terms_, salt_); + return _signIntentWithKey(ORDER_KEY, terms_, salt_); } function _signIntentWithKey( @@ -535,8 +535,8 @@ contract MetaSwapIntentDelegationManagerTest is Test { returns (Delegation memory delegation_) { Caveat[] memory caveats_ = new Caveat[](1); - caveats_[0] = Caveat({ enforcer: address(intentManager), terms: terms_, args: hex"" }); - return _signManager(intentManager, signerKey_, intentAccount, caveats_, salt_); + 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) { @@ -605,7 +605,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { (bytes[] memory permissionContexts_, ModeCode[] memory modes_, bytes[] memory executionContexts_) = _redemptionInputs(delegation_, executionContext_); vm.prank(relayer); - intentManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); } function _redemptionInputs( @@ -627,7 +627,7 @@ contract MetaSwapIntentDelegationManagerTest is Test { } function _erc20Executions(uint8 approvalCount_, uint256 outputAmount_) private view returns (Execution[] memory) { - return _erc20ExecutionsFor(intentAccount, approvalCount_, outputAmount_); + return _erc20ExecutionsFor(orderAccount, approvalCount_, outputAmount_); } function _erc20ExecutionsFor( @@ -672,19 +672,19 @@ contract MetaSwapIntentDelegationManagerTest is Test { }); } - function _noneMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { - return MetaSwapIntentDelegationManager.ApprovalMode.None; + function _noneMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.None; } - function _skipApprovalMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { - return MetaSwapIntentDelegationManager.ApprovalMode.SkipApproval; + function _skipApprovalMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.SkipApproval; } - function _approveMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { - return MetaSwapIntentDelegationManager.ApprovalMode.Approve; + function _approveMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.Approve; } - function _resetApproveMode() private pure returns (MetaSwapIntentDelegationManager.ApprovalMode) { - return MetaSwapIntentDelegationManager.ApprovalMode.ResetApprove; + function _resetApproveMode() private pure returns (MetaSwapOrderDelegationManager.ApprovalMode) { + return MetaSwapOrderDelegationManager.ApprovalMode.ResetApprove; } } From 6619b05494da6443c3297f1c1c453449429c8f9c Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 11 Sep 2026 04:04:50 +0200 Subject: [PATCH 08/13] docs: refresh MetaSwap order manager gas measurements --- documents/MetaSwapSpecializedDelegationManagers.md | 14 +++++++------- test/MetaSwapOrderDelegationManager.t.sol | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md index 1a312c29..1921e855 100644 --- a/documents/MetaSwapSpecializedDelegationManagers.md +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -58,18 +58,18 @@ Signatures try ECDSA first (EOA and EIP-7702 ETH keys). If that misses, empty ac Measured around `redeemDelegations` in `test/MetaSwapOrderDelegationManager.t.sol` and the specialized suite: -| Path | Gas | vs generic flexible | +| Path | Gas | vs generic | | ----------------------------------------- | --------- | ----------------------- | -| Generic DM + ExactBatch + LimitedCalls(1) | `230,987` | — | +| Generic DM + ExactBatch + LimitedCalls(1) | `230,987` | baseline exact | | Generic DM + FlexibleSettlementEnforcer | `200,783` | baseline flexible | -| Hookless flexible | `166,508` | −17.1% | -| Intent ExactCalldata | `158,997` | −31.2% vs exact generic | -| Intent FlexibleSettlement | `166,725` | −17.0% | +| Hookless flexible | `158,770` | −20.9% vs flexible | +| Order ExactCalldata | `152,242` | −34.1% vs exact generic | +| Order FlexibleSettlement | `158,990` | −20.8% vs flexible | Takeaways: -- Flattened exact intent is the cheapest path: no second enforcer, no LimitedCalls nested mapping, no self-`execute` wrap. -- Intent flexible matches hookless (~same gas); the unified manager does not pay a meaningful premium for dispatch. +- Flattened exact order is the cheapest path: no second enforcer, no LimitedCalls nested mapping, no self-`execute` wrap. +- Order flexible matches hookless (~same gas); the unified manager does not pay a meaningful premium for dispatch. ## Limitations diff --git a/test/MetaSwapOrderDelegationManager.t.sol b/test/MetaSwapOrderDelegationManager.t.sol index 390bc3e5..95af5954 100644 --- a/test/MetaSwapOrderDelegationManager.t.sol +++ b/test/MetaSwapOrderDelegationManager.t.sol @@ -471,7 +471,7 @@ contract MetaSwapOrderDelegationManagerTest is Test { uint256 gasBefore_ = gasleft(); vm.prank(relayer); orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); - emit log_named_uint("intent ExactCalldata", gasBefore_ - gasleft()); + emit log_named_uint("order ExactCalldata", gasBefore_ - gasleft()); } function test_gas_intentFlexible() public { @@ -485,7 +485,7 @@ contract MetaSwapOrderDelegationManagerTest is Test { uint256 gasBefore_ = gasleft(); vm.prank(relayer); orderManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); - emit log_named_uint("intent FlexibleSettlement", gasBefore_ - gasleft()); + emit log_named_uint("order FlexibleSettlement", gasBefore_ - gasleft()); } // -------- Helpers -------- From 04ab6ace2bf4ae212c6030c45a85e73d34072981 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 11 Sep 2026 12:48:35 +0200 Subject: [PATCH 09/13] test: benchmark MetaSwap redemption shapes --- .../MetaSwapSpecializedDelegationManagers.md | 34 +++-- test/MetaSwapOrderDelegationManager.t.sol | 138 +++++++++++++++++- 2 files changed, 157 insertions(+), 15 deletions(-) diff --git a/documents/MetaSwapSpecializedDelegationManagers.md b/documents/MetaSwapSpecializedDelegationManagers.md index 1921e855..ed4b3bfd 100644 --- a/documents/MetaSwapSpecializedDelegationManagers.md +++ b/documents/MetaSwapSpecializedDelegationManagers.md @@ -54,22 +54,30 @@ Signatures try ECDSA first (EOA and EIP-7702 ETH keys). If that misses, empty ac `disabledDelegations` is both cancel and one-shot consumption. Failed execution or insufficient output reverts atomically. -## Gas comparison (`approve(amount) + swap`, EIP-7702) - -Measured around `redeemDelegations` in `test/MetaSwapOrderDelegationManager.t.sol` and the specialized suite: - -| Path | Gas | vs generic | -| ----------------------------------------- | --------- | ----------------------- | -| Generic DM + ExactBatch + LimitedCalls(1) | `230,987` | baseline exact | -| Generic DM + FlexibleSettlementEnforcer | `200,783` | baseline flexible | -| Hookless flexible | `158,770` | −20.9% vs flexible | -| Order ExactCalldata | `152,242` | −34.1% vs exact generic | -| Order FlexibleSettlement | `158,990` | −20.8% vs flexible | +## Redemption gas comparison (EIP-7702) + +Measured with `gasleft()` immediately around `redeemDelegations` in +`test/MetaSwapOrderDelegationManager.t.sol`. Each pair executes the same swap shape from a one-element root delegation, +using an ECDSA signature and a zero starting output-token balance. Values include the manager call but exclude top-level +transaction intrinsic calldata gas. + +| Purpose | Swap shape | Existing `DelegationManager` path | Existing gas | Order gas | Saving | +| ------------------------ | ----------------- | --------------------------------------------- | ------------ | --------- | ------ | +| Gasless exact swap | Native swap | `ExactExecutionBatch` + `LimitedCalls(1)` | `167,475` | `96,618` | 42.3% | +| Gasless exact swap | `approve + swap` | `ExactExecutionBatch` + `LimitedCalls(1)` | `230,987` | `152,242` | 34.1% | +| Gasless exact swap | `reset + approve + swap` | `ExactExecutionBatch` + `LimitedCalls(1)` | `244,355` | `157,710` | 35.5% | +| Flexible limit order | Native swap | `MetaSwapFlexibleSettlementEnforcer` | `143,414` | `101,730` | 29.1% | +| Flexible limit order | `approve + swap` | `MetaSwapFlexibleSettlementEnforcer` | `200,783` | `158,990` | 20.8% | +| Flexible limit order | `reset + approve + swap` | `MetaSwapFlexibleSettlementEnforcer` | `207,974` | `166,072` | 20.1% | Takeaways: -- Flattened exact order is the cheapest path: no second enforcer, no LimitedCalls nested mapping, no self-`execute` wrap. -- Order flexible matches hookless (~same gas); the unified manager does not pay a meaningful premium for dispatch. +- Exact orders save 34–42% by replacing generic delegation loops, hook calls, full execution terms, and the + `LimitedCallsEnforcer` state/event with a specialized one-shot path. +- Flexible orders save 20–29% while retaining the existing enforcer's approval-shape, swap-calldata, and output-delta + validations. +- The specialized manager's compact redemption event is part of the measured saving; the generic manager emits the full + delegation. ## Limitations diff --git a/test/MetaSwapOrderDelegationManager.t.sol b/test/MetaSwapOrderDelegationManager.t.sol index 95af5954..218b5e4e 100644 --- a/test/MetaSwapOrderDelegationManager.t.sol +++ b/test/MetaSwapOrderDelegationManager.t.sol @@ -413,6 +413,38 @@ contract MetaSwapOrderDelegationManagerTest is Test { 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_genericFlexibleSettlementEnforcer() public { bytes memory terms_ = abi.encodePacked( address(metaSwap), @@ -437,6 +469,54 @@ contract MetaSwapOrderDelegationManagerTest is Test { emit log_named_uint("generic FlexibleSettlementEnforcer", gasBefore_ - gasleft()); } + function test_gas_genericFlexibleSettlementEnforcerResetApproval() public { + bytes memory terms_ = abi.encodePacked( + address(metaSwap), + address(tokenIn), + TOKEN_IN_AMOUNT, + uint8(MetaSwapFlexibleSettlementEnforcer.ApprovalMode.ResetApprove), + address(tokenOut), + genericAccount, + TOKEN_OUT_MIN + ); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(flexibleEnforcer), terms: terms_, args: hex"" }); + Delegation memory delegation_ = _signGeneric(caveats_, 107); + bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20ExecutionsFor(genericAccount, 2, TOKEN_OUT_AMOUNT)); + + (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 FlexibleSettlementEnforcer, reset approval", gasBefore_ - gasleft()); + } + + function test_gas_genericFlexibleSettlementEnforcerNative() public { + bytes memory terms_ = abi.encodePacked( + address(metaSwap), + address(0), + TOKEN_IN_AMOUNT, + uint8(MetaSwapFlexibleSettlementEnforcer.ApprovalMode.None), + address(tokenOut), + genericAccount, + TOKEN_OUT_MIN + ); + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ enforcer: address(flexibleEnforcer), terms: terms_, args: hex"" }); + Delegation memory delegation_ = _signGeneric(caveats_, 108); + 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); + genericManager.redeemDelegations(permissionContexts_, modes_, executionContexts_); + emit log_named_uint("generic FlexibleSettlementEnforcer, native", gasBefore_ - gasleft()); + } + function test_gas_hooklessFlexible() public { bytes memory terms_ = abi.encodePacked( address(metaSwap), @@ -461,7 +541,7 @@ contract MetaSwapOrderDelegationManagerTest is Test { emit log_named_uint("hookless flexible", gasBefore_ - gasleft()); } - function test_gas_intentExact() public { + function test_gas_orderExact() public { bytes memory encoded_ = ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT)); Delegation memory delegation_ = _signIntent(_exactTerms(keccak256(encoded_)), 103); @@ -474,7 +554,33 @@ contract MetaSwapOrderDelegationManagerTest is Test { emit log_named_uint("order ExactCalldata", gasBefore_ - gasleft()); } - function test_gas_intentFlexible() public { + 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 ExactCalldata, 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 ExactCalldata, 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)); @@ -488,6 +594,34 @@ contract MetaSwapOrderDelegationManagerTest is Test { 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 { From a0f25153106e2bd3687929301e2ee3b47ec2f0ae Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 11 Sep 2026 13:03:54 +0200 Subject: [PATCH 10/13] test: cover native output order settlement --- test/MetaSwapOrderDelegationManager.t.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/MetaSwapOrderDelegationManager.t.sol b/test/MetaSwapOrderDelegationManager.t.sol index 218b5e4e..46076cde 100644 --- a/test/MetaSwapOrderDelegationManager.t.sol +++ b/test/MetaSwapOrderDelegationManager.t.sol @@ -347,6 +347,27 @@ contract MetaSwapOrderDelegationManagerTest is Test { 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); From a1ea2b891d50c62d25458b47101cf6190902e73f Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 11 Sep 2026 14:11:02 +0200 Subject: [PATCH 11/13] test: port flexible settlement authorization boundary checks --- test/MetaSwapOrderDelegationManager.t.sol | 308 +++++++++++++++++++++- 1 file changed, 302 insertions(+), 6 deletions(-) diff --git a/test/MetaSwapOrderDelegationManager.t.sol b/test/MetaSwapOrderDelegationManager.t.sol index 46076cde..2b54adac 100644 --- a/test/MetaSwapOrderDelegationManager.t.sol +++ b/test/MetaSwapOrderDelegationManager.t.sol @@ -353,12 +353,7 @@ contract MetaSwapOrderDelegationManagerTest is Test { 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) - ) + ("redeemer-route", IERC20(address(tokenIn)), TOKEN_IN_AMOUNT, abi.encode(IERC20(address(0)), TOKEN_OUT_AMOUNT)) ); uint256 nativeBefore_ = orderAccount.balance; @@ -414,6 +409,217 @@ contract MetaSwapOrderDelegationManagerTest is Test { _redeemIntent(delegation_, ExecutionLib.encodeBatch(_erc20Executions(1, TOKEN_OUT_AMOUNT))); } + // -------- Flexible validation (ported from MetaSwapFlexibleSettlementEnforcer) -------- + + function test_flexibleRejectsInvalidTermsLength() public { + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo(new bytes(145)); + + vm.expectRevert(MetaSwapDelegationManagerBase.InvalidTerms.selector); + orderManager.getFlexibleTermsInfo(new bytes(147)); + } + + 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_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 { @@ -676,6 +882,96 @@ contract MetaSwapOrderDelegationManagerTest is Test { ); } + 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_ + ); + } + + 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_); } From e27bb53f1f419eb6f81ec715230aa2f3fc42102f Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 11 Sep 2026 16:16:44 +0200 Subject: [PATCH 12/13] fix: drop incomplete MetaSwap adapters from compile path --- src/enforcers/MetaSwapPrefundEnforcer.sol | 144 --- .../MetaSwapTransferSwapEnforcer.sol | 147 --- .../GaslessSwapDelegationManager.sol | 0 .../MetaSwapMinimalDelegationManager.sol | 0 test/GaslessSwapDelegationManager.t.sol | 477 --------- test/MetaSwapMinimalDelegationManager.t.sol | 231 ----- test/helpers/DelegationMetaSwapAdapter2.t.sol | 906 ------------------ test/helpers/MetaSwapForwardingAdapter.t.sol | 486 ---------- 8 files changed, 2391 deletions(-) delete mode 100644 src/enforcers/MetaSwapPrefundEnforcer.sol delete mode 100644 src/enforcers/MetaSwapTransferSwapEnforcer.sol rename src/{ => experiments}/GaslessSwapDelegationManager.sol (100%) rename src/{ => experiments}/MetaSwapMinimalDelegationManager.sol (100%) delete mode 100644 test/GaslessSwapDelegationManager.t.sol delete mode 100644 test/MetaSwapMinimalDelegationManager.t.sol delete mode 100644 test/helpers/DelegationMetaSwapAdapter2.t.sol delete mode 100644 test/helpers/MetaSwapForwardingAdapter.t.sol diff --git a/src/enforcers/MetaSwapPrefundEnforcer.sol b/src/enforcers/MetaSwapPrefundEnforcer.sol deleted file mode 100644 index 02ca9c85..00000000 --- a/src/enforcers/MetaSwapPrefundEnforcer.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; - -import { CaveatEnforcer } from "./CaveatEnforcer.sol"; -import { MetaSwapForwardingAdapter } from "../helpers/MetaSwapForwardingAdapter.sol"; -import { CallType, Execution, ModeCode } from "../utils/Types.sol"; -import { CALLTYPE_BATCH } from "../utils/Constants.sol"; - -/** - * @title MetaSwapPrefundEnforcer - * @notice Enforces a one-shot input transfer followed by a call to an immutable MetaSwap forwarding adapter. - * @dev ERC20 input is transferred with `transfer(adapter, amount)`. Native input is transferred with an empty - * call to the adapter carrying the exact amount. The second execution must call `swap` on the adapter and - * declare the same input token and amount. The adapter validates the signed route and settlement. - */ -contract MetaSwapPrefundEnforcer is CaveatEnforcer { - using ExecutionLib for bytes; - using ModeLib for ModeCode; - - uint256 private constant API_QUOTE_OFFSET = 5 * 32; - uint256 private constant MIN_SWAP_CALLDATA_LENGTH = 4 + API_QUOTE_OFFSET + 3 * 32; - - struct Terms { - address tokenIn; - uint256 tokenInAmount; - } - - MetaSwapForwardingAdapter public immutable adapter; - - mapping(address manager => mapping(bytes32 delegationHash => bool used)) public usedDelegations; - - event DelegationExecuted(address indexed delegationManager, bytes32 indexed delegationHash, address indexed delegator); - - /** - * @notice Binds this enforcer to one forwarding adapter. - */ - constructor(MetaSwapForwardingAdapter _adapter) { - require(address(_adapter) != address(0), "MetaSwapPrefundEnforcer:invalid-zero-address"); - adapter = _adapter; - } - - /** - * @notice Validates an atomic prefund-and-swap batch. - */ - function beforeHook( - bytes calldata _terms, - bytes calldata, - ModeCode _mode, - bytes calldata _executionCallData, - bytes32 _delegationHash, - address _delegator, - address - ) - public - override - onlyDefaultExecutionMode(_mode) - { - require(!usedDelegations[msg.sender][_delegationHash], "MetaSwapPrefundEnforcer:delegation-already-used"); - require( - CallType.unwrap(_mode.getCallType()) == CallType.unwrap(CALLTYPE_BATCH), - "MetaSwapPrefundEnforcer:invalid-call-type" - ); - - Terms memory terms_ = abi.decode(_terms, (Terms)); - require(terms_.tokenInAmount != 0, "MetaSwapPrefundEnforcer:invalid-zero-amount"); - - Execution[] calldata executions_ = _executionCallData.decodeBatch(); - require(executions_.length == 2, "MetaSwapPrefundEnforcer:invalid-batch-length"); - - _validatePrefund(executions_[0], terms_); - _validateSwap(executions_[1], terms_); - - usedDelegations[msg.sender][_delegationHash] = true; - emit DelegationExecuted(msg.sender, _delegationHash, _delegator); - } - - function _validatePrefund(Execution calldata _execution, Terms memory _terms) private view { - address adapter_ = address(adapter); - if (_terms.tokenIn == address(0)) { - if (_execution.target != adapter_ || _execution.value != _terms.tokenInAmount || _execution.callData.length != 0) { - revert("MetaSwapPrefundEnforcer:invalid-prefund-call"); - } - return; - } - - if (_execution.target != _terms.tokenIn || _execution.value != 0 || _execution.callData.length != 68) { - revert("MetaSwapPrefundEnforcer:invalid-prefund-call"); - } - - bytes calldata callData_ = _execution.callData; - require(bytes4(callData_[:4]) == IERC20.transfer.selector, "MetaSwapPrefundEnforcer:invalid-prefund-call"); - - address recipient_; - uint256 amount_; - assembly ("memory-safe") { - recipient_ := and(calldataload(add(callData_.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) - amount_ := calldataload(add(callData_.offset, 36)) - } - require( - recipient_ == adapter_ && amount_ == _terms.tokenInAmount, "MetaSwapPrefundEnforcer:invalid-prefund-call" - ); - } - - function _validateSwap(Execution calldata _execution, Terms memory _terms) private view { - bytes calldata callData_ = _execution.callData; - if ( - _execution.target != address(adapter) || _execution.value != 0 || callData_.length < MIN_SWAP_CALLDATA_LENGTH - || bytes4(callData_[:4]) != MetaSwapForwardingAdapter.swap.selector - ) { - revert("MetaSwapPrefundEnforcer:invalid-swap-call"); - } - - address tokenIn_; - uint256 tokenInAmount_; - uint256 quoteOffset_; - assembly ("memory-safe") { - tokenIn_ := and(calldataload(add(callData_.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) - tokenInAmount_ := calldataload(add(callData_.offset, 68)) - quoteOffset_ := calldataload(add(callData_.offset, 132)) - } - - if (tokenIn_ != _terms.tokenIn || tokenInAmount_ != _terms.tokenInAmount || quoteOffset_ != API_QUOTE_OFFSET) { - revert("MetaSwapPrefundEnforcer:invalid-swap-call"); - } - } - - /** - * @notice Decodes prefund terms. - */ - function getTermsInfo(bytes calldata _terms) external pure returns (Terms memory terms_) { - terms_ = abi.decode(_terms, (Terms)); - } - - /** - * @notice Encodes prefund terms. - */ - function encodeTerms(Terms calldata _terms) external pure returns (bytes memory) { - return abi.encode(_terms); - } -} diff --git a/src/enforcers/MetaSwapTransferSwapEnforcer.sol b/src/enforcers/MetaSwapTransferSwapEnforcer.sol deleted file mode 100644 index fe1d2b73..00000000 --- a/src/enforcers/MetaSwapTransferSwapEnforcer.sol +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; - -import { CaveatEnforcer } from "./CaveatEnforcer.sol"; -import { MetaSwapAdapter } from "../helpers/MetaSwapAdapter.sol"; -import { CallType, Execution, ModeCode } from "../utils/Types.sol"; -import { CALLTYPE_BATCH, CALLTYPE_SINGLE } from "../utils/Constants.sol"; - -/** - * @title MetaSwapTransferSwapEnforcer - * @notice Enforces a one-shot MetaSwap adapter execution. - * @dev MetaSwap API calldata remains flexible and is authenticated and decoded by the adapter. - * ERC-20 input requires an exact transfer-and-swap batch. Native input requires a single swap call whose - * execution value equals `tokenInAmount`. Address zero represents the native token. - */ -contract MetaSwapTransferSwapEnforcer is CaveatEnforcer { - using ExecutionLib for bytes; - using ModeLib for ModeCode; - - uint256 private constant API_QUOTE_OFFSET = 5 * 32; - uint256 private constant MIN_SWAP_CALLDATA_LENGTH = 4 + API_QUOTE_OFFSET + 3 * 32; - - struct Terms { - address adapter; - address tokenIn; - address tokenOut; - uint256 tokenInAmount; - uint256 minTokenOut; - } - - mapping(address manager => mapping(bytes32 delegationHash => bool used)) public usedDelegations; - - event DelegationExecuted(address indexed delegationManager, bytes32 indexed delegationHash, address indexed delegator); - - function beforeHook( - bytes calldata _terms, - bytes calldata, - ModeCode _mode, - bytes calldata _executionCallData, - bytes32 _delegationHash, - address _delegator, - address - ) - public - override - onlyDefaultExecutionMode(_mode) - { - require(!usedDelegations[msg.sender][_delegationHash], "MetaSwapTransferSwapEnforcer:delegation-already-used"); - - Terms memory terms_ = abi.decode(_terms, (Terms)); - require(terms_.adapter != address(0), "MetaSwapTransferSwapEnforcer:invalid-zero-address"); - require( - terms_.tokenInAmount != 0 && terms_.minTokenOut != 0, "MetaSwapTransferSwapEnforcer:invalid-zero-amount" - ); - require(terms_.tokenIn != terms_.tokenOut, "MetaSwapTransferSwapEnforcer:identical-tokens"); - - if (terms_.tokenIn == address(0)) { - require( - CallType.unwrap(_mode.getCallType()) == CallType.unwrap(CALLTYPE_SINGLE), - "MetaSwapTransferSwapEnforcer:invalid-call-type" - ); - (address target_, uint256 value_, bytes calldata callData_) = _executionCallData.decodeSingle(); - _validateSwap(target_, value_, callData_, terms_); - } else { - require( - CallType.unwrap(_mode.getCallType()) == CallType.unwrap(CALLTYPE_BATCH), - "MetaSwapTransferSwapEnforcer:invalid-call-type" - ); - Execution[] calldata executions_ = _executionCallData.decodeBatch(); - require(executions_.length == 2, "MetaSwapTransferSwapEnforcer:invalid-batch-length"); - - _validateTransfer(executions_[0], terms_); - _validateSwap(executions_[1].target, executions_[1].value, executions_[1].callData, terms_); - } - - usedDelegations[msg.sender][_delegationHash] = true; - - emit DelegationExecuted(msg.sender, _delegationHash, _delegator); - } - - function _validateTransfer(Execution calldata _execution, Terms memory _terms) private pure { - if (_execution.target != _terms.tokenIn || _execution.value != 0 || _execution.callData.length != 68) { - revert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); - } - require( - bytes4(_execution.callData[:4]) == IERC20.transfer.selector, - "MetaSwapTransferSwapEnforcer:invalid-transfer-call" - ); - - bytes calldata transferCallData_ = _execution.callData; - address recipient_; - uint256 amount_; - assembly ("memory-safe") { - recipient_ := and(calldataload(add(transferCallData_.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) - amount_ := calldataload(add(transferCallData_.offset, 36)) - } - require( - recipient_ == _terms.adapter && amount_ == _terms.tokenInAmount, - "MetaSwapTransferSwapEnforcer:invalid-transfer-call" - ); - } - - function _validateSwap(address _target, uint256 _value, bytes calldata _callData, Terms memory _terms) private pure { - uint256 expectedValue_ = _terms.tokenIn == address(0) ? _terms.tokenInAmount : 0; - if (_target != _terms.adapter || _value != expectedValue_ || _callData.length < MIN_SWAP_CALLDATA_LENGTH) { - revert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - } - require( - bytes4(_callData[:4]) == MetaSwapAdapter.swap.selector, "MetaSwapTransferSwapEnforcer:invalid-swap-call" - ); - - address tokenIn_; - address tokenOut_; - uint256 tokenInAmount_; - uint256 minTokenOut_; - uint256 quoteOffset_; - assembly ("memory-safe") { - tokenIn_ := and(calldataload(add(_callData.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff) - tokenOut_ := and(calldataload(add(_callData.offset, 36)), 0xffffffffffffffffffffffffffffffffffffffff) - tokenInAmount_ := calldataload(add(_callData.offset, 68)) - minTokenOut_ := calldataload(add(_callData.offset, 100)) - quoteOffset_ := calldataload(add(_callData.offset, 132)) - } - - // Enforce the canonical five-word ABI head without copying the dynamic quote into memory. - require(quoteOffset_ == API_QUOTE_OFFSET, "MetaSwapTransferSwapEnforcer:invalid-swap-call"); - - if ( - tokenIn_ != _terms.tokenIn || tokenOut_ != _terms.tokenOut || tokenInAmount_ != _terms.tokenInAmount - || minTokenOut_ < _terms.minTokenOut - ) { - revert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - } - } - - function getTermsInfo(bytes calldata _terms) external pure returns (Terms memory terms_) { - terms_ = abi.decode(_terms, (Terms)); - } - - function encodeTerms(Terms calldata _terms) external pure returns (bytes memory) { - return abi.encode(_terms); - } -} diff --git a/src/GaslessSwapDelegationManager.sol b/src/experiments/GaslessSwapDelegationManager.sol similarity index 100% rename from src/GaslessSwapDelegationManager.sol rename to src/experiments/GaslessSwapDelegationManager.sol diff --git a/src/MetaSwapMinimalDelegationManager.sol b/src/experiments/MetaSwapMinimalDelegationManager.sol similarity index 100% rename from src/MetaSwapMinimalDelegationManager.sol rename to src/experiments/MetaSwapMinimalDelegationManager.sol diff --git a/test/GaslessSwapDelegationManager.t.sol b/test/GaslessSwapDelegationManager.t.sol deleted file mode 100644 index 3583a69d..00000000 --- a/test/GaslessSwapDelegationManager.t.sol +++ /dev/null @@ -1,477 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; -import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; - -import { BaseTest } from "./utils/BaseTest.t.sol"; -import { BasicERC20 } from "./utils/BasicERC20.t.sol"; -import { MockLimitOrderRouter } from "./utils/MockLimitOrderRouter.sol"; -import { Implementation, SignatureType } from "./utils/Types.t.sol"; -import { GaslessSwapDelegationManager } from "../src/GaslessSwapDelegationManager.sol"; -import { EIP7702MultiManagerDeleGator } from "../src/EIP7702/EIP7702MultiManagerDeleGator.sol"; -import { EIP7702MultiManagerDeleGatorCore } from "../src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol"; -import { ERC20BalanceChangeEnforcer } from "../src/enforcers/ERC20BalanceChangeEnforcer.sol"; -import { ExactExecutionEnforcer } from "../src/enforcers/ExactExecutionEnforcer.sol"; -import { LimitedCallsEnforcer } from "../src/enforcers/LimitedCallsEnforcer.sol"; -import { MetaSwap7702CalldataEnforcer } from "../src/enforcers/MetaSwap7702CalldataEnforcer.sol"; -import { NativeBalanceChangeEnforcer } from "../src/enforcers/NativeBalanceChangeEnforcer.sol"; -import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; -import { IERC7821 } from "../src/interfaces/IERC7821.sol"; -import { IMetaSwap } from "../src/helpers/interfaces/IMetaSwap.sol"; -import { EncoderLib } from "../src/libraries/EncoderLib.sol"; -import { Caveat, Delegation, Execution, ModeCode } from "../src/utils/Types.sol"; - -/** - * @title GaslessSwapDelegationManagerTest - * @notice Exercises both supported profiles through an EIP-7702 account that approves multiple delegation managers. - */ -contract GaslessSwapDelegationManagerTest is BaseTest { - using MessageHashUtils for bytes32; - - uint256 internal constant SWAP_AMOUNT = 1 ether; - uint256 internal constant TOKEN_OUT_MIN = 0.9 ether; - - ExactExecutionEnforcer internal exactExecutionEnforcer; - MetaSwap7702CalldataEnforcer internal metaSwap7702CalldataEnforcer; - LimitedCallsEnforcer internal limitedCallsEnforcer; - NativeBalanceChangeEnforcer internal nativeBalanceChangeEnforcer; - ERC20BalanceChangeEnforcer internal erc20BalanceChangeEnforcer; - GaslessSwapDelegationManager internal swapManager; - - EIP7702MultiManagerDeleGator internal multiManagerImplementation; - EIP7702MultiManagerDeleGator internal aliceAccount; - - BasicERC20 internal tokenIn; - BasicERC20 internal tokenOut; - MockLimitOrderRouter internal router; - - address internal alice; - address internal relayer; - - constructor() { - IMPLEMENTATION = Implementation.EIP7702Stateless; - SIGNATURE_TYPE = SignatureType.EOA; - } - - function setUp() public override { - super.setUp(); - - exactExecutionEnforcer = new ExactExecutionEnforcer(); - metaSwap7702CalldataEnforcer = new MetaSwap7702CalldataEnforcer(); - limitedCallsEnforcer = new LimitedCallsEnforcer(); - nativeBalanceChangeEnforcer = new NativeBalanceChangeEnforcer(); - erc20BalanceChangeEnforcer = new ERC20BalanceChangeEnforcer(); - swapManager = new GaslessSwapDelegationManager( - address(exactExecutionEnforcer), - address(metaSwap7702CalldataEnforcer), - address(limitedCallsEnforcer), - address(nativeBalanceChangeEnforcer), - address(erc20BalanceChangeEnforcer) - ); - - alice = users.alice.addr; - relayer = makeAddr("Relayer"); - - multiManagerImplementation = new EIP7702MultiManagerDeleGator(); - vm.etch(alice, bytes.concat(hex"ef0100", abi.encodePacked(address(multiManagerImplementation)))); - aliceAccount = EIP7702MultiManagerDeleGator(payable(alice)); - - // The same 7702 account retains the canonical manager and opts into the specialized manager. - vm.startPrank(alice); - aliceAccount.approveDelegationManager(IDelegationManager(address(delegationManager))); - aliceAccount.approveDelegationManager(IDelegationManager(address(swapManager))); - vm.stopPrank(); - - tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); - tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); - router = new MockLimitOrderRouter(); - - tokenIn.mint(alice, 100 ether); - tokenOut.mint(address(router), 100 ether); - vm.deal(address(router), 100 ether); - router.setERC20AmountOut(SWAP_AMOUNT); - router.setNativeAmountOut(SWAP_AMOUNT); - } - - function test_multiManagerAccountApprovesCanonicalAndSwapManagers() public { - assertTrue(aliceAccount.isApprovedDelegationManager(IDelegationManager(address(delegationManager)))); - assertTrue(aliceAccount.isApprovedDelegationManager(IDelegationManager(address(swapManager)))); - } - - function test_gaslessProfile_nativeInERC20Out() public { - Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); - Delegation memory delegation_ = _signDelegation(_gaslessCaveats(execution_)); - - uint256 tokenBefore_ = tokenOut.balanceOf(alice); - uint256 nativeBefore_ = alice.balance; - - _redeem(delegation_, execution_); - - assertEq(tokenOut.balanceOf(alice), tokenBefore_ + SWAP_AMOUNT); - assertEq(alice.balance, nativeBefore_ - SWAP_AMOUNT); - } - - function test_gaslessProfile_erc20InNativeOut() public { - Execution memory execution_ = _wrap7702Batch(_erc20InNativeOutExecutions()); - Delegation memory delegation_ = _signDelegation(_gaslessCaveats(execution_)); - - uint256 tokenBefore_ = tokenIn.balanceOf(alice); - uint256 nativeBefore_ = alice.balance; - - _redeem(delegation_, execution_); - - assertEq(tokenIn.balanceOf(alice), tokenBefore_ - SWAP_AMOUNT); - assertEq(alice.balance, nativeBefore_ + SWAP_AMOUNT); - } - - function test_gaslessProfile_replayReverts() public { - Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); - Delegation memory delegation_ = _signDelegation(_gaslessCaveats(execution_)); - - _redeem(delegation_, execution_); - - vm.expectRevert("LimitedCallsEnforcer:limit-exceeded"); - _redeem(delegation_, execution_); - } - - function test_limitOrder_erc20OutputEnforcesMinimumIncrease() public { - Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(erc20BalanceChangeEnforcer), - terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), - args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); - - uint256 balanceBefore_ = tokenOut.balanceOf(alice); - _redeem(delegation_, execution_); - - assertEq(tokenOut.balanceOf(alice), balanceBefore_ + SWAP_AMOUNT); - } - - function test_limitOrder_nativeOutputEnforcesMinimumIncrease() public { - Execution memory execution_ = _wrap7702Batch(_erc20InNativeOutExecutions()); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(nativeBalanceChangeEnforcer), terms: abi.encodePacked(false, alice, TOKEN_OUT_MIN), args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); - - uint256 balanceBefore_ = alice.balance; - _redeem(delegation_, execution_); - - assertEq(alice.balance, balanceBefore_ + SWAP_AMOUNT); - } - - function test_flexibleMetaSwapLimitOrder_erc20OneApproval() public { - Execution memory execution_ = - _wrap7702Batch(_metaSwapERC20Executions(false, "best-route", abi.encode(tokenOut, SWAP_AMOUNT))); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(erc20BalanceChangeEnforcer), - terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), - args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), false, balanceCaveat_)); - - _redeem(delegation_, execution_); - - assertEq(tokenIn.balanceOf(alice), 99 ether); - assertEq(tokenOut.balanceOf(alice), SWAP_AMOUNT); - } - - function test_flexibleMetaSwapLimitOrder_erc20ResetApproval() public { - vm.prank(alice); - tokenIn.approve(address(router), 1); - - Execution memory execution_ = - _wrap7702Batch(_metaSwapERC20Executions(true, "best-route", abi.encode(tokenOut, SWAP_AMOUNT))); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(erc20BalanceChangeEnforcer), - terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), - args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), true, balanceCaveat_)); - - _redeem(delegation_, execution_); - - assertEq(tokenIn.balanceOf(alice), 99 ether); - assertEq(tokenOut.balanceOf(alice), SWAP_AMOUNT); - assertEq(tokenIn.allowance(alice, address(router)), 0); - } - - function test_flexibleMetaSwapLimitOrder_nativeInput() public { - Execution memory execution_ = _wrap7702Batch(_metaSwapNativeExecutions("best-route", abi.encode(tokenOut, SWAP_AMOUNT))); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(erc20BalanceChangeEnforcer), - terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), - args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(0), false, balanceCaveat_)); - - uint256 nativeBefore_ = alice.balance; - _redeem(delegation_, execution_); - - assertEq(alice.balance, nativeBefore_ - SWAP_AMOUNT); - assertEq(tokenOut.balanceOf(alice), SWAP_AMOUNT); - } - - function test_flexibleMetaSwapLimitOrder_nativeOutput() public { - Execution memory execution_ = _wrap7702Batch( - _metaSwapERC20Executions(false, "best-route", abi.encode(IERC20(address(0)), SWAP_AMOUNT)) - ); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(nativeBalanceChangeEnforcer), terms: abi.encodePacked(false, alice, TOKEN_OUT_MIN), args: hex"" - }); - Delegation memory delegation_ = - _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), false, balanceCaveat_)); - - uint256 nativeBefore_ = alice.balance; - _redeem(delegation_, execution_); - - assertEq(tokenIn.balanceOf(alice), 99 ether); - assertEq(alice.balance, nativeBefore_ + SWAP_AMOUNT); - } - - function test_flexibleMetaSwapLimitOrder_badRouteCanRetryWithDifferentCalldata() public { - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(erc20BalanceChangeEnforcer), - terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), - args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_dynamicLimitOrderCaveats(address(tokenIn), false, balanceCaveat_)); - - Execution memory badExecution_ = - _wrap7702Batch(_metaSwapERC20Executions(false, "bad", abi.encode(tokenOut, TOKEN_OUT_MIN - 1))); - vm.expectRevert("ERC20BalanceChangeEnforcer:insufficient-balance-increase"); - _redeem(delegation_, badExecution_); - - Execution memory goodExecution_ = - _wrap7702Batch(_metaSwapERC20Executions(false, "new-route", abi.encode(tokenOut, TOKEN_OUT_MIN))); - _redeem(delegation_, goodExecution_); - - assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); - } - - function test_limitOrder_insufficientOutputRevertsAndRemainsRetryable() public { - router.setERC20AmountOut(TOKEN_OUT_MIN - 1); - - Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(erc20BalanceChangeEnforcer), - terms: abi.encodePacked(false, address(tokenOut), alice, TOKEN_OUT_MIN), - args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); - bytes32 delegationHash_ = swapManager.getDelegationHash(delegation_); - - vm.expectRevert("ERC20BalanceChangeEnforcer:insufficient-balance-increase"); - _redeem(delegation_, execution_); - - assertEq(limitedCallsEnforcer.callCounts(address(swapManager), delegationHash_), 0); - assertFalse( - erc20BalanceChangeEnforcer.isLocked( - erc20BalanceChangeEnforcer.getHashKey(address(swapManager), address(tokenOut), delegationHash_) - ) - ); - - router.setERC20AmountOut(TOKEN_OUT_MIN); - _redeem(delegation_, execution_); - - assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); - assertEq(limitedCallsEnforcer.callCounts(address(swapManager), delegationHash_), 1); - } - - function test_limitOrder_rejectsBalanceRecipientOtherThanDelegator() public { - Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); - Caveat memory balanceCaveat_ = Caveat({ - enforcer: address(erc20BalanceChangeEnforcer), - terms: abi.encodePacked(false, address(tokenOut), makeAddr("OtherRecipient"), TOKEN_OUT_MIN), - args: hex"" - }); - Delegation memory delegation_ = _signDelegation(_limitOrderCaveats(execution_, balanceCaveat_)); - - vm.expectRevert(GaslessSwapDelegationManager.InvalidBalanceTerms.selector); - _redeem(delegation_, execution_); - } - - function test_rejectsUnapprovedManagerAtAccountBoundary() public { - GaslessSwapDelegationManager unapprovedManager_ = new GaslessSwapDelegationManager( - address(exactExecutionEnforcer), - address(metaSwap7702CalldataEnforcer), - address(limitedCallsEnforcer), - address(nativeBalanceChangeEnforcer), - address(erc20BalanceChangeEnforcer) - ); - Execution memory execution_ = _wrap7702Batch(_nativeInERC20OutExecutions()); - Delegation memory delegation_ = _signDelegationFor(unapprovedManager_, _gaslessCaveats(execution_)); - - vm.expectRevert(EIP7702MultiManagerDeleGatorCore.NotDelegationManager.selector); - _redeemThrough(unapprovedManager_, delegation_, execution_); - } - - function _nativeInERC20OutExecutions() internal view returns (Execution[] memory executions_) { - executions_ = new Execution[](1); - executions_[0] = Execution({ - target: address(router), - value: SWAP_AMOUNT, - callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) - }); - } - - function _erc20InNativeOutExecutions() internal view returns (Execution[] memory executions_) { - executions_ = new Execution[](2); - executions_[0] = Execution({ - target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), SWAP_AMOUNT)) - }); - executions_[1] = Execution({ - target: address(router), - value: 0, - callData: abi.encodeCall( - MockLimitOrderRouter.swapERC20ForNative, (IERC20(address(tokenIn)), SWAP_AMOUNT, payable(alice)) - ) - }); - } - - function _metaSwapERC20Executions( - bool resetApproval_, - string memory aggregatorId_, - bytes memory route_ - ) - internal - view - returns (Execution[] memory executions_) - { - uint256 swapIndex_ = resetApproval_ ? 2 : 1; - executions_ = new Execution[](swapIndex_ + 1); - if (resetApproval_) { - executions_[0] = - Execution({ target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), 0)) }); - } - executions_[swapIndex_ - 1] = Execution({ - target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.approve, (address(router), SWAP_AMOUNT)) - }); - executions_[swapIndex_] = Execution({ - target: address(router), - value: 0, - callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(address(tokenIn)), SWAP_AMOUNT, route_)) - }); - } - - function _metaSwapNativeExecutions( - string memory aggregatorId_, - bytes memory route_ - ) - internal - view - returns (Execution[] memory executions_) - { - executions_ = new Execution[](1); - executions_[0] = Execution({ - target: address(router), - value: SWAP_AMOUNT, - callData: abi.encodeCall(IMetaSwap.swap, (aggregatorId_, IERC20(address(0)), SWAP_AMOUNT, route_)) - }); - } - - function _wrap7702Batch(Execution[] memory executions_) internal view returns (Execution memory execution_) { - execution_ = Execution({ - target: alice, - value: 0, - callData: abi.encodeCall(IERC7821.execute, (ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(executions_))) - }); - } - - function _gaslessCaveats(Execution memory execution_) internal view returns (Caveat[] memory caveats_) { - caveats_ = new Caveat[](2); - caveats_[0] = Caveat({ - enforcer: address(exactExecutionEnforcer), - terms: ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), - args: hex"" - }); - caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); - } - - function _limitOrderCaveats( - Execution memory execution_, - Caveat memory balanceCaveat_ - ) - internal - view - returns (Caveat[] memory caveats_) - { - Caveat[] memory gaslessCaveats_ = _gaslessCaveats(execution_); - caveats_ = new Caveat[](3); - caveats_[0] = gaslessCaveats_[0]; - caveats_[1] = gaslessCaveats_[1]; - caveats_[2] = balanceCaveat_; - } - - function _dynamicLimitOrderCaveats( - address tokenIn_, - bool resetApproval_, - Caveat memory balanceCaveat_ - ) - internal - view - returns (Caveat[] memory caveats_) - { - caveats_ = new Caveat[](3); - caveats_[0] = Caveat({ - enforcer: address(metaSwap7702CalldataEnforcer), - terms: abi.encodePacked(address(router), tokenIn_, SWAP_AMOUNT, bytes1(resetApproval_ ? 0x01 : 0x00)), - args: hex"" - }); - caveats_[1] = Caveat({ enforcer: address(limitedCallsEnforcer), terms: abi.encode(uint256(1)), args: hex"" }); - caveats_[2] = balanceCaveat_; - } - - function _signDelegation(Caveat[] memory caveats_) internal view returns (Delegation memory delegation_) { - delegation_ = _signDelegationFor(swapManager, caveats_); - } - - function _signDelegationFor( - GaslessSwapDelegationManager manager_, - Caveat[] memory caveats_ - ) - internal - view - returns (Delegation memory delegation_) - { - delegation_ = Delegation({ - delegate: ANY_DELEGATE, delegator: alice, authority: ROOT_AUTHORITY, caveats: caveats_, salt: 0, signature: hex"" - }); - - bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); - bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(manager_.getDomainHash(), delegationHash_); - delegation_.signature = signHash(users.alice, typedDataHash_); - } - - function _redeem(Delegation memory delegation_, Execution memory execution_) internal { - _redeemThrough(swapManager, delegation_, execution_); - } - - function _redeemThrough( - GaslessSwapDelegationManager manager_, - Delegation memory delegation_, - Execution memory execution_ - ) - internal - { - Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = delegation_; - - bytes[] memory permissionContexts_ = new bytes[](1); - permissionContexts_[0] = abi.encode(delegations_); - - ModeCode[] memory modes_ = new ModeCode[](1); - modes_[0] = singleDefaultMode; - - bytes[] memory executionCallDatas_ = new bytes[](1); - executionCallDatas_[0] = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); - - vm.prank(relayer); - manager_.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); - } -} diff --git a/test/MetaSwapMinimalDelegationManager.t.sol b/test/MetaSwapMinimalDelegationManager.t.sol deleted file mode 100644 index 6cb61ec7..00000000 --- a/test/MetaSwapMinimalDelegationManager.t.sol +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; - -import { BaseTest } from "./utils/BaseTest.t.sol"; -import { BasicERC20 } from "./utils/BasicERC20.t.sol"; -import { MockLimitOrderRouter } from "./utils/MockLimitOrderRouter.sol"; -import { Implementation, SignatureType } from "./utils/Types.t.sol"; -import { EIP7702MultiManagerDeleGator } from "../src/EIP7702/EIP7702MultiManagerDeleGator.sol"; -import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; -import { IMetaSwap } from "../src/helpers/interfaces/IMetaSwap.sol"; -import { EncoderLib } from "../src/libraries/EncoderLib.sol"; -import { MetaSwapMinimalDelegationManager } from "../src/MetaSwapMinimalDelegationManager.sol"; -import { Caveat, Delegation, Execution, ModeCode } from "../src/utils/Types.sol"; - -contract MetaSwapMinimalDelegationManagerTest is BaseTest { - uint256 internal constant TOKEN_IN_AMOUNT = 1 ether; - uint256 internal constant TOKEN_OUT_MIN = 0.9 ether; - - MetaSwapMinimalDelegationManager internal minimalManager; - EIP7702MultiManagerDeleGator internal multiManagerImplementation; - EIP7702MultiManagerDeleGator internal aliceAccount; - - BasicERC20 internal tokenIn; - BasicERC20 internal tokenOut; - MockLimitOrderRouter internal metaSwap; - - address internal alice; - address internal relayer; - - constructor() { - IMPLEMENTATION = Implementation.EIP7702Stateless; - SIGNATURE_TYPE = SignatureType.EOA; - } - - function setUp() public override { - super.setUp(); - - minimalManager = new MetaSwapMinimalDelegationManager(); - multiManagerImplementation = new EIP7702MultiManagerDeleGator(); - alice = users.alice.addr; - relayer = makeAddr("Relayer"); - - vm.etch(alice, bytes.concat(hex"ef0100", abi.encodePacked(address(multiManagerImplementation)))); - aliceAccount = EIP7702MultiManagerDeleGator(payable(alice)); - vm.startPrank(alice); - aliceAccount.approveDelegationManager(IDelegationManager(address(delegationManager))); - aliceAccount.approveDelegationManager(IDelegationManager(address(minimalManager))); - vm.stopPrank(); - - tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); - tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); - metaSwap = new MockLimitOrderRouter(); - - tokenIn.mint(alice, 100 ether); - tokenOut.mint(address(metaSwap), 100 ether); - vm.deal(address(metaSwap), 100 ether); - metaSwap.setERC20AmountOut(TOKEN_IN_AMOUNT); - } - - function test_gaslessExact_executesSignedExecution() public { - Execution memory execution_ = Execution({ - target: address(metaSwap), - value: TOKEN_IN_AMOUNT, - callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) - }); - bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); - bytes32 executionHash_ = minimalManager.getGaslessExecutionHash(singleDefaultMode, executionCallData_); - Delegation memory delegation_ = _sign(_gaslessTerms(executionHash_)); - - _redeem(delegation_, singleDefaultMode, executionCallData_); - - assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); - } - - function test_gaslessExact_rejectsTamperedExecution() public { - Execution memory execution_ = Execution({ - target: address(metaSwap), - value: TOKEN_IN_AMOUNT, - callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) - }); - bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); - Delegation memory delegation_ = - _sign(_gaslessTerms(minimalManager.getGaslessExecutionHash(singleDefaultMode, executionCallData_))); - - execution_.value++; - vm.expectRevert(MetaSwapMinimalDelegationManager.InvalidMode.selector); - _redeem(delegation_, singleDefaultMode, ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData)); - } - - function test_gaslessExact_isOneShot() public { - Execution memory execution_ = Execution({ - target: address(metaSwap), - value: TOKEN_IN_AMOUNT, - callData: abi.encodeCall(MockLimitOrderRouter.swapNativeForERC20, (IERC20(address(tokenOut)), alice)) - }); - bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); - Delegation memory delegation_ = - _sign(_gaslessTerms(minimalManager.getGaslessExecutionHash(singleDefaultMode, executionCallData_))); - - _redeem(delegation_, singleDefaultMode, executionCallData_); - - vm.expectRevert(MetaSwapMinimalDelegationManager.DelegationAlreadyUsed.selector); - _redeem(delegation_, singleDefaultMode, executionCallData_); - } - - function test_limitOrder_erc20OneApproval() public { - Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), false)); - - _fill(delegation_, "best-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); - - assertEq(tokenIn.balanceOf(alice), 99 ether); - assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); - } - - function test_limitOrder_erc20ResetApproval() public { - vm.prank(alice); - tokenIn.approve(address(metaSwap), 1); - Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), true)); - - _fill(delegation_, "best-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); - - assertEq(tokenIn.balanceOf(alice), 99 ether); - assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); - assertEq(tokenIn.allowance(alice, address(metaSwap)), 0); - } - - function test_limitOrder_nativeInput() public { - Delegation memory delegation_ = _sign(_limitTerms(address(0), address(tokenOut), false)); - uint256 nativeBefore_ = alice.balance; - - _fill(delegation_, "best-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); - - assertEq(alice.balance, nativeBefore_ - TOKEN_IN_AMOUNT); - assertEq(tokenOut.balanceOf(alice), TOKEN_IN_AMOUNT); - } - - function test_limitOrder_nativeOutput() public { - Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(0), false)); - uint256 nativeBefore_ = alice.balance; - - _fill(delegation_, "best-route", abi.encode(IERC20(address(0)), TOKEN_IN_AMOUNT)); - - assertEq(tokenIn.balanceOf(alice), 99 ether); - assertEq(alice.balance, nativeBefore_ + TOKEN_IN_AMOUNT); - } - - function test_limitOrder_managerOverridesCallerSuppliedInputTokenAndAmount() public { - BasicERC20 otherToken_ = new BasicERC20(address(this), "Other", "OTHER", 0); - otherToken_.mint(alice, 10 ether); - vm.prank(alice); - otherToken_.approve(address(metaSwap), 10 ether); - Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), false)); - - // The route payload contains no tokenFrom or amount fields used to construct the MetaSwap call. - _fill(delegation_, "caller-route", abi.encode(tokenOut, TOKEN_IN_AMOUNT)); - - assertEq(otherToken_.balanceOf(alice), 10 ether); - assertEq(tokenIn.balanceOf(alice), 99 ether); - } - - function test_limitOrder_insufficientOutputCanRetryWithNewRoute() public { - Delegation memory delegation_ = _sign(_limitTerms(address(tokenIn), address(tokenOut), false)); - - vm.expectRevert( - abi.encodeWithSelector(MetaSwapMinimalDelegationManager.InsufficientOutput.selector, TOKEN_OUT_MIN, TOKEN_OUT_MIN - 1) - ); - _fill(delegation_, "bad-route", abi.encode(tokenOut, TOKEN_OUT_MIN - 1)); - - _fill(delegation_, "new-route", abi.encode(tokenOut, TOKEN_OUT_MIN)); - - assertEq(tokenOut.balanceOf(alice), TOKEN_OUT_MIN); - } - - function _gaslessTerms(bytes32 executionHash_) private view returns (Caveat[] memory caveats_) { - caveats_ = new Caveat[](1); - caveats_[0] = Caveat({ - enforcer: address(minimalManager), - terms: abi.encodePacked(bytes1(minimalManager.GASLESS_EXACT_PROFILE()), executionHash_), - args: hex"" - }); - } - - function _limitTerms(address tokenIn_, address tokenOut_, bool resetApproval_) private view returns (Caveat[] memory caveats_) { - caveats_ = new Caveat[](1); - caveats_[0] = Caveat({ - enforcer: address(minimalManager), - terms: abi.encodePacked( - bytes1(minimalManager.LIMIT_ORDER_PROFILE()), - address(metaSwap), - tokenIn_, - tokenOut_, - TOKEN_IN_AMOUNT, - TOKEN_OUT_MIN, - bytes1(resetApproval_ ? 0x01 : 0x00) - ), - args: hex"" - }); - } - - function _sign(Caveat[] memory caveats_) private view returns (Delegation memory delegation_) { - delegation_ = Delegation({ - delegate: ANY_DELEGATE, delegator: alice, authority: ROOT_AUTHORITY, caveats: caveats_, salt: 0, signature: hex"" - }); - - bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); - bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(minimalManager.getDomainHash(), delegationHash_); - delegation_.signature = signHash(users.alice, typedDataHash_); - } - - function _fill(Delegation memory delegation_, string memory aggregatorId_, bytes memory routeData_) private { - _redeem(delegation_, batchDefaultMode, abi.encode(aggregatorId_, routeData_)); - } - - function _redeem(Delegation memory delegation_, ModeCode mode_, bytes memory executionCallData_) private { - Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = delegation_; - bytes[] memory permissionContexts_ = new bytes[](1); - permissionContexts_[0] = abi.encode(delegations_); - ModeCode[] memory modes_ = new ModeCode[](1); - modes_[0] = mode_; - bytes[] memory executionCallDatas_ = new bytes[](1); - executionCallDatas_[0] = executionCallData_; - - vm.prank(relayer); - minimalManager.redeemDelegations(permissionContexts_, modes_, executionCallDatas_); - } -} diff --git a/test/helpers/DelegationMetaSwapAdapter2.t.sol b/test/helpers/DelegationMetaSwapAdapter2.t.sol deleted file mode 100644 index 5177c6ff..00000000 --- a/test/helpers/DelegationMetaSwapAdapter2.t.sol +++ /dev/null @@ -1,906 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; - -import { MetaSwapTransferSwapEnforcer } from "../../src/enforcers/MetaSwapTransferSwapEnforcer.sol"; -import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; -import { MetaSwapAdapter as DelegationMetaSwapAdapter2 } from "../../src/helpers/MetaSwapAdapter.sol"; -import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; -import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; -import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; -import { BasicERC20 } from "../utils/BasicERC20.t.sol"; -import { CaveatEnforcerBaseTest } from "../enforcers/CaveatEnforcerBaseTest.t.sol"; - -contract MetaSwapAdapter2Mock is IMetaSwap { - using SafeERC20 for IERC20; - - bool internal pullInput = true; - bool internal returnInput; - bool internal usePayoutOverride; - uint256 internal payoutOverride; - - receive() external payable { } - - function setBehavior(bool _pullInput, bool _returnInput, bool _usePayoutOverride, uint256 _payoutOverride) external { - pullInput = _pullInput; - returnInput = _returnInput; - usePayoutOverride = _usePayoutOverride; - payoutOverride = _payoutOverride; - } - - function swap(string calldata, IERC20 _tokenIn, uint256 _amountIn, bytes calldata _swapData) external payable { - (,, IERC20 tokenOut_,, uint256 quotedOutput_,,,,) = abi.decode( - abi.encodePacked(abi.encode(address(0)), _swapData), - (address, IERC20, IERC20, uint256, uint256, bytes, uint256, address, bool) - ); - - if (address(_tokenIn) == address(0)) { - require(msg.value == _amountIn, "invalid-native-input"); - if (returnInput) { - (bool refundSuccess_,) = msg.sender.call{ value: 1 }(""); - require(refundSuccess_, "native-refund-failed"); - } - } else { - require(msg.value == 0, "unexpected-value"); - if (pullInput) _tokenIn.safeTransferFrom(msg.sender, address(this), _amountIn); - if (returnInput) _tokenIn.safeTransfer(msg.sender, 1); - } - - uint256 payout_ = usePayoutOverride ? payoutOverride : quotedOutput_; - if (address(tokenOut_) == address(0)) { - (bool payoutSuccess_,) = msg.sender.call{ value: payout_ }(""); - require(payoutSuccess_, "native-output-failed"); - } else { - tokenOut_.safeTransfer(msg.sender, payout_); - } - } - - function setAdapter(string calldata, address, bytes4, bytes calldata) external { } - function removeAdapter(string calldata) external { } - - function adapters(string memory) external pure returns (Adapter memory) { - return Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); - } -} - -contract RejectNativeRecipient { - function execute( - DelegationMetaSwapAdapter2 _adapter, - IERC20 _tokenIn, - uint256 _tokenInAmount, - uint256 _minTokenOut, - DelegationMetaSwapAdapter2.ApiQuote calldata _quote - ) - external - { - _adapter.swap(_tokenIn, IERC20(address(0)), _tokenInAmount, _minTokenOut, _quote); - } - - receive() external payable { - revert(); - } -} - -contract ZeroFirstERC20 is ERC20 { - constructor() ERC20("Zero First", "ZERO") { } - - function mint(address _recipient, uint256 _amount) external { - _mint(_recipient, _amount); - } - - function seedAllowance(address _owner, address _spender, uint256 _amount) external { - _approve(_owner, _spender, _amount); - } - - function approve(address _spender, uint256 _amount) public override returns (bool) { - require(_amount == 0 || allowance(msg.sender, _spender) == 0, "zero-first"); - return super.approve(_spender, _amount); - } -} - -contract MetaSwapAdapterTest is CaveatEnforcerBaseTest { - uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; - uint256 internal constant MIN_TOKEN_OUT = 190 ether; - uint256 internal constant ACTUAL_TOKEN_OUT = 200 ether; - - BasicERC20 internal tokenIn; - BasicERC20 internal tokenOut; - MetaSwapAdapter2Mock internal metaSwap; - DelegationMetaSwapAdapter2 internal adapter; - MetaSwapTransferSwapEnforcer internal enforcer; - RedeemerEnforcer internal redeemerEnforcer; - - address internal automation; - address internal apiSigner; - uint256 internal apiSignerKey; - - function setUp() public override { - super.setUp(); - tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); - tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); - metaSwap = new MetaSwapAdapter2Mock(); - (apiSigner, apiSignerKey) = makeAddrAndKey("api-signer"); - adapter = new DelegationMetaSwapAdapter2(address(this), apiSigner, metaSwap); - enforcer = new MetaSwapTransferSwapEnforcer(); - redeemerEnforcer = new RedeemerEnforcer(); - automation = makeAddr("metamask-automation"); - - tokenIn.mint(address(users.alice.deleGator), TOKEN_IN_AMOUNT); - tokenOut.mint(address(metaSwap), 10_000 ether); - vm.deal(address(metaSwap), 10_000 ether); - } - - receive() external payable { } - - function _getEnforcer() internal view override returns (ICaveatEnforcer) { - return ICaveatEnforcer(address(enforcer)); - } - - function test_constructorRejectsZeroAddresses() public { - vm.expectRevert(); - new DelegationMetaSwapAdapter2(address(0), apiSigner, metaSwap); - - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAddress.selector); - new DelegationMetaSwapAdapter2(address(this), address(0), metaSwap); - - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAddress.selector); - new DelegationMetaSwapAdapter2(address(this), apiSigner, IMetaSwap(address(0))); - } - - function test_withdrawsErc20AndNativeTokens() public { - address recipient_ = makeAddr("withdraw-recipient"); - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - vm.deal(address(adapter), TOKEN_IN_AMOUNT); - - adapter.withdraw(tokenIn, recipient_, TOKEN_IN_AMOUNT); - adapter.withdraw(IERC20(address(0)), recipient_, TOKEN_IN_AMOUNT); - - assertEq(tokenIn.balanceOf(recipient_), TOKEN_IN_AMOUNT); - assertEq(recipient_.balance, TOKEN_IN_AMOUNT); - } - - function test_withdrawRejectsZeroRecipientAndNonOwner() public { - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAddress.selector); - adapter.withdraw(tokenIn, address(0), 1); - - vm.prank(makeAddr("not-owner")); - vm.expectRevert(); - adapter.withdraw(tokenIn, address(this), 1); - } - - function test_withdrawRevertsWhenRecipientRejectsNative() public { - RejectNativeRecipient recipient_ = new RejectNativeRecipient(); - vm.deal(address(adapter), 1); - - vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.FailedNativeTokenTransfer.selector, address(recipient_))); - adapter.withdraw(IERC20(address(0)), address(recipient_), 1); - } - - function test_adapterSwapHappyPath() public { - tokenIn.mint(address(this), TOKEN_IN_AMOUNT); - tokenIn.transfer(address(adapter), TOKEN_IN_AMOUNT); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - uint256 received_ = adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(received_, ACTUAL_TOKEN_OUT); - assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); - assertEq(tokenIn.balanceOf(address(adapter)), 0); - assertEq(tokenIn.allowance(address(adapter), address(metaSwap)), 0); - } - - function test_adapterSwapsNativeInputForErc20() public { - IERC20 nativeToken_ = IERC20(address(0)); - vm.deal(address(this), TOKEN_IN_AMOUNT); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - uint256 received_ = adapter.swap{ value: TOKEN_IN_AMOUNT }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(received_, ACTUAL_TOKEN_OUT); - assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); - assertEq(address(adapter).balance, 0); - } - - function test_adapterSwapsNativeInputWithoutConsumingExistingDust() public { - IERC20 nativeToken_ = IERC20(address(0)); - vm.deal(address(adapter), 1 ether); - vm.deal(address(this), TOKEN_IN_AMOUNT); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - adapter.swap{ value: TOKEN_IN_AMOUNT }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(address(adapter).balance, 1 ether); - } - - function test_adapterSwapsErc20InputForNative() public { - IERC20 nativeToken_ = IERC20(address(0)); - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - uint256 nativeBefore_ = address(this).balance; - - uint256 received_ = adapter.swap(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(received_, ACTUAL_TOKEN_OUT); - assertEq(address(this).balance - nativeBefore_, ACTUAL_TOKEN_OUT); - assertEq(address(adapter).balance, 0); - } - - function test_adapterRejectsIncorrectNativeValueAndUnexpectedErc20Value() public { - IERC20 nativeToken_ = IERC20(address(0)); - vm.deal(address(this), TOKEN_IN_AMOUNT); - DelegationMetaSwapAdapter2.ApiQuote memory nativeQuote_ = - _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert( - abi.encodeWithSelector(DelegationMetaSwapAdapter2.InvalidValue.selector, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT - 1) - ); - adapter.swap{ value: TOKEN_IN_AMOUNT - 1 }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, nativeQuote_); - - DelegationMetaSwapAdapter2.ApiQuote memory erc20Quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.InvalidValue.selector, 0, 1)); - adapter.swap{ value: 1 }(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, erc20Quote_); - } - - function test_adapterRevertsWhenMetaSwapRefundsNativeInput() public { - IERC20 nativeToken_ = IERC20(address(0)); - vm.deal(address(this), TOKEN_IN_AMOUNT); - metaSwap.setBehavior(true, true, false, 0); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.RemainingInputBalance.selector, 1)); - adapter.swap{ value: TOKEN_IN_AMOUNT }(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterAllowsSignedQuoteReuse() public { - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT * 2); - } - - function test_adapterRevertsWhenNativeRecipientRejectsOutput() public { - IERC20 nativeToken_ = IERC20(address(0)); - RejectNativeRecipient recipient_ = new RejectNativeRecipient(); - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.FailedNativeTokenTransfer.selector, address(recipient_))); - recipient_.execute(adapter, tokenIn, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterForceApproveHandlesZeroFirstToken() public { - ZeroFirstERC20 zeroFirst_ = new ZeroFirstERC20(); - zeroFirst_.mint(address(adapter), TOKEN_IN_AMOUNT); - zeroFirst_.seedAllowance(address(adapter), address(metaSwap), 1); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(zeroFirst_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - adapter.swap(zeroFirst_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(zeroFirst_.balanceOf(address(adapter)), 0); - assertEq(zeroFirst_.allowance(address(adapter), address(metaSwap)), 0); - assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); - } - - function test_adapterRevertsExpiredQuote() public { - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - vm.warp(quote_.expiration); - - vm.expectRevert(DelegationMetaSwapAdapter2.ApiQuoteExpired.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsInvalidSignature() public { - (, uint256 wrongKey_) = makeAddrAndKey("wrong-signer"); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, wrongKey_); - - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidApiSignature.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsZeroInputAmount() public { - DelegationMetaSwapAdapter2.ApiQuote memory quote_; - - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAmount.selector); - adapter.swap(tokenIn, tokenOut, 0, MIN_TOKEN_OUT, quote_); - - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidZeroAmount.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, 0, quote_); - } - - function test_adapterRevertsUnexpectedInputBalance() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT + 1); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert( - abi.encodeWithSelector(DelegationMetaSwapAdapter2.UnexpectedInputBalance.selector, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT + 1) - ); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsInvalidApiSelector() public { - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = _signedQuote(hex"deadbeef", apiSignerKey); - - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidApiData.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsEmptyAggregatorId() public { - bytes memory swapData_ = _swapData(tokenIn, tokenOut, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false); - bytes memory apiData_ = abi.encodeWithSelector(IMetaSwap.swap.selector, "", tokenIn, TOKEN_IN_AMOUNT, swapData_); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = _signedQuote(apiData_, apiSignerKey); - - vm.expectRevert(DelegationMetaSwapAdapter2.InvalidApiData.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsOuterTokenMismatch() public { - BasicERC20 wrongToken_ = new BasicERC20(address(this), "Wrong", "WRONG", 0); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(wrongToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert(DelegationMetaSwapAdapter2.TokenInMismatch.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsOuterAmountMismatch() public { - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT - 1, TOKEN_IN_AMOUNT - 1, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert(DelegationMetaSwapAdapter2.AmountInMismatch.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsInnerTokenOutMismatch() public { - BasicERC20 wrongToken_ = new BasicERC20(address(this), "Wrong", "WRONG", 0); - bytes memory swapData_ = _swapData(tokenIn, wrongToken_, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _signedQuote(_apiData(tokenIn, TOKEN_IN_AMOUNT, swapData_), apiSignerKey); - - vm.expectRevert(DelegationMetaSwapAdapter2.TokenOutMismatch.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsInnerTokenInMismatch() public { - BasicERC20 wrongToken_ = new BasicERC20(address(this), "Wrong", "WRONG", 0); - bytes memory swapData_ = _swapData(wrongToken_, tokenOut, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _signedQuote(_apiData(tokenIn, TOKEN_IN_AMOUNT, swapData_), apiSignerKey); - - vm.expectRevert(DelegationMetaSwapAdapter2.TokenInMismatch.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsInputFeeMismatch() public { - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT - 2, ACTUAL_TOKEN_OUT, 1, false, apiSignerKey); - - vm.expectRevert(DelegationMetaSwapAdapter2.AmountInMismatch.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterAllowsFeeFromOutput() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT - 2, ACTUAL_TOKEN_OUT, 1, true, apiSignerKey); - - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - assertEq(tokenOut.balanceOf(address(this)), ACTUAL_TOKEN_OUT); - } - - function test_adapterRevertsQuotedOutputBelowMinimum() public { - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT - 1, 0, false, apiSignerKey); - - vm.expectRevert( - abi.encodeWithSelector(DelegationMetaSwapAdapter2.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) - ); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsActualOutputBelowMinimum() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - metaSwap.setBehavior(true, false, true, MIN_TOKEN_OUT - 1); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert( - abi.encodeWithSelector(DelegationMetaSwapAdapter2.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) - ); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsRemainingAllowance() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - metaSwap.setBehavior(false, false, false, 0); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.RemainingAllowance.selector, TOKEN_IN_AMOUNT)); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRevertsRemainingInputBalance() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - metaSwap.setBehavior(true, true, false, 0); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - vm.expectRevert(abi.encodeWithSelector(DelegationMetaSwapAdapter2.RemainingInputBalance.selector, 1)); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_enforcerRejectsInvalidBatchLength() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - Execution[] memory oneExecution_ = new Execution[](1); - oneExecution_[0] = executions_[0]; - - vm.prank(address(delegationManager)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-batch-length"); - enforcer.beforeHook( - terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(oneExecution_), keccak256("test"), address(0), address(0) - ); - } - - function test_enforcerTermsHelpers() public { - MetaSwapTransferSwapEnforcer.Terms memory expected_ = MetaSwapTransferSwapEnforcer.Terms({ - adapter: address(adapter), - tokenIn: address(tokenIn), - tokenOut: address(tokenOut), - tokenInAmount: TOKEN_IN_AMOUNT, - minTokenOut: MIN_TOKEN_OUT - }); - - bytes memory encoded_ = enforcer.encodeTerms(expected_); - MetaSwapTransferSwapEnforcer.Terms memory decoded_ = enforcer.getTermsInfo(encoded_); - - assertEq(decoded_.adapter, expected_.adapter); - assertEq(decoded_.tokenIn, expected_.tokenIn); - assertEq(decoded_.tokenOut, expected_.tokenOut); - assertEq(decoded_.tokenInAmount, expected_.tokenInAmount); - assertEq(decoded_.minTokenOut, expected_.minTokenOut); - } - - function test_enforcerRejectsInvalidTerms() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - MetaSwapTransferSwapEnforcer.Terms memory termsData_ = abi.decode(terms_, (MetaSwapTransferSwapEnforcer.Terms)); - - termsData_.adapter = address(0); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-zero-address"); - _beforeHook(abi.encode(termsData_), batchDefaultMode, execution_, keccak256("zero-adapter")); - - termsData_.adapter = address(adapter); - termsData_.minTokenOut = 0; - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-zero-amount"); - _beforeHook(abi.encode(termsData_), batchDefaultMode, execution_, keccak256("zero-output")); - - termsData_.minTokenOut = MIN_TOKEN_OUT; - termsData_.tokenOut = address(tokenIn); - vm.expectRevert("MetaSwapTransferSwapEnforcer:identical-tokens"); - _beforeHook(abi.encode(termsData_), batchDefaultMode, execution_, keccak256("identical")); - } - - function test_enforcerRejectsTryExecutionMode() public { - vm.expectRevert("CaveatEnforcer:invalid-execution-type"); - _beforeHook(hex"", batchTryMode, hex"", keccak256("try-mode")); - } - - function test_enforcerAllowsNativeInputSingleCall() public { - (bytes memory terms_, bytes memory execution_) = _nativeInputOrder(TOKEN_IN_AMOUNT); - _beforeHook(terms_, singleDefaultMode, execution_, keccak256("native-single")); - } - - function test_enforcerRejectsWrongCallTypeForNativeAndErc20() public { - (bytes memory nativeTerms_, bytes memory nativeExecution_) = _nativeInputOrder(TOKEN_IN_AMOUNT); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-call-type"); - _beforeHook(nativeTerms_, batchDefaultMode, nativeExecution_, keccak256("native-batch")); - - (bytes memory erc20Terms_, bytes memory erc20Execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-call-type"); - _beforeHook(erc20Terms_, singleDefaultMode, erc20Execution_, keccak256("erc20-single")); - } - - function test_enforcerRejectsWrongNativeExecutionValue() public { - (bytes memory terms_, bytes memory execution_) = _nativeInputOrder(TOKEN_IN_AMOUNT - 1); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, singleDefaultMode, execution_, keccak256("native-value")); - } - - function test_enforcerRejectsZeroAmountTerms() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - MetaSwapTransferSwapEnforcer.Terms memory termsData_ = abi.decode(terms_, (MetaSwapTransferSwapEnforcer.Terms)); - termsData_.tokenInAmount = 0; - - vm.prank(address(delegationManager)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-zero-amount"); - enforcer.beforeHook(abi.encode(termsData_), hex"", batchDefaultMode, execution_, keccak256("test"), address(0), address(0)); - } - - function test_enforcerRejectsInvalidTransfer() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT - 1)); - - vm.prank(address(delegationManager)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); - enforcer.beforeHook( - terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) - ); - } - - function test_enforcerRejectsMalformedTransferExecutions() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - - executions_[0].target = address(tokenOut); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-target")); - - executions_[0].target = address(tokenIn); - executions_[0].value = 1; - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-value")); - - executions_[0].value = 0; - executions_[0].callData = hex"1234"; - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-length")); - - executions_[0].callData = abi.encodeCall(IERC20.approve, (address(adapter), TOKEN_IN_AMOUNT)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-selector")); - - executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(this), TOKEN_IN_AMOUNT)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-transfer-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("transfer-recipient")); - } - - function test_enforcerRejectsInvalidSwapBounds() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT - 1, quote_)); - - vm.prank(address(delegationManager)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - enforcer.beforeHook( - terms_, hex"", batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("test"), address(0), address(0) - ); - } - - function test_enforcerRejectsMalformedSwapExecutions() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - bytes memory validCallData_ = executions_[1].callData; - - executions_[1].target = address(metaSwap); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-target")); - - executions_[1].target = address(adapter); - executions_[1].value = 1; - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-value")); - - executions_[1].value = 0; - executions_[1].callData = hex"1234"; - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-length")); - - executions_[1].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-selector")); - - executions_[1].callData = validCallData_; - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-valid")); - } - - /// @notice Ensures the enforcer requires a complete canonical ABI head while leaving quote-body decoding to the adapter. - function test_enforcerRejectsMalformedQuoteHead() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - - executions_[1].callData = abi.encodePacked( - adapter.swap.selector, abi.encode(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, uint256(160)) - ); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("quote-truncated")); - - executions_[1].callData = abi.encodePacked( - adapter.swap.selector, - abi.encode(address(tokenIn), address(tokenOut), TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, uint256(0)), - new bytes(96) - ); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("quote-offset")); - } - - function test_enforcerRejectsMismatchedSwapArguments() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - - executions_[1].callData = abi.encodeCall(adapter.swap, (tokenOut, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-token-in")); - - executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenIn, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-token-out")); - - executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT - 1, MIN_TOKEN_OUT, quote_)); - vm.expectRevert("MetaSwapTransferSwapEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-amount")); - } - - function test_integrationRevertsWhenAdapterUnderpays() public { - (bytes memory terms_, bytes memory execution_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - Delegation memory delegation_ = _buildDelegation(automation, terms_, 80); - metaSwap.setBehavior(true, false, true, MIN_TOKEN_OUT - 1); - - vm.expectRevert( - abi.encodeWithSelector(DelegationMetaSwapAdapter2.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) - ); - _redeem(delegation_, execution_, automation); - } - - function test_integrationHappyPath() public { - (Delegation memory delegation_, bytes memory execution_) = _delegation(automation); - - _redeem(delegation_, execution_, automation); - - assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); - assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), ACTUAL_TOKEN_OUT); - assertEq(tokenIn.balanceOf(address(adapter)), 0); - assertEq(tokenIn.allowance(address(adapter), address(metaSwap)), 0); - } - - function test_integrationNativeInputHappyPath() public { - (bytes memory terms_, bytes memory execution_) = _nativeInputOrder(TOKEN_IN_AMOUNT); - Delegation memory delegation_ = _buildDelegation(automation, terms_, 78); - vm.deal(address(users.alice.deleGator), TOKEN_IN_AMOUNT); - - _redeemWithMode(delegation_, execution_, automation, ModeLib.encodeSimpleSingle()); - - assertEq(address(users.alice.deleGator).balance, 0); - assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), ACTUAL_TOKEN_OUT); - } - - function test_integrationNativeOutputHappyPath() public { - (bytes memory terms_, bytes memory execution_) = _nativeOutputOrder(); - Delegation memory delegation_ = _buildDelegation(automation, terms_, 79); - uint256 nativeBefore_ = address(users.alice.deleGator).balance; - - _redeemWithMode(delegation_, execution_, automation, ModeLib.encodeSimpleBatch()); - - assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); - assertEq(address(users.alice.deleGator).balance - nativeBefore_, ACTUAL_TOKEN_OUT); - } - - function test_integrationRejectsUnauthorizedRedeemer() public { - (Delegation memory delegation_, bytes memory execution_) = _delegation(ANY_DELEGATE); - - vm.expectRevert("RedeemerEnforcer:unauthorized-redeemer"); - _redeem(delegation_, execution_, users.bob.addr); - } - - function test_integrationRejectsReplay() public { - (Delegation memory delegation_, bytes memory execution_) = _delegation(automation); - _redeem(delegation_, execution_, automation); - - vm.expectRevert("MetaSwapTransferSwapEnforcer:delegation-already-used"); - _redeem(delegation_, execution_, automation); - } - - function _delegation(address _delegate) private view returns (Delegation memory delegation_, bytes memory execution_) { - (bytes memory terms_, bytes memory order_) = _validOrder(MIN_TOKEN_OUT, ACTUAL_TOKEN_OUT); - delegation_ = _buildDelegation(_delegate, terms_, 77); - execution_ = order_; - } - - function _buildDelegation( - address _delegate, - bytes memory _terms, - uint256 _salt - ) - private - view - returns (Delegation memory delegation_) - { - Caveat[] memory caveats_ = new Caveat[](2); - caveats_[0] = Caveat({ enforcer: address(enforcer), terms: _terms, args: hex"" }); - caveats_[1] = Caveat({ enforcer: address(redeemerEnforcer), terms: abi.encodePacked(automation), args: hex"" }); - - delegation_ = signDelegation( - users.alice, - Delegation({ - delegate: _delegate, - delegator: address(users.alice.deleGator), - authority: ROOT_AUTHORITY, - caveats: caveats_, - salt: _salt, - signature: hex"" - }) - ); - } - - function _validOrder( - uint256 _minOutput, - uint256 _quotedOutput - ) - private - view - returns (bytes memory terms_, bytes memory execution_) - { - MetaSwapTransferSwapEnforcer.Terms memory termsData_ = MetaSwapTransferSwapEnforcer.Terms({ - adapter: address(adapter), - tokenIn: address(tokenIn), - tokenOut: address(tokenOut), - tokenInAmount: TOKEN_IN_AMOUNT, - minTokenOut: MIN_TOKEN_OUT - }); - terms_ = abi.encode(termsData_); - - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, _quotedOutput, 0, false, apiSignerKey); - Execution[] memory executions_ = new Execution[](2); - executions_[0] = Execution({ - target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)) - }); - executions_[1] = Execution({ - target: address(adapter), - value: 0, - callData: abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT, _minOutput, quote_)) - }); - execution_ = ExecutionLib.encodeBatch(executions_); - } - - function _nativeInputOrder(uint256 _executionValue) private view returns (bytes memory terms_, bytes memory execution_) { - IERC20 nativeToken_ = IERC20(address(0)); - terms_ = abi.encode( - MetaSwapTransferSwapEnforcer.Terms({ - adapter: address(adapter), - tokenIn: address(0), - tokenOut: address(tokenOut), - tokenInAmount: TOKEN_IN_AMOUNT, - minTokenOut: MIN_TOKEN_OUT - }) - ); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - execution_ = ExecutionLib.encodeSingle( - address(adapter), - _executionValue, - abi.encodeCall(adapter.swap, (nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)) - ); - } - - function _nativeOutputOrder() private view returns (bytes memory terms_, bytes memory execution_) { - IERC20 nativeToken_ = IERC20(address(0)); - terms_ = abi.encode( - MetaSwapTransferSwapEnforcer.Terms({ - adapter: address(adapter), - tokenIn: address(tokenIn), - tokenOut: address(0), - tokenInAmount: TOKEN_IN_AMOUNT, - minTokenOut: MIN_TOKEN_OUT - }) - ); - DelegationMetaSwapAdapter2.ApiQuote memory quote_ = - _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, TOKEN_IN_AMOUNT, ACTUAL_TOKEN_OUT, 0, false, apiSignerKey); - Execution[] memory executions_ = new Execution[](2); - executions_[0] = Execution({ - target: address(tokenIn), value: 0, callData: abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)) - }); - executions_[1] = Execution({ - target: address(adapter), - value: 0, - callData: abi.encodeCall(adapter.swap, (tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)) - }); - execution_ = ExecutionLib.encodeBatch(executions_); - } - - function _beforeHook(bytes memory _terms, ModeCode _mode, bytes memory _execution, bytes32 _hash) private { - vm.prank(address(delegationManager)); - enforcer.beforeHook(_terms, hex"", _mode, _execution, _hash, address(users.alice.deleGator), address(0)); - } - - function _quote( - IERC20 _outerTokenIn, - IERC20 _innerTokenOut, - uint256 _outerAmountIn, - uint256 _innerAmountIn, - uint256 _quotedOutput, - uint256 _fee, - bool _feeFromOutput, - uint256 _signerKey - ) - private - view - returns (DelegationMetaSwapAdapter2.ApiQuote memory) - { - bytes memory swapData_ = _swapData(_outerTokenIn, _innerTokenOut, _innerAmountIn, _quotedOutput, _fee, _feeFromOutput); - return _signedQuote(_apiData(_outerTokenIn, _outerAmountIn, swapData_), _signerKey); - } - - function _signedQuote( - bytes memory _apiDataValue, - uint256 _signerKey - ) - private - view - returns (DelegationMetaSwapAdapter2.ApiQuote memory quote_) - { - quote_.apiData = _apiDataValue; - quote_.expiration = block.timestamp + 5 minutes; - (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_signerKey, adapter.getQuoteDigest(quote_.apiData, quote_.expiration)); - quote_.signature = abi.encodePacked(r_, s_, v_); - } - - function _apiData(IERC20 _token, uint256 _amount, bytes memory _swapDataValue) private pure returns (bytes memory) { - return abi.encodeWithSelector(IMetaSwap.swap.selector, "mock-aggregator", _token, _amount, _swapDataValue); - } - - function _swapData( - IERC20 _input, - IERC20 _output, - uint256 _amountIn, - uint256 _amountOut, - uint256 _fee, - bool _feeFromOutput - ) - private - pure - returns (bytes memory) - { - return abi.encode(_input, _output, _amountIn, _amountOut, hex"", _fee, address(0), _feeFromOutput); - } - - function _redeem(Delegation memory _delegationValue, bytes memory _execution, address _redeemer) private { - _redeemWithMode(_delegationValue, _execution, _redeemer, ModeLib.encodeSimpleBatch()); - } - - function _redeemWithMode( - Delegation memory _delegationValue, - bytes memory _execution, - address _redeemer, - ModeCode _mode - ) - private - { - Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = _delegationValue; - bytes[] memory contexts_ = new bytes[](1); - contexts_[0] = abi.encode(delegations_); - ModeCode[] memory modes_ = new ModeCode[](1); - modes_[0] = _mode; - bytes[] memory executions_ = new bytes[](1); - executions_[0] = _execution; - - vm.prank(_redeemer); - delegationManager.redeemDelegations(contexts_, modes_, executions_); - } -} diff --git a/test/helpers/MetaSwapForwardingAdapter.t.sol b/test/helpers/MetaSwapForwardingAdapter.t.sol deleted file mode 100644 index 6517ffa7..00000000 --- a/test/helpers/MetaSwapForwardingAdapter.t.sol +++ /dev/null @@ -1,486 +0,0 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; -import { ModeLib } from "@erc7579/lib/ModeLib.sol"; - -import { MetaSwapPrefundEnforcer } from "../../src/enforcers/MetaSwapPrefundEnforcer.sol"; -import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; -import { MetaSwapForwardingAdapter } from "../../src/helpers/MetaSwapForwardingAdapter.sol"; -import { IMetaSwap } from "../../src/helpers/interfaces/IMetaSwap.sol"; -import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; -import { Caveat, Delegation, Execution, ModeCode } from "../../src/utils/Types.sol"; -import { CaveatEnforcerBaseTest } from "../enforcers/CaveatEnforcerBaseTest.t.sol"; -import { BasicERC20 } from "../utils/BasicERC20.t.sol"; - -contract ForwardingMetaSwapMock is IMetaSwap { - using SafeERC20 for IERC20; - - bool internal skipInputPull; - bool internal refundInput; - bool internal useOutputOverride; - bool internal forceRevert; - uint256 internal outputOverride; - - error MockSwapFailed(); - error InvalidValue(); - error NativeTransferFailed(); - - receive() external payable { } - - function setBehavior( - bool _skipInputPull, - bool _refundInput, - bool _useOutputOverride, - uint256 _outputOverride, - bool _forceRevert - ) - external - { - skipInputPull = _skipInputPull; - refundInput = _refundInput; - useOutputOverride = _useOutputOverride; - outputOverride = _outputOverride; - forceRevert = _forceRevert; - } - - function swap(string calldata, IERC20 _tokenIn, uint256 _amountIn, bytes calldata _swapData) external payable { - if (forceRevert) revert MockSwapFailed(); - - (IERC20 tokenOut_, uint256 quotedOutput_) = abi.decode(_swapData, (IERC20, uint256)); - - if (address(_tokenIn) == address(0)) { - if (msg.value != _amountIn) revert InvalidValue(); - if (refundInput) { - (bool refundSuccess_,) = msg.sender.call{ value: 1 }(""); - if (!refundSuccess_) revert NativeTransferFailed(); - } - } else { - if (msg.value != 0) revert InvalidValue(); - if (!skipInputPull) _tokenIn.safeTransferFrom(msg.sender, address(this), _amountIn); - if (refundInput) _tokenIn.safeTransfer(msg.sender, 1); - } - - uint256 output_ = useOutputOverride ? outputOverride : quotedOutput_; - if (address(tokenOut_) == address(0)) { - (bool success_,) = msg.sender.call{ value: output_ }(""); - if (!success_) revert NativeTransferFailed(); - } else { - tokenOut_.safeTransfer(msg.sender, output_); - } - } - - function setAdapter(string calldata, address, bytes4, bytes calldata) external { } - function removeAdapter(string calldata) external { } - - function adapters(string memory) external pure returns (Adapter memory) { - return Adapter({ addr: address(0), selector: bytes4(0), data: hex"" }); - } -} - -contract MetaSwapForwardingAdapterTest is CaveatEnforcerBaseTest { - uint256 internal constant TOKEN_IN_AMOUNT = 100 ether; - uint256 internal constant MIN_TOKEN_OUT = 190 ether; - uint256 internal constant TOKEN_OUT_AMOUNT = 200 ether; - - BasicERC20 internal tokenIn; - BasicERC20 internal tokenOut; - ForwardingMetaSwapMock internal metaSwap; - MetaSwapForwardingAdapter internal adapter; - MetaSwapPrefundEnforcer internal enforcer; - RedeemerEnforcer internal redeemerEnforcer; - - address internal automation; - address internal apiSigner; - uint256 internal apiSignerKey; - - function setUp() public override { - super.setUp(); - - tokenIn = new BasicERC20(address(this), "Token In", "TIN", 0); - tokenOut = new BasicERC20(address(this), "Token Out", "TOUT", 0); - metaSwap = new ForwardingMetaSwapMock(); - (apiSigner, apiSignerKey) = makeAddrAndKey("forwarding-api-signer"); - adapter = new MetaSwapForwardingAdapter(address(this), apiSigner, metaSwap); - enforcer = new MetaSwapPrefundEnforcer(adapter); - redeemerEnforcer = new RedeemerEnforcer(); - automation = makeAddr("forwarding-automation"); - - tokenIn.mint(address(users.alice.deleGator), TOKEN_IN_AMOUNT); - tokenOut.mint(address(metaSwap), 10_000 ether); - vm.deal(address(metaSwap), 10_000 ether); - vm.deal(address(users.alice.deleGator), TOKEN_IN_AMOUNT); - } - - receive() external payable { } - - function _getEnforcer() internal view override returns (ICaveatEnforcer) { - return ICaveatEnforcer(address(enforcer)); - } - - function test_adapterForwardsExactApiDataForErc20Input() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - vm.expectCall(address(metaSwap), quote_.apiData); - - uint256 output_ = adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(output_, TOKEN_OUT_AMOUNT); - assertEq(tokenOut.balanceOf(address(this)), TOKEN_OUT_AMOUNT); - assertEq(tokenIn.balanceOf(address(adapter)), 0); - assertEq(tokenIn.allowance(address(adapter), address(metaSwap)), 0); - } - - function test_adapterForwardsPrefundedNativeInput() public { - IERC20 nativeToken_ = IERC20(address(0)); - vm.deal(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - vm.expectCall(address(metaSwap), quote_.apiData); - - adapter.swap(nativeToken_, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(tokenOut.balanceOf(address(this)), TOKEN_OUT_AMOUNT); - assertEq(address(adapter).balance, 0); - } - - function test_adapterForwardsNativeOutput() public { - IERC20 nativeToken_ = IERC20(address(0)); - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - uint256 balanceBefore_ = address(this).balance; - - adapter.swap(tokenIn, nativeToken_, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - assertEq(address(this).balance - balanceBefore_, TOKEN_OUT_AMOUNT); - } - - function test_adapterRejectsInvalidApiSelector() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _signedQuote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, hex"deadbeef", apiSignerKey); - - vm.expectRevert(MetaSwapForwardingAdapter.InvalidApiData.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRejectsManifestTampering() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - - vm.expectRevert(MetaSwapForwardingAdapter.InvalidApiSignature.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT - 1, quote_); - } - - function test_adapterRejectsExpiredAndInvalidSignatures() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory expiredQuote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - vm.warp(expiredQuote_.expiration); - vm.expectRevert(MetaSwapForwardingAdapter.ApiQuoteExpired.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, expiredQuote_); - - (, uint256 wrongSignerKey_) = makeAddrAndKey("wrong-forwarding-signer"); - MetaSwapForwardingAdapter.ApiQuote memory invalidQuote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, wrongSignerKey_); - vm.expectRevert(MetaSwapForwardingAdapter.InvalidApiSignature.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, invalidQuote_); - } - - function test_adapterRejectsInvalidInputState() public { - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - - vm.expectRevert(abi.encodeWithSelector(MetaSwapForwardingAdapter.UnexpectedInputBalance.selector, TOKEN_IN_AMOUNT, 0)); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - vm.expectRevert(MetaSwapForwardingAdapter.IdenticalTokens.selector); - adapter.swap(tokenIn, tokenIn, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterRejectsInsufficientOutputAndResidualInput() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - - metaSwap.setBehavior(false, false, true, MIN_TOKEN_OUT - 1, false); - vm.expectRevert( - abi.encodeWithSelector(MetaSwapForwardingAdapter.InsufficientOutput.selector, MIN_TOKEN_OUT, MIN_TOKEN_OUT - 1) - ); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - - metaSwap.setBehavior(false, true, false, 0, false); - vm.expectRevert(abi.encodeWithSelector(MetaSwapForwardingAdapter.RemainingInputBalance.selector, 1)); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_adapterBubblesMetaSwapRevert() public { - tokenIn.mint(address(adapter), TOKEN_IN_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - metaSwap.setBehavior(false, false, false, 0, true); - - vm.expectRevert(ForwardingMetaSwapMock.MockSwapFailed.selector); - adapter.swap(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_); - } - - function test_enforcerAllowsErc20AndNativePrefundBatches() public { - (bytes memory erc20Terms_, bytes memory erc20Execution_) = - _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - _beforeHook(erc20Terms_, batchDefaultMode, erc20Execution_, keccak256("erc20-prefund")); - - (bytes memory nativeTerms_, bytes memory nativeExecution_) = - _order(IERC20(address(0)), tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - _beforeHook(nativeTerms_, batchDefaultMode, nativeExecution_, keccak256("native-prefund")); - } - - function test_enforcerRejectsWrongCallTypeAndBatchLength() public { - (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-call-type"); - _beforeHook(terms_, singleDefaultMode, execution_, keccak256("single")); - - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - Execution[] memory shortBatch_ = new Execution[](1); - shortBatch_[0] = executions_[0]; - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-batch-length"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(shortBatch_), keccak256("short")); - } - - function test_enforcerRejectsMalformedErc20Prefund() public { - (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - - executions_[0].target = address(tokenOut); - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("prefund-target")); - - executions_[0].target = address(tokenIn); - executions_[0].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT - 1)); - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("prefund-amount")); - - executions_[0].callData = abi.encodeCall(IERC20.approve, (address(adapter), TOKEN_IN_AMOUNT)); - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("prefund-selector")); - } - - function test_enforcerRejectsMalformedNativePrefund() public { - (bytes memory terms_, bytes memory execution_) = - _order(IERC20(address(0)), tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - - executions_[0].value = TOKEN_IN_AMOUNT - 1; - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("native-value")); - - executions_[0].value = TOKEN_IN_AMOUNT; - executions_[0].callData = hex"00"; - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-prefund-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("native-data")); - } - - function test_enforcerRejectsMalformedSwapCall() public { - (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - bytes memory validSwapCall_ = executions_[1].callData; - - executions_[1].target = address(metaSwap); - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-target")); - - executions_[1].target = address(adapter); - executions_[1].callData = abi.encodeCall(IERC20.transfer, (address(adapter), TOKEN_IN_AMOUNT)); - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-selector")); - - executions_[1].callData = validSwapCall_; - executions_[1].value = 1; - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-value")); - } - - function test_enforcerRejectsSwapInputMismatch() public { - (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - Execution[] memory executions_ = abi.decode(execution_, (Execution[])); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - - executions_[1].callData = abi.encodeCall(adapter.swap, (tokenOut, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, quote_)); - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-token")); - - executions_[1].callData = abi.encodeCall(adapter.swap, (tokenIn, tokenOut, TOKEN_IN_AMOUNT - 1, MIN_TOKEN_OUT, quote_)); - vm.expectRevert("MetaSwapPrefundEnforcer:invalid-swap-call"); - _beforeHook(terms_, batchDefaultMode, ExecutionLib.encodeBatch(executions_), keccak256("swap-amount")); - } - - function test_enforcerRejectsReplay() public { - (bytes memory terms_, bytes memory execution_) = _order(tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - bytes32 delegationHash_ = keccak256("prefund-replay"); - - _beforeHook(terms_, batchDefaultMode, execution_, delegationHash_); - vm.expectRevert("MetaSwapPrefundEnforcer:delegation-already-used"); - _beforeHook(terms_, batchDefaultMode, execution_, delegationHash_); - } - - function test_integrationErc20PrefundAndForward() public { - (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) = _delegation(tokenIn); - vm.expectCall(address(metaSwap), apiData_); - - _redeem(delegation_, execution_, automation); - - assertEq(tokenIn.balanceOf(address(users.alice.deleGator)), 0); - assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), TOKEN_OUT_AMOUNT); - } - - function test_integrationNativePrefundAndForward() public { - (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) = _delegation(IERC20(address(0))); - vm.expectCall(address(metaSwap), apiData_); - - _redeem(delegation_, execution_, automation); - - assertEq(address(adapter).balance, 0); - assertEq(tokenOut.balanceOf(address(users.alice.deleGator)), TOKEN_OUT_AMOUNT); - } - - function test_integrationRejectsUnauthorizedRedeemer() public { - (Delegation memory delegation_, bytes memory execution_,) = _delegationFor(ANY_DELEGATE, tokenIn); - - vm.expectRevert("RedeemerEnforcer:unauthorized-redeemer"); - _redeem(delegation_, execution_, users.bob.addr); - } - - function _delegation(IERC20 _tokenIn) - private - view - returns (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) - { - return _delegationFor(automation, _tokenIn); - } - - function _delegationFor( - address _delegate, - IERC20 _tokenIn - ) - private - view - returns (Delegation memory delegation_, bytes memory execution_, bytes memory apiData_) - { - (bytes memory terms_, bytes memory order_) = _order(_tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT); - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(_tokenIn, tokenOut, TOKEN_IN_AMOUNT, MIN_TOKEN_OUT, TOKEN_OUT_AMOUNT, apiSignerKey); - - Caveat[] memory caveats_ = new Caveat[](2); - caveats_[0] = Caveat({ enforcer: address(enforcer), terms: terms_, args: hex"" }); - caveats_[1] = Caveat({ enforcer: address(redeemerEnforcer), terms: abi.encodePacked(automation), args: hex"" }); - - delegation_ = signDelegation( - users.alice, - Delegation({ - delegate: _delegate, - delegator: address(users.alice.deleGator), - authority: ROOT_AUTHORITY, - caveats: caveats_, - salt: 1, - signature: hex"" - }) - ); - execution_ = order_; - apiData_ = quote_.apiData; - } - - function _order( - IERC20 _tokenIn, - IERC20 _tokenOut, - uint256 _tokenInAmount, - uint256 _minTokenOut, - uint256 _tokenOutAmount - ) - private - view - returns (bytes memory terms_, bytes memory execution_) - { - MetaSwapForwardingAdapter.ApiQuote memory quote_ = - _quote(_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, _tokenOutAmount, apiSignerKey); - - terms_ = abi.encode(MetaSwapPrefundEnforcer.Terms({ tokenIn: address(_tokenIn), tokenInAmount: _tokenInAmount })); - - Execution[] memory executions_ = new Execution[](2); - if (address(_tokenIn) == address(0)) { - executions_[0] = Execution({ target: address(adapter), value: _tokenInAmount, callData: hex"" }); - } else { - executions_[0] = Execution({ - target: address(_tokenIn), value: 0, callData: abi.encodeCall(IERC20.transfer, (address(adapter), _tokenInAmount)) - }); - } - executions_[1] = Execution({ - target: address(adapter), - value: 0, - callData: abi.encodeCall(adapter.swap, (_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, quote_)) - }); - execution_ = ExecutionLib.encodeBatch(executions_); - } - - function _quote( - IERC20 _tokenIn, - IERC20 _tokenOut, - uint256 _tokenInAmount, - uint256 _minTokenOut, - uint256 _tokenOutAmount, - uint256 _signerKey - ) - private - view - returns (MetaSwapForwardingAdapter.ApiQuote memory quote_) - { - bytes memory apiData_ = abi.encodeCall( - IMetaSwap.swap, ("forwarding-aggregator", _tokenIn, _tokenInAmount, abi.encode(_tokenOut, _tokenOutAmount)) - ); - quote_ = _signedQuote(_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, apiData_, _signerKey); - } - - function _signedQuote( - IERC20 _tokenIn, - IERC20 _tokenOut, - uint256 _tokenInAmount, - uint256 _minTokenOut, - bytes memory _apiData, - uint256 _signerKey - ) - private - view - returns (MetaSwapForwardingAdapter.ApiQuote memory quote_) - { - quote_.apiData = _apiData; - quote_.expiration = block.timestamp + 5 minutes; - bytes32 digest_ = - adapter.getQuoteDigest(_tokenIn, _tokenOut, _tokenInAmount, _minTokenOut, quote_.apiData, quote_.expiration); - (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_signerKey, digest_); - quote_.signature = abi.encodePacked(r_, s_, v_); - } - - function _beforeHook(bytes memory _terms, ModeCode _mode, bytes memory _execution, bytes32 _hash) private { - vm.prank(address(delegationManager)); - enforcer.beforeHook(_terms, hex"", _mode, _execution, _hash, address(users.alice.deleGator), address(0)); - } - - function _redeem(Delegation memory _delegationValue, bytes memory _execution, address _redeemer) private { - Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = _delegationValue; - bytes[] memory contexts_ = new bytes[](1); - contexts_[0] = abi.encode(delegations_); - ModeCode[] memory modes_ = new ModeCode[](1); - modes_[0] = ModeLib.encodeSimpleBatch(); - bytes[] memory executions_ = new bytes[](1); - executions_[0] = _execution; - - vm.prank(_redeemer); - delegationManager.redeemDelegations(contexts_, modes_, executions_); - } -} From 47373df0d5d60f24bbfb2e953adb00ee107cd582 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 11 Sep 2026 16:19:54 +0200 Subject: [PATCH 13/13] fix: correct experiment manager import paths --- src/experiments/GaslessSwapDelegationManager.sol | 12 ++++++------ .../MetaSwapMinimalDelegationManager.sol | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/experiments/GaslessSwapDelegationManager.sol b/src/experiments/GaslessSwapDelegationManager.sol index 9e1da123..b9f0fa07 100644 --- a/src/experiments/GaslessSwapDelegationManager.sol +++ b/src/experiments/GaslessSwapDelegationManager.sol @@ -6,12 +6,12 @@ import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import { ICaveatEnforcer } from "./interfaces/ICaveatEnforcer.sol"; -import { IDelegationManager } from "./interfaces/IDelegationManager.sol"; -import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; -import { EncoderLib } from "./libraries/EncoderLib.sol"; -import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; -import { Caveat, Delegation, ModeCode } from "./utils/Types.sol"; +import { ICaveatEnforcer } from "../interfaces/ICaveatEnforcer.sol"; +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IDeleGatorCore } from "../interfaces/IDeleGatorCore.sol"; +import { EncoderLib } from "../libraries/EncoderLib.sol"; +import { ERC1271Lib } from "../libraries/ERC1271Lib.sol"; +import { Caveat, Delegation, ModeCode } from "../utils/Types.sol"; /** * @title GaslessSwapDelegationManager diff --git a/src/experiments/MetaSwapMinimalDelegationManager.sol b/src/experiments/MetaSwapMinimalDelegationManager.sol index 96463152..8632f1b5 100644 --- a/src/experiments/MetaSwapMinimalDelegationManager.sol +++ b/src/experiments/MetaSwapMinimalDelegationManager.sol @@ -9,13 +9,13 @@ import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/Mes import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; -import { IDelegationManager } from "./interfaces/IDelegationManager.sol"; -import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; -import { IMetaSwap } from "./helpers/interfaces/IMetaSwap.sol"; -import { EncoderLib } from "./libraries/EncoderLib.sol"; -import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; -import { CallType, Caveat, Delegation, Execution, ExecType, ModeCode } from "./utils/Types.sol"; -import { CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT } from "./utils/Constants.sol"; +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IDeleGatorCore } from "../interfaces/IDeleGatorCore.sol"; +import { IMetaSwap } from "../helpers/interfaces/IMetaSwap.sol"; +import { EncoderLib } from "../libraries/EncoderLib.sol"; +import { ERC1271Lib } from "../libraries/ERC1271Lib.sol"; +import { CallType, Caveat, Delegation, Execution, ExecType, ModeCode } from "../utils/Types.sol"; +import { CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT } from "../utils/Constants.sol"; /** * @title MetaSwapMinimalDelegationManager