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)); + } +}