From 1b345c2737a42d066897c3092ba2ac3aed7abc0a Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Sun, 22 Oct 2023 22:16:45 -0400
Subject: [PATCH 01/12] Implement VIPs for MakerPSM

---
 src/ISettlerActions.sol                |  3 ++
 src/Settler.sol                        | 23 ++++++++++-
 src/core/MakerPSM.sol                  | 57 ++++++++++++++++++++++++++
 test/integration/SettlerPairTest.t.sol |  1 +
 test/integration/WethWrapTest.t.sol    |  1 +
 5 files changed, 83 insertions(+), 2 deletions(-)
 create mode 100644 src/core/MakerPSM.sol

diff --git a/src/ISettlerActions.sol b/src/ISettlerActions.sol
index ed81d6785..96dc654b4 100644
--- a/src/ISettlerActions.sol
+++ b/src/ISettlerActions.sol
@@ -62,6 +62,9 @@ interface ISettlerActions {
         bytes memory sig
     ) external;
 
+    function MAKER_PSM_SELL_GEM(address recipient, uint256 bips, address psm, address gemToken) external;
+    function MAKER_PSM_BUY_GEM(address recipient, uint256 bips, address psm, address gemToken) external;
+
     /// @dev Trades against UniswapV3 using user funds via Permit2 for funding. Metatransaction variant. Signature is over all actions.
     function METATXN_UNISWAPV3_PERMIT2_SWAP_EXACT_IN(
         address recipient,
diff --git a/src/Settler.sol b/src/Settler.sol
index ac9ae99c6..f792fe52d 100644
--- a/src/Settler.sol
+++ b/src/Settler.sol
@@ -9,6 +9,7 @@ import {Basic} from "./core/Basic.sol";
 import {OtcOrderSettlement} from "./core/OtcOrderSettlement.sol";
 import {UniswapV3} from "./core/UniswapV3.sol";
 import {UniswapV2} from "./core/UniswapV2.sol";
+import {IPSM, MakerPSM} from "./core/MakerPSM.sol";
 import {IZeroEx, ZeroEx} from "./core/ZeroEx.sol";
 
 import {SafeTransferLib} from "./utils/SafeTransferLib.sol";
@@ -71,7 +72,7 @@ library CalldataDecoder {
     }
 }
 
-contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, UniswapV2, ZeroEx, FreeMemory {
+contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, UniswapV2, MakerPSM, ZeroEx, FreeMemory {
     using SafeTransferLib for ERC20;
     using SafeTransferLib for address payable;
     using UnsafeMath for uint256;
@@ -94,12 +95,20 @@ contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, Uniswa
 
     receive() external payable {}
 
-    constructor(address permit2, address zeroEx, address uniFactory, bytes32 poolInitCodeHash, address feeRecipient)
+    constructor(
+        address permit2,
+        address zeroEx,
+        address uniFactory,
+        bytes32 poolInitCodeHash,
+        address dai,
+        address feeRecipient
+    )
         Permit2Payment(permit2, feeRecipient)
         Basic()
         OtcOrderSettlement()
         UniswapV3(uniFactory, poolInitCodeHash)
         UniswapV2()
+        MakerPSM(dai)
         ZeroEx(zeroEx)
     {
         assert(ACTIONS_AND_SLIPPAGE_TYPEHASH == keccak256(bytes(ACTIONS_AND_SLIPPAGE_TYPE)));
@@ -339,6 +348,16 @@ contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, Uniswa
                 abi.decode(data, (address, uint256, uint256, bytes));
 
             sellToUniswapV2(path, bips, amountOutMin, recipient);
+        } else if (action == ISettlerActions.MAKER_PSM_SELL_GEM.selector) {
+            (address recipient, uint256 bips, IPSM psm, ERC20 gemToken) =
+                abi.decode(data, (address, uint256, IPSM, ERC20));
+
+            makerPsmSellGem(recipient, bips, psm, gemToken);
+        } else if (action == ISettlerActions.MAKER_PSM_BUY_GEM.selector) {
+            (address recipient, uint256 bips, IPSM psm, ERC20 gemToken) =
+                abi.decode(data, (address, uint256, IPSM, ERC20));
+
+            makerPsmBuyGem(recipient, bips, psm, gemToken);
         } else if (action == ISettlerActions.BASIC_SELL.selector) {
             (address pool, ERC20 sellToken, uint256 proportion, uint256 offset, bytes memory _data) =
                 abi.decode(data, (address, ERC20, uint256, uint256, bytes));
diff --git a/src/core/MakerPSM.sol b/src/core/MakerPSM.sol
new file mode 100644
index 000000000..40f40bfa5
--- /dev/null
+++ b/src/core/MakerPSM.sol
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.21;
+
+import {ERC20} from "solmate/src/tokens/ERC20.sol";
+
+import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
+import {FullMath} from "../utils/FullMath.sol";
+
+interface IPSM {
+    // @dev Get the fee for selling DAI to USDC in PSM
+    // @return tout toll out [wad]
+    function tout() external view returns (uint256);
+
+    // @dev Get the address of the underlying vault powering PSM
+    // @return address of gemJoin contract
+    function gemJoin() external view returns (address);
+
+    // @dev Sell USDC for DAI
+    // @param usr The address of the account trading USDC for DAI.
+    // @param gemAmt The amount of USDC to sell in USDC base units
+    function sellGem(address usr, uint256 gemAmt) external;
+
+    // @dev Buy USDC for DAI
+    // @param usr The address of the account trading DAI for USDC
+    // @param gemAmt The amount of USDC to buy in USDC base units
+    function buyGem(address usr, uint256 gemAmt) external;
+}
+
+abstract contract MakerPSM {
+    using FullMath for uint256;
+    using SafeTransferLib for ERC20;
+
+    // Maker units https://github.com/makerdao/dss/blob/master/DEVELOPING.md
+    // wad: fixed point decimal with 18 decimals (for basic quantities, e.g. balances)
+    uint256 internal constant WAD = 10 ** 18;
+
+    ERC20 internal immutable DAI;
+
+    constructor(address dai) {
+        DAI = ERC20(dai);
+    }
+
+    function makerPsmSellGem(address recipient, uint256 bips, IPSM psm, ERC20 gemToken) internal {
+        uint256 sellAmount = gemToken.balanceOf(address(this)).mulDiv(bips, 10_000);
+        gemToken.safeApproveIfBelow(psm.gemJoin(), sellAmount);
+        psm.sellGem(recipient, sellAmount);
+    }
+
+    function makerPsmBuyGem(address recipient, uint256 bips, IPSM psm, ERC20 gemToken) internal {
+        uint256 sellAmount = DAI.balanceOf(address(this)).mulDiv(bips, 10_000);
+        uint256 feeDivisor = WAD + psm.tout(); // eg. 1.001 * 10 ** 18 with 0.1% fee [tout is in wad];
+        uint256 buyAmount = sellAmount.mulDiv(10 ** uint256(gemToken.decimals()), feeDivisor);
+
+        DAI.safeApproveIfBelow(address(psm), sellAmount);
+        psm.buyGem(recipient, buyAmount);
+    }
+}
diff --git a/test/integration/SettlerPairTest.t.sol b/test/integration/SettlerPairTest.t.sol
index 1e3161c8b..468544fc7 100644
--- a/test/integration/SettlerPairTest.t.sol
+++ b/test/integration/SettlerPairTest.t.sol
@@ -73,6 +73,7 @@ abstract contract SettlerPairTest is BasePairTest {
             address(ZERO_EX), // ZeroEx
             0x1F98431c8aD98523631AE4a59f267346ea31F984, // UniV3 Factory
             0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, // UniV3 pool init code hash
+            0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI
             0x2222222222222222222222222222222222222222
         );
     }
diff --git a/test/integration/WethWrapTest.t.sol b/test/integration/WethWrapTest.t.sol
index 6880ff176..4c971aa0f 100644
--- a/test/integration/WethWrapTest.t.sol
+++ b/test/integration/WethWrapTest.t.sol
@@ -23,6 +23,7 @@ contract WethWrapTest is Test, GasSnapshot {
             0xDef1C0ded9bec7F1a1670819833240f027b25EfF, // ZeroEx
             0x1F98431c8aD98523631AE4a59f267346ea31F984, // UniV3 Factory
             0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, // UniV3 pool init code hash
+            0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI
             0x2222222222222222222222222222222222222222
         );
         vm.label(address(_settler), "Settler");

From 6d7744f855694061048100f1b5489bbe2704577e Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Sun, 22 Oct 2023 22:36:51 -0400
Subject: [PATCH 02/12] Contract too large; optimize bips math to avoid use of
 unnecessary FullMath

---
 src/core/MakerPSM.sol  | 23 ++++++++++++++---------
 src/core/UniswapV2.sol |  9 ++++++---
 src/core/UniswapV3.sol |  9 ++++++---
 3 files changed, 26 insertions(+), 15 deletions(-)

diff --git a/src/core/MakerPSM.sol b/src/core/MakerPSM.sol
index 40f40bfa5..b7b1775b4 100644
--- a/src/core/MakerPSM.sol
+++ b/src/core/MakerPSM.sol
@@ -4,7 +4,7 @@ pragma solidity ^0.8.21;
 import {ERC20} from "solmate/src/tokens/ERC20.sol";
 
 import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
-import {FullMath} from "../utils/FullMath.sol";
+import {UnsafeMath} from "../utils/UnsafeMath.sol";
 
 interface IPSM {
     // @dev Get the fee for selling DAI to USDC in PSM
@@ -27,7 +27,7 @@ interface IPSM {
 }
 
 abstract contract MakerPSM {
-    using FullMath for uint256;
+    using UnsafeMath for uint256;
     using SafeTransferLib for ERC20;
 
     // Maker units https://github.com/makerdao/dss/blob/master/DEVELOPING.md
@@ -41,17 +41,22 @@ abstract contract MakerPSM {
     }
 
     function makerPsmSellGem(address recipient, uint256 bips, IPSM psm, ERC20 gemToken) internal {
-        uint256 sellAmount = gemToken.balanceOf(address(this)).mulDiv(bips, 10_000);
+        // phantom overflow can't happen here because PSM prohibits gemToken with decimals > 18
+        uint256 sellAmount = (gemToken.balanceOf(address(this)) * bips).unsafeDiv(10_000);
         gemToken.safeApproveIfBelow(psm.gemJoin(), sellAmount);
         psm.sellGem(recipient, sellAmount);
     }
 
     function makerPsmBuyGem(address recipient, uint256 bips, IPSM psm, ERC20 gemToken) internal {
-        uint256 sellAmount = DAI.balanceOf(address(this)).mulDiv(bips, 10_000);
-        uint256 feeDivisor = WAD + psm.tout(); // eg. 1.001 * 10 ** 18 with 0.1% fee [tout is in wad];
-        uint256 buyAmount = sellAmount.mulDiv(10 ** uint256(gemToken.decimals()), feeDivisor);
-
-        DAI.safeApproveIfBelow(address(psm), sellAmount);
-        psm.buyGem(recipient, buyAmount);
+        // phantom overflow can't happen here because DAI has decimals = 18
+        uint256 sellAmount = (DAI.balanceOf(address(this)) * bips).unsafeDiv(10_000);
+        unchecked {
+            uint256 feeDivisor = psm.tout() + WAD; // eg. 1.001 * 10 ** 18 with 0.1% fee [tout is in wad];
+            // overflow can't happen at all because DAI is reasonable and PSM prohibits gemToken with decimals > 18
+            uint256 buyAmount = (sellAmount * 10 ** uint256(gemToken.decimals())).unsafeDiv(feeDivisor);
+
+            DAI.safeApproveIfBelow(address(psm), sellAmount);
+            psm.buyGem(recipient, buyAmount);
+        }
     }
 }
diff --git a/src/core/UniswapV2.sol b/src/core/UniswapV2.sol
index e6ff2b454..2e04af736 100644
--- a/src/core/UniswapV2.sol
+++ b/src/core/UniswapV2.sol
@@ -2,12 +2,12 @@
 pragma solidity ^0.8.21;
 
 import {ERC20} from "solmate/src/tokens/ERC20.sol";
-import {FullMath} from "../utils/FullMath.sol";
+import {UnsafeMath} from "../utils/UnsafeMath.sol";
 import {Panic} from "../utils/Panic.sol";
 import {VIPBase} from "./VIPBase.sol";
 
 abstract contract UniswapV2 is VIPBase {
-    using FullMath for uint256;
+    using UnsafeMath for uint256;
 
     // UniswapV2 Factory contract address prepended with '0xff' and left-aligned
     bytes32 private constant UNI_FF_FACTORY_ADDRESS = 0xFF5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f0000000000000000000000;
@@ -52,7 +52,10 @@ abstract contract UniswapV2 is VIPBase {
             Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
         }
 
-        uint256 sellAmount = ERC20(address(bytes20(encodedPath))).balanceOf(address(this)).mulDiv(bips, 10_000);
+        // We don't care about phantom overflow here because reserves are
+        // limited to 112 bits. Any token balance that would overflow here would
+        // also break UniV2.
+        uint256 sellAmount = (ERC20(address(bytes20(encodedPath))).balanceOf(address(this)) * bips).unsafeDiv(10_000);
         assembly ("memory-safe") {
             let ptr := mload(0x40)
             let swapCalldata := ptr
diff --git a/src/core/UniswapV3.sol b/src/core/UniswapV3.sol
index 832585d04..b50bd83b2 100644
--- a/src/core/UniswapV3.sol
+++ b/src/core/UniswapV3.sol
@@ -3,7 +3,7 @@ pragma solidity ^0.8.21;
 
 import {ERC20} from "solmate/src/tokens/ERC20.sol";
 import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol";
-import {FullMath} from "../utils/FullMath.sol";
+import {UnsafeMath} from "../utils/UnsafeMath.sol";
 import {Panic} from "../utils/Panic.sol";
 import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
 import {VIPBase} from "./VIPBase.sol";
@@ -31,7 +31,7 @@ interface IUniswapV3Pool {
 }
 
 abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
-    using FullMath for uint256;
+    using UnsafeMath for uint256;
     using SafeTransferLib for ERC20;
 
     /// @dev UniswapV3 Factory contract address prepended with '0xff' and left-aligned.
@@ -77,7 +77,10 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
     ) internal returns (uint256 buyAmount) {
         buyAmount = _swap(
             encodedPath,
-            ERC20(address(bytes20(encodedPath))).balanceOf(address(this)).mulDiv(bips, 10_000),
+            // We don't care about phantom overflow here because reserves are
+            // limited to 128 bits. Any token balance that would overflow here
+            // would also break UniV3.
+            (ERC20(address(bytes20(encodedPath))).balanceOf(address(this)) * bips).unsafeDiv(10_000),
             minBuyAmount,
             address(this), // payer
             recipient,

From 7389208a979a57e4db84b345f7046311cdf39d35 Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Sun, 22 Oct 2023 22:40:55 -0400
Subject: [PATCH 03/12] natspec

---
 src/core/MakerPSM.sol | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/core/MakerPSM.sol b/src/core/MakerPSM.sol
index b7b1775b4..623a36f0e 100644
--- a/src/core/MakerPSM.sol
+++ b/src/core/MakerPSM.sol
@@ -7,22 +7,22 @@ import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
 import {UnsafeMath} from "../utils/UnsafeMath.sol";
 
 interface IPSM {
-    // @dev Get the fee for selling DAI to USDC in PSM
-    // @return tout toll out [wad]
+    /// @dev Get the fee for selling DAI to USDC in PSM
+    /// @return tout toll out [wad]
     function tout() external view returns (uint256);
 
-    // @dev Get the address of the underlying vault powering PSM
-    // @return address of gemJoin contract
+    /// @dev Get the address of the underlying vault powering PSM
+    /// @return address of gemJoin contract
     function gemJoin() external view returns (address);
 
-    // @dev Sell USDC for DAI
-    // @param usr The address of the account trading USDC for DAI.
-    // @param gemAmt The amount of USDC to sell in USDC base units
+    /// @dev Sell USDC for DAI
+    /// @param usr The address of the account trading USDC for DAI.
+    /// @param gemAmt The amount of USDC to sell in USDC base units
     function sellGem(address usr, uint256 gemAmt) external;
 
-    // @dev Buy USDC for DAI
-    // @param usr The address of the account trading DAI for USDC
-    // @param gemAmt The amount of USDC to buy in USDC base units
+    /// @dev Buy USDC for DAI
+    /// @param usr The address of the account trading DAI for USDC
+    /// @param gemAmt The amount of USDC to buy in USDC base units
     function buyGem(address usr, uint256 gemAmt) external;
 }
 

From 2f47544a195777cadabe7973e8a17f9eed21f9ee Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Sun, 22 Oct 2023 22:42:10 -0400
Subject: [PATCH 04/12] Announce support for MakerPSM in compatibility matrix

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 5cee05a54..fe0cb8cbd 100644
--- a/README.md
+++ b/README.md
@@ -519,7 +519,7 @@ Where `actions` is added and contains the encoded actions the to perform.
 | KyberDMM     |    ✅     | |
 | KyberElastic |    ✅     | |
 | Lido         |    ✅     | |
-| MakerPsm     |    ❌     | Additional calculation required for `buyGem` |
+| MakerPsm     |    ✅     | Has VIP |
 | mStable      |    ✅     | |
 | Saddle       |    ✅     | Curve clone |
 | Shell        |    ✅     | |

From e52c6fdd5b9657269be07c3d0f8b18d0d31cfa69 Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Sun, 22 Oct 2023 22:54:34 -0400
Subject: [PATCH 05/12] Contract too large

---
 src/core/UniswapV3.sol | 38 +++++++++++++++++++++++---------------
 1 file changed, 23 insertions(+), 15 deletions(-)

diff --git a/src/core/UniswapV3.sol b/src/core/UniswapV3.sol
index b50bd83b2..720b2a7fe 100644
--- a/src/core/UniswapV3.sol
+++ b/src/core/UniswapV3.sol
@@ -157,12 +157,13 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
             bool zeroForOne;
             IUniswapV3Pool pool;
             {
-                ERC20 inputToken;
-                uint24 fee;
-                (inputToken, fee, outputToken) = _decodeFirstPoolInfoFromPath(encodedPath);
-                pool = _toPool(inputToken, fee, outputToken);
-                zeroForOne = inputToken < outputToken;
-                _updateSwapCallbackData(swapCallbackData, inputToken, outputToken, fee, payer);
+                (ERC20 token0, uint24 fee, ERC20 token1) = _decodeFirstPoolInfoFromPath(encodedPath);
+                outputToken = token1;
+                if (!(zeroForOne = token0 < token1)) {
+                    (token0, token1) = (token1, token0);
+                }
+                pool = _toPool(token0, fee, token1);
+                _updateSwapCallbackData(swapCallbackData, token0, fee, token1, payer);
             }
             (int256 amount0, int256 amount1) = pool.swap(
                 // Intermediate tokens go to this contract.
@@ -263,21 +264,21 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
     // Update `swapCallbackData` in place with new values.
     function _updateSwapCallbackData(
         bytes memory swapCallbackData,
-        ERC20 inputToken,
-        ERC20 outputToken,
+        ERC20 token0,
         uint24 fee,
+        ERC20 token1,
         address payer
     ) private pure {
         assembly ("memory-safe") {
-            mstore(add(swapCallbackData, 0x20), and(ADDRESS_MASK, inputToken))
-            mstore(add(swapCallbackData, 0x40), and(ADDRESS_MASK, outputToken))
-            mstore(add(swapCallbackData, 0x60), and(UINT24_MASK, fee))
+            mstore(add(swapCallbackData, 0x20), and(ADDRESS_MASK, token0))
+            mstore(add(swapCallbackData, 0x40), and(UINT24_MASK, fee))
+            mstore(add(swapCallbackData, 0x60), and(ADDRESS_MASK, token1))
             mstore(add(swapCallbackData, 0x80), and(ADDRESS_MASK, payer))
         }
     }
 
     // Compute the pool address given two tokens and a fee.
-    function _toPool(ERC20 inputToken, uint24 fee, ERC20 outputToken) private view returns (IUniswapV3Pool pool) {
+    function _toPool(ERC20 token0, uint24 fee, ERC20 token1) private view returns (IUniswapV3Pool pool) {
         // address(keccak256(abi.encodePacked(
         //     hex"ff",
         //     UNI_FACTORY_ADDRESS,
@@ -286,7 +287,6 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
         // )))
         bytes32 ffFactoryAddress = UNI_FF_FACTORY_ADDRESS;
         bytes32 poolInitCodeHash = UNI_POOL_INIT_CODE_HASH;
-        (ERC20 token0, ERC20 token1) = inputToken < outputToken ? (inputToken, outputToken) : (outputToken, inputToken);
         assembly ("memory-safe") {
             let s := mload(0x40)
             mstore(s, ffFactoryAddress)
@@ -313,8 +313,16 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
     ///        struct of: inputToken, outputToken, fee, payer
     function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
         // Decode the data.
-        (ERC20 token0, ERC20 token1, uint24 fee, address payer) = abi.decode(data, (ERC20, ERC20, uint24, address));
-        (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0);
+        ERC20 token0;
+        ERC20 token1;
+        uint24 fee;
+        address payer;
+        assembly ("memory-safe") {
+            token0 := calldataload(data.offset)
+            fee := calldataload(add(data.offset, 0x20))
+            token1 := calldataload(add(data.offset, 0x40))
+            payer := calldataload(add(data.offset, 0x60))
+        }
         // Only a valid pool contract can call this function.
         require(msg.sender == address(_toPool(token0, fee, token1)));
 

From 78d753b869649f716f08e973fd15faa51ca2019e Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Sun, 22 Oct 2023 23:24:44 -0400
Subject: [PATCH 06/12] Contract too large

---
 src/core/UniswapV3.sol | 43 +++++++++++++++++++++---------------------
 1 file changed, 22 insertions(+), 21 deletions(-)

diff --git a/src/core/UniswapV3.sol b/src/core/UniswapV3.sol
index 720b2a7fe..5f37f3c44 100644
--- a/src/core/UniswapV3.sol
+++ b/src/core/UniswapV3.sol
@@ -40,14 +40,14 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
     bytes32 private immutable UNI_POOL_INIT_CODE_HASH;
     /// @dev Minimum size of an encoded swap path:
     ///      sizeof(address(inputToken) | uint24(fee) | address(outputToken))
-    uint256 private constant SINGLE_HOP_PATH_SIZE = 43;
+    uint256 private constant SINGLE_HOP_PATH_SIZE = 0x2b;
     /// @dev How many bytes to skip ahead in an encoded path to start at the next hop:
     ///      sizeof(address(inputToken) | uint24(fee))
-    uint256 private constant PATH_SKIP_HOP_SIZE = 23;
+    uint256 private constant PATH_SKIP_HOP_SIZE = 0x17;
     /// @dev The size of the swap callback prefix data before the Permit2 data.
-    uint256 private constant SWAP_CALLBACK_PREFIX_DATA_SIZE = 0x80;
+    uint256 private constant SWAP_CALLBACK_PREFIX_DATA_SIZE = 0x3f;
     /// @dev The offset from the pointer to the length of the swap callback prefix data to the start of the Permit2 data.
-    uint256 private constant SWAP_CALLBACK_PERMIT2DATA_OFFSET = 0xa0;
+    uint256 private constant SWAP_CALLBACK_PERMIT2DATA_OFFSET = 0x5f;
     uint256 private constant PERMIT_DATA_SIZE = 0x80;
     /// @dev Minimum tick price sqrt ratio.
     uint160 private constant MIN_PRICE_SQRT_RATIO = 4295128739;
@@ -212,12 +212,9 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
             Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
         }
         assembly ("memory-safe") {
-            let p := add(encodedPath, 0x20)
-            inputToken := shr(0x60, mload(p))
-            p := add(p, 0x14)
-            fee := shr(0xe8, mload(p))
-            p := add(p, 0x03)
-            outputToken := shr(0x60, mload(p))
+            inputToken := mload(add(encodedPath, 0x14))
+            fee := mload(add(encodedPath, 0x17))
+            outputToken := mload(add(encodedPath, 0x2b))
         }
     }
 
@@ -270,10 +267,12 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
         address payer
     ) private pure {
         assembly ("memory-safe") {
-            mstore(add(swapCallbackData, 0x20), and(ADDRESS_MASK, token0))
-            mstore(add(swapCallbackData, 0x40), and(UINT24_MASK, fee))
-            mstore(add(swapCallbackData, 0x60), and(ADDRESS_MASK, token1))
-            mstore(add(swapCallbackData, 0x80), and(ADDRESS_MASK, payer))
+            let length := mload(swapCallbackData)
+            mstore(add(swapCallbackData, 0x3f), payer)
+            mstore(add(swapCallbackData, 0x2b), token1)
+            mstore(add(swapCallbackData, 0x17), fee)
+            mstore(add(swapCallbackData, 0x14), token0)
+            mstore(swapCallbackData, length)
         }
     }
 
@@ -309,19 +308,21 @@ abstract contract UniswapV3 is Permit2PaymentAbstract, VIPBase {
     ///      UniswapV3 pool.
     /// @param amount0Delta Token0 amount owed.
     /// @param amount1Delta Token1 amount owed.
-    /// @param data Arbitrary data forwarded from swap() caller. An ABI-encoded
-    ///        struct of: inputToken, outputToken, fee, payer
+    /// @param data Arbitrary data forwarded from swap() caller. A packed encoding of: inputToken, outputToken, fee, payer, abi.encode(permit, witness)
     function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
         // Decode the data.
         ERC20 token0;
-        ERC20 token1;
         uint24 fee;
+        ERC20 token1;
         address payer;
         assembly ("memory-safe") {
-            token0 := calldataload(data.offset)
-            fee := calldataload(add(data.offset, 0x20))
-            token1 := calldataload(add(data.offset, 0x40))
-            payer := calldataload(add(data.offset, 0x60))
+            {
+                let firstWord := calldataload(data.offset)
+                token0 := shr(0x60, firstWord)
+                fee := shr(0x48, firstWord)
+            }
+            token1 := calldataload(add(data.offset, 0xb))
+            payer := calldataload(add(data.offset, 0x1f))
         }
         // Only a valid pool contract can call this function.
         require(msg.sender == address(_toPool(token0, fee, token1)));

From da5934c5c0932d750b3ca922bb40a84f50dd37fa Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Mon, 23 Oct 2023 16:32:17 -0400
Subject: [PATCH 07/12] Contract too large

---
 .../settler_uniswapV2_DAI-WETH.snap           |   2 +-
 src/core/UniswapV2.sol                        | 107 +++++++++---------
 2 files changed, 53 insertions(+), 56 deletions(-)

diff --git a/.forge-snapshots/settler_uniswapV2_DAI-WETH.snap b/.forge-snapshots/settler_uniswapV2_DAI-WETH.snap
index 6ce86f0e0..ae472fd94 100644
--- a/.forge-snapshots/settler_uniswapV2_DAI-WETH.snap
+++ b/.forge-snapshots/settler_uniswapV2_DAI-WETH.snap
@@ -1 +1 @@
-136655
\ No newline at end of file
+136638
\ No newline at end of file
diff --git a/src/core/UniswapV2.sol b/src/core/UniswapV2.sol
index 2e04af736..58fe69aea 100644
--- a/src/core/UniswapV2.sol
+++ b/src/core/UniswapV2.sol
@@ -9,38 +9,33 @@ import {VIPBase} from "./VIPBase.sol";
 abstract contract UniswapV2 is VIPBase {
     using UnsafeMath for uint256;
 
-    // UniswapV2 Factory contract address prepended with '0xff' and left-aligned
-    bytes32 private constant UNI_FF_FACTORY_ADDRESS = 0xFF5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f0000000000000000000000;
+    // UniswapV2 Factory contract address prepended with '0xff'
+    uint256 private constant UNI_FF_FACTORY_ADDRESS = 0xff5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f;
     // UniswapV2 pool init code hash
     bytes32 private constant UNI_PAIR_INIT_CODE_HASH =
         0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
 
-    // SushiSwap Factory contract address prepended with '0xff' and left-aligned
-    bytes32 private constant SUSHI_FF_FACTORY_ADDRESS =
-        0xFFC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac0000000000000000000000;
+    // SushiSwap Factory contract address prepended with '0xff'
+    uint256 private constant SUSHI_FF_FACTORY_ADDRESS = 0xffc0aee478e3658e2610c5f7a4a2e1777ce9e4f2ac;
     // SushiSwap pool init code hash
     bytes32 private constant SUSHI_PAIR_INIT_CODE_HASH =
         0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303;
 
     // bytes4(keccak256("getReserves()"))
-    uint256 private constant UNI_PAIR_RESERVES_CALL_SELECTOR_32 =
-        0x0902f1ac00000000000000000000000000000000000000000000000000000000;
+    uint256 private constant UNI_PAIR_RESERVES_CALL_SELECTOR = 0x0902f1ac;
     // bytes4(keccak256("swap(uint256,uint256,address,bytes)"))
-    uint256 private constant UNI_PAIR_SWAP_CALL_SELECTOR_32 =
-        0x022c0d9f00000000000000000000000000000000000000000000000000000000;
+    uint256 private constant UNI_PAIR_SWAP_CALL_SELECTOR = 0x022c0d9f;
     // bytes4(keccak256("transfer(address,uint256)"))
-    uint256 private constant ERC20_TRANSFER_CALL_SELECTOR_32 =
-        0xa9059cbb00000000000000000000000000000000000000000000000000000000;
+    uint256 private constant ERC20_TRANSFER_CALL_SELECTOR = 0xa9059cbb;
     // bytes4(keccak256("balanceOf(address)"))
-    uint256 private constant ERC20_BALANCEOF_CALL_SELECTOR_32 =
-        0x70a0823100000000000000000000000000000000000000000000000000000000;
+    uint256 private constant ERC20_BALANCEOF_CALL_SELECTOR = 0x70a08231;
 
     // Minimum size of an encoded swap path:
     //   sizeof(address(sellToken) | uint8(hopInfo) | address(buyToken))
     // where first bit of `hopInfo` is `sellTokenHasFee` and the rest is `fork`
-    uint256 private constant SINGLE_HOP_PATH_SIZE = 20 + 1 + 20;
+    uint256 private constant SINGLE_HOP_PATH_SIZE = 0x29;
     // Number of bytes to shift the path by each hop
-    uint256 private constant HOP_SHIFT_SIZE = 20 + 1;
+    uint256 private constant HOP_SHIFT_SIZE = 0x15;
 
     /// @dev Sell a token for another token using UniswapV2.
     /// @param encodedPath Custom encoded path of the swap.
@@ -58,30 +53,30 @@ abstract contract UniswapV2 is VIPBase {
         uint256 sellAmount = (ERC20(address(bytes20(encodedPath))).balanceOf(address(this)) * bips).unsafeDiv(10_000);
         assembly ("memory-safe") {
             let ptr := mload(0x40)
-            let swapCalldata := ptr
+            let swapCalldata := add(ptr, 0x1c)
             let fromPool
 
             // set up swap call selector and empty callback data
-            mstore(ptr, UNI_PAIR_SWAP_CALL_SELECTOR_32)
-            mstore(add(ptr, 100), 128) // offset to length of data
-            mstore(add(ptr, 132), 0) // length of data
+            mstore(ptr, UNI_PAIR_SWAP_CALL_SELECTOR)
+            mstore(add(ptr, 0x80), 0x80) // offset to length of data
+            mstore(add(ptr, 0xa0), 0) // length of data
 
-            // 4b selector, 32b amount0Out, 32b amount1Out, 32b to, 64b data
-            ptr := add(ptr, 164)
+            // 28b padding, 4b selector, 32b amount0Out, 32b amount1Out, 32b to, 64b data
+            ptr := add(ptr, 0xc0)
 
             for {
                 let pathLength := mload(encodedPath)
-                let path := add(encodedPath, 32)
+                let path := add(encodedPath, 0x20)
             } iszero(lt(pathLength, SINGLE_HOP_PATH_SIZE)) {
                 pathLength := sub(pathLength, HOP_SHIFT_SIZE)
                 path := add(path, HOP_SHIFT_SIZE)
             } {
                 // decode hop info
-                let buyToken := shr(96, mload(add(path, 21)))
-                let sellToken := shr(88, mload(path))
+                let buyToken := shr(0x60, mload(add(path, HOP_SHIFT_SIZE)))
+                let sellToken := shr(0x58, mload(path))
                 let sellTokenHasFee := and(0x80, sellToken)
                 let fork := and(0x7f, sellToken)
-                sellToken := shr(8, sellToken)
+                sellToken := shr(0x08, sellToken)
                 let zeroForOne := lt(sellToken, buyToken)
 
                 // compute the pool address
@@ -93,55 +88,56 @@ abstract contract UniswapV2 is VIPBase {
                 // )))
                 switch zeroForOne
                 case 0 {
-                    mstore(add(ptr, 20), sellToken)
+                    mstore(add(ptr, 0x14), sellToken)
                     mstore(ptr, buyToken)
                 }
                 default {
-                    mstore(add(ptr, 20), buyToken)
+                    mstore(add(ptr, 0x14), buyToken)
                     mstore(ptr, sellToken)
                 }
-                let salt := keccak256(add(ptr, 12), 40)
+                let salt := keccak256(add(ptr, 0x0c), 0x28)
                 switch fork
                 case 0 {
                     // univ2
                     mstore(ptr, UNI_FF_FACTORY_ADDRESS)
-                    mstore(add(ptr, 21), salt)
-                    mstore(add(ptr, 53), UNI_PAIR_INIT_CODE_HASH)
+                    mstore(add(ptr, 0x20), salt)
+                    mstore(add(ptr, 0x40), UNI_PAIR_INIT_CODE_HASH)
                 }
                 case 1 {
                     // sushi
                     mstore(ptr, SUSHI_FF_FACTORY_ADDRESS)
-                    mstore(add(ptr, 21), salt)
-                    mstore(add(ptr, 53), SUSHI_PAIR_INIT_CODE_HASH)
+                    mstore(add(ptr, 0x20), salt)
+                    mstore(add(ptr, 0x40), SUSHI_PAIR_INIT_CODE_HASH)
                 }
                 default { revert(0, 0) }
-                let toPool := keccak256(ptr, 85)
+                let toPool := and(0xffffffffffffffffffffffffffffffffffffffff, keccak256(add(ptr, 0x0b), 0x55))
 
                 // if the next pool is the initial pool, transfer tokens from the settler to the initial pool
                 // otherwise, swap tokens and send to the next pool
                 switch fromPool
                 case 0 {
                     // transfer sellAmount of sellToken to the pool
-                    mstore(ptr, ERC20_TRANSFER_CALL_SELECTOR_32)
-                    mstore(add(ptr, 4), toPool)
-                    mstore(add(ptr, 36), sellAmount)
-                    if iszero(call(gas(), sellToken, 0, ptr, 68, ptr, 32)) { bubbleRevert() }
-                    if iszero(or(iszero(returndatasize()), and(iszero(lt(returndatasize(), 32)), eq(mload(ptr), 1)))) {
+                    mstore(ptr, ERC20_TRANSFER_CALL_SELECTOR)
+                    mstore(add(ptr, 0x20), toPool)
+                    mstore(add(ptr, 0x40), sellAmount)
+                    if iszero(call(gas(), sellToken, 0, add(ptr, 0x1c), 0x44, ptr, 0x20)) { bubbleRevert(swapCalldata) }
+                    if iszero(or(iszero(returndatasize()), and(iszero(lt(returndatasize(), 0x20)), eq(mload(ptr), 1))))
+                    {
                         revert(0, 0)
                     }
                 }
                 default {
                     // perform swap at the fromPool sending bought tokens to the toPool
-                    mstore(add(swapCalldata, 68), toPool)
-                    if iszero(call(gas(), fromPool, 0, swapCalldata, 164, 0, 0)) { bubbleRevert() }
+                    mstore(add(swapCalldata, 0x44), toPool)
+                    if iszero(call(gas(), fromPool, 0, swapCalldata, 0xa4, 0, 0)) { bubbleRevert(swapCalldata) }
                 }
 
                 // get toPool reserves
                 let sellReserve
                 let buyReserve
-                mstore(ptr, UNI_PAIR_RESERVES_CALL_SELECTOR_32)
-                if iszero(staticcall(gas(), toPool, ptr, 4, ptr, 64)) { bubbleRevert() }
-                if lt(returndatasize(), 64) { revert(0, 0) }
+                mstore(ptr, UNI_PAIR_RESERVES_CALL_SELECTOR)
+                if iszero(staticcall(gas(), toPool, add(ptr, 0x1c), 0x04, ptr, 0x40)) { bubbleRevert(swapCalldata) }
+                if lt(returndatasize(), 0x40) { revert(0, 0) }
                 switch zeroForOne
                 case 0 {
                     sellReserve := mload(add(ptr, 32))
@@ -155,10 +151,12 @@ abstract contract UniswapV2 is VIPBase {
                 // if the sellToken has a fee on transfer, determine the real sellAmount
                 if sellTokenHasFee {
                     // retrieve the sellToken balance of the pool
-                    mstore(ptr, ERC20_BALANCEOF_CALL_SELECTOR_32)
-                    mstore(add(ptr, 4), toPool)
-                    if iszero(staticcall(gas(), sellToken, ptr, 36, ptr, 32)) { bubbleRevert() }
-                    if lt(returndatasize(), 32) { revert(0, 0) }
+                    mstore(ptr, ERC20_BALANCEOF_CALL_SELECTOR)
+                    mstore(add(ptr, 0x20), toPool)
+                    if iszero(staticcall(gas(), sellToken, add(ptr, 0x1c), 0x24, ptr, 0x20)) {
+                        bubbleRevert(swapCalldata)
+                    }
+                    if lt(returndatasize(), 0x20) { revert(0, 0) }
                     let bal := mload(ptr)
 
                     // determine real sellAmount by comparing pool's sellToken balance to reserve amount
@@ -177,12 +175,12 @@ abstract contract UniswapV2 is VIPBase {
                 // set amount0Out and amount1Out
                 switch zeroForOne
                 case 0 {
-                    mstore(add(swapCalldata, 4), buyAmount)
-                    mstore(add(swapCalldata, 36), 0)
+                    mstore(add(swapCalldata, 0x04), buyAmount)
+                    mstore(add(swapCalldata, 0x24), 0)
                 }
                 default {
-                    mstore(add(swapCalldata, 4), 0)
-                    mstore(add(swapCalldata, 36), buyAmount)
+                    mstore(add(swapCalldata, 0x04), 0)
+                    mstore(add(swapCalldata, 0x24), buyAmount)
                 }
 
                 // shift pools and amounts for next iteration
@@ -193,13 +191,12 @@ abstract contract UniswapV2 is VIPBase {
             // final swap
             if fromPool {
                 // perform swap at the fromPool sending bought tokens to settler
-                mstore(add(swapCalldata, 68), recipient)
-                if iszero(call(gas(), fromPool, 0, swapCalldata, 164, 0, 0)) { bubbleRevert() }
+                mstore(add(swapCalldata, 0x44), and(0xffffffffffffffffffffffffffffffffffffffff, recipient))
+                if iszero(call(gas(), fromPool, 0, swapCalldata, 0xa4, 0, 0)) { bubbleRevert(swapCalldata) }
             }
 
             // revert with the return data from the most recent call
-            function bubbleRevert() {
-                let p := mload(0x40)
+            function bubbleRevert(p) {
                 returndatacopy(p, 0, returndatasize())
                 revert(p, returndatasize())
             }

From 0e0ea00b94e17c67767293bda42cc8cc07bbd3fc Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Mon, 23 Oct 2023 18:45:41 -0400
Subject: [PATCH 08/12] Drop support for ZeroEx (protocol v4) through a VIP;
 limit orders are supported through BASIC_SELL

---
 .../settler_zeroExOtc_DAI-WETH.snap           |  2 +-
 .../settler_zeroExOtc_USDC-WETH.snap          |  2 +-
 .../settler_zeroExOtc_USDT-WETH.snap          |  2 +-
 src/ISettlerActions.sol                       |  7 --
 src/Settler.sol                               | 18 +-----
 src/core/ZeroEx.sol                           | 64 -------------------
 test/integration/SettlerPairTest.t.sol        | 41 ++++++------
 test/integration/WethWrapTest.t.sol           |  1 -
 8 files changed, 25 insertions(+), 112 deletions(-)
 delete mode 100644 src/core/ZeroEx.sol

diff --git a/.forge-snapshots/settler_zeroExOtc_DAI-WETH.snap b/.forge-snapshots/settler_zeroExOtc_DAI-WETH.snap
index 2489c3749..d1c07ec30 100644
--- a/.forge-snapshots/settler_zeroExOtc_DAI-WETH.snap
+++ b/.forge-snapshots/settler_zeroExOtc_DAI-WETH.snap
@@ -1 +1 @@
-145780
\ No newline at end of file
+172912
\ No newline at end of file
diff --git a/.forge-snapshots/settler_zeroExOtc_USDC-WETH.snap b/.forge-snapshots/settler_zeroExOtc_USDC-WETH.snap
index 007ed85c0..1c6fb416e 100644
--- a/.forge-snapshots/settler_zeroExOtc_USDC-WETH.snap
+++ b/.forge-snapshots/settler_zeroExOtc_USDC-WETH.snap
@@ -1 +1 @@
-174977
\ No newline at end of file
+202822
\ No newline at end of file
diff --git a/.forge-snapshots/settler_zeroExOtc_USDT-WETH.snap b/.forge-snapshots/settler_zeroExOtc_USDT-WETH.snap
index ee5932aa1..a14b7c9e0 100644
--- a/.forge-snapshots/settler_zeroExOtc_USDT-WETH.snap
+++ b/.forge-snapshots/settler_zeroExOtc_USDT-WETH.snap
@@ -1 +1 @@
-160683
\ No newline at end of file
+188244
\ No newline at end of file
diff --git a/src/ISettlerActions.sol b/src/ISettlerActions.sol
index 96dc654b4..59b09f7e6 100644
--- a/src/ISettlerActions.sol
+++ b/src/ISettlerActions.sol
@@ -2,7 +2,6 @@
 pragma solidity ^0.8.21;
 
 import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol";
-import {IZeroEx} from "./core/ZeroEx.sol";
 
 interface ISettlerActions {
     /// @dev Transfer funds from msg.sender Permit2.
@@ -79,12 +78,6 @@ interface ISettlerActions {
 
     function POSITIVE_SLIPPAGE(address token, address recipient, uint256 expectedAmount) external;
 
-    // @dev Fill a 0x V4 OTC order using the 0x Exchange Proxy contract
-    // Pre-req: Funded
-    // Post-req: Payout
-    function ZERO_EX_OTC(IZeroEx.OtcOrder memory order, IZeroEx.Signature memory signature, uint256 sellAmount)
-        external;
-
     /// @dev Trades against a basic AMM which follows the approval, transferFrom(msg.sender) interaction
     // Pre-req: Funded
     // Post-req: Payout
diff --git a/src/Settler.sol b/src/Settler.sol
index f792fe52d..b630b70b7 100644
--- a/src/Settler.sol
+++ b/src/Settler.sol
@@ -10,7 +10,6 @@ import {OtcOrderSettlement} from "./core/OtcOrderSettlement.sol";
 import {UniswapV3} from "./core/UniswapV3.sol";
 import {UniswapV2} from "./core/UniswapV2.sol";
 import {IPSM, MakerPSM} from "./core/MakerPSM.sol";
-import {IZeroEx, ZeroEx} from "./core/ZeroEx.sol";
 
 import {SafeTransferLib} from "./utils/SafeTransferLib.sol";
 import {UnsafeMath} from "./utils/UnsafeMath.sol";
@@ -72,7 +71,7 @@ library CalldataDecoder {
     }
 }
 
-contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, UniswapV2, MakerPSM, ZeroEx, FreeMemory {
+contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, UniswapV2, MakerPSM, FreeMemory {
     using SafeTransferLib for ERC20;
     using SafeTransferLib for address payable;
     using UnsafeMath for uint256;
@@ -95,21 +94,13 @@ contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, Uniswa
 
     receive() external payable {}
 
-    constructor(
-        address permit2,
-        address zeroEx,
-        address uniFactory,
-        bytes32 poolInitCodeHash,
-        address dai,
-        address feeRecipient
-    )
+    constructor(address permit2, address uniFactory, bytes32 poolInitCodeHash, address dai, address feeRecipient)
         Permit2Payment(permit2, feeRecipient)
         Basic()
         OtcOrderSettlement()
         UniswapV3(uniFactory, poolInitCodeHash)
         UniswapV2()
         MakerPSM(dai)
-        ZeroEx(zeroEx)
     {
         assert(ACTIONS_AND_SLIPPAGE_TYPEHASH == keccak256(bytes(ACTIONS_AND_SLIPPAGE_TYPE)));
     }
@@ -380,11 +371,6 @@ contract Settler is Permit2Payment, Basic, OtcOrderSettlement, UniswapV3, Uniswa
                     }
                 }
             }
-        } else if (action == ISettlerActions.ZERO_EX_OTC.selector) {
-            (IZeroEx.OtcOrder memory order, IZeroEx.Signature memory signature, uint256 sellAmount) =
-                abi.decode(data, (IZeroEx.OtcOrder, IZeroEx.Signature, uint256));
-
-            sellTokenForTokenToZeroExOTC(order, signature, sellAmount);
         } else {
             revert ActionInvalid({i: i, action: action, data: data});
         }
diff --git a/src/core/ZeroEx.sol b/src/core/ZeroEx.sol
deleted file mode 100644
index 84d13eeff..000000000
--- a/src/core/ZeroEx.sol
+++ /dev/null
@@ -1,64 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.21;
-
-import {ERC20} from "solmate/src/tokens/ERC20.sol";
-
-import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
-
-interface IZeroEx {
-    /// @dev Allowed signature types.
-    enum SignatureType {
-        ILLEGAL,
-        INVALID,
-        EIP712,
-        ETHSIGN,
-        PRESIGNED
-    }
-
-    /// @dev Encoded EC signature.
-    struct Signature {
-        // How to validate the signature.
-        SignatureType signatureType;
-        // EC Signature data.
-        uint8 v;
-        // EC Signature data.
-        bytes32 r;
-        // EC Signature data.
-        bytes32 s;
-    }
-
-    /// @dev An OTC limit order.
-    struct OtcOrder {
-        ERC20 makerToken;
-        ERC20 takerToken;
-        uint128 makerAmount;
-        uint128 takerAmount;
-        address maker;
-        address taker;
-        address txOrigin;
-        uint256 expiryAndNonce; // [uint64 expiry, uint64 nonceBucket, uint128 nonce]
-    }
-
-    function fillOtcOrder(OtcOrder calldata order, Signature calldata makerSignature, uint128 takerTokenFillAmount)
-        external
-        returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);
-}
-
-abstract contract ZeroEx {
-    using SafeTransferLib for ERC20;
-
-    IZeroEx private immutable ZERO_EX;
-
-    constructor(address zeroEx) {
-        ZERO_EX = IZeroEx(zeroEx);
-    }
-
-    function sellTokenForTokenToZeroExOTC(
-        IZeroEx.OtcOrder memory order,
-        IZeroEx.Signature memory signature,
-        uint256 sellAmount
-    ) internal {
-        order.takerToken.safeApproveIfBelow(address(ZERO_EX), type(uint256).max);
-        ZERO_EX.fillOtcOrder(order, signature, uint128(sellAmount));
-    }
-}
diff --git a/test/integration/SettlerPairTest.t.sol b/test/integration/SettlerPairTest.t.sol
index 468544fc7..9c65d2106 100644
--- a/test/integration/SettlerPairTest.t.sol
+++ b/test/integration/SettlerPairTest.t.sol
@@ -70,7 +70,6 @@ abstract contract SettlerPairTest is BasePairTest {
     function getSettler() private returns (Settler) {
         return new Settler(
             address(PERMIT2),
-            address(ZERO_EX), // ZeroEx
             0x1F98431c8aD98523631AE4a59f267346ea31F984, // UniV3 Factory
             0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, // UniV3 pool init code hash
             0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI
@@ -81,22 +80,31 @@ abstract contract SettlerPairTest is BasePairTest {
     function testSettler_zeroExOtcOrder() public {
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(MAKER_PRIVATE_KEY, otcOrderHash);
 
-        // TODO can use safer encodeCall
-        bytes[] memory actions = new bytes[](2);
-        actions[0] = _getDefaultFromPermit2Action();
-        actions[1] = abi.encodeWithSelector(
-            ISettlerActions.ZERO_EX_OTC.selector,
-            otcOrder,
-            IZeroEx.Signature(IZeroEx.SignatureType.EIP712, v, r, s),
-            amount()
+        bytes[] memory actions = ActionDataBuilder.build(
+            _getDefaultFromPermit2Action(),
+            abi.encodeCall(
+                ISettlerActions.BASIC_SELL,
+                (
+                    address(ZERO_EX),
+                    address(fromToken()),
+                    10_000,
+                    0x184,
+                    abi.encodeCall(
+                        ZERO_EX.fillOtcOrder, (otcOrder, IZeroEx.Signature(IZeroEx.SignatureType.EIP712, v, r, s), 0)
+                        )
+                )
+            )
         );
 
         Settler _settler = settler;
+        Settler.AllowedSlippage memory allowedSlippage = Settler.AllowedSlippage({
+            buyToken: address(otcOrder.makerToken),
+            recipient: FROM,
+            minAmountOut: otcOrder.makerAmount
+        });
         vm.startPrank(FROM, FROM);
         snapStartName("settler_zeroExOtc");
-        _settler.execute(
-            actions, Settler.AllowedSlippage({buyToken: address(0), recipient: address(0), minAmountOut: 0 ether})
-        );
+        _settler.execute(actions, allowedSlippage);
         snapEnd();
     }
 
@@ -202,15 +210,6 @@ abstract contract SettlerPairTest is BasePairTest {
     }
 
     function testSettler_uniswapV3_sellToken_fee_full_custody() public {
-        ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({
-            permitted: ISignatureTransfer.TokenPermissions({token: address(fromToken()), amount: amount()}),
-            nonce: PERMIT2_FROM_NONCE,
-            deadline: block.timestamp + 100
-        });
-
-        bytes memory sig =
-            getPermitTransferSignature(permit, address(settler), FROM_PRIVATE_KEY, PERMIT2.DOMAIN_SEPARATOR());
-
         bytes[] memory actions = ActionDataBuilder.build(
             _getDefaultFromPermit2Action(),
             abi.encodeCall(
diff --git a/test/integration/WethWrapTest.t.sol b/test/integration/WethWrapTest.t.sol
index 4c971aa0f..53f630d59 100644
--- a/test/integration/WethWrapTest.t.sol
+++ b/test/integration/WethWrapTest.t.sol
@@ -20,7 +20,6 @@ contract WethWrapTest is Test, GasSnapshot {
 
         _settler = new Settler(
             0x000000000022D473030F116dDEE9F6B43aC78BA3, // Permit2
-            0xDef1C0ded9bec7F1a1670819833240f027b25EfF, // ZeroEx
             0x1F98431c8aD98523631AE4a59f267346ea31F984, // UniV3 Factory
             0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54, // UniV3 pool init code hash
             0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI

From 64f40a2d397f4c2811f3ad3af77e39c1d45dd097 Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Mon, 23 Oct 2023 18:53:57 -0400
Subject: [PATCH 09/12] Add OTC partial fill test

---
 ...ettler_zeroExOtc_partialFill_DAI-WETH.snap |  1 +
 ...ttler_zeroExOtc_partialFill_USDC-WETH.snap |  1 +
 ...ttler_zeroExOtc_partialFill_USDT-WETH.snap |  1 +
 test/integration/SettlerPairTest.t.sol        | 41 +++++++++++++++++++
 4 files changed, 44 insertions(+)
 create mode 100644 .forge-snapshots/settler_zeroExOtc_partialFill_DAI-WETH.snap
 create mode 100644 .forge-snapshots/settler_zeroExOtc_partialFill_USDC-WETH.snap
 create mode 100644 .forge-snapshots/settler_zeroExOtc_partialFill_USDT-WETH.snap

diff --git a/.forge-snapshots/settler_zeroExOtc_partialFill_DAI-WETH.snap b/.forge-snapshots/settler_zeroExOtc_partialFill_DAI-WETH.snap
new file mode 100644
index 000000000..f34e35ccc
--- /dev/null
+++ b/.forge-snapshots/settler_zeroExOtc_partialFill_DAI-WETH.snap
@@ -0,0 +1 @@
+180742
\ No newline at end of file
diff --git a/.forge-snapshots/settler_zeroExOtc_partialFill_USDC-WETH.snap b/.forge-snapshots/settler_zeroExOtc_partialFill_USDC-WETH.snap
new file mode 100644
index 000000000..594862187
--- /dev/null
+++ b/.forge-snapshots/settler_zeroExOtc_partialFill_USDC-WETH.snap
@@ -0,0 +1 @@
+212708
\ No newline at end of file
diff --git a/.forge-snapshots/settler_zeroExOtc_partialFill_USDT-WETH.snap b/.forge-snapshots/settler_zeroExOtc_partialFill_USDT-WETH.snap
new file mode 100644
index 000000000..1f8e928ea
--- /dev/null
+++ b/.forge-snapshots/settler_zeroExOtc_partialFill_USDT-WETH.snap
@@ -0,0 +1 @@
+197858
\ No newline at end of file
diff --git a/test/integration/SettlerPairTest.t.sol b/test/integration/SettlerPairTest.t.sol
index 9c65d2106..8b4afc33e 100644
--- a/test/integration/SettlerPairTest.t.sol
+++ b/test/integration/SettlerPairTest.t.sol
@@ -108,6 +108,47 @@ abstract contract SettlerPairTest is BasePairTest {
         snapEnd();
     }
 
+    function testSettler_zeroExOtcOrder_partialFill() public {
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(MAKER_PRIVATE_KEY, otcOrderHash);
+
+        bytes[] memory actions = ActionDataBuilder.build(
+            _getDefaultFromPermit2Action(),
+            abi.encodeCall(
+                ISettlerActions.BASIC_SELL,
+                (
+                    address(ZERO_EX),
+                    address(fromToken()),
+                    5_000,
+                    0x184,
+                    abi.encodeCall(
+                        ZERO_EX.fillOtcOrder, (otcOrder, IZeroEx.Signature(IZeroEx.SignatureType.EIP712, v, r, s), 0)
+                        )
+                )
+            ),
+            abi.encodeCall(
+                ISettlerActions.BASIC_SELL,
+                (
+                    address(fromToken()),
+                    address(fromToken()),
+                    10_000,
+                    0x24,
+                    abi.encodeCall(fromToken().transfer, (FROM, 0))
+                )
+            )
+        );
+
+        Settler _settler = settler;
+        Settler.AllowedSlippage memory allowedSlippage = Settler.AllowedSlippage({
+            buyToken: address(otcOrder.makerToken),
+            recipient: FROM,
+            minAmountOut: otcOrder.makerAmount / 2
+        });
+        vm.startPrank(FROM, FROM);
+        snapStartName("settler_zeroExOtc_partialFill");
+        _settler.execute(actions, allowedSlippage);
+        snapEnd();
+    }
+
     function testSettler_uniswapV3VIP() public {
         (ISignatureTransfer.PermitTransferFrom memory permit, bytes memory sig) = _getDefaultFromPermit2();
         bytes[] memory actions = ActionDataBuilder.build(

From bc6230c8ff48d81483b24f0669ebedf838ce6d64 Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Mon, 23 Oct 2023 18:56:41 -0400
Subject: [PATCH 10/12] Update gas table

---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index fe0cb8cbd..ed5dfdfc5 100644
--- a/README.md
+++ b/README.md
@@ -74,15 +74,15 @@ Note: The following is more akin to `gasLimit` than it is `gasUsed`, this is due
 | ------- | ------- | --------- | ------ | ------ |
 | 0x V4   | 0x V4   | USDC/WETH | 112785 | 0.00%  |
 | Settler | Settler | USDC/WETH | 113732 | 0.84%  |
-| Settler | 0x V4   | USDC/WETH | 174977 | 55.14% |
+| Settler | 0x V4   | USDC/WETH | 202822 | 79.83% |
 |         |         |           |        |        |
 | 0x V4   | 0x V4   | DAI/WETH  | 93311  | 0.00%  |
 | Settler | Settler | DAI/WETH  | 94258  | 1.01%  |
-| Settler | 0x V4   | DAI/WETH  | 145780 | 56.23% |
+| Settler | 0x V4   | DAI/WETH  | 172912 | 85.31% |
 |         |         |           |        |        |
 | 0x V4   | 0x V4   | USDT/WETH | 104423 | 0.00%  |
 | Settler | Settler | USDT/WETH | 105370 | 0.91%  |
-| Settler | 0x V4   | USDT/WETH | 160683 | 53.88% |
+| Settler | 0x V4   | USDT/WETH | 188244 | 80.27% |
 |         |         |           |        |        |
 
 | Curve             | DEX   | Pair      | Gas    | %       |

From 85a0ebab5aad2a8715fcc4ca0bfa69d5468fe7dd Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Mon, 23 Oct 2023 20:10:43 -0400
Subject: [PATCH 11/12] Add test for UniV2 multihop

---
 .../settler_uniswapV2_USDC-WETH.snap          |  2 +-
 .../settler_uniswapV2_USDT-WETH.snap          |  2 +-
 .../settler_uniswapV2_multihop_DAI-WETH.snap  |  1 +
 .../settler_uniswapV2_multihop_USDC-WETH.snap |  1 +
 .../settler_uniswapV2_multihop_USDT-WETH.snap |  1 +
 test/integration/SettlerPairTest.t.sol        | 19 +++++++++++++++++++
 6 files changed, 24 insertions(+), 2 deletions(-)
 create mode 100644 .forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap
 create mode 100644 .forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap
 create mode 100644 .forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap

diff --git a/.forge-snapshots/settler_uniswapV2_USDC-WETH.snap b/.forge-snapshots/settler_uniswapV2_USDC-WETH.snap
index bb325a196..72febbd1b 100644
--- a/.forge-snapshots/settler_uniswapV2_USDC-WETH.snap
+++ b/.forge-snapshots/settler_uniswapV2_USDC-WETH.snap
@@ -1 +1 @@
-160506
\ No newline at end of file
+160489
\ No newline at end of file
diff --git a/.forge-snapshots/settler_uniswapV2_USDT-WETH.snap b/.forge-snapshots/settler_uniswapV2_USDT-WETH.snap
index 998a9339c..3bcce22ab 100644
--- a/.forge-snapshots/settler_uniswapV2_USDT-WETH.snap
+++ b/.forge-snapshots/settler_uniswapV2_USDT-WETH.snap
@@ -1 +1 @@
-153015
\ No newline at end of file
+142580
\ No newline at end of file
diff --git a/.forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap b/.forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap
new file mode 100644
index 000000000..81e6cb09d
--- /dev/null
+++ b/.forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap
@@ -0,0 +1 @@
+193513
\ No newline at end of file
diff --git a/.forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap b/.forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap
new file mode 100644
index 000000000..12d09eb38
--- /dev/null
+++ b/.forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap
@@ -0,0 +1 @@
+217364
\ No newline at end of file
diff --git a/.forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap b/.forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap
new file mode 100644
index 000000000..9de3924f4
--- /dev/null
+++ b/.forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap
@@ -0,0 +1 @@
+199455
\ No newline at end of file
diff --git a/test/integration/SettlerPairTest.t.sol b/test/integration/SettlerPairTest.t.sol
index 8b4afc33e..a2705ccf3 100644
--- a/test/integration/SettlerPairTest.t.sol
+++ b/test/integration/SettlerPairTest.t.sol
@@ -290,6 +290,25 @@ abstract contract SettlerPairTest is BasePairTest {
         snapEnd();
     }
 
+    function testSettler_uniswapV2_multihop() public {
+        address wBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
+        bytes[] memory actions = ActionDataBuilder.build(
+            _getDefaultFromPermit2Action(),
+            abi.encodeCall(
+                ISettlerActions.UNISWAPV2_SWAP,
+                (FROM, 10_000, 0, bytes.concat(uniswapV2Path(), bytes1(0x00), bytes20(uint160(wBTC))))
+            )
+        );
+
+        Settler _settler = settler;
+        vm.startPrank(FROM);
+        snapStartName("settler_uniswapV2_multihop");
+        _settler.execute(
+            actions, Settler.AllowedSlippage({buyToken: address(0), recipient: address(0), minAmountOut: 0 ether})
+        );
+        snapEnd();
+    }
+
     function testSettler_curveV2_fee() public skipIf(getCurveV2PoolData().pool == address(0)) {
         ICurveV2Pool.CurveV2PoolData memory poolData = getCurveV2PoolData();
 

From d5607189f1a3bf4955c1f30b835e3ec474cdf1dd Mon Sep 17 00:00:00 2001
From: Duncan Townsend <git@duncancmt.com>
Date: Mon, 23 Oct 2023 20:17:56 -0400
Subject: [PATCH 12/12] Golf UniV2 multihop

---
 .forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap | 2 +-
 .../settler_uniswapV2_multihop_USDC-WETH.snap             | 2 +-
 .../settler_uniswapV2_multihop_USDT-WETH.snap             | 2 +-
 src/core/UniswapV2.sol                                    | 7 ++-----
 test/integration/SettlerPairTest.t.sol                    | 8 ++++++--
 5 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/.forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap b/.forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap
index 81e6cb09d..6c53c923e 100644
--- a/.forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap
+++ b/.forge-snapshots/settler_uniswapV2_multihop_DAI-WETH.snap
@@ -1 +1 @@
-193513
\ No newline at end of file
+189118
\ No newline at end of file
diff --git a/.forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap b/.forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap
index 12d09eb38..c84b9112d 100644
--- a/.forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap
+++ b/.forge-snapshots/settler_uniswapV2_multihop_USDC-WETH.snap
@@ -1 +1 @@
-217364
\ No newline at end of file
+212990
\ No newline at end of file
diff --git a/.forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap b/.forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap
index 9de3924f4..50ea82110 100644
--- a/.forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap
+++ b/.forge-snapshots/settler_uniswapV2_multihop_USDT-WETH.snap
@@ -1 +1 @@
-199455
\ No newline at end of file
+195056
\ No newline at end of file
diff --git a/src/core/UniswapV2.sol b/src/core/UniswapV2.sol
index 58fe69aea..a2dcce3df 100644
--- a/src/core/UniswapV2.sol
+++ b/src/core/UniswapV2.sol
@@ -65,12 +65,9 @@ abstract contract UniswapV2 is VIPBase {
             ptr := add(ptr, 0xc0)
 
             for {
-                let pathLength := mload(encodedPath)
                 let path := add(encodedPath, 0x20)
-            } iszero(lt(pathLength, SINGLE_HOP_PATH_SIZE)) {
-                pathLength := sub(pathLength, HOP_SHIFT_SIZE)
-                path := add(path, HOP_SHIFT_SIZE)
-            } {
+                let end := add(add(encodedPath, mload(encodedPath)), 0x0c)
+            } lt(path, end) { path := add(path, HOP_SHIFT_SIZE) } {
                 // decode hop info
                 let buyToken := shr(0x60, mload(add(path, HOP_SHIFT_SIZE)))
                 let sellToken := shr(0x58, mload(path))
diff --git a/test/integration/SettlerPairTest.t.sol b/test/integration/SettlerPairTest.t.sol
index a2705ccf3..dcabcd2a5 100644
--- a/test/integration/SettlerPairTest.t.sol
+++ b/test/integration/SettlerPairTest.t.sol
@@ -291,15 +291,17 @@ abstract contract SettlerPairTest is BasePairTest {
     }
 
     function testSettler_uniswapV2_multihop() public {
-        address wBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
+        ERC20 wBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
         bytes[] memory actions = ActionDataBuilder.build(
             _getDefaultFromPermit2Action(),
             abi.encodeCall(
                 ISettlerActions.UNISWAPV2_SWAP,
-                (FROM, 10_000, 0, bytes.concat(uniswapV2Path(), bytes1(0x00), bytes20(uint160(wBTC))))
+                (FROM, 10_000, 0, bytes.concat(uniswapV2Path(), bytes1(0x00), bytes20(uint160(address(wBTC)))))
             )
         );
 
+        uint256 balanceBefore = wBTC.balanceOf(FROM);
+
         Settler _settler = settler;
         vm.startPrank(FROM);
         snapStartName("settler_uniswapV2_multihop");
@@ -307,6 +309,8 @@ abstract contract SettlerPairTest is BasePairTest {
             actions, Settler.AllowedSlippage({buyToken: address(0), recipient: address(0), minAmountOut: 0 ether})
         );
         snapEnd();
+
+        assertGt(wBTC.balanceOf(FROM), balanceBefore);
     }
 
     function testSettler_curveV2_fee() public skipIf(getCurveV2PoolData().pool == address(0)) {