diff --git a/README.md b/README.md index 18876b6c..ba16b078 100644 --- a/README.md +++ b/README.md @@ -118,9 +118,11 @@ Note that some of the commands in the middle of the series are unused. These gap `UNWRAP_WETH_EXACT` takes `(recipient, amount)` and unwraps an exact amount of the contract's WETH, reverting if the balance is insufficient. +`V2_SWAP_EXACT_OUT` is all-or-nothing: it measures what the recipient actually receives and reverts with `V2TooLittleReceived` if that is less than `amountOut`, even by 1 wei. A token anywhere on the path that delivers less than the amount transferred, whether through a transfer fee, withholding, or share-based rounding such as stETH (which typically delivers 1-2 wei less), therefore makes the route revert. Swap such tokens with `V2_SWAP_EXACT_IN` instead. + `V4_SWAP` takes an encoded v4 action plan `(bytes actions, bytes[] params)` and runs it through the router's `V4Router`. Exact-output swaps are all-or-nothing and revert on a shortfall. An exact-output swap paid from the router's own balance (`payerIsUser = false`, or a native input) spends only what the route actually costs, so whatever the plan pre-funded beyond that stays in the router. Such a plan must end with a full-balance return such as `SWEEP` for each input currency, since any balance left in the router can be swept by anyone. A hook that funds the swap's input itself is the extreme case: the route costs nothing, so the entire pre-funded amount stays in the router and must be swept back. -`RESOLVE` takes `(address resolver, bytes context)` and performs a bounded, read-only `staticcall` to `resolver.resolveAmount(context)` (see `IAmountResolver`), storing the returned value in a transient register. A later command in the same plan can then use that value as an amount by passing the `Constants.USE_RESOLVED_AMOUNT` sentinel (`type(uint128).max`, the top of the `uint128` v4 amount range) in place of a literal. This is supported by the v2/v3 swap amount fields, the `amountIn`/`amountOut` of every v4 swap action inside `V4_SWAP`, and `TRANSFER`. This lets a route obtain an amount that is only known onchain at execution time (for example, a live lending-position debt) instead of baking an offchain guess into calldata. The register is transaction-scoped: it survives across commands and into an `EXECUTE_SUB_PLAN`, and is cleared when the top-level `execute` returns. Because the resolver is invoked via `staticcall` and at most one word of returndata is copied, a resolver can neither mutate state nor grief the router with a return bomb. A resolver that reverts, returns fewer than 32 bytes, or returns zero fails the command under the usual `FLAG_ALLOW_REVERT` semantics: zero is not an amount any command can act on, and the v4 swap helpers would read it as `OPEN_DELTA`. Every failure clears the register, and a command that consumes the sentinel while the register is empty reverts with `ResolvedAmountUnset`, so a tolerated RESOLVE failure can only be followed by consumers inside an `EXECUTE_SUB_PLAN` that also allows revert. +`RESOLVE` takes `(address resolver, bytes context)` and performs a bounded, read-only `staticcall` to `resolver.resolveAmount(context)` (see `IAmountResolver`), storing the returned value in a transient register. A later command in the same plan can then use that value as an amount by passing the `Constants.USE_RESOLVED_AMOUNT` sentinel (`type(uint128).max`, the top of the `uint128` v4 amount range) in place of a literal. This is supported by every exact amount the router moves: the `amountIn`/`amountOut` of the v2/v3 swap commands and of every v4 swap action inside `V4_SWAP`, the `amount` of the v4 `SETTLE` and `TAKE` actions, the `amount` of `PERMIT2_TRANSFER_FROM` and of each `PERMIT2_TRANSFER_FROM_BATCH` detail, the value of `TRANSFER`, the `amount` of `WRAP_ETH` and `UNWRAP_WETH_EXACT`, and the `inputAmount` of `ACROSS_V4_DEPOSIT_V3`. Minimums, caps and thresholds (`amountOutMin`, `amountInMax`, `SWEEP`'s `amountMin`, `UNWRAP_WETH`'s `amountMin`, `BALANCE_CHECK_ERC20`'s `minBalance`, and the v4 `SETTLE_ALL`, `TAKE_ALL` and `CLEAR_OR_TAKE` bounds), portions (`PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`, v4 `TAKE_PORTION`) and signed Permit2 permit amounts never read the sentinel. For `ACROSS_V4_DEPOSIT_V3` only the input is resolved: `outputAmount` stays the literal set when the route was built, so as with a `CONTRACT_BALANCE` input, leave enough room between the expected input and `outputAmount` for the relayer fee, or a lower-than-expected input can leave the deposit unfilled until `fillDeadline`. This lets a route obtain an amount that is only known onchain at execution time (for example, a live lending-position debt) instead of baking an offchain guess into calldata. One register is shared by a plan and all of its sub-plans, and every consumer reads the latest value written: a sub-plan starts with its parent's value, and a `RESOLVE` inside a sub-plan that succeeds replaces the parent's value for the parent's later commands. If an `EXECUTE_SUB_PLAN` with `FLAG_ALLOW_REVERT` fails, all of the sub-plan's writes are undone with it, its `RESOLVE` included, so the parent keeps the value it had before the sub-plan ran. The register is cleared when the top-level `execute` returns. Because the resolver is invoked via `staticcall` and at most one word of returndata is copied, a resolver can neither mutate state nor grief the router with a return bomb. A resolver that reverts, returns fewer than 32 bytes, or returns zero fails the command under the usual `FLAG_ALLOW_REVERT` semantics: zero is not an amount any command can act on, and the v4 swap helpers would read it as `OPEN_DELTA`. Every failure clears the register (unless it happens inside a sub-plan that then reverts, which undoes the clear along with everything else the sub-plan did), and a command that consumes the sentinel while the register is empty reverts with `ResolvedAmountUnset`, so a tolerated RESOLVE failure can only be followed by consumers inside an `EXECUTE_SUB_PLAN` that also allows revert. `V4_PROTOCOL_FEE_UPDATE` takes `(PoolKey key)` and `V3_PROTOCOL_FEE_UPDATE` takes `(address pool)`. Each pokes the protocol fee adapter that governance installed as the controller (the v4 `PoolManager.protocolFeeController()`, or the v3 factory `owner()`), read onchain so the router holds no adapter address, which pushes that pool's resolved protocol fee into pool state. Newly created pools initialize with no protocol fee, so a route can activate it in the same transaction as the first swap rather than waiting on the offchain keeper. The poke follows the same failure semantics as the other call-based commands: a revert from the adapter surfaces as `ExecutionFailed` unless `FLAG_ALLOW_REVERT` is set. Where the protocol is deployed but no adapter is installed, the call targets `address(0)` and is a no-op. Reading the controller is not covered by `FLAG_ALLOW_REVERT`: on a chain without a v4 `PoolManager` (or v3 factory) the command reverts the whole `execute`, so only include it on chains where that protocol is deployed. diff --git a/audit/Universal Router 2.3.0 And V4 Periphery Diff Audit.pdf b/audit/Universal Router 2.3.0 And V4 Periphery Diff Audit.pdf new file mode 100644 index 00000000..f919d985 Binary files /dev/null and b/audit/Universal Router 2.3.0 And V4 Periphery Diff Audit.pdf differ diff --git a/audit/Universal Router SwapProxy And Periphery Audit.pdf b/audit/Universal Router SwapProxy And Periphery Audit.pdf new file mode 100644 index 00000000..bec5fb08 Binary files /dev/null and b/audit/Universal Router SwapProxy And Periphery Audit.pdf differ diff --git a/contracts/base/Dispatcher.sol b/contracts/base/Dispatcher.sol index 1e7cd898..66ddc2ea 100644 --- a/contracts/base/Dispatcher.sol +++ b/contracts/base/Dispatcher.sol @@ -16,10 +16,10 @@ import {IAmountResolver} from '../interfaces/IAmountResolver.sol'; import {IV4FeeAdapter} from '../interfaces/external/IV4FeeAdapter.sol'; import {IV3FeeAdapter} from '../interfaces/external/IV3FeeAdapter.sol'; import {IUniswapV3Factory} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol'; -import {IProtocolFees} from '@uniswap/v4-core/src/interfaces/IProtocolFees.sol'; import {Lock} from './Lock.sol'; import {ERC20} from 'solmate/src/tokens/ERC20.sol'; import {IAllowanceTransfer} from 'permit2/src/interfaces/IAllowanceTransfer.sol'; +import {SafeCast160} from 'permit2/src/libraries/SafeCast160.sol'; import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol'; import {PoolKey} from '@uniswap/v4-core/src/types/PoolKey.sol'; import {IPoolManager} from '@uniswap/v4-core/src/interfaces/IPoolManager.sol'; @@ -136,7 +136,12 @@ abstract contract Dispatcher is recipient := calldataload(add(inputs.offset, 0x20)) amount := calldataload(add(inputs.offset, 0x40)) } - permit2TransferFrom(token, msgSender(), _mapRecipient(recipient), amount); + permit2TransferFrom( + token, + msgSender(), + _mapRecipient(recipient), + SafeCast160.toUint160(ResolvedAmount.map(amount)) + ); } else if (command == Commands.PERMIT2_PERMIT_BATCH) { IAllowanceTransfer.PermitBatch calldata permitBatch = decodePermitBatch(inputs); bytes calldata data = inputs.toBytes(1); @@ -284,7 +289,7 @@ abstract contract Dispatcher is recipient := calldataload(inputs.offset) amount := calldataload(add(inputs.offset, 0x20)) } - Payments.wrapETH(_mapRecipient(recipient), amount); + Payments.wrapETH(_mapRecipient(recipient), ResolvedAmount.map(amount)); } else if (command == Commands.UNWRAP_WETH) { checkInputLength(inputs, 0x40); // equivalent: abi.decode(inputs, (address, uint256)) @@ -325,7 +330,7 @@ abstract contract Dispatcher is recipient := calldataload(inputs.offset) amount := calldataload(add(inputs.offset, 0x20)) } - Payments.unwrapWETH9Exact(_mapRecipient(recipient), amount); + Payments.unwrapWETH9Exact(_mapRecipient(recipient), ResolvedAmount.map(amount)); } } } else { @@ -389,7 +394,7 @@ abstract contract Dispatcher is // PoolManager as its protocolFeeController and exposes a permissionless poke that pushes the // resolved fee into pool state. Reading the controller onchain means the router never holds an // adapter address and follows any future controller change. - (success, output) = IProtocolFees(address(poolManager)).protocolFeeController() + (success, output) = poolManager.protocolFeeController() .call(abi.encodeCall(IV4FeeAdapter.triggerFeeUpdate, (poolKey))); } else if (command == Commands.V3_PROTOCOL_FEE_UPDATE) { checkInputLength(inputs, 0x20); @@ -442,7 +447,8 @@ abstract contract Dispatcher is /// that reverts, returns fewer than 32 bytes, or returns zero fails the command, composing with /// FLAG_ALLOW_REVERT like the other call-based commands: zero is not an amount any command can act /// on, and the v4 swap helpers would read it as OPEN_DELTA. Every failure clears the register, so - /// no later command can consume a value this RESOLVE did not produce. + /// no later command can consume a value this RESOLVE did not produce. A failure inside a sub-plan that + /// then reverts is undone with the rest of the sub-plan, so the earlier value returns as if it never ran. function _resolve(address resolver, bytes calldata context) private returns (bool ok) { bytes memory callData = abi.encodeWithSelector(IAmountResolver.resolveAmount.selector, context); uint256 result; diff --git a/contracts/base/RouteSigner.sol b/contracts/base/RouteSigner.sol index ec4e4587..f56e7768 100644 --- a/contracts/base/RouteSigner.sol +++ b/contracts/base/RouteSigner.sol @@ -3,22 +3,11 @@ pragma solidity ^0.8.24; import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; +import {ROUTE_SIGNER_SLOT, ROUTE_INTENT_SLOT, ROUTE_DATA_SLOT} from '../libraries/TransientSlots.sol'; /// @title RouteSigner /// @notice Contract for managing signed execution context using transient storage abstract contract RouteSigner is EIP712 { - /// @notice Transient storage slot for the route signer address - /// @dev Must equal TransientSlots.ROUTE_SIGNER; inline assembly only accepts a literal here - bytes32 internal constant ROUTE_SIGNER_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000003; - - /// @notice Transient storage slot for the route intent - /// @dev Must equal TransientSlots.ROUTE_INTENT - bytes32 internal constant ROUTE_INTENT_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000004; - - /// @notice Transient storage slot for the route data - /// @dev Must equal TransientSlots.ROUTE_DATA - bytes32 internal constant ROUTE_DATA_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000005; - /// @notice EIP712 typehash for signed execution bytes32 internal constant EXECUTE_SIGNED_TYPEHASH = keccak256( 'ExecuteSigned(bytes commands,bytes[] inputs,bytes32 intent,bytes32 data,address sender,bytes32 nonce,uint256 deadline)' diff --git a/contracts/libraries/Commands.sol b/contracts/libraries/Commands.sol index 191f48ed..69a4875b 100644 --- a/contracts/libraries/Commands.sol +++ b/contracts/libraries/Commands.sol @@ -8,7 +8,7 @@ library Commands { bytes1 internal constant FLAG_ALLOW_REVERT = 0x80; bytes1 internal constant COMMAND_TYPE_MASK = 0x7f; - // Command Types. Maximum supported command at this moment is 0x3f. + // Command types are 7 bits (COMMAND_TYPE_MASK); 0x40-0x5f is the highest assigned range, reserved for 3rd party integrations. // The commands are executed in nested if blocks to minimise gas consumption // Command Types where value<=0x07, executed in the first nested-if block diff --git a/contracts/libraries/Constants.sol b/contracts/libraries/Constants.sol index d8a2bfe6..0dad63d5 100644 --- a/contracts/libraries/Constants.sol +++ b/contracts/libraries/Constants.sol @@ -32,6 +32,9 @@ library Constants { /// @dev Sentinel placed in a command's amount field to consume the ResolvedAmount register written by a prior /// RESOLVE command. It must fit uint128: v4 swap amounts are uint128 and the ABI decoder rejects a larger word /// before the router sees it, the same constraint that makes OPEN_DELTA zero. The top of that range is the literal - /// amount given up. The amountInMaximum and amountOutMinimum caps are not amount fields and keep their meaning. + /// amount given up. It is read by every exact amount the router moves: the v2/v3 and v4 swap amountIn/amountOut, + /// the v4 SETTLE and TAKE amounts, the PERMIT2_TRANSFER_FROM and PERMIT2_TRANSFER_FROM_BATCH amounts, the + /// TRANSFER value, the WRAP_ETH and UNWRAP_WETH_EXACT amounts, and the ACROSS_V4_DEPOSIT_V3 inputAmount. + /// Minimums, caps, thresholds, portions and signed permit amounts keep their literal meaning. uint256 internal constant USE_RESOLVED_AMOUNT = type(uint128).max; } diff --git a/contracts/libraries/Locker.sol b/contracts/libraries/Locker.sol index 7273062d..d62b9a87 100644 --- a/contracts/libraries/Locker.sol +++ b/contracts/libraries/Locker.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; +import {LOCKER_SLOT} from './TransientSlots.sol'; + /// @notice A library to implement a reentrancy lock in transient storage. /// @dev Instead of storing a boolean, the locker's address is stored to allow the contract to know who locked the contract library Locker { - // The slot holding the locker state, transiently. Must equal TransientSlots.LOCKER; inline assembly only accepts - // a literal here. - bytes32 constant LOCKER_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000001; - function set(address locker) internal { // The locker is always msg.sender or address(0) so does not need to be cleaned assembly ('memory-safe') { diff --git a/contracts/libraries/MaxInputAmount.sol b/contracts/libraries/MaxInputAmount.sol index 428c7c22..77b9fe07 100644 --- a/contracts/libraries/MaxInputAmount.sol +++ b/contracts/libraries/MaxInputAmount.sol @@ -1,12 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; +import {MAX_AMOUNT_IN_SLOT} from './TransientSlots.sol'; + /// @notice A library used to store the maximum desired amount of input tokens for exact output swaps; used for checking slippage library MaxInputAmount { - // The slot holding the the maximum desired amount of input tokens, transiently. Must equal - // TransientSlots.MAX_AMOUNT_IN; inline assembly only accepts a literal here. - bytes32 constant MAX_AMOUNT_IN_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000002; - function set(uint256 maxAmountIn) internal { assembly ('memory-safe') { tstore(MAX_AMOUNT_IN_SLOT, maxAmountIn) diff --git a/contracts/libraries/NestedUnlock.sol b/contracts/libraries/NestedUnlock.sol index 2f5966c3..23f9aede 100644 --- a/contracts/libraries/NestedUnlock.sol +++ b/contracts/libraries/NestedUnlock.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; +import {NESTED_UNLOCK_SLOT} from './TransientSlots.sol'; + /// @notice A library to record, in transient storage, that the caller has opted into executing a /// route inside a PoolManager unlock opened by another contract. library NestedUnlock { - // The slot holding the opt-in state, transiently. Must equal TransientSlots.NESTED_UNLOCK; inline assembly only - // accepts a literal here. - bytes32 constant NESTED_UNLOCK_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000006; - function set(bool permitted) internal { assembly ('memory-safe') { tstore(NESTED_UNLOCK_SLOT, permitted) diff --git a/contracts/libraries/ResolvedAmount.sol b/contracts/libraries/ResolvedAmount.sol index 1386c7a0..d53bb390 100644 --- a/contracts/libraries/ResolvedAmount.sol +++ b/contracts/libraries/ResolvedAmount.sol @@ -2,21 +2,21 @@ pragma solidity ^0.8.24; import {Constants} from './Constants.sol'; +import {RESOLVED_AMOUNT_SLOT} from './TransientSlots.sol'; /// @notice A transient register holding an amount resolved onchain by a RESOLVE command, for a later /// command to consume via the Constants.USE_RESOLVED_AMOUNT sentinel in one of its amount fields. -/// @dev The register is transaction-scoped: RESOLVE writes it, subsequent commands (including those in -/// an EXECUTE_SUB_PLAN) read it, and the top-level execute clears it on exit so it never leaks into a -/// later execute in the same transaction. Zero means empty: RESOLVE clears the register whenever it -/// fails, and a resolver returning zero counts as a failure, so no command acts on a value no RESOLVE produced. +/// @dev One register is shared by a plan and all of its sub-plans, and every consumer reads the latest value +/// written. A sub-plan starts with its parent's value, and a RESOLVE inside a sub-plan that succeeds replaces the +/// parent's value for the parent's later commands. A sub-plan that reverts under FLAG_ALLOW_REVERT has all of its +/// writes undone, its RESOLVE included, so the parent keeps the value it had before the sub-plan ran. The +/// top-level execute clears the register on exit so it never leaks into a later execute in the same transaction. +/// Zero means empty: RESOLVE clears the register whenever it fails, and a resolver returning zero counts as a +/// failure, so no command acts on a value no RESOLVE produced. library ResolvedAmount { /// @notice Thrown when a command consumes the USE_RESOLVED_AMOUNT sentinel while the register is empty error ResolvedAmountUnset(); - // The slot holding the resolved amount, transiently. Must equal TransientSlots.RESOLVED_AMOUNT; inline assembly - // only accepts a literal here. - bytes32 constant RESOLVED_AMOUNT_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000007; - function set(uint256 amount) internal { assembly ('memory-safe') { tstore(RESOLVED_AMOUNT_SLOT, amount) diff --git a/contracts/libraries/TransientSlots.sol b/contracts/libraries/TransientSlots.sol index 26a2de30..335b20b1 100644 --- a/contracts/libraries/TransientSlots.sol +++ b/contracts/libraries/TransientSlots.sol @@ -1,34 +1,34 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; -/// @title TransientSlots -/// @notice The single allocation table for every transient storage slot the router uses -/// @dev Slots are small literals rather than keccak hashes: each 32-byte constant costs a PUSH32, and the hashed form -/// spent roughly 700 bytes of a contract near the EIP-170 limit. Transient storage is per-contract, so the slots only -/// need to be distinct from one another, and no dependency (v4-periphery, permit2, solmate, OpenZeppelin 5.0) uses -/// transient storage in the router's context. Append new slots at the end and never reuse a number. -/// WARNING: solc >=0.8.28 allocates `transient` state variables from slot 0 upward, so they would collide with this -/// table. No contract in the router's inheritance tree, dependencies included, may declare one. To adopt `transient`, -/// migrate every slot below in one change and delete this table. -library TransientSlots { - /// @dev The address that holds the reentrancy lock (Locker.sol) - bytes32 internal constant LOCKER = 0x0000000000000000000000000000000000000000000000000000000000000001; - - /// @dev The maximum input amount for an in-flight v3 exact-output swap (MaxInputAmount.sol) - bytes32 internal constant MAX_AMOUNT_IN = 0x0000000000000000000000000000000000000000000000000000000000000002; - - /// @dev The signer of the executing signed route (RouteSigner.sol) - bytes32 internal constant ROUTE_SIGNER = 0x0000000000000000000000000000000000000000000000000000000000000003; - - /// @dev The intent of the executing signed route (RouteSigner.sol) - bytes32 internal constant ROUTE_INTENT = 0x0000000000000000000000000000000000000000000000000000000000000004; - - /// @dev The data of the executing signed route (RouteSigner.sol) - bytes32 internal constant ROUTE_DATA = 0x0000000000000000000000000000000000000000000000000000000000000005; - - /// @dev Whether the caller opted into running inside a foreign PoolManager unlock (NestedUnlock.sol) - bytes32 internal constant NESTED_UNLOCK = 0x0000000000000000000000000000000000000000000000000000000000000006; - - /// @dev The amount written by a RESOLVE command for a later command to consume (ResolvedAmount.sol) - bytes32 internal constant RESOLVED_AMOUNT = 0x0000000000000000000000000000000000000000000000000000000000000007; -} +// The single allocation table for every transient storage slot the router uses. The slots are file-level constants +// so that inline assembly in every consumer can reference them directly: assembly cannot reach a constant through +// another library or contract (`TransientSlots.LOCKER`), but it can read an imported file-level constant. +// Slots are small literals rather than keccak hashes: each 32-byte constant costs a PUSH32, and the hashed form spent +// roughly 700 bytes of a contract near the EIP-170 limit. Transient storage is per-contract, so the slots only need to +// be distinct from one another, and no dependency (v4-periphery, permit2, solmate, OpenZeppelin 5.0) uses transient +// storage in the router's context. Append new slots at the end and never reuse a number. +// WARNING: solc >=0.8.28 allocates `transient` state variables from slot 0 upward, so they would collide with this +// table. No contract in the router's inheritance tree, dependencies included, may declare one. To adopt `transient`, +// migrate every slot below in one change and delete this table. + +/// @dev The address that holds the reentrancy lock (Locker.sol) +bytes32 constant LOCKER_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000001; + +/// @dev The maximum input amount for an in-flight v3 exact-output swap (MaxInputAmount.sol) +bytes32 constant MAX_AMOUNT_IN_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000002; + +/// @dev The signer of the executing signed route (RouteSigner.sol) +bytes32 constant ROUTE_SIGNER_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000003; + +/// @dev The intent of the executing signed route (RouteSigner.sol) +bytes32 constant ROUTE_INTENT_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000004; + +/// @dev The data of the executing signed route (RouteSigner.sol) +bytes32 constant ROUTE_DATA_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000005; + +/// @dev Whether the caller opted into running inside a foreign PoolManager unlock (NestedUnlock.sol) +bytes32 constant NESTED_UNLOCK_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000006; + +/// @dev The amount written by a RESOLVE command for a later command to consume (ResolvedAmount.sol) +bytes32 constant RESOLVED_AMOUNT_SLOT = 0x0000000000000000000000000000000000000000000000000000000000000007; diff --git a/contracts/modules/ChainedActions.sol b/contracts/modules/ChainedActions.sol index 529a9529..13a11fe0 100644 --- a/contracts/modules/ChainedActions.sol +++ b/contracts/modules/ChainedActions.sol @@ -6,6 +6,7 @@ import {IV3SpokePool} from '../interfaces/external/IV3SpokePool.sol'; import {AcrossV4DepositV3Params} from '../interfaces/IUniversalRouter.sol'; import {IERC20, SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol'; +import {ResolvedAmount} from '../libraries/ResolvedAmount.sol'; abstract contract ChainedActions is Payments { using SafeERC20 for IERC20; @@ -19,7 +20,7 @@ abstract contract ChainedActions is Payments { function _acrossV4DepositV3(bytes calldata input) internal { AcrossV4DepositV3Params memory params = abi.decode(input, (AcrossV4DepositV3Params)); - uint256 inputAmount = params.inputAmount; + uint256 inputAmount = ResolvedAmount.map(params.inputAmount); uint256 callValue = 0; // Resolve sentinel value for inputAmount diff --git a/contracts/modules/Permit2Payments.sol b/contracts/modules/Permit2Payments.sol index 45fef308..46df5c1c 100644 --- a/contracts/modules/Permit2Payments.sol +++ b/contracts/modules/Permit2Payments.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {IAllowanceTransfer} from 'permit2/src/interfaces/IAllowanceTransfer.sol'; import {SafeCast160} from 'permit2/src/libraries/SafeCast160.sol'; import {Payments} from './Payments.sol'; +import {ResolvedAmount} from '../libraries/ResolvedAmount.sol'; /// @title Payments through Permit2 /// @notice Performs interactions with Permit2 to transfer tokens @@ -22,6 +23,8 @@ abstract contract Permit2Payments is Payments { } /// @notice Performs a batch transferFrom on Permit2 + /// @dev Transfers one detail at a time so each amount can be mapped through the RESOLVE register without + /// copying the calldata array to memory. Permit2's batch transferFrom performs the same per-detail transfers. /// @param batchDetails An array detailing each of the transfers that should occur /// @param owner The address that should be the owner of all transfers function permit2TransferFrom(IAllowanceTransfer.AllowanceTransferDetails[] calldata batchDetails, address owner) @@ -29,9 +32,10 @@ abstract contract Permit2Payments is Payments { { uint256 batchLength = batchDetails.length; for (uint256 i = 0; i < batchLength; ++i) { - if (batchDetails[i].from != owner) revert FromAddressIsNotOwner(); + IAllowanceTransfer.AllowanceTransferDetails calldata details = batchDetails[i]; + if (details.from != owner) revert FromAddressIsNotOwner(); + permit2TransferFrom(details.token, owner, details.to, ResolvedAmount.map(details.amount).toUint160()); } - PERMIT2.transferFrom(batchDetails); } /// @notice Either performs a regular payment or transferFrom on Permit2, depending on the payer address diff --git a/contracts/modules/uniswap/v2/V2SwapRouter.sol b/contracts/modules/uniswap/v2/V2SwapRouter.sol index e4cb65b8..1c9a020f 100644 --- a/contracts/modules/uniswap/v2/V2SwapRouter.sol +++ b/contracts/modules/uniswap/v2/V2SwapRouter.sol @@ -66,7 +66,8 @@ abstract contract V2SwapRouter is UniswapImmutables, Permit2Payments { } } // Delivery is measured at the recipient, so a token that hands over less than the pairs computed (fee on - // transfer or withholding, at any hop) fails the route instead of leaving a silent shortfall. + // transfer, withholding, or share-based rounding such as stETH delivering 1-2 wei less, at any hop) fails + // the route instead of leaving a silent shortfall. if (tokenOut.balanceOf(recipient) - balanceBefore < amountOutMinimum) revert V2TooLittleReceived(); } diff --git a/contracts/modules/uniswap/v4/V4SwapRouter.sol b/contracts/modules/uniswap/v4/V4SwapRouter.sol index 09afd186..dd98fb6c 100644 --- a/contracts/modules/uniswap/v4/V4SwapRouter.sol +++ b/contracts/modules/uniswap/v4/V4SwapRouter.sol @@ -29,6 +29,16 @@ abstract contract V4SwapRouter is PermissionedV4Router, Permit2Payments { return ResolvedAmount.map(amount).toUint128(); } + /// @notice Maps the USE_RESOLVED_AMOUNT sentinel in a SETTLE amount through the RESOLVE register + function _mapSettleAmount(uint256 amount, Currency currency) internal view override returns (uint256) { + return super._mapSettleAmount(ResolvedAmount.map(amount), currency); + } + + /// @notice Maps the USE_RESOLVED_AMOUNT sentinel in a TAKE amount through the RESOLVE register + function _mapTakeAmount(uint256 amount, Currency currency) internal view override returns (uint256) { + return super._mapTakeAmount(ResolvedAmount.map(amount), currency); + } + function _payStandard(Currency currency, address payer, uint256 amount) internal override { payOrPermit2Transfer(Currency.unwrap(currency), payer, address(poolManager), amount); } diff --git a/foundry.lock b/foundry.lock index c14cbd93..7de4660a 100644 --- a/foundry.lock +++ b/foundry.lock @@ -14,7 +14,7 @@ "lib/v4-periphery": { "branch": { "name": "dev-ur-2.3.0", - "rev": "ed72ce4716fddfb37e47a1c99e58738311d0fd5b" + "rev": "7ed84398b979827150306c8079271a2bc9b2a58b" } } } \ No newline at end of file diff --git a/lib/v4-periphery b/lib/v4-periphery index 02ffbe93..7ed84398 160000 --- a/lib/v4-periphery +++ b/lib/v4-periphery @@ -1 +1 @@ -Subproject commit 02ffbe93cdfdecaf34bf7434604332023f1e871c +Subproject commit 7ed84398b979827150306c8079271a2bc9b2a58b diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index f6b1c43d..c202fb48 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,3 +1,3 @@ { - "UniversalRouter bytecode size": "24228" + "UniversalRouter bytecode size": "24121" } \ No newline at end of file diff --git a/test/foundry-tests/Locker.t.sol b/test/foundry-tests/Locker.t.sol index 75f9278b..72ef7624 100644 --- a/test/foundry-tests/Locker.t.sol +++ b/test/foundry-tests/Locker.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.24; import 'forge-std/Test.sol'; import {Locker} from '../../contracts/libraries/Locker.sol'; -import {TransientSlots} from '../../contracts/libraries/TransientSlots.sol'; contract LockerTest is Test { function test_fuzz_set_get(address locker1, address locker2, address locker3) public { @@ -33,8 +32,4 @@ contract LockerTest is Test { Locker.set(address(0)); assertEq(Locker.isLocked(), false); } - - function test_lockerSlot() public pure { - assertEq(Locker.LOCKER_SLOT, TransientSlots.LOCKER); - } } diff --git a/test/foundry-tests/MaxInputAmount.t.sol b/test/foundry-tests/MaxInputAmount.t.sol index c9667243..e3b79829 100644 --- a/test/foundry-tests/MaxInputAmount.t.sol +++ b/test/foundry-tests/MaxInputAmount.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.24; import 'forge-std/Test.sol'; import {MaxInputAmount} from '../../contracts/libraries/MaxInputAmount.sol'; -import {TransientSlots} from '../../contracts/libraries/TransientSlots.sol'; contract MaxInputAmountTest is Test { function test_fuzz_maxAmtIn_set_get(uint256 value1, uint256 value2, uint256 value3) public { @@ -21,8 +20,4 @@ contract MaxInputAmountTest is Test { MaxInputAmount.set(0); assertEq(MaxInputAmount.get(), 0); } - - function test_maxAmtInSlot() public pure { - assertEq(MaxInputAmount.MAX_AMOUNT_IN_SLOT, TransientSlots.MAX_AMOUNT_IN); - } } diff --git a/test/foundry-tests/Resolve.t.sol b/test/foundry-tests/Resolve.t.sol index 3a9acf8a..ccd12248 100644 --- a/test/foundry-tests/Resolve.t.sol +++ b/test/foundry-tests/Resolve.t.sol @@ -10,6 +10,13 @@ import {RouterParameters} from '../../contracts/types/RouterParameters.sol'; import {IUniversalRouter} from '../../contracts/interfaces/IUniversalRouter.sol'; import {MockERC20} from './mock/MockERC20.sol'; import {MockResolver, RevertingResolver, ShortReturnResolver, BombResolver} from './mock/MockResolver.sol'; +import {DeployPermit2} from 'permit2/test/utils/DeployPermit2.sol'; +import {IAllowanceTransfer} from 'permit2/src/interfaces/IAllowanceTransfer.sol'; +import {Permit2Payments} from '../../contracts/modules/Permit2Payments.sol'; +import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol'; +import {WETH} from 'solmate/src/tokens/WETH.sol'; +import {MockSpokePool} from './mock/MockSpokePool.sol'; +import {AcrossV4DepositV3Params} from '../../contracts/interfaces/IUniversalRouter.sol'; /// @notice Exercises the RESOLVE command and the USE_RESOLVED_AMOUNT sentinel end-to-end through the /// TRANSFER command, which resolves its value field from the register a prior RESOLVE populates. @@ -216,3 +223,331 @@ contract ResolveTest is Test { assertEq(erc20.balanceOf(RECIPIENT), 1 ether, 'only the literal transfer after the sub-plan ran'); } } + +/// @notice Exercises the USE_RESOLVED_AMOUNT sentinel in the amount fields of PERMIT2_TRANSFER_FROM and +/// PERMIT2_TRANSFER_FROM_BATCH, which lets a route pull exactly the amount a prior RESOLVE produced instead +/// of an offchain upper bound. +contract ResolvePermit2TransferTest is Test, DeployPermit2 { + address constant RECIPIENT = address(1234); + address constant RECIPIENT2 = address(5678); + uint256 constant N = 7_777 ether; + + UniversalRouter router; + MockERC20 erc20; + address user = makeAddr('user'); + + function setUp() public { + IAllowanceTransfer permit2 = IAllowanceTransfer(deployPermit2()); + RouterParameters memory params = RouterParameters({ + permit2: address(permit2), + weth9: address(0), + v2Factory: address(0), + v3Factory: address(0), + pairInitCodeHash: bytes32(0), + poolInitCodeHash: bytes32(0), + v4PoolManager: address(0), + permissionsAdapterFactory: address(0), + v3NFTPositionManager: address(0), + v4PositionManager: address(0), + spokePool: address(0) + }); + router = new UniversalRouter(params); + erc20 = new MockERC20(); + + erc20.mint(user, 2 * N); + vm.startPrank(user); + erc20.approve(address(permit2), type(uint256).max); + permit2.approve(address(erc20), address(router), type(uint160).max, type(uint48).max); + vm.stopPrank(); + } + + function _resolveThenPermit2Transfer(address resolver, uint256 transferAmount) + internal + view + returns (bytes memory commands, bytes[] memory inputs) + { + commands = abi.encodePacked(bytes1(uint8(Commands.RESOLVE)), bytes1(uint8(Commands.PERMIT2_TRANSFER_FROM))); + inputs = new bytes[](2); + inputs[0] = abi.encode(resolver, bytes('')); + inputs[1] = abi.encode(address(erc20), RECIPIENT, transferAmount); + } + + function test_permit2TransferFrom_pullsResolvedAmount() public { + MockResolver resolver = new MockResolver(N); + (bytes memory commands, bytes[] memory inputs) = + _resolveThenPermit2Transfer(address(resolver), Constants.USE_RESOLVED_AMOUNT); + + vm.prank(user); + router.execute(commands, inputs); + + assertEq(erc20.balanceOf(RECIPIENT), N); + assertEq(erc20.balanceOf(user), N); + } + + function test_permit2TransferFrom_literalAmountUnaffected() public { + MockResolver resolver = new MockResolver(N); + (bytes memory commands, bytes[] memory inputs) = _resolveThenPermit2Transfer(address(resolver), 1 ether); + + vm.prank(user); + router.execute(commands, inputs); + + assertEq(erc20.balanceOf(RECIPIENT), 1 ether); + } + + function test_permit2TransferFrom_sentinelWithoutResolve_reverts() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_TRANSFER_FROM))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(address(erc20), RECIPIENT, Constants.USE_RESOLVED_AMOUNT); + + vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); + vm.prank(user); + router.execute(commands, inputs); + } + + function _batch(uint160 amount0, uint160 amount1) + internal + view + returns (IAllowanceTransfer.AllowanceTransferDetails[] memory details) + { + details = new IAllowanceTransfer.AllowanceTransferDetails[](2); + details[0] = IAllowanceTransfer.AllowanceTransferDetails({ + from: user, to: RECIPIENT, amount: amount0, token: address(erc20) + }); + details[1] = IAllowanceTransfer.AllowanceTransferDetails({ + from: user, to: RECIPIENT2, amount: amount1, token: address(erc20) + }); + } + + function test_permit2TransferFromBatch_pullsResolvedAmount() public { + MockResolver resolver = new MockResolver(N); + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.RESOLVE)), bytes1(uint8(Commands.PERMIT2_TRANSFER_FROM_BATCH))); + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(address(resolver), bytes('')); + // one detail consumes the resolved amount, the other carries a literal + inputs[1] = abi.encode(_batch(uint160(Constants.USE_RESOLVED_AMOUNT), 1 ether)); + + vm.prank(user); + router.execute(commands, inputs); + + assertEq(erc20.balanceOf(RECIPIENT), N); + assertEq(erc20.balanceOf(RECIPIENT2), 1 ether); + assertEq(erc20.balanceOf(user), N - 1 ether); + } + + function test_permit2TransferFromBatch_sentinelWithoutResolve_reverts() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_TRANSFER_FROM_BATCH))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(_batch(1 ether, uint160(Constants.USE_RESOLVED_AMOUNT))); + + vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); + vm.prank(user); + router.execute(commands, inputs); + } + + function _batchInput(IAllowanceTransfer.AllowanceTransferDetails[] memory details) + internal + pure + returns (bytes memory input) + { + input = abi.encode(details); + } + + function _executeBatch(bytes memory input) internal { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_TRANSFER_FROM_BATCH))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = input; + vm.prank(user); + router.execute(commands, inputs); + } + + /// @dev Sets the bits above an entry field's ABI type: word `field` (0 from, 1 to, 2 amount, 3 token) of + /// entry `entry`, after the 0x20 array offset and 0x20 length words. + function _dirty(bytes memory input, uint256 entry, uint256 field) internal pure { + uint256 position = 0x20 + 0x40 + entry * 0x80 + field * 0x20; + assembly { + let word := mload(add(input, position)) + mstore(add(input, position), or(word, shl(200, 1))) + } + } + + function test_permit2TransferFromBatch_rejectsTruncatedDetails() public { + bytes memory input = _batchInput(_batch(1 ether, 1 ether)); + // drop the last word, so the declared length of two entries no longer fits the input + assembly { + mstore(input, sub(mload(input), 0x20)) + } + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + _executeBatch(input); + } + + function test_permit2TransferFromBatch_rejectsOverstatedLength() public { + bytes memory input = _batchInput(_batch(1 ether, 1 ether)); + // claim three entries while only two are encoded + assembly { + mstore(add(input, 0x40), 3) + } + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + _executeBatch(input); + } + + function test_permit2TransferFromBatch_rejectsDirtyFields() public { + for (uint256 field = 0; field < 4; field++) { + bytes memory input = _batchInput(_batch(1 ether, 1 ether)); + _dirty(input, 1, field); + vm.expectRevert(); + _executeBatch(input); + } + assertEq(erc20.balanceOf(RECIPIENT), 0); + assertEq(erc20.balanceOf(RECIPIENT2), 0); + } + + function test_permit2TransferFromBatch_rejectsForeignOwner() public { + IAllowanceTransfer.AllowanceTransferDetails[] memory details = _batch(1 ether, 1 ether); + details[1].from = RECIPIENT; + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_TRANSFER_FROM_BATCH))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(details); + + vm.expectRevert(Permit2Payments.FromAddressIsNotOwner.selector); + vm.prank(user); + router.execute(commands, inputs); + } +} + +/// @notice Exercises the USE_RESOLVED_AMOUNT sentinel in WRAP_ETH, UNWRAP_WETH_EXACT and ACROSS_V4_DEPOSIT_V3. +contract ResolveWethAndAcrossTest is Test { + address constant RECIPIENT = address(1234); + uint256 constant N = 7 ether; + + UniversalRouter router; + WETH weth; + MockSpokePool spokePool; + MockResolver resolver; + + function setUp() public { + weth = new WETH(); + spokePool = new MockSpokePool(); + RouterParameters memory params = RouterParameters({ + permit2: address(0), + weth9: address(weth), + v2Factory: address(0), + v3Factory: address(0), + pairInitCodeHash: bytes32(0), + poolInitCodeHash: bytes32(0), + v4PoolManager: address(0), + permissionsAdapterFactory: address(0), + v3NFTPositionManager: address(0), + v4PositionManager: address(0), + spokePool: address(spokePool) + }); + router = new UniversalRouter(params); + resolver = new MockResolver(N); + } + + function _resolveThen(uint256 command, bytes memory input) + internal + view + returns (bytes memory commands, bytes[] memory inputs) + { + commands = abi.encodePacked(bytes1(uint8(Commands.RESOLVE)), bytes1(uint8(command))); + inputs = new bytes[](2); + inputs[0] = abi.encode(address(resolver), bytes('')); + inputs[1] = input; + } + + function _sentinelOnly(uint256 command, bytes memory input) + internal + pure + returns (bytes memory commands, bytes[] memory inputs) + { + commands = abi.encodePacked(bytes1(uint8(command))); + inputs = new bytes[](1); + inputs[0] = input; + } + + function _fundRouterWeth(uint256 amount) internal { + deal(address(this), amount); + weth.deposit{value: amount}(); + weth.transfer(address(router), amount); + } + + function _acrossInput(uint256 inputAmount) internal view returns (bytes memory) { + return abi.encode( + AcrossV4DepositV3Params({ + depositor: RECIPIENT, + recipient: RECIPIENT, + inputToken: address(weth), + outputToken: address(0), + inputAmount: inputAmount, + outputAmount: 1 ether, + destinationChainId: 10, + exclusiveRelayer: address(0), + quoteTimestamp: uint32(block.timestamp), + fillDeadline: uint32(block.timestamp + 1 hours), + exclusivityDeadline: 0, + message: bytes(''), + useNative: false + }) + ); + } + + function test_wrapETH_wrapsResolvedAmount() public { + (bytes memory commands, bytes[] memory inputs) = + _resolveThen(Commands.WRAP_ETH, abi.encode(RECIPIENT, Constants.USE_RESOLVED_AMOUNT)); + + router.execute{value: 2 * N}(commands, inputs); + + assertEq(weth.balanceOf(RECIPIENT), N); + assertEq(address(router).balance, N); + } + + function test_wrapETH_sentinelWithoutResolve_reverts() public { + (bytes memory commands, bytes[] memory inputs) = + _sentinelOnly(Commands.WRAP_ETH, abi.encode(RECIPIENT, Constants.USE_RESOLVED_AMOUNT)); + + vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); + router.execute{value: N}(commands, inputs); + } + + function test_unwrapWETHExact_unwrapsResolvedAmount() public { + _fundRouterWeth(2 * N); + (bytes memory commands, bytes[] memory inputs) = + _resolveThen(Commands.UNWRAP_WETH_EXACT, abi.encode(RECIPIENT, Constants.USE_RESOLVED_AMOUNT)); + + router.execute(commands, inputs); + + assertEq(RECIPIENT.balance, N); + assertEq(weth.balanceOf(address(router)), N); + } + + function test_unwrapWETHExact_sentinelWithoutResolve_reverts() public { + _fundRouterWeth(N); + (bytes memory commands, bytes[] memory inputs) = + _sentinelOnly(Commands.UNWRAP_WETH_EXACT, abi.encode(RECIPIENT, Constants.USE_RESOLVED_AMOUNT)); + + vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); + router.execute(commands, inputs); + } + + function test_acrossDeposit_depositsResolvedAmount() public { + _fundRouterWeth(2 * N); + (bytes memory commands, bytes[] memory inputs) = + _resolveThen(Commands.ACROSS_V4_DEPOSIT_V3, _acrossInput(Constants.USE_RESOLVED_AMOUNT)); + + router.execute(commands, inputs); + + assertEq(spokePool.lastInputAmount(), N); + assertEq(weth.balanceOf(address(spokePool)), N); + assertEq(weth.balanceOf(address(router)), N); + } + + function test_acrossDeposit_sentinelWithoutResolve_reverts() public { + _fundRouterWeth(N); + (bytes memory commands, bytes[] memory inputs) = + _sentinelOnly(Commands.ACROSS_V4_DEPOSIT_V3, _acrossInput(Constants.USE_RESOLVED_AMOUNT)); + + vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); + router.execute(commands, inputs); + } +} diff --git a/test/foundry-tests/ResolveV4.t.sol b/test/foundry-tests/ResolveV4.t.sol index 69f2485e..c2d7f503 100644 --- a/test/foundry-tests/ResolveV4.t.sol +++ b/test/foundry-tests/ResolveV4.t.sol @@ -164,4 +164,80 @@ contract ResolveV4Test is Test, Deployers { vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); router.execute(commands, inputs); } + + /// @dev SETTLE pays exactly the resolved amount into the PoolManager, and the exact-input swap then spends + /// that credit through OPEN_DELTA. + function test_resolve_v4Settle_paysExactlyTheResolvedAmount() public { + MockResolver resolver = new MockResolver(SELL_AMOUNT); + IV4Router.ExactInputSingleParams memory sp = IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: true, + amountIn: ActionConstants.OPEN_DELTA, + amountOutMinimum: 0, + minHopPriceX36: 0, + hookData: hex'' + }); + Plan memory plan = Planner.init(); + plan = plan.add(Actions.SETTLE, abi.encode(currency0, Constants.USE_RESOLVED_AMOUNT, false)); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(sp)); + plan = plan.add(Actions.TAKE, abi.encode(currency1, RECIPIENT, ActionConstants.OPEN_DELTA)); + (bytes memory commands, bytes[] memory inputs) = _resolveThenV4(address(resolver), plan.encode()); + + router.execute(commands, inputs); + + assertEq( + MockERC20(Currency.unwrap(currency0)).balanceOf(address(router)), + FUNDING - SELL_AMOUNT, + 'exactly the resolved amount was settled' + ); + assertGt(MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), 0); + } + + /// @dev TAKE sends exactly the resolved amount of the swap output to one recipient, and the rest goes to + /// another through OPEN_DELTA. + function test_resolve_v4Take_takesExactlyTheResolvedAmount() public { + address rest = address(0xCAFE); + MockResolver resolver = new MockResolver(LITERAL); + IV4Router.ExactInputSingleParams memory sp = IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: true, + amountIn: SELL_AMOUNT, + amountOutMinimum: 0, + minHopPriceX36: 0, + hookData: hex'' + }); + Plan memory plan = Planner.init(); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(sp)); + plan = plan.add(Actions.SETTLE, abi.encode(currency0, ActionConstants.OPEN_DELTA, false)); + plan = plan.add(Actions.TAKE, abi.encode(currency1, RECIPIENT, Constants.USE_RESOLVED_AMOUNT)); + plan = plan.add(Actions.TAKE, abi.encode(currency1, rest, ActionConstants.OPEN_DELTA)); + (bytes memory commands, bytes[] memory inputs) = _resolveThenV4(address(resolver), plan.encode()); + + router.execute(commands, inputs); + + assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(RECIPIENT), LITERAL, 'exactly the resolved amount'); + assertGt(MockERC20(Currency.unwrap(currency1)).balanceOf(rest), 0, 'the remainder went to the other taker'); + } + + function test_resolve_v4SettleSentinelWithoutResolve_reverts() public { + Plan memory plan = Planner.init(); + plan = plan.add(Actions.SETTLE, abi.encode(currency0, Constants.USE_RESOLVED_AMOUNT, false)); + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_SWAP))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = plan.encode(); + + vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); + router.execute(commands, inputs); + } + + function test_resolve_v4TakeSentinelWithoutResolve_reverts() public { + Plan memory plan = Planner.init(); + plan = plan.add(Actions.TAKE, abi.encode(currency1, RECIPIENT, Constants.USE_RESOLVED_AMOUNT)); + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_SWAP))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = plan.encode(); + + vm.expectRevert(ResolvedAmount.ResolvedAmountUnset.selector); + router.execute(commands, inputs); + } } diff --git a/test/foundry-tests/ResolvedAmount.t.sol b/test/foundry-tests/ResolvedAmount.t.sol index af1efd33..23cee5e3 100644 --- a/test/foundry-tests/ResolvedAmount.t.sol +++ b/test/foundry-tests/ResolvedAmount.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.24; import 'forge-std/Test.sol'; import {ResolvedAmount} from '../../contracts/libraries/ResolvedAmount.sol'; -import {TransientSlots} from '../../contracts/libraries/TransientSlots.sol'; contract ResolvedAmountTest is Test { function test_fuzz_resolvedAmount_set_get_reset(uint256 value1, uint256 value2) public { @@ -18,8 +17,4 @@ contract ResolvedAmountTest is Test { ResolvedAmount.reset(); assertEq(ResolvedAmount.get(), 0); } - - function test_resolvedAmountSlot() public pure { - assertEq(ResolvedAmount.RESOLVED_AMOUNT_SLOT, TransientSlots.RESOLVED_AMOUNT); - } } diff --git a/test/foundry-tests/TransientSlots.t.sol b/test/foundry-tests/TransientSlots.t.sol index 4aa2bdf7..407c88fd 100644 --- a/test/foundry-tests/TransientSlots.t.sol +++ b/test/foundry-tests/TransientSlots.t.sol @@ -2,34 +2,28 @@ pragma solidity ^0.8.24; import 'forge-std/Test.sol'; -import {TransientSlots} from '../../contracts/libraries/TransientSlots.sol'; -import {Locker} from '../../contracts/libraries/Locker.sol'; -import {MaxInputAmount} from '../../contracts/libraries/MaxInputAmount.sol'; -import {NestedUnlock} from '../../contracts/libraries/NestedUnlock.sol'; -import {ResolvedAmount} from '../../contracts/libraries/ResolvedAmount.sol'; -import {RouteSigner} from '../../contracts/base/RouteSigner.sol'; -import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import { + LOCKER_SLOT, + MAX_AMOUNT_IN_SLOT, + ROUTE_SIGNER_SLOT, + ROUTE_INTENT_SLOT, + ROUTE_DATA_SLOT, + NESTED_UNLOCK_SLOT, + RESOLVED_AMOUNT_SLOT +} from '../../contracts/libraries/TransientSlots.sol'; -contract RouteSignerSlots is RouteSigner { - constructor() EIP712('UniversalRouter', '2') {} - - function slots() external pure returns (bytes32, bytes32, bytes32) { - return (ROUTE_SIGNER_SLOT, ROUTE_INTENT_SLOT, ROUTE_DATA_SLOT); - } -} - -/// @notice Every transient slot the router touches must come from the TransientSlots table and be distinct, +/// @notice Every transient slot the router touches comes from the TransientSlots table and must be distinct, /// since transient storage is shared by all libraries compiled into the router. contract TransientSlotsTest is Test { function _allSlots() internal pure returns (bytes32[] memory slots) { slots = new bytes32[](7); - slots[0] = TransientSlots.LOCKER; - slots[1] = TransientSlots.MAX_AMOUNT_IN; - slots[2] = TransientSlots.ROUTE_SIGNER; - slots[3] = TransientSlots.ROUTE_INTENT; - slots[4] = TransientSlots.ROUTE_DATA; - slots[5] = TransientSlots.NESTED_UNLOCK; - slots[6] = TransientSlots.RESOLVED_AMOUNT; + slots[0] = LOCKER_SLOT; + slots[1] = MAX_AMOUNT_IN_SLOT; + slots[2] = ROUTE_SIGNER_SLOT; + slots[3] = ROUTE_INTENT_SLOT; + slots[4] = ROUTE_DATA_SLOT; + slots[5] = NESTED_UNLOCK_SLOT; + slots[6] = RESOLVED_AMOUNT_SLOT; } function test_slotsAreDistinct() public pure { @@ -41,19 +35,6 @@ contract TransientSlotsTest is Test { } } - /// @dev Inline assembly only accepts literal slot constants, so each library repeats its number; this pins - /// every copy to the table. - function test_librariesUseTableSlots() public { - assertEq(Locker.LOCKER_SLOT, TransientSlots.LOCKER); - assertEq(MaxInputAmount.MAX_AMOUNT_IN_SLOT, TransientSlots.MAX_AMOUNT_IN); - assertEq(NestedUnlock.NESTED_UNLOCK_SLOT, TransientSlots.NESTED_UNLOCK); - assertEq(ResolvedAmount.RESOLVED_AMOUNT_SLOT, TransientSlots.RESOLVED_AMOUNT); - (bytes32 signer, bytes32 intent, bytes32 data) = new RouteSignerSlots().slots(); - assertEq(signer, TransientSlots.ROUTE_SIGNER); - assertEq(intent, TransientSlots.ROUTE_INTENT); - assertEq(data, TransientSlots.ROUTE_DATA); - } - /// @dev Writing one slot must not disturb any other, which is what distinctness buys at runtime. function test_fuzz_slotsDoNotAlias(uint256 value) public { bytes32[] memory slots = _allSlots(); diff --git a/test/foundry-tests/mock/MockSpokePool.sol b/test/foundry-tests/mock/MockSpokePool.sol new file mode 100644 index 00000000..720f8b80 --- /dev/null +++ b/test/foundry-tests/mock/MockSpokePool.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IV3SpokePool} from '../../../contracts/interfaces/external/IV3SpokePool.sol'; + +/// @notice Pulls the deposited ERC20 like the Across SpokePool and records the amounts it was called with. +contract MockSpokePool is IV3SpokePool { + uint256 public lastInputAmount; + uint256 public lastOutputAmount; + + function depositV3( + address, + address, + address inputToken, + address, + uint256 inputAmount, + uint256 outputAmount, + uint256, + address, + uint32, + uint32, + uint32, + bytes calldata + ) external payable { + lastInputAmount = inputAmount; + lastOutputAmount = outputAmount; + if (msg.value == 0) IERC20(inputToken).transferFrom(msg.sender, address(this), inputAmount); + } +} diff --git a/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap index efa3baf0..c453052d 100644 --- a/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap @@ -3,6 +3,6 @@ exports[`Check Ownership Gas gas: balance check ERC20 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 38068, + "gasUsed": 38075, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap index 964b45ae..b77eacdc 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap @@ -38,7 +38,7 @@ Object { exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH_EXACT partial amount 1`] = ` Object { "calldataByteLength": 324, - "gasUsed": 49970, + "gasUsed": 50033, } `; @@ -52,6 +52,6 @@ Object { exports[`Payments Gas Tests Individual Command Tests gas: WRAP_ETH 1`] = ` Object { "calldataByteLength": 324, - "gasUsed": 53792, + "gasUsed": 53847, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap index df42a5ad..f4b91f7c 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap @@ -3,105 +3,105 @@ exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, both fail but the transaction succeeds 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 278374, + "gasUsed": 278437, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, neither fails 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 253939, + "gasUsed": 254002, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, second sub plan fails 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 253939, + "gasUsed": 254002, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, the first fails 1`] = ` Object { "calldataByteLength": 1956, - "gasUsed": 278374, + "gasUsed": 278437, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V2, then V3 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 193957, + "gasUsed": 193975, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V3, then V2 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 181393, + "gasUsed": 181414, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, different input tokens, each two hop, with batch permit 1`] = ` Object { "calldataByteLength": 1668, - "gasUsed": 301629, + "gasUsed": 301581, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit 1`] = ` Object { "calldataByteLength": 1348, - "gasUsed": 312059, + "gasUsed": 312389, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit transfer from batch 1`] = ` Object { "calldataByteLength": 1412, - "gasUsed": 313221, + "gasUsed": 313192, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, without explicit permit 1`] = ` Object { "calldataByteLength": 1028, - "gasUsed": 308469, + "gasUsed": 308511, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 1124, - "gasUsed": 181595, + "gasUsed": 181637, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop, ADDRESS_THIS flag 1`] = ` Object { "calldataByteLength": 1124, - "gasUsed": 181370, + "gasUsed": 181412, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, exactOut, one hop 1`] = ` Object { "calldataByteLength": 1092, - "gasUsed": 199020, + "gasUsed": 199062, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 1092, - "gasUsed": 189380, + "gasUsed": 189422, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ETH --> ERC20 split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 1252, - "gasUsed": 196519, + "gasUsed": 196581, } `; @@ -143,98 +143,98 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn trade, where an output fee is taken 1`] = ` Object { "calldataByteLength": 900, - "gasUsed": 128739, + "gasUsed": 128760, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 108587, + "gasUsed": 108608, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 244161, + "gasUsed": 244182, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops, no deadline 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 243894, + "gasUsed": 243915, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 176411, + "gasUsed": 176432, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops, MSG_SENDER flag 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 176411, + "gasUsed": 176432, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 110509, + "gasUsed": 110530, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 251840, + "gasUsed": 251861, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 181245, + "gasUsed": 181266, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 124978, + "gasUsed": 124999, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 868, - "gasUsed": 132136, + "gasUsed": 132157, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, with ETH fee 1`] = ` Object { "calldataByteLength": 1028, - "gasUsed": 140339, + "gasUsed": 140360, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ETH --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 108480, + "gasUsed": 108548, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ETH --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 836, - "gasUsed": 129345, + "gasUsed": 129410, } `; @@ -283,69 +283,69 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 108561, + "gasUsed": 108582, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 260632, + "gasUsed": 260647, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 181974, + "gasUsed": 181992, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 580, - "gasUsed": 116125, + "gasUsed": 116146, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 255505, + "gasUsed": 255526, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = ` Object { "calldataByteLength": 612, - "gasUsed": 177514, + "gasUsed": 177535, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactIn swap 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 125000, + "gasUsed": 125021, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactOut swap 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 132636, + "gasUsed": 132657, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactIn swap 1`] = ` Object { "calldataByteLength": 708, - "gasUsed": 218467, + "gasUsed": 218532, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactOut swap 1`] = ` Object { "calldataByteLength": 836, - "gasUsed": 127839, + "gasUsed": 127904, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap index fbe193f0..4a9787c6 100644 --- a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap @@ -7,32 +7,32 @@ Object { } `; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1131045`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1131132`; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1165716`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1165713`; exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps SwapRouter02 1`] = `1124979`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3160395`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3160725`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3315109`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3315034`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps SwapRouter02 1`] = `3195011`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4206917`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4207247`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4412491`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4412281`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions SwapRouter02 1`] = `4282374`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `519813`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `519825`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `520131`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `520143`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap SwapRouter02 1`] = `500008`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `304656`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `304629`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `304592`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `304565`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap SwapRouter02 1`] = `270033`;