diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d1e26b..874b1965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,8 +45,9 @@ Master list of UniV3 forks: * `UNISWAPV4`, `UNISWAPV4_VIP`, and `METATXN_UNISWAPV4_VIP` * See comments in [UniswapV4.sol](src/core/UniswapV4.sol) regarding how to encode `fills` - * See comments in [UniswapV4.sol](src/core/UniswapV4.sol) regarding how to - compute a perfect token hash function + * See comments in + [FlashAccountingCommon.sol](src/core/FlashAccountingCommon.sol) regarding + how to compute a perfect token hash function * Add `msgSender()(address)` accessor on Base to retrieve the current taker * Improve accuracy, gas, and convergence region coverage in SolidlyV1/VelodromeV2 action (`VELODROME`) * Add DodoV1 actions to more chains @@ -59,6 +60,13 @@ Master list of UniV3 forks: * Add `rebateClaimer()(address)` function on Mainnet Settlers for gas rebate program * Add SolidlyV3 UniV3 fork to Sonic * Add Wagmi UniV3 fork to Sonic +* Add actions for BalancerV3 + * `BALANCERV3`, `BALANCERV3_VIP`, and `METATXN_BALANCERV3_VIP` + * See comments in [BalancerV3.sol](src/core/BalancerV3.sol) regarding how to + encode `fills` + * See comments in + [FlashAccountingCommon.sol](src/core/FlashAccountingCommon.sol) regarding + how to compute a perfect token hash function ## 2024-12-18 diff --git a/src/ISettlerActions.sol b/src/ISettlerActions.sol index d6c372c5..a1e90868 100644 --- a/src/ISettlerActions.sol +++ b/src/ISettlerActions.sol @@ -74,6 +74,36 @@ interface ISettlerActions { uint256 amountOutMin ) external; + function BALANCERV3( + address recipient, + address sellToken, + uint256 bps, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + uint256 amountOutMin + ) external; + function BALANCERV3_VIP( + address recipient, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + ISignatureTransfer.PermitTransferFrom memory permit, + bytes memory sig, + uint256 amountOutMin + ) external; + function METATXN_BALANCERV3_VIP( + address recipient, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + ISignatureTransfer.PermitTransferFrom memory permit, + uint256 amountOutMin + ) external; + /// @dev Trades against UniswapV3 using the contracts balance for funding // Pre-req: Funded // Post-req: Payout diff --git a/src/chains/Mainnet/Common.sol b/src/chains/Mainnet/Common.sol index 9ec8f176..d77ed24a 100644 --- a/src/chains/Mainnet/Common.sol +++ b/src/chains/Mainnet/Common.sol @@ -10,6 +10,7 @@ import {CurveTricrypto} from "../../core/CurveTricrypto.sol"; import {DodoV1, IDodoV1} from "../../core/DodoV1.sol"; import {DodoV2, IDodoV2} from "../../core/DodoV2.sol"; import {UniswapV4} from "../../core/UniswapV4.sol"; +import {BalancerV3} from "../../core/BalancerV3.sol"; import {FreeMemory} from "../../utils/FreeMemory.sol"; import {ISettlerActions} from "../../ISettlerActions.sol"; @@ -50,7 +51,8 @@ abstract contract MainnetMixin is CurveTricrypto, DodoV1, DodoV2, - UniswapV4 + UniswapV4, + BalancerV3 { constructor() { assert(block.chainid == 1 || block.chainid == 31337); @@ -83,6 +85,19 @@ abstract contract MainnetMixin is abi.decode(data, (address, uint256, bool, uint256)); sellToMakerPsm(recipient, bps, buyGem, amountOutMin); + } else if (action == uint32(ISettlerActions.BALANCERV3.selector)) { + ( + address recipient, + IERC20 sellToken, + uint256 bps, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + uint256 amountOutMin + ) = abi.decode(data, (address, IERC20, uint256, bool, uint256, uint256, bytes, uint256)); + + sellToBalancerV3(recipient, sellToken, bps, feeOnTransfer, hashMul, hashMod, fills, amountOutMin); } else if (action == uint32(ISettlerActions.MAVERICKV2.selector)) { ( address recipient, diff --git a/src/chains/Mainnet/MetaTxn.sol b/src/chains/Mainnet/MetaTxn.sol index ebca8b5c..c7ceb953 100644 --- a/src/chains/Mainnet/MetaTxn.sol +++ b/src/chains/Mainnet/MetaTxn.sol @@ -25,7 +25,7 @@ contract MainnetSettlerMetaTxn is SettlerMetaTxn, MainnetMixin { { if (super._dispatchVIP(action, data, sig)) { return true; - } else if (action == uint32(ISettlerActions.UNISWAPV4_VIP.selector)) { + } else if (action == uint32(ISettlerActions.METATXN_UNISWAPV4_VIP.selector)) { ( address recipient, bool feeOnTransfer, @@ -39,6 +39,20 @@ contract MainnetSettlerMetaTxn is SettlerMetaTxn, MainnetMixin { ); sellToUniswapV4VIP(recipient, feeOnTransfer, hashMul, hashMod, fills, permit, sig, amountOutMin); + } else if (action == uint32(ISettlerActions.METATXN_BALANCERV3_VIP.selector)) { + ( + address recipient, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + ISignatureTransfer.PermitTransferFrom memory permit, + uint256 amountOutMin + ) = abi.decode( + data, (address, bool, uint256, uint256, bytes, ISignatureTransfer.PermitTransferFrom, uint256) + ); + + sellToBalancerV3VIP(recipient, feeOnTransfer, hashMul, hashMod, fills, permit, sig, amountOutMin); } else if (action == uint32(ISettlerActions.METATXN_MAVERICKV2_VIP.selector)) { ( address recipient, diff --git a/src/chains/Mainnet/TakerSubmitted.sol b/src/chains/Mainnet/TakerSubmitted.sol index b6d78703..38c5e5a4 100644 --- a/src/chains/Mainnet/TakerSubmitted.sol +++ b/src/chains/Mainnet/TakerSubmitted.sol @@ -36,6 +36,21 @@ contract MainnetSettler is Settler, MainnetMixin { ); sellToUniswapV4VIP(recipient, feeOnTransfer, hashMul, hashMod, fills, permit, sig, amountOutMin); + } else if (action == uint32(ISettlerActions.BALANCERV3_VIP.selector)) { + ( + address recipient, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + ISignatureTransfer.PermitTransferFrom memory permit, + bytes memory sig, + uint256 amountOutMin + ) = abi.decode( + data, (address, bool, uint256, uint256, bytes, ISignatureTransfer.PermitTransferFrom, bytes, uint256) + ); + + sellToBalancerV3VIP(recipient, feeOnTransfer, hashMul, hashMod, fills, permit, sig, amountOutMin); } else if (action == uint32(ISettlerActions.MAVERICKV2_VIP.selector)) { ( address recipient, diff --git a/src/core/BalancerV3.sol b/src/core/BalancerV3.sol new file mode 100644 index 00000000..7a1d4d2b --- /dev/null +++ b/src/core/BalancerV3.sol @@ -0,0 +1,572 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {IERC20} from "@forge-std/interfaces/IERC20.sol"; +import {IERC4626} from "@forge-std/interfaces/IERC4626.sol"; +import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; +import {SafeTransferLib} from "../vendor/SafeTransferLib.sol"; +import {SettlerAbstract} from "../SettlerAbstract.sol"; + +import {Panic} from "../utils/Panic.sol"; +import {UnsafeMath} from "../utils/UnsafeMath.sol"; + +import {TooMuchSlippage, ZeroSellAmount} from "./SettlerErrors.sol"; + +import {Encoder, NotesLib, StateLib, Decoder, Take} from "./FlashAccountingCommon.sol"; + +import {FreeMemory} from "../utils/FreeMemory.sol"; + +interface IBalancerV3Vault { + /** + * @notice Creates a context for a sequence of operations (i.e., "unlocks" the Vault). + * @dev Performs a callback on msg.sender with arguments provided in `data`. The Callback is `transient`, + * meaning all balances for the caller have to be settled at the end. + * + * @param data Contains function signature and args to be passed to the msg.sender + * @return result Resulting data from the call + */ + function unlock(bytes calldata data) external returns (bytes memory); + + /** + * @notice Settles deltas for a token; must be successful for the current lock to be released. + * @dev Protects the caller against leftover dust in the Vault for the token being settled. The caller + * should know in advance how many tokens were paid to the Vault, so it can provide it as a hint to discard any + * excess in the Vault balance. + * + * If the given hint is equal to or higher than the difference in reserves, the difference in reserves is given as + * credit to the caller. If it's higher, the caller sent fewer tokens than expected, so settlement would fail. + * + * If the given hint is lower than the difference in reserves, the hint is given as credit to the caller. + * In this case, the excess would be absorbed by the Vault (and reflected correctly in the reserves), but would + * not affect settlement. + * + * The credit supplied by the Vault can be calculated as `min(reserveDifference, amountHint)`, where the reserve + * difference equals current balance of the token minus existing reserves of the token when the function is called. + * + * @param token Address of the token + * @param amountHint Amount paid as reported by the caller + * @return credit Credit received in return of the payment + */ + function settle(IERC20 token, uint256 amountHint) external returns (uint256 credit); + + /** + * @notice Sends tokens to a recipient. + * @dev There is no inverse operation for this function. Transfer funds to the Vault and call `settle` to cancel + * debts. + * + * @param token Address of the token + * @param to Recipient address + * @param amount Amount of tokens to send + */ + function sendTo(IERC20 token, address to, uint256 amount) external; + + enum SwapKind { + EXACT_IN, + EXACT_OUT + } + + /** + * @notice Data passed into primary Vault `swap` operations. + * @param kind Type of swap (Exact In or Exact Out) + * @param pool The pool with the tokens being swapped + * @param tokenIn The token entering the Vault (balance increases) + * @param tokenOut The token leaving the Vault (balance decreases) + * @param amountGiven Amount specified for tokenIn or tokenOut (depending on the type of swap) + * @param limit Minimum or maximum value of the calculated amount (depending on the type of swap) + * @param userData Additional (optional) user data + */ + struct VaultSwapParams { + SwapKind kind; + address pool; + IERC20 tokenIn; + IERC20 tokenOut; + uint256 amountGiven; + uint256 limit; + bytes userData; + } + + /** + * @notice Swaps tokens based on provided parameters. + * @dev All parameters are given in raw token decimal encoding. + * @param vaultSwapParams Parameters for the swap (see above for struct definition) + * @return amountCalculated Calculated swap amount + * @return amountIn Amount of input tokens for the swap + * @return amountOut Amount of output tokens from the swap + */ + function swap(VaultSwapParams memory vaultSwapParams) + external + returns (uint256 amountCalculated, uint256 amountIn, uint256 amountOut); + + enum WrappingDirection { + WRAP, + UNWRAP + } + + /** + * @notice Data for a wrap/unwrap operation. + * @param kind Type of swap (Exact In or Exact Out) + * @param direction Direction of the wrapping operation (Wrap or Unwrap) + * @param wrappedToken Wrapped token, compatible with interface ERC4626 + * @param amountGiven Amount specified for tokenIn or tokenOut (depends on the type of swap and wrapping direction) + * @param limit Minimum or maximum amount specified for the other token (depends on the type of swap and wrapping + * direction) + */ + struct BufferWrapOrUnwrapParams { + SwapKind kind; + WrappingDirection direction; + IERC4626 wrappedToken; + uint256 amountGiven; + uint256 limit; + } + + /** + * @notice Wraps/unwraps tokens based on the parameters provided. + * @dev All parameters are given in raw token decimal encoding. It requires the buffer to be initialized, + * and uses the internal wrapped token buffer when it has enough liquidity to avoid external calls. + * + * @param params Parameters for the wrap/unwrap operation (see struct definition) + * @return amountCalculated Calculated swap amount + * @return amountIn Amount of input tokens for the swap + * @return amountOut Amount of output tokens from the swap + */ + function erc4626BufferWrapOrUnwrap(BufferWrapOrUnwrapParams memory params) + external + returns (uint256 amountCalculated, uint256 amountIn, uint256 amountOut); +} + +library UnsafeVault { + function unsafeSettle(IBalancerV3Vault vault, IERC20 token, uint256 amount) internal returns (uint256 credit) { + assembly ("memory-safe") { + let ptr := mload(0x40) + + mstore(0x00, 0x15afd409) // selector for `settle(address,uint256)` + mstore(0x20, and(0xffffffffffffffffffffffffffffffffffffffff, token)) + mstore(0x40, amount) + + if iszero(call(gas(), vault, 0x00, 0x1c, 0x44, 0x00, 0x20)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + credit := mload(0x00) + + mstore(0x40, ptr) + } + } + + function unsafeSwap(IBalancerV3Vault vault, IBalancerV3Vault.VaultSwapParams memory params) + internal + returns (uint256 amountIn, uint256 amountOut) + { + assembly ("memory-safe") { + // `VaultSwapParams` is a dynamic type with exactly 1 sub-object, and that sub-object is + // dynamic (all the other members are value types). Therefore, the layout in calldata is + // nearly identical to the layout in memory, but there's an extra indirection pointer + // that needs to be prepended. + // We know that it's safe to (temporarily) clobber the two words in memory immediately + // before `params` because they are user-allocated (they're part of `wrapParams`). If + // they were not user-allocated, this would be illegal as it could clobber a word that + // `solc` spilled from the stack into memory. + + let ptr := mload(0x40) + let clobberedPtr0 := sub(params, 0x40) + let clobberedVal0 := mload(clobberedPtr0) + let clobberedPtr1 := sub(params, 0x20) + let clobberedVal1 := mload(clobberedPtr1) + + mstore(clobberedPtr0, 0x2bfb780c) // selector for `swap((uint8,address,address,address,uint256,uint256,bytes))` + mstore(clobberedPtr1, 0x20) // indirection pointer to the dynamic type `VaultSwapParams` + + // Because we laid out `swapParams` as the last object in memory before + // `swapParam.userData`, the two objects are contiguous. Their encoding in calldata is + // exactly the same as their encoding in memory, but with pointers changed to offsets. + let userDataPtr := add(0xc0, params) + let userData := mload(userDataPtr) + let userDataLen := mload(userData) + let len := sub(add(add(0x20, userDataLen), userData), params) + mstore(userDataPtr, sub(userData, params)) + + // The length of the whole call's calldata is 36 bytes longer than the encoding of + // `params` in memory to account for the prepending of the selector (4 bytes) and the + // indirection pointer (32 bytes) + if iszero(call(gas(), vault, 0x00, add(0x1c, clobberedPtr0), add(0x24, len), 0x00, 0x60)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + amountIn := mload(0x20) + amountOut := mload(0x40) + + // mstore(userDataPtr, userData) // we don't need this because we're immediately going to deallocate + mstore(clobberedPtr0, clobberedVal0) + mstore(clobberedPtr1, clobberedVal1) + mstore(0x40, ptr) + } + } + + function unsafeErc4626BufferWrapOrUnwrap( + IBalancerV3Vault vault, + IBalancerV3Vault.BufferWrapOrUnwrapParams memory params + ) internal returns (uint256 amountIn, uint256 amountOut) { + assembly ("memory-safe") { + // `BufferWrapOrUnwrapParams` is a static type and contains no sub-objects (all its + // members are value types), so the layout in calldata is just the layout in memory, + // without any indirection pointers. + // We know that it's safe to (temporarily) clobber the word in memory immediately before + // `params` because it is user-allocated (it's part of the `Notes` heap). If it were not + // user-allocated, this would be illegal as it could clobber a word that `solc` spilled + // from the stack into memory. + + let ptr := mload(0x40) + let clobberedPtr := sub(params, 0x20) + let clobberedVal := mload(clobberedPtr) + mstore(sub(params, 0x20), 0x43583be5) // selector for `erc4626BufferWrapOrUnwrap((uint8,uint8,address,uint256,uint256))` + + if iszero(call(gas(), vault, 0x00, add(0x1c, clobberedPtr), 0xa4, 0x00, 0x60)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + amountIn := mload(0x20) + amountOut := mload(0x40) + + mstore(clobberedPtr, clobberedVal) + mstore(0x40, ptr) + } + } +} + +IBalancerV3Vault constant VAULT = IBalancerV3Vault(0xbA1333333333a1BA1108E8412f11850A5C319bA9); + +abstract contract BalancerV3 is SettlerAbstract, FreeMemory { + using SafeTransferLib for IERC20; + using UnsafeMath for uint256; + using NotesLib for NotesLib.Note; + using NotesLib for NotesLib.Note[]; + using StateLib for StateLib.State; + + using UnsafeVault for IBalancerV3Vault; + + constructor() { + assert(BASIS == Encoder.BASIS); + assert(BASIS == Decoder.BASIS); + assert(ETH_ADDRESS == Decoder.ETH_ADDRESS); + } + + //// How to generate `fills` for BalancerV3: + //// + //// Linearize your DAG of fills by doing a topological sort on the tokens involved. Swapping + //// against a boosted pool (usually) creates 3 fills: wrap, swap, unwrap. The tokens involved + //// includes each ERC4626 tokenized vault token for any boosted pools. In the topological sort + //// of tokens, when there is a choice of the next token, break ties by preferring a token if it + //// is the lexicographically largest token that is bought among fills with sell token equal to + //// the previous token in the topological sort. Then sort the fills belonging to each sell + //// token by their buy token. This technique isn't *quite* optimal, but it's pretty close. The + //// buy token of the final fill is special-cased. It is the token that will be transferred to + //// `recipient` and have its slippage checked against `amountOutMin`. In the event that you are + //// encoding a series of fills with more than one output token, ensure that at least one of the + //// global buy token's fills is positioned appropriately. + //// + //// Now that you have a list of fills, encode each fill as follows. + //// First, decide if the fill is a swap or an ERC4626 wrap/unwrap. + //// Second, encode the `bps` for the fill as 2 bytes. Remember that this `bps` is relative to + //// the running balance at the moment that the fill is settled. If the fill is a wrap, set the + //// most significant bit of `bps`. If the fill is an unwrap, set the second most significant + //// bit of `bps` + //// Third, encode the packing key for that fill as 1 byte. The packing key byte depends on the + //// tokens involved in the previous fill. If the fill is a wrap, the buy token must be the + //// ERC4626 vault. If the fill is an unwrap, the sell token must be the ERC4626 vault. If the + //// fill is a swap against a boosted pool, both sell and buy tokens must be ERC4626 vaults. God + //// help you if you're dealing with a boosted pool where only some of the tokens involved are + //// ERC4626. The packing key for the first fill must be 1; i.e. encode only the buy token for + //// the first fill. + //// 0 -> sell and buy tokens remain unchanged from the previous fill (pure multiplex) + //// 1 -> sell token remains unchanged from the previous fill, buy token is encoded (diamond multiplex) + //// 2 -> sell token becomes the buy token from the previous fill, new buy token is encoded (multihop) + //// 3 -> both sell and buy token are encoded + //// Obviously, after encoding the packing key, you encode 0, 1, or 2 tokens (each as 20 bytes), + //// as appropriate. + //// If the fill is a wrap/unwrap, you're done. Move on to the next fill. If the fill is a swap, + //// the following fields are mandatory: + //// Fourth, encode the pool address as 20 bytes. + //// Fifth, encode the hook data for the fill. Encode the length of the hook data as 3 bytes, + //// then append the hook data itself. + //// + //// Repeat the process for each fill and concatenate the results without padding. + + function sellToBalancerV3( + address recipient, + IERC20 sellToken, + uint256 bps, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + uint256 amountOutMin + ) internal returns (uint256 buyAmount) { + if (bps > BASIS) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + bytes memory data = Encoder.encode( + uint32(IBalancerV3Vault.unlock.selector), + recipient, + sellToken, + bps, + feeOnTransfer, + hashMul, + hashMod, + fills, + amountOutMin + ); + bytes memory encodedBuyAmount = + _setOperatorAndCall(address(VAULT), data, uint32(uint256(uint160(recipient)) >> 128), _balV3Callback); + // buyAmount = abi.decode(abi.decode(encodedBuyAmount, (bytes)), (uint256)); + assembly ("memory-safe") { + // We can skip all the checks performed by `abi.decode` because we know that this is the + // verbatim result from `balV3UnlockCallback` and that `balV3UnlockCallback` encoded the + // buy amount correctly. + buyAmount := mload(add(0x60, encodedBuyAmount)) + } + } + + function sellToBalancerV3VIP( + address recipient, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + ISignatureTransfer.PermitTransferFrom memory permit, + bytes memory sig, + uint256 amountOutMin + ) internal returns (uint256 buyAmount) { + bytes memory data = Encoder.encodeVIP( + uint32(IBalancerV3Vault.unlock.selector), + recipient, + feeOnTransfer, + hashMul, + hashMod, + fills, + permit, + sig, + _isForwarded(), + amountOutMin + ); + bytes memory encodedBuyAmount = + _setOperatorAndCall(address(VAULT), data, uint32(uint256(uint160(recipient)) >> 128), _balV3Callback); + // buyAmount = abi.decode(abi.decode(encodedBuyAmount, (bytes)), (uint256)); + assembly ("memory-safe") { + // We can skip all the checks performed by `abi.decode` because we know that this is the + // verbatim result from `balV3UnlockCallback` and that `balV3UnlockCallback` encoded the + // buy amount correctly. + buyAmount := mload(add(0x60, encodedBuyAmount)) + } + } + + function _balV3Callback(bytes calldata) private returns (bytes memory) { + // `VAULT` doesn't prepend a selector and ABIEncode the payload. It just echoes the decoded + // payload verbatim back to us. Therefore, we use `_msgData()` instead of the argument to + // this function because `_msgData()` still has the first 4 bytes of the payload attached. + return balV3UnlockCallback(_msgData()); + } + + function _setSwapParams( + IBalancerV3Vault.VaultSwapParams memory swapParams, + StateLib.State memory state, + bytes calldata data + ) private view returns (bytes calldata) { + assembly ("memory-safe") { + mstore(add(0x20, swapParams), shr(0x60, calldataload(data.offset))) + data.offset := add(0x14, data.offset) + data.length := sub(data.length, 0x14) + // we don't check for array out-of-bounds here; we will check it later in `Decoder.overflowCheck` + } + swapParams.tokenIn = state.sell.token; + swapParams.tokenOut = state.buy.token; + return data; + } + + function _decodeUserdataAndSwap( + IBalancerV3Vault.VaultSwapParams memory swapParams, + StateLib.State memory state, + bytes calldata data + ) private DANGEROUS_freeMemory returns (bytes calldata) { + (data, swapParams.userData) = Decoder.decodeBytes(data); + Decoder.overflowCheck(data); + + (uint256 amountIn, uint256 amountOut) = IBalancerV3Vault(msg.sender).unsafeSwap(swapParams); + unchecked { + // `amountIn` is always exactly `swapParams.amountGiven` + state.sell.amount -= amountIn; + // `amountOut` can never get super close to `type(uint256).max` because `VAULT` does its + // internal calculations in fixnum with a basis of `1 ether`, giving us a headroom of + // ~60 bits. + state.buy.amount += amountOut; + } + assembly ("memory-safe") { + mstore(add(0xc0, swapParams), 0x60) + } + + return data; + } + + function _erc4626WrapUnwrap( + IBalancerV3Vault.BufferWrapOrUnwrapParams memory wrapParams, + StateLib.State memory state + ) private { + (uint256 amountIn, uint256 amountOut) = IBalancerV3Vault(msg.sender).unsafeErc4626BufferWrapOrUnwrap(wrapParams); + unchecked { + // `amountIn` is always exactly `wrapParams.amountGiven` + state.sell.amount -= amountIn; + } + // `amountOut` may depend on the behavior of the ERC4626 vault. We can make no assumptions + // about the reasonableness of the range of values that may be returned. + state.buy.amount += amountOut; + } + + function _balV3Pay( + IERC20 sellToken, + address payer, + uint256 sellAmount, + ISignatureTransfer.PermitTransferFrom calldata permit, + bool isForwarded, + bytes calldata sig + ) private returns (uint256) { + if (payer == address(this)) { + sellToken.safeTransfer(msg.sender, sellAmount); + } else { + // assert(payer == address(0)); + ISignatureTransfer.SignatureTransferDetails memory transferDetails = + ISignatureTransfer.SignatureTransferDetails({to: msg.sender, requestedAmount: sellAmount}); + _transferFrom(permit, transferDetails, sig, isForwarded); + } + return IBalancerV3Vault(msg.sender).unsafeSettle(sellToken, sellAmount); + } + + // the mandatory fields are + // 2 - sell bps + // 1 - pool key tokens case + uint256 private constant _HOP_DATA_LENGTH = 3; + + function balV3UnlockCallback(bytes calldata data) private returns (bytes memory) { + address recipient; + uint256 minBuyAmount; + uint256 hashMul; + uint256 hashMod; + bool feeOnTransfer; + address payer; + (data, recipient, minBuyAmount, hashMul, hashMod, feeOnTransfer, payer) = Decoder.decodeHeader(data); + + // Set up `state` and `notes`. The other values are ancillary and might be used when we need + // to settle global sell token debt at the end of swapping. + ( + bytes calldata newData, + StateLib.State memory state, + NotesLib.Note[] memory notes, + ISignatureTransfer.PermitTransferFrom calldata permit, + bool isForwarded, + bytes calldata sig + ) = Decoder.initialize(data, hashMul, hashMod, payer); + if (payer != address(this)) { + state.globalSell.amount = _permitToSellAmountCalldata(permit); + } + if (feeOnTransfer) { + state.globalSell.amount = + _balV3Pay(state.globalSell.token, payer, state.globalSell.amount, permit, isForwarded, sig); + } + if (state.globalSell.amount == 0) { + revert ZeroSellAmount(state.globalSell.token); + } + state.globalSellAmount = state.globalSell.amount; + data = newData; + + IBalancerV3Vault.BufferWrapOrUnwrapParams memory wrapParams; + /* + wrapParams.kind = IBalancerV3Vault.SwapKind.EXACT_IN; + wrapParams.limit = 0; // TODO: price limits for partial filling + */ + + // We position `swapParams` at the end of allocated memory so that when we `calldatacopy` + // the `userData`, it ends up contiguous + IBalancerV3Vault.VaultSwapParams memory swapParams; + /* + swapParams.kind = IBalancerV3Vault.SwapKind.EXACT_IN; + swapParams.limit = 0; // TODO: price limits for partial filling + */ + + while (data.length >= _HOP_DATA_LENGTH) { + uint16 bps; + assembly ("memory-safe") { + bps := shr(0xf0, calldataload(data.offset)) + + data.offset := add(0x02, data.offset) + data.length := sub(data.length, 0x02) + // we don't check for array out-of-bounds here; we will check it later in `Decoder.overflowCheck` + } + + data = Decoder.updateState(state, notes, data); + + if (bps & 0xc000 > 0) { + Decoder.overflowCheck(data); + + if (bps & 0x4000 > 0) { + wrapParams.direction = IBalancerV3Vault.WrappingDirection.UNWRAP; + wrapParams.wrappedToken = IERC4626(address(state.sell.token)); + } else { + wrapParams.direction = IBalancerV3Vault.WrappingDirection.WRAP; + wrapParams.wrappedToken = IERC4626(address(state.buy.token)); + } + bps &= 0x3fff; + wrapParams.amountGiven = (state.sell.amount * bps).unsafeDiv(BASIS); + + _erc4626WrapUnwrap(wrapParams, state); + } else { + data = _setSwapParams(swapParams, state, data); + swapParams.amountGiven = (state.sell.amount * bps).unsafeDiv(BASIS); + data = _decodeUserdataAndSwap(swapParams, state, data); + } + } + + // `data` has been consumed. All that remains is to settle out the net result of all the + // swaps. Any credit in any token other than `state.buy.token` will be swept to + // Settler. `state.buy.token` will be sent to `recipient`. + { + (IERC20 globalSellToken, uint256 globalSellAmount) = (state.globalSell.token, state.globalSell.amount); + uint256 globalBuyAmount = + Take.take(state, notes, uint32(IBalancerV3Vault.sendTo.selector), recipient, minBuyAmount); + if (feeOnTransfer) { + // We've already transferred the sell token to the vault and + // `settle`'d. `globalSellAmount` is the verbatim credit in that token stored by the + // vault. We only need to handle the case of incomplete filling. + if (globalSellAmount != 0) { + Take._callSelector( + uint32(IBalancerV3Vault.sendTo.selector), + globalSellToken, + payer == address(this) ? address(this) : _msgSender(), + globalSellAmount + ); + } + } else { + // While `notes` records a credit value, the vault actually records a debt for the + // global sell token. We recover the exact amount of that debt and then pay it. + // `globalSellAmount` is _usually_ zero, but if it isn't it represents a partial + // fill. This subtraction recovers the actual debt recorded in the vault. + uint256 debt; + unchecked { + debt = state.globalSellAmount - globalSellAmount; + } + if (debt == 0) { + revert ZeroSellAmount(globalSellToken); + } + _balV3Pay(globalSellToken, payer, debt, permit, isForwarded, sig); + } + + bytes memory returndata; + assembly ("memory-safe") { + returndata := mload(0x40) + mstore(returndata, 0x60) + mstore(add(0x20, returndata), 0x20) + mstore(add(0x40, returndata), 0x20) + mstore(add(0x60, returndata), globalBuyAmount) + mstore(0x40, add(0x80, returndata)) + } + return returndata; + } + } +} diff --git a/src/core/Basic.sol b/src/core/Basic.sol index ebaeb61b..c2f0fcac 100644 --- a/src/core/Basic.sol +++ b/src/core/Basic.sol @@ -9,8 +9,10 @@ import {SafeTransferLib} from "../vendor/SafeTransferLib.sol"; import {FullMath} from "../vendor/FullMath.sol"; import {Panic} from "../utils/Panic.sol"; import {Revert} from "../utils/Revert.sol"; +import {UnsafeMath} from "../utils/UnsafeMath.sol"; abstract contract Basic is SettlerAbstract { + using UnsafeMath for uint256; using SafeTransferLib for IERC20; using FullMath for uint256; using Revert for bool; @@ -26,7 +28,7 @@ abstract contract Basic is SettlerAbstract { bytes memory returnData; uint256 value; if (sellToken == ETH_ADDRESS) { - value = address(this).balance.mulDiv(bps, BASIS); + value = (address(this).balance * bps).unsafeDiv(BASIS); if (data.length == 0) { if (offset != 0) revert InvalidOffset(); (success, returnData) = payable(pool).call{value: value}(""); diff --git a/src/core/DodoV1.sol b/src/core/DodoV1.sol index 3ffde293..5d94530b 100644 --- a/src/core/DodoV1.sol +++ b/src/core/DodoV1.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.25; import {IERC20} from "@forge-std/interfaces/IERC20.sol"; import {SettlerAbstract} from "../SettlerAbstract.sol"; import {TooMuchSlippage} from "./SettlerErrors.sol"; -import {FullMath} from "../vendor/FullMath.sol"; import {SafeTransferLib} from "../vendor/SafeTransferLib.sol"; +import {UnsafeMath} from "../utils/UnsafeMath.sol"; interface IDodoV1 { function sellBaseToken(uint256 amount, uint256 minReceiveQuote, bytes calldata data) external returns (uint256); @@ -34,17 +34,7 @@ interface IDodoV1 { } library Math { - function divCeil(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 quotient = a / b; - unchecked { - uint256 remainder = a - quotient * b; - if (remainder > 0) { - return quotient + 1; - } else { - return quotient; - } - } - } + using UnsafeMath for uint256; function sqrt(uint256 x) internal pure returns (uint256 y) { unchecked { @@ -52,13 +42,14 @@ library Math { y = x; while (z < y) { y = z; - z = (x / z + z) / 2; + z = (x.unsafeDiv(z) + z) / 2; } } } } library DecimalMath { + using UnsafeMath for uint256; using Math for uint256; uint256 constant ONE = 10 ** 18; @@ -71,24 +62,25 @@ library DecimalMath { function mulCeil(uint256 target, uint256 d) internal pure returns (uint256) { unchecked { - return (target * d).divCeil(ONE); + return (target * d).unsafeDivUp(ONE); } } function divFloor(uint256 target, uint256 d) internal pure returns (uint256) { unchecked { - return target * ONE / d; + return (target * ONE).unsafeDiv(d); } } function divCeil(uint256 target, uint256 d) internal pure returns (uint256) { unchecked { - return (target * ONE).divCeil(d); + return (target * ONE).unsafeDivUp(d); } } } library DodoMath { + using UnsafeMath for uint256; using Math for uint256; /* @@ -105,7 +97,7 @@ library DodoMath { { unchecked { uint256 fairAmount = DecimalMath.mul(i, V1 - V2); // i*delta - uint256 V0V0V1V2 = DecimalMath.divCeil(V0 * V0 / V1, V2); + uint256 V0V0V1V2 = DecimalMath.divCeil((V0 * V0).unsafeDiv(V1), V2); uint256 penalty = DecimalMath.mul(k, V0V0V1V2); // k(V0^2/V1/V2) return DecimalMath.mul(fairAmount, DecimalMath.ONE - k + penalty); } @@ -133,7 +125,7 @@ library DodoMath { unchecked { // calculate -b value and sig // -b = (1-k)Q1-kQ0^2/Q1+i*deltaB - uint256 kQ02Q1 = DecimalMath.mul(k, Q0) * Q0 / Q1; // kQ0^2/Q1 + uint256 kQ02Q1 = (DecimalMath.mul(k, Q0) * Q0).unsafeDiv(Q1); // kQ0^2/Q1 uint256 b = DecimalMath.mul(DecimalMath.ONE - k, Q1); // (1-k)Q1 bool minusbSig = true; if (deltaBSig) { @@ -284,13 +276,16 @@ abstract contract DodoSellHelper { } abstract contract DodoV1 is SettlerAbstract, DodoSellHelper { - using FullMath for uint256; + using UnsafeMath for uint256; using SafeTransferLib for IERC20; function sellToDodoV1(IERC20 sellToken, uint256 bps, IDodoV1 dodo, bool quoteForBase, uint256 minBuyAmount) internal { - uint256 sellAmount = sellToken.fastBalanceOf(address(this)).mulDiv(bps, BASIS); + uint256 sellAmount; + unchecked { + sellAmount = (sellToken.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); + } sellToken.safeApproveIfBelow(address(dodo), sellAmount); if (quoteForBase) { uint256 buyAmount = dodoQuerySellQuoteToken(dodo, sellAmount); diff --git a/src/core/DodoV2.sol b/src/core/DodoV2.sol index f92aa48a..d501fa1c 100644 --- a/src/core/DodoV2.sol +++ b/src/core/DodoV2.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.25; import {IERC20} from "@forge-std/interfaces/IERC20.sol"; import {SettlerAbstract} from "../SettlerAbstract.sol"; import {TooMuchSlippage} from "./SettlerErrors.sol"; -import {FullMath} from "../vendor/FullMath.sol"; import {SafeTransferLib} from "../vendor/SafeTransferLib.sol"; +import {UnsafeMath} from "../utils/UnsafeMath.sol"; interface IDodoV2 { function sellBase(address to) external returns (uint256 receiveQuoteAmount); @@ -16,7 +16,7 @@ interface IDodoV2 { } abstract contract DodoV2 is SettlerAbstract { - using FullMath for uint256; + using UnsafeMath for uint256; using SafeTransferLib for IERC20; function sellToDodoV2( @@ -28,7 +28,10 @@ abstract contract DodoV2 is SettlerAbstract { uint256 minBuyAmount ) internal returns (uint256 buyAmount) { if (bps != 0) { - uint256 sellAmount = sellToken.fastBalanceOf(address(this)).mulDiv(bps, BASIS); + uint256 sellAmount; + unchecked { + sellAmount = (sellToken.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); + } sellToken.safeTransfer(address(dodo), sellAmount); } if (quoteForBase) { diff --git a/src/core/FlashAccountingCommon.sol b/src/core/FlashAccountingCommon.sol new file mode 100644 index 00000000..af973542 --- /dev/null +++ b/src/core/FlashAccountingCommon.sol @@ -0,0 +1,666 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {IERC20} from "@forge-std/interfaces/IERC20.sol"; +import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; +import {SafeTransferLib} from "../vendor/SafeTransferLib.sol"; + +import {Panic} from "../utils/Panic.sol"; +import {UnsafeMath} from "../utils/UnsafeMath.sol"; + +import {TooMuchSlippage, BoughtSellToken} from "./SettlerErrors.sol"; + +/// This library is a highly-optimized, in-memory, enumerable mapping from tokens to amounts. It +/// consists of 2 components that must be kept synchronized. There is a `memory` array of `Note` +/// (aka `Note[] memory`) that has up to `_MAX_TOKENS` pre-allocated. And there is an implicit heap +/// packed at the end of the array that stores the `Note`s. Each `Note` has a backpointer that knows +/// its location in the `Notes[] memory`. While the length of the `Notes[]` array grows and shrinks +/// as tokens are added and retired, heap objects are only cleared/deallocated when the context +/// returns. Looking up the `Note` object corresponding to a token uses the perfect hash formed by +/// `hashMul` and `hashMod`. Pay special attention to these parameters. See further below for +/// recommendations on how to select values for them. A hash collision will result in a revert with +/// signature `TokenHashCollision(address,address)`. +library NotesLib { + uint256 private constant _ADDRESS_MASK = 0x00ffffffffffffffffffffffffffffffffffffffff; + + /// This is the maximum number of tokens that may be involved in a UniV4 action. Increasing or + /// decreasing this value requires no other changes elsewhere in this file. + uint256 private constant _MAX_TOKENS = 8; + + type NotePtr is uint256; + type NotePtrPtr is uint256; + + struct Note { + uint256 amount; + IERC20 token; + NotePtrPtr backPtr; + } + + function construct() internal pure returns (Note[] memory r) { + assembly ("memory-safe") { + r := mload(0x40) + // set the length of `r` to zero + mstore(r, 0x00) + // zeroize the heap + codecopy(add(add(0x20, shl(0x05, _MAX_TOKENS)), r), codesize(), mul(0x60, _MAX_TOKENS)) + // allocate memory + mstore(0x40, add(add(0x20, shl(0x07, _MAX_TOKENS)), r)) + } + } + + function eq(Note memory x, Note memory y) internal pure returns (bool r) { + assembly ("memory-safe") { + r := eq(x, y) + } + } + + function unsafeGet(Note[] memory a, uint256 i) internal pure returns (IERC20 retToken, uint256 retAmount) { + assembly ("memory-safe") { + let x := mload(add(add(0x20, shl(0x05, i)), a)) + retToken := mload(add(0x20, x)) + retAmount := mload(x) + } + } + + //// How to generate a perfect hash: + //// + //// The arguments `hashMul` and `hashMod` are required to form a perfect hash for a table with + //// size `NotesLib._MAX_TOKENS` when applied to all the tokens involved in fills. The hash + //// function is constructed as `uint256 hash = mulmod(uint256(uint160(address(token))), + //// hashMul, hashMod) % NotesLib._MAX_TOKENS`. + //// + //// The "simple" or "obvious" way to do this is to simply try random 128-bit numbers for both + //// `hashMul` and `hashMod` until you obtain a function that has no collisions when applied to + //// the tokens involved in fills. A substantially more optimized algorithm can be obtained by + //// selecting several (at least 10) prime values for `hashMod`, precomputing the limb moduluses + //// for each value, and then selecting randomly from among them. The author recommends using + //// the 10 largest 64-bit prime numbers: 2^64 - {59, 83, 95, 179, 189, 257, 279, 323, 353, + //// 363}. `hashMul` can then be selected randomly or via some other optimized method. + //// + //// Note that in spite of the fact that some AMMs represent Ether (or the native asset of the + //// chain) as `address(0)`, we represent Ether as `SettlerAbstract.ETH_ADDRESS` (the address of + //// all `e`s) for homogeneity with other parts of the codebase, and because the decision to + //// represent Ether as `address(0)` was stupid in the first place. `address(0)` represents the + //// absence of a thing, not a special case of the thing. It creates confusion with + //// uninitialized memory, storage, and variables. + function get(Note[] memory a, IERC20 newToken, uint256 hashMul, uint256 hashMod) + internal + pure + returns (NotePtr x) + { + assembly ("memory-safe") { + newToken := and(_ADDRESS_MASK, newToken) + x := add(add(0x20, shl(0x05, _MAX_TOKENS)), a) // `x` now points at the first `Note` on the heap + x := add(mod(mulmod(newToken, hashMul, hashMod), mul(0x60, _MAX_TOKENS)), x) // combine with token hash + // `x` now points at the exact `Note` object we want; let's check it to be sure, though + let x_token_ptr := add(0x20, x) + + // check that we haven't encountered a hash collision. checking for a hash collision is + // equivalent to checking for array out-of-bounds or overflow. + { + let old_token := mload(x_token_ptr) + if mul(or(mload(add(0x40, x)), old_token), xor(old_token, newToken)) { + mstore(0x00, 0x9a62e8b4) // selector for `TokenHashCollision(address,address)` + mstore(0x20, old_token) + mstore(0x40, newToken) + revert(0x1c, 0x44) + } + } + + // zero `newToken` is a footgun; check for it + if iszero(newToken) { + mstore(0x00, 0xad1991f5) // selector for `ZeroToken()` + revert(0x1c, 0x04) + } + + // initialize the token (possibly redundant) + mstore(x_token_ptr, newToken) + } + } + + function add(Note[] memory a, Note memory x) internal pure { + assembly ("memory-safe") { + let backptr_ptr := add(0x40, x) + let backptr := mload(backptr_ptr) + if iszero(backptr) { + let len := add(0x01, mload(a)) + // We don't need to check for overflow or out-of-bounds access here; the checks in + // `get` above for token collision handle that for us. It's not possible to `get` + // more than `_MAX_TOKENS` tokens + mstore(a, len) + backptr := add(shl(0x05, len), a) + mstore(backptr, x) + mstore(backptr_ptr, backptr) + } + } + } + + function del(Note[] memory a, Note memory x) internal pure { + assembly ("memory-safe") { + let x_backptr_ptr := add(0x40, x) + let x_backptr := mload(x_backptr_ptr) + if x_backptr { + // Clear the backpointer in the referred-to `Note` + mstore(x_backptr_ptr, 0x00) + // We do not deallocate `x` + + // Decrement the length of `a` + let len := mload(a) + mstore(a, sub(len, 0x01)) + + // Check if this is a "swap and pop" or just a "pop" + let end_ptr := add(shl(0x05, len), a) + if iszero(eq(end_ptr, x_backptr)) { + // Overwrite the vacated indirection pointer `x_backptr` with the value at the end. + let end := mload(end_ptr) + mstore(x_backptr, end) + + // Fix up the backpointer in `end` to point to the new location of the indirection + // pointer. + let end_backptr_ptr := add(0x40, end) + mstore(end_backptr_ptr, x_backptr) + } + } + } + } +} + +library StateLib { + using NotesLib for NotesLib.Note; + using NotesLib for NotesLib.Note[]; + + struct State { + NotesLib.Note buy; + NotesLib.Note sell; + NotesLib.Note globalSell; + uint256 globalSellAmount; + uint256 _hashMul; + uint256 _hashMod; + } + + function construct(State memory state, IERC20 token, uint256 hashMul, uint256 hashMod) + internal + pure + returns (NotesLib.Note[] memory notes) + { + assembly ("memory-safe") { + // Solc is real dumb and has allocated a bunch of extra memory for us. Thanks solc. + mstore(0x40, add(0xc0, state)) + } + // All the pointers in `state` are now pointing into unallocated memory + notes = NotesLib.construct(); + // The pointers in `state` are now illegally aliasing elements in `notes` + NotesLib.NotePtr notePtr = notes.get(token, hashMul, hashMod); + + // Here we actually set the pointers into a legal area of memory + setBuy(state, notePtr); + setSell(state, notePtr); + assembly ("memory-safe") { + // Set `state.globalSell` + mstore(add(0x40, state), notePtr) + } + state._hashMul = hashMul; + state._hashMod = hashMod; + } + + function setSell(State memory state, NotesLib.NotePtr notePtr) private pure { + assembly ("memory-safe") { + mstore(add(0x20, state), notePtr) + } + } + + function setSell(State memory state, NotesLib.Note[] memory notes, IERC20 token) internal pure { + setSell(state, notes.get(token, state._hashMul, state._hashMod)); + } + + function setBuy(State memory state, NotesLib.NotePtr notePtr) private pure { + assembly ("memory-safe") { + mstore(state, notePtr) + } + } + + function setBuy(State memory state, NotesLib.Note[] memory notes, IERC20 token) internal pure { + setBuy(state, notes.get(token, state._hashMul, state._hashMod)); + } +} + +library Encoder { + uint256 internal constant BASIS = 10_000; + + function encode( + uint32 unlockSelector, + address recipient, + IERC20 sellToken, + uint256 bps, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + uint256 amountOutMin + ) internal view returns (bytes memory data) { + if (bps > BASIS) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + if (amountOutMin > uint128(type(int128).max)) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + hashMul *= 96; + hashMod *= 96; + if (hashMul > type(uint128).max) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + if (hashMod > type(uint128).max) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + assembly ("memory-safe") { + data := mload(0x40) + + let pathLen := mload(fills) + mcopy(add(0xd3, data), add(0x20, fills), pathLen) + + mstore(add(0xb3, data), bps) + mstore(add(0xb1, data), sellToken) + mstore(add(0x9d, data), address()) // payer + // feeOnTransfer (1 byte) + + mstore(add(0x88, data), hashMod) + mstore(add(0x78, data), hashMul) + mstore(add(0x68, data), amountOutMin) + mstore(add(0x58, data), recipient) + mstore(add(0x44, data), add(0x6f, pathLen)) + mstore(add(0x24, data), 0x20) + mstore(add(0x04, data), and(0xffffffff, unlockSelector)) + mstore(data, add(0xb3, pathLen)) + mstore8(add(0xa8, data), feeOnTransfer) + + mstore(0x40, add(data, add(0xd3, pathLen))) + } + } + + function encodeVIP( + uint32 unlockSelector, + address recipient, + bool feeOnTransfer, + uint256 hashMul, + uint256 hashMod, + bytes memory fills, + ISignatureTransfer.PermitTransferFrom memory permit, + bytes memory sig, + bool isForwarded, + uint256 amountOutMin + ) internal pure returns (bytes memory data) { + if (amountOutMin > uint128(type(int128).max)) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + hashMul *= 96; + hashMod *= 96; + if (hashMul > type(uint128).max) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + if (hashMod > type(uint128).max) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + assembly ("memory-safe") { + data := mload(0x40) + + let pathLen := mload(fills) + let sigLen := mload(sig) + + { + let ptr := add(0x132, data) + + // sig length as 3 bytes goes at the end of the callback + mstore(sub(add(sigLen, add(pathLen, ptr)), 0x1d), sigLen) + + // fills go at the end of the header + mcopy(ptr, add(0x20, fills), pathLen) + ptr := add(pathLen, ptr) + + // signature comes after the fills + mcopy(ptr, add(0x20, sig), sigLen) + ptr := add(sigLen, ptr) + + mstore(0x40, add(0x03, ptr)) + } + + mstore8(add(0x131, data), isForwarded) + mcopy(add(0xf1, data), add(0x20, permit), 0x40) + mcopy(add(0xb1, data), mload(permit), 0x40) // aliases `payer` on purpose + mstore(add(0x9d, data), 0x00) // payer + // feeOnTransfer (1 byte) + + mstore(add(0x88, data), hashMod) + mstore(add(0x78, data), hashMul) + mstore(add(0x68, data), amountOutMin) + mstore(add(0x58, data), recipient) + mstore(add(0x44, data), add(0xd1, add(pathLen, sigLen))) + mstore(add(0x24, data), 0x20) + mstore(add(0x04, data), and(0xffffffff, unlockSelector)) + mstore(data, add(0x115, add(pathLen, sigLen))) + + mstore8(add(0xa8, data), feeOnTransfer) + } + } +} + +library Decoder { + using SafeTransferLib for IERC20; + using UnsafeMath for uint256; + using NotesLib for NotesLib.Note; + using NotesLib for NotesLib.Note[]; + using StateLib for StateLib.State; + + uint256 internal constant BASIS = 10_000; + IERC20 internal constant ETH_ADDRESS = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + + /// Update `state` for the next fill packed in `data`. This also may allocate/append `Note`s + /// into `notes`. Returns the suffix of the bytes that are not consumed in the decoding + /// process. The first byte of `data` describes which of the compact representations for the hop + /// is used. + /// + /// 0 -> sell and buy tokens remain unchanged from the previous fill (pure multiplex) + /// 1 -> sell token remains unchanged from the previous fill, buy token is read from `data` (diamond multiplex) + /// 2 -> sell token becomes the buy token from the previous fill, new buy token is read from `data` (multihop) + /// 3 -> both sell and buy token are read from `data` + /// + /// This function is responsible for calling `NotesLib.get(Note[] memory, IERC20, uint256, + /// uint256)` (via `StateLib.setSell` and `StateLib.setBuy`), which maintains the `notes` array + /// and heap. + function updateState(StateLib.State memory state, NotesLib.Note[] memory notes, bytes calldata data) + internal + pure + returns (bytes calldata) + { + bytes32 dataWord; + assembly ("memory-safe") { + dataWord := calldataload(data.offset) + } + uint256 dataConsumed = 1; + + uint256 caseKey = uint256(dataWord) >> 248; + if (caseKey != 0) { + notes.add(state.buy); + + if (caseKey > 1) { + if (state.sell.amount == 0) { + notes.del(state.sell); + } + if (caseKey == 2) { + state.sell = state.buy; + } else { + assert(caseKey == 3); + + IERC20 sellToken = IERC20(address(uint160(uint256(dataWord) >> 88))); + assembly ("memory-safe") { + dataWord := calldataload(add(0x14, data.offset)) + } + unchecked { + dataConsumed += 20; + } + + state.setSell(notes, sellToken); + } + } + + IERC20 buyToken = IERC20(address(uint160(uint256(dataWord) >> 88))); + unchecked { + dataConsumed += 20; + } + + state.setBuy(notes, buyToken); + if (state.buy.eq(state.globalSell)) { + revert BoughtSellToken(state.globalSell.token); + } + } + + assembly ("memory-safe") { + data.offset := add(dataConsumed, data.offset) + data.length := sub(data.length, dataConsumed) + // we don't check for array out-of-bounds here; we will check it later in `_getHookData` + } + + return data; + } + + function overflowCheck(bytes calldata data) internal pure { + assembly ("memory-safe") { + if gt(data.length, 0xffffff) { // length underflow + mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` + mstore(0x20, 0x32) // array out-of-bounds + revert(0x1c, 0x24) + } + } + } + + /// Decode an ABI-ish encoded `bytes` from `data`. It is "-ish" in the sense that the encoding + /// of the length doesn't take up an entire word. The length is encoded as only 3 bytes (2^24 + /// bytes of calldata consumes ~67M gas, much more than the block limit). The payload is also + /// unpadded. The next fill's `bps` is encoded immediately after the `hookData` payload. + function decodeBytes(bytes calldata data) internal pure returns (bytes calldata hookData, bytes calldata retData) { + assembly ("memory-safe") { + hookData.length := shr(0xe8, calldataload(data.offset)) + hookData.offset := add(0x03, data.offset) + let hop := add(0x03, hookData.length) + + retData.offset := add(data.offset, hop) + retData.length := sub(data.length, hop) + } + } + + function decodeHeader(bytes calldata data) + internal + view + returns ( + bytes calldata newData, + // These values are user-supplied + address recipient, + uint256 minBuyAmount, + uint256 hashMul, + uint256 hashMod, + bool feeOnTransfer, + // `payer` is special and is authenticated + address payer + ) + { + // These values are user-supplied + assembly ("memory-safe") { + recipient := shr(0x60, calldataload(data.offset)) + let packed := calldataload(add(0x14, data.offset)) + minBuyAmount := shr(0x80, packed) + hashMul := and(0xffffffffffffffffffffffffffffffff, packed) + packed := calldataload(add(0x34, data.offset)) + hashMod := shr(0x80, packed) + feeOnTransfer := iszero(iszero(and(0x1000000000000000000000000000000, packed))) + + data.offset := add(0x45, data.offset) + data.length := sub(data.length, 0x45) + // we don't check for array out-of-bounds here; we will check it later in `initialize` + } + + // `payer` is special and is authenticated + assembly ("memory-safe") { + payer := shr(0x60, calldataload(data.offset)) + + data.offset := add(0x14, data.offset) + data.length := sub(data.length, 0x14) + // we don't check for array out-of-bounds here; we will check it later in `initialize` + } + + newData = data; + } + + function initialize(bytes calldata data, uint256 hashMul, uint256 hashMod, address payer) + internal + view + returns ( + bytes calldata newData, + StateLib.State memory state, + NotesLib.Note[] memory notes, + ISignatureTransfer.PermitTransferFrom calldata permit, + bool isForwarded, + bytes calldata sig + ) + { + { + IERC20 sellToken; + assembly ("memory-safe") { + sellToken := shr(0x60, calldataload(data.offset)) + } + // We don't advance `data` here because there's a special interaction between `payer` + // (which is the 20 bytes in calldata immediately before `data`), `sellToken`, and + // `permit` that's handled below. + notes = state.construct(sellToken, hashMul, hashMod); + } + + // This assembly block is just here to appease the compiler. We only use `permit` and `sig` + // in the codepaths where they are set away from the values initialized here. + assembly ("memory-safe") { + permit := calldatasize() + sig.offset := calldatasize() + sig.length := 0x00 + } + + if (state.globalSell.token == ETH_ADDRESS) { + assert(payer == address(this)); + + uint16 bps; + assembly ("memory-safe") { + // `data` hasn't been advanced from decoding `sellToken` above. so we have to + // implicitly advance it by 20 bytes to decode `bps` then advance by 22 bytes + + bps := shr(0x50, calldataload(data.offset)) + + data.offset := add(0x16, data.offset) + data.length := sub(data.length, 0x16) + // We check for array out-of-bounds below + } + + unchecked { + state.globalSell.amount = (address(this).balance * bps).unsafeDiv(BASIS); + } + } else { + if (payer == address(this)) { + uint16 bps; + assembly ("memory-safe") { + // `data` hasn't been advanced from decoding `sellToken` above. so we have to + // implicitly advance it by 20 bytes to decode `bps` then advance by 22 bytes + + bps := shr(0x50, calldataload(data.offset)) + + data.offset := add(0x16, data.offset) + data.length := sub(data.length, 0x16) + // We check for array out-of-bounds below + } + + unchecked { + state.globalSell.amount = + (state.globalSell.token.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); + } + } else { + assert(payer == address(0)); + + assembly ("memory-safe") { + // this is super dirty, but it works because although `permit` is aliasing in + // the middle of `payer`, because `payer` is all zeroes, it's treated as padding + // for the first word of `permit`, which is the sell token + permit := sub(data.offset, 0x0c) + isForwarded := and(0x01, calldataload(add(0x55, data.offset))) + + // `sig` is packed at the end of `data`, in "reverse ABI-ish encoded" fashion + sig.offset := sub(add(data.offset, data.length), 0x03) + sig.length := shr(0xe8, calldataload(sig.offset)) + sig.offset := sub(sig.offset, sig.length) + + // Remove `permit` and `isForwarded` from the front of `data` + data.offset := add(0x75, data.offset) + if gt(data.offset, sig.offset) { revert(0x00, 0x00) } + + // Remove `sig` from the back of `data` + data.length := sub(sub(data.length, 0x78), sig.length) + // We check for array out-of-bounds below + } + } + } + + if (data.length > 16777215) { + Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + } + newData = data; + } +} + +library Take { + using UnsafeMath for uint256; + using NotesLib for NotesLib.Note; + using NotesLib for NotesLib.Note[]; + + function _callSelector(uint256 selector, IERC20 token, address to, uint256 amount) internal { + assembly ("memory-safe") { + token := and(0xffffffffffffffffffffffffffffffffffffffff, token) + if iszero(amount) { + mstore(0x00, 0xcbf0dbf5) // selector for `ZeroBuyAmount(address)` + mstore(0x20, token) + revert(0x1c, 0x24) + } + let ptr := mload(0x40) + mstore(ptr, selector) + if eq(token, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee) { token := 0x00 } + mstore(add(0x20, ptr), token) + mstore(add(0x40, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, to)) + mstore(add(0x60, ptr), amount) + if iszero(call(gas(), caller(), 0x00, add(0x1c, ptr), 0x64, 0x00, 0x00)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + } + } + + /// `_take` is responsible for removing the accumulated credit in each token from the vault. The + /// current `state.buy` is the global buy token. We return the settled amount of that token + /// (`buyAmount`), after checking it against the slippage limit (`minBuyAmount`). Each token + /// with credit causes a corresponding call to `msg.sender.(token, recipient, + /// amount)`. + function take( + StateLib.State memory state, + NotesLib.Note[] memory notes, + uint32 selector, + address recipient, + uint256 minBuyAmount + ) internal returns (uint256 buyAmount) { + notes.del(state.buy); + if (state.sell.amount == 0) { + notes.del(state.sell); + } + + uint256 length = notes.length; + // `length` of zero implies that we fully liquidated the global sell token (there is no + // `amount` remaining) and that the only token in which we have credit is the global buy + // token. We're about to `take` that token below. + if (length != 0) { + { + NotesLib.Note memory firstNote = notes[0]; // out-of-bounds is impossible + if (!firstNote.eq(state.globalSell)) { + // The global sell token being in a position other than the 1st would imply that + // at some point we _bought_ that token. This is illegal and results in a revert + // with reason `BoughtSellToken(address)`. + _callSelector(selector, firstNote.token, address(this), firstNote.amount); + } + } + for (uint256 i = 1; i < length; i = i.unsafeInc()) { + (IERC20 token, uint256 amount) = notes.unsafeGet(i); + _callSelector(selector, token, address(this), amount); + } + } + + // The final token to be bought is considered the global buy token. We bypass `notes` and + // read it directly from `state`. Check the slippage limit. Transfer to the recipient. + { + IERC20 buyToken = state.buy.token; + buyAmount = state.buy.amount; + if (buyAmount < minBuyAmount) { + revert TooMuchSlippage(buyToken, minBuyAmount, buyAmount); + } + _callSelector(selector, buyToken, recipient, buyAmount); + } + } +} diff --git a/src/core/MakerPSM.sol b/src/core/MakerPSM.sol index 259a0d94..ab2333e6 100644 --- a/src/core/MakerPSM.sol +++ b/src/core/MakerPSM.sol @@ -58,9 +58,10 @@ abstract contract MakerPSM is SettlerAbstract { returns (uint256 buyAmount) { if (buyGem) { - // phantom overflow can't happen here because DAI has decimals = 18 - uint256 sellAmount = (DAI.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); unchecked { + // phantom overflow can't happen here because DAI has decimals = 18 + uint256 sellAmount = (DAI.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); + uint256 feeDivisor = LitePSM.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 buyAmount = (sellAmount * USDC_basis).unsafeDiv(feeDivisor); @@ -73,7 +74,10 @@ abstract contract MakerPSM is SettlerAbstract { } } else { // phantom overflow can't happen here because PSM prohibits gemToken with decimals > 18 - uint256 sellAmount = (USDC.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); + uint256 sellAmount; + unchecked { + sellAmount = (USDC.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); + } // USDC.safeApproveIfBelow(LitePSM.gemJoin(), sellAmount); buyAmount = LitePSM.sellGem(recipient, sellAmount); if (buyAmount < amountOutMin) { diff --git a/src/core/Permit2Payment.sol b/src/core/Permit2Payment.sol index 0429b7ef..1863759a 100644 --- a/src/core/Permit2Payment.sol +++ b/src/core/Permit2Payment.sol @@ -28,12 +28,12 @@ import {Context} from "../Context.sol"; import {AllowanceHolderContext} from "../allowanceholder/AllowanceHolderContext.sol"; library TransientStorage { - // bytes32(uint256(keccak256("operator slot")) - 1) - bytes32 private constant _OPERATOR_SLOT = 0x009355806b743562f351db2e3726091207f49fa1cdccd5c65a7d4860ce3abbe9; - // bytes32(uint256(keccak256("witness slot")) - 1) - bytes32 private constant _WITNESS_SLOT = 0x1643bf8e9fdaef48c4abf5a998de359be44a235ac7aebfbc05485e093720deaa; - // bytes32(uint256(keccak256("payer slot")) - 1) - bytes32 private constant _PAYER_SLOT = 0x46bacb9b87ba1d2910347e4a3e052d06c824a45acd1e9517bb0cb8d0d5cde893; + // bytes32((uint256(keccak256("operator slot")) - 1) & type(uint128).max) + bytes32 private constant _OPERATOR_SLOT = 0x0000000000000000000000000000000007f49fa1cdccd5c65a7d4860ce3abbe9; + // bytes32((uint256(keccak256("witness slot")) - 1) & type(uint128).max) + bytes32 private constant _WITNESS_SLOT = 0x00000000000000000000000000000000e44a235ac7aebfbc05485e093720deaa; + // bytes32((uint256(keccak256("payer slot")) - 1) & type(uint128).max) + bytes32 private constant _PAYER_SLOT = 0x00000000000000000000000000000000c824a45acd1e9517bb0cb8d0d5cde893; // We assume (and our CI enforces) that internal function pointers cannot be // greater than 2 bytes. On chains not supporting the ViaIR pipeline, not diff --git a/src/core/UniswapV4.sol b/src/core/UniswapV4.sol index 66c093ef..c548cc8d 100644 --- a/src/core/UniswapV4.sol +++ b/src/core/UniswapV4.sol @@ -9,20 +9,12 @@ import {SettlerAbstract} from "../SettlerAbstract.sol"; import {Panic} from "../utils/Panic.sol"; import {UnsafeMath} from "../utils/UnsafeMath.sol"; -import { - TooMuchSlippage, - DeltaNotPositive, - DeltaNotNegative, - ZeroSellAmount, - ZeroBuyAmount, - BoughtSellToken, - TokenHashCollision, - ZeroToken -} from "./SettlerErrors.sol"; +import {TooMuchSlippage, DeltaNotPositive, DeltaNotNegative, ZeroSellAmount} from "./SettlerErrors.sol"; import { BalanceDelta, IHooks, IPoolManager, UnsafePoolManager, POOL_MANAGER, IUnlockCallback } from "./UniswapV4Types.sol"; +import {Encoder, NotesLib, StateLib, Decoder, Take} from "./FlashAccountingCommon.sol"; library CreditDebt { using UnsafeMath for int256; @@ -42,199 +34,6 @@ library CreditDebt { } } -/// This library is a highly-optimized, in-memory, enumerable mapping from tokens to amounts. It -/// consists of 2 components that must be kept synchronized. There is a `memory` array of `Note` -/// (aka `Note[] memory`) that has up to `_MAX_TOKENS` pre-allocated. And there is an implicit heap -/// packed at the end of the array that stores the `Note`s. Each `Note` has a backpointer that knows -/// its location in the `Notes[] memory`. While the length of the `Notes[]` array grows and shrinks -/// as tokens are added and retired, heap objects are only cleared/deallocated when the context of -/// `unlockCallback` returns. Looking up the `Note` object corresponding to a token uses the perfect -/// hash formed by `hashMul` and `hashMod`. Pay special attention to these parameters. See further -/// below in `contract UniswapV4` for recommendations on how to select values for them. A hash -/// collision will result in a revert with signature `TokenHashCollision(address,address)`. -library NotesLib { - uint256 private constant _ADDRESS_MASK = 0x00ffffffffffffffffffffffffffffffffffffffff; - - /// This is the maximum number of tokens that may be involved in a UniV4 action. Increasing or - /// decreasing this value requires no other changes elsewhere in this file. - uint256 private constant _MAX_TOKENS = 8; - - type NotePtr is uint256; - type NotePtrPtr is uint256; - - struct Note { - uint256 amount; - IERC20 token; - NotePtrPtr backPtr; - } - - function construct() internal pure returns (Note[] memory r) { - assembly ("memory-safe") { - r := mload(0x40) - // set the length of `r` to zero - mstore(r, 0x00) - // zeroize the heap - codecopy(add(add(0x20, shl(0x05, _MAX_TOKENS)), r), codesize(), mul(0x60, _MAX_TOKENS)) - // allocate memory - mstore(0x40, add(add(0x20, shl(0x07, _MAX_TOKENS)), r)) - } - } - - function eq(Note memory x, Note memory y) internal pure returns (bool r) { - assembly ("memory-safe") { - r := eq(x, y) - } - } - - function unsafeGet(Note[] memory a, uint256 i) internal pure returns (IERC20 retToken, uint256 retAmount) { - assembly ("memory-safe") { - let x := mload(add(add(0x20, shl(0x05, i)), a)) - retToken := mload(add(0x20, x)) - retAmount := mload(x) - } - } - - function get(Note[] memory a, IERC20 newToken, uint256 hashMul, uint256 hashMod) - internal - pure - returns (NotePtr x) - { - assembly ("memory-safe") { - newToken := and(_ADDRESS_MASK, newToken) - x := add(add(0x20, shl(0x05, _MAX_TOKENS)), a) // `x` now points at the first `Note` on the heap - x := add(mod(mulmod(newToken, hashMul, hashMod), mul(0x60, _MAX_TOKENS)), x) // combine with token hash - // `x` now points at the exact `Note` object we want; let's check it to be sure, though - let x_token_ptr := add(0x20, x) - - // check that we haven't encountered a hash collision. checking for a hash collision is - // equivalent to checking for array out-of-bounds or overflow. - { - let old_token := mload(x_token_ptr) - if mul(or(mload(add(0x40, x)), old_token), xor(old_token, newToken)) { - mstore(0x00, 0x9a62e8b4) // selector for `TokenHashCollision(address,address)` - mstore(0x20, old_token) - mstore(0x40, newToken) - revert(0x1c, 0x44) - } - } - - // zero `newToken` is a footgun; check for it - if iszero(newToken) { - mstore(0x00, 0xad1991f5) // selector for `ZeroToken()` - revert(0x1c, 0x04) - } - - // initialize the token (possibly redundant) - mstore(x_token_ptr, newToken) - } - } - - function add(Note[] memory a, Note memory x) internal pure { - assembly ("memory-safe") { - let backptr_ptr := add(0x40, x) - let backptr := mload(backptr_ptr) - if iszero(backptr) { - let len := add(0x01, mload(a)) - // We don't need to check for overflow or out-of-bounds access here; the checks in - // `get` above for token collision handle that for us. It's not possible to `get` - // more than `_MAX_TOKENS` tokens - mstore(a, len) - backptr := add(shl(0x05, len), a) - mstore(backptr, x) - mstore(backptr_ptr, backptr) - } - } - } - - function del(Note[] memory a, Note memory x) internal pure { - assembly ("memory-safe") { - let x_backptr_ptr := add(0x40, x) - let x_backptr := mload(x_backptr_ptr) - if x_backptr { - // Clear the backpointer in the referred-to `Note` - mstore(x_backptr_ptr, 0x00) - // We do not deallocate `x` - - // Decrement the length of `a` - let len := mload(a) - mstore(a, sub(len, 0x01)) - - // Check if this is a "swap and pop" or just a "pop" - let end_ptr := add(shl(0x05, len), a) - if iszero(eq(end_ptr, x_backptr)) { - // Overwrite the vacated indirection pointer `x_backptr` with the value at the end. - let end := mload(end_ptr) - mstore(x_backptr, end) - - // Fix up the backpointer in `end` to point to the new location of the indirection - // pointer. - let end_backptr_ptr := add(0x40, end) - mstore(end_backptr_ptr, x_backptr) - } - } - } - } -} - -library StateLib { - using NotesLib for NotesLib.Note; - using NotesLib for NotesLib.Note[]; - - struct State { - NotesLib.Note buy; - NotesLib.Note sell; - NotesLib.Note globalSell; - uint256 globalSellAmount; - uint256 _hashMul; - uint256 _hashMod; - } - - function construct(State memory state, IERC20 token, uint256 hashMul, uint256 hashMod) - internal - pure - returns (NotesLib.Note[] memory notes) - { - assembly ("memory-safe") { - // Solc is real dumb and has allocated a bunch of extra memory for us. Thanks solc. - mstore(0x40, add(0xc0, state)) - } - // All the pointers in `state` are now pointing into unallocated memory - notes = NotesLib.construct(); - // The pointers in `state` are now illegally aliasing elements in `notes` - NotesLib.NotePtr notePtr = notes.get(token, hashMul, hashMod); - - // Here we actually set the pointers into a legal area of memory - setBuy(state, notePtr); - setSell(state, notePtr); - assembly ("memory-safe") { - // Set `state.globalSell` - mstore(add(0x40, state), notePtr) - } - state._hashMul = hashMul; - state._hashMod = hashMod; - } - - function setSell(State memory state, NotesLib.NotePtr notePtr) private pure { - assembly ("memory-safe") { - mstore(add(0x20, state), notePtr) - } - } - - function setSell(State memory state, NotesLib.Note[] memory notes, IERC20 token) internal pure { - setSell(state, notes.get(token, state._hashMul, state._hashMod)); - } - - function setBuy(State memory state, NotesLib.NotePtr notePtr) private pure { - assembly ("memory-safe") { - mstore(state, notePtr) - } - } - - function setBuy(State memory state, NotesLib.Note[] memory notes, IERC20 token) internal pure { - setBuy(state, notes.get(token, state._hashMul, state._hashMod)); - } -} - abstract contract UniswapV4 is SettlerAbstract { using SafeTransferLib for IERC20; using UnsafeMath for uint256; @@ -245,6 +44,12 @@ abstract contract UniswapV4 is SettlerAbstract { using NotesLib for NotesLib.Note[]; using StateLib for StateLib.State; + constructor() { + assert(BASIS == Encoder.BASIS); + assert(BASIS == Decoder.BASIS); + assert(ETH_ADDRESS == Decoder.ETH_ADDRESS); + } + //// These two functions are the entrypoints to this set of actions. Because UniV4 has a //// mandatory callback, and the vast majority of the business logic has to be executed inside //// the callback, they're pretty minimal. Both end up inside the last function in this file @@ -285,28 +90,6 @@ abstract contract UniswapV4 is SettlerAbstract { //// //// Repeat the process for each fill and concatenate the results without padding. - //// How to generate a perfect hash for UniV4: - //// - //// The arguments `hashMul` and `hashMod` are required to form a perfect hash for a table with - //// size `_MAX_TOKENS` when applied to all the tokens involved in fills. The hash function is - //// constructed as `uint256 hash = mulmod(uint256(uint160(address(token))), hashMul, hashMod) % - //// _MAX_TOKENS`. - //// - //// The "simple" or "obvious" way to do this is to simply try random 128-bit numbers for both - //// `hashMul` and `hashMod` until you obtain a function that has no collisions when applied to - //// the tokens involved in fills. A substantially more optimized algorithm can be obtained by - //// selecting several (at least 10) prime values for `hashMod`, precomputing the limb moduluses - //// for each value, and then selecting randomly from among them. The author recommends using - //// the 10 largest 64-bit prime numbers: 2^64 - {59, 83, 95, 179, 189, 257, 279, 323, 353, - //// 363}. `hashMul` can then be selected randomly or via some other optimized method. - //// - //// Note that in spite of the fact that the pool manager represents Ether (or the native asset - //// of the chain) as `address(0)`, we represent Ether as `SettlerAbstract.ETH_ADDRESS` (the - //// address of all `e`s) for homogeneity with other parts of the codebase, and because the - //// decision to represent Ether as `address(0)` was stupid in the first place. `address(0)` - //// represents the absence of a thing, not a special case of the thing. It creates confusion - //// with uninitialized memory, storage, and variables. - function sellToUniswapV4( address recipient, IERC20 sellToken, @@ -317,44 +100,17 @@ abstract contract UniswapV4 is SettlerAbstract { bytes memory fills, uint256 amountOutMin ) internal returns (uint256 buyAmount) { - if (amountOutMin > uint128(type(int128).max)) { - Panic.panic(Panic.ARITHMETIC_OVERFLOW); - } - if (bps > BASIS) { - Panic.panic(Panic.ARITHMETIC_OVERFLOW); - } - hashMul *= 96; - hashMod *= 96; - if (hashMul > type(uint128).max) { - Panic.panic(Panic.ARITHMETIC_OVERFLOW); - } - if (hashMod > type(uint128).max) { - Panic.panic(Panic.ARITHMETIC_OVERFLOW); - } - bytes memory data; - assembly ("memory-safe") { - data := mload(0x40) - - let pathLen := mload(fills) - mcopy(add(0xd3, data), add(0x20, fills), pathLen) - - mstore(add(0xb3, data), bps) - mstore(add(0xb1, data), sellToken) - mstore(add(0x9d, data), address()) // payer - // feeOnTransfer (1 byte) - - mstore(add(0x88, data), hashMod) - mstore(add(0x78, data), hashMul) - mstore(add(0x68, data), amountOutMin) - mstore(add(0x58, data), recipient) - mstore(add(0x44, data), add(0x6f, pathLen)) - mstore(add(0x24, data), 0x20) - mstore(add(0x04, data), 0x48c89491) // selector for `unlock(bytes)` - mstore(data, add(0xb3, pathLen)) - mstore8(add(0xa8, data), feeOnTransfer) - - mstore(0x40, add(data, add(0xd3, pathLen))) - } + bytes memory data = Encoder.encode( + uint32(IPoolManager.unlock.selector), + recipient, + sellToken, + bps, + feeOnTransfer, + hashMul, + hashMod, + fills, + amountOutMin + ); bytes memory encodedBuyAmount = _setOperatorAndCall( address(POOL_MANAGER), data, uint32(IUnlockCallback.unlockCallback.selector), _uniV4Callback ); @@ -377,59 +133,18 @@ abstract contract UniswapV4 is SettlerAbstract { bytes memory sig, uint256 amountOutMin ) internal returns (uint256 buyAmount) { - if (amountOutMin > uint128(type(int128).max)) { - Panic.panic(Panic.ARITHMETIC_OVERFLOW); - } - hashMul *= 96; - hashMod *= 96; - if (hashMul > type(uint128).max) { - Panic.panic(Panic.ARITHMETIC_OVERFLOW); - } - if (hashMod > type(uint128).max) { - Panic.panic(Panic.ARITHMETIC_OVERFLOW); - } - bool isForwarded = _isForwarded(); - bytes memory data; - assembly ("memory-safe") { - data := mload(0x40) - - let pathLen := mload(fills) - let sigLen := mload(sig) - - { - let ptr := add(0x132, data) - - // sig length as 3 bytes goes at the end of the callback - mstore(sub(add(sigLen, add(pathLen, ptr)), 0x1d), sigLen) - - // fills go at the end of the header - mcopy(ptr, add(0x20, fills), pathLen) - ptr := add(pathLen, ptr) - - // signature comes after the fills - mcopy(ptr, add(0x20, sig), sigLen) - ptr := add(sigLen, ptr) - - mstore(0x40, add(0x03, ptr)) - } - - mstore8(add(0x131, data), isForwarded) - mcopy(add(0xf1, data), add(0x20, permit), 0x40) - mcopy(add(0xb1, data), mload(permit), 0x40) // aliases `payer` on purpose - mstore(add(0x9d, data), 0x00) // payer - // feeOnTransfer (1 byte) - - mstore(add(0x88, data), hashMod) - mstore(add(0x78, data), hashMul) - mstore(add(0x68, data), amountOutMin) - mstore(add(0x58, data), recipient) - mstore(add(0x44, data), add(0xd1, add(pathLen, sigLen))) - mstore(add(0x24, data), 0x20) - mstore(add(0x04, data), 0x48c89491) // selector for `unlock(bytes)` - mstore(data, add(0x115, add(pathLen, sigLen))) - - mstore8(add(0xa8, data), feeOnTransfer) - } + bytes memory data = Encoder.encodeVIP( + uint32(IPoolManager.unlock.selector), + recipient, + feeOnTransfer, + hashMul, + hashMod, + fills, + permit, + sig, + _isForwarded(), + amountOutMin + ); bytes memory encodedBuyAmount = _setOperatorAndCall( address(POOL_MANAGER), data, uint32(IUnlockCallback.unlockCallback.selector), _uniV4Callback ); @@ -482,75 +197,6 @@ abstract contract UniswapV4 is SettlerAbstract { // 3 - hook data length uint256 private constant _HOP_DATA_LENGTH = 32; - /// Update `state` for the next fill packed in `data`. This also may allocate/append `Note`s - /// into `notes`. Returns the suffix of the bytes that are not consumed in the decoding - /// process. The first byte of `data` describes which of the compact representations for the hop - /// is used. - /// - /// 0 -> sell and buy tokens remain unchanged from the previous fill (pure multiplex) - /// 1 -> sell token remains unchanged from the previous fill, buy token is read from `data` (diamond multiplex) - /// 2 -> sell token becomes the buy token from the previous fill, new buy token is read from `data` (multihop) - /// 3 -> both sell and buy token are read from `data` - /// - /// This function is responsible for calling `NotesLib.get(Note[] memory, IERC20, uint256, - /// uint256)` (via `StateLib.setSell` and `StateLib.setBuy`), which maintains the `notes` array - /// and heap. - function _updateState(StateLib.State memory state, NotesLib.Note[] memory notes, bytes calldata data) - private - pure - returns (bytes calldata) - { - bytes32 dataWord; - assembly ("memory-safe") { - dataWord := calldataload(data.offset) - } - uint256 dataConsumed = 1; - - uint256 caseKey = uint256(dataWord) >> 248; - if (caseKey != 0) { - notes.add(state.buy); - - if (caseKey > 1) { - if (state.sell.amount == 0) { - notes.del(state.sell); - } - if (caseKey == 2) { - state.sell = state.buy; - } else { - assert(caseKey == 3); - - IERC20 sellToken = IERC20(address(uint160(uint256(dataWord) >> 88))); - assembly ("memory-safe") { - dataWord := calldataload(add(0x14, data.offset)) - } - unchecked { - dataConsumed += 20; - } - - state.setSell(notes, sellToken); - } - } - - IERC20 buyToken = IERC20(address(uint160(uint256(dataWord) >> 88))); - unchecked { - dataConsumed += 20; - } - - state.setBuy(notes, buyToken); - if (state.buy.eq(state.globalSell)) { - revert BoughtSellToken(state.globalSell.token); - } - } - - assembly ("memory-safe") { - data.offset := add(dataConsumed, data.offset) - data.length := sub(data.length, dataConsumed) - // we don't check for array out-of-bounds here; we will check it later in `_getHookData` - } - - return data; - } - uint256 private constant _ADDRESS_MASK = 0x00ffffffffffffffffffffffffffffffffffffffff; /// Decode a `PoolKey` from its packed representation in `bytes` and the token information in @@ -580,7 +226,7 @@ abstract contract UniswapV4 is SettlerAbstract { data.offset := add(0x1a, data.offset) data.length := sub(data.length, 0x1a) - // we don't check for array out-of-bounds here; we will check it later in `_getHookData` + // we don't check for array out-of-bounds here; we will check it later in `Decoder.overflowCheck` } key.fee = uint24(packed >> 184); @@ -590,71 +236,6 @@ abstract contract UniswapV4 is SettlerAbstract { return (zeroForOne, data); } - /// Decode an ABI-ish encoded `bytes` from `data`. It is "-ish" in the sense that the encoding - /// of the length doesn't take up an entire word. The length is encoded as only 3 bytes (2^24 - /// bytes of calldata consumes ~67M gas, much more than the block limit). The payload is also - /// unpadded. The next fill's `bps` is encoded immediately after the `hookData` payload. - function _getHookData(bytes calldata data) private pure returns (bytes calldata hookData, bytes calldata retData) { - assembly ("memory-safe") { - hookData.length := shr(0xe8, calldataload(data.offset)) - hookData.offset := add(0x03, data.offset) - let hop := add(0x03, hookData.length) - - retData.offset := add(data.offset, hop) - retData.length := sub(data.length, hop) - if gt(retData.length, 0xffffff) { // length underflow - mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` - mstore(0x20, 0x32) // array out-of-bounds - revert(0x1c, 0x24) - } - } - } - - /// `_take` is responsible for removing the accumulated credit in each token from the pool - /// manager. The current `state.buy` is the global buy token. We return the settled amount of - /// that token (`buyAmount`), after checking it against the slippage limit - /// (`minBuyAmount`). Each token with credit causes a corresponding call to `POOL_MANAGER.take`. - function _take(StateLib.State memory state, NotesLib.Note[] memory notes, address recipient, uint256 minBuyAmount) - private - returns (uint256 buyAmount) - { - notes.del(state.buy); - if (state.sell.amount == 0) { - notes.del(state.sell); - } - - uint256 length = notes.length; - // `length` of zero implies that we fully liquidated the global sell token (there is no - // `amount` remaining) and that the only token in which we have credit is the global buy - // token. We're about to `take` that token below. - if (length != 0) { - { - NotesLib.Note memory firstNote = notes[0]; // out-of-bounds is impossible - if (!firstNote.eq(state.globalSell)) { - // The global sell token being in a position other than the 1st would imply that - // at some point we _bought_ that token. This is illegal and results in a revert - // with reason `BoughtSellToken(address)`. - IPoolManager(msg.sender).unsafeTake(firstNote.token, address(this), firstNote.amount); - } - } - for (uint256 i = 1; i < length; i = i.unsafeInc()) { - (IERC20 token, uint256 amount) = notes.unsafeGet(i); - IPoolManager(msg.sender).unsafeTake(token, address(this), amount); - } - } - - // The final token to be bought is considered the global buy token. We bypass `notes` and - // read it directly from `state`. Check the slippage limit. Transfer to the recipient. - { - IERC20 buyToken = state.buy.token; - buyAmount = state.buy.amount; - if (buyAmount < minBuyAmount) { - revert TooMuchSlippage(buyToken, minBuyAmount, buyAmount); - } - IPoolManager(msg.sender).unsafeTake(buyToken, recipient, buyAmount); - } - } - function _pay( IERC20 sellToken, address payer, @@ -675,145 +256,14 @@ abstract contract UniswapV4 is SettlerAbstract { return IPoolManager(msg.sender).unsafeSettle(); } - function _initialize(bytes calldata data, bool feeOnTransfer, uint256 hashMul, uint256 hashMod, address payer) - private - returns ( - bytes calldata newData, - StateLib.State memory state, - NotesLib.Note[] memory notes, - ISignatureTransfer.PermitTransferFrom calldata permit, - bool isForwarded, - bytes calldata sig - ) - { - { - IERC20 sellToken; - assembly ("memory-safe") { - sellToken := shr(0x60, calldataload(data.offset)) - } - // We don't advance `data` here because there's a special interaction between `payer` - // (which is the 20 bytes in calldata immediately before `data`), `sellToken`, and - // `permit` that's handled below. - notes = state.construct(sellToken, hashMul, hashMod); - } - - // This assembly block is just here to appease the compiler. We only use `permit` and `sig` - // in the codepaths where they are set away from the values initialized here. - assembly ("memory-safe") { - permit := calldatasize() - sig.offset := calldatasize() - sig.length := 0x00 - } - - if (state.globalSell.token == ETH_ADDRESS) { - assert(payer == address(this)); - - uint16 bps; - assembly ("memory-safe") { - // `data` hasn't been advanced from decoding `sellToken` above. so we have to - // implicitly advance it by 20 bytes to decode `bps` then advance by 22 bytes - - bps := shr(0x50, calldataload(data.offset)) - - data.offset := add(0x16, data.offset) - data.length := sub(data.length, 0x16) - // We check for array out-of-bounds below - } - - unchecked { - state.globalSell.amount = (address(this).balance * bps).unsafeDiv(BASIS); - } - } else { - if (payer == address(this)) { - uint16 bps; - assembly ("memory-safe") { - // `data` hasn't been advanced from decoding `sellToken` above. so we have to - // implicitly advance it by 20 bytes to decode `bps` then advance by 22 bytes - - bps := shr(0x50, calldataload(data.offset)) - - data.offset := add(0x16, data.offset) - data.length := sub(data.length, 0x16) - // We check for array out-of-bounds below - } - - unchecked { - state.globalSell.amount = - (state.globalSell.token.fastBalanceOf(address(this)) * bps).unsafeDiv(BASIS); - } - } else { - assert(payer == address(0)); - - assembly ("memory-safe") { - // this is super dirty, but it works because although `permit` is aliasing in - // the middle of `payer`, because `payer` is all zeroes, it's treated as padding - // for the first word of `permit`, which is the sell token - permit := sub(data.offset, 0x0c) - isForwarded := and(0x01, calldataload(add(0x55, data.offset))) - - // `sig` is packed at the end of `data`, in "reverse ABI-ish encoded" fashion - sig.offset := sub(add(data.offset, data.length), 0x03) - sig.length := shr(0xe8, calldataload(sig.offset)) - sig.offset := sub(sig.offset, sig.length) - - // Remove `permit` and `isForwarded` from the front of `data` - data.offset := add(0x75, data.offset) - if gt(data.offset, sig.offset) { revert(0x00, 0x00) } - - // Remove `sig` from the back of `data` - data.length := sub(sub(data.length, 0x78), sig.length) - // We check for array out-of-bounds below - } - - state.globalSell.amount = _permitToSellAmountCalldata(permit); - } - - if (feeOnTransfer) { - state.globalSell.amount = - _pay(state.globalSell.token, payer, state.globalSell.amount, permit, isForwarded, sig); - } - } - - if (data.length > 16777215) { - Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); - } - if (state.globalSell.amount == 0) { - revert ZeroSellAmount(state.globalSell.token); - } - state.globalSellAmount = state.globalSell.amount; - newData = data; - } - function unlockCallback(bytes calldata data) private returns (bytes memory) { - // These values are user-supplied address recipient; uint256 minBuyAmount; uint256 hashMul; uint256 hashMod; bool feeOnTransfer; - assembly ("memory-safe") { - recipient := shr(0x60, calldataload(data.offset)) - let packed := calldataload(add(0x14, data.offset)) - minBuyAmount := shr(0x80, packed) - hashMul := and(0xffffffffffffffffffffffffffffffff, packed) - packed := calldataload(add(0x34, data.offset)) - hashMod := shr(0x80, packed) - feeOnTransfer := iszero(iszero(and(0x1000000000000000000000000000000, packed))) - - data.offset := add(0x45, data.offset) - data.length := sub(data.length, 0x45) - // we don't check for array out-of-bounds here; we will check it later in `_initialize` - } - - // `payer` is special and is authenticated address payer; - assembly ("memory-safe") { - payer := shr(0x60, calldataload(data.offset)) - - data.offset := add(0x14, data.offset) - data.length := sub(data.length, 0x14) - // we don't check for array out-of-bounds here; we will check it later in `_initialize` - } + (data, recipient, minBuyAmount, hashMul, hashMod, feeOnTransfer, payer) = Decoder.decodeHeader(data); // Set up `state` and `notes`. The other values are ancillary and might be used when we need // to settle global sell token debt at the end of swapping. @@ -824,7 +274,18 @@ abstract contract UniswapV4 is SettlerAbstract { ISignatureTransfer.PermitTransferFrom calldata permit, bool isForwarded, bytes calldata sig - ) = _initialize(data, feeOnTransfer, hashMul, hashMod, payer); + ) = Decoder.initialize(data, hashMul, hashMod, payer); + if (payer != address(this)) { + state.globalSell.amount = _permitToSellAmountCalldata(permit); + } + if (feeOnTransfer) { + state.globalSell.amount = + _pay(state.globalSell.token, payer, state.globalSell.amount, permit, isForwarded, sig); + } + if (state.globalSell.amount == 0) { + revert ZeroSellAmount(state.globalSell.token); + } + state.globalSellAmount = state.globalSell.amount; data = newData; // Now that we've unpacked and decoded the header, we can begin decoding the array of swaps @@ -838,14 +299,15 @@ abstract contract UniswapV4 is SettlerAbstract { data.offset := add(0x02, data.offset) data.length := sub(data.length, 0x02) - // we don't check for array out-of-bounds here; we will check it later in `_getHookData` + // we don't check for array out-of-bounds here; we will check it later in `Decoder.overflowCheck` } - data = _updateState(state, notes, data); + data = Decoder.updateState(state, notes, data); bool zeroForOne; (zeroForOne, data) = _setPoolKey(key, state, data); bytes calldata hookData; - (hookData, data) = _getHookData(data); + (hookData, data) = Decoder.decodeBytes(data); + Decoder.overflowCheck(data); params.zeroForOne = zeroForOne; unchecked { @@ -876,14 +338,18 @@ abstract contract UniswapV4 is SettlerAbstract { // Settler. `state.buy.token` will be sent to `recipient`. { (IERC20 globalSellToken, uint256 globalSellAmount) = (state.globalSell.token, state.globalSell.amount); - uint256 globalBuyAmount = _take(state, notes, recipient, minBuyAmount); + uint256 globalBuyAmount = + Take.take(state, notes, uint32(IPoolManager.take.selector), recipient, minBuyAmount); if (feeOnTransfer) { // We've already transferred the sell token to the pool manager and // `settle`'d. `globalSellAmount` is the verbatim credit in that token stored by the // pool manager. We only need to handle the case of incomplete filling. if (globalSellAmount != 0) { - IPoolManager(msg.sender).unsafeTake( - globalSellToken, payer == address(this) ? address(this) : _msgSender(), globalSellAmount + Take._callSelector( + uint32(IPoolManager.take.selector), + globalSellToken, + payer == address(this) ? address(this) : _msgSender(), + globalSellAmount ); } } else { diff --git a/src/core/UniswapV4Types.sol b/src/core/UniswapV4Types.sol index e3df26c6..d2c61d1d 100644 --- a/src/core/UniswapV4Types.sol +++ b/src/core/UniswapV4Types.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.25; import {IERC20} from "@forge-std/interfaces/IERC20.sol"; +import {Take} from "./FlashAccountingCommon.sol"; type IHooks is address; @@ -91,10 +92,9 @@ interface IPoolManager { /// Solc emits code that is both gas inefficient and codesize bloated. By reimplementing these /// function calls in Yul, we obtain significant improvements. Solc also emits an EXTCODESIZE check -/// when an external function doesn't return anything (`sync` and `take`). Obviously, we know that -/// POOL_MANAGER has code, so this omits those checks. Also, for compatibility, these functions -/// identify `SettlerAbstract.ETH_ADDRESS` (the address of all `e`s) and replace it with -/// `address(0)`. +/// when an external function doesn't return anything (`sync`). Obviously, we know that POOL_MANAGER +/// has code, so this omits those checks. Also, for compatibility, these functions identify +/// `SettlerAbstract.ETH_ADDRESS` (the address of all `e`s) and replace it with `address(0)`. library UnsafePoolManager { function unsafeSync(IPoolManager poolManager, IERC20 token) internal { // `sync` doesn't need to check whether `token` is `ETH_ADDRESS` because calling `sync` for @@ -110,27 +110,6 @@ library UnsafePoolManager { } } - function unsafeTake(IPoolManager poolManager, IERC20 token, address to, uint256 amount) internal { - assembly ("memory-safe") { - token := and(0xffffffffffffffffffffffffffffffffffffffff, token) - if iszero(amount) { - mstore(0x00, 0xcbf0dbf5) // selector for `ZeroBuyAmount(address)` - mstore(0x20, token) - revert(0x1c, 0x24) - } - let ptr := mload(0x40) - mstore(ptr, 0x0b0d9c09) // selector for `take(address,address,uint256)` - if eq(token, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee) { token := 0x00 } - mstore(add(0x20, ptr), token) - mstore(add(0x40, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, to)) - mstore(add(0x60, ptr), amount) - if iszero(call(gas(), poolManager, 0x00, add(0x1c, ptr), 0x64, 0x00, 0x00)) { - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) - } - } - } - function unsafeSwap( IPoolManager poolManager, IPoolManager.PoolKey memory key, diff --git a/src/core/Velodrome.sol b/src/core/Velodrome.sol index 514f5098..6ecde0b6 100644 --- a/src/core/Velodrome.sol +++ b/src/core/Velodrome.sol @@ -262,7 +262,9 @@ abstract contract Velodrome is SettlerAbstract { // Convert `buyAmount` from `_VELODROME_TOKEN_BASIS` to native units buyAmount = buyAmount * buyBasis / _VELODROME_TOKEN_BASIS; } + // Compensate for rounding error in the pair's calculation of the constant function buyAmount--; + if (buyAmount < minAmountOut) { revert TooMuchSlippage(sellToken, minAmountOut, buyAmount); } diff --git a/src/utils/UnsafeMath.sol b/src/utils/UnsafeMath.sol index 463af995..3aca0f40 100644 --- a/src/utils/UnsafeMath.sol +++ b/src/utils/UnsafeMath.sol @@ -67,4 +67,10 @@ library UnsafeMath { r := addmod(a, b, m) } } + + function unsafeDivUp(uint256 n, uint256 d) internal pure returns (uint256 r) { + assembly ("memory-safe") { + r := add(gt(mod(n, d), 0x00), div(n, d)) + } + } }