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)) {